diff --git a/core/src/main/java/com/google/bitcoin/core/Wallet.java b/core/src/main/java/com/google/bitcoin/core/Wallet.java index 5e158396..6e61c5e5 100644 --- a/core/src/main/java/com/google/bitcoin/core/Wallet.java +++ b/core/src/main/java/com/google/bitcoin/core/Wallet.java @@ -206,19 +206,19 @@ public class Wallet extends BaseTaggableObject implements Serializable, BlockCha * see loadFromFile. */ public Wallet(NetworkParameters params) { - this(params, new KeyChainGroup()); + this(params, new KeyChainGroup(params)); } public static Wallet fromSeed(NetworkParameters params, DeterministicSeed seed) { - return new Wallet(params, new KeyChainGroup(seed)); + return new Wallet(params, new KeyChainGroup(params, seed)); } public static Wallet fromWatchingKey(NetworkParameters params, DeterministicKey watchKey, long creationTimeSeconds) { - return new Wallet(params, new KeyChainGroup(watchKey, creationTimeSeconds)); + return new Wallet(params, new KeyChainGroup(params, watchKey, creationTimeSeconds)); } public static Wallet fromWatchingKey(NetworkParameters params, DeterministicKey watchKey) { - return new Wallet(params, new KeyChainGroup(watchKey)); + return new Wallet(params, new KeyChainGroup(params, watchKey)); } // TODO: When this class moves to the Wallet package, along with the protobuf serializer, then hide this. @@ -310,7 +310,7 @@ public class Wallet extends BaseTaggableObject implements Serializable, BlockCha lock.lock(); try { maybeUpgradeToHD(); - return keychain.currentAddress(purpose, params); + return keychain.currentAddress(purpose); } finally { lock.unlock(); } @@ -372,8 +372,7 @@ public class Wallet extends BaseTaggableObject implements Serializable, BlockCha public Address freshAddress(KeyChain.KeyPurpose purpose) { lock.lock(); try { - maybeUpgradeToHD(); - Address key = keychain.freshAddress(purpose, params); + Address key = keychain.freshAddress(purpose); saveNow(); return key; } finally { @@ -555,6 +554,20 @@ public class Wallet extends BaseTaggableObject implements Serializable, BlockCha } } + /** + * Makes given account keys follow the account key of the active keychain. After that you will be able + * to get P2SH addresses to receive coins to. + * This method should be called only once before key rotation, otherwise it will throw an IllegalStateException. + */ + public void addFollowingAccounts(List followingAccountKeys) { + lock.lock(); + try { + keychain.addFollowingAccounts(followingAccountKeys); + } finally { + lock.unlock(); + } + } + /** See {@link com.google.bitcoin.wallet.DeterministicKeyChain#setLookaheadSize(int)} for more info on this. */ public void setKeychainLookaheadSize(int lookaheadSize) { lock.lock(); @@ -2765,7 +2778,7 @@ public class Wallet extends BaseTaggableObject implements Serializable, BlockCha // Do the keys. builder.append("\nKeys:\n"); - builder.append(keychain.toString(params, includePrivateKeys)); + builder.append(keychain.toString(includePrivateKeys)); if (!watchedScripts.isEmpty()) { builder.append("\nWatched scripts:\n"); diff --git a/core/src/main/java/com/google/bitcoin/kits/WalletAppKit.java b/core/src/main/java/com/google/bitcoin/kits/WalletAppKit.java index bf9c7a0b..83eed650 100644 --- a/core/src/main/java/com/google/bitcoin/kits/WalletAppKit.java +++ b/core/src/main/java/com/google/bitcoin/kits/WalletAppKit.java @@ -240,7 +240,7 @@ public class WalletAppKit extends AbstractIdleService { walletStream.close(); } } else { - vWallet = walletFactory != null ? walletFactory.create(params, new KeyChainGroup()) : new Wallet(params); + vWallet = walletFactory != null ? walletFactory.create(params, new KeyChainGroup(params)) : new Wallet(params); vWallet.freshReceiveKey(); for (WalletExtension e : provideWalletExtensions()) { vWallet.addExtension(e); diff --git a/core/src/main/java/com/google/bitcoin/script/ScriptBuilder.java b/core/src/main/java/com/google/bitcoin/script/ScriptBuilder.java index 2f9f5746..19f9a795 100644 --- a/core/src/main/java/com/google/bitcoin/script/ScriptBuilder.java +++ b/core/src/main/java/com/google/bitcoin/script/ScriptBuilder.java @@ -18,13 +18,14 @@ package com.google.bitcoin.script; import com.google.bitcoin.core.Address; import com.google.bitcoin.core.ECKey; +import com.google.bitcoin.core.Utils; import com.google.bitcoin.crypto.TransactionSignature; import com.google.common.collect.Lists; +import com.google.common.primitives.UnsignedBytes; import javax.annotation.Nullable; -import java.util.ArrayList; -import java.util.Arrays; -import java.util.List; +import java.math.BigInteger; +import java.util.*; import static com.google.bitcoin.script.ScriptOpCodes.*; import static com.google.common.base.Preconditions.checkArgument; @@ -185,4 +186,22 @@ public class ScriptBuilder { checkArgument(hash.length == 20); return new ScriptBuilder().op(OP_HASH160).data(hash).op(OP_EQUAL).build(); } + + /** + * Creates a P2SH output script with given public keys and threshold. Given public keys will be placed in + * redeem script in the lexicographical sorting order. + */ + public static Script createP2SHOutputScript(int threshold, List pubkeys) { + pubkeys = new ArrayList(pubkeys); + final Comparator comparator = UnsignedBytes.lexicographicalComparator(); + Collections.sort(pubkeys, new Comparator() { + @Override + public int compare(ECKey k1, ECKey k2) { + return comparator.compare(k1.getPubKey(), k2.getPubKey()); + } + }); + Script redeemScript = ScriptBuilder.createMultiSigOutputScript(threshold, pubkeys); + byte[] hash = Utils.sha256hash160(redeemScript.getProgram()); + return ScriptBuilder.createP2SHOutputScript(hash); + } } diff --git a/core/src/main/java/com/google/bitcoin/store/WalletProtobufSerializer.java b/core/src/main/java/com/google/bitcoin/store/WalletProtobufSerializer.java index fe6486ed..80a91a22 100644 --- a/core/src/main/java/com/google/bitcoin/store/WalletProtobufSerializer.java +++ b/core/src/main/java/com/google/bitcoin/store/WalletProtobufSerializer.java @@ -386,9 +386,9 @@ public class WalletProtobufSerializer { if (walletProto.hasEncryptionParameters()) { Protos.ScryptParameters encryptionParameters = walletProto.getEncryptionParameters(); final KeyCrypterScrypt keyCrypter = new KeyCrypterScrypt(encryptionParameters); - chain = KeyChainGroup.fromProtobufEncrypted(walletProto.getKeyList(), keyCrypter); + chain = KeyChainGroup.fromProtobufEncrypted(params, walletProto.getKeyList(), keyCrypter); } else { - chain = KeyChainGroup.fromProtobufUnencrypted(walletProto.getKeyList()); + chain = KeyChainGroup.fromProtobufUnencrypted(params, walletProto.getKeyList()); } Wallet wallet = factory.create(params, chain); diff --git a/core/src/main/java/com/google/bitcoin/wallet/DeterministicKeyChain.java b/core/src/main/java/com/google/bitcoin/wallet/DeterministicKeyChain.java index e09177c1..abe226ab 100644 --- a/core/src/main/java/com/google/bitcoin/wallet/DeterministicKeyChain.java +++ b/core/src/main/java/com/google/bitcoin/wallet/DeterministicKeyChain.java @@ -116,6 +116,9 @@ public class DeterministicKeyChain implements EncryptableKeyChain { // money. private final BasicKeyChain basicKeyChain; + // If set this chain is following another chain in a married KeyChainGroup + private boolean isFollowing; + /** * Generates a new key chain with a 128 bit seed selected randomly from the given {@link java.security.SecureRandom} * object. @@ -165,6 +168,25 @@ public class DeterministicKeyChain implements EncryptableKeyChain { this(watchingKey, Utils.currentTimeSeconds()); } + /** + *

