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.
|
* see loadFromFile.
|
||||||
*/
|
*/
|
||||||
public Wallet(NetworkParameters params) {
|
public Wallet(NetworkParameters params) {
|
||||||
this(params, new KeyChainGroup());
|
this(params, new KeyChainGroup(params));
|
||||||
}
|
}
|
||||||
|
|
||||||
public static Wallet fromSeed(NetworkParameters params, DeterministicSeed seed) {
|
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) {
|
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) {
|
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.
|
// 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();
|
lock.lock();
|
||||||
try {
|
try {
|
||||||
maybeUpgradeToHD();
|
maybeUpgradeToHD();
|
||||||
return keychain.currentAddress(purpose, params);
|
return keychain.currentAddress(purpose);
|
||||||
} finally {
|
} finally {
|
||||||
lock.unlock();
|
lock.unlock();
|
||||||
}
|
}
|
||||||
@ -372,8 +372,7 @@ public class Wallet extends BaseTaggableObject implements Serializable, BlockCha
|
|||||||
public Address freshAddress(KeyChain.KeyPurpose purpose) {
|
public Address freshAddress(KeyChain.KeyPurpose purpose) {
|
||||||
lock.lock();
|
lock.lock();
|
||||||
try {
|
try {
|
||||||
maybeUpgradeToHD();
|
Address key = keychain.freshAddress(purpose);
|
||||||
Address key = keychain.freshAddress(purpose, params);
|
|
||||||
saveNow();
|
saveNow();
|
||||||
return key;
|
return key;
|
||||||
} finally {
|
} 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. */
|
/** See {@link com.google.bitcoin.wallet.DeterministicKeyChain#setLookaheadSize(int)} for more info on this. */
|
||||||
public void setKeychainLookaheadSize(int lookaheadSize) {
|
public void setKeychainLookaheadSize(int lookaheadSize) {
|
||||||
lock.lock();
|
lock.lock();
|
||||||
@ -2765,7 +2778,7 @@ public class Wallet extends BaseTaggableObject implements Serializable, BlockCha
|
|||||||
|
|
||||||
// Do the keys.
|
// Do the keys.
|
||||||
builder.append("\nKeys:\n");
|
builder.append("\nKeys:\n");
|
||||||
builder.append(keychain.toString(params, includePrivateKeys));
|
builder.append(keychain.toString(includePrivateKeys));
|
||||||
|
|
||||||
if (!watchedScripts.isEmpty()) {
|
if (!watchedScripts.isEmpty()) {
|
||||||
builder.append("\nWatched scripts:\n");
|
builder.append("\nWatched scripts:\n");
|
||||||
|
@ -240,7 +240,7 @@ public class WalletAppKit extends AbstractIdleService {
|
|||||||
walletStream.close();
|
walletStream.close();
|
||||||
}
|
}
|
||||||
} else {
|
} 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();
|
vWallet.freshReceiveKey();
|
||||||
for (WalletExtension e : provideWalletExtensions()) {
|
for (WalletExtension e : provideWalletExtensions()) {
|
||||||
vWallet.addExtension(e);
|
vWallet.addExtension(e);
|
||||||
|
@ -18,13 +18,14 @@ package com.google.bitcoin.script;
|
|||||||
|
|
||||||
import com.google.bitcoin.core.Address;
|
import com.google.bitcoin.core.Address;
|
||||||
import com.google.bitcoin.core.ECKey;
|
import com.google.bitcoin.core.ECKey;
|
||||||
|
import com.google.bitcoin.core.Utils;
|
||||||
import com.google.bitcoin.crypto.TransactionSignature;
|
import com.google.bitcoin.crypto.TransactionSignature;
|
||||||
import com.google.common.collect.Lists;
|
import com.google.common.collect.Lists;
|
||||||
|
import com.google.common.primitives.UnsignedBytes;
|
||||||
|
|
||||||
import javax.annotation.Nullable;
|
import javax.annotation.Nullable;
|
||||||
import java.util.ArrayList;
|
import java.math.BigInteger;
|
||||||
import java.util.Arrays;
|
import java.util.*;
|
||||||
import java.util.List;
|
|
||||||
|
|
||||||
import static com.google.bitcoin.script.ScriptOpCodes.*;
|
import static com.google.bitcoin.script.ScriptOpCodes.*;
|
||||||
import static com.google.common.base.Preconditions.checkArgument;
|
import static com.google.common.base.Preconditions.checkArgument;
|
||||||
@ -185,4 +186,22 @@ public class ScriptBuilder {
|
|||||||
checkArgument(hash.length == 20);
|
checkArgument(hash.length == 20);
|
||||||
return new ScriptBuilder().op(OP_HASH160).data(hash).op(OP_EQUAL).build();
|
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()) {
|
if (walletProto.hasEncryptionParameters()) {
|
||||||
Protos.ScryptParameters encryptionParameters = walletProto.getEncryptionParameters();
|
Protos.ScryptParameters encryptionParameters = walletProto.getEncryptionParameters();
|
||||||
final KeyCrypterScrypt keyCrypter = new KeyCrypterScrypt(encryptionParameters);
|
final KeyCrypterScrypt keyCrypter = new KeyCrypterScrypt(encryptionParameters);
|
||||||
chain = KeyChainGroup.fromProtobufEncrypted(walletProto.getKeyList(), keyCrypter);
|
chain = KeyChainGroup.fromProtobufEncrypted(params, walletProto.getKeyList(), keyCrypter);
|
||||||
} else {
|
} else {
|
||||||
chain = KeyChainGroup.fromProtobufUnencrypted(walletProto.getKeyList());
|
chain = KeyChainGroup.fromProtobufUnencrypted(params, walletProto.getKeyList());
|
||||||
}
|
}
|
||||||
Wallet wallet = factory.create(params, chain);
|
Wallet wallet = factory.create(params, chain);
|
||||||
|
|
||||||
|
@ -116,6 +116,9 @@ public class DeterministicKeyChain implements EncryptableKeyChain {
|
|||||||
// money.
|
// money.
|
||||||
private final BasicKeyChain basicKeyChain;
|
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}
|
* Generates a new key chain with a 128 bit seed selected randomly from the given {@link java.security.SecureRandom}
|
||||||
* object.
|
* object.
|
||||||
@ -165,6 +168,25 @@ public class DeterministicKeyChain implements EncryptableKeyChain {
|
|||||||
this(watchingKey, Utils.currentTimeSeconds());
|
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
|
* 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
|
* 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
|
// Serialization support
|
||||||
@ -485,6 +514,10 @@ public class DeterministicKeyChain implements EncryptableKeyChain {
|
|||||||
detKey.setIssuedSubkeys(issuedInternalKeys);
|
detKey.setIssuedSubkeys(issuedInternalKeys);
|
||||||
detKey.setLookaheadSize(lookaheadSize);
|
detKey.setLookaheadSize(lookaheadSize);
|
||||||
}
|
}
|
||||||
|
// flag the very first key of following keychain
|
||||||
|
if (entries.isEmpty() && isFollowing()) {
|
||||||
|
detKey.setIsFollowing(true);
|
||||||
|
}
|
||||||
entries.add(proto.build());
|
entries.add(proto.build());
|
||||||
}
|
}
|
||||||
return entries;
|
return entries;
|
||||||
@ -537,13 +570,27 @@ public class DeterministicKeyChain implements EncryptableKeyChain {
|
|||||||
final ImmutableList<ChildNumber> immutablePath = ImmutableList.copyOf(path);
|
final ImmutableList<ChildNumber> immutablePath = ImmutableList.copyOf(path);
|
||||||
// Possibly create the chain, if we didn't already do so yet.
|
// Possibly create the chain, if we didn't already do so yet.
|
||||||
boolean isWatchingAccountKey = false;
|
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 (chain == null) {
|
||||||
if (seed == null) {
|
if (seed == null) {
|
||||||
DeterministicKey accountKey = new DeterministicKey(immutablePath, chainCode, pubkey, null, null);
|
DeterministicKey accountKey = new DeterministicKey(immutablePath, chainCode, pubkey, null, null);
|
||||||
if (!accountKey.getPath().equals(ACCOUNT_ZERO_PATH))
|
if (!accountKey.getPath().equals(ACCOUNT_ZERO_PATH))
|
||||||
throw new UnreadableWalletException("Expecting account key but found key with path: " +
|
throw new UnreadableWalletException("Expecting account key but found key with path: " +
|
||||||
HDUtils.formatPath(accountKey.getPath()));
|
HDUtils.formatPath(accountKey.getPath()));
|
||||||
chain = new DeterministicKeyChain(accountKey);
|
chain = new DeterministicKeyChain(accountKey, isFollowingKey);
|
||||||
isWatchingAccountKey = true;
|
isWatchingAccountKey = true;
|
||||||
} else {
|
} else {
|
||||||
chain = new DeterministicKeyChain(seed, crypter);
|
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.ChildNumber;
|
||||||
import com.google.bitcoin.crypto.DeterministicKey;
|
import com.google.bitcoin.crypto.DeterministicKey;
|
||||||
import com.google.bitcoin.crypto.KeyCrypter;
|
import com.google.bitcoin.crypto.KeyCrypter;
|
||||||
|
import com.google.bitcoin.script.ScriptBuilder;
|
||||||
import com.google.bitcoin.store.UnreadableWalletException;
|
import com.google.bitcoin.store.UnreadableWalletException;
|
||||||
import com.google.bitcoin.utils.ListenerRegistration;
|
import com.google.bitcoin.utils.ListenerRegistration;
|
||||||
import com.google.bitcoin.utils.Threading;
|
import com.google.bitcoin.utils.Threading;
|
||||||
import com.google.common.base.Joiner;
|
import com.google.common.base.Joiner;
|
||||||
|
import com.google.common.collect.HashMultimap;
|
||||||
import com.google.common.collect.ImmutableList;
|
import com.google.common.collect.ImmutableList;
|
||||||
import com.google.common.collect.Lists;
|
import com.google.common.collect.Lists;
|
||||||
|
import com.google.common.collect.Multimap;
|
||||||
import org.bitcoinj.wallet.Protos;
|
import org.bitcoinj.wallet.Protos;
|
||||||
import org.slf4j.Logger;
|
import org.slf4j.Logger;
|
||||||
import org.slf4j.LoggerFactory;
|
import org.slf4j.LoggerFactory;
|
||||||
@ -58,28 +61,34 @@ import static com.google.common.base.Preconditions.*;
|
|||||||
public class KeyChainGroup {
|
public class KeyChainGroup {
|
||||||
private static final Logger log = LoggerFactory.getLogger(KeyChainGroup.class);
|
private static final Logger log = LoggerFactory.getLogger(KeyChainGroup.class);
|
||||||
private BasicKeyChain basic;
|
private BasicKeyChain basic;
|
||||||
|
private NetworkParameters params;
|
||||||
private final List<DeterministicKeyChain> chains;
|
private final List<DeterministicKeyChain> chains;
|
||||||
private final EnumMap<KeyChain.KeyPurpose, DeterministicKey> currentKeys;
|
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;
|
@Nullable private KeyCrypter keyCrypter;
|
||||||
private int lookaheadSize = -1;
|
private int lookaheadSize = -1;
|
||||||
private int lookaheadThreshold = -1;
|
private int lookaheadThreshold = -1;
|
||||||
|
|
||||||
/** Creates a keychain group with no basic chain, and a single, lazily created HD chain. */
|
/** Creates a keychain group with no basic chain, and a single, lazily created HD chain. */
|
||||||
public KeyChainGroup() {
|
public KeyChainGroup(NetworkParameters params) {
|
||||||
this(null, new ArrayList<DeterministicKeyChain>(1), null, null);
|
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. */
|
/** Creates a keychain group with no basic chain, and an HD chain initialized from the given seed. */
|
||||||
public KeyChainGroup(DeterministicSeed seed) {
|
public KeyChainGroup(NetworkParameters params, DeterministicSeed seed) {
|
||||||
this(null, ImmutableList.of(new DeterministicKeyChain(seed)), null, null);
|
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.
|
* 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()}.
|
* This HAS to be an account key as returned by {@link DeterministicKeyChain#getWatchingKey()}.
|
||||||
*/
|
*/
|
||||||
public KeyChainGroup(DeterministicKey watchKey) {
|
public KeyChainGroup(NetworkParameters params, DeterministicKey watchKey) {
|
||||||
this(null, ImmutableList.of(DeterministicKeyChain.watch(watchKey)), null, null);
|
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.
|
* was assumed to be first used at the given UNIX time.
|
||||||
* This HAS to be an account key as returned by {@link DeterministicKeyChain#getWatchingKey()}.
|
* This HAS to be an account key as returned by {@link DeterministicKeyChain#getWatchingKey()}.
|
||||||
*/
|
*/
|
||||||
public KeyChainGroup(DeterministicKey watchKey, long creationTimeSecondsSecs) {
|
public KeyChainGroup(NetworkParameters params, DeterministicKey watchKey, long creationTimeSecondsSecs) {
|
||||||
this(null, ImmutableList.of(DeterministicKeyChain.watch(watchKey, creationTimeSecondsSecs)), null, null);
|
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.
|
// 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.basic = basicKeyChain == null ? new BasicKeyChain() : basicKeyChain;
|
||||||
this.chains = new ArrayList<DeterministicKeyChain>(checkNotNull(chains));
|
this.chains = new ArrayList<DeterministicKeyChain>(checkNotNull(chains));
|
||||||
this.keyCrypter = crypter;
|
this.keyCrypter = crypter;
|
||||||
this.currentKeys = currentKeys == null
|
this.currentKeys = currentKeys == null
|
||||||
? new EnumMap<KeyChain.KeyPurpose, DeterministicKey>(KeyChain.KeyPurpose.class)
|
? new EnumMap<KeyChain.KeyPurpose, DeterministicKey>(KeyChain.KeyPurpose.class)
|
||||||
: currentKeys;
|
: 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() {
|
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
|
* {@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
|
* 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).
|
* 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) {
|
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);
|
final DeterministicKey current = currentKeys.get(purpose);
|
||||||
return current != null ? current : freshKey(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)}
|
* 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);
|
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
|
* 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
|
* {@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
|
* 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.
|
* 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) {
|
public DeterministicKey freshKey(KeyChain.KeyPurpose purpose) {
|
||||||
return freshKeys(purpose, 1).get(0);
|
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
|
* {@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
|
* 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.
|
* 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) {
|
public List<DeterministicKey> freshKeys(KeyChain.KeyPurpose purpose, int numberOfKeys) {
|
||||||
DeterministicKeyChain chain = getActiveKeyChain();
|
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.
|
List<DeterministicKey> keys = chain.getKeys(purpose, numberOfKeys); // Always returns the next key along the key chain.
|
||||||
currentKeys.put(purpose, keys.get(keys.size() - 1));
|
currentKeys.put(purpose, keys.get(keys.size() - 1));
|
||||||
return keys;
|
return keys;
|
||||||
@ -162,9 +236,29 @@ public class KeyChainGroup {
|
|||||||
/**
|
/**
|
||||||
* Returns address for a {@link #freshKey(com.google.bitcoin.wallet.KeyChain.KeyPurpose)}
|
* 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);
|
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. */
|
/** Returns the key chain that's used for generation of fresh/current keys. This is always the newest HD chain. */
|
||||||
public DeterministicKeyChain getActiveKeyChain() {
|
public DeterministicKeyChain getActiveKeyChain() {
|
||||||
@ -182,8 +276,8 @@ public class KeyChainGroup {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Sets the lookahead buffer size for ALL deterministic key chains, see
|
* Sets the lookahead buffer size for ALL deterministic key chains as well as for following key chains if any exist,
|
||||||
* {@link com.google.bitcoin.wallet.DeterministicKeyChain#setLookaheadSize(int)}
|
* see {@link com.google.bitcoin.wallet.DeterministicKeyChain#setLookaheadSize(int)}
|
||||||
* for more information.
|
* for more information.
|
||||||
*/
|
*/
|
||||||
public void setLookaheadSize(int lookaheadSize) {
|
public void setLookaheadSize(int lookaheadSize) {
|
||||||
@ -191,6 +285,9 @@ public class KeyChainGroup {
|
|||||||
for (DeterministicKeyChain chain : chains) {
|
for (DeterministicKeyChain chain : chains) {
|
||||||
chain.setLookaheadSize(lookaheadSize);
|
chain.setLookaheadSize(lookaheadSize);
|
||||||
}
|
}
|
||||||
|
for (DeterministicKeyChain chain : followingKeychains.values()) {
|
||||||
|
chain.setLookaheadSize(lookaheadSize);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -331,6 +428,21 @@ public class KeyChainGroup {
|
|||||||
return basic.removeKey(key);
|
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
|
* 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}.
|
* {@link com.google.bitcoin.crypto.KeyCrypterScrypt}.
|
||||||
@ -451,29 +563,35 @@ public class KeyChainGroup {
|
|||||||
else
|
else
|
||||||
result = Lists.newArrayList();
|
result = Lists.newArrayList();
|
||||||
for (DeterministicKeyChain chain : chains) {
|
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();
|
List<Protos.Key> protos = chain.serializeToProtobuf();
|
||||||
result.addAll(protos);
|
result.addAll(protos);
|
||||||
}
|
}
|
||||||
return result;
|
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);
|
BasicKeyChain basicKeyChain = BasicKeyChain.fromProtobufUnencrypted(keys);
|
||||||
List<DeterministicKeyChain> chains = DeterministicKeyChain.fromProtobuf(keys, null);
|
List<DeterministicKeyChain> chains = DeterministicKeyChain.fromProtobuf(keys, null);
|
||||||
EnumMap<KeyChain.KeyPurpose, DeterministicKey> currentKeys = null;
|
EnumMap<KeyChain.KeyPurpose, DeterministicKey> currentKeys = null;
|
||||||
if (!chains.isEmpty())
|
if (!chains.isEmpty())
|
||||||
currentKeys = createCurrentKeysMap(chains);
|
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);
|
checkNotNull(crypter);
|
||||||
BasicKeyChain basicKeyChain = BasicKeyChain.fromProtobufEncrypted(keys, crypter);
|
BasicKeyChain basicKeyChain = BasicKeyChain.fromProtobufEncrypted(keys, crypter);
|
||||||
List<DeterministicKeyChain> chains = DeterministicKeyChain.fromProtobuf(keys, crypter);
|
List<DeterministicKeyChain> chains = DeterministicKeyChain.fromProtobuf(keys, crypter);
|
||||||
EnumMap<KeyChain.KeyPurpose, DeterministicKey> currentKeys = null;
|
EnumMap<KeyChain.KeyPurpose, DeterministicKey> currentKeys = null;
|
||||||
if (!chains.isEmpty())
|
if (!chains.isEmpty())
|
||||||
currentKeys = createCurrentKeysMap(chains);
|
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;
|
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();
|
final StringBuilder builder = new StringBuilder();
|
||||||
if (basic != null) {
|
if (basic != null) {
|
||||||
for (ECKey key : basic.getKeys())
|
for (ECKey key : basic.getKeys())
|
||||||
formatKeyWithAddress(params, includePrivateKeys, key, builder);
|
formatKeyWithAddress(includePrivateKeys, key, builder);
|
||||||
}
|
}
|
||||||
for (DeterministicKeyChain chain : chains) {
|
for (DeterministicKeyChain chain : chains) {
|
||||||
DeterministicSeed seed = chain.getSeed();
|
DeterministicSeed seed = chain.getSeed();
|
||||||
@ -596,18 +731,15 @@ public class KeyChainGroup {
|
|||||||
builder.append(String.format("Key to watch: %s%n%n", watchingKey.serializePubB58()));
|
builder.append(String.format("Key to watch: %s%n%n", watchingKey.serializePubB58()));
|
||||||
}
|
}
|
||||||
for (ECKey key : chain.getKeys())
|
for (ECKey key : chain.getKeys())
|
||||||
formatKeyWithAddress(params, includePrivateKeys, key, builder);
|
formatKeyWithAddress(includePrivateKeys, key, builder);
|
||||||
}
|
}
|
||||||
return builder.toString();
|
return builder.toString();
|
||||||
}
|
}
|
||||||
|
|
||||||
private void formatKeyWithAddress(@Nullable NetworkParameters params, boolean includePrivateKeys,
|
private void formatKeyWithAddress(boolean includePrivateKeys, ECKey key, StringBuilder builder) {
|
||||||
ECKey key, StringBuilder builder) {
|
|
||||||
if (params != null) {
|
|
||||||
final Address address = key.toAddress(params);
|
final Address address = key.toAddress(params);
|
||||||
builder.append(" addr:");
|
builder.append(" addr:");
|
||||||
builder.append(address.toString());
|
builder.append(address.toString());
|
||||||
}
|
|
||||||
builder.append(" hash160:");
|
builder.append(" hash160:");
|
||||||
builder.append(Utils.HEX.encode(key.getPubKeyHash()));
|
builder.append(Utils.HEX.encode(key.getPubKeyHash()));
|
||||||
builder.append(" ");
|
builder.append(" ");
|
||||||
|
@ -1245,6 +1245,30 @@ public final class Protos {
|
|||||||
* <code>optional uint32 lookahead_size = 4;</code>
|
* <code>optional uint32 lookahead_size = 4;</code>
|
||||||
*/
|
*/
|
||||||
int getLookaheadSize();
|
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}
|
* Protobuf type {@code wallet.DeterministicKey}
|
||||||
@ -1338,6 +1362,11 @@ public final class Protos {
|
|||||||
lookaheadSize_ = input.readUInt32();
|
lookaheadSize_ = input.readUInt32();
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
|
case 40: {
|
||||||
|
bitField0_ |= 0x00000008;
|
||||||
|
isFollowing_ = input.readBool();
|
||||||
|
break;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
} catch (com.google.protobuf.InvalidProtocolBufferException e) {
|
} catch (com.google.protobuf.InvalidProtocolBufferException e) {
|
||||||
@ -1495,11 +1524,42 @@ public final class Protos {
|
|||||||
return lookaheadSize_;
|
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() {
|
private void initFields() {
|
||||||
chainCode_ = com.google.protobuf.ByteString.EMPTY;
|
chainCode_ = com.google.protobuf.ByteString.EMPTY;
|
||||||
path_ = java.util.Collections.emptyList();
|
path_ = java.util.Collections.emptyList();
|
||||||
issuedSubkeys_ = 0;
|
issuedSubkeys_ = 0;
|
||||||
lookaheadSize_ = 0;
|
lookaheadSize_ = 0;
|
||||||
|
isFollowing_ = false;
|
||||||
}
|
}
|
||||||
private byte memoizedIsInitialized = -1;
|
private byte memoizedIsInitialized = -1;
|
||||||
public final boolean isInitialized() {
|
public final boolean isInitialized() {
|
||||||
@ -1529,6 +1589,9 @@ public final class Protos {
|
|||||||
if (((bitField0_ & 0x00000004) == 0x00000004)) {
|
if (((bitField0_ & 0x00000004) == 0x00000004)) {
|
||||||
output.writeUInt32(4, lookaheadSize_);
|
output.writeUInt32(4, lookaheadSize_);
|
||||||
}
|
}
|
||||||
|
if (((bitField0_ & 0x00000008) == 0x00000008)) {
|
||||||
|
output.writeBool(5, isFollowing_);
|
||||||
|
}
|
||||||
getUnknownFields().writeTo(output);
|
getUnknownFields().writeTo(output);
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -1559,6 +1622,10 @@ public final class Protos {
|
|||||||
size += com.google.protobuf.CodedOutputStream
|
size += com.google.protobuf.CodedOutputStream
|
||||||
.computeUInt32Size(4, lookaheadSize_);
|
.computeUInt32Size(4, lookaheadSize_);
|
||||||
}
|
}
|
||||||
|
if (((bitField0_ & 0x00000008) == 0x00000008)) {
|
||||||
|
size += com.google.protobuf.CodedOutputStream
|
||||||
|
.computeBoolSize(5, isFollowing_);
|
||||||
|
}
|
||||||
size += getUnknownFields().getSerializedSize();
|
size += getUnknownFields().getSerializedSize();
|
||||||
memoizedSerializedSize = size;
|
memoizedSerializedSize = size;
|
||||||
return size;
|
return size;
|
||||||
@ -1688,6 +1755,8 @@ public final class Protos {
|
|||||||
bitField0_ = (bitField0_ & ~0x00000004);
|
bitField0_ = (bitField0_ & ~0x00000004);
|
||||||
lookaheadSize_ = 0;
|
lookaheadSize_ = 0;
|
||||||
bitField0_ = (bitField0_ & ~0x00000008);
|
bitField0_ = (bitField0_ & ~0x00000008);
|
||||||
|
isFollowing_ = false;
|
||||||
|
bitField0_ = (bitField0_ & ~0x00000010);
|
||||||
return this;
|
return this;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -1733,6 +1802,10 @@ public final class Protos {
|
|||||||
to_bitField0_ |= 0x00000004;
|
to_bitField0_ |= 0x00000004;
|
||||||
}
|
}
|
||||||
result.lookaheadSize_ = lookaheadSize_;
|
result.lookaheadSize_ = lookaheadSize_;
|
||||||
|
if (((from_bitField0_ & 0x00000010) == 0x00000010)) {
|
||||||
|
to_bitField0_ |= 0x00000008;
|
||||||
|
}
|
||||||
|
result.isFollowing_ = isFollowing_;
|
||||||
result.bitField0_ = to_bitField0_;
|
result.bitField0_ = to_bitField0_;
|
||||||
onBuilt();
|
onBuilt();
|
||||||
return result;
|
return result;
|
||||||
@ -1768,6 +1841,9 @@ public final class Protos {
|
|||||||
if (other.hasLookaheadSize()) {
|
if (other.hasLookaheadSize()) {
|
||||||
setLookaheadSize(other.getLookaheadSize());
|
setLookaheadSize(other.getLookaheadSize());
|
||||||
}
|
}
|
||||||
|
if (other.hasIsFollowing()) {
|
||||||
|
setIsFollowing(other.getIsFollowing());
|
||||||
|
}
|
||||||
this.mergeUnknownFields(other.getUnknownFields());
|
this.mergeUnknownFields(other.getUnknownFields());
|
||||||
return this;
|
return this;
|
||||||
}
|
}
|
||||||
@ -2058,6 +2134,67 @@ public final class Protos {
|
|||||||
return this;
|
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)
|
// @@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" +
|
"\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" +
|
"ces\030\003 \002(\004\"M\n\rEncryptedData\022\035\n\025initialisa" +
|
||||||
"tion_vector\030\001 \002(\014\022\035\n\025encrypted_private_k" +
|
"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" +
|
"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" +
|
"\030\003 \001(\r\022\026\n\016lookahead_size\030\004 \001(\r\022\023\n\013isFoll" +
|
||||||
"\n\004type\030\001 \002(\0162\020.wallet.Key.Type\022\024\n\014secret" +
|
"owing\030\005 \001(\010\"\302\002\n\003Key\022\036\n\004type\030\001 \002(\0162\020.wall" +
|
||||||
"_bytes\030\002 \001(\014\022-\n\016encrypted_data\030\006 \001(\0132\025.w" +
|
"et.Key.Type\022\024\n\014secret_bytes\030\002 \001(\014\022-\n\016enc" +
|
||||||
"allet.EncryptedData\022\022\n\npublic_key\030\003 \001(\014\022",
|
"rypted_data\030\006 \001(\0132\025.wallet.EncryptedData",
|
||||||
"\r\n\005label\030\004 \001(\t\022\032\n\022creation_timestamp\030\005 \001" +
|
"\022\022\n\npublic_key\030\003 \001(\014\022\r\n\005label\030\004 \001(\t\022\032\n\022c" +
|
||||||
"(\003\0223\n\021deterministic_key\030\007 \001(\0132\030.wallet.D" +
|
"reation_timestamp\030\005 \001(\003\0223\n\021deterministic" +
|
||||||
"eterministicKey\"b\n\004Type\022\014\n\010ORIGINAL\020\001\022\030\n" +
|
"_key\030\007 \001(\0132\030.wallet.DeterministicKey\"b\n\004" +
|
||||||
"\024ENCRYPTED_SCRYPT_AES\020\002\022\033\n\027DETERMINISTIC" +
|
"Type\022\014\n\010ORIGINAL\020\001\022\030\n\024ENCRYPTED_SCRYPT_A" +
|
||||||
"_ROOT_SEED\020\003\022\025\n\021DETERMINISTIC_KEY\020\004\"5\n\006S" +
|
"ES\020\002\022\033\n\027DETERMINISTIC_ROOT_SEED\020\003\022\025\n\021DET" +
|
||||||
"cript\022\017\n\007program\030\001 \002(\014\022\032\n\022creation_times" +
|
"ERMINISTIC_KEY\020\004\"5\n\006Script\022\017\n\007program\030\001 " +
|
||||||
"tamp\030\002 \002(\003\"\222\001\n\020TransactionInput\022\"\n\032trans" +
|
"\002(\014\022\032\n\022creation_timestamp\030\002 \002(\003\"\222\001\n\020Tran" +
|
||||||
"action_out_point_hash\030\001 \002(\014\022#\n\033transacti" +
|
"sactionInput\022\"\n\032transaction_out_point_ha" +
|
||||||
"on_out_point_index\030\002 \002(\r\022\024\n\014script_bytes" +
|
"sh\030\001 \002(\014\022#\n\033transaction_out_point_index\030" +
|
||||||
"\030\003 \002(\014\022\020\n\010sequence\030\004 \001(\r\022\r\n\005value\030\005 \001(\003\"",
|
"\002 \002(\r\022\024\n\014script_bytes\030\003 \002(\014\022\020\n\010sequence\030",
|
||||||
"\177\n\021TransactionOutput\022\r\n\005value\030\001 \002(\003\022\024\n\014s" +
|
"\004 \001(\r\022\r\n\005value\030\005 \001(\003\"\177\n\021TransactionOutpu" +
|
||||||
"cript_bytes\030\002 \002(\014\022!\n\031spent_by_transactio" +
|
"t\022\r\n\005value\030\001 \002(\003\022\024\n\014script_bytes\030\002 \002(\014\022!" +
|
||||||
"n_hash\030\003 \001(\014\022\"\n\032spent_by_transaction_ind" +
|
"\n\031spent_by_transaction_hash\030\003 \001(\014\022\"\n\032spe" +
|
||||||
"ex\030\004 \001(\005\"\234\003\n\025TransactionConfidence\0220\n\004ty" +
|
"nt_by_transaction_index\030\004 \001(\005\"\234\003\n\025Transa" +
|
||||||
"pe\030\001 \001(\0162\".wallet.TransactionConfidence." +
|
"ctionConfidence\0220\n\004type\030\001 \001(\0162\".wallet.T" +
|
||||||
"Type\022\032\n\022appeared_at_height\030\002 \001(\005\022\036\n\026over" +
|
"ransactionConfidence.Type\022\032\n\022appeared_at" +
|
||||||
"riding_transaction\030\003 \001(\014\022\r\n\005depth\030\004 \001(\005\022" +
|
"_height\030\002 \001(\005\022\036\n\026overriding_transaction\030" +
|
||||||
"\021\n\twork_done\030\005 \001(\003\022)\n\014broadcast_by\030\006 \003(\013" +
|
"\003 \001(\014\022\r\n\005depth\030\004 \001(\005\022\021\n\twork_done\030\005 \001(\003\022" +
|
||||||
"2\023.wallet.PeerAddress\0224\n\006source\030\007 \001(\0162$." +
|
")\n\014broadcast_by\030\006 \003(\0132\023.wallet.PeerAddre" +
|
||||||
"wallet.TransactionConfidence.Source\"O\n\004T",
|
"ss\0224\n\006source\030\007 \001(\0162$.wallet.TransactionC",
|
||||||
"ype\022\013\n\007UNKNOWN\020\000\022\014\n\010BUILDING\020\001\022\013\n\007PENDIN" +
|
"onfidence.Source\"O\n\004Type\022\013\n\007UNKNOWN\020\000\022\014\n" +
|
||||||
"G\020\002\022\025\n\021NOT_IN_BEST_CHAIN\020\003\022\010\n\004DEAD\020\004\"A\n\006" +
|
"\010BUILDING\020\001\022\013\n\007PENDING\020\002\022\025\n\021NOT_IN_BEST_" +
|
||||||
"Source\022\022\n\016SOURCE_UNKNOWN\020\000\022\022\n\016SOURCE_NET" +
|
"CHAIN\020\003\022\010\n\004DEAD\020\004\"A\n\006Source\022\022\n\016SOURCE_UN" +
|
||||||
"WORK\020\001\022\017\n\013SOURCE_SELF\020\002\"\236\004\n\013Transaction\022" +
|
"KNOWN\020\000\022\022\n\016SOURCE_NETWORK\020\001\022\017\n\013SOURCE_SE" +
|
||||||
"\017\n\007version\030\001 \002(\005\022\014\n\004hash\030\002 \002(\014\022&\n\004pool\030\003" +
|
"LF\020\002\"\236\004\n\013Transaction\022\017\n\007version\030\001 \002(\005\022\014\n" +
|
||||||
" \001(\0162\030.wallet.Transaction.Pool\022\021\n\tlock_t" +
|
"\004hash\030\002 \002(\014\022&\n\004pool\030\003 \001(\0162\030.wallet.Trans" +
|
||||||
"ime\030\004 \001(\r\022\022\n\nupdated_at\030\005 \001(\003\0223\n\021transac" +
|
"action.Pool\022\021\n\tlock_time\030\004 \001(\r\022\022\n\nupdate" +
|
||||||
"tion_input\030\006 \003(\0132\030.wallet.TransactionInp" +
|
"d_at\030\005 \001(\003\0223\n\021transaction_input\030\006 \003(\0132\030." +
|
||||||
"ut\0225\n\022transaction_output\030\007 \003(\0132\031.wallet." +
|
"wallet.TransactionInput\0225\n\022transaction_o" +
|
||||||
"TransactionOutput\022\022\n\nblock_hash\030\010 \003(\014\022 \n",
|
"utput\030\007 \003(\0132\031.wallet.TransactionOutput\022\022",
|
||||||
"\030block_relativity_offsets\030\013 \003(\005\0221\n\nconfi" +
|
"\n\nblock_hash\030\010 \003(\014\022 \n\030block_relativity_o" +
|
||||||
"dence\030\t \001(\0132\035.wallet.TransactionConfiden" +
|
"ffsets\030\013 \003(\005\0221\n\nconfidence\030\t \001(\0132\035.walle" +
|
||||||
"ce\0225\n\007purpose\030\n \001(\0162\033.wallet.Transaction" +
|
"t.TransactionConfidence\0225\n\007purpose\030\n \001(\016" +
|
||||||
".Purpose:\007UNKNOWN\"Y\n\004Pool\022\013\n\007UNSPENT\020\004\022\t" +
|
"2\033.wallet.Transaction.Purpose:\007UNKNOWN\"Y" +
|
||||||
"\n\005SPENT\020\005\022\014\n\010INACTIVE\020\002\022\010\n\004DEAD\020\n\022\013\n\007PEN" +
|
"\n\004Pool\022\013\n\007UNSPENT\020\004\022\t\n\005SPENT\020\005\022\014\n\010INACTI" +
|
||||||
"DING\020\020\022\024\n\020PENDING_INACTIVE\020\022\":\n\007Purpose\022" +
|
"VE\020\002\022\010\n\004DEAD\020\n\022\013\n\007PENDING\020\020\022\024\n\020PENDING_I" +
|
||||||
"\013\n\007UNKNOWN\020\000\022\020\n\014USER_PAYMENT\020\001\022\020\n\014KEY_RO" +
|
"NACTIVE\020\022\":\n\007Purpose\022\013\n\007UNKNOWN\020\000\022\020\n\014USE" +
|
||||||
"TATION\020\002\"N\n\020ScryptParameters\022\014\n\004salt\030\001 \002" +
|
"R_PAYMENT\020\001\022\020\n\014KEY_ROTATION\020\002\"N\n\020ScryptP" +
|
||||||
"(\014\022\020\n\001n\030\002 \001(\003:\00516384\022\014\n\001r\030\003 \001(\005:\0018\022\014\n\001p\030" +
|
"arameters\022\014\n\004salt\030\001 \002(\014\022\020\n\001n\030\002 \001(\003:\0051638" +
|
||||||
"\004 \001(\005:\0011\"8\n\tExtension\022\n\n\002id\030\001 \002(\t\022\014\n\004dat",
|
"4\022\014\n\001r\030\003 \001(\005:\0018\022\014\n\001p\030\004 \001(\005:\0011\"8\n\tExtensi",
|
||||||
"a\030\002 \002(\014\022\021\n\tmandatory\030\003 \002(\010\" \n\003Tag\022\013\n\003tag" +
|
"on\022\n\n\002id\030\001 \002(\t\022\014\n\004data\030\002 \002(\014\022\021\n\tmandator" +
|
||||||
"\030\001 \002(\t\022\014\n\004data\030\002 \002(\014\"\261\004\n\006Wallet\022\032\n\022netwo" +
|
"y\030\003 \002(\010\" \n\003Tag\022\013\n\003tag\030\001 \002(\t\022\014\n\004data\030\002 \002(" +
|
||||||
"rk_identifier\030\001 \002(\t\022\034\n\024last_seen_block_h" +
|
"\014\"\261\004\n\006Wallet\022\032\n\022network_identifier\030\001 \002(\t" +
|
||||||
"ash\030\002 \001(\014\022\036\n\026last_seen_block_height\030\014 \001(" +
|
"\022\034\n\024last_seen_block_hash\030\002 \001(\014\022\036\n\026last_s" +
|
||||||
"\r\022!\n\031last_seen_block_time_secs\030\016 \001(\003\022\030\n\003" +
|
"een_block_height\030\014 \001(\r\022!\n\031last_seen_bloc" +
|
||||||
"key\030\003 \003(\0132\013.wallet.Key\022(\n\013transaction\030\004 " +
|
"k_time_secs\030\016 \001(\003\022\030\n\003key\030\003 \003(\0132\013.wallet." +
|
||||||
"\003(\0132\023.wallet.Transaction\022&\n\016watched_scri" +
|
"Key\022(\n\013transaction\030\004 \003(\0132\023.wallet.Transa" +
|
||||||
"pt\030\017 \003(\0132\016.wallet.Script\022C\n\017encryption_t" +
|
"ction\022&\n\016watched_script\030\017 \003(\0132\016.wallet.S" +
|
||||||
"ype\030\005 \001(\0162\035.wallet.Wallet.EncryptionType" +
|
"cript\022C\n\017encryption_type\030\005 \001(\0162\035.wallet." +
|
||||||
":\013UNENCRYPTED\0227\n\025encryption_parameters\030\006",
|
"Wallet.EncryptionType:\013UNENCRYPTED\0227\n\025en",
|
||||||
" \001(\0132\030.wallet.ScryptParameters\022\022\n\007versio" +
|
"cryption_parameters\030\006 \001(\0132\030.wallet.Scryp" +
|
||||||
"n\030\007 \001(\005:\0011\022$\n\textension\030\n \003(\0132\021.wallet.E" +
|
"tParameters\022\022\n\007version\030\007 \001(\005:\0011\022$\n\texten" +
|
||||||
"xtension\022\023\n\013description\030\013 \001(\t\022\031\n\021key_rot" +
|
"sion\030\n \003(\0132\021.wallet.Extension\022\023\n\013descrip" +
|
||||||
"ation_time\030\r \001(\004\022\031\n\004tags\030\020 \003(\0132\013.wallet." +
|
"tion\030\013 \001(\t\022\031\n\021key_rotation_time\030\r \001(\004\022\031\n" +
|
||||||
"Tag\";\n\016EncryptionType\022\017\n\013UNENCRYPTED\020\001\022\030" +
|
"\004tags\030\020 \003(\0132\013.wallet.Tag\";\n\016EncryptionTy" +
|
||||||
"\n\024ENCRYPTED_SCRYPT_AES\020\002B\035\n\023org.bitcoinj" +
|
"pe\022\017\n\013UNENCRYPTED\020\001\022\030\n\024ENCRYPTED_SCRYPT_" +
|
||||||
".walletB\006Protos"
|
"AES\020\002B\035\n\023org.bitcoinj.walletB\006Protos"
|
||||||
};
|
};
|
||||||
com.google.protobuf.Descriptors.FileDescriptor.InternalDescriptorAssigner assigner =
|
com.google.protobuf.Descriptors.FileDescriptor.InternalDescriptorAssigner assigner =
|
||||||
new com.google.protobuf.Descriptors.FileDescriptor.InternalDescriptorAssigner() {
|
new com.google.protobuf.Descriptors.FileDescriptor.InternalDescriptorAssigner() {
|
||||||
@ -16107,7 +16244,7 @@ public final class Protos {
|
|||||||
internal_static_wallet_DeterministicKey_fieldAccessorTable = new
|
internal_static_wallet_DeterministicKey_fieldAccessorTable = new
|
||||||
com.google.protobuf.GeneratedMessage.FieldAccessorTable(
|
com.google.protobuf.GeneratedMessage.FieldAccessorTable(
|
||||||
internal_static_wallet_DeterministicKey_descriptor,
|
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 =
|
internal_static_wallet_Key_descriptor =
|
||||||
getDescriptor().getMessageTypes().get(3);
|
getDescriptor().getMessageTypes().get(3);
|
||||||
internal_static_wallet_Key_fieldAccessorTable = new
|
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.MainNetParams;
|
||||||
import com.google.bitcoin.params.TestNet3Params;
|
import com.google.bitcoin.params.TestNet3Params;
|
||||||
|
import com.google.bitcoin.script.Script;
|
||||||
import com.google.bitcoin.script.ScriptBuilder;
|
import com.google.bitcoin.script.ScriptBuilder;
|
||||||
import org.junit.Test;
|
import org.junit.Test;
|
||||||
|
|
||||||
import java.util.Arrays;
|
import java.util.Arrays;
|
||||||
|
import java.util.List;
|
||||||
|
|
||||||
import static com.google.bitcoin.core.Utils.HEX;
|
import static com.google.bitcoin.core.Utils.HEX;
|
||||||
import static org.junit.Assert.*;
|
import static org.junit.Assert.*;
|
||||||
@ -120,4 +122,20 @@ public class AddressTest {
|
|||||||
Address c = Address.fromP2SHScript(mainParams, ScriptBuilder.createP2SHOutputScript(hex));
|
Address c = Address.fromP2SHScript(mainParams, ScriptBuilder.createP2SHOutputScript(hex));
|
||||||
assertEquals("35b9vsyH1KoFT5a5KtrKusaCcPLkiSo1tU", c.toString());
|
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;
|
package com.google.bitcoin.wallet;
|
||||||
|
|
||||||
import com.google.bitcoin.core.BloomFilter;
|
import com.google.bitcoin.core.*;
|
||||||
import com.google.bitcoin.core.ECKey;
|
import com.google.bitcoin.crypto.*;
|
||||||
import com.google.bitcoin.core.Sha256Hash;
|
import com.google.bitcoin.params.MainNetParams;
|
||||||
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.utils.BriefLogFormatter;
|
import com.google.bitcoin.utils.BriefLogFormatter;
|
||||||
import com.google.bitcoin.utils.Threading;
|
import com.google.bitcoin.utils.Threading;
|
||||||
import com.google.common.collect.ImmutableList;
|
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.
|
// Number of initial keys in this tests HD wallet, including interior keys.
|
||||||
private static final int INITIAL_KEYS = 4;
|
private static final int INITIAL_KEYS = 4;
|
||||||
private static final int LOOKAHEAD_SIZE = 5;
|
private static final int LOOKAHEAD_SIZE = 5;
|
||||||
|
private static final NetworkParameters params = MainNetParams.get();
|
||||||
|
private static final String XPUB = "xpub68KFnj3bqUx1s7mHejLDBPywCAKdJEu1b49uniEEn2WSbHmZ7xbLqFTjJbtx1LUcAt1DwhoqWHmo2s5WMJp6wi38CiF2hYD49qVViKVvAoi";
|
||||||
private KeyChainGroup group;
|
private KeyChainGroup group;
|
||||||
|
|
||||||
@Before
|
@Before
|
||||||
public void setup() {
|
public void setup() {
|
||||||
BriefLogFormatter.init();
|
BriefLogFormatter.init();
|
||||||
Utils.setMockClock();
|
Utils.setMockClock();
|
||||||
group = new KeyChainGroup();
|
group = new KeyChainGroup(params);
|
||||||
group.setLookaheadSize(LOOKAHEAD_SIZE); // Don't want slow tests.
|
group.setLookaheadSize(LOOKAHEAD_SIZE); // Don't want slow tests.
|
||||||
group.getActiveKeyChain(); // Force create a chain.
|
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
|
@Test
|
||||||
public void freshCurrentKeys() throws Exception {
|
public void freshCurrentKeys() throws Exception {
|
||||||
assertEquals(INITIAL_KEYS, group.numKeys());
|
assertEquals(INITIAL_KEYS, group.numKeys());
|
||||||
@ -82,6 +90,23 @@ public class KeyChainGroupTest {
|
|||||||
assertEquals(c2, c3);
|
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
|
@Test
|
||||||
public void imports() throws Exception {
|
public void imports() throws Exception {
|
||||||
ECKey key1 = new ECKey();
|
ECKey key1 = new ECKey();
|
||||||
@ -119,6 +144,36 @@ public class KeyChainGroupTest {
|
|||||||
assertNull(group.findKeyFromPubHash(d.getPubKeyHash()));
|
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.
|
// Check encryption with and without a basic keychain.
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
@ -206,7 +261,7 @@ public class KeyChainGroupTest {
|
|||||||
|
|
||||||
@Test
|
@Test
|
||||||
public void encryptionWhilstEmpty() throws Exception {
|
public void encryptionWhilstEmpty() throws Exception {
|
||||||
group = new KeyChainGroup();
|
group = new KeyChainGroup(params);
|
||||||
group.setLookaheadSize(5);
|
group.setLookaheadSize(5);
|
||||||
KeyCrypterScrypt scrypt = new KeyCrypterScrypt(2);
|
KeyCrypterScrypt scrypt = new KeyCrypterScrypt(2);
|
||||||
final KeyParameter aesKey = scrypt.deriveKey("password");
|
final KeyParameter aesKey = scrypt.deriveKey("password");
|
||||||
@ -286,7 +341,7 @@ public class KeyChainGroupTest {
|
|||||||
@Test
|
@Test
|
||||||
public void serialization() throws Exception {
|
public void serialization() throws Exception {
|
||||||
assertEquals(INITIAL_KEYS + 1 /* for the seed */, group.serializeToProtobuf().size());
|
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);
|
group.freshKey(KeyChain.KeyPurpose.RECEIVE_FUNDS);
|
||||||
DeterministicKey key1 = group.freshKey(KeyChain.KeyPurpose.RECEIVE_FUNDS);
|
DeterministicKey key1 = group.freshKey(KeyChain.KeyPurpose.RECEIVE_FUNDS);
|
||||||
DeterministicKey key2 = group.freshKey(KeyChain.KeyPurpose.CHANGE);
|
DeterministicKey key2 = group.freshKey(KeyChain.KeyPurpose.CHANGE);
|
||||||
@ -296,13 +351,13 @@ public class KeyChainGroupTest {
|
|||||||
List<Protos.Key> protoKeys2 = group.serializeToProtobuf();
|
List<Protos.Key> protoKeys2 = group.serializeToProtobuf();
|
||||||
assertEquals(INITIAL_KEYS + ((LOOKAHEAD_SIZE + 1) * 2) + 1 /* for the seed */ + 2, protoKeys2.size());
|
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());
|
assertEquals(INITIAL_KEYS + ((LOOKAHEAD_SIZE + 1) * 2) + 1 /* for the seed */ + 1, protoKeys1.size());
|
||||||
assertTrue(group.hasKey(key1));
|
assertTrue(group.hasKey(key1));
|
||||||
assertTrue(group.hasKey(key2));
|
assertTrue(group.hasKey(key2));
|
||||||
assertEquals(key2, group.currentKey(KeyChain.KeyPurpose.CHANGE));
|
assertEquals(key2, group.currentKey(KeyChain.KeyPurpose.CHANGE));
|
||||||
assertEquals(key1, group.currentKey(KeyChain.KeyPurpose.RECEIVE_FUNDS));
|
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());
|
assertEquals(INITIAL_KEYS + ((LOOKAHEAD_SIZE + 1) * 2) + 1 /* for the seed */ + 2, protoKeys2.size());
|
||||||
assertTrue(group.hasKey(key1));
|
assertTrue(group.hasKey(key1));
|
||||||
assertTrue(group.hasKey(key2));
|
assertTrue(group.hasKey(key2));
|
||||||
@ -311,7 +366,7 @@ public class KeyChainGroupTest {
|
|||||||
final KeyParameter aesKey = scrypt.deriveKey("password");
|
final KeyParameter aesKey = scrypt.deriveKey("password");
|
||||||
group.encrypt(scrypt, aesKey);
|
group.encrypt(scrypt, aesKey);
|
||||||
List<Protos.Key> protoKeys3 = group.serializeToProtobuf();
|
List<Protos.Key> protoKeys3 = group.serializeToProtobuf();
|
||||||
group = KeyChainGroup.fromProtobufEncrypted(protoKeys3, scrypt);
|
group = KeyChainGroup.fromProtobufEncrypted(params, protoKeys3, scrypt);
|
||||||
assertTrue(group.isEncrypted());
|
assertTrue(group.isEncrypted());
|
||||||
assertTrue(group.checkPassword("password"));
|
assertTrue(group.checkPassword("password"));
|
||||||
group.decrypt(aesKey);
|
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.
|
// 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
|
@Test
|
||||||
public void constructFromSeed() throws Exception {
|
public void constructFromSeed() throws Exception {
|
||||||
ECKey key1 = group.freshKey(KeyChain.KeyPurpose.RECEIVE_FUNDS);
|
ECKey key1 = group.freshKey(KeyChain.KeyPurpose.RECEIVE_FUNDS);
|
||||||
final DeterministicSeed seed = checkNotNull(group.getActiveKeyChain().getSeed());
|
final DeterministicSeed seed = checkNotNull(group.getActiveKeyChain().getSeed());
|
||||||
KeyChainGroup group2 = new KeyChainGroup(seed);
|
KeyChainGroup group2 = new KeyChainGroup(params, seed);
|
||||||
group2.setLookaheadSize(5);
|
group2.setLookaheadSize(5);
|
||||||
ECKey key2 = group2.freshKey(KeyChain.KeyPurpose.RECEIVE_FUNDS);
|
ECKey key2 = group2.freshKey(KeyChain.KeyPurpose.RECEIVE_FUNDS);
|
||||||
assertEquals(key1, key2);
|
assertEquals(key1, key2);
|
||||||
@ -332,7 +429,7 @@ public class KeyChainGroupTest {
|
|||||||
@Test(expected = DeterministicUpgradeRequiredException.class)
|
@Test(expected = DeterministicUpgradeRequiredException.class)
|
||||||
public void deterministicUpgradeRequired() throws Exception {
|
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.
|
// 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());
|
group.importKeys(new ECKey(), new ECKey());
|
||||||
assertTrue(group.isDeterministicUpgradeRequired());
|
assertTrue(group.isDeterministicUpgradeRequired());
|
||||||
group.freshKey(KeyChain.KeyPurpose.RECEIVE_FUNDS); // throws
|
group.freshKey(KeyChain.KeyPurpose.RECEIVE_FUNDS); // throws
|
||||||
@ -342,7 +439,7 @@ public class KeyChainGroupTest {
|
|||||||
public void deterministicUpgradeUnencrypted() throws Exception {
|
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
|
// 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.
|
// 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.
|
group.setLookaheadSize(LOOKAHEAD_SIZE); // Don't want slow tests.
|
||||||
ECKey key1 = new ECKey();
|
ECKey key1 = new ECKey();
|
||||||
Utils.rollMockClock(86400);
|
Utils.rollMockClock(86400);
|
||||||
@ -356,7 +453,7 @@ public class KeyChainGroupTest {
|
|||||||
DeterministicSeed seed1 = group.getActiveKeyChain().getSeed();
|
DeterministicSeed seed1 = group.getActiveKeyChain().getSeed();
|
||||||
assertNotNull(seed1);
|
assertNotNull(seed1);
|
||||||
|
|
||||||
group = KeyChainGroup.fromProtobufUnencrypted(protobufs);
|
group = KeyChainGroup.fromProtobufUnencrypted(params, protobufs);
|
||||||
group.upgradeToDeterministic(0, null); // Should give same result as last time.
|
group.upgradeToDeterministic(0, null); // Should give same result as last time.
|
||||||
DeterministicKey dkey2 = group.freshKey(KeyChain.KeyPurpose.RECEIVE_FUNDS);
|
DeterministicKey dkey2 = group.freshKey(KeyChain.KeyPurpose.RECEIVE_FUNDS);
|
||||||
DeterministicSeed seed2 = group.getActiveKeyChain().getSeed();
|
DeterministicSeed seed2 = group.getActiveKeyChain().getSeed();
|
||||||
@ -370,7 +467,7 @@ public class KeyChainGroupTest {
|
|||||||
|
|
||||||
@Test
|
@Test
|
||||||
public void deterministicUpgradeRotating() throws Exception {
|
public void deterministicUpgradeRotating() throws Exception {
|
||||||
group = new KeyChainGroup();
|
group = new KeyChainGroup(params);
|
||||||
group.setLookaheadSize(LOOKAHEAD_SIZE); // Don't want slow tests.
|
group.setLookaheadSize(LOOKAHEAD_SIZE); // Don't want slow tests.
|
||||||
long now = Utils.currentTimeSeconds();
|
long now = Utils.currentTimeSeconds();
|
||||||
ECKey key1 = new ECKey();
|
ECKey key1 = new ECKey();
|
||||||
@ -389,7 +486,7 @@ public class KeyChainGroupTest {
|
|||||||
|
|
||||||
@Test
|
@Test
|
||||||
public void deterministicUpgradeEncrypted() throws Exception {
|
public void deterministicUpgradeEncrypted() throws Exception {
|
||||||
group = new KeyChainGroup();
|
group = new KeyChainGroup(params);
|
||||||
final ECKey key = new ECKey();
|
final ECKey key = new ECKey();
|
||||||
group.importKeys(key);
|
group.importKeys(key);
|
||||||
final KeyCrypterScrypt crypter = new KeyCrypterScrypt();
|
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.
|
// If this field is missing it means we're not issuing subkeys of this key to users.
|
||||||
optional uint32 issued_subkeys = 3;
|
optional uint32 issued_subkeys = 3;
|
||||||
optional uint32 lookahead_size = 4;
|
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