From 534a1e3a5c55c30a44b7c9b3c753e66e86b1f11c Mon Sep 17 00:00:00 2001 From: Harald Hoyer Date: Fri, 11 Apr 2014 11:49:54 +0200 Subject: [PATCH] HD Wallets: add a threshold for the DeterministicKey lookahead DeterministicKeyChain.maybeLookAhead() would pre-generate a new key, for every issued key, even if it is only one. If we replay the blockchain and update the issuedKeys counter, maybeLookAhead() would trigger the regeneration and resending of the bloom filter for every used key. This patch adds a threshold, where keys are only pre-generated after more keys are needed than the value of the threshold. Signed-off-by: Harald Hoyer --- .../java/com/google/bitcoin/core/Wallet.java | 22 +++++++ .../bitcoin/wallet/DeterministicKeyChain.java | 61 +++++++++++++++++-- .../google/bitcoin/wallet/KeyChainGroup.java | 23 +++++++ 3 files changed, 100 insertions(+), 6 deletions(-) diff --git a/core/src/main/java/com/google/bitcoin/core/Wallet.java b/core/src/main/java/com/google/bitcoin/core/Wallet.java index 0eed8db0..862a06d7 100644 --- a/core/src/main/java/com/google/bitcoin/core/Wallet.java +++ b/core/src/main/java/com/google/bitcoin/core/Wallet.java @@ -468,6 +468,28 @@ public class Wallet extends BaseTaggableObject implements Serializable, BlockCha return keychain.getLookaheadSize(); } + /** See {@link com.google.bitcoin.wallet.DeterministicKeyChain#setLookaheadThreshold(int)} for more info on this. */ + public void setLookaheadThreshold(int num) { + lock.lock(); + try { + keychain.setLookaheadThreshold(num); + } finally { + lock.unlock(); + } + } + + /** See {@link com.google.bitcoin.wallet.DeterministicKeyChain#setLookaheadThreshold(int)} for more info on this. */ + public int getKeychainLookaheadThreshold() { + int threshold = 0; + lock.lock(); + try { + threshold = keychain.getLookaheadThreshold(); + } finally { + lock.unlock(); + } + return threshold; + } + /** * Returns a public-only DeterministicKey that can be used to set up a watching wallet: that is, a wallet that * can import transactions from the block chain just as the normal wallet can, but which cannot spend. Watching 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 b5eefdd5..6fb897eb 100644 --- a/core/src/main/java/com/google/bitcoin/wallet/DeterministicKeyChain.java +++ b/core/src/main/java/com/google/bitcoin/wallet/DeterministicKeyChain.java @@ -34,6 +34,7 @@ import javax.annotation.Nullable; import java.math.BigInteger; import java.security.SecureRandom; import java.util.ArrayList; +import java.util.Collections; import java.util.LinkedList; import java.util.List; import java.util.Map; @@ -100,6 +101,7 @@ public class DeterministicKeyChain implements EncryptableKeyChain { // 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; + private int lookaheadThreshold = 33; // The parent keys for external keys (handed out to other people) and internal keys (used for change addresses). private DeterministicKey externalKey, internalKey; @@ -198,6 +200,7 @@ public class DeterministicKeyChain implements EncryptableKeyChain { this.issuedInternalKeys = chain.issuedInternalKeys; this.lookaheadSize = chain.lookaheadSize; + this.lookaheadThreshold = chain.lookaheadThreshold; this.seed = chain.seed.encrypt(crypter, aesKey); basicKeyChain = new BasicKeyChain(crypter); @@ -673,6 +676,40 @@ public class DeterministicKeyChain implements EncryptableKeyChain { } } + /** + * Sets the threshold for the key pre-generation. + * If a key is used in a transaction, the keychain would pre-generate a new key, for every issued key, + * even if it is only one. If the blockchain is replayed, every key would trigger a regeneration + * of the bloom filter sent to the peers as a consequence. + * To prevent this, new keys are only generated, if more than the threshold value are needed. + */ + public void setLookaheadThreshold(int num) { + lock.lock(); + if (num >= lookaheadSize) + throw new IllegalArgumentException("Threshold larger or equal to the lookaheadSize"); + + try { + 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. private void maybeLookAhead() { lock.lock(); @@ -688,16 +725,28 @@ public class DeterministicKeyChain implements EncryptableKeyChain { } } - // Returned keys must be inserted into the basic key chain. + /** + * 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. + * + * Returned keys must be inserted into the basic key chain. + */ private List maybeLookAhead(DeterministicKey parent, int issued) { checkState(lock.isHeldByCurrentThread()); final int numChildren = hierarchy.getNumChildren(parent.getPath()); - int needed = issued + getLookaheadSize() - numChildren; - checkState(needed >= 0, "needed = " + needed); + final int needed = issued + getLookaheadSize() - numChildren; + + log.info("maybeLookAhead(): {} needed = lookaheadSize({}) - (numChildren({}) - issued({}) = {} < lookaheadThreshold({}))", + parent.getPathAsString(), getLookaheadSize(), numChildren, + issued, needed, getLookaheadThreshold()); + + /* Even if needed is negative, we have more than enough */ + if (needed <= getLookaheadThreshold()) + return Collections.emptyList(); + List result = new ArrayList(needed); - if (needed == 0) return result; long now = System.currentTimeMillis(); - log.info("Pre-generating {} keys for {}", needed, parent.getPathAsString()); + log.info("maybeLookAhead(): Pre-generating {} keys for {}", needed, parent.getPathAsString()); for (int i = 0; i < needed; i++) { // TODO: Handle the case where the derived key is >= curve order. DeterministicKey key = HDKeyDerivation.deriveChildKey(parent, numChildren + i); @@ -705,7 +754,7 @@ public class DeterministicKeyChain implements EncryptableKeyChain { hierarchy.putKey(key); result.add(key); } - log.info("Took {} msec", System.currentTimeMillis() - now); + log.info("maybeLookAhead(): Took {} msec", System.currentTimeMillis() - now); return result; } 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 ecd686cd..79ec433e 100644 --- a/core/src/main/java/com/google/bitcoin/wallet/KeyChainGroup.java +++ b/core/src/main/java/com/google/bitcoin/wallet/KeyChainGroup.java @@ -57,6 +57,7 @@ public class KeyChainGroup { private final EnumMap currentKeys; @Nullable private KeyCrypter keyCrypter; private int lookaheadSize = -1; + private int lookaheadThreshold = -1; /** Creates a keychain group with no basic chain, and a single randomly initialized HD chain. */ public KeyChainGroup() { @@ -99,6 +100,8 @@ public class KeyChainGroup { chain.addEventListener(registration.listener, registration.executor); if (lookaheadSize >= 0) chain.setLookaheadSize(lookaheadSize); + if (lookaheadThreshold >= 0) + chain.setLookaheadThreshold(lookaheadThreshold); chains.add(chain); } @@ -157,6 +160,26 @@ public class KeyChainGroup { return lookaheadSize; } + /** + * Sets the lookahead buffer threshold for ALL deterministic key chains, see + * {@link com.google.bitcoin.wallet.DeterministicKeyChain#setLookaheadThreshold(int)} + * for more information. + */ + public void setLookaheadThreshold(int num) { + for (DeterministicKeyChain chain : chains) { + chain.setLookaheadThreshold(num); + } + } + + /** + * Gets the current lookahead threshold being used for ALL deterministic key chains. See + * {@link com.google.bitcoin.wallet.DeterministicKeyChain#setLookaheadThreshold(int)} + * for more information. + */ + public int getLookaheadThreshold() { + return lookaheadThreshold; + } + /** Imports the given keys into the basic chain, creating it if necessary. */ public int importKeys(List keys) { return basic.importKeys(keys);