Skip to content

Commit

Permalink
HD Wallets: add a threshold for the DeterministicKey lookahead
Browse files Browse the repository at this point in the history
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 <harald@harald-hoyer.de>
  • Loading branch information
haraldh authored and mikehearn committed May 29, 2014
1 parent 42bfbb9 commit 534a1e3
Show file tree
Hide file tree
Showing 3 changed files with 100 additions and 6 deletions.
22 changes: 22 additions & 0 deletions core/src/main/java/com/google/bitcoin/core/Wallet.java
Original file line number Diff line number Diff line change
Expand Up @@ -468,6 +468,28 @@ public int getKeychainLookaheadSize() {
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
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,7 @@
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;
Expand Down Expand Up @@ -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;
Expand Down Expand Up @@ -198,6 +200,7 @@ private DeterministicKeyChain(KeyCrypter crypter, KeyParameter aesKey, Determini
this.issuedInternalKeys = chain.issuedInternalKeys;

this.lookaheadSize = chain.lookaheadSize;
this.lookaheadThreshold = chain.lookaheadThreshold;

this.seed = chain.seed.encrypt(crypter, aesKey);
basicKeyChain = new BasicKeyChain(crypter);
Expand Down Expand Up @@ -673,6 +676,40 @@ public void setLookaheadSize(int lookaheadSize) {
}
}

/**
* 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();
Expand All @@ -688,24 +725,36 @@ private void maybeLookAhead() {
}
}

// 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<DeterministicKey> 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<DeterministicKey> result = new ArrayList<DeterministicKey>(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);
key = key.getPubOnly();
hierarchy.putKey(key);
result.add(key);
}
log.info("Took {} msec", System.currentTimeMillis() - now);
log.info("maybeLookAhead(): Took {} msec", System.currentTimeMillis() - now);
return result;
}

Expand Down
23 changes: 23 additions & 0 deletions core/src/main/java/com/google/bitcoin/wallet/KeyChainGroup.java
Original file line number Diff line number Diff line change
Expand Up @@ -57,6 +57,7 @@ public class KeyChainGroup {
private final EnumMap<KeyChain.KeyPurpose, DeterministicKey> 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() {
Expand Down Expand Up @@ -99,6 +100,8 @@ private void createAndActivateNewHDChain() {
chain.addEventListener(registration.listener, registration.executor);
if (lookaheadSize >= 0)
chain.setLookaheadSize(lookaheadSize);
if (lookaheadThreshold >= 0)
chain.setLookaheadThreshold(lookaheadThreshold);
chains.add(chain);
}

Expand Down Expand Up @@ -157,6 +160,26 @@ public int getLookaheadSize() {
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<ECKey> keys) {
return basic.importKeys(keys);
Expand Down

0 comments on commit 534a1e3

Please sign in to comment.