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:
parent
b7cb4d8c47
commit
06755aefde
@ -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");
|
||||
|
@ -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);
|
||||
|
@ -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);
|
||||
}
|
||||
}
|
||||
|
@ -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);
|
||||
|
||||
|
@ -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);
|
||||
|
@ -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,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.
|
||||
* <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,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<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. */
|
||||
@ -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) {
|
||||
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(" ");
|
||||
|
@ -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
|
||||
|
@ -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());
|
||||
}
|
||||
}
|
||||
|
@ -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();
|
||||
|
@ -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;
|
||||
}
|
||||
|
||||
/**
|
||||
|
Loading…
Reference in New Issue
Block a user