3
0
mirror of https://github.com/Qortal/altcoinj.git synced 2025-02-12 10:15:52 +00:00

Wallet: Add static constructor createBasic() which creates a wallet with just a basic keychain, and no key derivation.

This commit is contained in:
Andreas Schildbach 2019-02-06 16:38:24 +01:00
parent 6278dc39b1
commit d35583236a
3 changed files with 113 additions and 60 deletions

View File

@ -72,7 +72,8 @@ public class KeyChainGroup implements KeyBag {
private BasicKeyChain basic;
private NetworkParameters params;
protected final LinkedList<DeterministicKeyChain> chains;
// Keychains for deterministically derived keys. If this is null, no chains should be created automatically.
protected final @Nullable LinkedList<DeterministicKeyChain> chains;
// currentKeys is used for normal, non-multisig/married wallets. currentAddresses is used when we're handing out
// P2SH addresses. They're mutually exclusive.
private final EnumMap<KeyChain.KeyPurpose, DeterministicKey> currentKeys;
@ -81,6 +82,11 @@ public class KeyChainGroup implements KeyBag {
private int lookaheadSize = -1;
private int lookaheadThreshold = -1;
/** Creates a keychain group with just a basic chain. No deterministic chains will be created automatically. */
public static KeyChainGroup createBasic(NetworkParameters params) {
return new KeyChainGroup(params, new BasicKeyChain(), null, null, null);
}
/** Creates a keychain group with no basic chain, and a single, lazily created HD chain. */
public KeyChainGroup(NetworkParameters params) {
this(params, null, new ArrayList<DeterministicKeyChain>(1), null, null);
@ -118,19 +124,19 @@ public class KeyChainGroup implements KeyBag {
: DeterministicKeyChain.builder().spend(accountKey).build()), null, null);
}
private KeyChainGroup(NetworkParameters params, @Nullable BasicKeyChain basicKeyChain, List<DeterministicKeyChain> chains,
private KeyChainGroup(NetworkParameters params, @Nullable BasicKeyChain basicKeyChain, @Nullable List<DeterministicKeyChain> chains,
@Nullable EnumMap<KeyChain.KeyPurpose, DeterministicKey> currentKeys, @Nullable KeyCrypter crypter) {
this.params = params;
this.basic = basicKeyChain == null ? new BasicKeyChain() : basicKeyChain;
this.chains = new LinkedList<>(checkNotNull(chains));
this.chains = chains != null ? new LinkedList<DeterministicKeyChain>(chains) : null;
this.keyCrypter = crypter;
this.currentKeys = currentKeys == null
? new EnumMap<KeyChain.KeyPurpose, DeterministicKey>(KeyChain.KeyPurpose.class)
: currentKeys;
this.currentAddresses = new EnumMap<>(KeyChain.KeyPurpose.class);
maybeLookaheadScripts();
if (isMarried()) {
maybeLookaheadScripts();
for (Map.Entry<KeyChain.KeyPurpose, DeterministicKey> entry : this.currentKeys.entrySet()) {
Address address = ScriptBuilder
.createP2SHOutputScript(getActiveKeyChain().getRedeemData(entry.getValue()).redeemScript)
@ -140,6 +146,11 @@ public class KeyChainGroup implements KeyBag {
}
}
/** Returns true if it contains any deterministic keychain or one could be created. */
public boolean isSupportsDeterministicChains() {
return chains != null;
}
// This keeps married redeem data in sync with the number of keys issued
private void maybeLookaheadScripts() {
for (DeterministicKeyChain chain : chains) {
@ -149,6 +160,7 @@ public class KeyChainGroup implements KeyBag {
/** Adds a new HD chain to the chains list, and make it the default chain (from which keys are issued). */
public void createAndActivateNewHDChain() {
checkState(isSupportsDeterministicChains(), "doesn't support deterministic chains");
// We can't do auto upgrade here because we don't know the rotation time, if any.
final DeterministicKeyChain chain = DeterministicKeyChain.builder().random(new SecureRandom()).build();
addAndActivateHDChain(chain);
@ -159,6 +171,7 @@ public class KeyChainGroup implements KeyBag {
* Useful for adding a complex pre-configured keychain, such as a married wallet.
*/
public void addAndActivateHDChain(DeterministicKeyChain chain) {
checkState(isSupportsDeterministicChains(), "doesn't support deterministic chains");
log.info("Creating and activating a new HD chain: {}", chain);
for (ListenerRegistration<KeyChainEventListener> registration : basic.getListeners())
chain.addEventListener(registration.listener, registration.executor);
@ -268,6 +281,7 @@ 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. */
public final DeterministicKeyChain getActiveKeyChain() {
checkState(isSupportsDeterministicChains(), "doesn't support deterministic chains");
if (chains.isEmpty()) {
if (basic.numKeys() > 0) {
log.warn("No HD chain present but random keys are: you probably deserialized an old wallet.");
@ -287,10 +301,10 @@ public class KeyChainGroup implements KeyBag {
* for more information.
*/
public void setLookaheadSize(int lookaheadSize) {
checkState(isSupportsDeterministicChains(), "doesn't support deterministic chains");
this.lookaheadSize = lookaheadSize;
for (DeterministicKeyChain chain : chains) {
for (DeterministicKeyChain chain : chains)
chain.setLookaheadSize(lookaheadSize);
}
}
/**
@ -299,6 +313,7 @@ public class KeyChainGroup implements KeyBag {
* for more information.
*/
public int getLookaheadSize() {
checkState(isSupportsDeterministicChains(), "doesn't support deterministic chains");
if (lookaheadSize == -1)
return getActiveKeyChain().getLookaheadSize();
else
@ -311,9 +326,9 @@ public class KeyChainGroup implements KeyBag {
* for more information.
*/
public void setLookaheadThreshold(int num) {
for (DeterministicKeyChain chain : chains) {
checkState(isSupportsDeterministicChains(), "doesn't support deterministic chains");
for (DeterministicKeyChain chain : chains)
chain.setLookaheadThreshold(num);
}
}
/**
@ -322,6 +337,7 @@ public class KeyChainGroup implements KeyBag {
* for more information.
*/
public int getLookaheadThreshold() {
checkState(isSupportsDeterministicChains(), "doesn't support deterministic chains");
if (lookaheadThreshold == -1)
return getActiveKeyChain().getLookaheadThreshold();
else
@ -397,10 +413,10 @@ public class KeyChainGroup implements KeyBag {
ECKey result;
if ((result = basic.findKeyFromPubHash(pubKeyHash)) != null)
return result;
for (DeterministicKeyChain chain : chains) {
if ((result = chain.findKeyFromPubHash(pubKeyHash)) != null)
return result;
}
if (chains != null)
for (DeterministicKeyChain chain : chains)
if ((result = chain.findKeyFromPubHash(pubKeyHash)) != null)
return result;
return null;
}
@ -409,11 +425,13 @@ public class KeyChainGroup implements KeyBag {
* See {@link DeterministicKeyChain#markKeyAsUsed(DeterministicKey)} for more info on this.
*/
public void markPubKeyHashAsUsed(byte[] pubKeyHash) {
for (DeterministicKeyChain chain : chains) {
DeterministicKey key;
if ((key = chain.markPubHashAsUsed(pubKeyHash)) != null) {
maybeMarkCurrentKeyAsUsed(key);
return;
if (chains != null) {
for (DeterministicKeyChain chain : chains) {
DeterministicKey key;
if ((key = chain.markPubHashAsUsed(pubKeyHash)) != null) {
maybeMarkCurrentKeyAsUsed(key);
return;
}
}
}
}
@ -446,9 +464,10 @@ public class KeyChainGroup implements KeyBag {
public boolean hasKey(ECKey key) {
if (basic.hasKey(key))
return true;
for (DeterministicKeyChain chain : chains)
if (chain.hasKey(key))
return true;
if (chains != null)
for (DeterministicKeyChain chain : chains)
if (chain.hasKey(key))
return true;
return false;
}
@ -458,10 +477,10 @@ public class KeyChainGroup implements KeyBag {
ECKey result;
if ((result = basic.findKeyFromPubKey(pubKey)) != null)
return result;
for (DeterministicKeyChain chain : chains) {
if ((result = chain.findKeyFromPubKey(pubKey)) != null)
return result;
}
if (chains != null)
for (DeterministicKeyChain chain : chains)
if ((result = chain.findKeyFromPubKey(pubKey)) != null)
return result;
return null;
}
@ -470,11 +489,13 @@ public class KeyChainGroup implements KeyBag {
* See {@link DeterministicKeyChain#markKeyAsUsed(DeterministicKey)} for more info on this.
*/
public void markPubKeyAsUsed(byte[] pubkey) {
for (DeterministicKeyChain chain : chains) {
DeterministicKey key;
if ((key = chain.markPubKeyAsUsed(pubkey)) != null) {
maybeMarkCurrentKeyAsUsed(key);
return;
if (chains != null) {
for (DeterministicKeyChain chain : chains) {
DeterministicKey key;
if ((key = chain.markPubKeyAsUsed(pubkey)) != null) {
maybeMarkCurrentKeyAsUsed(key);
return;
}
}
}
}
@ -482,8 +503,9 @@ public class KeyChainGroup implements KeyBag {
/** Returns the number of keys managed by this group, including the lookahead buffers. */
public int numKeys() {
int result = basic.numKeys();
for (DeterministicKeyChain chain : chains)
result += chain.numKeys();
if (chains != null)
for (DeterministicKeyChain chain : chains)
result += chain.numKeys();
return result;
}
@ -503,7 +525,7 @@ public class KeyChainGroup implements KeyBag {
* @see org.bitcoinj.wallet.MarriedKeyChain
*/
public final boolean isMarried() {
return !chains.isEmpty() && getActiveKeyChain().isMarried();
return chains != null && !chains.isEmpty() && getActiveKeyChain().isMarried();
}
/**
@ -520,13 +542,14 @@ public class KeyChainGroup implements KeyBag {
// This code must be exception safe.
BasicKeyChain newBasic = basic.toEncrypted(keyCrypter, aesKey);
List<DeterministicKeyChain> newChains = new ArrayList<>(chains.size());
if (chains.isEmpty() && basic.numKeys() == 0) {
if (chains != null && chains.isEmpty() && basic.numKeys() == 0) {
// No HD chains and no random keys: encrypting an entirely empty keychain group. But we can't do that, we
// must have something to encrypt: so instantiate a new HD chain here.
createAndActivateNewHDChain();
}
for (DeterministicKeyChain chain : chains)
newChains.add(chain.toEncrypted(keyCrypter, aesKey));
if (chains != null)
for (DeterministicKeyChain chain : chains)
newChains.add(chain.toEncrypted(keyCrypter, aesKey));
this.keyCrypter = keyCrypter;
basic = newBasic;
chains.clear();
@ -544,8 +567,9 @@ public class KeyChainGroup implements KeyBag {
checkNotNull(aesKey);
BasicKeyChain newBasic = basic.toDecrypted(aesKey);
List<DeterministicKeyChain> newChains = new ArrayList<>(chains.size());
for (DeterministicKeyChain chain : chains)
newChains.add(chain.toDecrypted(aesKey));
if (chains != null)
for (DeterministicKeyChain chain : chains)
newChains.add(chain.toDecrypted(aesKey));
this.keyCrypter = null;
basic = newBasic;
@ -567,7 +591,7 @@ public class KeyChainGroup implements KeyBag {
public boolean isWatching() {
BasicKeyChain.State basicState = basic.isWatching();
BasicKeyChain.State activeState = BasicKeyChain.State.EMPTY;
if (!chains.isEmpty()) {
if (chains != null && !chains.isEmpty()) {
if (getActiveKeyChain().isWatching())
activeState = BasicKeyChain.State.WATCHING;
else
@ -598,16 +622,17 @@ public class KeyChainGroup implements KeyBag {
public long getEarliestKeyCreationTime() {
long time = basic.getEarliestKeyCreationTime(); // Long.MAX_VALUE if empty.
for (DeterministicKeyChain chain : chains)
time = Math.min(time, chain.getEarliestKeyCreationTime());
if (chains != null)
for (DeterministicKeyChain chain : chains)
time = Math.min(time, chain.getEarliestKeyCreationTime());
return time;
}
public int getBloomFilterElementCount() {
int result = basic.numBloomFilterEntries();
for (DeterministicKeyChain chain : chains) {
result += chain.numBloomFilterEntries();
}
if (chains != null)
for (DeterministicKeyChain chain : chains)
result += chain.numBloomFilterEntries();
return result;
}
@ -615,10 +640,9 @@ public class KeyChainGroup implements KeyBag {
BloomFilter filter = new BloomFilter(size, falsePositiveRate, nTweak);
if (basic.numKeys() > 0)
filter.merge(basic.getFilter(size, falsePositiveRate, nTweak));
for (DeterministicKeyChain chain : chains) {
filter.merge(chain.getFilter(size, falsePositiveRate, nTweak));
}
if (chains != null)
for (DeterministicKeyChain chain : chains)
filter.merge(chain.getFilter(size, falsePositiveRate, nTweak));
return filter;
}
@ -636,15 +660,17 @@ public class KeyChainGroup implements KeyBag {
checkNotNull(listener);
checkNotNull(executor);
basic.addEventListener(listener, executor);
for (DeterministicKeyChain chain : chains)
chain.addEventListener(listener, executor);
if (chains != null)
for (DeterministicKeyChain chain : chains)
chain.addEventListener(listener, executor);
}
/** Removes a listener for events that are run when keys are added. */
public boolean removeEventListener(KeyChainEventListener listener) {
checkNotNull(listener);
for (DeterministicKeyChain chain : chains)
chain.removeEventListener(listener);
if (chains != null)
for (DeterministicKeyChain chain : chains)
chain.removeEventListener(listener);
return basic.removeEventListener(listener);
}
@ -655,10 +681,9 @@ public class KeyChainGroup implements KeyBag {
result = basic.serializeToProtobuf();
else
result = Lists.newArrayList();
for (DeterministicKeyChain chain : chains) {
List<Protos.Key> protos = chain.serializeToProtobuf();
result.addAll(protos);
}
if (chains != null)
for (DeterministicKeyChain chain : chains)
result.addAll(chain.serializeToProtobuf());
return result;
}
@ -709,6 +734,7 @@ public class KeyChainGroup implements KeyBag {
* @return the DeterministicKeyChain that was created by the upgrade.
*/
public DeterministicKeyChain upgradeToDeterministic(long keyRotationTimeSecs, @Nullable KeyParameter aesKey) throws DeterministicUpgradeRequiresPassword, AllRandomKeysRotating {
checkState(isSupportsDeterministicChains(), "doesn't support deterministic chains");
checkState(basic.numKeys() > 0);
checkArgument(keyRotationTimeSecs >= 0);
// Subtract one because the key rotation time might have been set to the creation time of the first known good
@ -764,7 +790,7 @@ public class KeyChainGroup implements KeyBag {
/** Returns true if the group contains random keys but no HD chains. */
public boolean isDeterministicUpgradeRequired() {
return basic.numKeys() > 0 && chains.isEmpty();
return basic.numKeys() > 0 && chains != null && chains.isEmpty();
}
private static EnumMap<KeyChain.KeyPurpose, DeterministicKey> createCurrentKeysMap(List<DeterministicKeyChain> chains) {
@ -814,13 +840,15 @@ public class KeyChainGroup implements KeyBag {
final StringBuilder builder = new StringBuilder();
if (basic != null)
builder.append(basic.toString(includePrivateKeys, aesKey, params));
for (DeterministicKeyChain chain : chains)
builder.append(chain.toString(includePrivateKeys, aesKey, params)).append('\n');
if (chains != null)
for (DeterministicKeyChain chain : chains)
builder.append(chain.toString(includePrivateKeys, aesKey, params)).append('\n');
return builder.toString();
}
/** Returns a copy of the current list of chains. */
public List<DeterministicKeyChain> getDeterministicKeyChains() {
checkState(isSupportsDeterministicChains(), "doesn't support deterministic chains");
return new ArrayList<>(chains);
}
/**
@ -828,6 +856,7 @@ public class KeyChainGroup implements KeyBag {
* lookahead and thus the Bloom filter that was previously calculated has become stale.
*/
public int getCombinedKeyLookaheadEpochs() {
checkState(isSupportsDeterministicChains(), "doesn't support deterministic chains");
int epoch = 0;
for (DeterministicKeyChain chain : chains)
epoch += chain.getKeyLookaheadEpoch();

View File

@ -265,6 +265,15 @@ public class Wallet extends BaseTaggableObject
this(context, new KeyChainGroup(context.getParams()));
}
/**
* Creates a new, empty wallet with just a basic keychain and no transactions. No deterministic chains will be created
* automatically. This is meant for when you just want to import a few keys and operate on them.
* @param params network parameters
*/
public static Wallet createBasic(NetworkParameters params) {
return new Wallet(params, KeyChainGroup.createBasic(params));
}
/**
* @param params network parameters
* @param seed deterministic seed
@ -352,12 +361,13 @@ public class Wallet extends BaseTaggableObject
this.context = checkNotNull(context);
this.params = checkNotNull(context.getParams());
this.keyChainGroup = checkNotNull(keyChainGroup);
if (params.getId().equals(NetworkParameters.ID_UNITTESTNET))
if (this.keyChainGroup.isSupportsDeterministicChains()
&& params.getId().equals(NetworkParameters.ID_UNITTESTNET))
this.keyChainGroup.setLookaheadSize(5); // Cut down excess computation for unit tests.
// If this keyChainGroup was created fresh just now (new wallet), make HD so a backup can be made immediately
// without having to call current/freshReceiveKey. If there are already keys in the chain of any kind then
// we're probably being deserialized so leave things alone: the API user can upgrade later.
if (this.keyChainGroup.numKeys() == 0)
if (this.keyChainGroup.isSupportsDeterministicChains() && this.keyChainGroup.numKeys() == 0)
this.keyChainGroup.createAndActivateNewHDChain();
watchedScripts = Sets.newHashSet();
unspent = new HashMap<>();

View File

@ -134,6 +134,20 @@ public class WalletTest extends TestWithWallet {
wallet.addAndActivateHDChain(chain);
}
@Test
public void createBasic() {
Wallet wallet = Wallet.createBasic(UNITTEST);
assertEquals(0, wallet.getKeyChainGroupSize());
wallet.importKey(new ECKey());
assertEquals(1, wallet.getKeyChainGroupSize());
}
@Test(expected = IllegalStateException.class)
public void createBasic_noDerivation() {
Wallet wallet = Wallet.createBasic(UNITTEST);
wallet.currentReceiveAddress();
}
@Test
public void getSeedAsWords1() {
// Can't verify much here as the wallet is random each time. We could fix the RNG for the unit tests and solve.