public void timeLockedTransaction(boolean useNotFound) throws Exception { connectWithVersion(useNotFound ? 70001 : 60001); // Test that if we receive a relevant transaction that has a lock time, it doesn't result in a // notification // until we explicitly opt in to seeing those. ECKey key = new ECKey(); Wallet wallet = new Wallet(unitTestParams); wallet.addKey(key); peer.addWallet(wallet); final Transaction[] vtx = new Transaction[1]; wallet.addEventListener( new AbstractWalletEventListener() { @Override public void onCoinsReceived( Wallet wallet, Transaction tx, BigInteger prevBalance, BigInteger newBalance) { vtx[0] = tx; } }); // Send a normal relevant transaction, it's received correctly. Transaction t1 = TestUtils.createFakeTx(unitTestParams, Utils.toNanoCoins(1, 0), key); inbound(writeTarget, t1); GetDataMessage getdata = (GetDataMessage) outbound(writeTarget); if (useNotFound) { inbound(writeTarget, new NotFoundMessage(unitTestParams, getdata.getItems())); } else { bouncePing(); } pingAndWait(writeTarget); Threading.waitForUserCode(); assertNotNull(vtx[0]); vtx[0] = null; // Send a timelocked transaction, nothing happens. Transaction t2 = TestUtils.createFakeTx(unitTestParams, Utils.toNanoCoins(2, 0), key); t2.setLockTime(999999); inbound(writeTarget, t2); Threading.waitForUserCode(); assertNull(vtx[0]); // Now we want to hear about them. Send another, we are told about it. wallet.setAcceptRiskyTransactions(true); inbound(writeTarget, t2); getdata = (GetDataMessage) outbound(writeTarget); if (useNotFound) { inbound(writeTarget, new NotFoundMessage(unitTestParams, getdata.getItems())); } else { bouncePing(); } pingAndWait(writeTarget); Threading.waitForUserCode(); assertEquals(t2, vtx[0]); }
// Check that inventory message containing blocks we want is processed correctly. @Test public void newBlock() throws Exception { Block b1 = createFakeBlock(blockStore).block; blockChain.add(b1); final Block b2 = makeSolvedTestBlock(b1); // Receive notification of a new block. final InventoryMessage inv = new InventoryMessage(unitTestParams); InventoryItem item = new InventoryItem(InventoryItem.Type.Block, b2.getHash()); inv.addItem(item); final AtomicInteger newBlockMessagesReceived = new AtomicInteger(0); connect(); // Round-trip a ping so that we never see the response verack if we attach too quick pingAndWait(writeTarget); peer.addEventListener( new AbstractPeerEventListener() { @Override public synchronized Message onPreMessageReceived(Peer p, Message m) { if (p != peer) fail.set(true); if (m instanceof Pong) return m; int newValue = newBlockMessagesReceived.incrementAndGet(); if (newValue == 1 && !inv.equals(m)) fail.set(true); else if (newValue == 2 && !b2.equals(m)) fail.set(true); else if (newValue > 3) fail.set(true); return m; } @Override public synchronized void onBlocksDownloaded(Peer p, Block block, int blocksLeft) { int newValue = newBlockMessagesReceived.incrementAndGet(); if (newValue != 3 || p != peer || !block.equals(b2) || blocksLeft != OTHER_PEER_CHAIN_HEIGHT - 2) fail.set(true); } }, Threading.SAME_THREAD); long height = peer.getBestHeight(); inbound(writeTarget, inv); pingAndWait(writeTarget); assertEquals(height + 1, peer.getBestHeight()); // Response to the getdata message. inbound(writeTarget, b2); pingAndWait(writeTarget); Threading.waitForUserCode(); pingAndWait(writeTarget); assertEquals(3, newBlockMessagesReceived.get()); GetDataMessage getdata = (GetDataMessage) outbound(writeTarget); List<InventoryItem> items = getdata.getItems(); assertEquals(1, items.size()); assertEquals(b2.getHash(), items.get(0).hash); assertEquals(InventoryItem.Type.Block, items.get(0).type); }
@Test public void exceptionListener() throws Exception { wallet.addEventListener( new AbstractWalletEventListener() { @Override public void onCoinsReceived( Wallet wallet, Transaction tx, BigInteger prevBalance, BigInteger newBalance) { throw new NullPointerException("boo!"); } }); final Throwable[] throwables = new Throwable[1]; Threading.uncaughtExceptionHandler = new Thread.UncaughtExceptionHandler() { @Override public void uncaughtException(Thread thread, Throwable throwable) { throwables[0] = throwable; } }; // In real usage we're not really meant to adjust the uncaught exception handler after stuff // started happening // but in the unit test environment other tests have just run so the thread is probably still // kicking around. // Force it to crash so it'll be recreated with our new handler. Threading.USER_THREAD.execute( new Runnable() { @Override public void run() { throw new RuntimeException(); } }); connect(); Transaction t1 = new Transaction(unitTestParams); t1.addInput(new TransactionInput(unitTestParams, t1, new byte[] {})); t1.addOutput(Utils.toNanoCoins(1, 0), new ECKey().toAddress(unitTestParams)); Transaction t2 = new Transaction(unitTestParams); t2.addInput(t1.getOutput(0)); t2.addOutput(Utils.toNanoCoins(1, 0), wallet.getChangeAddress()); inbound(writeTarget, t2); final InventoryItem inventoryItem = new InventoryItem(InventoryItem.Type.Transaction, t2.getInput(0).getOutpoint().getHash()); final NotFoundMessage nfm = new NotFoundMessage(unitTestParams, Lists.newArrayList(inventoryItem)); inbound(writeTarget, nfm); pingAndWait(writeTarget); Threading.waitForUserCode(); assertTrue(throwables[0] instanceof NullPointerException); Threading.uncaughtExceptionHandler = null; }
@Override public void onCreate() { new LinuxSecureRandom(); // init proper random number generator initLogging(); StrictMode.setThreadPolicy( new StrictMode.ThreadPolicy.Builder() .detectAll() .permitDiskReads() .permitDiskWrites() .penaltyLog() .build()); Threading.throwOnLockCycles(); log.info( "configuration: " + (Constants.TEST ? "test" : "prod") + ", " + Constants.NETWORK_PARAMETERS.getId()); super.onCreate(); try { packageInfo = getPackageManager().getPackageInfo(getPackageName(), 0); } catch (final NameNotFoundException x) { throw new RuntimeException(x); } CrashReporter.init(getCacheDir()); Threading.uncaughtExceptionHandler = new Thread.UncaughtExceptionHandler() { @Override public void uncaughtException(final Thread thread, final Throwable throwable) { log.info("bitcoinj uncaught exception", throwable); CrashReporter.saveBackgroundTrace(throwable, packageInfo); } }; prefs = PreferenceManager.getDefaultSharedPreferences(this); activityManager = (ActivityManager) getSystemService(Context.ACTIVITY_SERVICE); blockchainServiceIntent = new Intent(this, BlockchainServiceImpl.class); blockchainServiceCancelCoinsReceivedIntent = new Intent( BlockchainService.ACTION_CANCEL_COINS_RECEIVED, null, this, BlockchainServiceImpl.class); blockchainServiceResetBlockchainIntent = new Intent( BlockchainService.ACTION_RESET_BLOCKCHAIN, null, this, BlockchainServiceImpl.class); walletFile = getFileStreamPath(Constants.WALLET_FILENAME_PROTOBUF); migrateWalletToProtobuf(); loadWalletFromProtobuf(); wallet.autosaveToFile(walletFile, 1, TimeUnit.SECONDS, new WalletAutosaveEventListener()); final int lastVersionCode = prefs.getInt(Constants.PREFS_KEY_LAST_VERSION, 0); prefs.edit().putInt(Constants.PREFS_KEY_LAST_VERSION, packageInfo.versionCode).commit(); if (packageInfo.versionCode > lastVersionCode) log.info("detected app upgrade: " + lastVersionCode + " -> " + packageInfo.versionCode); else if (packageInfo.versionCode < lastVersionCode) log.warn("detected app downgrade: " + lastVersionCode + " -> " + packageInfo.versionCode); if (lastVersionCode > 0 && lastVersionCode < KEY_ROTATION_VERSION_CODE && packageInfo.versionCode >= KEY_ROTATION_VERSION_CODE) { log.info("detected version jump crossing key rotation"); wallet.setKeyRotationTime(System.currentTimeMillis() / 1000); } ensureKey(); }
private void checkTimeLockedDependency(boolean shouldAccept, boolean useNotFound) throws Exception { // Initial setup. connectWithVersion(useNotFound ? 70001 : 60001); ECKey key = new ECKey(); Wallet wallet = new Wallet(unitTestParams); wallet.addKey(key); wallet.setAcceptRiskyTransactions(shouldAccept); peer.addWallet(wallet); final Transaction[] vtx = new Transaction[1]; wallet.addEventListener( new AbstractWalletEventListener() { @Override public void onCoinsReceived( Wallet wallet, Transaction tx, BigInteger prevBalance, BigInteger newBalance) { vtx[0] = tx; } }); // t1 -> t2 [locked] -> t3 (not available) Transaction t2 = new Transaction(unitTestParams); t2.setLockTime(999999); // Add a fake input to t3 that goes nowhere. Sha256Hash t3 = Sha256Hash.create("abc".getBytes(Charset.forName("UTF-8"))); t2.addInput( new TransactionInput( unitTestParams, t2, new byte[] {}, new TransactionOutPoint(unitTestParams, 0, t3))); t2.getInput(0).setSequenceNumber(0xDEADBEEF); t2.addOutput(Utils.toNanoCoins(1, 0), new ECKey()); Transaction t1 = new Transaction(unitTestParams); t1.addInput(t2.getOutput(0)); t1.addOutput(Utils.toNanoCoins(1, 0), key); // Make it relevant. // Announce t1. InventoryMessage inv = new InventoryMessage(unitTestParams); inv.addTransaction(t1); inbound(writeTarget, inv); // Send it. GetDataMessage getdata = (GetDataMessage) outbound(writeTarget); assertEquals(t1.getHash(), getdata.getItems().get(0).hash); inbound(writeTarget, t1); // Nothing arrived at our event listener yet. assertNull(vtx[0]); // We request t2. getdata = (GetDataMessage) outbound(writeTarget); assertEquals(t2.getHash(), getdata.getItems().get(0).hash); inbound(writeTarget, t2); if (!useNotFound) bouncePing(); // We request t3. getdata = (GetDataMessage) outbound(writeTarget); assertEquals(t3, getdata.getItems().get(0).hash); // Can't find it: bottom of tree. if (useNotFound) { NotFoundMessage notFound = new NotFoundMessage(unitTestParams); notFound.addItem(new InventoryItem(InventoryItem.Type.Transaction, t3)); inbound(writeTarget, notFound); } else { bouncePing(); } pingAndWait(writeTarget); Threading.waitForUserCode(); // We're done but still not notified because it was timelocked. if (shouldAccept) assertNotNull(vtx[0]); else assertNull(vtx[0]); }
public void recursiveDownload(boolean useNotFound) throws Exception { // Using ping or notfound? connectWithVersion(useNotFound ? 70001 : 60001); // Check that we can download all dependencies of an unconfirmed relevant transaction from the // mempool. ECKey to = new ECKey(); final Transaction[] onTx = new Transaction[1]; peer.addEventListener( new AbstractPeerEventListener() { @Override public void onTransaction(Peer peer1, Transaction t) { onTx[0] = t; } }, Threading.SAME_THREAD); // Make the some fake transactions in the following graph: // t1 -> t2 -> [t5] // -> t3 -> t4 -> [t6] // -> [t7] // -> [t8] // The ones in brackets are assumed to be in the chain and are represented only by hashes. Transaction t2 = TestUtils.createFakeTx(unitTestParams, Utils.toNanoCoins(1, 0), to); Sha256Hash t5 = t2.getInput(0).getOutpoint().getHash(); Transaction t4 = TestUtils.createFakeTx(unitTestParams, Utils.toNanoCoins(1, 0), new ECKey()); Sha256Hash t6 = t4.getInput(0).getOutpoint().getHash(); t4.addOutput(Utils.toNanoCoins(1, 0), new ECKey()); Transaction t3 = new Transaction(unitTestParams); t3.addInput(t4.getOutput(0)); t3.addOutput(Utils.toNanoCoins(1, 0), new ECKey()); Transaction t1 = new Transaction(unitTestParams); t1.addInput(t2.getOutput(0)); t1.addInput(t3.getOutput(0)); Sha256Hash someHash = new Sha256Hash("2b801dd82f01d17bbde881687bf72bc62e2faa8ab8133d36fcb8c3abe7459da6"); t1.addInput( new TransactionInput( unitTestParams, t1, new byte[] {}, new TransactionOutPoint(unitTestParams, 0, someHash))); Sha256Hash anotherHash = new Sha256Hash("3b801dd82f01d17bbde881687bf72bc62e2faa8ab8133d36fcb8c3abe7459da6"); t1.addInput( new TransactionInput( unitTestParams, t1, new byte[] {}, new TransactionOutPoint(unitTestParams, 1, anotherHash))); t1.addOutput(Utils.toNanoCoins(1, 0), to); t1 = TestUtils.roundTripTransaction(unitTestParams, t1); t2 = TestUtils.roundTripTransaction(unitTestParams, t2); t3 = TestUtils.roundTripTransaction(unitTestParams, t3); t4 = TestUtils.roundTripTransaction(unitTestParams, t4); // Announce the first one. Wait for it to be downloaded. InventoryMessage inv = new InventoryMessage(unitTestParams); inv.addTransaction(t1); inbound(writeTarget, inv); GetDataMessage getdata = (GetDataMessage) outbound(writeTarget); Threading.waitForUserCode(); assertEquals(t1.getHash(), getdata.getItems().get(0).hash); inbound(writeTarget, t1); pingAndWait(writeTarget); assertEquals(t1, onTx[0]); // We want its dependencies so ask for them. ListenableFuture<List<Transaction>> futures = peer.downloadDependencies(t1); assertFalse(futures.isDone()); // It will recursively ask for the dependencies of t1: t2, t3, someHash and anotherHash. getdata = (GetDataMessage) outbound(writeTarget); assertEquals(4, getdata.getItems().size()); assertEquals(t2.getHash(), getdata.getItems().get(0).hash); assertEquals(t3.getHash(), getdata.getItems().get(1).hash); assertEquals(someHash, getdata.getItems().get(2).hash); assertEquals(anotherHash, getdata.getItems().get(3).hash); long nonce = -1; if (!useNotFound) nonce = ((Ping) outbound(writeTarget)).getNonce(); // For some random reason, t4 is delivered at this point before it's needed - perhaps it was a // Bloom filter // false positive. We do this to check that the mempool is being checked for seen transactions // before // requesting them. inbound(writeTarget, t4); // Deliver the requested transactions. inbound(writeTarget, t2); inbound(writeTarget, t3); if (useNotFound) { NotFoundMessage notFound = new NotFoundMessage(unitTestParams); notFound.addItem(new InventoryItem(InventoryItem.Type.Transaction, someHash)); notFound.addItem(new InventoryItem(InventoryItem.Type.Transaction, anotherHash)); inbound(writeTarget, notFound); } else { inbound(writeTarget, new Pong(nonce)); } assertFalse(futures.isDone()); // It will recursively ask for the dependencies of t2: t5 and t4, but not t3 because it already // found t4. getdata = (GetDataMessage) outbound(writeTarget); assertEquals(getdata.getItems().get(0).hash, t2.getInput(0).getOutpoint().getHash()); // t5 isn't found and t4 is. if (useNotFound) { NotFoundMessage notFound = new NotFoundMessage(unitTestParams); notFound.addItem(new InventoryItem(InventoryItem.Type.Transaction, t5)); inbound(writeTarget, notFound); } else { bouncePing(); } assertFalse(futures.isDone()); // Continue to explore the t4 branch and ask for t6, which is in the chain. getdata = (GetDataMessage) outbound(writeTarget); assertEquals(t6, getdata.getItems().get(0).hash); if (useNotFound) { NotFoundMessage notFound = new NotFoundMessage(unitTestParams); notFound.addItem(new InventoryItem(InventoryItem.Type.Transaction, t6)); inbound(writeTarget, notFound); } else { bouncePing(); } pingAndWait(writeTarget); // That's it, we explored the entire tree. assertTrue(futures.isDone()); List<Transaction> results = futures.get(); assertTrue(results.contains(t2)); assertTrue(results.contains(t3)); assertTrue(results.contains(t4)); }
/** * A deterministic key chain is a {@link KeyChain} that uses the <a * href="https://github.com/bitcoin/bips/blob/master/bip-0032.mediawiki">BIP 32 standard</a>, as * implemented by {@link com.google.bitcoin.crypto.DeterministicHierarchy}, to derive all the keys * in the keychain from a master seed. This type of wallet is extremely convenient and flexible. * Although backing up full wallet files is always a good idea, to recover money only the root seed * needs to be preserved and that is a number small enough that it can be written down on paper or, * when represented using a BIP 39 {@link com.google.bitcoin.crypto.MnemonicCode}, dictated over the * phone (possibly even memorized). * * <p>Deterministic key chains have other advantages: parts of the key tree can be selectively * revealed to allow for auditing, and new public keys can be generated without access to the * private keys, yielding a highly secure configuration for web servers which can accept payments * into a wallet but not spend from them. This does not work quite how you would expect due to a * quirk of elliptic curve mathematics and the techniques used to deal with it. A watching wallet is * not instantiated using the public part of the master key as you may imagine. Instead, you need to * take the account key (first child of the master key) and provide the public part of that to the * watching wallet instead. You can do this by calling {@link #getWatchingKey()} and then * serializing it with {@link com.google.bitcoin.crypto.DeterministicKey#serializePubB58()}. The * resulting "xpub..." string encodes sufficient information about the account key to create a * watching chain via {@link * com.google.bitcoin.crypto.DeterministicKey#deserializeB58(com.google.bitcoin.crypto.DeterministicKey, * String)} (with null as the first parameter) and then {@link * DeterministicKeyChain#DeterministicKeyChain(com.google.bitcoin.crypto.DeterministicKey)}. * * <p>This class builds on {@link com.google.bitcoin.crypto.DeterministicHierarchy} and {@link * com.google.bitcoin.crypto.DeterministicKey} by adding support for serialization to and from * protobufs, and encryption of parts of the key tree. Internally it arranges itself as per the BIP * 32 spec, with the seed being used to derive a master key, which is then used to derive an account * key, the account key is used to derive two child keys called the <i>internal</i> and * <i>external</i> keys (for change and handing out addresses respectively) and finally the actual * leaf keys that users use hanging off the end. The leaf keys are special in that they don't * internally store the private part at all, instead choosing to rederive the private key from the * parent when needed for signing. This simplifies the design for encrypted key chains. * * <p>The key chain manages a <i>lookahead zone</i>. This zone is required because when scanning the * chain, you don't know exactly which keys might receive payments. The user may have handed out * several addresses and received payments on them, but for latency reasons the block chain is * requested from remote peers in bulk, meaning you must "look ahead" when calculating keys to put * in the Bloom filter. The default lookahead zone is 100 keys, meaning if the user hands out more * than 100 addresses and receives payment on them before the chain is next scanned, some * transactions might be missed. 100 is a reasonable choice for consumer wallets running on CPU * constrained devices. For industrial wallets that are receiving keys all the time, a higher value * is more appropriate. Ideally DKC and the wallet would know how to adjust this value * automatically, but that's not implemented at the moment. * * <p>In fact the real size of the lookahead zone is larger than requested, by default, it's one * third larger. This is because the act of deriving new keys means recalculating the Bloom filters * and this is an expensive operation. Thus, to ensure we don't have to recalculate on every single * new key/address requested or seen we add more buffer space and only extend the lookahead zone * when that buffer is exhausted. For example with a lookahead zone of 100 keys, you can request 33 * keys before more keys will be calculated and the Bloom filter rebuilt and rebroadcast. But even * when you are requesting the 33rd key, you will still be looking 100 keys ahead. */ public class DeterministicKeyChain implements EncryptableKeyChain { private static final Logger log = LoggerFactory.getLogger(DeterministicKeyChain.class); public static final String DEFAULT_PASSPHRASE_FOR_MNEMONIC = ""; private final ReentrantLock lock = Threading.lock("DeterministicKeyChain"); private DeterministicHierarchy hierarchy; @Nullable private DeterministicKey rootKey; @Nullable private DeterministicSeed seed; // Ignored if seed != null. Useful for watching hierarchies. private long creationTimeSeconds = MnemonicCode.BIP39_STANDARDISATION_TIME_SECS; // Paths through the key tree. External keys are ones that are communicated to other parties. // Internal keys are // keys created for change addresses, coinbases, mixing, etc - anything that isn't communicated. // The distinction // is somewhat arbitrary but can be useful for audits. The first number is the "account number" // but we don't use // that feature yet. In future we might hand out different accounts for cases where we wish to // hand payers // a payment request that can generate lots of addresses independently. public static final ImmutableList<ChildNumber> ACCOUNT_ZERO_PATH = ImmutableList.of(ChildNumber.ZERO_HARDENED); public static final ImmutableList<ChildNumber> EXTERNAL_PATH = ImmutableList.of(ChildNumber.ZERO_HARDENED, ChildNumber.ZERO); public static final ImmutableList<ChildNumber> INTERNAL_PATH = ImmutableList.of(ChildNumber.ZERO_HARDENED, ChildNumber.ONE); // We try to ensure we have at least this many keys ready and waiting to be handed out via // getKey(). // See docs for getLookaheadSize() for more info on what this is for. The -1 value means it hasn't // been calculated // yet. For new chains it's set to whatever the default is, unless overridden by setLookaheadSize. // For deserialized // chains, it will be calculated on demand from the number of loaded keys. private static final int LAZY_CALCULATE_LOOKAHEAD = -1; private int lookaheadSize = 100; // The lookahead threshold causes us to batch up creation of new keys to minimize the frequency of // Bloom filter // regenerations, which are expensive and will (in future) trigger chain download stalls/retries. // One third // is an efficiency tradeoff. private int lookaheadThreshold = calcDefaultLookaheadThreshold(); private int calcDefaultLookaheadThreshold() { return lookaheadSize / 3; } // The parent keys for external keys (handed out to other people) and internal keys (used for // change addresses). private DeterministicKey externalKey, internalKey; // How many keys on each path have actually been used. This may be fewer than the number that have // been deserialized // or held in memory, because of the lookahead zone. private int issuedExternalKeys, issuedInternalKeys; // We simplify by wrapping a basic key chain and that way we get some functionality like key // lookup and event // listeners "for free". All keys in the key tree appear here, even if they aren't meant to be // used for receiving // money. private final BasicKeyChain basicKeyChain; // If set this chain is following another chain in a married KeyChainGroup private boolean isFollowing; /** * Generates a new key chain with entropy selected randomly from the given {@link * java.security.SecureRandom} object and the default entropy size. */ public DeterministicKeyChain(SecureRandom random) { this( random, DeterministicSeed.DEFAULT_SEED_ENTROPY_BITS, DEFAULT_PASSPHRASE_FOR_MNEMONIC, Utils.currentTimeSeconds()); } /** * Generates a new key chain with entropy selected randomly from the given {@link * java.security.SecureRandom} object and of the requested size in bits. */ public DeterministicKeyChain(SecureRandom random, int bits) { this(random, bits, DEFAULT_PASSPHRASE_FOR_MNEMONIC, Utils.currentTimeSeconds()); } /** * Generates a new key chain with entropy selected randomly from the given {@link * java.security.SecureRandom} object and of the requested size in bits. The derived seed is * further protected with a user selected passphrase (see BIP 39). */ public DeterministicKeyChain( SecureRandom random, int bits, String passphrase, long seedCreationTimeSecs) { this(new DeterministicSeed(random, bits, passphrase, seedCreationTimeSecs)); } /** * Creates a deterministic key chain starting from the given seed. All keys yielded by this chain * will be the same if the starting seed is the same. You should provide the creation time in * seconds since the UNIX epoch for the seed: this lets us know from what part of the chain we can * expect to see derived keys appear. */ public DeterministicKeyChain(byte[] entropy, String passphrase, long seedCreationTimeSecs) { this(new DeterministicSeed(entropy, passphrase, seedCreationTimeSecs)); } /** * Creates a deterministic key chain starting from the given seed. All keys yielded by this chain * will be the same if the starting seed is the same. */ protected DeterministicKeyChain(DeterministicSeed seed) { this(seed, null); } /** * Creates a deterministic key chain that watches the given (public only) root key. You can use * this to calculate balances and generally follow along, but spending is not possible with such a * chain. Currently you can't use this method to watch an arbitrary fragment of some other tree, * this limitation may be removed in future. */ public DeterministicKeyChain(DeterministicKey watchingKey, long creationTimeSeconds) { checkArgument(watchingKey.isPubKeyOnly(), "Private subtrees not currently supported"); checkArgument(watchingKey.getPath().size() == 1, "You can only watch an account key currently"); basicKeyChain = new BasicKeyChain(); this.creationTimeSeconds = creationTimeSeconds; this.seed = null; initializeHierarchyUnencrypted(watchingKey); } public DeterministicKeyChain(DeterministicKey watchingKey) { this(watchingKey, Utils.currentTimeSeconds()); } /** * Creates a deterministic key chain with the given watch key. If <code>isFollowing</code> flag is * set then this keychain follows some other keychain. In a married wallet following keychain * represents "spouse's" keychain. * * <p>Watch key has to be an account key. */ private DeterministicKeyChain(DeterministicKey watchKey, boolean isFollowing) { this(watchKey, Utils.currentTimeSeconds()); this.isFollowing = isFollowing; } /** * Creates a deterministic key chain with the given watch key and that follows some other * keychain. In a married wallet following keychain represents "spouse" Watch key has to be an * account key. */ public static DeterministicKeyChain watchAndFollow(DeterministicKey watchKey) { return new DeterministicKeyChain(watchKey, true); } /** * Creates a key chain that watches the given account key. The creation time is taken to be the * time that BIP 32 was standardised: most likely, you can optimise by selecting a more accurate * creation time for your key and using the other watch method. */ public static DeterministicKeyChain watch(DeterministicKey accountKey) { return watch(accountKey, DeterministicHierarchy.BIP32_STANDARDISATION_TIME_SECS); } /** * Creates a key chain that watches the given account key, and assumes there are no transactions * involving it until the given time (this is an optimisation for chain scanning purposes). */ public static DeterministicKeyChain watch( DeterministicKey accountKey, long seedCreationTimeSecs) { return new DeterministicKeyChain(accountKey, seedCreationTimeSecs); } DeterministicKeyChain(DeterministicSeed seed, @Nullable KeyCrypter crypter) { this.seed = seed; basicKeyChain = new BasicKeyChain(crypter); if (!seed.isEncrypted()) { rootKey = HDKeyDerivation.createMasterPrivateKey(checkNotNull(seed.getSeedBytes())); rootKey.setCreationTimeSeconds(seed.getCreationTimeSeconds()); initializeHierarchyUnencrypted(rootKey); } // Else... // We can't initialize ourselves with just an encrypted seed, so we expected deserialization // code to do the // rest of the setup (loading the root key). } // For use in encryption. private DeterministicKeyChain( KeyCrypter crypter, KeyParameter aesKey, DeterministicKeyChain chain) { // Can't encrypt a watching chain. checkNotNull(chain.rootKey); checkNotNull(chain.seed); checkArgument(!chain.rootKey.isEncrypted(), "Chain already encrypted"); this.issuedExternalKeys = chain.issuedExternalKeys; this.issuedInternalKeys = chain.issuedInternalKeys; this.lookaheadSize = chain.lookaheadSize; this.lookaheadThreshold = chain.lookaheadThreshold; this.seed = chain.seed.encrypt(crypter, aesKey); basicKeyChain = new BasicKeyChain(crypter); // The first number is the "account number" but we don't use that feature. rootKey = chain.rootKey.encrypt(crypter, aesKey, null); hierarchy = new DeterministicHierarchy(rootKey); basicKeyChain.importKey(rootKey); DeterministicKey account = encryptNonLeaf(aesKey, chain, rootKey, ACCOUNT_ZERO_PATH); externalKey = encryptNonLeaf(aesKey, chain, account, EXTERNAL_PATH); internalKey = encryptNonLeaf(aesKey, chain, account, INTERNAL_PATH); // Now copy the (pubkey only) leaf keys across to avoid rederiving them. The private key bytes // are missing // anyway so there's nothing to encrypt. for (ECKey eckey : chain.basicKeyChain.getKeys()) { DeterministicKey key = (DeterministicKey) eckey; if (key.getPath().size() != 3) continue; // Not a leaf key. DeterministicKey parent = hierarchy.get(checkNotNull(key.getParent()).getPath(), false, false); // Clone the key to the new encrypted hierarchy. key = new DeterministicKey(key.getPubOnly(), parent); hierarchy.putKey(key); basicKeyChain.importKey(key); } } private DeterministicKey encryptNonLeaf( KeyParameter aesKey, DeterministicKeyChain chain, DeterministicKey parent, ImmutableList<ChildNumber> path) { DeterministicKey key = chain.hierarchy.get(path, false, false); key = key.encrypt(checkNotNull(basicKeyChain.getKeyCrypter()), aesKey, parent); hierarchy.putKey(key); basicKeyChain.importKey(key); return key; } // Derives the account path keys and inserts them into the basic key chain. This is important to // preserve their // order for serialization, amongst other things. private void initializeHierarchyUnencrypted(DeterministicKey baseKey) { if (baseKey.getPath().isEmpty()) { // baseKey is a master/root key derived directly from a seed. addToBasicChain(rootKey); hierarchy = new DeterministicHierarchy(rootKey); addToBasicChain(hierarchy.get(ACCOUNT_ZERO_PATH, false, true)); } else if (baseKey.getPath().size() == 1) { // baseKey is a "watching key" that we were given so we could follow along with this account. rootKey = null; addToBasicChain(baseKey); hierarchy = new DeterministicHierarchy(baseKey); } else { throw new IllegalArgumentException(); } externalKey = hierarchy.deriveChild(ACCOUNT_ZERO_PATH, false, false, ChildNumber.ZERO); internalKey = hierarchy.deriveChild(ACCOUNT_ZERO_PATH, false, false, ChildNumber.ONE); addToBasicChain(externalKey); addToBasicChain(internalKey); } /** Returns a freshly derived key that has not been returned by this method before. */ @Override public DeterministicKey getKey(KeyPurpose purpose) { return getKeys(purpose, 1).get(0); } /** Returns freshly derived key/s that have not been returned by this method before. */ @Override public List<DeterministicKey> getKeys(KeyPurpose purpose, int numberOfKeys) { checkArgument(numberOfKeys > 0); lock.lock(); try { DeterministicKey parentKey; int index; switch (purpose) { // Map both REFUND and RECEIVE_KEYS to the same branch for now. Refunds are a feature of // the BIP 70 // payment protocol. Later we may wish to map it to a different branch (in a new wallet // version?). // This would allow a watching wallet to only be able to see inbound payments, but not // change // (i.e. spends) or refunds. Might be useful for auditing ... case RECEIVE_FUNDS: case REFUND: issuedExternalKeys += numberOfKeys; index = issuedExternalKeys; parentKey = externalKey; break; case AUTHENTICATION: case CHANGE: issuedInternalKeys += numberOfKeys; index = issuedInternalKeys; parentKey = internalKey; break; default: throw new UnsupportedOperationException(); } // Optimization: potentially do a very quick key generation for just the number of keys we // need if we // didn't already create them, ignoring the configured lookahead size. This ensures we'll be // able to // retrieve the keys in the following loop, but if we're totally fresh and didn't get a chance // to // calculate the lookahead keys yet, this will not block waiting to calculate 100+ EC point // multiplies. // On slow/crappy Android phones looking ahead 100 keys can take ~5 seconds but the OS will // kill us // if we block for just one second on the UI thread. Because UI threads may need an address in // order // to render the screen, we need getKeys to be fast even if the wallet is totally brand new // and lookahead // didn't happen yet. // // It's safe to do this because when a network thread tries to calculate a Bloom filter, we'll // go ahead // and calculate the full lookahead zone there, so network requests will always use the right // amount. List<DeterministicKey> lookahead = maybeLookAhead(parentKey, index, 0, 0); basicKeyChain.importKeys(lookahead); List<DeterministicKey> keys = new ArrayList<DeterministicKey>(numberOfKeys); for (int i = 0; i < numberOfKeys; i++) { ImmutableList<ChildNumber> path = HDUtils.append(parentKey.getPath(), new ChildNumber(index - numberOfKeys + i, false)); DeterministicKey k = hierarchy.get(path, false, false); // Just a last minute sanity check before we hand the key out to the app for usage. This // isn't inspired // by any real problem reports from bitcoinj users, but I've heard of cases via the // grapevine of // places that lost money due to bitflips causing addresses to not match keys. Of course in // an // environment with flaky RAM there's no real way to always win: bitflips could be // introduced at any // other layer. But as we're potentially retrieving from long term storage here, check // anyway. checkForBitFlip(k); keys.add(k); } return keys; } finally { lock.unlock(); } } private void checkForBitFlip(DeterministicKey k) { DeterministicKey parent = checkNotNull(k.getParent()); byte[] rederived = HDKeyDerivation.deriveChildKeyBytesFromPublic( parent, k.getChildNumber(), HDKeyDerivation.PublicDeriveMode.WITH_INVERSION) .keyBytes; byte[] actual = k.getPubKey(); if (!Arrays.equals(rederived, actual)) throw new IllegalStateException( String.format( "Bit-flip check failed: %s vs %s", Arrays.toString(rederived), Arrays.toString(actual))); } private void addToBasicChain(DeterministicKey key) { basicKeyChain.importKeys(ImmutableList.of(key)); } /** * Mark the DeterministicKey as used. Also correct the issued{Internal|External}Keys counter, * because all lower children seem to be requested already. If the counter was updated, we also * might trigger lookahead. */ public DeterministicKey markKeyAsUsed(DeterministicKey k) { int numChildren = k.getChildNumber().i() + 1; if (k.getParent() == internalKey) { if (issuedInternalKeys < numChildren) { issuedInternalKeys = numChildren; maybeLookAhead(); } } else if (k.getParent() == externalKey) { if (issuedExternalKeys < numChildren) { issuedExternalKeys = numChildren; maybeLookAhead(); } } return k; } public DeterministicKey findKeyFromPubHash(byte[] pubkeyHash) { lock.lock(); try { return (DeterministicKey) basicKeyChain.findKeyFromPubHash(pubkeyHash); } finally { lock.unlock(); } } public DeterministicKey findKeyFromPubKey(byte[] pubkey) { lock.lock(); try { return (DeterministicKey) basicKeyChain.findKeyFromPubKey(pubkey); } finally { lock.unlock(); } } /** * Mark the DeterministicKeys as used, if they match the pubkeyHash See {@link * com.google.bitcoin.wallet.DeterministicKeyChain#markKeyAsUsed(DeterministicKey)} for more info * on this. */ @Nullable public DeterministicKey markPubHashAsUsed(byte[] pubkeyHash) { lock.lock(); try { DeterministicKey k = (DeterministicKey) basicKeyChain.findKeyFromPubHash(pubkeyHash); if (k != null) markKeyAsUsed(k); return k; } finally { lock.unlock(); } } /** * Mark the DeterministicKeys as used, if they match the pubkey See {@link * com.google.bitcoin.wallet.DeterministicKeyChain#markKeyAsUsed(DeterministicKey)} for more info * on this. */ @Nullable public DeterministicKey markPubKeyAsUsed(byte[] pubkey) { lock.lock(); try { DeterministicKey k = (DeterministicKey) basicKeyChain.findKeyFromPubKey(pubkey); if (k != null) markKeyAsUsed(k); return k; } finally { lock.unlock(); } } @Override public boolean hasKey(ECKey key) { lock.lock(); try { return basicKeyChain.hasKey(key); } finally { lock.unlock(); } } /** Returns the deterministic key for the given absolute path in the hierarchy. */ protected DeterministicKey getKeyByPath(ChildNumber... path) { return getKeyByPath(ImmutableList.<ChildNumber>copyOf(path)); } /** Returns the deterministic key for the given absolute path in the hierarchy. */ protected DeterministicKey getKeyByPath(List<ChildNumber> path) { return getKeyByPath(path, false); } /** * Returns the deterministic key for the given absolute path in the hierarchy, optionally creating * it */ public DeterministicKey getKeyByPath(List<ChildNumber> path, boolean create) { return hierarchy.get(path, false, create); } /** * An alias for <code>getKeyByPath(DeterministicKeyChain.ACCOUNT_ZERO_PATH).getPubOnly()</code>. * Use this when you would like to create a watching key chain that follows this one, but can't * spend money from it. The returned key can be serialized and then passed into {@link * #watch(com.google.bitcoin.crypto.DeterministicKey)} on another system to watch the hierarchy. */ public DeterministicKey getWatchingKey() { return getKeyByPath(ACCOUNT_ZERO_PATH).getPubOnly(); } @Override public int numKeys() { // We need to return here the total number of keys including the lookahead zone, not the number // of keys we // have issued via getKey/freshReceiveKey. lock.lock(); try { maybeLookAhead(); return basicKeyChain.numKeys(); } finally { lock.unlock(); } } /** * Returns number of leaf keys used including both internal and external paths. This may be fewer * than the number that have been deserialized or held in memory, because of the lookahead zone. */ public int numLeafKeysIssued() { lock.lock(); try { return issuedExternalKeys + issuedInternalKeys; } finally { lock.unlock(); } } @Override public long getEarliestKeyCreationTime() { return seed != null ? seed.getCreationTimeSeconds() : creationTimeSeconds; } @Override public void addEventListener(KeyChainEventListener listener) { basicKeyChain.addEventListener(listener); } @Override public void addEventListener(KeyChainEventListener listener, Executor executor) { basicKeyChain.addEventListener(listener, executor); } @Override public boolean removeEventListener(KeyChainEventListener listener) { return basicKeyChain.removeEventListener(listener); } /** Returns a list of words that represent the seed or null if this chain is a watching chain. */ @Nullable public List<String> getMnemonicCode() { if (seed == null) return null; lock.lock(); try { return seed.getMnemonicCode(); } finally { lock.unlock(); } } /** Return true if this keychain is following another keychain */ public boolean isFollowing() { return isFollowing; } ////////////////////////////////////////////////////////////////////////////////////////////////////////////////// // // Serialization support // ////////////////////////////////////////////////////////////////////////////////////////////////////////////////// @Override public List<Protos.Key> serializeToProtobuf() { lock.lock(); try { // Most of the serialization work is delegated to the basic key chain, which will serialize // the bulk of the // data (handling encryption along the way), and letting us patch it up with the extra data we // care about. LinkedList<Protos.Key> entries = newLinkedList(); if (seed != null) { Protos.Key.Builder mnemonicEntry = BasicKeyChain.serializeEncryptableItem(seed); mnemonicEntry.setType(Protos.Key.Type.DETERMINISTIC_MNEMONIC); entries.add(mnemonicEntry.build()); } Map<ECKey, Protos.Key.Builder> keys = basicKeyChain.serializeToEditableProtobufs(); for (Map.Entry<ECKey, Protos.Key.Builder> entry : keys.entrySet()) { DeterministicKey key = (DeterministicKey) entry.getKey(); Protos.Key.Builder proto = entry.getValue(); proto.setType(Protos.Key.Type.DETERMINISTIC_KEY); final Protos.DeterministicKey.Builder detKey = proto.getDeterministicKeyBuilder(); detKey.setChainCode(ByteString.copyFrom(key.getChainCode())); for (ChildNumber num : key.getPath()) detKey.addPath(num.i()); if (key.equals(externalKey)) { detKey.setIssuedSubkeys(issuedExternalKeys); detKey.setLookaheadSize(lookaheadSize); } else if (key.equals(internalKey)) { detKey.setIssuedSubkeys(issuedInternalKeys); detKey.setLookaheadSize(lookaheadSize); } // Flag the very first key of following keychain. if (entries.isEmpty() && isFollowing()) { detKey.setIsFollowing(true); } if (key.getParent() != null) { // HD keys inherit the timestamp of their parent if they have one, so no need to serialize // it. proto.clearCreationTimestamp(); } entries.add(proto.build()); } return entries; } finally { lock.unlock(); } } /** * Returns all the key chains found in the given list of keys. Typically there will only be one, * but in the case of key rotation it can happen that there are multiple chains found. */ public static List<DeterministicKeyChain> fromProtobuf( List<Protos.Key> keys, @Nullable KeyCrypter crypter) throws UnreadableWalletException { List<DeterministicKeyChain> chains = newLinkedList(); DeterministicSeed seed = null; DeterministicKeyChain chain = null; int lookaheadSize = -1; for (Protos.Key key : keys) { final Protos.Key.Type t = key.getType(); if (t == Protos.Key.Type.DETERMINISTIC_MNEMONIC) { if (chain != null) { checkState(lookaheadSize >= 0); chain.setLookaheadSize(lookaheadSize); chain.maybeLookAhead(); chains.add(chain); chain = null; } long timestamp = key.getCreationTimestamp() / 1000; String passphrase = DEFAULT_PASSPHRASE_FOR_MNEMONIC; // FIXME allow non-empty passphrase if (key.hasSecretBytes()) { seed = new DeterministicSeed(key.getSecretBytes().toStringUtf8(), passphrase, timestamp); } else if (key.hasEncryptedData()) { EncryptedData data = new EncryptedData( key.getEncryptedData().getInitialisationVector().toByteArray(), key.getEncryptedData().getEncryptedPrivateKey().toByteArray()); seed = new DeterministicSeed(data, timestamp); } else { throw new UnreadableWalletException("Malformed key proto: " + key.toString()); } if (log.isDebugEnabled()) log.debug("Deserializing: DETERMINISTIC_MNEMONIC: {}", seed); } else if (t == Protos.Key.Type.DETERMINISTIC_KEY) { if (!key.hasDeterministicKey()) throw new UnreadableWalletException( "Deterministic key missing extra data: " + key.toString()); byte[] chainCode = key.getDeterministicKey().getChainCode().toByteArray(); // Deserialize the path through the tree. LinkedList<ChildNumber> path = newLinkedList(); for (int i : key.getDeterministicKey().getPathList()) path.add(new ChildNumber(i)); // Deserialize the public key and path. ECPoint pubkey = ECKey.CURVE.getCurve().decodePoint(key.getPublicKey().toByteArray()); final ImmutableList<ChildNumber> immutablePath = ImmutableList.copyOf(path); // Possibly create the chain, if we didn't already do so yet. boolean isWatchingAccountKey = false; boolean isFollowingKey = false; // save previous chain if any if the key is marked as following. Current key and the next // ones are to be // placed in new following key chain if (key.getDeterministicKey().getIsFollowing()) { if (chain != null) { checkState(lookaheadSize >= 0); chain.setLookaheadSize(lookaheadSize); chain.maybeLookAhead(); chains.add(chain); chain = null; seed = null; } isFollowingKey = true; } if (chain == null) { if (seed == null) { DeterministicKey accountKey = new DeterministicKey(immutablePath, chainCode, pubkey, null, null); if (!accountKey.getPath().equals(ACCOUNT_ZERO_PATH)) throw new UnreadableWalletException( "Expecting account key but found key with path: " + HDUtils.formatPath(accountKey.getPath())); chain = new DeterministicKeyChain(accountKey, isFollowingKey); isWatchingAccountKey = true; } else { chain = new DeterministicKeyChain(seed, crypter); chain.lookaheadSize = LAZY_CALCULATE_LOOKAHEAD; // If the seed is encrypted, then the chain is incomplete at this point. However, we // will load // it up below as we parse in the keys. We just need to check at the end that we've // loaded // everything afterwards. } } // Find the parent key assuming this is not the root key, and not an account key for a // watching chain. DeterministicKey parent = null; if (!path.isEmpty() && !isWatchingAccountKey) { ChildNumber index = path.removeLast(); parent = chain.hierarchy.get(path, false, false); path.add(index); } DeterministicKey detkey; if (key.hasSecretBytes()) { // Not encrypted: private key is available. final BigInteger priv = new BigInteger(1, key.getSecretBytes().toByteArray()); detkey = new DeterministicKey(immutablePath, chainCode, pubkey, priv, parent); } else { if (key.hasEncryptedData()) { Protos.EncryptedData proto = key.getEncryptedData(); EncryptedData data = new EncryptedData( proto.getInitialisationVector().toByteArray(), proto.getEncryptedPrivateKey().toByteArray()); checkNotNull(crypter, "Encountered an encrypted key but no key crypter provided"); detkey = new DeterministicKey(immutablePath, chainCode, crypter, pubkey, data, parent); } else { // No secret key bytes and key is not encrypted: either a watching key or private key // bytes // will be rederived on the fly from the parent. detkey = new DeterministicKey(immutablePath, chainCode, pubkey, null, parent); } } if (key.hasCreationTimestamp()) detkey.setCreationTimeSeconds(key.getCreationTimestamp() / 1000); if (log.isDebugEnabled()) log.debug("Deserializing: DETERMINISTIC_KEY: {}", detkey); if (!isWatchingAccountKey) { // If the non-encrypted case, the non-leaf keys (account, internal, external) have already // been // rederived and inserted at this point and the two lines below are just a no-op. In the // encrypted // case though, we can't rederive and we must reinsert, potentially building the heirarchy // object // if need be. if (path.size() == 0) { // Master key. chain.rootKey = detkey; chain.hierarchy = new DeterministicHierarchy(detkey); } else if (path.size() == 2) { if (detkey.getChildNumber().num() == 0) { chain.externalKey = detkey; chain.issuedExternalKeys = key.getDeterministicKey().getIssuedSubkeys(); lookaheadSize = Math.max(lookaheadSize, key.getDeterministicKey().getLookaheadSize()); } else if (detkey.getChildNumber().num() == 1) { chain.internalKey = detkey; chain.issuedInternalKeys = key.getDeterministicKey().getIssuedSubkeys(); } } } chain.hierarchy.putKey(detkey); chain.basicKeyChain.importKey(detkey); } } if (chain != null) { checkState(lookaheadSize >= 0); chain.setLookaheadSize(lookaheadSize); chain.maybeLookAhead(); chains.add(chain); } return chains; } ////////////////////////////////////////////////////////////////////////////////////////////////////////////////// // // Encryption support // ////////////////////////////////////////////////////////////////////////////////////////////////////////////////// @Override public DeterministicKeyChain toEncrypted(CharSequence password) { checkNotNull(password); checkArgument(password.length() > 0); checkState(seed != null, "Attempt to encrypt a watching chain."); checkState(!seed.isEncrypted()); KeyCrypter scrypt = new KeyCrypterScrypt(); KeyParameter derivedKey = scrypt.deriveKey(password); return toEncrypted(scrypt, derivedKey); } @Override public DeterministicKeyChain toEncrypted(KeyCrypter keyCrypter, KeyParameter aesKey) { return new DeterministicKeyChain(keyCrypter, aesKey, this); } @Override public DeterministicKeyChain toDecrypted(CharSequence password) { checkNotNull(password); checkArgument(password.length() > 0); KeyCrypter crypter = getKeyCrypter(); checkState(crypter != null, "Chain not encrypted"); KeyParameter derivedKey = crypter.deriveKey(password); return toDecrypted(derivedKey); } @Override public DeterministicKeyChain toDecrypted(KeyParameter aesKey) { checkState(getKeyCrypter() != null, "Key chain not encrypted"); checkState(seed != null, "Can't decrypt a watching chain"); checkState(seed.isEncrypted()); String passphrase = DEFAULT_PASSPHRASE_FOR_MNEMONIC; // FIXME allow non-empty passphrase DeterministicSeed decSeed = seed.decrypt(getKeyCrypter(), passphrase, aesKey); DeterministicKeyChain chain = new DeterministicKeyChain(decSeed); // Now double check that the keys match to catch the case where the key is wrong but padding // didn't catch it. if (!chain.getWatchingKey().getPubKeyPoint().equals(getWatchingKey().getPubKeyPoint())) throw new KeyCrypterException("Provided AES key is wrong"); chain.lookaheadSize = lookaheadSize; // Now copy the (pubkey only) leaf keys across to avoid rederiving them. The private key bytes // are missing // anyway so there's nothing to decrypt. for (ECKey eckey : basicKeyChain.getKeys()) { DeterministicKey key = (DeterministicKey) eckey; if (key.getPath().size() != 3) continue; // Not a leaf key. checkState(key.isEncrypted()); DeterministicKey parent = chain.hierarchy.get(checkNotNull(key.getParent()).getPath(), false, false); // Clone the key to the new decrypted hierarchy. key = new DeterministicKey(key.getPubOnly(), parent); chain.hierarchy.putKey(key); chain.basicKeyChain.importKey(key); } chain.issuedExternalKeys = issuedExternalKeys; chain.issuedInternalKeys = issuedInternalKeys; return chain; } @Override public boolean checkPassword(CharSequence password) { checkNotNull(password); checkState(getKeyCrypter() != null, "Key chain not encrypted"); return checkAESKey(getKeyCrypter().deriveKey(password)); } @Override public boolean checkAESKey(KeyParameter aesKey) { checkState(rootKey != null, "Can't check password for a watching chain"); checkNotNull(aesKey); checkState(getKeyCrypter() != null, "Key chain not encrypted"); try { return rootKey.decrypt(aesKey).getPubKeyPoint().equals(rootKey.getPubKeyPoint()); } catch (KeyCrypterException e) { return false; } } @Nullable @Override public KeyCrypter getKeyCrypter() { return basicKeyChain.getKeyCrypter(); } ////////////////////////////////////////////////////////////////////////////////////////////////////////////////// // // Bloom filtering support // ////////////////////////////////////////////////////////////////////////////////////////////////////////////////// @Override public int numBloomFilterEntries() { return numKeys() * 2; } @Override public BloomFilter getFilter(int size, double falsePositiveRate, long tweak) { lock.lock(); try { checkArgument(size >= numBloomFilterEntries()); maybeLookAhead(); return basicKeyChain.getFilter(size, falsePositiveRate, tweak); } finally { lock.unlock(); } } /** * The number of public keys we should pre-generate on each path before they are requested by the * app. This is required so that when scanning through the chain given only a seed, we can give * enough keys to the remote node via the Bloom filter such that we see transactions that are * "from the future", for example transactions created by a different app that's sharing the same * seed, or transactions we made before but we're replaying the chain given just the seed. The * default is 100. */ public int getLookaheadSize() { lock.lock(); try { return lookaheadSize; } finally { lock.unlock(); } } /** * Sets a new lookahead size. See {@link #getLookaheadSize()} for details on what this is. Setting * a new size that's larger than the current size will return immediately and the new size will * only take effect next time a fresh filter is requested (e.g. due to a new peer being * connected). So you should set this before starting to sync the chain, if you want to modify it. * If you haven't modified the lookahead threshold manually then it will be automatically set to * be a third of the new size. */ public void setLookaheadSize(int lookaheadSize) { lock.lock(); try { boolean readjustThreshold = this.lookaheadThreshold == calcDefaultLookaheadThreshold(); this.lookaheadSize = lookaheadSize; if (readjustThreshold) this.lookaheadThreshold = calcDefaultLookaheadThreshold(); } finally { lock.unlock(); } } /** * Sets the threshold for the key pre-generation. This is used to avoid adding new keys and thus * re-calculating Bloom filters every time a new key is calculated. Without a lookahead threshold, * every time we received a relevant transaction we'd extend the lookahead zone and generate a new * filter, which is inefficient. */ public void setLookaheadThreshold(int num) { lock.lock(); try { if (num >= lookaheadSize) throw new IllegalArgumentException("Threshold larger or equal to the lookaheadSize"); this.lookaheadThreshold = num; } finally { lock.unlock(); } } /** * Gets the threshold for the key pre-generation. See {@link #setLookaheadThreshold(int)} for * details on what this is. */ public int getLookaheadThreshold() { lock.lock(); try { if (lookaheadThreshold >= lookaheadSize) return 0; return lookaheadThreshold; } finally { lock.unlock(); } } /** * Pre-generate enough keys to reach the lookahead size. You can call this if you need to * explicitly invoke the lookahead procedure, but it's normally unnecessary as it will be done * automatically when needed. */ public void maybeLookAhead() { lock.lock(); try { List<DeterministicKey> keys = maybeLookAhead(externalKey, issuedExternalKeys); keys.addAll(maybeLookAhead(internalKey, issuedInternalKeys)); // Batch add all keys at once so there's only one event listener invocation, as this will be // listened to // by the wallet and used to rebuild/broadcast the Bloom filter. That's expensive so we don't // want to do // it more often than necessary. basicKeyChain.importKeys(keys); } finally { lock.unlock(); } } private List<DeterministicKey> maybeLookAhead(DeterministicKey parent, int issued) { checkState(lock.isHeldByCurrentThread()); return maybeLookAhead(parent, issued, getLookaheadSize(), getLookaheadThreshold()); } /** * Pre-generate enough keys to reach the lookahead size, but only if there are more than the * lookaheadThreshold to be generated, so that the Bloom filter does not have to be regenerated * that often. * * <p>The returned mutable list of keys must be inserted into the basic key chain. */ private List<DeterministicKey> maybeLookAhead( DeterministicKey parent, int issued, int lookaheadSize, int lookaheadThreshold) { checkState(lock.isHeldByCurrentThread()); final int numChildren = hierarchy.getNumChildren(parent.getPath()); final int needed = issued + lookaheadSize + lookaheadThreshold - numChildren; if (needed <= lookaheadThreshold) return new ArrayList<DeterministicKey>(); log.info( "{} keys needed for {} = {} issued + {} lookahead size + {} lookahead threshold - {} num children", needed, parent.getPathAsString(), issued, lookaheadSize, lookaheadThreshold, numChildren); List<DeterministicKey> result = new ArrayList<DeterministicKey>(needed); long now = System.currentTimeMillis(); int nextChild = numChildren; for (int i = 0; i < needed; i++) { DeterministicKey key = HDKeyDerivation.deriveThisOrNextChildKey(parent, nextChild); key = key.getPubOnly(); hierarchy.putKey(key); result.add(key); nextChild = key.getChildNumber().num() + 1; } log.info("Took {} msec", System.currentTimeMillis() - now); return result; } /** * Returns number of keys used on external path. This may be fewer than the number that have been * deserialized or held in memory, because of the lookahead zone. */ public int getIssuedExternalKeys() { lock.lock(); try { return issuedExternalKeys; } finally { lock.unlock(); } } /** * Returns number of keys used on internal path. This may be fewer than the number that have been * deserialized or held in memory, because of the lookahead zone. */ public int getIssuedInternalKeys() { lock.lock(); try { return issuedInternalKeys; } finally { lock.unlock(); } } /** Returns the seed or null if this chain is a watching chain. */ @Nullable public DeterministicSeed getSeed() { lock.lock(); try { return seed; } finally { lock.unlock(); } } // For internal usage only /* package */ List<ECKey> getKeys(boolean includeLookahead) { List<ECKey> keys = basicKeyChain.getKeys(); if (!includeLookahead) { int treeSize = internalKey.getPath().size(); List<ECKey> issuedKeys = new LinkedList<ECKey>(); for (ECKey key : keys) { DeterministicKey detkey = (DeterministicKey) key; DeterministicKey parent = detkey.getParent(); if (parent == null) continue; if (detkey.getPath().size() <= treeSize) continue; if (parent.equals(internalKey) && detkey.getChildNumber().i() > issuedInternalKeys) continue; if (parent.equals(externalKey) && detkey.getChildNumber().i() > issuedExternalKeys) continue; issuedKeys.add(detkey); } return issuedKeys; } return keys; } /** Returns leaf keys issued by this chain (including lookahead zone) */ public List<DeterministicKey> getLeafKeys() { ImmutableList.Builder<DeterministicKey> keys = ImmutableList.builder(); for (ECKey key : getKeys(true)) { DeterministicKey dKey = (DeterministicKey) key; if (dKey.getPath().size() > 2) { keys.add(dKey); } } return keys.build(); } }
/** * This class maintains a set of {@link StoredClientChannel}s, automatically (re)broadcasting the * contract transaction and broadcasting the refund transaction over the given {@link * TransactionBroadcaster}. */ public class StoredPaymentChannelClientStates implements WalletExtension { private static final Logger log = LoggerFactory.getLogger(StoredPaymentChannelClientStates.class); static final String EXTENSION_ID = StoredPaymentChannelClientStates.class.getName(); @GuardedBy("lock") @VisibleForTesting final HashMultimap<Sha256Hash, StoredClientChannel> mapChannels = HashMultimap.create(); @VisibleForTesting final Timer channelTimeoutHandler = new Timer(true); private Wallet containingWallet; private final TransactionBroadcaster announcePeerGroup; protected final ReentrantLock lock = Threading.lock("StoredPaymentChannelClientStates"); /** * Creates a new StoredPaymentChannelClientStates and associates it with the given {@link Wallet} * and {@link TransactionBroadcaster} which are used to complete and announce contract and refund * transactions. */ public StoredPaymentChannelClientStates( Wallet containingWallet, TransactionBroadcaster announcePeerGroup) { this.announcePeerGroup = checkNotNull(announcePeerGroup); this.containingWallet = checkNotNull(containingWallet); } /** Returns this extension from the given wallet, or null if no such extension was added. */ @Nullable public static StoredPaymentChannelClientStates getFromWallet(Wallet wallet) { return (StoredPaymentChannelClientStates) wallet.getExtensions().get(EXTENSION_ID); } /** * Returns the outstanding amount of money sent back to us for all channels to this server added * together. */ public BigInteger getBalanceForServer(Sha256Hash id) { BigInteger balance = BigInteger.ZERO; lock.lock(); try { Set<StoredClientChannel> setChannels = mapChannels.get(id); for (StoredClientChannel channel : setChannels) { synchronized (channel) { if (channel.close != null) continue; balance = balance.add(channel.valueToMe); } } return balance; } finally { lock.unlock(); } } /** * Returns the number of seconds from now until this servers next channel will expire, or zero if * no unexpired channels found. */ public long getSecondsUntilExpiry(Sha256Hash id) { lock.lock(); try { final Set<StoredClientChannel> setChannels = mapChannels.get(id); final long nowSeconds = Utils.currentTimeSeconds(); int earliestTime = Integer.MAX_VALUE; for (StoredClientChannel channel : setChannels) { synchronized (channel) { if (channel.expiryTimeSeconds() > nowSeconds) earliestTime = Math.min(earliestTime, (int) channel.expiryTimeSeconds()); } } return earliestTime == Integer.MAX_VALUE ? 0 : earliestTime - nowSeconds; } finally { lock.unlock(); } } /** Finds an inactive channel with the given id and returns it, or returns null. */ @Nullable StoredClientChannel getUsableChannelForServerID(Sha256Hash id) { lock.lock(); try { Set<StoredClientChannel> setChannels = mapChannels.get(id); for (StoredClientChannel channel : setChannels) { synchronized (channel) { // Check if the channel is usable (has money, inactive) and if so, activate it. log.info( "Considering channel {} contract {}", channel.hashCode(), channel.contract.getHash()); if (channel.close != null || channel.valueToMe.equals(BigInteger.ZERO)) { log.info(" ... but is closed or empty"); continue; } if (!channel.active) { log.info(" ... activating"); channel.active = true; return channel; } log.info(" ... but is already active"); } } } finally { lock.unlock(); } return null; } /** Finds a channel with the given id and contract hash and returns it, or returns null. */ @Nullable StoredClientChannel getChannel(Sha256Hash id, Sha256Hash contractHash) { lock.lock(); try { Set<StoredClientChannel> setChannels = mapChannels.get(id); for (StoredClientChannel channel : setChannels) { if (channel.contract.getHash().equals(contractHash)) return channel; } return null; } finally { lock.unlock(); } } /** * Adds the given channel to this set of stored states, broadcasting the contract and refund * transactions when the channel expires and notifies the wallet of an update to this wallet * extension */ void putChannel(final StoredClientChannel channel) { putChannel(channel, true); } // Adds this channel and optionally notifies the wallet of an update to this extension (used // during deserialize) private void putChannel(final StoredClientChannel channel, boolean updateWallet) { lock.lock(); try { mapChannels.put(channel.id, channel); channelTimeoutHandler.schedule( new TimerTask() { @Override public void run() { removeChannel(channel); announcePeerGroup.broadcastTransaction(channel.contract); announcePeerGroup.broadcastTransaction(channel.refund); } // Add the difference between real time and Utils.now() so that test-cases can use a // mock clock. }, new Date( channel.expiryTimeSeconds() * 1000 + (System.currentTimeMillis() - Utils.currentTimeMillis()))); } finally { lock.unlock(); } if (updateWallet) containingWallet.addOrUpdateExtension(this); } /** * Removes the channel with the given id from this set of stored states and notifies the wallet of * an update to this wallet extension. * * <p>Note that the channel will still have its contract and refund transactions broadcast via the * connected {@link TransactionBroadcaster} as long as this {@link * StoredPaymentChannelClientStates} continues to exist in memory. */ void removeChannel(StoredClientChannel channel) { lock.lock(); try { mapChannels.remove(channel.id, channel); } finally { lock.unlock(); } containingWallet.addOrUpdateExtension(this); } @Override public String getWalletExtensionID() { return EXTENSION_ID; } @Override public boolean isWalletExtensionMandatory() { return false; } @Override public byte[] serializeWalletExtension() { lock.lock(); try { ClientState.StoredClientPaymentChannels.Builder builder = ClientState.StoredClientPaymentChannels.newBuilder(); for (StoredClientChannel channel : mapChannels.values()) { // First a few asserts to make sure things won't break checkState( channel.valueToMe.signum() >= 0 && channel.valueToMe.compareTo(NetworkParameters.MAX_MONEY) < 0); checkState( channel.refundFees.signum() >= 0 && channel.refundFees.compareTo(NetworkParameters.MAX_MONEY) < 0); checkNotNull(channel.myKey.getPrivKeyBytes()); checkState(channel.refund.getConfidence().getSource() == TransactionConfidence.Source.SELF); final ClientState.StoredClientPaymentChannel.Builder value = ClientState.StoredClientPaymentChannel.newBuilder() .setId(ByteString.copyFrom(channel.id.getBytes())) .setContractTransaction(ByteString.copyFrom(channel.contract.bitcoinSerialize())) .setRefundTransaction(ByteString.copyFrom(channel.refund.bitcoinSerialize())) .setMyKey(ByteString.copyFrom(channel.myKey.getPrivKeyBytes())) .setValueToMe(channel.valueToMe.longValue()) .setRefundFees(channel.refundFees.longValue()); if (channel.close != null) value.setCloseTransactionHash(ByteString.copyFrom(channel.close.getHash().getBytes())); builder.addChannels(value); } return builder.build().toByteArray(); } finally { lock.unlock(); } } @Override public void deserializeWalletExtension(Wallet containingWallet, byte[] data) throws Exception { lock.lock(); try { checkState(this.containingWallet == null || this.containingWallet == containingWallet); this.containingWallet = containingWallet; NetworkParameters params = containingWallet.getParams(); ClientState.StoredClientPaymentChannels states = ClientState.StoredClientPaymentChannels.parseFrom(data); for (ClientState.StoredClientPaymentChannel storedState : states.getChannelsList()) { Transaction refundTransaction = new Transaction(params, storedState.getRefundTransaction().toByteArray()); refundTransaction.getConfidence().setSource(TransactionConfidence.Source.SELF); StoredClientChannel channel = new StoredClientChannel( new Sha256Hash(storedState.getId().toByteArray()), new Transaction(params, storedState.getContractTransaction().toByteArray()), refundTransaction, new ECKey(new BigInteger(1, storedState.getMyKey().toByteArray()), null, true), BigInteger.valueOf(storedState.getValueToMe()), BigInteger.valueOf(storedState.getRefundFees()), false); if (storedState.hasCloseTransactionHash()) channel.close = containingWallet.getTransaction(new Sha256Hash(storedState.toByteArray())); putChannel(channel, false); } } finally { lock.unlock(); } } @Override public String toString() { lock.lock(); try { StringBuilder buf = new StringBuilder("Client payment channel states:\n"); for (StoredClientChannel channel : mapChannels.values()) buf.append(" ").append(channel).append("\n"); return buf.toString(); } finally { lock.unlock(); } } }