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.
This commit is contained in:
Andreas Schildbach
2019-02-08 16:13:33 +01:00
parent 16b53836b8
commit 3c73f5e8a1
5 changed files with 152 additions and 22 deletions

View File

@@ -44,9 +44,14 @@ import static com.google.common.base.Preconditions.*;
/**
* <p>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.</p>
*
* <p>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).</p>
*
* <p>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.
* <p>Add chain from a random source.</p>
* <p>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.</p>
* <p>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.</p>
* @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.
* <p>Add chain from a given seed.</p>
* <p>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.</p>
* <p>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.</p>
* @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.
}
/**
* <p>Returns a fresh address for a given {@link KeyChain.KeyPurpose} and of a given
* {@link Script.ScriptType}.</p>
* <p>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)}.</p>
*/
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<DeterministicKeyChain> getActiveKeyChains(long keyRotationTimeSecs) {
checkState(isSupportsDeterministicChains(), "doesn't support deterministic chains");
List<DeterministicKeyChain> 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())

View File

@@ -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<ECKey> 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<DeterministicKeyChain> 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);
}
/**
* <p>Returns a fresh receive address for a given {@link Script.ScriptType}.</p>
* <p>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)}.</p>
*/
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()}.

View File

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