3
0
mirror of https://github.com/Qortal/altcoinj.git synced 2025-01-31 15:22:16 +00:00

Refactor married keychains

* move handling of following keychains into the leading keychain
* move multisig threshold into the leading keychain
* extract MarriedKeyChain from DeterministicKeyChain
This commit is contained in:
Devrandom 2014-09-20 10:27:51 -07:00 committed by Mike Hearn
parent be496b95a3
commit 22f0600afe
15 changed files with 890 additions and 594 deletions

View File

@ -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");
}
}
}

View File

@ -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
}
/**
* <p>Alias for <code>addFollowingAccountKeys(followingAccountKeys, (followingAccountKeys.size() + 1) / 2 + 1)</code></p>
* <p>Creates married wallet requiring majority of keys to spend (2-of-3, 3-of-5 and so on)</p>
* <p>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</p>
* Add a pre-configured keychain to the wallet. Useful for setting up a complex keychain,
* such as for a married wallet. For example:
* <pre>
* MarriedKeyChain chain = MarriedKeyChain.builder()
* .random(new SecureRandom())
* .followingKeys(followingKeys)
* .threshold(2).build();
* wallet.addAndActivateHDChain(chain);
* </p>
*/
public void addFollowingAccountKeys(List<DeterministicKey> 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).</p>
* <p>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</p>
* This method should be called only once before key rotation, otherwise it will throw an IllegalStateException.
*/
public void addFollowingAccountKeys(List<DeterministicKey> 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. */

View File

@ -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");
}
}
}

View File

@ -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);

View File

