HD wallets: experimental change to not trigger full lookahead when deriving keys. This allows a savvy app to get keys/addresses at startup fast, if they do so before starting up the peergroup (which wants all keys in the zone so it can calculate a Bloom filter). May be reverted if it causes trouble.

This commit is contained in:
Mike Hearn 2014-08-11 17:53:33 +02:00
parent d824666c2f
commit e8ba287029
7 changed files with 70 additions and 67 deletions

View File

@ -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<DeterministicKey> 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<DeterministicKey> lookahead = maybeLookAhead(parentKey, index, 0, 0);
basicKeyChain.importKeys(lookahead);
List<DeterministicKey> keys = new ArrayList<DeterministicKey>(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<ChildNumber> 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<DeterministicKey> keys = maybeLookAhead(externalKey, issuedExternalKeys);
@ -920,17 +934,20 @@ public class DeterministicKeyChain implements EncryptableKeyChain {
}
}
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.
*
* The returned mutable list of keys must be inserted into the basic key chain.
*/
private List<DeterministicKey> maybeLookAhead(DeterministicKey parent, int issued) {
private List<DeterministicKey> 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)

View File

@ -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<ECKey> getMarriedKeysWithFollowed(DeterministicKey followedKey, Collection<DeterministicKeyChain> followingChains) {
ImmutableList.Builder<ECKey> 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();

View File

@ -2456,7 +2456,7 @@ public class WalletTest extends TestWithWallet {
}
}, Threading.SAME_THREAD);
wallet.freshReceiveKey();
assertEquals(7, keys.size());
assertEquals(1, keys.size());
}
@Test

View File

@ -95,17 +95,23 @@ public class DeterministicKeyChainTest {
ECKey key = chain.getKey(KeyChain.KeyPurpose.CHANGE);
assertEquals(1, listenerKeys.size()); // 1 event
final List<ECKey> 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<ECKey> 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<Protos.Key> 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);

View File

@ -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<Protos.Key> 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<Protos.Key> 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

View File

@ -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
}

View File

@ -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
}