mirror of
https://github.com/bitcoinj/bitcoinj.git
synced 2024-11-19 18:00:39 +01:00
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:
parent
16b53836b8
commit
3c73f5e8a1
@ -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())
|
||||
|
@ -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()}.
|
||||
|
@ -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();
|
||||
|
@ -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) {
|
||||
|
@ -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.
|
||||
|
Loading…
Reference in New Issue
Block a user