From d400573e9b590944db783e7c4c0bcc39d863e599 Mon Sep 17 00:00:00 2001 From: Manfred Karrer Date: Tue, 1 Nov 2016 18:34:31 +0100 Subject: [PATCH] Add code for WalletAppKit to WalletAppKitBitSquare --- .../io/bitsquare/btc/TradeWalletService.java | 5 +- .../bitsquare/btc/WalletAppKitBitSquare.java | 548 +++++++++++++++++- 2 files changed, 541 insertions(+), 12 deletions(-) diff --git a/core/src/main/java/io/bitsquare/btc/TradeWalletService.java b/core/src/main/java/io/bitsquare/btc/TradeWalletService.java index 0381de4e30..62c39f9323 100644 --- a/core/src/main/java/io/bitsquare/btc/TradeWalletService.java +++ b/core/src/main/java/io/bitsquare/btc/TradeWalletService.java @@ -33,7 +33,6 @@ import io.bitsquare.user.Preferences; import org.bitcoinj.core.*; import org.bitcoinj.crypto.DeterministicKey; import org.bitcoinj.crypto.TransactionSignature; -import org.bitcoinj.kits.WalletAppKit; import org.bitcoinj.script.Script; import org.bitcoinj.script.ScriptBuilder; import org.jetbrains.annotations.NotNull; @@ -98,7 +97,7 @@ public class TradeWalletService { @Nullable private Wallet wallet; @Nullable - private WalletAppKit walletAppKit; + private WalletAppKitBitSquare walletAppKit; @Nullable private KeyParameter aesKey; private AddressEntryList addressEntryList; @@ -114,7 +113,7 @@ public class TradeWalletService { } // After WalletService is initialized we get the walletAppKit set - public void setWalletAppKit(WalletAppKit walletAppKit) { + public void setWalletAppKit(WalletAppKitBitSquare walletAppKit) { this.walletAppKit = walletAppKit; wallet = walletAppKit.wallet(); } diff --git a/core/src/main/java/io/bitsquare/btc/WalletAppKitBitSquare.java b/core/src/main/java/io/bitsquare/btc/WalletAppKitBitSquare.java index cb1a7a9564..fe78366f26 100644 --- a/core/src/main/java/io/bitsquare/btc/WalletAppKitBitSquare.java +++ b/core/src/main/java/io/bitsquare/btc/WalletAppKitBitSquare.java @@ -17,37 +17,507 @@ package io.bitsquare.btc; +import com.google.common.collect.ImmutableList; +import com.google.common.util.concurrent.AbstractIdleService; +import com.google.common.util.concurrent.FutureCallback; +import com.google.common.util.concurrent.Futures; import com.runjva.sourceforge.jsocks.protocol.Socks5Proxy; -import org.bitcoinj.core.NetworkParameters; -import org.bitcoinj.core.PeerGroup; -import org.bitcoinj.kits.WalletAppKit; +import com.subgraph.orchid.TorClient; +import org.bitcoinj.core.*; import org.bitcoinj.net.BlockingClientManager; +import org.bitcoinj.net.discovery.DnsDiscovery; +import org.bitcoinj.net.discovery.PeerDiscovery; +import org.bitcoinj.params.RegTestParams; +import org.bitcoinj.protocols.channels.StoredPaymentChannelClientStates; +import org.bitcoinj.protocols.channels.StoredPaymentChannelServerStates; +import org.bitcoinj.store.BlockStore; +import org.bitcoinj.store.BlockStoreException; +import org.bitcoinj.store.SPVBlockStore; +import org.bitcoinj.store.WalletProtobufSerializer; +import org.bitcoinj.wallet.DeterministicSeed; +import org.bitcoinj.wallet.KeyChainGroup; +import org.bitcoinj.wallet.Protos; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; -import java.io.File; +import javax.annotation.Nullable; +import java.io.*; +import java.net.InetAddress; import java.net.InetSocketAddress; import java.net.Proxy; +import java.net.UnknownHostException; +import java.nio.channels.FileLock; +import java.util.List; +import java.util.concurrent.TimeUnit; import java.util.concurrent.TimeoutException; -public class WalletAppKitBitSquare extends WalletAppKit { +import static com.google.common.base.Preconditions.checkNotNull; +import static com.google.common.base.Preconditions.checkState; + + +/** + * Support multiple wallets and usage of Tor proxy. + *

+ * Based on code from org.bitcoinj.kits.WalletAppKit but cannot subclass it as there are too many private access methods + */ +public class WalletAppKitBitSquare extends AbstractIdleService { + protected static final Logger log = LoggerFactory.getLogger(WalletAppKitBitSquare.class); + + protected final String filePrefix; + protected final NetworkParameters params; + protected volatile BlockChain vChain; + protected volatile BlockStore vStore; + protected volatile Wallet vWallet; + protected volatile PeerGroup vPeerGroup; + + protected final File directory; + protected volatile File vWalletFile; + + protected boolean useAutoSave = true; + protected PeerAddress[] peerAddresses; + protected PeerEventListener downloadListener; + protected boolean autoStop = true; + protected InputStream checkpoints; + protected boolean blockingStartup = true; + 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; + @Nullable + protected PeerDiscovery discovery; + + protected volatile Context context; + private long bloomFilterTweak = 0; + private double bloomFilterFPRate = -1; + private int lookaheadSize = -1; + private Socks5Proxy socks5Proxy; /** - * Creates a new WalletAppKit, with a newly created {@link Context}. Files will be stored in the given directory. + * Creates a new WalletAppKitBitSquare, with a newly created {@link Context}. Files will be stored in the given directory. */ public WalletAppKitBitSquare(NetworkParameters params, Socks5Proxy socks5Proxy, File directory, String filePrefix) { - super(params, directory, filePrefix); + this(new Context(params), directory, filePrefix); + //super(params, directory, filePrefix); this.socks5Proxy = socks5Proxy; } + public Socks5Proxy getProxy() { return socks5Proxy; } - protected PeerGroup createPeerGroup() throws TimeoutException { + /** + * Creates a new WalletAppKitBitSquare, with a newly created {@link Context}. Files will be stored in the given directory. + */ + public WalletAppKitBitSquare(NetworkParameters params, File directory, String filePrefix) { + this(new Context(params), directory, filePrefix); + } + + /** + * Creates a new WalletAppKitBitSquare, with the given {@link Context}. Files will be stored in the given directory. + */ + public WalletAppKitBitSquare(Context context, File directory, String filePrefix) { + this.context = context; + this.params = checkNotNull(context.getParams()); + this.directory = checkNotNull(directory); + this.filePrefix = checkNotNull(filePrefix); + if (!Utils.isAndroidRuntime()) { + InputStream stream = WalletAppKitBitSquare.class.getResourceAsStream("/" + params.getId() + ".checkpoints"); + if (stream != null) + setCheckpoints(stream); + } + } + + /** + * Will only connect to the given addresses. Cannot be called after startup. + */ + public WalletAppKitBitSquare setPeerNodes(PeerAddress... addresses) { + checkState(state() == State.NEW, "Cannot call after startup"); + this.peerAddresses = addresses; + return this; + } + + /** + * Will only connect to localhost. Cannot be called after startup. + */ + public WalletAppKitBitSquare connectToLocalHost() { + try { + final InetAddress localHost = InetAddress.getLocalHost(); + return setPeerNodes(new PeerAddress(localHost, params.getPort())); + } catch (UnknownHostException e) { + // Borked machine with no loopback adapter configured properly. + throw new RuntimeException(e); + } + } + + /** + * If true, the wallet will save itself to disk automatically whenever it changes. + */ + public WalletAppKitBitSquare setAutoSave(boolean value) { + checkState(state() == State.NEW, "Cannot call after startup"); + useAutoSave = value; + return this; + } + + /** + * If you want to learn about the sync process, you can provide a listener here. For instance, a + * {@link org.bitcoinj.core.DownloadProgressTracker} is a good choice. This has no effect unless setBlockingStartup(false) has been called + * too, due to some missing implementation code. + */ + public WalletAppKitBitSquare setDownloadListener(PeerEventListener listener) { + this.downloadListener = listener; + return this; + } + + /** + * If true, will register a shutdown hook to stop the library. Defaults to true. + */ + public WalletAppKitBitSquare setAutoStop(boolean autoStop) { + this.autoStop = autoStop; + return this; + } + + /** + * If set, the file is expected to contain a checkpoints file calculated with BuildCheckpoints. It makes initial + * block sync faster for new users - please refer to the documentation on the bitcoinj website for further details. + */ + public WalletAppKitBitSquare setCheckpoints(InputStream checkpoints) { + if (this.checkpoints != null) + Utils.closeUnchecked(this.checkpoints); + this.checkpoints = checkNotNull(checkpoints); + return this; + } + + /** + * If true (the default) then the startup of this service won't be considered complete until the network has been + * brought up, peer connections established and the block chain synchronised. Therefore {@link #startAndWait()} can + * potentially take a very long time. If false, then startup is considered complete once the network activity + * begins and peer connections/block chain sync will continue in the background. + */ + public WalletAppKitBitSquare setBlockingStartup(boolean blockingStartup) { + this.blockingStartup = blockingStartup; + return this; + } + + /** + * Sets the string that will appear in the subver field of the version message. + * + * @param userAgent A short string that should be the name of your app, e.g. "My Wallet" + * @param version A short string that contains the version number, e.g. "1.0-BETA" + */ + public WalletAppKitBitSquare setUserAgent(String userAgent, String version) { + this.userAgent = checkNotNull(userAgent); + this.version = checkNotNull(version); + return this; + } + + /** + * If called, then an embedded Tor client library will be used to connect to the P2P network. The user does not need + * any additional software for this: it's all pure Java. As of April 2014 this mode is experimental. + */ + public WalletAppKitBitSquare useTor() { + this.useTor = true; + 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 WalletAppKitBitSquare restoreWalletFromSeed(DeterministicSeed seed) { + this.restoreFromSeed = seed; + return this; + } + + /** + * Sets the peer discovery class to use. If none is provided then DNS is used, which is a reasonable default. + */ + public WalletAppKitBitSquare setDiscovery(@Nullable PeerDiscovery discovery) { + this.discovery = discovery; + return this; + } + + public WalletAppKitBitSquare setBloomFilterFalsePositiveRate(double bloomFilterFPRate) { + this.bloomFilterFPRate = bloomFilterFPRate; + return this; + } + + public WalletAppKitBitSquare setBloomFilterTweak(long bloomFilterTweak) { + this.bloomFilterTweak = bloomFilterTweak; + return this; + } + + public WalletAppKitBitSquare setLookaheadSize(int lookaheadSize) { + this.lookaheadSize = lookaheadSize; + return this; + } + + /** + *

Override this to return wallet extensions if any are necessary.

+ *

+ *

When this is called, chain(), store(), and peerGroup() will return the created objects, however they are not + * initialized/started.

+ */ + protected List provideWalletExtensions() throws Exception { + return ImmutableList.of(); + } + + /** + * Override this to use a {@link BlockStore} that isn't the default of {@link SPVBlockStore}. + */ + protected BlockStore provideBlockStore(File file) throws BlockStoreException { + return new SPVBlockStore(params, file); + } + + /** + * This method is invoked on a background thread after all objects are initialised, but before the peer group + * or block chain download is started. You can tweak the objects configuration here. + */ + protected void onSetupCompleted() { + } + + /** + * Tests to see if the spvchain file has an operating system file lock on it. Useful for checking if your app + * is already running. If another copy of your app is running and you start the appkit anyway, an exception will + * be thrown during the startup process. Returns false if the chain file does not exist or is a directory. + */ + public boolean isChainFileLocked() throws IOException { + RandomAccessFile file2 = null; + try { + File file = new File(directory, filePrefix + ".spvchain"); + if (!file.exists()) + return false; + if (file.isDirectory()) + return false; + file2 = new RandomAccessFile(file, "rw"); + FileLock lock = file2.getChannel().tryLock(); + if (lock == null) + return true; + lock.release(); + return false; + } finally { + if (file2 != null) + file2.close(); + } + } + + @Override + protected void startUp() throws Exception { + // Runs in a separate thread. + Context.propagate(context); + if (!directory.exists()) { + if (!directory.mkdirs()) { + throw new IOException("Could not create directory " + directory.getAbsolutePath()); + } + } + 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) || restoreFromSeed != null; + vWallet = createOrLoadWallet(shouldReplayWallet); + + // Initiate Bitcoin network objects (block store, blockchain and peer group) + vStore = provideBlockStore(chainFile); + if (!chainFileExists || restoreFromSeed != null) { + if (checkpoints != null) { + // Initialize the chain file with a checkpoint to speed up first-run sync. + long time; + 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 IOException("Failed to delete chain file in preparation for restore."); + vStore = new SPVBlockStore(params, chainFile); + } + } else { + time = vWallet.getEarliestKeyCreationTime(); + } + if (time > 0) + CheckpointManager.checkpoint(params, checkpoints, vStore, time); + else + log.warn("Creating a new uncheckpointed block store due to a wallet with a creation time of zero: this will result in a very slow chain sync"); + } else if (chainFileExists) { + log.info("Deleting the chain file in preparation from restore."); + vStore.close(); + if (!chainFile.delete()) + throw new IOException("Failed to delete chain file in preparation for restore."); + vStore = new SPVBlockStore(params, chainFile); + } + } + vChain = new BlockChain(params, vStore); + vPeerGroup = createPeerGroup(); + + if (bloomFilterFPRate != -1) + vPeerGroup.setBloomFilterFalsePositiveRate(bloomFilterFPRate); + + if (bloomFilterTweak != 0) + vPeerGroup.setBloomFilterTweak(bloomFilterTweak); + + if (this.userAgent != null) + vPeerGroup.setUserAgent(userAgent, version); + + // Set up peer addresses or discovery first, so if wallet extensions try to broadcast a transaction + // before we're actually connected the broadcast waits for an appropriate number of connections. + if (peerAddresses != null) { + for (PeerAddress addr : peerAddresses) vPeerGroup.addAddress(addr); + vPeerGroup.setMaxConnections(peerAddresses.length); + peerAddresses = null; + } else if (params != RegTestParams.get() && !useTor) { + vPeerGroup.addPeerDiscovery(discovery != null ? discovery : new DnsDiscovery(params)); + } + vChain.addWallet(vWallet); + vPeerGroup.addWallet(vWallet); + onSetupCompleted(); + + if (blockingStartup) { + vPeerGroup.start(); + // Make sure we shut down cleanly. + installShutdownHook(); + completeExtensionInitiations(vPeerGroup); + + // TODO: Be able to use the provided download listener when doing a blocking startup. + final DownloadProgressTracker listener = new DownloadProgressTracker(); + vPeerGroup.startBlockChainDownload(listener); + listener.await(); + } else { + Futures.addCallback(vPeerGroup.startAsync(), new FutureCallback() { + @Override + public void onSuccess(@Nullable Object result) { + completeExtensionInitiations(vPeerGroup); + final PeerEventListener l = downloadListener == null ? new DownloadProgressTracker() : downloadListener; + vPeerGroup.startBlockChainDownload(l); + } + + @Override + public void onFailure(Throwable t) { + throw new RuntimeException(t); + + } + }); + } + } catch (BlockStoreException e) { + throw new IOException(e); + } + } + + private Wallet createOrLoadWallet(boolean shouldReplayWallet) throws Exception { + Wallet wallet; + + maybeMoveOldWalletOutOfTheWay(); + + if (vWalletFile.exists()) { + wallet = loadWallet(shouldReplayWallet); + } else { + wallet = createWallet(); + wallet.freshReceiveKey(); + for (WalletExtension e : provideWalletExtensions()) { + wallet.addExtension(e); + } + + // Currently the only way we can be sure that an extension is aware of its containing wallet is by + // deserializing the extension (see WalletExtension#deserializeWalletExtension(Wallet, byte[])) + // Hence, we first save and then load wallet to ensure any extensions are correctly initialized. + wallet.saveToFile(vWalletFile); + wallet = loadWallet(false); + } + + if (useAutoSave) wallet.autosaveToFile(vWalletFile, 5, TimeUnit.SECONDS, null); + + return wallet; + } + + private Wallet loadWallet(boolean shouldReplayWallet) throws Exception { + Wallet wallet; + FileInputStream walletStream = new FileInputStream(vWalletFile); + try { + List extensions = provideWalletExtensions(); + WalletExtension[] extArray = extensions.toArray(new WalletExtension[extensions.size()]); + Protos.Wallet proto = WalletProtobufSerializer.parseToProto(walletStream); + final WalletProtobufSerializer serializer; + if (walletFactory != null) + serializer = new WalletProtobufSerializer(walletFactory); + else + serializer = new WalletProtobufSerializer(); + wallet = serializer.readWallet(params, extArray, proto); + if (shouldReplayWallet) + wallet.reset(); + } finally { + walletStream.close(); + } + return wallet; + } + + protected Wallet createWallet() { + KeyChainGroup kcg; + if (restoreFromSeed != null) + kcg = new KeyChainGroup(params, restoreFromSeed); + else + kcg = new KeyChainGroup(params); + + if (lookaheadSize != -1) + kcg.setLookaheadSize(lookaheadSize); + + 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"); + } + } + + /* + * As soon as the transaction broadcaster han been created we will pass it to the + * payment channel extensions + */ + private void completeExtensionInitiations(TransactionBroadcaster transactionBroadcaster) { + StoredPaymentChannelClientStates clientStoredChannels = (StoredPaymentChannelClientStates) + vWallet.getExtensions().get(StoredPaymentChannelClientStates.class.getName()); + if (clientStoredChannels != null) { + clientStoredChannels.setTransactionBroadcaster(transactionBroadcaster); + } + StoredPaymentChannelServerStates serverStoredChannels = (StoredPaymentChannelServerStates) + vWallet.getExtensions().get(StoredPaymentChannelServerStates.class.getName()); + if (serverStoredChannels != null) { + serverStoredChannels.setTransactionBroadcaster(transactionBroadcaster); + } + } + + + protected PeerGroup createPeerGroup() throws TimeoutException { // no proxy case. if (socks5Proxy == null) { - return super.createPeerGroup(); + if (useTor) { + TorClient torClient = new TorClient(); + torClient.getConfig().setDataDirectory(directory); + return PeerGroup.newWithTor(params, vChain, torClient); + } else + return new PeerGroup(params, vChain); } // proxy case. @@ -65,4 +535,64 @@ public class WalletAppKitBitSquare extends WalletAppKit { return peerGroup; } + + private void installShutdownHook() { + if (autoStop) Runtime.getRuntime().addShutdownHook(new Thread() { + @Override + public void run() { + try { + WalletAppKitBitSquare.this.stopAsync(); + WalletAppKitBitSquare.this.awaitTerminated(); + } catch (Exception e) { + throw new RuntimeException(e); + } + } + }); + } + + @Override + protected void shutDown() throws Exception { + // Runs in a separate thread. + try { + Context.propagate(context); + vPeerGroup.stop(); + vWallet.saveToFile(vWalletFile); + vStore.close(); + + vPeerGroup = null; + vWallet = null; + vStore = null; + vChain = null; + } catch (BlockStoreException e) { + throw new IOException(e); + } + } + + public NetworkParameters params() { + return params; + } + + public BlockChain chain() { + checkState(state() == State.STARTING || state() == State.RUNNING, "Cannot call until startup is complete"); + return vChain; + } + + public BlockStore store() { + checkState(state() == State.STARTING || state() == State.RUNNING, "Cannot call until startup is complete"); + return vStore; + } + + public Wallet wallet() { + checkState(state() == State.STARTING || state() == State.RUNNING, "Cannot call until startup is complete"); + return vWallet; + } + + public PeerGroup peerGroup() { + checkState(state() == State.STARTING || state() == State.RUNNING, "Cannot call until startup is complete"); + return vPeerGroup; + } + + public File directory() { + return directory; + } }