mirror of
https://github.com/bitcoinj/bitcoinj.git
synced 2024-11-20 10:12:19 +01:00
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:
parent
d824666c2f
commit
e8ba287029
@ -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)
|
||||
|
@ -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();
|
||||
|
@ -2456,7 +2456,7 @@ public class WalletTest extends TestWithWallet {
|
||||
}
|
||||
}, Threading.SAME_THREAD);
|
||||
wallet.freshReceiveKey();
|
||||
assertEquals(7, keys.size());
|
||||
assertEquals(1, keys.size());
|
||||
}
|
||||
|
||||
@Test
|
||||
|
@ -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);
|
||||
|
@ -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
|
||||
|
@ -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
|
||||
}
|
@ -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
|
||||
}
|
Loading…
Reference in New Issue
Block a user