Creates a deterministic key chain with the given watch key. If isFollowing flag is set then this keychain follows + * some other keychain. In a married wallet following keychain represents "spouse's" keychain.

+ *

Watch key has to be an account key.

+ */ + private DeterministicKeyChain(DeterministicKey watchKey, boolean isFollowing) { + this(watchKey, Utils.currentTimeSeconds()); + this.isFollowing = isFollowing; + } + + /** + * Creates a deterministic key chain with the given watch key and that follows some other keychain. In a married + * wallet following keychain represents "spouse" + * Watch key has to be an account key. + */ + public static DeterministicKeyChain watchAndFollow(DeterministicKey watchKey) { + return new DeterministicKeyChain(watchKey, true); + } + /** * Creates a key chain that watches the given account key. The creation time is taken to be the time that BIP 32 * was standardised: most likely, you can optimise by selecting a more accurate creation time for your key and @@ -451,6 +473,13 @@ public class DeterministicKeyChain implements EncryptableKeyChain { } } + /** + * Return true if this keychain is following another keychain + */ + public boolean isFollowing() { + return isFollowing; + } + ////////////////////////////////////////////////////////////////////////////////////////////////////////////////// // // Serialization support @@ -485,6 +514,10 @@ public class DeterministicKeyChain implements EncryptableKeyChain { detKey.setIssuedSubkeys(issuedInternalKeys); detKey.setLookaheadSize(lookaheadSize); } + // flag the very first key of following keychain + if (entries.isEmpty() && isFollowing()) { + detKey.setIsFollowing(true); + } entries.add(proto.build()); } return entries; @@ -537,13 +570,27 @@ public class DeterministicKeyChain implements EncryptableKeyChain { final ImmutableList immutablePath = ImmutableList.copyOf(path); // Possibly create the chain, if we didn't already do so yet. boolean isWatchingAccountKey = false; + boolean isFollowingKey = false; + // save previous chain if any if the key is marked as following. Current key and the next ones are to be + // placed in new following key chain + if (key.getDeterministicKey().getIsFollowing()) { + if (chain != null) { + checkState(lookaheadSize >= 0); + chain.setLookaheadSize(lookaheadSize); + chain.maybeLookAhead(); + chains.add(chain); + chain = null; + seed = null; + } + isFollowingKey = true; + } if (chain == null) { 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); + chain = new DeterministicKeyChain(accountKey, isFollowingKey); isWatchingAccountKey = true; } else { chain = new DeterministicKeyChain(seed, crypter); diff --git a/core/src/main/java/com/google/bitcoin/wallet/KeyChainGroup.java b/core/src/main/java/com/google/bitcoin/wallet/KeyChainGroup.java index 1f1721b0..2f7f4fdc 100644 --- a/core/src/main/java/com/google/bitcoin/wallet/KeyChainGroup.java +++ b/core/src/main/java/com/google/bitcoin/wallet/KeyChainGroup.java @@ -21,12 +21,15 @@ import com.google.bitcoin.core.*; import com.google.bitcoin.crypto.ChildNumber; import com.google.bitcoin.crypto.DeterministicKey; import com.google.bitcoin.crypto.KeyCrypter; +import com.google.bitcoin.script.ScriptBuilder; import com.google.bitcoin.store.UnreadableWalletException; import com.google.bitcoin.utils.ListenerRegistration; import com.google.bitcoin.utils.Threading; import com.google.common.base.Joiner; +import com.google.common.collect.HashMultimap; import com.google.common.collect.ImmutableList; import com.google.common.collect.Lists; +import com.google.common.collect.Multimap; import org.bitcoinj.wallet.Protos; import org.slf4j.Logger; import org.slf4j.LoggerFactory; @@ -58,28 +61,34 @@ import static com.google.common.base.Preconditions.*; public class KeyChainGroup { private static final Logger log = LoggerFactory.getLogger(KeyChainGroup.class); private BasicKeyChain basic; + private NetworkParameters params; private final List chains; private final EnumMap currentKeys; + + // The map keys are the watching keys of the followed chains and values are the following chains + private Multimap followingKeychains; + + private EnumMap currentAddresses; @Nullable private KeyCrypter keyCrypter; private int lookaheadSize = -1; private int lookaheadThreshold = -1; /** Creates a keychain group with no basic chain, and a single, lazily created HD chain. */ - public KeyChainGroup() { - this(null, new ArrayList(1), null, null); + public KeyChainGroup(NetworkParameters params) { + this(params, null, new ArrayList(1), null, null, null); } /** Creates a keychain group with no basic chain, and an HD chain initialized from the given seed. */ - public KeyChainGroup(DeterministicSeed seed) { - this(null, ImmutableList.of(new DeterministicKeyChain(seed)), null, null); + public KeyChainGroup(NetworkParameters params, DeterministicSeed seed) { + this(params, null, ImmutableList.of(new DeterministicKeyChain(seed)), null, null, null); } /** * Creates a keychain group with no basic chain, and an HD chain that is watching the given watching key. * This HAS to be an account key as returned by {@link DeterministicKeyChain#getWatchingKey()}. */ - public KeyChainGroup(DeterministicKey watchKey) { - this(null, ImmutableList.of(DeterministicKeyChain.watch(watchKey)), null, null); + public KeyChainGroup(NetworkParameters params, DeterministicKey watchKey) { + this(params, null, ImmutableList.of(DeterministicKeyChain.watch(watchKey)), null, null, null); } /** @@ -87,18 +96,55 @@ public class KeyChainGroup { * was assumed to be first used at the given UNIX time. * This HAS to be an account key as returned by {@link DeterministicKeyChain#getWatchingKey()}. */ - public KeyChainGroup(DeterministicKey watchKey, long creationTimeSecondsSecs) { - this(null, ImmutableList.of(DeterministicKeyChain.watch(watchKey, creationTimeSecondsSecs)), null, null); + public KeyChainGroup(NetworkParameters params, DeterministicKey watchKey, long creationTimeSecondsSecs) { + this(params, null, ImmutableList.of(DeterministicKeyChain.watch(watchKey, creationTimeSecondsSecs)), null, null, null); + } + + /** + * Creates a keychain group with no basic chain, with an HD chain initialized from the given seed and being followed + * by given list of watch keys. Watch keys have to be account keys. + */ + public KeyChainGroup(NetworkParameters params, DeterministicSeed seed, List followingAccountKeys) { + this(params, seed); + + addFollowingAccounts(followingAccountKeys); + } + + /** + * Makes given account keys follow the account key of the active keychain. After that you will be able + * to get P2SH addresses to receive coins to. + * This method should be called only once before key rotation, otherwise it will throw an IllegalStateException. + */ + public void addFollowingAccounts(List followingAccountKeys) { + if (isMarried()) { + throw new IllegalStateException("KeyChainGroup is married already"); + } + + 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); + } + followingKeychains.put(accountKey, chain); + } } // Used for deserialization. - private KeyChainGroup(@Nullable BasicKeyChain basicKeyChain, List chains, @Nullable EnumMap currentKeys, @Nullable KeyCrypter crypter) { + private KeyChainGroup(NetworkParameters params, @Nullable BasicKeyChain basicKeyChain, List chains, @Nullable EnumMap currentKeys, Multimap followingKeychains, @Nullable KeyCrypter crypter) { + this.params = params; this.basic = basicKeyChain == null ? new BasicKeyChain() : basicKeyChain; this.chains = new ArrayList(checkNotNull(chains)); this.keyCrypter = crypter; this.currentKeys = currentKeys == null ? new EnumMap(KeyChain.KeyPurpose.class) : currentKeys; + this.currentAddresses = new EnumMap(KeyChain.KeyPurpose.class); + this.followingKeychains = HashMultimap.create(); + if (followingKeychains != null) { + this.followingKeychains.putAll(followingKeychains); + } } private void createAndActivateNewHDChain() { @@ -119,8 +165,17 @@ public class KeyChainGroup { * {@link com.google.bitcoin.wallet.KeyChain.KeyPurpose#RECEIVE_FUNDS}. The returned key is stable until * it's actually seen in a pending or confirmed transaction, at which point this method will start returning * a different key (for each purpose independently). + *

