From 5910a7f25ebc8c60882a741f288dd017a3f61bff Mon Sep 17 00:00:00 2001 From: Kosta Korenkov <7r0ggy@gmail.com> Date: Sun, 24 Aug 2014 21:04:35 +0700 Subject: [PATCH] Married wallets: multisig threshold could be specified That allows to create multisigs like 3-of-3. If not specified, majority of keys will be required (2-of-3, 3-of-5 and so on) --- .../java/com/google/bitcoin/core/Wallet.java | 42 +++++- .../store/WalletProtobufSerializer.java | 8 +- .../testing/KeyChainTransactionSigner.java | 50 +++++++ .../google/bitcoin/wallet/KeyChainGroup.java | 75 ++++++++-- .../main/java/org/bitcoinj/wallet/Protos.java | 137 +++++++++++++++++- .../com/google/bitcoin/core/WalletTest.java | 59 ++++---- .../store/WalletProtobufSerializerTest.java | 23 ++- .../bitcoin/wallet/KeyChainGroupTest.java | 18 ++- core/src/wallet.proto | 7 +- 9 files changed, 352 insertions(+), 67 deletions(-) create mode 100644 core/src/main/java/com/google/bitcoin/testing/KeyChainTransactionSigner.java diff --git a/core/src/main/java/com/google/bitcoin/core/Wallet.java b/core/src/main/java/com/google/bitcoin/core/Wallet.java index d3cd925b..31226991 100644 --- a/core/src/main/java/com/google/bitcoin/core/Wallet.java +++ b/core/src/main/java/com/google/bitcoin/core/Wallet.java @@ -293,6 +293,21 @@ public class Wallet extends BaseTaggableObject implements Serializable, BlockCha return params; } + /** + * 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. + */ + public int getSigsRequiredToSpend() { + lock.lock(); + try { + return keychain.getSigsRequiredToSpend(); + } finally { + lock.unlock(); + } + } + /** *

Adds given transaction signer to the list of signers. It will be added to the end of the signers list, so if * this wallet already has some signers added, given signer will be executed after all of them.

@@ -615,9 +630,11 @@ public class Wallet extends BaseTaggableObject implements Serializable, BlockCha } /** - * 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. - * This method should be called only once before key rotation, otherwise it will throw an IllegalStateException. + *

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

*/ public void addFollowingAccountKeys(List followingAccountKeys) { lock.lock(); @@ -628,6 +645,25 @@ public class Wallet extends BaseTaggableObject implements Serializable, BlockCha } } + /** + * 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(); + } + } + /** See {@link com.google.bitcoin.wallet.DeterministicKeyChain#setLookaheadSize(int)} for more info on this. */ public void setKeychainLookaheadSize(int lookaheadSize) { lock.lock(); diff --git a/core/src/main/java/com/google/bitcoin/store/WalletProtobufSerializer.java b/core/src/main/java/com/google/bitcoin/store/WalletProtobufSerializer.java index 8539d008..f48ea41f 100644 --- a/core/src/main/java/com/google/bitcoin/store/WalletProtobufSerializer.java +++ b/core/src/main/java/com/google/bitcoin/store/WalletProtobufSerializer.java @@ -205,6 +205,8 @@ public class WalletProtobufSerializer { walletBuilder.addTransactionSigners(protoSigner); } + walletBuilder.setSigsRequiredToSpend(wallet.getSigsRequiredToSpend()); + // Populate the wallet version. walletBuilder.setVersion(wallet.getVersion()); @@ -408,14 +410,16 @@ 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(), keyCrypter); + chain = KeyChainGroup.fromProtobufEncrypted(params, walletProto.getKeyList(), sigsRequiredToSpend, keyCrypter); } else { - chain = KeyChainGroup.fromProtobufUnencrypted(params, walletProto.getKeyList()); + chain = KeyChainGroup.fromProtobufUnencrypted(params, walletProto.getKeyList(), sigsRequiredToSpend); } Wallet wallet = factory.create(params, chain); diff --git a/core/src/main/java/com/google/bitcoin/testing/KeyChainTransactionSigner.java b/core/src/main/java/com/google/bitcoin/testing/KeyChainTransactionSigner.java new file mode 100644 index 00000000..14072ea3 --- /dev/null +++ b/core/src/main/java/com/google/bitcoin/testing/KeyChainTransactionSigner.java @@ -0,0 +1,50 @@ +/** + * Copyright 2014 Kosta Korenkov + * + * 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 com.google.bitcoin.testing; + +import com.google.bitcoin.core.Sha256Hash; +import com.google.bitcoin.crypto.ChildNumber; +import com.google.bitcoin.crypto.DeterministicKey; +import com.google.bitcoin.signers.CustomTransactionSigner; +import com.google.bitcoin.wallet.DeterministicKeyChain; +import com.google.common.collect.ImmutableList; + +import java.util.List; + +/** + *

Transaction signer which uses provided keychain to get signing keys from. It relies on previous signer to provide + * derivation path to be used to get signing key and, once gets the key, just signs given transaction immediately.

+ * It should not be used in test scenarios involving serialization as it doesn't have proper serialize/deserialize + * implementation. + */ +public class KeyChainTransactionSigner extends CustomTransactionSigner { + + private DeterministicKeyChain keyChain; + + public KeyChainTransactionSigner() { + } + + public KeyChainTransactionSigner(DeterministicKeyChain keyChain) { + this.keyChain = keyChain; + } + + @Override + protected SignatureAndKey getSignature(Sha256Hash sighash, List derivationPath) { + ImmutableList keyPath = ImmutableList.copyOf(derivationPath); + DeterministicKey key = keyChain.getKeyByPath(keyPath, true); + return new SignatureAndKey(key.sign(sighash), key.getPubOnly()); + } +} diff --git a/core/src/main/java/com/google/bitcoin/wallet/KeyChainGroup.java b/core/src/main/java/com/google/bitcoin/wallet/KeyChainGroup.java index 235d4d8f..58d8e19d 100644 --- a/core/src/main/java/com/google/bitcoin/wallet/KeyChainGroup.java +++ b/core/src/main/java/com/google/bitcoin/wallet/KeyChainGroup.java @@ -26,6 +26,7 @@ import com.google.bitcoin.script.ScriptBuilder; import com.google.bitcoin.store.UnreadableWalletException; import com.google.bitcoin.utils.ListenerRegistration; import com.google.bitcoin.utils.Threading; +import com.google.common.annotations.VisibleForTesting; import com.google.common.base.Joiner; import com.google.common.collect.HashMultimap; import com.google.common.collect.ImmutableList; @@ -74,6 +75,10 @@ public class KeyChainGroup implements KeyBag { // 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; @@ -85,12 +90,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, null); + this(params, null, new ArrayList(1), null, null, 1, 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, null); + this(params, null, ImmutableList.of(new DeterministicKeyChain(seed)), null, null, 1, null); } /** @@ -98,7 +103,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, null); + this(params, null, ImmutableList.of(DeterministicKeyChain.watch(watchKey)), null, null, 1, null); } /** @@ -107,28 +112,49 @@ 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, null); + 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) { + public KeyChainGroup(NetworkParameters params, DeterministicSeed seed, List followingAccountKeys, int sigsRequiredToSpend) { this(params, seed); - addFollowingAccountKeys(followingAccountKeys); + addFollowingAccountKeys(followingAccountKeys, sigsRequiredToSpend); } /** - * 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. - * 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 + *

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"); @@ -142,7 +168,9 @@ public class KeyChainGroup implements KeyBag { } // Used for deserialization. - private KeyChainGroup(NetworkParameters params, @Nullable BasicKeyChain basicKeyChain, List chains, @Nullable EnumMap currentKeys, Multimap followingKeychains, @Nullable KeyCrypter crypter) { + private KeyChainGroup(NetworkParameters params, @Nullable BasicKeyChain basicKeyChain, List chains, + @Nullable EnumMap currentKeys, Multimap followingKeychains, int sigsRequiredToSpend, @Nullable KeyCrypter crypter) { this.params = params; this.basic = basicKeyChain == null ? new BasicKeyChain() : basicKeyChain; this.chains = new ArrayList(checkNotNull(chains)); @@ -155,6 +183,7 @@ public class KeyChainGroup implements KeyBag { if (followingKeychains != null) { this.followingKeychains.putAll(followingKeychains); } + this.sigsRequiredToSpend = sigsRequiredToSpend; marriedKeysRedeemData = new LinkedHashMap(); maybeLookaheadScripts(); @@ -660,7 +689,7 @@ public class KeyChainGroup implements KeyBag { } private Script makeRedeemScript(List marriedKeys) { - return ScriptBuilder.createRedeemScript((marriedKeys.size() / 2) + 1, marriedKeys); + return ScriptBuilder.createRedeemScript(sigsRequiredToSpend, marriedKeys); } /** Adds a listener for events that are run when keys are added, on the user thread. */ @@ -703,17 +732,21 @@ public class KeyChainGroup implements KeyBag { return result; } - public static KeyChainGroup fromProtobufUnencrypted(NetworkParameters params, List keys) throws UnreadableWalletException { + public static KeyChainGroup fromProtobufUnencrypted(NetworkParameters params, List keys, int sigsRequiredToSpend) throws UnreadableWalletException { + checkArgument(sigsRequiredToSpend > 0); BasicKeyChain basicKeyChain = BasicKeyChain.fromProtobufUnencrypted(keys); List chains = DeterministicKeyChain.fromProtobuf(keys, null); EnumMap currentKeys = null; if (!chains.isEmpty()) currentKeys = createCurrentKeysMap(chains); Multimap followingKeychains = extractFollowingKeychains(chains); - return new KeyChainGroup(params, basicKeyChain, chains, currentKeys, followingKeychains, null); + 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); } - public static KeyChainGroup fromProtobufEncrypted(NetworkParameters params, List keys, KeyCrypter crypter) throws UnreadableWalletException { + public static KeyChainGroup fromProtobufEncrypted(NetworkParameters params, List keys, int sigsRequiredToSpend, KeyCrypter crypter) throws UnreadableWalletException { + checkArgument(sigsRequiredToSpend > 0); checkNotNull(crypter); BasicKeyChain basicKeyChain = BasicKeyChain.fromProtobufEncrypted(keys, crypter); List chains = DeterministicKeyChain.fromProtobuf(keys, crypter); @@ -721,7 +754,9 @@ public class KeyChainGroup implements KeyBag { if (!chains.isEmpty()) currentKeys = createCurrentKeysMap(chains); Multimap followingKeychains = extractFollowingKeychains(chains); - return new KeyChainGroup(params, basicKeyChain, chains, currentKeys, followingKeychains, crypter); + 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); } /** @@ -912,4 +947,12 @@ public class KeyChainGroup implements KeyBag { 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; + } } diff --git a/core/src/main/java/org/bitcoinj/wallet/Protos.java b/core/src/main/java/org/bitcoinj/wallet/Protos.java index 958d2b61..bcb1fc97 100644 --- a/core/src/main/java/org/bitcoinj/wallet/Protos.java +++ b/core/src/main/java/org/bitcoinj/wallet/Protos.java @@ -13903,6 +13903,26 @@ 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} @@ -14066,6 +14086,11 @@ 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) { @@ -14733,6 +14758,32 @@ 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; @@ -14749,6 +14800,7 @@ 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() { @@ -14853,6 +14905,9 @@ 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); } @@ -14922,6 +14977,10 @@ 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; @@ -15107,6 +15166,8 @@ public final class Protos { } else { transactionSignersBuilder_.clear(); } + sigsRequiredToSpend_ = 1; + bitField0_ = (bitField0_ & ~0x00008000); return this; } @@ -15229,6 +15290,10 @@ 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; @@ -15432,6 +15497,9 @@ public final class Protos { } } } + if (other.hasSigsRequiredToSpend()) { + setSigsRequiredToSpend(other.getSigsRequiredToSpend()); + } this.mergeUnknownFields(other.getUnknownFields()); return this; } @@ -17610,6 +17678,59 @@ 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) } @@ -18538,7 +18659,7 @@ public final class Protos { "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\nclas" + - "s_name\030\001 \002(\t\022\014\n\004data\030\002 \001(\014\"\351\004\n\006Wallet\022\032\n" + + "s_name\030\001 \002(\t\022\014\n\004data\030\002 \001(\014\"\211\005\n\006Wallet\022\032\n" + "\022network_identifier\030\001 \002(\t\022\034\n\024last_seen_b" + "lock_hash\030\002 \001(\014\022\036\n\026last_seen_block_heigh" + "t\030\014 \001(\r\022!\n\031last_seen_block_time_secs\030\016 \001", @@ -18552,12 +18673,12 @@ public final class Protos { "llet.Extension\022\023\n\013description\030\013 \001(\t\022\031\n\021k" + "ey_rotation_time\030\r \001(\004\022\031\n\004tags\030\020 \003(\0132\013.w" + "allet.Tag\0226\n\023transaction_signers\030\021 \003(\0132\031", - ".wallet.TransactionSigner\";\n\016EncryptionT" + - "ype\022\017\n\013UNENCRYPTED\020\001\022\030\n\024ENCRYPTED_SCRYPT" + - "_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\006Proto" + - "s" + ".wallet.TransactionSigner\022\036\n\023sigsRequire" + + "dToSpend\030\022 \001(\r:\0011\";\n\016EncryptionType\022\017\n\013U" + + "NENCRYPTED\020\001\022\030\n\024ENCRYPTED_SCRYPT_AES\020\002\"R" + + "\n\014ExchangeRate\022\022\n\ncoin_value\030\001 \002(\003\022\022\n\nfi" + + "at_value\030\002 \002(\003\022\032\n\022fiat_currency_code\030\003 \002" + + "(\tB\035\n\023org.bitcoinj.walletB\006Protos" }; com.google.protobuf.Descriptors.FileDescriptor.InternalDescriptorAssigner assigner = new com.google.protobuf.Descriptors.FileDescriptor.InternalDescriptorAssigner() { @@ -18647,7 +18768,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", }); + new java.lang.String[] { "NetworkIdentifier", "LastSeenBlockHash", "LastSeenBlockHeight", "LastSeenBlockTimeSecs", "Key", "Transaction", "WatchedScript", "EncryptionType", "EncryptionParameters", "Version", "Extension", "Description", "KeyRotationTime", "Tags", "TransactionSigners", "SigsRequiredToSpend", }); internal_static_wallet_ExchangeRate_descriptor = getDescriptor().getMessageTypes().get(14); internal_static_wallet_ExchangeRate_fieldAccessorTable = new diff --git a/core/src/test/java/com/google/bitcoin/core/WalletTest.java b/core/src/test/java/com/google/bitcoin/core/WalletTest.java index 2f73f121..e6cd274e 100644 --- a/core/src/test/java/com/google/bitcoin/core/WalletTest.java +++ b/core/src/test/java/com/google/bitcoin/core/WalletTest.java @@ -19,15 +19,11 @@ package com.google.bitcoin.core; import com.google.bitcoin.core.Wallet.SendRequest; import com.google.bitcoin.crypto.*; -import com.google.bitcoin.signers.CustomTransactionSigner; import com.google.bitcoin.signers.TransactionSigner; import com.google.bitcoin.store.BlockStoreException; import com.google.bitcoin.store.MemoryBlockStore; import com.google.bitcoin.store.WalletProtobufSerializer; -import com.google.bitcoin.testing.FakeTxBuilder; -import com.google.bitcoin.testing.MockTransactionBroadcaster; -import com.google.bitcoin.testing.NopTransactionSigner; -import com.google.bitcoin.testing.TestWithWallet; +import com.google.bitcoin.testing.*; import com.google.bitcoin.utils.ExchangeRate; import com.google.bitcoin.utils.Fiat; import com.google.bitcoin.utils.Threading; @@ -96,31 +92,26 @@ public class WalletTest extends TestWithWallet { super.tearDown(); } - private void createMarriedWalletWithSigner() throws BlockStoreException { - createMarriedWallet(true); + private void createMarriedWallet(int threshold, int numKeys) throws BlockStoreException { + createMarriedWallet(threshold, numKeys, true); } - private void createMarriedWallet(boolean addSigners) throws BlockStoreException { + + private void createMarriedWallet(int threshold, int numKeys, boolean addSigners) throws BlockStoreException { wallet = new Wallet(params); blockStore = new MemoryBlockStore(params); chain = new BlockChain(params, wallet, blockStore); - final DeterministicKeyChain keyChain = new DeterministicKeyChain(new SecureRandom()); - DeterministicKey partnerKey = DeterministicKey.deserializeB58(null, keyChain.getWatchingKey().serializePubB58()); - - if (addSigners) { - CustomTransactionSigner signer = new CustomTransactionSigner() { - @Override - protected SignatureAndKey getSignature(Sha256Hash sighash, List derivationPath) { - ImmutableList keyPath = ImmutableList.copyOf(derivationPath); - DeterministicKey key = keyChain.getKeyByPath(keyPath, true); - return new SignatureAndKey(key.sign(sighash), key.getPubOnly()); - } - }; - wallet.addTransactionSigner(signer); + List followingKeys = Lists.newArrayList(); + for (int i = 0; i < numKeys - 1; i++) { + final DeterministicKeyChain keyChain = new DeterministicKeyChain(new SecureRandom()); + DeterministicKey partnerKey = DeterministicKey.deserializeB58(null, keyChain.getWatchingKey().serializePubB58()); + followingKeys.add(partnerKey); + if (addSigners && i < threshold) + wallet.addTransactionSigner(new KeyChainTransactionSigner(keyChain)); } - wallet.addFollowingAccountKeys(ImmutableList.of(partnerKey)); + wallet.addFollowingAccountKeys(followingKeys, threshold); } @Test @@ -152,10 +143,22 @@ public class WalletTest extends TestWithWallet { @Test public void basicSpendingFromP2SH() throws Exception { - createMarriedWalletWithSigner(); - Address destination = new ECKey().toAddress(params); + createMarriedWallet(2, 2); myAddress = wallet.currentAddress(KeyChain.KeyPurpose.RECEIVE_FUNDS); - basicSpendingCommon(wallet, myAddress, destination, false); + basicSpendingCommon(wallet, myAddress, new ECKey().toAddress(params), false); + + createMarriedWallet(2, 3); + myAddress = wallet.currentAddress(KeyChain.KeyPurpose.RECEIVE_FUNDS); + basicSpendingCommon(wallet, myAddress, new ECKey().toAddress(params), false); + + createMarriedWallet(3, 3); + myAddress = wallet.currentAddress(KeyChain.KeyPurpose.RECEIVE_FUNDS); + basicSpendingCommon(wallet, myAddress, new ECKey().toAddress(params), false); + } + + @Test (expected = IllegalArgumentException.class) + public void thresholdShouldNotExceedNumberOfKeys() throws Exception { + createMarriedWallet(3, 2); } @Test @@ -1236,7 +1239,7 @@ public class WalletTest extends TestWithWallet { @Test public void marriedKeychainBloomFilter() throws Exception { - createMarriedWalletWithSigner(); + createMarriedWallet(2, 2); Address address = wallet.currentReceiveAddress(); assertTrue(wallet.getBloomFilter(0.001).contains(address.getHash160())); @@ -2451,7 +2454,7 @@ public class WalletTest extends TestWithWallet { @Test (expected = TransactionSigner.MissingSignatureException.class) public void completeTxPartiallySignedMarriedThrowsByDefault() throws Exception { - createMarriedWallet(false); + createMarriedWallet(2, 2, false); myAddress = wallet.currentAddress(KeyChain.KeyPurpose.RECEIVE_FUNDS); sendMoneyToWallet(wallet, COIN, myAddress, AbstractBlockChain.NewBlockType.BEST_CHAIN); @@ -2461,7 +2464,7 @@ public class WalletTest extends TestWithWallet { public void completeTxPartiallySignedMarried(Wallet.MissingSigsMode missSigMode, byte[] expectedSig) throws Exception { // create married wallet without signer - createMarriedWallet(false); + createMarriedWallet(2, 2, false); myAddress = wallet.currentAddress(KeyChain.KeyPurpose.RECEIVE_FUNDS); sendMoneyToWallet(wallet, COIN, myAddress, AbstractBlockChain.NewBlockType.BEST_CHAIN); diff --git a/core/src/test/java/com/google/bitcoin/store/WalletProtobufSerializerTest.java b/core/src/test/java/com/google/bitcoin/store/WalletProtobufSerializerTest.java index c0ecd762..a7bb9878 100644 --- a/core/src/test/java/com/google/bitcoin/store/WalletProtobufSerializerTest.java +++ b/core/src/test/java/com/google/bitcoin/store/WalletProtobufSerializerTest.java @@ -20,12 +20,16 @@ package com.google.bitcoin.store; import com.google.bitcoin.core.*; import com.google.bitcoin.core.TransactionConfidence.ConfidenceType; +import com.google.bitcoin.crypto.DeterministicKey; import com.google.bitcoin.params.MainNetParams; import com.google.bitcoin.params.UnitTestParams; import com.google.bitcoin.script.ScriptBuilder; import com.google.bitcoin.testing.FakeTxBuilder; import com.google.bitcoin.utils.BriefLogFormatter; import com.google.bitcoin.utils.Threading; +import com.google.bitcoin.wallet.DeterministicKeyChain; +import com.google.bitcoin.wallet.KeyChain; +import com.google.common.collect.ImmutableList; import com.google.protobuf.ByteString; import org.bitcoinj.wallet.Protos; import org.junit.Before; @@ -35,6 +39,7 @@ import java.io.ByteArrayInputStream; import java.io.ByteArrayOutputStream; import java.math.BigInteger; import java.net.InetAddress; +import java.security.SecureRandom; import java.util.ArrayList; import java.util.Date; import java.util.Iterator; @@ -258,7 +263,6 @@ public class WalletProtobufSerializerTest { private static Wallet roundTrip(Wallet wallet) throws Exception { ByteArrayOutputStream output = new ByteArrayOutputStream(); - //System.out.println(WalletProtobufSerializer.walletToText(wallet)); new WalletProtobufSerializer().writeWallet(wallet, output); ByteArrayInputStream test = new ByteArrayInputStream(output.toByteArray()); assertTrue(WalletProtobufSerializer.isWallet(test)); @@ -279,6 +283,23 @@ public class WalletProtobufSerializerTest { wallet1.findKeyFromPubHash(myKey.getPubKeyHash()).getCreationTimeSeconds()); } + @Test + 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()); + + 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(myAddress, wallet1.currentAddress(KeyChain.KeyPurpose.RECEIVE_FUNDS)); + } + @Test public void coinbaseTxns() throws Exception { // Covers issue 420 where the outpoint index of a coinbase tx input was being mis-serialized. diff --git a/core/src/test/java/com/google/bitcoin/wallet/KeyChainGroupTest.java b/core/src/test/java/com/google/bitcoin/wallet/KeyChainGroupTest.java index 8e3205f5..7e432e56 100644 --- a/core/src/test/java/com/google/bitcoin/wallet/KeyChainGroupTest.java +++ b/core/src/test/java/com/google/bitcoin/wallet/KeyChainGroupTest.java @@ -58,7 +58,7 @@ 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)); + KeyChainGroup group = new KeyChainGroup(params, seed, ImmutableList.of(watchingAccountKey), 2); group.setLookaheadSize(LOOKAHEAD_SIZE); group.getActiveKeyChain(); return group; @@ -400,7 +400,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()); + group = KeyChainGroup.fromProtobufUnencrypted(params, group.serializeToProtobuf(), 1); group.freshKey(KeyChain.KeyPurpose.RECEIVE_FUNDS); DeterministicKey key1 = group.freshKey(KeyChain.KeyPurpose.RECEIVE_FUNDS); DeterministicKey key2 = group.freshKey(KeyChain.KeyPurpose.CHANGE); @@ -411,13 +411,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); + group = KeyChainGroup.fromProtobufUnencrypted(params, protoKeys1, 1); 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); + group = KeyChainGroup.fromProtobufUnencrypted(params, protoKeys2, 1); assertEquals(INITIAL_KEYS + ((LOOKAHEAD_SIZE + 1) * 2) + 1 /* for the seed */ + 2, protoKeys2.size()); assertTrue(group.hasKey(key1)); assertTrue(group.hasKey(key2)); @@ -426,7 +426,7 @@ public class KeyChainGroupTest { final KeyParameter aesKey = scrypt.deriveKey("password"); group.encrypt(scrypt, aesKey); List protoKeys3 = group.serializeToProtobuf(); - group = KeyChainGroup.fromProtobufEncrypted(params, protoKeys3, scrypt); + group = KeyChainGroup.fromProtobufEncrypted(params, protoKeys3, 1, scrypt); assertTrue(group.isEncrypted()); assertTrue(group.checkPassword("password")); group.decrypt(aesKey); @@ -443,7 +443,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); + group = KeyChainGroup.fromProtobufUnencrypted(params, protoKeys1, 1); assertEquals(3 + (group.getLookaheadSize() + group.getLookaheadThreshold() + 1) * 2, group.serializeToProtobuf().size()); } @@ -452,10 +452,12 @@ public class KeyChainGroupTest { group = createMarriedKeyChainGroup(); Address address1 = group.currentAddress(KeyChain.KeyPurpose.RECEIVE_FUNDS); assertTrue(group.isMarried()); + assertEquals(2, group.getSigsRequiredToSpend()); List protoKeys = group.serializeToProtobuf(); - KeyChainGroup group2 = KeyChainGroup.fromProtobufUnencrypted(params, protoKeys); + KeyChainGroup group2 = KeyChainGroup.fromProtobufUnencrypted(params, protoKeys, 2); assertTrue(group2.isMarried()); + assertEquals(2, group.getSigsRequiredToSpend()); Address address2 = group2.currentAddress(KeyChain.KeyPurpose.RECEIVE_FUNDS); assertEquals(address1, address2); } @@ -517,7 +519,7 @@ public class KeyChainGroupTest { DeterministicSeed seed1 = group.getActiveKeyChain().getSeed(); assertNotNull(seed1); - group = KeyChainGroup.fromProtobufUnencrypted(params, protobufs); + group = KeyChainGroup.fromProtobufUnencrypted(params, protobufs, 1); 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/wallet.proto b/core/src/wallet.proto index 5f1e9eb7..b5229110 100644 --- a/core/src/wallet.proto +++ b/core/src/wallet.proto @@ -105,6 +105,7 @@ message Key { // Either the private EC key bytes (without any ASN.1 wrapping), or the deterministic root seed. // If the secret is encrypted, or this is a "watching entry" then this is missing. optional bytes secret_bytes = 2; + // If the secret data is encrypted, then secret_bytes is missing and this field is set. optional EncryptedData encrypted_data = 6; @@ -371,7 +372,11 @@ message Wallet { // transaction signers added to the wallet repeated TransactionSigner transaction_signers = 17; - // Next tag: 18 + // 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 } /** An exchange rate between Bitcoin and some fiat currency. */