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);