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

View File

@ -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) {

View File

@ -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.