diff --git a/core/src/main/java/com/google/bitcoin/wallet/DeterministicKeyChain.java b/core/src/main/java/com/google/bitcoin/wallet/DeterministicKeyChain.java index 777c83764..034de0b37 100644 --- a/core/src/main/java/com/google/bitcoin/wallet/DeterministicKeyChain.java +++ b/core/src/main/java/com/google/bitcoin/wallet/DeterministicKeyChain.java @@ -323,7 +323,7 @@ public class DeterministicKeyChain implements EncryptableKeyChain { /** 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); + return getKeys(purpose, 1).get(0); } /** Returns freshly derived key/s that have not been returned by this method before. */ @@ -353,12 +353,23 @@ public class DeterministicKeyChain implements EncryptableKeyChain { default: throw new UnsupportedOperationException(); } - List lookahead = maybeLookAhead(parentKey, index); + // 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 lookahead = maybeLookAhead(parentKey, index, 0, 0); basicKeyChain.importKeys(lookahead); List keys = new ArrayList(numberOfKeys); - for (int i = 0; i < numberOfKeys; i++) { - keys.add(hierarchy.get(HDUtils.append(parentKey.getPath(), new ChildNumber(index - numberOfKeys + i, false)), false, false)); + ImmutableList path = HDUtils.append(parentKey.getPath(), new ChildNumber(index - numberOfKeys + i, false)); + keys.add(hierarchy.get(path, false, false)); } return keys; } finally { @@ -905,8 +916,11 @@ public class DeterministicKeyChain implements EncryptableKeyChain { } } - // Pre-generate enough keys to reach the lookahead size. - private void maybeLookAhead() { + /** + * 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 keys = maybeLookAhead(externalKey, issuedExternalKeys); @@ -920,17 +934,20 @@ public class DeterministicKeyChain implements EncryptableKeyChain { } } + private List 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. * * The returned mutable list of keys must be inserted into the basic key chain. */ - private List maybeLookAhead(DeterministicKey parent, int issued) { + private List maybeLookAhead(DeterministicKey parent, int issued, int lookaheadSize, int lookaheadThreshold) { checkState(lock.isHeldByCurrentThread()); final int numChildren = hierarchy.getNumChildren(parent.getPath()); - final int lookaheadSize = getLookaheadSize(); - final int lookaheadThreshold = getLookaheadThreshold(); final int needed = issued + lookaheadSize + lookaheadThreshold - numChildren; if (needed <= lookaheadThreshold) diff --git a/core/src/main/java/com/google/bitcoin/wallet/KeyChainGroup.java b/core/src/main/java/com/google/bitcoin/wallet/KeyChainGroup.java index a5781e77d..13e431c37 100644 --- a/core/src/main/java/com/google/bitcoin/wallet/KeyChainGroup.java +++ b/core/src/main/java/com/google/bitcoin/wallet/KeyChainGroup.java @@ -185,6 +185,7 @@ public class KeyChainGroup implements KeyBag { for (DeterministicKeyChain chain : chains) { if (isMarried(chain)) { + chain.maybeLookAhead(); for (DeterministicKey followedKey : chain.getLeafKeys()) { RedeemData redeemData = getRedeemData(followedKey, chain.getWatchingKey()); Script scriptPubKey = ScriptBuilder.createP2SHOutputScript(redeemData.redeemScript); @@ -319,6 +320,7 @@ public class KeyChainGroup implements KeyBag { private List getMarriedKeysWithFollowed(DeterministicKey followedKey, Collection followingChains) { ImmutableList.Builder keys = ImmutableList.builder(); for (DeterministicKeyChain keyChain : followingChains) { + keyChain.maybeLookAhead(); keys.add(keyChain.getKeyByPath(followedKey.getPath())); } keys.add(followedKey); @@ -611,6 +613,7 @@ public class KeyChainGroup implements KeyBag { int result = basic.numBloomFilterEntries(); for (DeterministicKeyChain chain : chains) { if (isMarried(chain)) { + chain.maybeLookAhead(); result += chain.getLeafKeys().size() * 2; } else { result += chain.numBloomFilterEntries(); diff --git a/core/src/test/java/com/google/bitcoin/core/WalletTest.java b/core/src/test/java/com/google/bitcoin/core/WalletTest.java index cc3ecad2e..5313d51c1 100644 --- a/core/src/test/java/com/google/bitcoin/core/WalletTest.java +++ b/core/src/test/java/com/google/bitcoin/core/WalletTest.java @@ -2456,7 +2456,7 @@ public class WalletTest extends TestWithWallet { } }, Threading.SAME_THREAD); wallet.freshReceiveKey(); - assertEquals(7, keys.size()); + assertEquals(1, keys.size()); } @Test diff --git a/core/src/test/java/com/google/bitcoin/wallet/DeterministicKeyChainTest.java b/core/src/test/java/com/google/bitcoin/wallet/DeterministicKeyChainTest.java index 80914fdfe..86f851e1c 100644 --- a/core/src/test/java/com/google/bitcoin/wallet/DeterministicKeyChainTest.java +++ b/core/src/test/java/com/google/bitcoin/wallet/DeterministicKeyChainTest.java @@ -95,17 +95,23 @@ public class DeterministicKeyChainTest { ECKey key = chain.getKey(KeyChain.KeyPurpose.CHANGE); assertEquals(1, listenerKeys.size()); // 1 event final List firstEvent = listenerKeys.get(0); - assertEquals(7, firstEvent.size()); // 5 lookahead keys, +1 lookahead threhsold, +1 to satisfy the request. + assertEquals(1, firstEvent.size()); assertTrue(firstEvent.contains(key)); // order is not specified. listenerKeys.clear(); + + chain.maybeLookAhead(); + final List secondEvent = listenerKeys.get(0); + assertEquals(12, secondEvent.size()); // (5 lookahead keys, +1 lookahead threshold) * 2 chains + listenerKeys.clear(); + chain.getKey(KeyChain.KeyPurpose.CHANGE); // At this point we've entered the threshold zone so more keys won't immediately trigger more generations. assertEquals(0, listenerKeys.size()); // 1 event - final int lookaheadThreshold = chain.getLookaheadThreshold(); + final int lookaheadThreshold = chain.getLookaheadThreshold() + chain.getLookaheadSize(); for (int i = 0; i < lookaheadThreshold; i++) chain.getKey(KeyChain.KeyPurpose.CHANGE); assertEquals(1, listenerKeys.size()); // 1 event - assertEquals(lookaheadThreshold + 1, listenerKeys.get(0).size()); // 1 key. + assertEquals(1, listenerKeys.get(0).size()); // 1 key. } @Test @@ -120,13 +126,20 @@ public class DeterministicKeyChainTest { @Test public void serializeUnencrypted() throws UnreadableWalletException { + chain.maybeLookAhead(); DeterministicKey key1 = chain.getKey(KeyChain.KeyPurpose.RECEIVE_FUNDS); DeterministicKey key2 = chain.getKey(KeyChain.KeyPurpose.RECEIVE_FUNDS); DeterministicKey key3 = chain.getKey(KeyChain.KeyPurpose.CHANGE); - List keys = chain.serializeToProtobuf(); // 1 root seed, 1 master key, 1 account key, 2 internal keys, 3 derived, 20 lookahead and 5 lookahead threshold. - assertEquals(33, keys.size()); + int numItems = + 1 // root seed + + 1 // master key + + 1 // account key + + 2 // ext/int parent keys + + (chain.getLookaheadSize() + chain.getLookaheadThreshold()) * 2 // lookahead zone on each chain + ; + assertEquals(numItems, keys.size()); // Get another key that will be lost during round-tripping, to ensure we can derive it again. DeterministicKey key4 = chain.getKey(KeyChain.KeyPurpose.CHANGE); @@ -224,6 +237,7 @@ public class DeterministicKeyChainTest { chain = DeterministicKeyChain.watch(watchingKey); assertEquals(DeterministicHierarchy.BIP32_STANDARDISATION_TIME_SECS, chain.getEarliestKeyCreationTime()); chain.setLookaheadSize(10); + chain.maybeLookAhead(); assertEquals(key1.getPubKeyPoint(), chain.getKey(KeyChain.KeyPurpose.RECEIVE_FUNDS).getPubKeyPoint()); assertEquals(key2.getPubKeyPoint(), chain.getKey(KeyChain.KeyPurpose.RECEIVE_FUNDS).getPubKeyPoint()); @@ -255,11 +269,11 @@ public class DeterministicKeyChainTest { public void bloom1() { DeterministicKey key2 = chain.getKey(KeyChain.KeyPurpose.RECEIVE_FUNDS); DeterministicKey key1 = chain.getKey(KeyChain.KeyPurpose.RECEIVE_FUNDS); - // ((13*2)+2+3)*2 + int numEntries = (((chain.getLookaheadSize() + chain.getLookaheadThreshold()) * 2) // * 2 because of internal/external + chain.numLeafKeysIssued() - + 3 // one account key + two chain keys (internal/external) + + 4 // one root key + one account key + two chain keys (internal/external) ) * 2; // because the filter contains keys and key hashes. assertEquals(numEntries, chain.numBloomFilterEntries()); BloomFilter filter = chain.getFilter(numEntries, 0.001, 1); diff --git a/core/src/test/java/com/google/bitcoin/wallet/KeyChainGroupTest.java b/core/src/test/java/com/google/bitcoin/wallet/KeyChainGroupTest.java index abe010d33..8e3205f55 100644 --- a/core/src/test/java/com/google/bitcoin/wallet/KeyChainGroupTest.java +++ b/core/src/test/java/com/google/bitcoin/wallet/KeyChainGroupTest.java @@ -173,9 +173,10 @@ public class KeyChainGroupTest { Address a2 = group.freshAddress(KeyChain.KeyPurpose.RECEIVE_FUNDS); assertTrue(a1.isP2SHAddress()); assertNotEquals(a1, a2); + group.getBloomFilterElementCount(); assertEquals(((group.getLookaheadSize() + group.getLookaheadThreshold()) * 2) // * 2 because of internal/external - + 2 // keys issued - + 3, group.numKeys()); + + (2 - group.getLookaheadThreshold()) // keys issued + + 4 /* master, account, int, ext */, group.numKeys()); Address a3 = group.currentAddress(KeyChain.KeyPurpose.RECEIVE_FUNDS); assertEquals(a2, a3); @@ -310,7 +311,7 @@ public class KeyChainGroupTest { // We ran ahead of the lookahead buffer. assertFalse(filter.contains(group.freshKey(KeyChain.KeyPurpose.RECEIVE_FUNDS).getPubKey())); group.importKeys(key2); - filter = group.getBloomFilter(group.getBloomFilterElementCount(), 0.001, (long)(Math.random() * Long.MAX_VALUE)); + filter = group.getBloomFilter(group.getBloomFilterElementCount(), 0.001, (long) (Math.random() * Long.MAX_VALUE)); assertTrue(filter.contains(key1.getPubKeyHash())); assertTrue(filter.contains(key1.getPubKey())); assertTrue(filter.contains(key2.getPubKey())); @@ -321,10 +322,12 @@ public class KeyChainGroupTest { group = createMarriedKeyChainGroup(); Address address = group.freshAddress(KeyChain.KeyPurpose.RECEIVE_FUNDS); assertTrue(group.findRedeemDataFromScriptHash(address.getHash160()) != null); + group.getBloomFilterElementCount(); KeyChainGroup group2 = createMarriedKeyChainGroup(); group2.freshAddress(KeyChain.KeyPurpose.RECEIVE_FUNDS); + group2.getBloomFilterElementCount(); // Force lookahead. // test address from lookahead zone and lookahead threshold zone - for (int i = 0; i < LOOKAHEAD_SIZE + group.getLookaheadThreshold(); i++) { + for (int i = 0; i < group.getLookaheadSize() + group.getLookaheadThreshold(); i++) { address = group.freshAddress(KeyChain.KeyPurpose.RECEIVE_FUNDS); assertTrue(group2.findRedeemDataFromScriptHash(address.getHash160()) != null); } @@ -334,21 +337,21 @@ public class KeyChainGroupTest { @Test public void bloomFilterForMarriedChains() throws Exception { group = createMarriedKeyChainGroup(); - // only leaf keys are used for populating bloom filter, so initial number is zero - assertEquals(0, group.getBloomFilterElementCount()); + int bufferSize = group.getLookaheadSize() + group.getLookaheadThreshold(); + int expected = bufferSize * 2 /* chains */ * 2 /* elements */; + assertEquals(expected, group.getBloomFilterElementCount()); Address address1 = group.freshAddress(KeyChain.KeyPurpose.RECEIVE_FUNDS); - final int size = (LOOKAHEAD_SIZE + group.getLookaheadThreshold() + 1 /* for the just created key */) * 2; - assertEquals(size, group.getBloomFilterElementCount()); - BloomFilter filter = group.getBloomFilter(size, 0.001, (long)(Math.random() * Long.MAX_VALUE)); + assertEquals(expected, group.getBloomFilterElementCount()); + BloomFilter filter = group.getBloomFilter(expected + 2, 0.001, (long)(Math.random() * Long.MAX_VALUE)); assertTrue(filter.contains(address1.getHash160())); Address address2 = group.freshAddress(KeyChain.KeyPurpose.CHANGE); - assertFalse(filter.contains(address2.getHash160())); + assertTrue(filter.contains(address2.getHash160())); // Check that the filter contains the lookahead buffer. - for (int i = 0; i < LOOKAHEAD_SIZE + group.getLookaheadThreshold(); i++) { + for (int i = 0; i < bufferSize - 1 /* issued address */; i++) { Address address = group.freshAddress(KeyChain.KeyPurpose.RECEIVE_FUNDS); - assertTrue(filter.contains(address.getHash160())); + assertTrue("key " + i, filter.contains(address.getHash160())); } // We ran ahead of the lookahead buffer. assertFalse(filter.contains(group.freshAddress(KeyChain.KeyPurpose.RECEIVE_FUNDS).getHash160())); @@ -401,6 +404,7 @@ public class KeyChainGroupTest { group.freshKey(KeyChain.KeyPurpose.RECEIVE_FUNDS); DeterministicKey key1 = group.freshKey(KeyChain.KeyPurpose.RECEIVE_FUNDS); DeterministicKey key2 = group.freshKey(KeyChain.KeyPurpose.CHANGE); + group.getBloomFilterElementCount(); List protoKeys1 = group.serializeToProtobuf(); assertEquals(INITIAL_KEYS + ((LOOKAHEAD_SIZE + 1) * 2) + 1 /* for the seed */ + 1, protoKeys1.size()); group.importKeys(new ECKey()); @@ -436,10 +440,11 @@ public class KeyChainGroupTest { group.setLookaheadSize(LOOKAHEAD_SIZE); group.freshKey(KeyChain.KeyPurpose.RECEIVE_FUNDS); group.freshKey(KeyChain.KeyPurpose.CHANGE); + group.getBloomFilterElementCount(); // Force lookahead. List protoKeys1 = group.serializeToProtobuf(); - assertEquals(3 + (LOOKAHEAD_SIZE + group.getLookaheadThreshold() + 1) * 2, protoKeys1.size()); + assertEquals(3 + (group.getLookaheadSize() + group.getLookaheadThreshold() + 1) * 2, protoKeys1.size()); group = KeyChainGroup.fromProtobufUnencrypted(params, protoKeys1); - assertEquals(3 + (LOOKAHEAD_SIZE + group.getLookaheadThreshold() + 1) * 2, group.serializeToProtobuf().size()); + assertEquals(3 + (group.getLookaheadSize() + group.getLookaheadThreshold() + 1) * 2, group.serializeToProtobuf().size()); } @Test diff --git a/core/src/test/resources/com/google/bitcoin/wallet/deterministic-wallet-serialization.txt b/core/src/test/resources/com/google/bitcoin/wallet/deterministic-wallet-serialization.txt index 0d8731928..ae095fad8 100644 --- a/core/src/test/resources/com/google/bitcoin/wallet/deterministic-wallet-serialization.txt +++ b/core/src/test/resources/com/google/bitcoin/wallet/deterministic-wallet-serialization.txt @@ -157,15 +157,6 @@ deterministic_key { path: 12 } -type: DETERMINISTIC_KEY -public_key: "\003?\v\222\341\321\"@\2624\336H\221\2570bC\251\377\000vm\032\357U\250Dl:\200\020Q-" -deterministic_key { - chain_code: "\205R\372F\245\232*\0351Y??\321\362I\222ne\277H+\304\rL\234\313\016|\a\372a\a" - path: 2147483648 - path: 0 - path: 13 -} - type: DETERMINISTIC_KEY public_key: "\002\225b\3515\202\233\335\320.7\265\274uh\230N\242\254\317J\364\331\2345\220)\362\334\216\202\\" deterministic_key { @@ -281,13 +272,4 @@ deterministic_key { path: 2147483648 path: 1 path: 12 -} - -type: DETERMINISTIC_KEY -public_key: "\003\301\302\254\214C}\362f\315GV\033]\257\a\231\t[\001=\0046\213\220\341S\324\266\202&\206N" -deterministic_key { - chain_code: "\272\\\225\354.UQ8\264\346\a\310h\350\031\227\024c\340\337;W7\f\322\301\304\232P\360\373\035" - path: 2147483648 - path: 1 - path: 13 } \ No newline at end of file diff --git a/core/src/test/resources/com/google/bitcoin/wallet/watching-wallet-serialization.txt b/core/src/test/resources/com/google/bitcoin/wallet/watching-wallet-serialization.txt index eefb27fb1..d4b79408e 100644 --- a/core/src/test/resources/com/google/bitcoin/wallet/watching-wallet-serialization.txt +++ b/core/src/test/resources/com/google/bitcoin/wallet/watching-wallet-serialization.txt @@ -143,15 +143,6 @@ deterministic_key { path: 12 } -type: DETERMINISTIC_KEY -public_key: "\003?\v\222\341\321\"@\2624\336H\221\2570bC\251\377\000vm\032\357U\250Dl:\200\020Q-" -deterministic_key { - chain_code: "\205R\372F\245\232*\0351Y??\321\362I\222ne\277H+\304\rL\234\313\016|\a\372a\a" - path: 2147483648 - path: 0 - path: 13 -} - type: DETERMINISTIC_KEY public_key: "\002\225b\3515\202\233\335\320.7\265\274uh\230N\242\254\317J\364\331\2345\220)\362\334\216\202\\" deterministic_key { @@ -267,13 +258,4 @@ deterministic_key { path: 2147483648 path: 1 path: 12 -} - -type: DETERMINISTIC_KEY -public_key: "\003\301\302\254\214C}\362f\315GV\033]\257\a\231\t[\001=\0046\213\220\341S\324\266\202&\206N" -deterministic_key { - chain_code: "\272\\\225\354.UQ8\264\346\a\310h\350\031\227\024c\340\337;W7\f\322\301\304\232P\360\373\035" - path: 2147483648 - path: 1 - path: 13 } \ No newline at end of file