@ -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.
* </p>
*/
@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<T extends Builder<T>> {
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.</p>
* <p>Watch key has to be an account key.</p>
*/
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<Protos.Key> result) {
}
@Override
public List<Protos.Key> serializeToProtobuf() {
List<Protos.Key> 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<Protos.Key> 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<ECKey, Protos.Key.Builder> keys = basicKeyChain.serializeToEditableProtobufs();
for (Map.Entry<ECKey, Protos.Key.Builder> 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<Protos.Key> 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<Protos.Key> 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<ECKey, Protos.Key.Builder> keys = basicKeyChain.serializeToEditableProtobufs();
for (Map.Entry<ECKey, Protos.Key.Builder> 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<String> 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;
}
}

View File

@ -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<DeterministicKeyChain> chains;
private final LinkedList<DeterministicKeyChain> chains;
private final EnumMap<KeyChain.KeyPurpose, DeterministicKey> currentKeys;
// The map keys are the watching keys of the followed chains and values are the following chains
private Multimap<DeterministicKey, DeterministicKeyChain> 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<ByteString, RedeemData> marriedKeysRedeemData;
private EnumMap<KeyChain.KeyPurpose, Address> 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<DeterministicKeyChain>(1), null, null, 1, null);
this(params, null, new ArrayList<DeterministicKeyChain>(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<DeterministicKey> followingAccountKeys, int sigsRequiredToSpend) {
this(params, seed);
addFollowingAccountKeys(followingAccountKeys, sigsRequiredToSpend);
}
/**
* <p>Alias for <code>addFollowingAccountKeys(followingAccountKeys, (followingAccountKeys.size() + 1) / 2 + 1)</code></p>
* <p>Creates married keychain requiring majority of keys to spend (2-of-3, 3-of-5 and so on)</p>
* <p>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</p>
*/
public void addFollowingAccountKeys(List<DeterministicKey> followingAccountKeys) {
addFollowingAccountKeys(followingAccountKeys, (followingAccountKeys.size() + 1) / 2 + 1);
}
/**
* <p>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.</p>
* <p>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</p>
* <p>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.</p>
*/
public void addFollowingAccountKeys(List<DeterministicKey> 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<DeterministicKeyChain> chains,
@Nullable EnumMap<KeyChain.KeyPurpose, DeterministicKey> currentKeys, Multimap<DeterministicKey,
DeterministicKeyChain> followingKeychains, int sigsRequiredToSpend, @Nullable KeyCrypter crypter) {
@Nullable EnumMap<KeyChain.KeyPurpose, DeterministicKey> currentKeys, @Nullable KeyCrypter crypter) {
this.params = params;
this.basic = basicKeyChain == null ? new BasicKeyChain() : basicKeyChain;
this.chains = new ArrayList<DeterministicKeyChain>(checkNotNull(chains));
this.chains = new LinkedList<DeterministicKeyChain>(checkNotNull(chains));
this.keyCrypter = crypter;
this.currentKeys = currentKeys == null
? new EnumMap<KeyChain.KeyPurpose, DeterministicKey>(KeyChain.KeyPurpose.class)
: currentKeys;
this.currentAddresses = new EnumMap<KeyChain.KeyPurpose, Address>(KeyChain.KeyPurpose.class);
this.followingKeychains = HashMultimap.create();
if (followingKeychains != null) {
this.followingKeychains.putAll(followingKeychains);
}
this.sigsRequiredToSpend = sigsRequiredToSpend;
marriedKeysRedeemData = new LinkedHashMap<ByteString, RedeemData>();
maybeLookaheadScripts();
if (!this.followingKeychains.isEmpty()) {
DeterministicKey followedWatchKey = getActiveKeyChain().getWatchingKey();
if (isMarried()) {
for (Map.Entry<KeyChain.KeyPurpose, DeterministicKey> 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<KeyChainEventListener> 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<DeterministicKey> 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<ECKey> 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<ECKey> freshMarriedKeys(KeyChain.KeyPurpose purpose, DeterministicKeyChain followedKeyChain) {
DeterministicKey followedKey = followedKeyChain.getKey(purpose);
ImmutableList.Builder<ECKey> keys = ImmutableList.<ECKey>builder().add(followedKey);
Collection<DeterministicKeyChain> 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<ECKey> getMarriedKeysWithFollowed(DeterministicKey followedKey, Collection<DeterministicKeyChain> followingChains) {
ImmutableList.Builder<ECKey> 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<DeterministicKeyChain> 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<ByteString, RedeemData> 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<ECKey> 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<DeterministicKeyChain> followingChains = followingKeychains.get(followedAccountKey);
List<ECKey> marriedKeys = getMarriedKeysWithFollowed(followedKey, followingChains);
return RedeemData.of(marriedKeys, makeRedeemScript(marriedKeys));
}
private Script makeRedeemScript(List<ECKey> 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.Key> protos = chain.serializeToProtobuf();
result.addAll(protos);
}
return result;
}
public static KeyChainGroup fromProtobufUnencrypted(NetworkParameters params, List<Protos.Key> keys, int sigsRequiredToSpend) throws UnreadableWalletException {
checkArgument(sigsRequiredToSpend > 0);
public static KeyChainGroup fromProtobufUnencrypted(NetworkParameters params, List<Protos.Key> keys) throws UnreadableWalletException {
BasicKeyChain basicKeyChain = BasicKeyChain.fromProtobufUnencrypted(keys);
List<DeterministicKeyChain> chains = DeterministicKeyChain.fromProtobuf(keys, null);
EnumMap<KeyChain.KeyPurpose, DeterministicKey> currentKeys = null;
if (!chains.isEmpty())
currentKeys = createCurrentKeysMap(chains);
Multimap<DeterministicKey, DeterministicKeyChain> 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<Protos.Key> keys, int sigsRequiredToSpend, KeyCrypter crypter) throws UnreadableWalletException {
checkArgument(sigsRequiredToSpend > 0);
public static KeyChainGroup fromProtobufEncrypted(NetworkParameters params, List<Protos.Key> keys, KeyCrypter crypter) throws UnreadableWalletException {
checkNotNull(crypter);
BasicKeyChain basicKeyChain = BasicKeyChain.fromProtobufEncrypted(keys, crypter);
List<DeterministicKeyChain> chains = DeterministicKeyChain.fromProtobuf(keys, crypter);
EnumMap<KeyChain.KeyPurpose, DeterministicKey> currentKeys = null;
if (!chains.isEmpty())
currentKeys = createCurrentKeysMap(chains);
Multimap<DeterministicKey, DeterministicKeyChain> 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<DeterministicKey, DeterministicKeyChain> extractFollowingKeychains(List<DeterministicKeyChain> chains) {
private static void extractFollowingKeychains(List<DeterministicKeyChain> chains) {
// look for following key chains and map them to the watch keys of followed keychains
Multimap<DeterministicKey, DeterministicKeyChain> followingKeychains = HashMultimap.create();
List<DeterministicKeyChain> followingChains = new ArrayList<DeterministicKeyChain>();
List<DeterministicKeyChain> followingChains = Lists.newArrayList();
for (Iterator<DeterministicKeyChain> 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<String> 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<String> 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<DeterministicKeyChain> 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<DeterministicKeyChain> getDeterministicKeyChains() {
return new ArrayList<DeterministicKeyChain>(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.

View File

@ -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;
/**
* <p>A multi-signature keychain using synchronized HD keys (a.k.a HDM)</p>
* <p>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.</p>
* <p>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</p>
* <p>This method will throw an IllegalStateException, if the keychain is already married or already has leaf keys
* issued.</p>
*/
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<ByteString, RedeemData> marriedKeysRedeemData = new LinkedHashMap<ByteString, RedeemData>();
private List<DeterministicKeyChain> followingKeyChains;
/** Builds a {@link MarriedKeyChain} */
public static class Builder<T extends Builder<T>> extends DeterministicKeyChain.Builder<T> {
private List<DeterministicKey> followingKeys;
private int threshold;
protected Builder() {
}
public T followingKeys(List<DeterministicKey> followingKeys) {
this.followingKeys = followingKeys;
return self();
}
public T followingKeys(DeterministicKey followingKey, DeterministicKey ...followingKeys) {
this.followingKeys = Lists.asList(followingKey, followingKeys);
return self();
}
/**
* Threshold, or <code>(followingKeys.size() + 1) / 2 + 1)</code> (majority) if unspecified.</p>
* <p>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</p>
*/
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<DeterministicKeyChain> 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<ECKey> keys = ImmutableList.<ECKey>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<ECKey> marriedKeys = keys.build();
Script redeemScript = ScriptBuilder.createRedeemScript(sigsRequiredToSpend, marriedKeys);
return ScriptBuilder.createP2SHOutputScript(redeemScript);
}
private List<ECKey> getMarriedKeysWithFollowed(DeterministicKey followedKey) {
ImmutableList.Builder<ECKey> 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<ECKey> marriedKeys = getMarriedKeysWithFollowed(followedKey);
Script redeemScript = ScriptBuilder.createRedeemScript(sigsRequiredToSpend, marriedKeys);
return RedeemData.of(marriedKeys, redeemScript);
}
private void addFollowingAccountKeys(List<DeterministicKey> 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<DeterministicKeyChain> 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<Protos.Key> 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<ByteString, RedeemData> 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;
}
}

View File

@ -1269,6 +1269,26 @@ public final class Protos {
* </pre>
*/
boolean getIsFollowing();
// optional uint32 sigsRequiredToSpend = 6 [default = 1];
/**
* <code>optional uint32 sigsRequiredToSpend = 6 [default = 1];</code>
*
* <pre>
* 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.
* </pre>
*/
boolean hasSigsRequiredToSpend();
/**
* <code>optional uint32 sigsRequiredToSpend = 6 [default = 1];</code>
*
* <pre>
* 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.
* </pre>
*/
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_;
/**
* <code>optional uint32 sigsRequiredToSpend = 6 [default = 1];</code>
*
* <pre>
* 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.
* </pre>
*/
public boolean hasSigsRequiredToSpend() {
return ((bitField0_ & 0x00000010) == 0x00000010);
}
/**
* <code>optional uint32 sigsRequiredToSpend = 6 [default = 1];</code>
*
* <pre>
* 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.
* </pre>
*/
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;
/**
* <code>optional uint32 sigsRequiredToSpend = 6 [default = 1];</code>
*
* <pre>
* 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.
* </pre>
*/
public boolean hasSigsRequiredToSpend() {
return ((bitField0_ & 0x00000020) == 0x00000020);
}
/**
* <code>optional uint32 sigsRequiredToSpend = 6 [default = 1];</code>
*
* <pre>
* 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.
* </pre>
*/
public int getSigsRequiredToSpend() {
return sigsRequiredToSpend_;
}
/**
* <code>optional uint32 sigsRequiredToSpend = 6 [default = 1];</code>
*
* <pre>
* 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.
* </pre>
*/
public Builder setSigsRequiredToSpend(int value) {
bitField0_ |= 0x00000020;
sigsRequiredToSpend_ = value;
onChanged();
return this;
}
/**
* <code>optional uint32 sigsRequiredToSpend = 6 [default = 1];</code>
*
* <pre>
* 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.
* </pre>
*/
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];
/**
* <code>optional uint32 sigsRequiredToSpend = 18 [default = 1];</code>
*
* <pre>
* 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.
* </pre>
*/
boolean hasSigsRequiredToSpend();
/**
* <code>optional uint32 sigsRequiredToSpend = 18 [default = 1];</code>
*
* <pre>
* 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.
* </pre>
*/
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_;
/**
* <code>optional uint32 sigsRequiredToSpend = 18 [default = 1];</code>
*
* <pre>
* 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.
* </pre>
*/
public boolean hasSigsRequiredToSpend() {
return ((bitField0_ & 0x00000200) == 0x00000200);
}
/**
* <code>optional uint32 sigsRequiredToSpend = 18 [default = 1];</code>
*
* <pre>
* 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.
* </pre>
*/
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;
/**
* <code>optional uint32 sigsRequiredToSpend = 18 [default = 1];</code>
*
* <pre>
* 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.
* </pre>
*/
public boolean hasSigsRequiredToSpend() {
return ((bitField0_ & 0x00008000) == 0x00008000);
}
/**
* <code>optional uint32 sigsRequiredToSpend = 18 [default = 1];</code>
*
* <pre>
* 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.
* </pre>
*/
public int getSigsRequiredToSpend() {
return sigsRequiredToSpend_;
}
/**
* <code>optional uint32 sigsRequiredToSpend = 18 [default = 1];</code>
*
* <pre>
* 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.
* </pre>
*/
public Builder setSigsRequiredToSpend(int value) {
bitField0_ |= 0x00008000;
sigsRequiredToSpend_ = value;
onChanged();
return this;
}
/**
* <code>optional uint32 sigsRequiredToSpend = 18 [default = 1];</code>
*
* <pre>
* 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.
* </pre>
*/
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

View File

@ -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);

View File

@ -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));
}

View File

@ -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<Protos.Key> 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<Protos.Key> 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<Protos.Key> 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<Protos.Key> 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<DeterministicKey> 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();

View File

@ -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

View File

@ -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

View File

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

View File

@ -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 {