This method is not supposed to be used for married keychains and will throw UnsupportedOperationException if + * the active chain is married. + * For married keychains use {@link #currentAddress(com.google.bitcoin.wallet.KeyChain.KeyPurpose)} + * to get a proper P2SH address

*/ public DeterministicKey currentKey(KeyChain.KeyPurpose purpose) { + DeterministicKeyChain chain = getActiveKeyChain(); + if (isMarried(chain)) { + throw new UnsupportedOperationException("Key is not suitable to receive coins for married keychains." + + " Use freshAddress to get P2SH address instead"); + } final DeterministicKey current = currentKeys.get(purpose); return current != null ? current : freshKey(purpose); } @@ -128,8 +183,14 @@ public class KeyChainGroup { /** * Returns address for a {@link #currentKey(com.google.bitcoin.wallet.KeyChain.KeyPurpose)} */ - public Address currentAddress(KeyChain.KeyPurpose purpose, NetworkParameters params) { - return currentKey(purpose).toAddress(params); + public Address currentAddress(KeyChain.KeyPurpose purpose) { + DeterministicKeyChain chain = getActiveKeyChain(); + if (isMarried(chain)) { + Address current = currentAddresses.get(purpose); + return current != null ? current : freshAddress(purpose); + } else { + return currentKey(purpose).toAddress(params); + } } /** @@ -139,6 +200,10 @@ public class KeyChainGroup { * {@link com.google.bitcoin.wallet.KeyChain.KeyPurpose#RECEIVE_FUNDS} the returned key is suitable for being put * into a receive coins wizard type UI. You should use this when the user is definitely going to hand this key out * to someone who wishes to send money. + *

This method is not supposed to be used for married keychains and will throw UnsupportedOperationException if + * the active chain is married. + * For married keychains use {@link #freshAddress(com.google.bitcoin.wallet.KeyChain.KeyPurpose)} + * to get a proper P2SH address

*/ public DeterministicKey freshKey(KeyChain.KeyPurpose purpose) { return freshKeys(purpose, 1).get(0); @@ -151,9 +216,18 @@ public class KeyChainGroup { * {@link com.google.bitcoin.wallet.KeyChain.KeyPurpose#RECEIVE_FUNDS} the returned key is suitable for being put * into a receive coins wizard type UI. You should use this when the user is definitely going to hand this key out * to someone who wishes to send money. + *

This method is not supposed to be used for married keychains and will throw UnsupportedOperationException if + * the active chain is married. + * For married keychains use {@link #freshAddress(com.google.bitcoin.wallet.KeyChain.KeyPurpose)} + * to get a proper P2SH address

*/ public List freshKeys(KeyChain.KeyPurpose purpose, int numberOfKeys) { DeterministicKeyChain chain = getActiveKeyChain(); + if (isMarried(chain)) { + throw new UnsupportedOperationException("Key is not suitable to receive coins for married keychains." + + " Use freshAddress to get P2SH address instead"); + } + List keys = chain.getKeys(purpose, numberOfKeys); // Always returns the next key along the key chain. currentKeys.put(purpose, keys.get(keys.size() - 1)); return keys; @@ -162,8 +236,28 @@ public class KeyChainGroup { /** * Returns address for a {@link #freshKey(com.google.bitcoin.wallet.KeyChain.KeyPurpose)} */ - public Address freshAddress(KeyChain.KeyPurpose purpose, NetworkParameters params) { - return freshKey(purpose).toAddress(params); + public Address freshAddress(KeyChain.KeyPurpose purpose) { + DeterministicKeyChain chain = getActiveKeyChain(); + DeterministicKey key = chain.getKey(purpose); + if (isMarried(chain)) { + List keys = ImmutableList.builder() + .addAll(getFollowingKeys(purpose, chain.getWatchingKey())) + .add(key).build(); + Address freshAddress = Address.fromP2SHScript(params, ScriptBuilder.createP2SHOutputScript(2, keys)); + currentAddresses.put(purpose, freshAddress); + return freshAddress; + } else { + return freshKey(purpose).toAddress(params); + } + } + + private List getFollowingKeys(KeyChain.KeyPurpose purpose, DeterministicKey followedChainWatchKey) { + List keys = new ArrayList(); + Collection keyChains = followingKeychains.get(followedChainWatchKey); + for (DeterministicKeyChain keyChain : keyChains) { + keys.add(keyChain.getKey(purpose)); + } + return keys; } /** Returns the key chain that's used for generation of fresh/current keys. This is always the newest HD chain. */ @@ -182,8 +276,8 @@ public class KeyChainGroup { } /** - * Sets the lookahead buffer size for ALL deterministic key chains, see - * {@link com.google.bitcoin.wallet.DeterministicKeyChain#setLookaheadSize(int)} + * Sets the lookahead buffer size for ALL deterministic key chains as well as for following key chains if any exist, + * see {@link com.google.bitcoin.wallet.DeterministicKeyChain#setLookaheadSize(int)} * for more information. */ public void setLookaheadSize(int lookaheadSize) { @@ -191,6 +285,9 @@ public class KeyChainGroup { for (DeterministicKeyChain chain : chains) { chain.setLookaheadSize(lookaheadSize); } + for (DeterministicKeyChain chain : followingKeychains.values()) { + chain.setLookaheadSize(lookaheadSize); + } } /** @@ -331,6 +428,21 @@ public class KeyChainGroup { return basic.removeKey(key); } + /** + * 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 + */ + public boolean isMarried() { + return isMarried(getActiveKeyChain()); + } + /** * Encrypt the keys in the group using the KeyCrypter and the AES key. A good default KeyCrypter to use is * {@link com.google.bitcoin.crypto.KeyCrypterScrypt}. @@ -451,29 +563,35 @@ public class KeyChainGroup { else result = Lists.newArrayList(); for (DeterministicKeyChain chain : chains) { + // prepend each chain with it's following chains if any + for (DeterministicKeyChain followingChain : followingKeychains.get(chain.getWatchingKey())) { + result.addAll(followingChain.serializeToProtobuf()); + } List protos = chain.serializeToProtobuf(); result.addAll(protos); } return result; } - public static KeyChainGroup fromProtobufUnencrypted(List keys) throws UnreadableWalletException { + public static KeyChainGroup fromProtobufUnencrypted(NetworkParameters params, List keys) throws UnreadableWalletException { BasicKeyChain basicKeyChain = BasicKeyChain.fromProtobufUnencrypted(keys); List chains = DeterministicKeyChain.fromProtobuf(keys, null); EnumMap currentKeys = null; if (!chains.isEmpty()) currentKeys = createCurrentKeysMap(chains); - return new KeyChainGroup(basicKeyChain, chains, currentKeys, null); + Multimap followingKeychains = extractFollowingKeychains(chains); + return new KeyChainGroup(params, basicKeyChain, chains, currentKeys, followingKeychains, null); } - public static KeyChainGroup fromProtobufEncrypted(List keys, KeyCrypter crypter) throws UnreadableWalletException { + public static KeyChainGroup fromProtobufEncrypted(NetworkParameters params, List keys, KeyCrypter crypter) throws UnreadableWalletException { checkNotNull(crypter); BasicKeyChain basicKeyChain = BasicKeyChain.fromProtobufEncrypted(keys, crypter); List chains = DeterministicKeyChain.fromProtobuf(keys, crypter); EnumMap currentKeys = null; if (!chains.isEmpty()) currentKeys = createCurrentKeysMap(chains); - return new KeyChainGroup(basicKeyChain, chains, currentKeys, crypter); + Multimap followingKeychains = extractFollowingKeychains(chains); + return new KeyChainGroup(params, basicKeyChain, chains, currentKeys, followingKeychains, crypter); } /** @@ -567,11 +685,28 @@ public class KeyChainGroup { return currentKeys; } - public String toString(@Nullable NetworkParameters params, boolean includePrivateKeys) { + private static Multimap extractFollowingKeychains(List chains) { + // look for following key chains and map them to the watch keys of followed keychains + Multimap followingKeychains = HashMultimap.create(); + List followingChains = new ArrayList(); + for (Iterator it = chains.iterator(); it.hasNext(); ) { + DeterministicKeyChain chain = it.next(); + if (chain.isFollowing()) { + followingChains.add(chain); + it.remove(); + } else if (!followingChains.isEmpty()) { + followingKeychains.putAll(chain.getWatchingKey(), followingChains); + followingChains.clear(); + } + } + return followingKeychains; + } + + public String toString(boolean includePrivateKeys) { final StringBuilder builder = new StringBuilder(); if (basic != null) { for (ECKey key : basic.getKeys()) - formatKeyWithAddress(params, includePrivateKeys, key, builder); + formatKeyWithAddress(includePrivateKeys, key, builder); } for (DeterministicKeyChain chain : chains) { DeterministicSeed seed = chain.getSeed(); @@ -596,18 +731,15 @@ public class KeyChainGroup { builder.append(String.format("Key to watch: %s%n%n", watchingKey.serializePubB58())); } for (ECKey key : chain.getKeys()) - formatKeyWithAddress(params, includePrivateKeys, key, builder); + formatKeyWithAddress(includePrivateKeys, key, builder); } return builder.toString(); } - private void formatKeyWithAddress(@Nullable NetworkParameters params, boolean includePrivateKeys, - ECKey key, StringBuilder builder) { - if (params != null) { - final Address address = key.toAddress(params); - builder.append(" addr:"); - builder.append(address.toString()); - } + 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())); builder.append(" "); diff --git a/core/src/main/java/org/bitcoinj/wallet/Protos.java b/core/src/main/java/org/bitcoinj/wallet/Protos.java index a69fbe92..caac259a 100644 --- a/core/src/main/java/org/bitcoinj/wallet/Protos.java +++ b/core/src/main/java/org/bitcoinj/wallet/Protos.java @@ -1245,6 +1245,30 @@ public final class Protos { * optional uint32 lookahead_size = 4; */ int getLookaheadSize(); + + // optional bool isFollowing = 5; + /** + * optional bool isFollowing = 5; + * + *
+     **
+     * Flag indicating that this key is a root of a following chain. This chain is following the next non-following chain.
+     * Following/followed chains concept is used for married keychains, where the set of keys combined together to produce
+     * a single P2SH multisignature address
+     * 
+ */ + boolean hasIsFollowing(); + /** + * optional bool isFollowing = 5; + * + *
+     **
+     * Flag indicating that this key is a root of a following chain. This chain is following the next non-following chain.
+     * Following/followed chains concept is used for married keychains, where the set of keys combined together to produce
+     * a single P2SH multisignature address
+     * 
+ */ + boolean getIsFollowing(); } /** * Protobuf type {@code wallet.DeterministicKey} @@ -1338,6 +1362,11 @@ public final class Protos { lookaheadSize_ = input.readUInt32(); break; } + case 40: { + bitField0_ |= 0x00000008; + isFollowing_ = input.readBool(); + break; + } } } } catch (com.google.protobuf.InvalidProtocolBufferException e) { @@ -1495,11 +1524,42 @@ public final class Protos { return lookaheadSize_; } + // optional bool isFollowing = 5; + public static final int ISFOLLOWING_FIELD_NUMBER = 5; + private boolean isFollowing_; + /** + * optional bool isFollowing = 5; + * + *
+     **
+     * Flag indicating that this key is a root of a following chain. This chain is following the next non-following chain.
+     * Following/followed chains concept is used for married keychains, where the set of keys combined together to produce
+     * a single P2SH multisignature address
+     * 
+ */ + public boolean hasIsFollowing() { + return ((bitField0_ & 0x00000008) == 0x00000008); + } + /** + * optional bool isFollowing = 5; + * + *
+     **
+     * Flag indicating that this key is a root of a following chain. This chain is following the next non-following chain.
+     * Following/followed chains concept is used for married keychains, where the set of keys combined together to produce
+     * a single P2SH multisignature address
+     * 
+ */ + public boolean getIsFollowing() { + return isFollowing_; + } + private void initFields() { chainCode_ = com.google.protobuf.ByteString.EMPTY; path_ = java.util.Collections.emptyList(); issuedSubkeys_ = 0; lookaheadSize_ = 0; + isFollowing_ = false; } private byte memoizedIsInitialized = -1; public final boolean isInitialized() { @@ -1529,6 +1589,9 @@ public final class Protos { if (((bitField0_ & 0x00000004) == 0x00000004)) { output.writeUInt32(4, lookaheadSize_); } + if (((bitField0_ & 0x00000008) == 0x00000008)) { + output.writeBool(5, isFollowing_); + } getUnknownFields().writeTo(output); } @@ -1559,6 +1622,10 @@ public final class Protos { size += com.google.protobuf.CodedOutputStream .computeUInt32Size(4, lookaheadSize_); } + if (((bitField0_ & 0x00000008) == 0x00000008)) { + size += com.google.protobuf.CodedOutputStream + .computeBoolSize(5, isFollowing_); + } size += getUnknownFields().getSerializedSize(); memoizedSerializedSize = size; return size; @@ -1688,6 +1755,8 @@ public final class Protos { bitField0_ = (bitField0_ & ~0x00000004); lookaheadSize_ = 0; bitField0_ = (bitField0_ & ~0x00000008); + isFollowing_ = false; + bitField0_ = (bitField0_ & ~0x00000010); return this; } @@ -1733,6 +1802,10 @@ public final class Protos { to_bitField0_ |= 0x00000004; } result.lookaheadSize_ = lookaheadSize_; + if (((from_bitField0_ & 0x00000010) == 0x00000010)) { + to_bitField0_ |= 0x00000008; + } + result.isFollowing_ = isFollowing_; result.bitField0_ = to_bitField0_; onBuilt(); return result; @@ -1768,6 +1841,9 @@ public final class Protos { if (other.hasLookaheadSize()) { setLookaheadSize(other.getLookaheadSize()); } + if (other.hasIsFollowing()) { + setIsFollowing(other.getIsFollowing()); + } this.mergeUnknownFields(other.getUnknownFields()); return this; } @@ -2058,6 +2134,67 @@ public final class Protos { return this; } + // optional bool isFollowing = 5; + private boolean isFollowing_ ; + /** + * optional bool isFollowing = 5; + * + *
+       **
+       * Flag indicating that this key is a root of a following chain. This chain is following the next non-following chain.
+       * Following/followed chains concept is used for married keychains, where the set of keys combined together to produce
+       * a single P2SH multisignature address
+       * 
+ */ + public boolean hasIsFollowing() { + return ((bitField0_ & 0x00000010) == 0x00000010); + } + /** + * optional bool isFollowing = 5; + * + *
+       **
+       * Flag indicating that this key is a root of a following chain. This chain is following the next non-following chain.
+       * Following/followed chains concept is used for married keychains, where the set of keys combined together to produce
+       * a single P2SH multisignature address
+       * 
+ */ + public boolean getIsFollowing() { + return isFollowing_; + } + /** + * optional bool isFollowing = 5; + * + *
+       **
+       * Flag indicating that this key is a root of a following chain. This chain is following the next non-following chain.
+       * Following/followed chains concept is used for married keychains, where the set of keys combined together to produce
+       * a single P2SH multisignature address
+       * 
+ */ + public Builder setIsFollowing(boolean value) { + bitField0_ |= 0x00000010; + isFollowing_ = value; + onChanged(); + return this; + } + /** + * optional bool isFollowing = 5; + * + *
+       **
+       * Flag indicating that this key is a root of a following chain. This chain is following the next non-following chain.
+       * Following/followed chains concept is used for married keychains, where the set of keys combined together to produce
+       * a single P2SH multisignature address
+       * 
+ */ + public Builder clearIsFollowing() { + bitField0_ = (bitField0_ & ~0x00000010); + isFollowing_ = false; + onChanged(); + return this; + } + // @@protoc_insertion_point(builder_scope:wallet.DeterministicKey) } @@ -16021,69 +16158,69 @@ 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\"d\n\020DeterministicKey\022\022\n\nchain_co" + + "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\"\302\002\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.w" + - "allet.EncryptedData\022\022\n\npublic_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.D" + - "eterministicKey\"b\n\004Type\022\014\n\010ORIGINAL\020\001\022\030\n" + - "\024ENCRYPTED_SCRYPT_AES\020\002\022\033\n\027DETERMINISTIC" + - "_ROOT_SEED\020\003\022\025\n\021DETERMINISTIC_KEY\020\004\"5\n\006S" + - "cript\022\017\n\007program\030\001 \002(\014\022\032\n\022creation_times" + - "tamp\030\002 \002(\003\"\222\001\n\020TransactionInput\022\"\n\032trans" + - "action_out_point_hash\030\001 \002(\014\022#\n\033transacti" + - "on_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\021TransactionOutput\022\r\n\005value\030\001 \002(\003\022\024\n\014s" + - "cript_bytes\030\002 \002(\014\022!\n\031spent_by_transactio" + - "n_hash\030\003 \001(\014\022\"\n\032spent_by_transaction_ind" + - "ex\030\004 \001(\005\"\234\003\n\025TransactionConfidence\0220\n\004ty" + - "pe\030\001 \001(\0162\".wallet.TransactionConfidence." + - "Type\022\032\n\022appeared_at_height\030\002 \001(\005\022\036\n\026over" + - "riding_transaction\030\003 \001(\014\022\r\n\005depth\030\004 \001(\005\022" + - "\021\n\twork_done\030\005 \001(\003\022)\n\014broadcast_by\030\006 \003(\013" + - "2\023.wallet.PeerAddress\0224\n\006source\030\007 \001(\0162$." + - "wallet.TransactionConfidence.Source\"O\n\004T", - "ype\022\013\n\007UNKNOWN\020\000\022\014\n\010BUILDING\020\001\022\013\n\007PENDIN" + - "G\020\002\022\025\n\021NOT_IN_BEST_CHAIN\020\003\022\010\n\004DEAD\020\004\"A\n\006" + - "Source\022\022\n\016SOURCE_UNKNOWN\020\000\022\022\n\016SOURCE_NET" + - "WORK\020\001\022\017\n\013SOURCE_SELF\020\002\"\236\004\n\013Transaction\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_t" + - "ime\030\004 \001(\r\022\022\n\nupdated_at\030\005 \001(\003\0223\n\021transac" + - "tion_input\030\006 \003(\0132\030.wallet.TransactionInp" + - "ut\0225\n\022transaction_output\030\007 \003(\0132\031.wallet." + - "TransactionOutput\022\022\n\nblock_hash\030\010 \003(\014\022 \n", - "\030block_relativity_offsets\030\013 \003(\005\0221\n\nconfi" + - "dence\030\t \001(\0132\035.wallet.TransactionConfiden" + - "ce\0225\n\007purpose\030\n \001(\0162\033.wallet.Transaction" + - ".Purpose:\007UNKNOWN\"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\007PEN" + - "DING\020\020\022\024\n\020PENDING_INACTIVE\020\022\":\n\007Purpose\022" + - "\013\n\007UNKNOWN\020\000\022\020\n\014USER_PAYMENT\020\001\022\020\n\014KEY_RO" + - "TATION\020\002\"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\"\261\004\n\006Wallet\022\032\n\022netwo" + - "rk_identifier\030\001 \002(\t\022\034\n\024last_seen_block_h" + - "ash\030\002 \001(\014\022\036\n\026last_seen_block_height\030\014 \001(" + - "\r\022!\n\031last_seen_block_time_secs\030\016 \001(\003\022\030\n\003" + - "key\030\003 \003(\0132\013.wallet.Key\022(\n\013transaction\030\004 " + - "\003(\0132\023.wallet.Transaction\022&\n\016watched_scri" + - "pt\030\017 \003(\0132\016.wallet.Script\022C\n\017encryption_t" + - "ype\030\005 \001(\0162\035.wallet.Wallet.EncryptionType" + - ":\013UNENCRYPTED\0227\n\025encryption_parameters\030\006", - " \001(\0132\030.wallet.ScryptParameters\022\022\n\007versio" + - "n\030\007 \001(\005:\0011\022$\n\textension\030\n \003(\0132\021.wallet.E" + - "xtension\022\023\n\013description\030\013 \001(\t\022\031\n\021key_rot" + - "ation_time\030\r \001(\004\022\031\n\004tags\030\020 \003(\0132\013.wallet." + - "Tag\";\n\016EncryptionType\022\017\n\013UNENCRYPTED\020\001\022\030" + - "\n\024ENCRYPTED_SCRYPT_AES\020\002B\035\n\023org.bitcoinj" + - ".walletB\006Protos" + "\030\003 \001(\r\022\026\n\016lookahead_size\030\004 \001(\r\022\023\n\013isFoll" + + "owing\030\005 \001(\010\"\302\002\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\"b\n\004" + + "Type\022\014\n\010ORIGINAL\020\001\022\030\n\024ENCRYPTED_SCRYPT_A" + + "ES\020\002\022\033\n\027DETERMINISTIC_ROOT_SEED\020\003\022\025\n\021DET" + + "ERMINISTIC_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\020Tran" + + "sactionInput\022\"\n\032transaction_out_point_ha" + + "sh\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\021TransactionOutpu" + + "t\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\032spe" + + "nt_by_transaction_index\030\004 \001(\005\"\234\003\n\025Transa" + + "ctionConfidence\0220\n\004type\030\001 \001(\0162\".wallet.T" + + "ransactionConfidence.Type\022\032\n\022appeared_at" + + "_height\030\002 \001(\005\022\036\n\026overriding_transaction\030" + + "\003 \001(\014\022\r\n\005depth\030\004 \001(\005\022\021\n\twork_done\030\005 \001(\003\022" + + ")\n\014broadcast_by\030\006 \003(\0132\023.wallet.PeerAddre" + + "ss\0224\n\006source\030\007 \001(\0162$.wallet.TransactionC", + "onfidence.Source\"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\004DEAD\020\004\"A\n\006Source\022\022\n\016SOURCE_UN" + + "KNOWN\020\000\022\022\n\016SOURCE_NETWORK\020\001\022\017\n\013SOURCE_SE" + + "LF\020\002\"\236\004\n\013Transaction\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.Trans" + + "action.Pool\022\021\n\tlock_time\030\004 \001(\r\022\022\n\nupdate" + + "d_at\030\005 \001(\003\0223\n\021transaction_input\030\006 \003(\0132\030." + + "wallet.TransactionInput\0225\n\022transaction_o" + + "utput\030\007 \003(\0132\031.wallet.TransactionOutput\022\022", + "\n\nblock_hash\030\010 \003(\014\022 \n\030block_relativity_o" + + "ffsets\030\013 \003(\005\0221\n\nconfidence\030\t \001(\0132\035.walle" + + "t.TransactionConfidence\0225\n\007purpose\030\n \001(\016" + + "2\033.wallet.Transaction.Purpose:\007UNKNOWN\"Y" + + "\n\004Pool\022\013\n\007UNSPENT\020\004\022\t\n\005SPENT\020\005\022\014\n\010INACTI" + + "VE\020\002\022\010\n\004DEAD\020\n\022\013\n\007PENDING\020\020\022\024\n\020PENDING_I" + + "NACTIVE\020\022\":\n\007Purpose\022\013\n\007UNKNOWN\020\000\022\020\n\014USE" + + "R_PAYMENT\020\001\022\020\n\014KEY_ROTATION\020\002\"N\n\020ScryptP" + + "arameters\022\014\n\004salt\030\001 \002(\014\022\020\n\001n\030\002 \001(\003:\0051638" + + "4\022\014\n\001r\030\003 \001(\005:\0018\022\014\n\001p\030\004 \001(\005:\0011\"8\n\tExtensi", + "on\022\n\n\002id\030\001 \002(\t\022\014\n\004data\030\002 \002(\014\022\021\n\tmandator" + + "y\030\003 \002(\010\" \n\003Tag\022\013\n\003tag\030\001 \002(\t\022\014\n\004data\030\002 \002(" + + "\014\"\261\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_s" + + "een_block_height\030\014 \001(\r\022!\n\031last_seen_bloc" + + "k_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.Transa" + + "ction\022&\n\016watched_script\030\017 \003(\0132\016.wallet.S" + + "cript\022C\n\017encryption_type\030\005 \001(\0162\035.wallet." + + "Wallet.EncryptionType:\013UNENCRYPTED\0227\n\025en", + "cryption_parameters\030\006 \001(\0132\030.wallet.Scryp" + + "tParameters\022\022\n\007version\030\007 \001(\005:\0011\022$\n\texten" + + "sion\030\n \003(\0132\021.wallet.Extension\022\023\n\013descrip" + + "tion\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\";\n\016EncryptionTy" + + "pe\022\017\n\013UNENCRYPTED\020\001\022\030\n\024ENCRYPTED_SCRYPT_" + + "AES\020\002B\035\n\023org.bitcoinj.walletB\006Protos" }; com.google.protobuf.Descriptors.FileDescriptor.InternalDescriptorAssigner assigner = new com.google.protobuf.Descriptors.FileDescriptor.InternalDescriptorAssigner() { @@ -16107,7 +16244,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", }); + new java.lang.String[] { "ChainCode", "Path", "IssuedSubkeys", "LookaheadSize", "IsFollowing", }); internal_static_wallet_Key_descriptor = getDescriptor().getMessageTypes().get(3); internal_static_wallet_Key_fieldAccessorTable = new diff --git a/core/src/test/java/com/google/bitcoin/core/AddressTest.java b/core/src/test/java/com/google/bitcoin/core/AddressTest.java index 097324cd..f324d0fc 100644 --- a/core/src/test/java/com/google/bitcoin/core/AddressTest.java +++ b/core/src/test/java/com/google/bitcoin/core/AddressTest.java @@ -19,10 +19,12 @@ package com.google.bitcoin.core; import com.google.bitcoin.params.MainNetParams; import com.google.bitcoin.params.TestNet3Params; +import com.google.bitcoin.script.Script; import com.google.bitcoin.script.ScriptBuilder; import org.junit.Test; import java.util.Arrays; +import java.util.List; import static com.google.bitcoin.core.Utils.HEX; import static org.junit.Assert.*; @@ -120,4 +122,20 @@ public class AddressTest { Address c = Address.fromP2SHScript(mainParams, ScriptBuilder.createP2SHOutputScript(hex)); assertEquals("35b9vsyH1KoFT5a5KtrKusaCcPLkiSo1tU", c.toString()); } + + @Test + public void p2shAddressCreationFromKeys() throws Exception { + // import some keys from this example: https://gist.github.com/gavinandresen/3966071 + ECKey key1 = new DumpedPrivateKey(mainParams, "5JaTXbAUmfPYZFRwrYaALK48fN6sFJp4rHqq2QSXs8ucfpE4yQU").getKey(); + key1 = ECKey.fromPrivate(key1.getPrivKeyBytes()); + ECKey key2 = new DumpedPrivateKey(mainParams, "5Jb7fCeh1Wtm4yBBg3q3XbT6B525i17kVhy3vMC9AqfR6FH2qGk").getKey(); + key2 = ECKey.fromPrivate(key2.getPrivKeyBytes()); + ECKey key3 = new DumpedPrivateKey(mainParams, "5JFjmGo5Fww9p8gvx48qBYDJNAzR9pmH5S389axMtDyPT8ddqmw").getKey(); + key3 = ECKey.fromPrivate(key3.getPrivKeyBytes()); + + List keys = Arrays.asList(key1, key2, key3); + Script p2shScript = ScriptBuilder.createP2SHOutputScript(2, keys); + Address address = Address.fromP2SHScript(mainParams, p2shScript); + assertEquals("3N25saC4dT24RphDAwLtD8LUN4E2gZPJke", address.toString()); + } } diff --git a/core/src/test/java/com/google/bitcoin/wallet/KeyChainGroupTest.java b/core/src/test/java/com/google/bitcoin/wallet/KeyChainGroupTest.java index c6cf1648..92503b3f 100644 --- a/core/src/test/java/com/google/bitcoin/wallet/KeyChainGroupTest.java +++ b/core/src/test/java/com/google/bitcoin/wallet/KeyChainGroupTest.java @@ -16,13 +16,9 @@ package com.google.bitcoin.wallet; -import com.google.bitcoin.core.BloomFilter; -import com.google.bitcoin.core.ECKey; -import com.google.bitcoin.core.Sha256Hash; -import com.google.bitcoin.core.Utils; -import com.google.bitcoin.crypto.DeterministicKey; -import com.google.bitcoin.crypto.KeyCrypterException; -import com.google.bitcoin.crypto.KeyCrypterScrypt; +import com.google.bitcoin.core.*; +import com.google.bitcoin.crypto.*; +import com.google.bitcoin.params.MainNetParams; import com.google.bitcoin.utils.BriefLogFormatter; import com.google.bitcoin.utils.Threading; import com.google.common.collect.ImmutableList; @@ -43,17 +39,29 @@ public class KeyChainGroupTest { // Number of initial keys in this tests HD wallet, including interior keys. private static final int INITIAL_KEYS = 4; private static final int LOOKAHEAD_SIZE = 5; + private static final NetworkParameters params = MainNetParams.get(); + private static final String XPUB = "xpub68KFnj3bqUx1s7mHejLDBPywCAKdJEu1b49uniEEn2WSbHmZ7xbLqFTjJbtx1LUcAt1DwhoqWHmo2s5WMJp6wi38CiF2hYD49qVViKVvAoi"; private KeyChainGroup group; @Before public void setup() { BriefLogFormatter.init(); Utils.setMockClock(); - group = new KeyChainGroup(); + group = new KeyChainGroup(params); group.setLookaheadSize(LOOKAHEAD_SIZE); // Don't want slow tests. group.getActiveKeyChain(); // Force create a chain. } + private KeyChainGroup createMarriedKeyChainGroup() { + byte[] seedBytes = Sha256Hash.create("don't use a string seed like this in real life".getBytes()).getBytes(); + DeterministicSeed seed = new DeterministicSeed(seedBytes, MnemonicCode.BIP39_STANDARDISATION_TIME_SECS); + DeterministicKey watchingKey = DeterministicKey.deserializeB58(null, XPUB); + KeyChainGroup group = new KeyChainGroup(params, seed, ImmutableList.of(watchingKey)); + group.setLookaheadSize(LOOKAHEAD_SIZE); + group.getActiveKeyChain(); + return group; + } + @Test public void freshCurrentKeys() throws Exception { assertEquals(INITIAL_KEYS, group.numKeys()); @@ -82,6 +90,23 @@ public class KeyChainGroupTest { assertEquals(c2, c3); } + @Test + public void freshCurrentKeysForMarriedKeychain() throws Exception { + group = createMarriedKeyChainGroup(); + + try { + group.freshKey(KeyChain.KeyPurpose.RECEIVE_FUNDS); + fail(); + } catch (UnsupportedOperationException e) { + } + + try { + group.currentKey(KeyChain.KeyPurpose.RECEIVE_FUNDS); + fail(); + } catch (UnsupportedOperationException e) { + } + } + @Test public void imports() throws Exception { ECKey key1 = new ECKey(); @@ -119,6 +144,36 @@ public class KeyChainGroupTest { assertNull(group.findKeyFromPubHash(d.getPubKeyHash())); } + @Test + public void currentP2SHAddress() throws Exception { + group = createMarriedKeyChainGroup(); + + assertEquals(INITIAL_KEYS, group.numKeys()); + Address a1 = group.currentAddress(KeyChain.KeyPurpose.RECEIVE_FUNDS); + assertEquals(INITIAL_KEYS + 1 + LOOKAHEAD_SIZE, group.numKeys()); + assertTrue(a1.isP2SHAddress()); + + Address a2 = group.currentAddress(KeyChain.KeyPurpose.RECEIVE_FUNDS); + assertEquals(a1, a2); + assertEquals(INITIAL_KEYS + 1 + LOOKAHEAD_SIZE, group.numKeys()); + + Address a3 = group.currentAddress(KeyChain.KeyPurpose.CHANGE); + assertNotEquals(a2, a3); + } + + @Test + public void freshAddress() throws Exception { + group = createMarriedKeyChainGroup(); + Address a1 = group.freshAddress(KeyChain.KeyPurpose.RECEIVE_FUNDS); + Address a2 = group.freshAddress(KeyChain.KeyPurpose.RECEIVE_FUNDS); + assertTrue(a1.isP2SHAddress()); + assertNotEquals(a1, a2); + assertEquals(INITIAL_KEYS + 2 + LOOKAHEAD_SIZE, group.numKeys()); + + Address a3 = group.currentAddress(KeyChain.KeyPurpose.RECEIVE_FUNDS); + assertEquals(a2, a3); + } + // Check encryption with and without a basic keychain. @Test @@ -206,7 +261,7 @@ public class KeyChainGroupTest { @Test public void encryptionWhilstEmpty() throws Exception { - group = new KeyChainGroup(); + group = new KeyChainGroup(params); group.setLookaheadSize(5); KeyCrypterScrypt scrypt = new KeyCrypterScrypt(2); final KeyParameter aesKey = scrypt.deriveKey("password"); @@ -286,7 +341,7 @@ public class KeyChainGroupTest { @Test public void serialization() throws Exception { assertEquals(INITIAL_KEYS + 1 /* for the seed */, group.serializeToProtobuf().size()); - group = KeyChainGroup.fromProtobufUnencrypted(group.serializeToProtobuf()); + 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); @@ -296,13 +351,13 @@ public class KeyChainGroupTest { List protoKeys2 = group.serializeToProtobuf(); assertEquals(INITIAL_KEYS + ((LOOKAHEAD_SIZE + 1) * 2) + 1 /* for the seed */ + 2, protoKeys2.size()); - group = KeyChainGroup.fromProtobufUnencrypted(protoKeys1); + 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(protoKeys2); + 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)); @@ -311,7 +366,7 @@ public class KeyChainGroupTest { final KeyParameter aesKey = scrypt.deriveKey("password"); group.encrypt(scrypt, aesKey); List protoKeys3 = group.serializeToProtobuf(); - group = KeyChainGroup.fromProtobufEncrypted(protoKeys3, scrypt); + group = KeyChainGroup.fromProtobufEncrypted(params, protoKeys3, scrypt); assertTrue(group.isEncrypted()); assertTrue(group.checkPassword("password")); group.decrypt(aesKey); @@ -319,11 +374,53 @@ public class KeyChainGroupTest { // No need for extensive contents testing here, as that's done in the keychain class tests. } + @Test + public void serializeWatching() throws Exception { + group = new KeyChainGroup(params, DeterministicKey.deserializeB58(null, XPUB)); + group.setLookaheadSize(LOOKAHEAD_SIZE); + group.freshKey(KeyChain.KeyPurpose.RECEIVE_FUNDS); + group.freshKey(KeyChain.KeyPurpose.CHANGE); + List protoKeys1 = group.serializeToProtobuf(); + assertEquals(3 + (LOOKAHEAD_SIZE + 1) * 2, protoKeys1.size()); + group = KeyChainGroup.fromProtobufUnencrypted(params, protoKeys1); + assertEquals(3 + (LOOKAHEAD_SIZE + 1) * 2, group.serializeToProtobuf().size()); + } + + @Test + public void serializeMarried() throws Exception { + group = createMarriedKeyChainGroup(); + DeterministicKeyChain keyChain = group.getActiveKeyChain(); + keyChain.getKey(KeyChain.KeyPurpose.RECEIVE_FUNDS); + DeterministicKey key1 = keyChain.getKey(KeyChain.KeyPurpose.RECEIVE_FUNDS); + ImmutableList path = key1.getPath(); + assertTrue(group.isMarried(keyChain)); + + List protoKeys3 = group.serializeToProtobuf(); + group = KeyChainGroup.fromProtobufUnencrypted(params, protoKeys3); + assertTrue(group.isMarried(keyChain)); + DeterministicKey key2 = keyChain.getKeyByPath(path); + assertEquals(key1, key2); + } + + @Test + public void addFollowingAccounts() throws Exception { + assertFalse(group.isMarried()); + group.addFollowingAccounts(ImmutableList.of(DeterministicKey.deserializeB58(null, XPUB))); + assertTrue(group.isMarried()); + } + + @Test (expected = IllegalStateException.class) + public void addFollowingAccountsTwiceShouldFail() { + ImmutableList followingKeys = ImmutableList.of(DeterministicKey.deserializeB58(null, XPUB)); + group.addFollowingAccounts(followingKeys); + group.addFollowingAccounts(followingKeys); + } + @Test public void constructFromSeed() throws Exception { ECKey key1 = group.freshKey(KeyChain.KeyPurpose.RECEIVE_FUNDS); final DeterministicSeed seed = checkNotNull(group.getActiveKeyChain().getSeed()); - KeyChainGroup group2 = new KeyChainGroup(seed); + KeyChainGroup group2 = new KeyChainGroup(params, seed); group2.setLookaheadSize(5); ECKey key2 = group2.freshKey(KeyChain.KeyPurpose.RECEIVE_FUNDS); assertEquals(key1, key2); @@ -332,7 +429,7 @@ public class KeyChainGroupTest { @Test(expected = DeterministicUpgradeRequiredException.class) public void deterministicUpgradeRequired() throws Exception { // Check that if we try to use HD features in a KCG that only has random keys, we get an exception. - group = new KeyChainGroup(); + group = new KeyChainGroup(params); group.importKeys(new ECKey(), new ECKey()); assertTrue(group.isDeterministicUpgradeRequired()); group.freshKey(KeyChain.KeyPurpose.RECEIVE_FUNDS); // throws @@ -342,7 +439,7 @@ public class KeyChainGroupTest { public void deterministicUpgradeUnencrypted() throws Exception { // Check that a group that contains only random keys has its HD chain created using the private key bytes of // the oldest random key, so upgrading the same wallet twice gives the same outcome. - group = new KeyChainGroup(); + group = new KeyChainGroup(params); group.setLookaheadSize(LOOKAHEAD_SIZE); // Don't want slow tests. ECKey key1 = new ECKey(); Utils.rollMockClock(86400); @@ -356,7 +453,7 @@ public class KeyChainGroupTest { DeterministicSeed seed1 = group.getActiveKeyChain().getSeed(); assertNotNull(seed1); - group = KeyChainGroup.fromProtobufUnencrypted(protobufs); + 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(); @@ -370,7 +467,7 @@ public class KeyChainGroupTest { @Test public void deterministicUpgradeRotating() throws Exception { - group = new KeyChainGroup(); + group = new KeyChainGroup(params); group.setLookaheadSize(LOOKAHEAD_SIZE); // Don't want slow tests. long now = Utils.currentTimeSeconds(); ECKey key1 = new ECKey(); @@ -389,7 +486,7 @@ public class KeyChainGroupTest { @Test public void deterministicUpgradeEncrypted() throws Exception { - group = new KeyChainGroup(); + group = new KeyChainGroup(params); final ECKey key = new ECKey(); group.importKeys(key); final KeyCrypterScrypt crypter = new KeyCrypterScrypt(); diff --git a/core/src/wallet.proto b/core/src/wallet.proto index 2847f1f1..ef347e85 100644 --- a/core/src/wallet.proto +++ b/core/src/wallet.proto @@ -59,6 +59,13 @@ message DeterministicKey { // If this field is missing it means we're not issuing subkeys of this key to users. optional uint32 issued_subkeys = 3; optional uint32 lookahead_size = 4; + + /** + * Flag indicating that this key is a root of a following chain. This chain is following the next non-following chain. + * Following/followed chains concept is used for married keychains, where the set of keys combined together to produce + * a single P2SH multisignature address + */ + optional bool isFollowing = 5; } /**