From 3c73f5e8a17a097133b06fcb53d308e8105835ed Mon Sep 17 00:00:00 2001 From: Andreas Schildbach Date: Fri, 8 Feb 2019 16:13:33 +0100 Subject: [PATCH] KeyChainGroup: Introduce concept of multiple active keychains. The newest/last active keychain is the default. Almost all of this class only works on the default active keychain. All other active keychains are meant as fallback for if a sender doesn't understand a certain new script type. New P2WPKH KeyChainGroups are created with a P2PKH fallback chain. This will likely go away in future as P2WPKH and Bech32 are becoming the norm. --- .../org/bitcoinj/wallet/KeyChainGroup.java | 95 ++++++++++++++++--- .../main/java/org/bitcoinj/wallet/Wallet.java | 44 ++++++++- .../bitcoinj/wallet/KeyChainGroupTest.java | 19 ++++ .../java/org/bitcoinj/tools/WalletTool.java | 14 +-- .../org/bitcoinj/tools/wallet-tool-help.txt | 2 +- 5 files changed, 152 insertions(+), 22 deletions(-) diff --git a/core/src/main/java/org/bitcoinj/wallet/KeyChainGroup.java b/core/src/main/java/org/bitcoinj/wallet/KeyChainGroup.java index 4987460f..95200c20 100644 --- a/core/src/main/java/org/bitcoinj/wallet/KeyChainGroup.java +++ b/core/src/main/java/org/bitcoinj/wallet/KeyChainGroup.java @@ -44,9 +44,14 @@ import static com.google.common.base.Preconditions.*; /** *

A KeyChainGroup is used by the {@link Wallet} and manages: a {@link BasicKeyChain} object * (which will normally be empty), and zero or more {@link DeterministicKeyChain}s. The last added - * deterministic keychain is always the active keychain, that's the one we normally derive keys and + * deterministic keychain is always the default active keychain, that's the one we normally derive keys and * addresses from.

* + *

