From 22f0600afe4a10da8b52c82269842b4187fd4363 Mon Sep 17 00:00:00 2001 From: Devrandom Date: Sat, 20 Sep 2014 10:27:51 -0700 Subject: [PATCH] Refactor married keychains * move handling of following keychains into the leading keychain * move multisig threshold into the leading keychain * extract MarriedKeyChain from DeterministicKeyChain --- .../main/java/org/bitcoinj/core/ECKey.java | 14 + .../main/java/org/bitcoinj/core/Wallet.java | 56 +-- .../org/bitcoinj/crypto/DeterministicKey.java | 18 + .../store/WalletProtobufSerializer.java | 8 +- .../wallet/DeterministicKeyChain.java | 284 +++++++++++-- .../org/bitcoinj/wallet/KeyChainGroup.java | 324 +++----------- .../org/bitcoinj/wallet/MarriedKeyChain.java | 286 +++++++++++++ .../main/java/org/bitcoinj/wallet/Protos.java | 396 +++++++++--------- .../java/org/bitcoinj/core/WalletTest.java | 12 +- .../store/WalletProtobufSerializerTest.java | 14 +- .../bitcoinj/wallet/KeyChainGroupTest.java | 49 +-- .../deterministic-wallet-serialization.txt | 2 + .../wallet/watching-wallet-serialization.txt | 2 + core/src/wallet.proto | 10 +- .../java/org/bitcoinj/tools/WalletTool.java | 9 +- 15 files changed, 890 insertions(+), 594 deletions(-) create mode 100644 core/src/main/java/org/bitcoinj/wallet/MarriedKeyChain.java diff --git a/core/src/main/java/org/bitcoinj/core/ECKey.java b/core/src/main/java/org/bitcoinj/core/ECKey.java index 2df6e7ab..e096529b 100644 --- a/core/src/main/java/org/bitcoinj/core/ECKey.java +++ b/core/src/main/java/org/bitcoinj/core/ECKey.java @@ -1134,4 +1134,18 @@ public class ECKey implements EncryptableItem, Serializable { helper.add("isEncrypted", isEncrypted()); return helper.toString(); } + + public void formatKeyWithAddress(boolean includePrivateKeys, StringBuilder builder, NetworkParameters params) { + final Address address = toAddress(params); + builder.append(" addr:"); + builder.append(address.toString()); + builder.append(" hash160:"); + builder.append(Utils.HEX.encode(getPubKeyHash())); + builder.append("\n"); + if (includePrivateKeys) { + builder.append(" "); + builder.append(toStringWithPrivate()); + builder.append("\n"); + } + } } diff --git a/core/src/main/java/org/bitcoinj/core/Wallet.java b/core/src/main/java/org/bitcoinj/core/Wallet.java index c27c0e2d..581eb3a4 100644 --- a/core/src/main/java/org/bitcoinj/core/Wallet.java +++ b/core/src/main/java/org/bitcoinj/core/Wallet.java @@ -293,18 +293,10 @@ public class Wallet extends BaseTaggableObject implements Serializable, BlockCha } /** - * Returns the number of signatures required to spend from this wallet. For a normal non-married wallet this will - * always be 1. For a married wallet this will be the N from N-of-M CHECKMULTISIG scripts used in this wallet. - * This value is either directly specified during the marriage (see {@link #addFollowingAccountKeys(java.util.List, int)}) - * or, if not specified, calculated implicitly as a simple majority of keys. + * Gets the active keychain via {@link KeyChainGroup#getActiveKeyChain()} */ - public int getSigsRequiredToSpend() { - lock.lock(); - try { - return keychain.getSigsRequiredToSpend(); - } finally { - lock.unlock(); - } + public DeterministicKeyChain getActiveKeychain() { + return keychain.getActiveKeyChain(); } /** @@ -629,38 +621,18 @@ public class Wallet extends BaseTaggableObject implements Serializable, BlockCha } /** - *

Alias for addFollowingAccountKeys(followingAccountKeys, (followingAccountKeys.size() + 1) / 2 + 1)

- *

Creates married wallet requiring majority of keys to spend (2-of-3, 3-of-5 and so on)

- *

IMPORTANT: As of Bitcoin Core 0.9 all multisig transactions which require more than 3 public keys are - * non-standard and such spends won't be processed by peers with default settings, essentially making such - * transactions almost nonspendable

+ * Add a pre-configured keychain to the wallet. Useful for setting up a complex keychain, + * such as for a married wallet. For example: + *
+     * MarriedKeyChain chain = MarriedKeyChain.builder()
+     *     .random(new SecureRandom())
+     *     .followingKeys(followingKeys)
+     *     .threshold(2).build();
+     * wallet.addAndActivateHDChain(chain);
+     * 

*/ - public void addFollowingAccountKeys(List followingAccountKeys) { - lock.lock(); - try { - keychain.addFollowingAccountKeys(followingAccountKeys); - } finally { - lock.unlock(); - } - } - - /** - * Makes given account keys follow the account key of the active keychain. After that you will be able - * to get P2SH addresses to receive coins to. Given threshold value specifies how many signatures required to - * spend transactions for this married wallet. This value should not exceed total number of keys involved - * (one followed key plus number of following keys).

- *

IMPORTANT: As of Bitcoin Core 0.9 all multisig transactions which require more than 3 public keys are - * non-standard and such spends won't be processed by peers with default settings, essentially making such - * transactions almost nonspendable

- * This method should be called only once before key rotation, otherwise it will throw an IllegalStateException. - */ - public void addFollowingAccountKeys(List followingAccountKeys, int threshold) { - lock.lock(); - try { - keychain.addFollowingAccountKeys(followingAccountKeys, threshold); - } finally { - lock.unlock(); - } + public void addAndActivateHDChain(DeterministicKeyChain chain) { + keychain.addAndActivateHDChain(chain); } /** See {@link org.bitcoinj.wallet.DeterministicKeyChain#setLookaheadSize(int)} for more info on this. */ diff --git a/core/src/main/java/org/bitcoinj/crypto/DeterministicKey.java b/core/src/main/java/org/bitcoinj/crypto/DeterministicKey.java index 451ed139..26875d19 100644 --- a/core/src/main/java/org/bitcoinj/crypto/DeterministicKey.java +++ b/core/src/main/java/org/bitcoinj/crypto/DeterministicKey.java @@ -448,4 +448,22 @@ public class DeterministicKey extends ECKey { helper.add("creationTimeSeconds", creationTimeSeconds); return helper.toString(); } + + @Override + public void formatKeyWithAddress(boolean includePrivateKeys, StringBuilder builder, NetworkParameters params) { + final Address address = toAddress(params); + builder.append(" addr:"); + builder.append(address.toString()); + builder.append(" hash160:"); + builder.append(Utils.HEX.encode(getPubKeyHash())); + builder.append(" ("); + builder.append(getPathAsString()); + builder.append(")"); + builder.append("\n"); + if (includePrivateKeys) { + builder.append(" "); + builder.append(toStringWithPrivate()); + builder.append("\n"); + } + } } diff --git a/core/src/main/java/org/bitcoinj/store/WalletProtobufSerializer.java b/core/src/main/java/org/bitcoinj/store/WalletProtobufSerializer.java index fb8984cc..341aaee3 100644 --- a/core/src/main/java/org/bitcoinj/store/WalletProtobufSerializer.java +++ b/core/src/main/java/org/bitcoinj/store/WalletProtobufSerializer.java @@ -205,8 +205,6 @@ public class WalletProtobufSerializer { walletBuilder.addTransactionSigners(protoSigner); } - walletBuilder.setSigsRequiredToSpend(wallet.getSigsRequiredToSpend()); - // Populate the wallet version. walletBuilder.setVersion(wallet.getVersion()); @@ -410,16 +408,14 @@ public class WalletProtobufSerializer { if (!walletProto.getNetworkIdentifier().equals(params.getId())) throw new UnreadableWalletException.WrongNetwork(); - int sigsRequiredToSpend = walletProto.getSigsRequiredToSpend(); - // Read the scrypt parameters that specify how encryption and decryption is performed. KeyChainGroup chain; if (walletProto.hasEncryptionParameters()) { Protos.ScryptParameters encryptionParameters = walletProto.getEncryptionParameters(); final KeyCrypterScrypt keyCrypter = new KeyCrypterScrypt(encryptionParameters); - chain = KeyChainGroup.fromProtobufEncrypted(params, walletProto.getKeyList(), sigsRequiredToSpend, keyCrypter); + chain = KeyChainGroup.fromProtobufEncrypted(params, walletProto.getKeyList(), keyCrypter); } else { - chain = KeyChainGroup.fromProtobufUnencrypted(params, walletProto.getKeyList(), sigsRequiredToSpend); + chain = KeyChainGroup.fromProtobufUnencrypted(params, walletProto.getKeyList()); } Wallet wallet = factory.create(params, chain); diff --git a/core/src/main/java/org/bitcoinj/wallet/DeterministicKeyChain.java b/core/src/main/java/org/bitcoinj/wallet/DeterministicKeyChain.java index 9fe5bf31..fdd80f04 100644 --- a/core/src/main/java/org/bitcoinj/wallet/DeterministicKeyChain.java +++ b/core/src/main/java/org/bitcoinj/wallet/DeterministicKeyChain.java @@ -18,10 +18,14 @@ package org.bitcoinj.wallet; import org.bitcoinj.core.BloomFilter; import org.bitcoinj.core.ECKey; +import org.bitcoinj.core.NetworkParameters; import org.bitcoinj.core.Utils; import org.bitcoinj.crypto.*; +import org.bitcoinj.script.Script; import org.bitcoinj.store.UnreadableWalletException; import org.bitcoinj.utils.Threading; + +import com.google.common.base.Joiner; import com.google.common.collect.ImmutableList; import com.google.protobuf.ByteString; import org.slf4j.Logger; @@ -37,6 +41,7 @@ import java.util.concurrent.Executor; import java.util.concurrent.locks.ReentrantLock; import static com.google.common.base.Preconditions.*; +import static com.google.common.collect.Lists.newArrayList; import static com.google.common.collect.Lists.newLinkedList; /** @@ -87,11 +92,12 @@ import static com.google.common.collect.Lists.newLinkedList; * But even when you are requesting the 33rd key, you will still be looking 100 keys ahead. *

*/ +@SuppressWarnings("PublicStaticCollectionField") public class DeterministicKeyChain implements EncryptableKeyChain { private static final Logger log = LoggerFactory.getLogger(DeterministicKeyChain.class); public static final String DEFAULT_PASSPHRASE_FOR_MNEMONIC = ""; - private final ReentrantLock lock = Threading.lock("DeterministicKeyChain"); + protected final ReentrantLock lock = Threading.lock("DeterministicKeyChain"); private DeterministicHierarchy hierarchy; @Nullable private DeterministicKey rootKey; @@ -114,11 +120,11 @@ public class DeterministicKeyChain implements EncryptableKeyChain { // yet. For new chains it's set to whatever the default is, unless overridden by setLookaheadSize. For deserialized // chains, it will be calculated on demand from the number of loaded keys. private static final int LAZY_CALCULATE_LOOKAHEAD = -1; - private int lookaheadSize = 100; + protected int lookaheadSize = 100; // The lookahead threshold causes us to batch up creation of new keys to minimize the frequency of Bloom filter // regenerations, which are expensive and will (in future) trigger chain download stalls/retries. One third // is an efficiency tradeoff. - private int lookaheadThreshold = calcDefaultLookaheadThreshold(); + protected int lookaheadThreshold = calcDefaultLookaheadThreshold(); private int calcDefaultLookaheadThreshold() { return lookaheadSize / 3; @@ -144,6 +150,99 @@ public class DeterministicKeyChain implements EncryptableKeyChain { // If set this chain is following another chain in a married KeyChainGroup private boolean isFollowing; + // holds a number of signatures required to spend. It's the N from N-of-M CHECKMULTISIG script for P2SH transactions + // and always 1 for other transaction types + protected int sigsRequiredToSpend = 1; + + + public static class Builder> { + protected SecureRandom random; + protected int bits = 128; + protected String passphrase; + protected long seedCreationTimeSecs; + protected byte[] entropy; + protected DeterministicSeed seed; + + protected Builder() { + } + + @SuppressWarnings("unchecked") + protected T self() { + return (T)this; + } + + /** + * Creates a deterministic key chain starting from the given entropy. All keys yielded by this chain will be the same + * if the starting entropy is the same. You should provide the creation time in seconds since the UNIX epoch for the + * seed: this lets us know from what part of the chain we can expect to see derived keys appear. + */ + public T entropy(byte[] entropy) { + this.entropy = entropy; + return self(); + } + + /** + * Creates a deterministic key chain starting from the given seed. All keys yielded by this chain will be the same + * if the starting seed is the same. + */ + public T seed(DeterministicSeed seed) { + this.seed = seed; + return self(); + } + + /** + * Generates a new key chain with entropy selected randomly from the given {@link java.security.SecureRandom} + * object and of the requested size in bits. The derived seed is further protected with a user selected passphrase + * (see BIP 39). + * @param random the random number generator - use new SecureRandom(). + * @param bits The number of bits of entropy to use when generating entropy. Either 128 (default), 192 or 256. + */ + public T random(SecureRandom random, int bits) { + this.random = random; + this.bits = bits; + return self(); + } + + /** + * Generates a new key chain with 128 bits of entropy selected randomly from the given {@link java.security.SecureRandom} + * object. The derived seed is further protected with a user selected passphrase + * (see BIP 39). + * @param random the random number generator - use new SecureRandom(). + */ + public T random(SecureRandom random) { + this.random = random; + return self(); + } + + /** The passphrase to use with the generated mnemonic, or null. Currently must be empty. */ + public T passphrase(String passphrase) { + // FIXME support non-empty passphrase + this.passphrase = passphrase; + return self(); + } + + + public DeterministicKeyChain build() { + checkState(random != null || entropy != null || seed != null, "Must provide either entropy or random"); + checkState(passphrase == null || seed == null, "Passphrase must not be specified with seed"); + DeterministicKeyChain chain; + + if (random != null) { + chain = new DeterministicKeyChain(random, bits, passphrase, seedCreationTimeSecs); + } else if (entropy != null) { + chain = new DeterministicKeyChain(entropy, passphrase, seedCreationTimeSecs); + } else { + chain = new DeterministicKeyChain(seed); + } + + return chain; + } + } + + public static Builder builder() { + return new Builder(); + } + /** * Generates a new key chain with entropy selected randomly from the given {@link java.security.SecureRandom} * object and the default entropy size. @@ -170,7 +269,7 @@ public class DeterministicKeyChain implements EncryptableKeyChain { } /** - * Creates a deterministic key chain starting from the given seed. All keys yielded by this chain will be the same + * Creates a deterministic key chain starting from the given entropy. All keys yielded by this chain will be the same * if the starting seed is the same. You should provide the creation time in seconds since the UNIX epoch for the * seed: this lets us know from what part of the chain we can expect to see derived keys appear. */ @@ -209,7 +308,7 @@ public class DeterministicKeyChain implements EncryptableKeyChain { * some other keychain. In a married wallet following keychain represents "spouse's" keychain.

*

Watch key has to be an account key.

*/ - private DeterministicKeyChain(DeterministicKey watchKey, boolean isFollowing) { + protected DeterministicKeyChain(DeterministicKey watchKey, boolean isFollowing) { this(watchKey, Utils.currentTimeSeconds()); this.isFollowing = isFollowing; } @@ -581,49 +680,61 @@ public class DeterministicKeyChain implements EncryptableKeyChain { // ////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + protected void beforeSerializeToProtobuf(List result) { + } + @Override public List serializeToProtobuf() { + List result = newArrayList(); lock.lock(); try { - // Most of the serialization work is delegated to the basic key chain, which will serialize the bulk of the - // data (handling encryption along the way), and letting us patch it up with the extra data we care about. - LinkedList entries = newLinkedList(); - if (seed != null) { - Protos.Key.Builder mnemonicEntry = BasicKeyChain.serializeEncryptableItem(seed); - mnemonicEntry.setType(Protos.Key.Type.DETERMINISTIC_MNEMONIC); - serializeSeedEncryptableItem(seed, mnemonicEntry); - entries.add(mnemonicEntry.build()); - } - Map keys = basicKeyChain.serializeToEditableProtobufs(); - for (Map.Entry entry : keys.entrySet()) { - DeterministicKey key = (DeterministicKey) entry.getKey(); - Protos.Key.Builder proto = entry.getValue(); - proto.setType(Protos.Key.Type.DETERMINISTIC_KEY); - final Protos.DeterministicKey.Builder detKey = proto.getDeterministicKeyBuilder(); - detKey.setChainCode(ByteString.copyFrom(key.getChainCode())); - for (ChildNumber num : key.getPath()) - detKey.addPath(num.i()); - if (key.equals(externalKey)) { - detKey.setIssuedSubkeys(issuedExternalKeys); - detKey.setLookaheadSize(lookaheadSize); - } else if (key.equals(internalKey)) { - detKey.setIssuedSubkeys(issuedInternalKeys); - detKey.setLookaheadSize(lookaheadSize); - } - // Flag the very first key of following keychain. - if (entries.isEmpty() && isFollowing()) { - detKey.setIsFollowing(true); - } - if (key.getParent() != null) { - // HD keys inherit the timestamp of their parent if they have one, so no need to serialize it. - proto.clearCreationTimestamp(); - } - entries.add(proto.build()); - } - return entries; + beforeSerializeToProtobuf(result); + result.addAll(serializeMyselfToProtobuf()); } finally { lock.unlock(); } + return result; + } + + protected List serializeMyselfToProtobuf() { + // Most of the serialization work is delegated to the basic key chain, which will serialize the bulk of the + // data (handling encryption along the way), and letting us patch it up with the extra data we care about. + LinkedList entries = newLinkedList(); + if (seed != null) { + Protos.Key.Builder mnemonicEntry = BasicKeyChain.serializeEncryptableItem(seed); + mnemonicEntry.setType(Protos.Key.Type.DETERMINISTIC_MNEMONIC); + serializeSeedEncryptableItem(seed, mnemonicEntry); + entries.add(mnemonicEntry.build()); + } + Map keys = basicKeyChain.serializeToEditableProtobufs(); + for (Map.Entry entry : keys.entrySet()) { + DeterministicKey key = (DeterministicKey) entry.getKey(); + Protos.Key.Builder proto = entry.getValue(); + proto.setType(Protos.Key.Type.DETERMINISTIC_KEY); + final Protos.DeterministicKey.Builder detKey = proto.getDeterministicKeyBuilder(); + detKey.setChainCode(ByteString.copyFrom(key.getChainCode())); + for (ChildNumber num : key.getPath()) + detKey.addPath(num.i()); + if (key.equals(externalKey)) { + detKey.setIssuedSubkeys(issuedExternalKeys); + detKey.setLookaheadSize(lookaheadSize); + detKey.setSigsRequiredToSpend(getSigsRequiredToSpend()); + } else if (key.equals(internalKey)) { + detKey.setIssuedSubkeys(issuedInternalKeys); + detKey.setLookaheadSize(lookaheadSize); + detKey.setSigsRequiredToSpend(getSigsRequiredToSpend()); + } + // Flag the very first key of following keychain. + if (entries.isEmpty() && isFollowing()) { + detKey.setIsFollowing(true); + } + if (key.getParent() != null) { + // HD keys inherit the timestamp of their parent if they have one, so no need to serialize it. + proto.clearCreationTimestamp(); + } + entries.add(proto.build()); + } + return entries; } /** @@ -636,12 +747,15 @@ public class DeterministicKeyChain implements EncryptableKeyChain { DeterministicKeyChain chain = null; int lookaheadSize = -1; + int sigsRequiredToSpend = 1; + for (Protos.Key key : keys) { final Protos.Key.Type t = key.getType(); if (t == Protos.Key.Type.DETERMINISTIC_MNEMONIC) { if (chain != null) { checkState(lookaheadSize >= 0); chain.setLookaheadSize(lookaheadSize); + chain.setSigsRequiredToSpend(sigsRequiredToSpend); chain.maybeLookAhead(); chains.add(chain); chain = null; @@ -693,6 +807,7 @@ public class DeterministicKeyChain implements EncryptableKeyChain { if (chain != null) { checkState(lookaheadSize >= 0); chain.setLookaheadSize(lookaheadSize); + chain.setSigsRequiredToSpend(sigsRequiredToSpend); chain.maybeLookAhead(); chains.add(chain); chain = null; @@ -701,15 +816,23 @@ public class DeterministicKeyChain implements EncryptableKeyChain { isFollowingKey = true; } if (chain == null) { + // If this is not a following chain and previous was, this must be married + boolean isMarried = !isFollowingKey && !chains.isEmpty() && chains.get(chains.size() - 1).isFollowing(); if (seed == null) { DeterministicKey accountKey = new DeterministicKey(immutablePath, chainCode, pubkey, null, null); if (!accountKey.getPath().equals(ACCOUNT_ZERO_PATH)) throw new UnreadableWalletException("Expecting account key but found key with path: " + HDUtils.formatPath(accountKey.getPath())); - chain = new DeterministicKeyChain(accountKey, isFollowingKey); + if (isMarried) + chain = new MarriedKeyChain(accountKey); + else + chain = new DeterministicKeyChain(accountKey, isFollowingKey); isWatchingAccountKey = true; } else { - chain = new DeterministicKeyChain(seed, crypter); + if (isMarried) + chain = new MarriedKeyChain(seed, crypter); + else + chain = new DeterministicKeyChain(seed, crypter); chain.lookaheadSize = LAZY_CALCULATE_LOOKAHEAD; // If the seed is encrypted, then the chain is incomplete at this point. However, we will load // it up below as we parse in the keys. We just need to check at the end that we've loaded @@ -759,6 +882,7 @@ public class DeterministicKeyChain implements EncryptableKeyChain { chain.externalKey = detkey; chain.issuedExternalKeys = key.getDeterministicKey().getIssuedSubkeys(); lookaheadSize = Math.max(lookaheadSize, key.getDeterministicKey().getLookaheadSize()); + sigsRequiredToSpend = key.getDeterministicKey().getSigsRequiredToSpend(); } else if (detkey.getChildNumber().num() == 1) { chain.internalKey = detkey; chain.issuedInternalKeys = key.getDeterministicKey().getIssuedSubkeys(); @@ -772,6 +896,7 @@ public class DeterministicKeyChain implements EncryptableKeyChain { if (chain != null) { checkState(lookaheadSize >= 0); chain.setLookaheadSize(lookaheadSize); + chain.setSigsRequiredToSpend(sigsRequiredToSpend); chain.maybeLookAhead(); chains.add(chain); } @@ -1013,6 +1138,10 @@ public class DeterministicKeyChain implements EncryptableKeyChain { return result; } + /** Housekeeping call to call when lookahead might be needed. Normally called automatically by KeychainGroup. */ + public void maybeLookAheadScripts() { + } + /** * Returns number of keys used on external path. This may be fewer than the number that have been deserialized * or held in memory, because of the lookahead zone. @@ -1115,4 +1244,73 @@ public class DeterministicKeyChain implements EncryptableKeyChain { lock.unlock(); } } + + /** + * Whether the keychain is married. A keychain is married when it vends P2SH addresses + * from multiple keychains in a multisig relationship. + * @see org.bitcoinj.wallet.MarriedKeyChain + */ + public boolean isMarried() { + return false; + } + + /** Get redeem data for a key. Only applicable to married keychains. */ + public RedeemData getRedeemData(DeterministicKey followedKey) { + throw new UnsupportedOperationException(); + } + + /** Create a new key and return the matching output script. Only applicable to married keychains. */ + public Script freshOutputScript(KeyPurpose purpose) { + throw new UnsupportedOperationException(); + } + + public String toString(boolean includePrivateKeys, NetworkParameters params) { + final StringBuilder builder2 = new StringBuilder(); + if (seed != null) { + if (seed.isEncrypted()) { + builder2.append(String.format("Seed is encrypted%n")); + } else if (includePrivateKeys) { + final List words = seed.getMnemonicCode(); + builder2.append( + String.format("Seed as words: %s%nSeed as hex: %s%n", Joiner.on(' ').join(words), + seed.toHexString()) + ); + } + builder2.append(String.format("Seed birthday: %d [%s]%n", seed.getCreationTimeSeconds(), new Date(seed.getCreationTimeSeconds() * 1000))); + } + final DeterministicKey watchingKey = getWatchingKey(); + // Don't show if it's been imported from a watching wallet already, because it'd result in a weird/ + // unintuitive result where the watching key in a watching wallet is not the one it was created with + // due to the parent fingerprint being missing/not stored. In future we could store the parent fingerprint + // optionally as well to fix this, but it seems unimportant for now. + if (watchingKey.getParent() != null) { + builder2.append(String.format("Key to watch: %s%n", watchingKey.serializePubB58())); + } + formatAddresses(includePrivateKeys, params, builder2); + return builder2.toString(); + } + + protected void formatAddresses(boolean includePrivateKeys, NetworkParameters params, StringBuilder builder2) { + for (ECKey key : getKeys(false)) + key.formatKeyWithAddress(includePrivateKeys, builder2, params); + } + + /** The number of signatures required to spend coins received by this keychain. */ + public void setSigsRequiredToSpend(int sigsRequiredToSpend) { + this.sigsRequiredToSpend = sigsRequiredToSpend; + } + + /** + * Returns the number of signatures required to spend transactions for this KeyChain. It's the N from + * N-of-M CHECKMULTISIG script for P2SH transactions and always 1 for other transaction types. + */ + public int getSigsRequiredToSpend() { + return sigsRequiredToSpend; + } + + /** Returns the redeem script by its hash or null if this keychain did not generate the script. */ + @Nullable + public RedeemData findRedeemDataByScriptHash(ByteString bytes) { + return null; + } } diff --git a/core/src/main/java/org/bitcoinj/wallet/KeyChainGroup.java b/core/src/main/java/org/bitcoinj/wallet/KeyChainGroup.java index 7f1ff2b6..50f6ce99 100644 --- a/core/src/main/java/org/bitcoinj/wallet/KeyChainGroup.java +++ b/core/src/main/java/org/bitcoinj/wallet/KeyChainGroup.java @@ -26,6 +26,7 @@ import org.bitcoinj.script.ScriptBuilder; import org.bitcoinj.store.UnreadableWalletException; import org.bitcoinj.utils.ListenerRegistration; import org.bitcoinj.utils.Threading; + import com.google.common.base.Joiner; import com.google.common.collect.HashMultimap; import com.google.common.collect.ImmutableList; @@ -67,20 +68,9 @@ public class KeyChainGroup implements KeyBag { private BasicKeyChain basic; private NetworkParameters params; - private final List chains; + private final LinkedList chains; private final EnumMap currentKeys; - // The map keys are the watching keys of the followed chains and values are the following chains - private Multimap followingKeychains; - - // holds a number of signatures required to spend. It's the N from N-of-M CHECKMULTISIG script for P2SH transactions - // and always 1 for other transaction types - private int sigsRequiredToSpend; - - // The map holds P2SH redeem script and corresponding ECKeys issued by this KeyChainGroup (including lookahead) - // mapped to redeem script hashes. - private LinkedHashMap marriedKeysRedeemData; - private EnumMap currentAddresses; @Nullable private KeyCrypter keyCrypter; private int lookaheadSize = -1; @@ -88,12 +78,12 @@ public class KeyChainGroup implements KeyBag { /** 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, 1, null); + this(params, null, new ArrayList(1), null, null); } /** Creates a keychain group with no basic chain, and an HD chain initialized from the given seed. */ public KeyChainGroup(NetworkParameters params, DeterministicSeed seed) { - this(params, null, ImmutableList.of(new DeterministicKeyChain(seed)), null, null, 1, null); + this(params, null, ImmutableList.of(new DeterministicKeyChain(seed)), null, null); } /** @@ -101,7 +91,7 @@ public class KeyChainGroup implements KeyBag { * This HAS to be an account key as returned by {@link DeterministicKeyChain#getWatchingKey()}. */ public KeyChainGroup(NetworkParameters params, DeterministicKey watchKey) { - this(params, null, ImmutableList.of(DeterministicKeyChain.watch(watchKey)), null, null, 1, null); + this(params, null, ImmutableList.of(DeterministicKeyChain.watch(watchKey)), null, null); } /** @@ -110,115 +100,36 @@ public class KeyChainGroup implements KeyBag { * This HAS to be an account key as returned by {@link DeterministicKeyChain#getWatchingKey()}. */ public KeyChainGroup(NetworkParameters params, DeterministicKey watchKey, long creationTimeSecondsSecs) { - this(params, null, ImmutableList.of(DeterministicKeyChain.watch(watchKey, creationTimeSecondsSecs)), null, null, 1, null); - } - - /** - * Creates a keychain group with no basic chain, with an HD chain initialized from the given seed and being followed - * by given list of watch keys. Watch keys have to be account keys. - */ - public KeyChainGroup(NetworkParameters params, DeterministicSeed seed, List followingAccountKeys, int sigsRequiredToSpend) { - this(params, seed); - - addFollowingAccountKeys(followingAccountKeys, sigsRequiredToSpend); - } - - /** - *

Alias for addFollowingAccountKeys(followingAccountKeys, (followingAccountKeys.size() + 1) / 2 + 1)

- *

Creates married keychain requiring majority of keys to spend (2-of-3, 3-of-5 and so on)

- *

IMPORTANT: As of Bitcoin Core 0.9 all multisig transactions which require more than 3 public keys are non-standard - * and such spends won't be processed by peers with default settings, essentially making such transactions almost - * nonspendable

- */ - public void addFollowingAccountKeys(List followingAccountKeys) { - addFollowingAccountKeys(followingAccountKeys, (followingAccountKeys.size() + 1) / 2 + 1); - } - - /** - *

Makes given account keys follow the account key of the active keychain. After that active keychain will be - * treated as married and you will be able to get P2SH addresses to receive coins to. Given sigsRequiredToSpend value - * specifies how many signatures required to spend transactions for this married keychain. This value should not exceed - * total number of keys involved (one followed key plus number of following keys), otherwise IllegalArgumentException - * will be thrown.

- *

IMPORTANT: As of Bitcoin Core 0.9 all multisig transactions which require more than 3 public keys are non-standard - * and such spends won't be processed by peers with default settings, essentially making such transactions almost - * nonspendable

- *

This method will throw an IllegalStateException, if active keychain is already married or already has leaf keys - * issued. In future this behaviour may be replaced with key rotation.

- */ - public void addFollowingAccountKeys(List followingAccountKeys, int sigsRequiredToSpend) { - checkArgument(sigsRequiredToSpend <= followingAccountKeys.size() + 1, "Multisig threshold can't exceed total number of keys"); - checkState(!isMarried(), "KeyChainGroup is married already"); - checkState(getActiveKeyChain().numLeafKeysIssued() == 0, "Active keychain already has keys in use"); - - this.sigsRequiredToSpend = sigsRequiredToSpend; - - DeterministicKey accountKey = getActiveKeyChain().getWatchingKey(); - for (DeterministicKey key : followingAccountKeys) { - checkArgument(key.getPath().size() == 1, "Following keys have to be account keys"); - DeterministicKeyChain chain = DeterministicKeyChain.watchAndFollow(key); - if (lookaheadSize >= 0) - chain.setLookaheadSize(lookaheadSize); - if (lookaheadThreshold >= 0) - chain.setLookaheadThreshold(lookaheadThreshold); - followingKeychains.put(accountKey, chain); - } + this(params, null, ImmutableList.of(DeterministicKeyChain.watch(watchKey, creationTimeSecondsSecs)), null, null); } // Used for deserialization. private KeyChainGroup(NetworkParameters params, @Nullable BasicKeyChain basicKeyChain, List chains, - @Nullable EnumMap currentKeys, Multimap followingKeychains, int sigsRequiredToSpend, @Nullable KeyCrypter crypter) { + @Nullable EnumMap currentKeys, @Nullable KeyCrypter crypter) { this.params = params; this.basic = basicKeyChain == null ? new BasicKeyChain() : basicKeyChain; - this.chains = new ArrayList(checkNotNull(chains)); + this.chains = new LinkedList(checkNotNull(chains)); this.keyCrypter = crypter; this.currentKeys = currentKeys == null ? new EnumMap(KeyChain.KeyPurpose.class) : currentKeys; this.currentAddresses = new EnumMap(KeyChain.KeyPurpose.class); - this.followingKeychains = HashMultimap.create(); - if (followingKeychains != null) { - this.followingKeychains.putAll(followingKeychains); - } - this.sigsRequiredToSpend = sigsRequiredToSpend; - marriedKeysRedeemData = new LinkedHashMap(); maybeLookaheadScripts(); - if (!this.followingKeychains.isEmpty()) { - DeterministicKey followedWatchKey = getActiveKeyChain().getWatchingKey(); + if (isMarried()) { for (Map.Entry entry : this.currentKeys.entrySet()) { - Address address = makeP2SHOutputScript(entry.getValue(), followedWatchKey).getToAddress(params); + Address address = makeP2SHOutputScript(entry.getValue(), getActiveKeyChain()).getToAddress(params); currentAddresses.put(entry.getKey(), address); } } } /** - * This keeps {@link #marriedKeysRedeemData} in sync with the number of keys issued + * This keeps married redeem data in sync with the number of keys issued */ private void maybeLookaheadScripts() { - if (chains.isEmpty()) - return; - - int numLeafKeys = 0; for (DeterministicKeyChain chain : chains) { - numLeafKeys += chain.getLeafKeys().size(); - } - - checkState(marriedKeysRedeemData.size() <= numLeafKeys, "Number of scripts is greater than number of leaf keys"); - if (marriedKeysRedeemData.size() == numLeafKeys) - return; - - for (DeterministicKeyChain chain : chains) { - if (isMarried(chain)) { - chain.maybeLookAhead(); - for (DeterministicKey followedKey : chain.getLeafKeys()) { - RedeemData redeemData = getRedeemData(followedKey, chain.getWatchingKey()); - Script scriptPubKey = ScriptBuilder.createP2SHOutputScript(redeemData.redeemScript); - marriedKeysRedeemData.put(ByteString.copyFrom(scriptPubKey.getPubKeyHash()), redeemData); - } - } + chain.maybeLookAheadScripts(); } } @@ -226,6 +137,14 @@ public class KeyChainGroup implements KeyBag { public void createAndActivateNewHDChain() { // We can't do auto upgrade here because we don't know the rotation time, if any. final DeterministicKeyChain chain = new DeterministicKeyChain(new SecureRandom()); + addAndActivateHDChain(chain); + } + + /** + * Adds an HD chain to the chains list, and make it the default chain (from which keys are issued). + * Useful for adding a complex pre-configured keychain, such as a married wallet. + */ + public void addAndActivateHDChain(DeterministicKeyChain chain) { log.info("Creating and activating a new HD chain: {}", chain); for (ListenerRegistration registration : basic.getListeners()) chain.addEventListener(registration.listener, registration.executor); @@ -249,7 +168,7 @@ public class KeyChainGroup implements KeyBag { */ public DeterministicKey currentKey(KeyChain.KeyPurpose purpose) { DeterministicKeyChain chain = getActiveKeyChain(); - if (isMarried(chain)) { + if (chain.isMarried()) { throw new UnsupportedOperationException("Key is not suitable to receive coins for married keychains." + " Use freshAddress to get P2SH address instead"); } @@ -266,7 +185,7 @@ public class KeyChainGroup implements KeyBag { */ public Address currentAddress(KeyChain.KeyPurpose purpose) { DeterministicKeyChain chain = getActiveKeyChain(); - if (isMarried(chain)) { + if (chain.isMarried()) { Address current = currentAddresses.get(purpose); if (current == null) { current = freshAddress(purpose); @@ -308,7 +227,7 @@ public class KeyChainGroup implements KeyBag { */ public List freshKeys(KeyChain.KeyPurpose purpose, int numberOfKeys) { DeterministicKeyChain chain = getActiveKeyChain(); - if (isMarried(chain)) { + if (chain.isMarried()) { throw new UnsupportedOperationException("Key is not suitable to receive coins for married keychains." + " Use freshAddress to get P2SH address instead"); } @@ -320,10 +239,10 @@ public class KeyChainGroup implements KeyBag { */ public Address freshAddress(KeyChain.KeyPurpose purpose) { DeterministicKeyChain chain = getActiveKeyChain(); - if (isMarried(chain)) { - List marriedKeys = freshMarriedKeys(purpose, chain); - Script p2shScript = makeP2SHOutputScript(marriedKeys); - Address freshAddress = Address.fromP2SHScript(params, p2shScript); + if (chain.isMarried()) { + Script outputScript = chain.freshOutputScript(purpose); + checkState(outputScript.isPayToScriptHash()); // Only handle P2SH for now + Address freshAddress = Address.fromP2SHScript(params, outputScript); maybeLookaheadScripts(); currentAddresses.put(purpose, freshAddress); return freshAddress; @@ -332,28 +251,6 @@ public class KeyChainGroup implements KeyBag { } } - private List freshMarriedKeys(KeyChain.KeyPurpose purpose, DeterministicKeyChain followedKeyChain) { - DeterministicKey followedKey = followedKeyChain.getKey(purpose); - ImmutableList.Builder keys = ImmutableList.builder().add(followedKey); - Collection keyChains = followingKeychains.get(followedKeyChain.getWatchingKey()); - for (DeterministicKeyChain keyChain : keyChains) { - DeterministicKey followingKey = keyChain.getKey(purpose); - checkState(followedKey.getChildNumber().equals(followingKey.getChildNumber()), "Following keychains should be in sync"); - keys.add(followingKey); - } - return keys.build(); - } - - private List getMarriedKeysWithFollowed(DeterministicKey followedKey, Collection followingChains) { - ImmutableList.Builder keys = ImmutableList.builder(); - for (DeterministicKeyChain keyChain : followingChains) { - keyChain.maybeLookAhead(); - keys.add(keyChain.getKeyByPath(followedKey.getPath())); - } - keys.add(followedKey); - return keys.build(); - } - /** Returns the key chain that's used for generation of fresh/current keys. This is always the newest HD chain. */ public DeterministicKeyChain getActiveKeyChain() { if (chains.isEmpty()) { @@ -379,9 +276,6 @@ public class KeyChainGroup implements KeyBag { for (DeterministicKeyChain chain : chains) { chain.setLookaheadSize(lookaheadSize); } - for (DeterministicKeyChain chain : followingKeychains.values()) { - chain.setLookaheadSize(lookaheadSize); - } } /** @@ -456,7 +350,14 @@ public class KeyChainGroup implements KeyBag { @Nullable public RedeemData findRedeemDataFromScriptHash(byte[] scriptHash) { - return marriedKeysRedeemData.get(ByteString.copyFrom(scriptHash)); + // Iterate in reverse order, since the active keychain is the one most likely to have the hit + for (Iterator iter = chains.descendingIterator() ; iter.hasNext() ; ) { + DeterministicKeyChain chain = iter.next(); + RedeemData redeemData = chain.findRedeemDataByScriptHash(ByteString.copyFrom(scriptHash)); + if (redeemData != null) + return redeemData; + } + return null; } @Nullable @@ -552,18 +453,12 @@ public class KeyChainGroup implements KeyBag { } /** - * Returns true if the given keychain is being followed by at least one another keychain - */ - public boolean isMarried(DeterministicKeyChain keychain) { - DeterministicKey watchingKey = keychain.getWatchingKey(); - return followingKeychains.containsKey(watchingKey) && followingKeychains.get(watchingKey).size() > 0; - } - - /** - * An alias for {@link #isMarried(DeterministicKeyChain)} called for the active keychain + * Whether the active keychain is married. A keychain is married when it vends P2SH addresses + * from multiple keychains in a multisig relationship. + * @see org.bitcoinj.wallet.MarriedKeyChain */ public boolean isMarried() { - return isMarried(getActiveKeyChain()); + return !chains.isEmpty() && getActiveKeyChain().isMarried(); } /** @@ -638,12 +533,7 @@ public class KeyChainGroup implements KeyBag { public int getBloomFilterElementCount() { int result = basic.numBloomFilterEntries(); for (DeterministicKeyChain chain : chains) { - if (isMarried(chain)) { - chain.maybeLookAhead(); - result += chain.getLeafKeys().size() * 2; - } else { - result += chain.numBloomFilterEntries(); - } + result += chain.numBloomFilterEntries(); } return result; } @@ -653,15 +543,8 @@ public class KeyChainGroup implements KeyBag { if (basic.numKeys() > 0) filter.merge(basic.getFilter(size, falsePositiveRate, nTweak)); - for (Map.Entry entry : marriedKeysRedeemData.entrySet()) { - filter.insert(entry.getKey().toByteArray()); - filter.insert(entry.getValue().redeemScript.getProgram()); - } - for (DeterministicKeyChain chain : chains) { - if (!isMarried(chain)) { - filter.merge(chain.getFilter(size, falsePositiveRate, nTweak)); - } + filter.merge(chain.getFilter(size, falsePositiveRate, nTweak)); } return filter; } @@ -671,22 +554,8 @@ public class KeyChainGroup implements KeyBag { throw new UnsupportedOperationException(); // Unused. } - private Script makeP2SHOutputScript(List marriedKeys) { - return ScriptBuilder.createP2SHOutputScript(makeRedeemScript(marriedKeys)); - } - - private Script makeP2SHOutputScript(DeterministicKey followedKey, DeterministicKey followedAccountKey) { - return ScriptBuilder.createP2SHOutputScript(getRedeemData(followedKey, followedAccountKey).redeemScript); - } - - private RedeemData getRedeemData(DeterministicKey followedKey, DeterministicKey followedAccountKey) { - Collection followingChains = followingKeychains.get(followedAccountKey); - List marriedKeys = getMarriedKeysWithFollowed(followedKey, followingChains); - return RedeemData.of(marriedKeys, makeRedeemScript(marriedKeys)); - } - - private Script makeRedeemScript(List marriedKeys) { - return ScriptBuilder.createRedeemScript(sigsRequiredToSpend, marriedKeys); + private Script makeP2SHOutputScript(DeterministicKey followedKey, DeterministicKeyChain chain) { + return ScriptBuilder.createP2SHOutputScript(chain.getRedeemData(followedKey).redeemScript); } /** Adds a listener for events that are run when keys are added, on the user thread. */ @@ -719,41 +588,31 @@ public class KeyChainGroup implements KeyBag { else result = Lists.newArrayList(); for (DeterministicKeyChain chain : chains) { - // prepend each chain with it's following chains if any - for (DeterministicKeyChain followingChain : followingKeychains.get(chain.getWatchingKey())) { - result.addAll(followingChain.serializeToProtobuf()); - } List protos = chain.serializeToProtobuf(); result.addAll(protos); } return result; } - public static KeyChainGroup fromProtobufUnencrypted(NetworkParameters params, List keys, int sigsRequiredToSpend) throws UnreadableWalletException { - checkArgument(sigsRequiredToSpend > 0); + public static KeyChainGroup fromProtobufUnencrypted(NetworkParameters params, List keys) throws UnreadableWalletException { BasicKeyChain basicKeyChain = BasicKeyChain.fromProtobufUnencrypted(keys); List chains = DeterministicKeyChain.fromProtobuf(keys, null); EnumMap currentKeys = null; if (!chains.isEmpty()) currentKeys = createCurrentKeysMap(chains); - Multimap followingKeychains = extractFollowingKeychains(chains); - if (sigsRequiredToSpend < 2 && followingKeychains.size() > 0) - throw new IllegalArgumentException("Married KeyChainGroup requires multiple signatures to spend"); - return new KeyChainGroup(params, basicKeyChain, chains, currentKeys, followingKeychains, sigsRequiredToSpend, null); + extractFollowingKeychains(chains); + return new KeyChainGroup(params, basicKeyChain, chains, currentKeys, null); } - public static KeyChainGroup fromProtobufEncrypted(NetworkParameters params, List keys, int sigsRequiredToSpend, KeyCrypter crypter) throws UnreadableWalletException { - checkArgument(sigsRequiredToSpend > 0); + public static KeyChainGroup fromProtobufEncrypted(NetworkParameters params, List keys, KeyCrypter crypter) throws UnreadableWalletException { checkNotNull(crypter); BasicKeyChain basicKeyChain = BasicKeyChain.fromProtobufEncrypted(keys, crypter); List chains = DeterministicKeyChain.fromProtobuf(keys, crypter); EnumMap currentKeys = null; if (!chains.isEmpty()) currentKeys = createCurrentKeysMap(chains); - Multimap followingKeychains = extractFollowingKeychains(chains); - if (sigsRequiredToSpend < 2 && followingKeychains.size() > 0) - throw new IllegalArgumentException("Married KeyChainGroup requires multiple signatures to spend"); - return new KeyChainGroup(params, basicKeyChain, chains, currentKeys, followingKeychains, sigsRequiredToSpend, crypter); + extractFollowingKeychains(chains); + return new KeyChainGroup(params, basicKeyChain, chains, currentKeys, crypter); } /** @@ -848,111 +707,42 @@ public class KeyChainGroup implements KeyBag { return currentKeys; } - private static Multimap extractFollowingKeychains(List chains) { + private static void extractFollowingKeychains(List chains) { // look for following key chains and map them to the watch keys of followed keychains Multimap followingKeychains = HashMultimap.create(); - List followingChains = new ArrayList(); + List followingChains = Lists.newArrayList(); for (Iterator it = chains.iterator(); it.hasNext(); ) { DeterministicKeyChain chain = it.next(); if (chain.isFollowing()) { followingChains.add(chain); it.remove(); } else if (!followingChains.isEmpty()) { - followingKeychains.putAll(chain.getWatchingKey(), followingChains); - followingChains.clear(); + if (!(chain instanceof MarriedKeyChain)) + throw new IllegalStateException(); + ((MarriedKeyChain)chain).setFollowingKeyChains(followingChains); + followingChains = Lists.newArrayList(); } } - return followingKeychains; } public String toString(boolean includePrivateKeys) { final StringBuilder builder = new StringBuilder(); if (basic != null) { for (ECKey key : basic.getKeys()) - formatKeyWithAddress(includePrivateKeys, key, builder); + key.formatKeyWithAddress(includePrivateKeys, builder, params); } List chainStrs = Lists.newLinkedList(); for (DeterministicKeyChain chain : chains) { - final StringBuilder builder2 = new StringBuilder(); - DeterministicSeed seed = chain.getSeed(); - if (seed != null) { - if (seed.isEncrypted()) { - builder2.append(String.format("Seed is encrypted%n")); - } else if (includePrivateKeys) { - final List words = seed.getMnemonicCode(); - builder2.append( - String.format("Seed as words: %s%nSeed as hex: %s%n", Joiner.on(' ').join(words), - seed.toHexString()) - ); - } - builder2.append(String.format("Seed birthday: %d [%s]%n", seed.getCreationTimeSeconds(), new Date(seed.getCreationTimeSeconds() * 1000))); - } - final DeterministicKey watchingKey = chain.getWatchingKey(); - // Don't show if it's been imported from a watching wallet already, because it'd result in a weird/ - // unintuitive result where the watching key in a watching wallet is not the one it was created with - // due to the parent fingerprint being missing/not stored. In future we could store the parent fingerprint - // optionally as well to fix this, but it seems unimportant for now. - if (watchingKey.getParent() != null) { - builder2.append(String.format("Key to watch: %s%n", watchingKey.serializePubB58())); - } - if (isMarried(chain)) { - Collection followingChains = followingKeychains.get(chain.getWatchingKey()); - for (DeterministicKeyChain followingChain : followingChains) { - builder2.append(String.format("Following chain: %s%n", followingChain.getWatchingKey().serializePubB58())); - } - builder2.append(String.format("%n")); - for (RedeemData redeemData : marriedKeysRedeemData.values()) - formatScript(ScriptBuilder.createP2SHOutputScript(redeemData.redeemScript), builder2); - } else { - for (ECKey key : chain.getKeys(false)) - formatKeyWithAddress(includePrivateKeys, key, builder2); - } - chainStrs.add(builder2.toString()); + chainStrs.add(chain.toString(includePrivateKeys, params)); } builder.append(Joiner.on(String.format("%n")).join(chainStrs)); return builder.toString(); } - private void formatScript(Script script, StringBuilder builder) { - builder.append(" addr:"); - builder.append(script.getToAddress(params)); - builder.append(" hash160:"); - builder.append(Utils.HEX.encode(script.getPubKeyHash())); - builder.append("\n"); - } - - private void formatKeyWithAddress(boolean includePrivateKeys, ECKey key, StringBuilder builder) { - final Address address = key.toAddress(params); - builder.append(" addr:"); - builder.append(address.toString()); - builder.append(" hash160:"); - builder.append(Utils.HEX.encode(key.getPubKeyHash())); - if (key instanceof DeterministicKey) { - builder.append(" ("); - builder.append((((DeterministicKey) key).getPathAsString())); - builder.append(")"); - } - builder.append("\n"); - if (includePrivateKeys) { - builder.append(" "); - builder.append(key.toStringWithPrivate()); - builder.append("\n"); - } - } - /** Returns a copy of the current list of chains. */ public List getDeterministicKeyChains() { return new ArrayList(chains); } - - /** - * Returns the number of signatures required to spend transactions for this KeyChainGroup. It's the N from - * N-of-M CHECKMULTISIG script for P2SH transactions and always 1 for other transaction types. - */ - public int getSigsRequiredToSpend() { - return sigsRequiredToSpend; - } - /** * Returns a counter that increases (by an arbitrary amount) each time new keys have been calculated due to * lookahead and thus the Bloom filter that was previously calculated has become stale. diff --git a/core/src/main/java/org/bitcoinj/wallet/MarriedKeyChain.java b/core/src/main/java/org/bitcoinj/wallet/MarriedKeyChain.java new file mode 100644 index 00000000..739b561f --- /dev/null +++ b/core/src/main/java/org/bitcoinj/wallet/MarriedKeyChain.java @@ -0,0 +1,286 @@ +/** + * Copyright 2013 The bitcoinj developers. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.bitcoinj.wallet; + +import com.google.common.collect.ImmutableList; +import com.google.common.collect.Lists; +import com.google.protobuf.ByteString; + +import org.bitcoinj.core.BloomFilter; +import org.bitcoinj.core.ECKey; +import org.bitcoinj.core.NetworkParameters; +import org.bitcoinj.core.Utils; +import org.bitcoinj.crypto.DeterministicKey; +import org.bitcoinj.crypto.KeyCrypter; +import org.bitcoinj.script.Script; +import org.bitcoinj.script.ScriptBuilder; + +import java.security.SecureRandom; +import java.util.LinkedHashMap; +import java.util.List; +import java.util.Map; + +import javax.annotation.Nullable; + +import static com.google.common.base.Preconditions.checkArgument; +import static com.google.common.base.Preconditions.checkNotNull; +import static com.google.common.base.Preconditions.checkState; + +/** + *

A multi-signature keychain using synchronized HD keys (a.k.a HDM)

+ *

This keychain keeps track of following keychains that follow the account key of this keychain. + * You can get P2SH addresses to receive coins to from this chain. The threshold - sigsRequiredToSpend + * specifies how many signatures required to spend transactions for this married keychain. This value should not exceed + * total number of keys involved (one followed key plus number of following keys), otherwise IllegalArgumentException + * will be thrown.

+ *

IMPORTANT: As of Bitcoin Core 0.9 all multisig transactions which require more than 3 public keys are non-standard + * and such spends won't be processed by peers with default settings, essentially making such transactions almost + * nonspendable

+ *

This method will throw an IllegalStateException, if the keychain is already married or already has leaf keys + * issued.

+ */ +public class MarriedKeyChain extends DeterministicKeyChain { + // The map holds P2SH redeem script and corresponding ECKeys issued by this KeyChainGroup (including lookahead) + // mapped to redeem script hashes. + private LinkedHashMap marriedKeysRedeemData = new LinkedHashMap(); + + private List followingKeyChains; + + /** Builds a {@link MarriedKeyChain} */ + public static class Builder> extends DeterministicKeyChain.Builder { + private List followingKeys; + private int threshold; + + protected Builder() { + } + + public T followingKeys(List followingKeys) { + this.followingKeys = followingKeys; + return self(); + } + + public T followingKeys(DeterministicKey followingKey, DeterministicKey ...followingKeys) { + this.followingKeys = Lists.asList(followingKey, followingKeys); + return self(); + } + + /** + * Threshold, or (followingKeys.size() + 1) / 2 + 1) (majority) if unspecified.

+ *

IMPORTANT: As of Bitcoin Core 0.9 all multisig transactions which require more than 3 public keys are non-standard + * and such spends won't be processed by peers with default settings, essentially making such transactions almost + * nonspendable

+ */ + public T threshold(int threshold) { + this.threshold = threshold; + return self(); + } + + public MarriedKeyChain build() { + checkState(random != null || entropy != null || seed != null, "Must provide either entropy or random"); + checkNotNull(followingKeys, "followingKeys must be provided"); + MarriedKeyChain chain; + if (threshold == 0) + threshold = (followingKeys.size() + 1) / 2 + 1; + if (random != null) { + chain = new MarriedKeyChain(random, bits, passphrase, seedCreationTimeSecs); + } else if (entropy != null) { + chain = new MarriedKeyChain(entropy, passphrase, seedCreationTimeSecs); + } else { + chain = new MarriedKeyChain(seed); + } + chain.addFollowingAccountKeys(followingKeys, threshold); + return chain; + } + } + + public static Builder builder() { + return new Builder(); + } + + // Protobuf deserialization constructors + MarriedKeyChain(DeterministicKey accountKey) { + super(accountKey, false); + } + + MarriedKeyChain(DeterministicSeed seed, KeyCrypter crypter) { + super(seed, crypter); + } + + // Builder constructors + private MarriedKeyChain(SecureRandom random, int bits, String passphrase, long seedCreationTimeSecs) { + super(random, bits, passphrase, seedCreationTimeSecs); + } + + private MarriedKeyChain(byte[] entropy, String passphrase, long seedCreationTimeSecs) { + super(entropy, passphrase, seedCreationTimeSecs); + } + + private MarriedKeyChain(DeterministicSeed seed) { + super(seed); + } + + void setFollowingKeyChains(List followingKeyChains) { + checkArgument(!followingKeyChains.isEmpty()); + this.followingKeyChains = followingKeyChains; + } + + @Override + public boolean isMarried() { + return true; + } + + /** Create a new married key and return the matching output script */ + @Override + public Script freshOutputScript(KeyPurpose purpose) { + DeterministicKey followedKey = getKey(purpose); + ImmutableList.Builder keys = ImmutableList.builder().add(followedKey); + for (DeterministicKeyChain keyChain : followingKeyChains) { + DeterministicKey followingKey = keyChain.getKey(purpose); + checkState(followedKey.getChildNumber().equals(followingKey.getChildNumber()), "Following keychains should be in sync"); + keys.add(followingKey); + } + List marriedKeys = keys.build(); + Script redeemScript = ScriptBuilder.createRedeemScript(sigsRequiredToSpend, marriedKeys); + return ScriptBuilder.createP2SHOutputScript(redeemScript); + } + + private List getMarriedKeysWithFollowed(DeterministicKey followedKey) { + ImmutableList.Builder keys = ImmutableList.builder(); + for (DeterministicKeyChain keyChain : followingKeyChains) { + keyChain.maybeLookAhead(); + keys.add(keyChain.getKeyByPath(followedKey.getPath())); + } + keys.add(followedKey); + return keys.build(); + } + + /** Get the redeem data for a key in this married chain */ + @Override + public RedeemData getRedeemData(DeterministicKey followedKey) { + checkState(isMarried()); + List marriedKeys = getMarriedKeysWithFollowed(followedKey); + Script redeemScript = ScriptBuilder.createRedeemScript(sigsRequiredToSpend, marriedKeys); + return RedeemData.of(marriedKeys, redeemScript); + } + + private void addFollowingAccountKeys(List followingAccountKeys, int sigsRequiredToSpend) { + checkArgument(sigsRequiredToSpend <= followingAccountKeys.size() + 1, "Multisig threshold can't exceed total number of keys"); + checkState(numLeafKeysIssued() == 0, "Active keychain already has keys in use"); + checkState(followingKeyChains == null); + + List followingKeyChains = Lists.newArrayList(); + + for (DeterministicKey key : followingAccountKeys) { + checkArgument(key.getPath().size() == 1, "Following keys have to be account keys"); + DeterministicKeyChain chain = DeterministicKeyChain.watchAndFollow(key); + if (lookaheadSize >= 0) + chain.setLookaheadSize(lookaheadSize); + if (lookaheadThreshold >= 0) + chain.setLookaheadThreshold(lookaheadThreshold); + followingKeyChains.add(chain); + } + + this.sigsRequiredToSpend = sigsRequiredToSpend; + this.followingKeyChains = followingKeyChains; + } + + @Override + public void setLookaheadSize(int lookaheadSize) { + lock.lock(); + try { + super.setLookaheadSize(lookaheadSize); + if (followingKeyChains != null) { + for (DeterministicKeyChain followingChain : followingKeyChains) { + followingChain.setLookaheadSize(lookaheadSize); + } + } + } finally { + lock.unlock(); + } + } + + @Override + protected void beforeSerializeToProtobuf(List result) { + super.beforeSerializeToProtobuf(result); + for (DeterministicKeyChain chain : followingKeyChains) { + result.addAll(chain.serializeMyselfToProtobuf()); + } + } + + @Override + protected void formatAddresses(boolean includePrivateKeys, NetworkParameters params, StringBuilder builder2) { + for (DeterministicKeyChain followingChain : followingKeyChains) { + builder2.append(String.format("Following chain: %s%n", followingChain.getWatchingKey().serializePubB58())); + } + builder2.append(String.format("%n")); + for (RedeemData redeemData : marriedKeysRedeemData.values()) + formatScript(ScriptBuilder.createP2SHOutputScript(redeemData.redeemScript), builder2, params); + } + + private void formatScript(Script script, StringBuilder builder, NetworkParameters params) { + builder.append(" addr:"); + builder.append(script.getToAddress(params)); + builder.append(" hash160:"); + builder.append(Utils.HEX.encode(script.getPubKeyHash())); + builder.append("\n"); + } + + @Override + public void maybeLookAheadScripts() { + super.maybeLookAheadScripts(); + int numLeafKeys = getLeafKeys().size(); + + checkState(marriedKeysRedeemData.size() <= numLeafKeys, "Number of scripts is greater than number of leaf keys"); + if (marriedKeysRedeemData.size() == numLeafKeys) + return; + + maybeLookAhead(); + for (DeterministicKey followedKey : getLeafKeys()) { + RedeemData redeemData = getRedeemData(followedKey); + Script scriptPubKey = ScriptBuilder.createP2SHOutputScript(redeemData.redeemScript); + marriedKeysRedeemData.put(ByteString.copyFrom(scriptPubKey.getPubKeyHash()), redeemData); + } + } + + @Nullable + @Override + public RedeemData findRedeemDataByScriptHash(ByteString bytes) { + return marriedKeysRedeemData.get(bytes); + } + + @Override + public BloomFilter getFilter(int size, double falsePositiveRate, long tweak) { + lock.lock(); + BloomFilter filter; + try { + filter = new BloomFilter(size, falsePositiveRate, tweak); + for (Map.Entry entry : marriedKeysRedeemData.entrySet()) { + filter.insert(entry.getKey().toByteArray()); + filter.insert(entry.getValue().redeemScript.getProgram()); + } + } finally { + lock.unlock(); + } + return filter; + } + + @Override + public int numBloomFilterEntries() { + maybeLookAhead(); + return getLeafKeys().size() * 2; + } +} diff --git a/core/src/main/java/org/bitcoinj/wallet/Protos.java b/core/src/main/java/org/bitcoinj/wallet/Protos.java index 75eba3b4..f10b447f 100644 --- a/core/src/main/java/org/bitcoinj/wallet/Protos.java +++ b/core/src/main/java/org/bitcoinj/wallet/Protos.java @@ -1269,6 +1269,26 @@ public final class Protos { *
*/ boolean getIsFollowing(); + + // optional uint32 sigsRequiredToSpend = 6 [default = 1]; + /** + * optional uint32 sigsRequiredToSpend = 6 [default = 1]; + * + *
+     * Number of signatures required to spend. This field is needed only for married keychains to reconstruct KeyChain
+     * and represents the N value from N-of-M CHECKMULTISIG script. For regular single keychains it will always be 1.
+     * 
+ */ + boolean hasSigsRequiredToSpend(); + /** + * optional uint32 sigsRequiredToSpend = 6 [default = 1]; + * + *
+     * Number of signatures required to spend. This field is needed only for married keychains to reconstruct KeyChain
+     * and represents the N value from N-of-M CHECKMULTISIG script. For regular single keychains it will always be 1.
+     * 
+ */ + int getSigsRequiredToSpend(); } /** * Protobuf type {@code wallet.DeterministicKey} @@ -1367,6 +1387,11 @@ public final class Protos { isFollowing_ = input.readBool(); break; } + case 48: { + bitField0_ |= 0x00000010; + sigsRequiredToSpend_ = input.readUInt32(); + break; + } } } } catch (com.google.protobuf.InvalidProtocolBufferException e) { @@ -1554,12 +1579,39 @@ public final class Protos { return isFollowing_; } + // optional uint32 sigsRequiredToSpend = 6 [default = 1]; + public static final int SIGSREQUIREDTOSPEND_FIELD_NUMBER = 6; + private int sigsRequiredToSpend_; + /** + * optional uint32 sigsRequiredToSpend = 6 [default = 1]; + * + *
+     * Number of signatures required to spend. This field is needed only for married keychains to reconstruct KeyChain
+     * and represents the N value from N-of-M CHECKMULTISIG script. For regular single keychains it will always be 1.
+     * 
+ */ + public boolean hasSigsRequiredToSpend() { + return ((bitField0_ & 0x00000010) == 0x00000010); + } + /** + * optional uint32 sigsRequiredToSpend = 6 [default = 1]; + * + *
+     * Number of signatures required to spend. This field is needed only for married keychains to reconstruct KeyChain
+     * and represents the N value from N-of-M CHECKMULTISIG script. For regular single keychains it will always be 1.
+     * 
+ */ + public int getSigsRequiredToSpend() { + return sigsRequiredToSpend_; + } + private void initFields() { chainCode_ = com.google.protobuf.ByteString.EMPTY; path_ = java.util.Collections.emptyList(); issuedSubkeys_ = 0; lookaheadSize_ = 0; isFollowing_ = false; + sigsRequiredToSpend_ = 1; } private byte memoizedIsInitialized = -1; public final boolean isInitialized() { @@ -1592,6 +1644,9 @@ public final class Protos { if (((bitField0_ & 0x00000008) == 0x00000008)) { output.writeBool(5, isFollowing_); } + if (((bitField0_ & 0x00000010) == 0x00000010)) { + output.writeUInt32(6, sigsRequiredToSpend_); + } getUnknownFields().writeTo(output); } @@ -1626,6 +1681,10 @@ public final class Protos { size += com.google.protobuf.CodedOutputStream .computeBoolSize(5, isFollowing_); } + if (((bitField0_ & 0x00000010) == 0x00000010)) { + size += com.google.protobuf.CodedOutputStream + .computeUInt32Size(6, sigsRequiredToSpend_); + } size += getUnknownFields().getSerializedSize(); memoizedSerializedSize = size; return size; @@ -1757,6 +1816,8 @@ public final class Protos { bitField0_ = (bitField0_ & ~0x00000008); isFollowing_ = false; bitField0_ = (bitField0_ & ~0x00000010); + sigsRequiredToSpend_ = 1; + bitField0_ = (bitField0_ & ~0x00000020); return this; } @@ -1806,6 +1867,10 @@ public final class Protos { to_bitField0_ |= 0x00000008; } result.isFollowing_ = isFollowing_; + if (((from_bitField0_ & 0x00000020) == 0x00000020)) { + to_bitField0_ |= 0x00000010; + } + result.sigsRequiredToSpend_ = sigsRequiredToSpend_; result.bitField0_ = to_bitField0_; onBuilt(); return result; @@ -1844,6 +1909,9 @@ public final class Protos { if (other.hasIsFollowing()) { setIsFollowing(other.getIsFollowing()); } + if (other.hasSigsRequiredToSpend()) { + setSigsRequiredToSpend(other.getSigsRequiredToSpend()); + } this.mergeUnknownFields(other.getUnknownFields()); return this; } @@ -2195,6 +2263,59 @@ public final class Protos { return this; } + // optional uint32 sigsRequiredToSpend = 6 [default = 1]; + private int sigsRequiredToSpend_ = 1; + /** + * optional uint32 sigsRequiredToSpend = 6 [default = 1]; + * + *
+       * Number of signatures required to spend. This field is needed only for married keychains to reconstruct KeyChain
+       * and represents the N value from N-of-M CHECKMULTISIG script. For regular single keychains it will always be 1.
+       * 
+ */ + public boolean hasSigsRequiredToSpend() { + return ((bitField0_ & 0x00000020) == 0x00000020); + } + /** + * optional uint32 sigsRequiredToSpend = 6 [default = 1]; + * + *
+       * Number of signatures required to spend. This field is needed only for married keychains to reconstruct KeyChain
+       * and represents the N value from N-of-M CHECKMULTISIG script. For regular single keychains it will always be 1.
+       * 
+ */ + public int getSigsRequiredToSpend() { + return sigsRequiredToSpend_; + } + /** + * optional uint32 sigsRequiredToSpend = 6 [default = 1]; + * + *
+       * Number of signatures required to spend. This field is needed only for married keychains to reconstruct KeyChain
+       * and represents the N value from N-of-M CHECKMULTISIG script. For regular single keychains it will always be 1.
+       * 
+ */ + public Builder setSigsRequiredToSpend(int value) { + bitField0_ |= 0x00000020; + sigsRequiredToSpend_ = value; + onChanged(); + return this; + } + /** + * optional uint32 sigsRequiredToSpend = 6 [default = 1]; + * + *
+       * Number of signatures required to spend. This field is needed only for married keychains to reconstruct KeyChain
+       * and represents the N value from N-of-M CHECKMULTISIG script. For regular single keychains it will always be 1.
+       * 
+ */ + public Builder clearSigsRequiredToSpend() { + bitField0_ = (bitField0_ & ~0x00000020); + sigsRequiredToSpend_ = 1; + onChanged(); + return this; + } + // @@protoc_insertion_point(builder_scope:wallet.DeterministicKey) } @@ -14291,26 +14412,6 @@ public final class Protos { */ org.bitcoinj.wallet.Protos.TransactionSignerOrBuilder getTransactionSignersOrBuilder( int index); - - // optional uint32 sigsRequiredToSpend = 18 [default = 1]; - /** - * optional uint32 sigsRequiredToSpend = 18 [default = 1]; - * - *
-     * Number of signatures required to spend. This field is needed only for married wallets to reconstruct KeyChainGroup
-     * and represents the N value from N-of-M CHECKMULTISIG script. For regular single wallets it will always be 1.
-     * 
- */ - boolean hasSigsRequiredToSpend(); - /** - * optional uint32 sigsRequiredToSpend = 18 [default = 1]; - * - *
-     * Number of signatures required to spend. This field is needed only for married wallets to reconstruct KeyChainGroup
-     * and represents the N value from N-of-M CHECKMULTISIG script. For regular single wallets it will always be 1.
-     * 
- */ - int getSigsRequiredToSpend(); } /** * Protobuf type {@code wallet.Wallet} @@ -14474,11 +14575,6 @@ public final class Protos { transactionSigners_.add(input.readMessage(org.bitcoinj.wallet.Protos.TransactionSigner.PARSER, extensionRegistry)); break; } - case 144: { - bitField0_ |= 0x00000200; - sigsRequiredToSpend_ = input.readUInt32(); - break; - } } } } catch (com.google.protobuf.InvalidProtocolBufferException e) { @@ -15146,32 +15242,6 @@ public final class Protos { return transactionSigners_.get(index); } - // optional uint32 sigsRequiredToSpend = 18 [default = 1]; - public static final int SIGSREQUIREDTOSPEND_FIELD_NUMBER = 18; - private int sigsRequiredToSpend_; - /** - * optional uint32 sigsRequiredToSpend = 18 [default = 1]; - * - *
-     * Number of signatures required to spend. This field is needed only for married wallets to reconstruct KeyChainGroup
-     * and represents the N value from N-of-M CHECKMULTISIG script. For regular single wallets it will always be 1.
-     * 
- */ - public boolean hasSigsRequiredToSpend() { - return ((bitField0_ & 0x00000200) == 0x00000200); - } - /** - * optional uint32 sigsRequiredToSpend = 18 [default = 1]; - * - *
-     * Number of signatures required to spend. This field is needed only for married wallets to reconstruct KeyChainGroup
-     * and represents the N value from N-of-M CHECKMULTISIG script. For regular single wallets it will always be 1.
-     * 
- */ - public int getSigsRequiredToSpend() { - return sigsRequiredToSpend_; - } - private void initFields() { networkIdentifier_ = ""; lastSeenBlockHash_ = com.google.protobuf.ByteString.EMPTY; @@ -15188,7 +15258,6 @@ public final class Protos { keyRotationTime_ = 0L; tags_ = java.util.Collections.emptyList(); transactionSigners_ = java.util.Collections.emptyList(); - sigsRequiredToSpend_ = 1; } private byte memoizedIsInitialized = -1; public final boolean isInitialized() { @@ -15293,9 +15362,6 @@ public final class Protos { for (int i = 0; i < transactionSigners_.size(); i++) { output.writeMessage(17, transactionSigners_.get(i)); } - if (((bitField0_ & 0x00000200) == 0x00000200)) { - output.writeUInt32(18, sigsRequiredToSpend_); - } getUnknownFields().writeTo(output); } @@ -15365,10 +15431,6 @@ public final class Protos { size += com.google.protobuf.CodedOutputStream .computeMessageSize(17, transactionSigners_.get(i)); } - if (((bitField0_ & 0x00000200) == 0x00000200)) { - size += com.google.protobuf.CodedOutputStream - .computeUInt32Size(18, sigsRequiredToSpend_); - } size += getUnknownFields().getSerializedSize(); memoizedSerializedSize = size; return size; @@ -15554,8 +15616,6 @@ public final class Protos { } else { transactionSignersBuilder_.clear(); } - sigsRequiredToSpend_ = 1; - bitField0_ = (bitField0_ & ~0x00008000); return this; } @@ -15678,10 +15738,6 @@ public final class Protos { } else { result.transactionSigners_ = transactionSignersBuilder_.build(); } - if (((from_bitField0_ & 0x00008000) == 0x00008000)) { - to_bitField0_ |= 0x00000200; - } - result.sigsRequiredToSpend_ = sigsRequiredToSpend_; result.bitField0_ = to_bitField0_; onBuilt(); return result; @@ -15885,9 +15941,6 @@ public final class Protos { } } } - if (other.hasSigsRequiredToSpend()) { - setSigsRequiredToSpend(other.getSigsRequiredToSpend()); - } this.mergeUnknownFields(other.getUnknownFields()); return this; } @@ -18066,59 +18119,6 @@ public final class Protos { return transactionSignersBuilder_; } - // optional uint32 sigsRequiredToSpend = 18 [default = 1]; - private int sigsRequiredToSpend_ = 1; - /** - * optional uint32 sigsRequiredToSpend = 18 [default = 1]; - * - *
-       * Number of signatures required to spend. This field is needed only for married wallets to reconstruct KeyChainGroup
-       * and represents the N value from N-of-M CHECKMULTISIG script. For regular single wallets it will always be 1.
-       * 
- */ - public boolean hasSigsRequiredToSpend() { - return ((bitField0_ & 0x00008000) == 0x00008000); - } - /** - * optional uint32 sigsRequiredToSpend = 18 [default = 1]; - * - *
-       * Number of signatures required to spend. This field is needed only for married wallets to reconstruct KeyChainGroup
-       * and represents the N value from N-of-M CHECKMULTISIG script. For regular single wallets it will always be 1.
-       * 
- */ - public int getSigsRequiredToSpend() { - return sigsRequiredToSpend_; - } - /** - * optional uint32 sigsRequiredToSpend = 18 [default = 1]; - * - *
-       * Number of signatures required to spend. This field is needed only for married wallets to reconstruct KeyChainGroup
-       * and represents the N value from N-of-M CHECKMULTISIG script. For regular single wallets it will always be 1.
-       * 
- */ - public Builder setSigsRequiredToSpend(int value) { - bitField0_ |= 0x00008000; - sigsRequiredToSpend_ = value; - onChanged(); - return this; - } - /** - * optional uint32 sigsRequiredToSpend = 18 [default = 1]; - * - *
-       * Number of signatures required to spend. This field is needed only for married wallets to reconstruct KeyChainGroup
-       * and represents the N value from N-of-M CHECKMULTISIG script. For regular single wallets it will always be 1.
-       * 
- */ - public Builder clearSigsRequiredToSpend() { - bitField0_ = (bitField0_ & ~0x00008000); - sigsRequiredToSpend_ = 1; - onChanged(); - return this; - } - // @@protoc_insertion_point(builder_scope:wallet.Wallet) } @@ -18995,81 +18995,81 @@ public final class Protos { "\nip_address\030\001 \002(\014\022\014\n\004port\030\002 \002(\r\022\020\n\010servi" + "ces\030\003 \002(\004\"M\n\rEncryptedData\022\035\n\025initialisa" + "tion_vector\030\001 \002(\014\022\035\n\025encrypted_private_k" + - "ey\030\002 \002(\014\"y\n\020DeterministicKey\022\022\n\nchain_co" + - "de\030\001 \002(\014\022\014\n\004path\030\002 \003(\r\022\026\n\016issued_subkeys" + - "\030\003 \001(\r\022\026\n\016lookahead_size\030\004 \001(\r\022\023\n\013isFoll" + - "owing\030\005 \001(\010\"\232\003\n\003Key\022\036\n\004type\030\001 \002(\0162\020.wall" + - "et.Key.Type\022\024\n\014secret_bytes\030\002 \001(\014\022-\n\016enc" + - "rypted_data\030\006 \001(\0132\025.wallet.EncryptedData", - "\022\022\n\npublic_key\030\003 \001(\014\022\r\n\005label\030\004 \001(\t\022\032\n\022c" + - "reation_timestamp\030\005 \001(\003\0223\n\021deterministic" + - "_key\030\007 \001(\0132\030.wallet.DeterministicKey\022\032\n\022" + - "deterministic_seed\030\010 \001(\014\022;\n\034encrypted_de" + - "terministic_seed\030\t \001(\0132\025.wallet.Encrypte" + - "dData\"a\n\004Type\022\014\n\010ORIGINAL\020\001\022\030\n\024ENCRYPTED" + - "_SCRYPT_AES\020\002\022\032\n\026DETERMINISTIC_MNEMONIC\020" + - "\003\022\025\n\021DETERMINISTIC_KEY\020\004\"5\n\006Script\022\017\n\007pr" + - "ogram\030\001 \002(\014\022\032\n\022creation_timestamp\030\002 \002(\003\"" + - "\222\001\n\020TransactionInput\022\"\n\032transaction_out_", - "point_hash\030\001 \002(\014\022#\n\033transaction_out_poin" + - "t_index\030\002 \002(\r\022\024\n\014script_bytes\030\003 \002(\014\022\020\n\010s" + - "equence\030\004 \001(\r\022\r\n\005value\030\005 \001(\003\"\177\n\021Transact" + - "ionOutput\022\r\n\005value\030\001 \002(\003\022\024\n\014script_bytes" + - "\030\002 \002(\014\022!\n\031spent_by_transaction_hash\030\003 \001(" + - "\014\022\"\n\032spent_by_transaction_index\030\004 \001(\005\"\211\003" + - "\n\025TransactionConfidence\0220\n\004type\030\001 \001(\0162\"." + - "wallet.TransactionConfidence.Type\022\032\n\022app" + - "eared_at_height\030\002 \001(\005\022\036\n\026overriding_tran" + - "saction\030\003 \001(\014\022\r\n\005depth\030\004 \001(\005\022)\n\014broadcas", - "t_by\030\006 \003(\0132\023.wallet.PeerAddress\0224\n\006sourc" + - "e\030\007 \001(\0162$.wallet.TransactionConfidence.S" + - "ource\"O\n\004Type\022\013\n\007UNKNOWN\020\000\022\014\n\010BUILDING\020\001" + - "\022\013\n\007PENDING\020\002\022\025\n\021NOT_IN_BEST_CHAIN\020\003\022\010\n\004" + - "DEAD\020\004\"A\n\006Source\022\022\n\016SOURCE_UNKNOWN\020\000\022\022\n\016" + - "SOURCE_NETWORK\020\001\022\017\n\013SOURCE_SELF\020\002\"\264\005\n\013Tr" + - "ansaction\022\017\n\007version\030\001 \002(\005\022\014\n\004hash\030\002 \002(\014" + - "\022&\n\004pool\030\003 \001(\0162\030.wallet.Transaction.Pool" + - "\022\021\n\tlock_time\030\004 \001(\r\022\022\n\nupdated_at\030\005 \001(\003\022" + - "3\n\021transaction_input\030\006 \003(\0132\030.wallet.Tran", - "sactionInput\0225\n\022transaction_output\030\007 \003(\013" + - "2\031.wallet.TransactionOutput\022\022\n\nblock_has" + - "h\030\010 \003(\014\022 \n\030block_relativity_offsets\030\013 \003(" + - "\005\0221\n\nconfidence\030\t \001(\0132\035.wallet.Transacti" + - "onConfidence\0225\n\007purpose\030\n \001(\0162\033.wallet.T" + - "ransaction.Purpose:\007UNKNOWN\022+\n\rexchange_" + - "rate\030\014 \001(\0132\024.wallet.ExchangeRate\022\014\n\004memo" + - "\030\r \001(\t\"Y\n\004Pool\022\013\n\007UNSPENT\020\004\022\t\n\005SPENT\020\005\022\014" + - "\n\010INACTIVE\020\002\022\010\n\004DEAD\020\n\022\013\n\007PENDING\020\020\022\024\n\020P" + - "ENDING_INACTIVE\020\022\"\224\001\n\007Purpose\022\013\n\007UNKNOWN", - "\020\000\022\020\n\014USER_PAYMENT\020\001\022\020\n\014KEY_ROTATION\020\002\022\034" + - "\n\030ASSURANCE_CONTRACT_CLAIM\020\003\022\035\n\031ASSURANC" + - "E_CONTRACT_PLEDGE\020\004\022\033\n\027ASSURANCE_CONTRAC" + - "T_STUB\020\005\"N\n\020ScryptParameters\022\014\n\004salt\030\001 \002" + - "(\014\022\020\n\001n\030\002 \001(\003:\00516384\022\014\n\001r\030\003 \001(\005:\0018\022\014\n\001p\030" + - "\004 \001(\005:\0011\"8\n\tExtension\022\n\n\002id\030\001 \002(\t\022\014\n\004dat" + - "a\030\002 \002(\014\022\021\n\tmandatory\030\003 \002(\010\" \n\003Tag\022\013\n\003tag" + - "\030\001 \002(\t\022\014\n\004data\030\002 \002(\014\"5\n\021TransactionSigne" + - "r\022\022\n\nclass_name\030\001 \002(\t\022\014\n\004data\030\002 \001(\014\"\211\005\n\006" + - "Wallet\022\032\n\022network_identifier\030\001 \002(\t\022\034\n\024la", - "st_seen_block_hash\030\002 \001(\014\022\036\n\026last_seen_bl" + - "ock_height\030\014 \001(\r\022!\n\031last_seen_block_time" + - "_secs\030\016 \001(\003\022\030\n\003key\030\003 \003(\0132\013.wallet.Key\022(\n" + - "\013transaction\030\004 \003(\0132\023.wallet.Transaction\022" + - "&\n\016watched_script\030\017 \003(\0132\016.wallet.Script\022" + - "C\n\017encryption_type\030\005 \001(\0162\035.wallet.Wallet" + - ".EncryptionType:\013UNENCRYPTED\0227\n\025encrypti" + - "on_parameters\030\006 \001(\0132\030.wallet.ScryptParam" + - "eters\022\022\n\007version\030\007 \001(\005:\0011\022$\n\textension\030\n" + - " \003(\0132\021.wallet.Extension\022\023\n\013description\030\013", - " \001(\t\022\031\n\021key_rotation_time\030\r \001(\004\022\031\n\004tags\030" + - "\020 \003(\0132\013.wallet.Tag\0226\n\023transaction_signer" + - "s\030\021 \003(\0132\031.wallet.TransactionSigner\022\036\n\023si" + - "gsRequiredToSpend\030\022 \001(\r:\0011\";\n\016Encryption" + - "Type\022\017\n\013UNENCRYPTED\020\001\022\030\n\024ENCRYPTED_SCRYP" + - "T_AES\020\002\"R\n\014ExchangeRate\022\022\n\ncoin_value\030\001 " + - "\002(\003\022\022\n\nfiat_value\030\002 \002(\003\022\032\n\022fiat_currency" + - "_code\030\003 \002(\tB\035\n\023org.bitcoinj.walletB\006Prot" + - "os" + "ey\030\002 \002(\014\"\231\001\n\020DeterministicKey\022\022\n\nchain_c" + + "ode\030\001 \002(\014\022\014\n\004path\030\002 \003(\r\022\026\n\016issued_subkey" + + "s\030\003 \001(\r\022\026\n\016lookahead_size\030\004 \001(\r\022\023\n\013isFol" + + "lowing\030\005 \001(\010\022\036\n\023sigsRequiredToSpend\030\006 \001(" + + "\r:\0011\"\232\003\n\003Key\022\036\n\004type\030\001 \002(\0162\020.wallet.Key." + + "Type\022\024\n\014secret_bytes\030\002 \001(\014\022-\n\016encrypted_", + "data\030\006 \001(\0132\025.wallet.EncryptedData\022\022\n\npub" + + "lic_key\030\003 \001(\014\022\r\n\005label\030\004 \001(\t\022\032\n\022creation" + + "_timestamp\030\005 \001(\003\0223\n\021deterministic_key\030\007 " + + "\001(\0132\030.wallet.DeterministicKey\022\032\n\022determi" + + "nistic_seed\030\010 \001(\014\022;\n\034encrypted_determini" + + "stic_seed\030\t \001(\0132\025.wallet.EncryptedData\"a" + + "\n\004Type\022\014\n\010ORIGINAL\020\001\022\030\n\024ENCRYPTED_SCRYPT" + + "_AES\020\002\022\032\n\026DETERMINISTIC_MNEMONIC\020\003\022\025\n\021DE" + + "TERMINISTIC_KEY\020\004\"5\n\006Script\022\017\n\007program\030\001" + + " \002(\014\022\032\n\022creation_timestamp\030\002 \002(\003\"\222\001\n\020Tra", + "nsactionInput\022\"\n\032transaction_out_point_h" + + "ash\030\001 \002(\014\022#\n\033transaction_out_point_index" + + "\030\002 \002(\r\022\024\n\014script_bytes\030\003 \002(\014\022\020\n\010sequence" + + "\030\004 \001(\r\022\r\n\005value\030\005 \001(\003\"\177\n\021TransactionOutp" + + "ut\022\r\n\005value\030\001 \002(\003\022\024\n\014script_bytes\030\002 \002(\014\022" + + "!\n\031spent_by_transaction_hash\030\003 \001(\014\022\"\n\032sp" + + "ent_by_transaction_index\030\004 \001(\005\"\211\003\n\025Trans" + + "actionConfidence\0220\n\004type\030\001 \001(\0162\".wallet." + + "TransactionConfidence.Type\022\032\n\022appeared_a" + + "t_height\030\002 \001(\005\022\036\n\026overriding_transaction", + "\030\003 \001(\014\022\r\n\005depth\030\004 \001(\005\022)\n\014broadcast_by\030\006 " + + "\003(\0132\023.wallet.PeerAddress\0224\n\006source\030\007 \001(\016" + + "2$.wallet.TransactionConfidence.Source\"O" + + "\n\004Type\022\013\n\007UNKNOWN\020\000\022\014\n\010BUILDING\020\001\022\013\n\007PEN" + + "DING\020\002\022\025\n\021NOT_IN_BEST_CHAIN\020\003\022\010\n\004DEAD\020\004\"" + + "A\n\006Source\022\022\n\016SOURCE_UNKNOWN\020\000\022\022\n\016SOURCE_" + + "NETWORK\020\001\022\017\n\013SOURCE_SELF\020\002\"\264\005\n\013Transacti" + + "on\022\017\n\007version\030\001 \002(\005\022\014\n\004hash\030\002 \002(\014\022&\n\004poo" + + "l\030\003 \001(\0162\030.wallet.Transaction.Pool\022\021\n\tloc" + + "k_time\030\004 \001(\r\022\022\n\nupdated_at\030\005 \001(\003\0223\n\021tran", + "saction_input\030\006 \003(\0132\030.wallet.Transaction" + + "Input\0225\n\022transaction_output\030\007 \003(\0132\031.wall" + + "et.TransactionOutput\022\022\n\nblock_hash\030\010 \003(\014" + + "\022 \n\030block_relativity_offsets\030\013 \003(\005\0221\n\nco" + + "nfidence\030\t \001(\0132\035.wallet.TransactionConfi" + + "dence\0225\n\007purpose\030\n \001(\0162\033.wallet.Transact" + + "ion.Purpose:\007UNKNOWN\022+\n\rexchange_rate\030\014 " + + "\001(\0132\024.wallet.ExchangeRate\022\014\n\004memo\030\r \001(\t\"" + + "Y\n\004Pool\022\013\n\007UNSPENT\020\004\022\t\n\005SPENT\020\005\022\014\n\010INACT" + + "IVE\020\002\022\010\n\004DEAD\020\n\022\013\n\007PENDING\020\020\022\024\n\020PENDING_", + "INACTIVE\020\022\"\224\001\n\007Purpose\022\013\n\007UNKNOWN\020\000\022\020\n\014U" + + "SER_PAYMENT\020\001\022\020\n\014KEY_ROTATION\020\002\022\034\n\030ASSUR" + + "ANCE_CONTRACT_CLAIM\020\003\022\035\n\031ASSURANCE_CONTR" + + "ACT_PLEDGE\020\004\022\033\n\027ASSURANCE_CONTRACT_STUB\020" + + "\005\"N\n\020ScryptParameters\022\014\n\004salt\030\001 \002(\014\022\020\n\001n" + + "\030\002 \001(\003:\00516384\022\014\n\001r\030\003 \001(\005:\0018\022\014\n\001p\030\004 \001(\005:\001" + + "1\"8\n\tExtension\022\n\n\002id\030\001 \002(\t\022\014\n\004data\030\002 \002(\014" + + "\022\021\n\tmandatory\030\003 \002(\010\" \n\003Tag\022\013\n\003tag\030\001 \002(\t\022" + + "\014\n\004data\030\002 \002(\014\"5\n\021TransactionSigner\022\022\n\ncl" + + "ass_name\030\001 \002(\t\022\014\n\004data\030\002 \001(\014\"\351\004\n\006Wallet\022", + "\032\n\022network_identifier\030\001 \002(\t\022\034\n\024last_seen" + + "_block_hash\030\002 \001(\014\022\036\n\026last_seen_block_hei" + + "ght\030\014 \001(\r\022!\n\031last_seen_block_time_secs\030\016" + + " \001(\003\022\030\n\003key\030\003 \003(\0132\013.wallet.Key\022(\n\013transa" + + "ction\030\004 \003(\0132\023.wallet.Transaction\022&\n\016watc" + + "hed_script\030\017 \003(\0132\016.wallet.Script\022C\n\017encr" + + "yption_type\030\005 \001(\0162\035.wallet.Wallet.Encryp" + + "tionType:\013UNENCRYPTED\0227\n\025encryption_para" + + "meters\030\006 \001(\0132\030.wallet.ScryptParameters\022\022" + + "\n\007version\030\007 \001(\005:\0011\022$\n\textension\030\n \003(\0132\021.", + "wallet.Extension\022\023\n\013description\030\013 \001(\t\022\031\n" + + "\021key_rotation_time\030\r \001(\004\022\031\n\004tags\030\020 \003(\0132\013" + + ".wallet.Tag\0226\n\023transaction_signers\030\021 \003(\013" + + "2\031.wallet.TransactionSigner\";\n\016Encryptio" + + "nType\022\017\n\013UNENCRYPTED\020\001\022\030\n\024ENCRYPTED_SCRY" + + "PT_AES\020\002\"R\n\014ExchangeRate\022\022\n\ncoin_value\030\001" + + " \002(\003\022\022\n\nfiat_value\030\002 \002(\003\022\032\n\022fiat_currenc" + + "y_code\030\003 \002(\tB\035\n\023org.bitcoinj.walletB\006Pro" + + "tos" }; com.google.protobuf.Descriptors.FileDescriptor.InternalDescriptorAssigner assigner = new com.google.protobuf.Descriptors.FileDescriptor.InternalDescriptorAssigner() { @@ -19093,7 +19093,7 @@ public final class Protos { internal_static_wallet_DeterministicKey_fieldAccessorTable = new com.google.protobuf.GeneratedMessage.FieldAccessorTable( internal_static_wallet_DeterministicKey_descriptor, - new java.lang.String[] { "ChainCode", "Path", "IssuedSubkeys", "LookaheadSize", "IsFollowing", }); + new java.lang.String[] { "ChainCode", "Path", "IssuedSubkeys", "LookaheadSize", "IsFollowing", "SigsRequiredToSpend", }); internal_static_wallet_Key_descriptor = getDescriptor().getMessageTypes().get(3); internal_static_wallet_Key_fieldAccessorTable = new @@ -19159,7 +19159,7 @@ public final class Protos { internal_static_wallet_Wallet_fieldAccessorTable = new com.google.protobuf.GeneratedMessage.FieldAccessorTable( internal_static_wallet_Wallet_descriptor, - new java.lang.String[] { "NetworkIdentifier", "LastSeenBlockHash", "LastSeenBlockHeight", "LastSeenBlockTimeSecs", "Key", "Transaction", "WatchedScript", "EncryptionType", "EncryptionParameters", "Version", "Extension", "Description", "KeyRotationTime", "Tags", "TransactionSigners", "SigsRequiredToSpend", }); + new java.lang.String[] { "NetworkIdentifier", "LastSeenBlockHash", "LastSeenBlockHeight", "LastSeenBlockTimeSecs", "Key", "Transaction", "WatchedScript", "EncryptionType", "EncryptionParameters", "Version", "Extension", "Description", "KeyRotationTime", "Tags", "TransactionSigners", }); internal_static_wallet_ExchangeRate_descriptor = getDescriptor().getMessageTypes().get(14); internal_static_wallet_ExchangeRate_fieldAccessorTable = new diff --git a/core/src/test/java/org/bitcoinj/core/WalletTest.java b/core/src/test/java/org/bitcoinj/core/WalletTest.java index 04a6b77f..c3c82ae4 100644 --- a/core/src/test/java/org/bitcoinj/core/WalletTest.java +++ b/core/src/test/java/org/bitcoinj/core/WalletTest.java @@ -111,7 +111,11 @@ public class WalletTest extends TestWithWallet { wallet.addTransactionSigner(new KeyChainTransactionSigner(keyChain)); } - wallet.addFollowingAccountKeys(followingKeys, threshold); + MarriedKeyChain chain = MarriedKeyChain.builder() + .random(new SecureRandom()) + .followingKeys(followingKeys) + .threshold(threshold).build(); + wallet.addAndActivateHDChain(chain); } @Test @@ -2652,7 +2656,11 @@ public class WalletTest extends TestWithWallet { } }; wallet.addTransactionSigner(signer); - wallet.addFollowingAccountKeys(ImmutableList.of(partnerKey)); + MarriedKeyChain chain = MarriedKeyChain.builder() + .random(new SecureRandom()) + .followingKeys(partnerKey) + .build(); + wallet.addAndActivateHDChain(chain); myAddress = wallet.currentAddress(KeyChain.KeyPurpose.RECEIVE_FUNDS); sendMoneyToWallet(wallet, COIN, myAddress, AbstractBlockChain.NewBlockType.BEST_CHAIN); diff --git a/core/src/test/java/org/bitcoinj/store/WalletProtobufSerializerTest.java b/core/src/test/java/org/bitcoinj/store/WalletProtobufSerializerTest.java index bed91ac5..67c300cc 100644 --- a/core/src/test/java/org/bitcoinj/store/WalletProtobufSerializerTest.java +++ b/core/src/test/java/org/bitcoinj/store/WalletProtobufSerializerTest.java @@ -31,6 +31,8 @@ import org.bitcoinj.wallet.DeterministicKeyChain; import org.bitcoinj.wallet.KeyChain; import com.google.common.collect.ImmutableList; import com.google.protobuf.ByteString; + +import org.bitcoinj.wallet.MarriedKeyChain; import org.bitcoinj.wallet.Protos; import org.junit.Before; import org.junit.Test; @@ -287,16 +289,20 @@ public class WalletProtobufSerializerTest { public void testRoundTripMarriedWallet() throws Exception { // create 2-of-2 married wallet myWallet = new Wallet(params); - final DeterministicKeyChain keyChain = new DeterministicKeyChain(new SecureRandom()); - DeterministicKey partnerKey = DeterministicKey.deserializeB58(null, keyChain.getWatchingKey().serializePubB58()); + final DeterministicKeyChain partnerChain = new DeterministicKeyChain(new SecureRandom()); + DeterministicKey partnerKey = DeterministicKey.deserializeB58(null, partnerChain.getWatchingKey().serializePubB58()); + MarriedKeyChain chain = MarriedKeyChain.builder() + .random(new SecureRandom()) + .followingKeys(partnerKey) + .threshold(2).build(); + myWallet.addAndActivateHDChain(chain); - myWallet.addFollowingAccountKeys(ImmutableList.of(partnerKey), 2); myAddress = myWallet.currentAddress(KeyChain.KeyPurpose.RECEIVE_FUNDS); Wallet wallet1 = roundTrip(myWallet); assertEquals(0, wallet1.getTransactions(true).size()); assertEquals(Coin.ZERO, wallet1.getBalance()); - assertEquals(2, wallet1.getSigsRequiredToSpend()); + assertEquals(2, wallet1.getActiveKeychain().getSigsRequiredToSpend()); assertEquals(myAddress, wallet1.currentAddress(KeyChain.KeyPurpose.RECEIVE_FUNDS)); } diff --git a/core/src/test/java/org/bitcoinj/wallet/KeyChainGroupTest.java b/core/src/test/java/org/bitcoinj/wallet/KeyChainGroupTest.java index 4e0520d7..8629d731 100644 --- a/core/src/test/java/org/bitcoinj/wallet/KeyChainGroupTest.java +++ b/core/src/test/java/org/bitcoinj/wallet/KeyChainGroupTest.java @@ -56,14 +56,24 @@ public class KeyChainGroupTest { } private KeyChainGroup createMarriedKeyChainGroup() { - byte[] entropy = Sha256Hash.create("don't use a seed like this in real life".getBytes()).getBytes(); - DeterministicSeed seed = new DeterministicSeed(entropy, "", MnemonicCode.BIP39_STANDARDISATION_TIME_SECS); - KeyChainGroup group = new KeyChainGroup(params, seed, ImmutableList.of(watchingAccountKey), 2); + KeyChainGroup group = new KeyChainGroup(params); + DeterministicKeyChain chain = createMarriedKeyChain(); + group.addAndActivateHDChain(chain); group.setLookaheadSize(LOOKAHEAD_SIZE); group.getActiveKeyChain(); return group; } + private MarriedKeyChain createMarriedKeyChain() { + byte[] entropy = Sha256Hash.create("don't use a seed like this in real life".getBytes()).getBytes(); + DeterministicSeed seed = new DeterministicSeed(entropy, "", MnemonicCode.BIP39_STANDARDISATION_TIME_SECS); + MarriedKeyChain chain = MarriedKeyChain.builder() + .seed(seed) + .followingKeys(watchingAccountKey) + .threshold(2).build(); + return chain; + } + @Test public void freshCurrentKeys() throws Exception { int numKeys = ((group.getLookaheadSize() + group.getLookaheadThreshold()) * 2) // * 2 because of internal/external @@ -400,7 +410,7 @@ public class KeyChainGroupTest { @Test public void serialization() throws Exception { assertEquals(INITIAL_KEYS + 1 /* for the seed */, group.serializeToProtobuf().size()); - group = KeyChainGroup.fromProtobufUnencrypted(params, group.serializeToProtobuf(), 1); + group = KeyChainGroup.fromProtobufUnencrypted(params, group.serializeToProtobuf()); group.freshKey(KeyChain.KeyPurpose.RECEIVE_FUNDS); DeterministicKey key1 = group.freshKey(KeyChain.KeyPurpose.RECEIVE_FUNDS); DeterministicKey key2 = group.freshKey(KeyChain.KeyPurpose.CHANGE); @@ -411,13 +421,13 @@ public class KeyChainGroupTest { List protoKeys2 = group.serializeToProtobuf(); assertEquals(INITIAL_KEYS + ((LOOKAHEAD_SIZE + 1) * 2) + 1 /* for the seed */ + 2, protoKeys2.size()); - group = KeyChainGroup.fromProtobufUnencrypted(params, protoKeys1, 1); + group = KeyChainGroup.fromProtobufUnencrypted(params, protoKeys1); assertEquals(INITIAL_KEYS + ((LOOKAHEAD_SIZE + 1) * 2) + 1 /* for the seed */ + 1, protoKeys1.size()); assertTrue(group.hasKey(key1)); assertTrue(group.hasKey(key2)); assertEquals(key2, group.currentKey(KeyChain.KeyPurpose.CHANGE)); assertEquals(key1, group.currentKey(KeyChain.KeyPurpose.RECEIVE_FUNDS)); - group = KeyChainGroup.fromProtobufUnencrypted(params, protoKeys2, 1); + group = KeyChainGroup.fromProtobufUnencrypted(params, protoKeys2); assertEquals(INITIAL_KEYS + ((LOOKAHEAD_SIZE + 1) * 2) + 1 /* for the seed */ + 2, protoKeys2.size()); assertTrue(group.hasKey(key1)); assertTrue(group.hasKey(key2)); @@ -426,7 +436,7 @@ public class KeyChainGroupTest { final KeyParameter aesKey = scrypt.deriveKey("password"); group.encrypt(scrypt, aesKey); List protoKeys3 = group.serializeToProtobuf(); - group = KeyChainGroup.fromProtobufEncrypted(params, protoKeys3, 1, scrypt); + group = KeyChainGroup.fromProtobufEncrypted(params, protoKeys3, scrypt); assertTrue(group.isEncrypted()); assertTrue(group.checkPassword("password")); group.decrypt(aesKey); @@ -443,7 +453,7 @@ public class KeyChainGroupTest { group.getBloomFilterElementCount(); // Force lookahead. List protoKeys1 = group.serializeToProtobuf(); assertEquals(3 + (group.getLookaheadSize() + group.getLookaheadThreshold() + 1) * 2, protoKeys1.size()); - group = KeyChainGroup.fromProtobufUnencrypted(params, protoKeys1, 1); + group = KeyChainGroup.fromProtobufUnencrypted(params, protoKeys1); assertEquals(3 + (group.getLookaheadSize() + group.getLookaheadThreshold() + 1) * 2, group.serializeToProtobuf().size()); } @@ -452,12 +462,12 @@ public class KeyChainGroupTest { group = createMarriedKeyChainGroup(); Address address1 = group.currentAddress(KeyChain.KeyPurpose.RECEIVE_FUNDS); assertTrue(group.isMarried()); - assertEquals(2, group.getSigsRequiredToSpend()); + assertEquals(2, group.getActiveKeyChain().getSigsRequiredToSpend()); List protoKeys = group.serializeToProtobuf(); - KeyChainGroup group2 = KeyChainGroup.fromProtobufUnencrypted(params, protoKeys, 2); + KeyChainGroup group2 = KeyChainGroup.fromProtobufUnencrypted(params, protoKeys); assertTrue(group2.isMarried()); - assertEquals(2, group.getSigsRequiredToSpend()); + assertEquals(2, group.getActiveKeyChain().getSigsRequiredToSpend()); Address address2 = group2.currentAddress(KeyChain.KeyPurpose.RECEIVE_FUNDS); assertEquals(address1, address2); } @@ -465,23 +475,10 @@ public class KeyChainGroupTest { @Test public void addFollowingAccounts() throws Exception { assertFalse(group.isMarried()); - group.addFollowingAccountKeys(ImmutableList.of(watchingAccountKey)); + group.addAndActivateHDChain(createMarriedKeyChain()); assertTrue(group.isMarried()); } - @Test (expected = IllegalStateException.class) - public void addFollowingAccountsTwiceShouldFail() { - ImmutableList followingKeys = ImmutableList.of(watchingAccountKey); - group.addFollowingAccountKeys(followingKeys); - group.addFollowingAccountKeys(followingKeys); - } - - @Test (expected = IllegalStateException.class) - public void addFollowingAccountsForUsedKeychainShouldFail() { - group.freshAddress(KeyChain.KeyPurpose.RECEIVE_FUNDS); - group.addFollowingAccountKeys(ImmutableList.of(watchingAccountKey)); - } - @Test public void constructFromSeed() throws Exception { ECKey key1 = group.freshKey(KeyChain.KeyPurpose.RECEIVE_FUNDS); @@ -519,7 +516,7 @@ public class KeyChainGroupTest { DeterministicSeed seed1 = group.getActiveKeyChain().getSeed(); assertNotNull(seed1); - group = KeyChainGroup.fromProtobufUnencrypted(params, protobufs, 1); + group = KeyChainGroup.fromProtobufUnencrypted(params, protobufs); group.upgradeToDeterministic(0, null); // Should give same result as last time. DeterministicKey dkey2 = group.freshKey(KeyChain.KeyPurpose.RECEIVE_FUNDS); DeterministicSeed seed2 = group.getActiveKeyChain().getSeed(); diff --git a/core/src/test/resources/org/bitcoinj/wallet/deterministic-wallet-serialization.txt b/core/src/test/resources/org/bitcoinj/wallet/deterministic-wallet-serialization.txt index 33b501c1..5e648b16 100644 --- a/core/src/test/resources/org/bitcoinj/wallet/deterministic-wallet-serialization.txt +++ b/core/src/test/resources/org/bitcoinj/wallet/deterministic-wallet-serialization.txt @@ -28,6 +28,7 @@ deterministic_key { path: 0 issued_subkeys: 2 lookahead_size: 10 + sigsRequiredToSpend: 1 } type: DETERMINISTIC_KEY @@ -39,6 +40,7 @@ deterministic_key { path: 1 issued_subkeys: 1 lookahead_size: 10 + sigsRequiredToSpend: 1 } type: DETERMINISTIC_KEY diff --git a/core/src/test/resources/org/bitcoinj/wallet/watching-wallet-serialization.txt b/core/src/test/resources/org/bitcoinj/wallet/watching-wallet-serialization.txt index d4b79408..8ab174ab 100644 --- a/core/src/test/resources/org/bitcoinj/wallet/watching-wallet-serialization.txt +++ b/core/src/test/resources/org/bitcoinj/wallet/watching-wallet-serialization.txt @@ -14,6 +14,7 @@ deterministic_key { path: 0 issued_subkeys: 2 lookahead_size: 10 + sigsRequiredToSpend: 1 } type: DETERMINISTIC_KEY @@ -24,6 +25,7 @@ deterministic_key { path: 1 issued_subkeys: 1 lookahead_size: 10 + sigsRequiredToSpend: 1 } type: DETERMINISTIC_KEY diff --git a/core/src/wallet.proto b/core/src/wallet.proto index 4aa4d921..27dad389 100644 --- a/core/src/wallet.proto +++ b/core/src/wallet.proto @@ -66,6 +66,10 @@ message DeterministicKey { * a single P2SH multisignature address */ optional bool isFollowing = 5; + + // Number of signatures required to spend. This field is needed only for married keychains to reconstruct KeyChain + // and represents the N value from N-of-M CHECKMULTISIG script. For regular single keychains it will always be 1. + optional uint32 sigsRequiredToSpend = 6 [default = 1]; } /** @@ -379,11 +383,7 @@ message Wallet { // transaction signers added to the wallet repeated TransactionSigner transaction_signers = 17; - // Number of signatures required to spend. This field is needed only for married wallets to reconstruct KeyChainGroup - // and represents the N value from N-of-M CHECKMULTISIG script. For regular single wallets it will always be 1. - optional uint32 sigsRequiredToSpend = 18 [default = 1]; - - // Next tag: 19 + // Next tag: 18 } /** An exchange rate between Bitcoin and some fiat currency. */ diff --git a/tools/src/main/java/org/bitcoinj/tools/WalletTool.java b/tools/src/main/java/org/bitcoinj/tools/WalletTool.java index c255c7c4..084ad256 100644 --- a/tools/src/main/java/org/bitcoinj/tools/WalletTool.java +++ b/tools/src/main/java/org/bitcoinj/tools/WalletTool.java @@ -48,6 +48,8 @@ import joptsimple.OptionParser; import joptsimple.OptionSet; import joptsimple.OptionSpec; import joptsimple.util.DateConverter; + +import org.bitcoinj.wallet.MarriedKeyChain; import org.bitcoinj.wallet.Protos; import org.slf4j.Logger; import org.slf4j.LoggerFactory; @@ -59,6 +61,7 @@ import java.io.*; import java.math.BigInteger; import java.net.InetAddress; import java.net.UnknownHostException; +import java.security.SecureRandom; import java.text.ParseException; import java.util.Date; import java.util.List; @@ -428,7 +431,11 @@ public class WalletTool { for (String xpubkey : xpubkeys) { keys.add(DeterministicKey.deserializeB58(null, xpubkey.trim())); } - wallet.addFollowingAccountKeys(keys.build()); + MarriedKeyChain chain = MarriedKeyChain.builder() + .random(new SecureRandom()) + .followingKeys(keys.build()) + .build(); + wallet.addAndActivateHDChain(chain); } private static void rotate() throws BlockStoreException {