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

Married HD wallets: introduce shadow keychain notion

Pull request: #99
Based on design notes:
https://groups.google.com/d/msg/bitcoinj/Uxl-z40OLuQ/e2m4mEWR6gMJ
This commit is contained in:
troggy 2014-05-31 19:30:44 +03:00 committed by Mike Hearn
parent b7cb4d8c47
commit 06755aefde
10 changed files with 596 additions and 126 deletions

View File

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

View File

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

View File

@ -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<ECKey> pubkeys) {
pubkeys = new ArrayList<ECKey>(pubkeys);
final Comparator comparator = UnsignedBytes.lexicographicalComparator();
Collections.sort(pubkeys, new Comparator<ECKey>() {
@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);
}
}

View File

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

View File

@ -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());
}
/**
* <p>Creates a deterministic key chain with the given watch key. If <code>isFollowing</code> flag is set then this keychain follows
* 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) {
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<ChildNumber> 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);

View File

@ -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<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;
private EnumMap<KeyChain.KeyPurpose, Address> 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<DeterministicKeyChain>(1), null, null);
public KeyChainGroup(NetworkParameters params) {
this(params, null, new ArrayList<DeterministicKeyChain>(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<DeterministicKey> 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<DeterministicKey> 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<DeterministicKeyChain> chains, @Nullable EnumMap<KeyChain.KeyPurpose, DeterministicKey> currentKeys, @Nullable KeyCrypter crypter) {
private KeyChainGroup(NetworkParameters params, @Nullable BasicKeyChain basicKeyChain, List<DeterministicKeyChain> chains, @Nullable EnumMap<KeyChain.KeyPurpose, DeterministicKey> currentKeys, Multimap<DeterministicKey, DeterministicKeyChain> followingKeychains, @Nullable KeyCrypter crypter) {
this.params = params;
this.basic = basicKeyChain == null ? new BasicKeyChain() : basicKeyChain;
this.chains = new ArrayList<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);
}
}
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).
* <p>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</p>
*/
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,9 +183,15 @@ public class KeyChainGroup {
/**
* Returns address for a {@link #currentKey(com.google.bitcoin.wallet.KeyChain.KeyPurpose)}
*/
public Address currentAddress(KeyChain.KeyPurpose purpose, NetworkParameters 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);
}
}
/**
* Returns a key that has not been returned by this method before (fresh). You can think of this as being
@ -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.
* <p>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</p>
*/
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.
* <p>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</p>
*/
public List<DeterministicKey> 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<DeterministicKey> 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,9 +236,29 @@ public class KeyChainGroup {
/**
* Returns address for a {@link #freshKey(com.google.bitcoin.wallet.KeyChain.KeyPurpose)}
*/
public Address freshAddress(KeyChain.KeyPurpose purpose, NetworkParameters params) {
public Address freshAddress(KeyChain.KeyPurpose purpose) {
DeterministicKeyChain chain = getActiveKeyChain();
DeterministicKey key = chain.getKey(purpose);
if (isMarried(chain)) {
List<ECKey> keys = ImmutableList.<ECKey>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<ECKey> getFollowingKeys(KeyChain.KeyPurpose purpose, DeterministicKey followedChainWatchKey) {
List<ECKey> keys = new ArrayList<ECKey>();
Collection<DeterministicKeyChain> 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. */
public DeterministicKeyChain getActiveKeyChain() {
@ -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.Key> protos = chain.serializeToProtobuf();
result.addAll(protos);
}
return result;
}
public static KeyChainGroup fromProtobufUnencrypted(List<Protos.Key> keys) throws UnreadableWalletException {
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);
return new KeyChainGroup(basicKeyChain, chains, currentKeys, null);
Multimap<DeterministicKey, DeterministicKeyChain> followingKeychains = extractFollowingKeychains(chains);
return new KeyChainGroup(params, basicKeyChain, chains, currentKeys, followingKeychains, null);
}
public static KeyChainGroup fromProtobufEncrypted(List<Protos.Key> keys, KeyCrypter crypter) throws UnreadableWalletException {
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);
return new KeyChainGroup(basicKeyChain, chains, currentKeys, crypter);
Multimap<DeterministicKey, DeterministicKeyChain> 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<DeterministicKey, DeterministicKeyChain> 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>();
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();
}
}
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) {
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(" ");

View File

@ -1245,6 +1245,30 @@ public final class Protos {
* <code>optional uint32 lookahead_size = 4;</code>
*/
int getLookaheadSize();
// optional bool isFollowing = 5;
/**
* <code>optional bool isFollowing = 5;</code>
*
* <pre>
**
* 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
* </pre>
*/
boolean hasIsFollowing();
/**
* <code>optional bool isFollowing = 5;</code>
*
* <pre>
**
* 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
* </pre>
*/
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_;
/**
* <code>optional bool isFollowing = 5;</code>
*
* <pre>
**
* 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
* </pre>
*/
public boolean hasIsFollowing() {
return ((bitField0_ & 0x00000008) == 0x00000008);
}
/**
* <code>optional bool isFollowing = 5;</code>
*
* <pre>
**
* 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
* </pre>
*/
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_ ;
/**
* <code>optional bool isFollowing = 5;</code>
*
* <pre>
**
* 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
* </pre>
*/
public boolean hasIsFollowing() {
return ((bitField0_ & 0x00000010) == 0x00000010);
}
/**
* <code>optional bool isFollowing = 5;</code>
*
* <pre>
**
* 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
* </pre>
*/
public boolean getIsFollowing() {
return isFollowing_;
}
/**
* <code>optional bool isFollowing = 5;</code>
*
* <pre>
**
* 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
* </pre>
*/
public Builder setIsFollowing(boolean value) {
bitField0_ |= 0x00000010;
isFollowing_ = value;
onChanged();
return this;
}
/**
* <code>optional bool isFollowing = 5;</code>
*
* <pre>
**
* 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
* </pre>
*/
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

View File

@ -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<ECKey> keys = Arrays.asList(key1, key2, key3);
Script p2shScript = ScriptBuilder.createP2SHOutputScript(2, keys);
Address address = Address.fromP2SHScript(mainParams, p2shScript);
assertEquals("3N25saC4dT24RphDAwLtD8LUN4E2gZPJke", address.toString());
}
}

View File

@ -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<Protos.Key> 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<Protos.Key> 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<Protos.Key> 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<ChildNumber> path = key1.getPath();
assertTrue(group.isMarried(keyChain));
List<Protos.Key> 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<DeterministicKey> 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();

View File

@ -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;
}
/**