From d35583236ab95ccb9c57240a94c42a5676602494 Mon Sep 17 00:00:00 2001 From: Andreas Schildbach Date: Wed, 6 Feb 2019 16:38:24 +0100 Subject: [PATCH] Wallet: Add static constructor createBasic() which creates a wallet with just a basic keychain, and no key derivation. --- .../org/bitcoinj/wallet/KeyChainGroup.java | 145 +++++++++++------- .../main/java/org/bitcoinj/wallet/Wallet.java | 14 +- .../java/org/bitcoinj/wallet/WalletTest.java | 14 ++ 3 files changed, 113 insertions(+), 60 deletions(-) diff --git a/core/src/main/java/org/bitcoinj/wallet/KeyChainGroup.java b/core/src/main/java/org/bitcoinj/wallet/KeyChainGroup.java index 6f2e7bd3..b22dfbb5 100644 --- a/core/src/main/java/org/bitcoinj/wallet/KeyChainGroup.java +++ b/core/src/main/java/org/bitcoinj/wallet/KeyChainGroup.java @@ -72,7 +72,8 @@ public class KeyChainGroup implements KeyBag { private BasicKeyChain basic; private NetworkParameters params; - protected final LinkedList chains; + // Keychains for deterministically derived keys. If this is null, no chains should be created automatically. + protected final @Nullable LinkedList 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 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(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 chains, + private KeyChainGroup(NetworkParameters params, @Nullable BasicKeyChain basicKeyChain, @Nullable List chains, @Nullable EnumMap 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(chains) : null; this.keyCrypter = crypter; this.currentKeys = currentKeys == null ? new EnumMap(KeyChain.KeyPurpose.class) : currentKeys; this.currentAddresses = new EnumMap<>(KeyChain.KeyPurpose.class); - maybeLookaheadScripts(); if (isMarried()) { + maybeLookaheadScripts(); for (Map.Entry 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 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 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 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 = 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 createCurrentKeysMap(List 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 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(); diff --git a/core/src/main/java/org/bitcoinj/wallet/Wallet.java b/core/src/main/java/org/bitcoinj/wallet/Wallet.java index 542a496f..60123ecf 100644 --- a/core/src/main/java/org/bitcoinj/wallet/Wallet.java +++ b/core/src/main/java/org/bitcoinj/wallet/Wallet.java @@ -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<>(); diff --git a/core/src/test/java/org/bitcoinj/wallet/WalletTest.java b/core/src/test/java/org/bitcoinj/wallet/WalletTest.java index cc2d9fa8..94ae6e73 100644 --- a/core/src/test/java/org/bitcoinj/wallet/WalletTest.java +++ b/core/src/test/java/org/bitcoinj/wallet/WalletTest.java @@ -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.