There can be active keychains for each output script type. However this class almost entirely only works on + * the default active keychain (see {@link #getActiveKeyChain()}). The other active keychains + * (see {@link #getActiveKeyChain(ScriptType, long)}) are meant as fallback for if a sender doesn't understand a + * certain new script type (e.g. P2WPKH which comes with the new Bech32 address format).

+ * *

If a key rotation time is set, it may be necessary to add a new DeterministicKeyChain with a fresh seed * and also preserve the old one, so funds can be swept from the rotating keys. In this case, there may be * more than one deterministic chain. The latest chain is called the active chain and is where new keys are served @@ -76,27 +81,51 @@ public class KeyChainGroup implements KeyBag { } /** - * Add chain from a random source. + *

Add chain from a random source.

+ *

In the case of P2PKH, just a P2PKH chain is created and activated which is then the default chain for fresh + * addresses. It can be upgraded to P2WPKH later.

+ *

In the case of P2WPKH, both a P2PKH and a P2WPKH chain are created and activated, the latter being the default + * chain. This behaviour will likely be changed with bitcoinj 0.16 such that only a P2WPKH chain is created and + * activated.

* @param outputScriptType type of addresses (aka output scripts) to generate for receiving */ public Builder fromRandom(Script.ScriptType outputScriptType) { - this.chains.clear(); - DeterministicKeyChain chain = DeterministicKeyChain.builder().random(new SecureRandom()) - .outputScriptType(outputScriptType).accountPath(structure.accountPathFor(outputScriptType)).build(); - this.chains.add(chain); + DeterministicSeed seed = new DeterministicSeed(new SecureRandom(), + DeterministicSeed.DEFAULT_SEED_ENTROPY_BITS, ""); + fromSeed(seed, outputScriptType); return this; } /** - * Add chain from a given seed. + *

Add chain from a given seed.

+ *

In the case of P2PKH, just a P2PKH chain is created and activated which is then the default chain for fresh + * addresses. It can be upgraded to P2WPKH later.

+ *

In the case of P2WPKH, both a P2PKH and a P2WPKH chain are created and activated, the latter being the default + * chain. This behaviour will likely be changed with bitcoinj 0.16 such that only a P2WPKH chain is created and + * activated.

* @param seed deterministic seed to derive all keys from * @param outputScriptType type of addresses (aka output scripts) to generate for receiving */ public Builder fromSeed(DeterministicSeed seed, Script.ScriptType outputScriptType) { - this.chains.clear(); - DeterministicKeyChain chain = DeterministicKeyChain.builder().seed(seed).outputScriptType(outputScriptType) - .accountPath(structure.accountPathFor(outputScriptType)).build(); - this.chains.add(chain); + if (outputScriptType == Script.ScriptType.P2PKH) { + DeterministicKeyChain chain = DeterministicKeyChain.builder().seed(seed) + .outputScriptType(Script.ScriptType.P2PKH) + .accountPath(structure.accountPathFor(Script.ScriptType.P2PKH)).build(); + this.chains.clear(); + this.chains.add(chain); + } else if (outputScriptType == Script.ScriptType.P2WPKH) { + DeterministicKeyChain fallbackChain = DeterministicKeyChain.builder().seed(seed) + .outputScriptType(Script.ScriptType.P2PKH) + .accountPath(structure.accountPathFor(Script.ScriptType.P2PKH)).build(); + DeterministicKeyChain defaultChain = DeterministicKeyChain.builder().seed(seed) + .outputScriptType(Script.ScriptType.P2WPKH) + .accountPath(structure.accountPathFor(Script.ScriptType.P2WPKH)).build(); + this.chains.clear(); + this.chains.add(fallbackChain); + this.chains.add(defaultChain); + } else { + throw new IllegalArgumentException(outputScriptType.toString()); + } return this; } @@ -324,6 +353,18 @@ public class KeyChainGroup implements KeyBag { return chain.getKeys(purpose, numberOfKeys); // Always returns the next key along the key chain. } + /** + *

Returns a fresh address for a given {@link KeyChain.KeyPurpose} and of a given + * {@link Script.ScriptType}.

+ *

This method is meant for when you really need a fallback address. Normally, you should be + * using {@link #freshAddress(KeyChain.KeyPurpose)} or + * {@link #currentAddress(KeyChain.KeyPurpose)}.

+ */ + public Address freshAddress(KeyChain.KeyPurpose purpose, Script.ScriptType outputScriptType, long keyRotationTimeSecs) { + DeterministicKeyChain chain = getActiveKeyChain(outputScriptType, keyRotationTimeSecs); + return Address.fromKey(params, chain.getKey(purpose), outputScriptType); + } + /** * Returns address for a {@link #freshKey(KeyChain.KeyPurpose)} */ @@ -345,7 +386,37 @@ public class KeyChainGroup implements KeyBag { } } - /** Returns the key chain that's used for generation of fresh/current keys. This is always the newest HD chain. */ + /** + * Returns the key chains that are used for generation of fresh/current keys, in the order of how they + * were added. The default active chain will come last in the list. + */ + public List getActiveKeyChains(long keyRotationTimeSecs) { + checkState(isSupportsDeterministicChains(), "doesn't support deterministic chains"); + List activeChains = new LinkedList<>(); + for (DeterministicKeyChain chain : chains) + if (chain.getEarliestKeyCreationTime() >= keyRotationTimeSecs) + activeChains.add(chain); + return activeChains; + } + + /** + * Returns the key chain that's used for generation of fresh/current keys of the given type. If it's not the default + * type and no active chain for this type exists, {@code null} is returned. No upgrade or downgrade is tried. + */ + public final DeterministicKeyChain getActiveKeyChain(Script.ScriptType outputScriptType, long keyRotationTimeSecs) { + checkState(isSupportsDeterministicChains(), "doesn't support deterministic chains"); + for (DeterministicKeyChain chain : ImmutableList.copyOf(chains).reverse()) + if (chain.getOutputScriptType() == outputScriptType + && chain.getEarliestKeyCreationTime() >= keyRotationTimeSecs) + return chain; + return null; + } + + /** + * Returns the key chain that's used for generation of default fresh/current keys. This is always the newest + * deterministic chain. If no deterministic chain is present but imported keys instead, a deterministic upgrate is + * tried. + */ public final DeterministicKeyChain getActiveKeyChain() { checkState(isSupportsDeterministicChains(), "doesn't support deterministic chains"); if (chains.isEmpty()) diff --git a/core/src/main/java/org/bitcoinj/wallet/Wallet.java b/core/src/main/java/org/bitcoinj/wallet/Wallet.java index 1f7210b6..47cf07b1 100644 --- a/core/src/main/java/org/bitcoinj/wallet/Wallet.java +++ b/core/src/main/java/org/bitcoinj/wallet/Wallet.java @@ -443,8 +443,9 @@ public class Wallet extends BaseTaggableObject } /** - * Creates a wallet containing a given set of keys. All further keys will be derived from the oldest key. + * @deprecated Use {@link #createBasic(NetworkParameters)}, then {@link #importKeys(List)}. */ + @Deprecated public static Wallet fromKeys(NetworkParameters params, List keys) { for (ECKey key : keys) checkArgument(!(key instanceof DeterministicKey)); @@ -518,10 +519,28 @@ public class Wallet extends BaseTaggableObject } /** - * Gets the active keychain via {@link KeyChainGroup#getActiveKeyChain()} + * Gets the active keychains via {@link KeyChainGroup#getActiveKeyChains(long)}. + */ + public List getActiveKeyChains() { + keyChainGroupLock.lock(); + try { + long keyRotationTimeSecs = vKeyRotationTimestamp; + return keyChainGroup.getActiveKeyChains(keyRotationTimeSecs); + } finally { + keyChainGroupLock.unlock(); + } + } + + /** + * Gets the default active keychain via {@link KeyChainGroup#getActiveKeyChain()}. */ public DeterministicKeyChain getActiveKeyChain() { - return keyChainGroup.getActiveKeyChain(); + keyChainGroupLock.lock(); + try { + return keyChainGroup.getActiveKeyChain(); + } finally { + keyChainGroupLock.unlock(); + } } /** @@ -664,6 +683,25 @@ public class Wallet extends BaseTaggableObject return freshAddress(KeyChain.KeyPurpose.RECEIVE_FUNDS); } + /** + *

Returns a fresh receive address for a given {@link Script.ScriptType}.

+ *

This method is meant for when you really need a fallback address. Normally, you should be + * using {@link #freshAddress(KeyChain.KeyPurpose)} or + * {@link #currentAddress(KeyChain.KeyPurpose)}.

+ */ + public Address freshReceiveAddress(Script.ScriptType scriptType) { + Address address; + keyChainGroupLock.lock(); + try { + long keyRotationTimeSecs = vKeyRotationTimestamp; + address = keyChainGroup.freshAddress(KeyChain.KeyPurpose.RECEIVE_FUNDS, scriptType, keyRotationTimeSecs); + } finally { + keyChainGroupLock.unlock(); + } + saveNow(); + return address; + } + /** * Returns only the keys that have been issued by {@link #freshReceiveKey()}, {@link #freshReceiveAddress()}, * {@link #currentReceiveKey()} or {@link #currentReceiveAddress()}. diff --git a/core/src/test/java/org/bitcoinj/wallet/KeyChainGroupTest.java b/core/src/test/java/org/bitcoinj/wallet/KeyChainGroupTest.java index 0b677162..347a9848 100644 --- a/core/src/test/java/org/bitcoinj/wallet/KeyChainGroupTest.java +++ b/core/src/test/java/org/bitcoinj/wallet/KeyChainGroupTest.java @@ -66,6 +66,25 @@ public class KeyChainGroupTest { watchingAccountKey = DeterministicKey.deserializeB58(null, XPUB, MAINNET); } + @Test + public void createDeterministic_P2PKH() { + KeyChainGroup kcg = KeyChainGroup.builder(MAINNET).fromRandom(Script.ScriptType.P2PKH).build(); + // check default + Address address = kcg.currentAddress(KeyPurpose.RECEIVE_FUNDS); + assertEquals(Script.ScriptType.P2PKH, address.getOutputScriptType()); + } + + @Test + public void createDeterministic_P2WPKH() { + KeyChainGroup kcg = KeyChainGroup.builder(MAINNET).fromRandom(Script.ScriptType.P2WPKH).build(); + // check default + Address address = kcg.currentAddress(KeyPurpose.RECEIVE_FUNDS); + assertEquals(Script.ScriptType.P2WPKH, address.getOutputScriptType()); + // check fallback (this will go away at some point) + address = kcg.freshAddress(KeyPurpose.RECEIVE_FUNDS, Script.ScriptType.P2PKH, 0); + assertEquals(Script.ScriptType.P2PKH, address.getOutputScriptType()); + } + private KeyChainGroup createMarriedKeyChainGroup() { DeterministicKeyChain chain = createMarriedKeyChain(); KeyChainGroup group = KeyChainGroup.builder(MAINNET).lookaheadSize(LOOKAHEAD_SIZE).addChain(chain).build(); diff --git a/tools/src/main/java/org/bitcoinj/tools/WalletTool.java b/tools/src/main/java/org/bitcoinj/tools/WalletTool.java index 40d36477..8b6d7c58 100644 --- a/tools/src/main/java/org/bitcoinj/tools/WalletTool.java +++ b/tools/src/main/java/org/bitcoinj/tools/WalletTool.java @@ -33,6 +33,7 @@ import org.bitcoinj.store.*; import org.bitcoinj.uri.BitcoinURI; import org.bitcoinj.uri.BitcoinURIParseException; import org.bitcoinj.utils.BriefLogFormatter; +import org.bitcoinj.wallet.DeterministicKeyChain; import org.bitcoinj.wallet.DeterministicSeed; import com.google.common.base.Splitter; import com.google.common.collect.ImmutableList; @@ -1514,17 +1515,18 @@ public class WalletTool { } private static void setCreationTime() { - DeterministicSeed seed = wallet.getActiveKeyChain().getSeed(); - if (seed == null) { - System.err.println("Active chain does not have a seed."); - return; - } long creationTime = getCreationTimeSeconds(); + for (DeterministicKeyChain chain : wallet.getActiveKeyChains()) { + DeterministicSeed seed = chain.getSeed(); + if (seed == null) + System.out.println("Active chain does not have a seed: " + chain); + else + seed.setCreationTimeSeconds(creationTime); + } if (creationTime > 0) System.out.println("Setting creation time to: " + Utils.dateTimeFormat(creationTime * 1000)); else System.out.println("Clearing creation time."); - seed.setCreationTimeSeconds(creationTime); } static synchronized void onChange(final CountDownLatch latch) { diff --git a/tools/src/main/resources/org/bitcoinj/tools/wallet-tool-help.txt b/tools/src/main/resources/org/bitcoinj/tools/wallet-tool-help.txt index f0f27a20..90b3e534 100644 --- a/tools/src/main/resources/org/bitcoinj/tools/wallet-tool-help.txt +++ b/tools/src/main/resources/org/bitcoinj/tools/wallet-tool-help.txt @@ -61,7 +61,7 @@ Usage: wallet-tool --flags action-name created before this date will be re-spent to a key (from an HD tree) that was created after it. If --date is missing, the current time is assumed. If the time covers all keys, a new HD tree will be created from a new random seed. - set-creation-time Modify the creation time of the active chain of this wallet. This is useful for repairing + set-creation-time Modify the creation time of the active chains of this wallet. This is useful for repairing wallets that accidently have been created "in the future". Currently, watching wallets are not supported. If --date is specified, that's the creation date.