From 78383f98f49b0e9f8ee9d072a4712b9b307ad586 Mon Sep 17 00:00:00 2001 From: Mike Hearn Date: Fri, 11 Jul 2014 00:26:37 +0200 Subject: [PATCH] WalletAppKit: support for restoring a wallet from a seed. The old wallet is moved out of the way. --- .../com/google/bitcoin/kits/WalletAppKit.java | 90 ++++++++++++++++--- 1 file changed, 77 insertions(+), 13 deletions(-) diff --git a/core/src/main/java/com/google/bitcoin/kits/WalletAppKit.java b/core/src/main/java/com/google/bitcoin/kits/WalletAppKit.java index 084ea841..ef624169 100644 --- a/core/src/main/java/com/google/bitcoin/kits/WalletAppKit.java +++ b/core/src/main/java/com/google/bitcoin/kits/WalletAppKit.java @@ -22,6 +22,7 @@ import com.google.bitcoin.net.discovery.DnsDiscovery; import com.google.bitcoin.store.BlockStoreException; import com.google.bitcoin.store.SPVBlockStore; import com.google.bitcoin.store.WalletProtobufSerializer; +import com.google.bitcoin.wallet.DeterministicSeed; import com.google.bitcoin.wallet.KeyChainGroup; import com.google.common.collect.ImmutableList; import com.google.common.util.concurrent.AbstractIdleService; @@ -29,7 +30,10 @@ import com.google.common.util.concurrent.MoreExecutors; import com.google.common.util.concurrent.Service; import com.subgraph.orchid.TorClient; import org.bitcoinj.wallet.Protos; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import javax.annotation.Nullable; import java.io.*; import java.net.InetAddress; import java.net.UnknownHostException; @@ -64,6 +68,8 @@ import static com.google.common.base.Preconditions.checkState; * out what went wrong more precisely. Same thing if you just use the {@link #startAsync()} method.

*/ public class WalletAppKit extends AbstractIdleService { + protected static final Logger log = LoggerFactory.getLogger(WalletAppKit.class); + protected final String filePrefix; protected final NetworkParameters params; protected volatile BlockChain vChain; @@ -83,6 +89,7 @@ public class WalletAppKit extends AbstractIdleService { protected boolean useTor = false; // Perhaps in future we can change this to true. protected String userAgent, version; protected WalletProtobufSerializer.WalletFactory walletFactory; + @Nullable protected DeterministicSeed restoreFromSeed; public WalletAppKit(NetworkParameters params, File directory, String filePrefix) { this.params = checkNotNull(params); @@ -170,6 +177,19 @@ public class WalletAppKit extends AbstractIdleService { return this; } + /** + * If a seed is set here then any existing wallet that matches the file name will be renamed to a backup name, + * the chain file will be deleted, and the wallet object will be instantiated with the given seed instead of + * a fresh one being created. This is intended for restoring a wallet from the original seed. To implement restore + * you would shut down the existing appkit, if any, then recreate it with the seed given by the user, then start + * up the new kit. The next time your app starts it should work as normal (that is, don't keep calling this each + * time). + */ + public WalletAppKit restoreWalletFromSeed(DeterministicSeed seed) { + this.restoreFromSeed = seed; + return this; + } + /** *

Override this to return wallet extensions if any are necessary.

* @@ -217,24 +237,36 @@ public class WalletAppKit extends AbstractIdleService { throw new IOException("Could not create named directory."); } } + log.info("Starting up with directory = {}", directory); try { File chainFile = new File(directory, filePrefix + ".spvchain"); boolean chainFileExists = chainFile.exists(); vWalletFile = new File(directory, filePrefix + ".wallet"); - boolean shouldReplayWallet = vWalletFile.exists() && !chainFileExists; - + boolean shouldReplayWallet = (vWalletFile.exists() && !chainFileExists) || restoreFromSeed != null; vStore = new SPVBlockStore(params, chainFile); - if (!chainFileExists && checkpoints != null) { - // Ugly hack! We have to create the wallet once here to learn the earliest key time, and then throw it - // away. The reason is that wallet extensions might need access to peergroups/chains/etc so we have to - // create the wallet later, but we need to know the time early here before we create the BlockChain - // object. + if ((!chainFileExists || restoreFromSeed != null) && checkpoints != null) { + // Initialize the chain file with a checkpoint to speed up first-run sync. long time = Long.MAX_VALUE; - if (vWalletFile.exists()) { - FileInputStream stream = new FileInputStream(vWalletFile); - final WalletProtobufSerializer serializer = new WalletProtobufSerializer(); - final Wallet wallet = serializer.readWallet(params, null, WalletProtobufSerializer.parseToProto(stream)); - time = wallet.getEarliestKeyCreationTime(); + if (restoreFromSeed != null) { + time = restoreFromSeed.getCreationTimeSeconds(); + if (chainFileExists) { + log.info("Deleting the chain file in preparation from restore."); + vStore.close(); + if (!chainFile.delete()) + throw new Exception("Failed to delete chain file in preparation for restore."); + vStore = new SPVBlockStore(params, chainFile); + } + } else { + // Ugly hack! We have to create the wallet once here to learn the earliest key time, and then throw it + // away. The reason is that wallet extensions might need access to peergroups/chains/etc so we have to + // create the wallet later, but we need to know the time early here before we create the BlockChain + // object. + if (vWalletFile.exists()) { + FileInputStream stream = new FileInputStream(vWalletFile); + final WalletProtobufSerializer serializer = new WalletProtobufSerializer(); + final Wallet wallet = serializer.readWallet(params, null, WalletProtobufSerializer.parseToProto(stream)); + time = wallet.getEarliestKeyCreationTime(); + } } CheckpointManager.checkpoint(params, checkpoints, vStore, time); } @@ -242,6 +274,9 @@ public class WalletAppKit extends AbstractIdleService { vPeerGroup = createPeerGroup(); if (this.userAgent != null) vPeerGroup.setUserAgent(userAgent, version); + + maybeMoveOldWalletOutOfTheWay(); + if (vWalletFile.exists()) { FileInputStream walletStream = new FileInputStream(vWalletFile); try { @@ -261,7 +296,7 @@ public class WalletAppKit extends AbstractIdleService { walletStream.close(); } } else { - vWallet = walletFactory != null ? walletFactory.create(params, new KeyChainGroup(params)) : new Wallet(params); + vWallet = createWallet(); vWallet.freshReceiveKey(); for (WalletExtension e : provideWalletExtensions()) { vWallet.addExtension(e); @@ -312,6 +347,35 @@ public class WalletAppKit extends AbstractIdleService { } } + protected Wallet createWallet() { + KeyChainGroup kcg; + if (restoreFromSeed != null) + kcg = new KeyChainGroup(params, restoreFromSeed); + else + kcg = new KeyChainGroup(params); + if (walletFactory != null) { + return walletFactory.create(params, kcg); + } else { + return new Wallet(params, kcg); // default + } + } + + private void maybeMoveOldWalletOutOfTheWay() { + if (restoreFromSeed == null) return; + if (!vWalletFile.exists()) return; + int counter = 1; + File newName; + do { + newName = new File(vWalletFile.getParent(), "Backup " + counter + " for " + vWalletFile.getName()); + counter++; + } while (newName.exists()); + log.info("Renaming old wallet file {} to {}", vWalletFile, newName); + if (!vWalletFile.renameTo(newName)) { + // This should not happen unless something is really messed up. + throw new RuntimeException("Failed to rename wallet for restore"); + } + } + protected PeerGroup createPeerGroup() throws TimeoutException { if (useTor) { return PeerGroup.newWithTor(params, vChain, new TorClient());