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. */