mirror of
https://github.com/Qortal/altcoinj.git
synced 2025-01-31 15:22:16 +00:00
Refactor married keychains
* move handling of following keychains into the leading keychain * move multisig threshold into the leading keychain * extract MarriedKeyChain from DeterministicKeyChain
This commit is contained in:
parent
be496b95a3
commit
22f0600afe
@ -1134,4 +1134,18 @@ public class ECKey implements EncryptableItem, Serializable {
|
|||||||
helper.add("isEncrypted", isEncrypted());
|
helper.add("isEncrypted", isEncrypted());
|
||||||
return helper.toString();
|
return helper.toString();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public void formatKeyWithAddress(boolean includePrivateKeys, StringBuilder builder, NetworkParameters params) {
|
||||||
|
final Address address = toAddress(params);
|
||||||
|
builder.append(" addr:");
|
||||||
|
builder.append(address.toString());
|
||||||
|
builder.append(" hash160:");
|
||||||
|
builder.append(Utils.HEX.encode(getPubKeyHash()));
|
||||||
|
builder.append("\n");
|
||||||
|
if (includePrivateKeys) {
|
||||||
|
builder.append(" ");
|
||||||
|
builder.append(toStringWithPrivate());
|
||||||
|
builder.append("\n");
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
@ -293,18 +293,10 @@ public class Wallet extends BaseTaggableObject implements Serializable, BlockCha
|
|||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Returns the number of signatures required to spend from this wallet. For a normal non-married wallet this will
|
* Gets the active keychain via {@link KeyChainGroup#getActiveKeyChain()}
|
||||||
* always be 1. For a married wallet this will be the N from N-of-M CHECKMULTISIG scripts used in this wallet.
|
|
||||||
* This value is either directly specified during the marriage (see {@link #addFollowingAccountKeys(java.util.List, int)})
|
|
||||||
* or, if not specified, calculated implicitly as a simple majority of keys.
|
|
||||||
*/
|
*/
|
||||||
public int getSigsRequiredToSpend() {
|
public DeterministicKeyChain getActiveKeychain() {
|
||||||
lock.lock();
|
return keychain.getActiveKeyChain();
|
||||||
try {
|
|
||||||
return keychain.getSigsRequiredToSpend();
|
|
||||||
} finally {
|
|
||||||
lock.unlock();
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -629,38 +621,18 @@ public class Wallet extends BaseTaggableObject implements Serializable, BlockCha
|
|||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* <p>Alias for <code>addFollowingAccountKeys(followingAccountKeys, (followingAccountKeys.size() + 1) / 2 + 1)</code></p>
|
* Add a pre-configured keychain to the wallet. Useful for setting up a complex keychain,
|
||||||
* <p>Creates married wallet requiring majority of keys to spend (2-of-3, 3-of-5 and so on)</p>
|
* such as for a married wallet. For example:
|
||||||
* <p>IMPORTANT: As of Bitcoin Core 0.9 all multisig transactions which require more than 3 public keys are
|
* <pre>
|
||||||
* non-standard and such spends won't be processed by peers with default settings, essentially making such
|
* MarriedKeyChain chain = MarriedKeyChain.builder()
|
||||||
* transactions almost nonspendable</p>
|
* .random(new SecureRandom())
|
||||||
|
* .followingKeys(followingKeys)
|
||||||
|
* .threshold(2).build();
|
||||||
|
* wallet.addAndActivateHDChain(chain);
|
||||||
|
* </p>
|
||||||
*/
|
*/
|
||||||
public void addFollowingAccountKeys(List<DeterministicKey> followingAccountKeys) {
|
public void addAndActivateHDChain(DeterministicKeyChain chain) {
|
||||||
lock.lock();
|
keychain.addAndActivateHDChain(chain);
|
||||||
try {
|
|
||||||
keychain.addFollowingAccountKeys(followingAccountKeys);
|
|
||||||
} finally {
|
|
||||||
lock.unlock();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Makes given account keys follow the account key of the active keychain. After that you will be able
|
|
||||||
* to get P2SH addresses to receive coins to. Given threshold value specifies how many signatures required to
|
|
||||||
* spend transactions for this married wallet. This value should not exceed total number of keys involved
|
|
||||||
* (one followed key plus number of following keys).</p>
|
|
||||||
* <p>IMPORTANT: As of Bitcoin Core 0.9 all multisig transactions which require more than 3 public keys are
|
|
||||||
* non-standard and such spends won't be processed by peers with default settings, essentially making such
|
|
||||||
* transactions almost nonspendable</p>
|
|
||||||
* This method should be called only once before key rotation, otherwise it will throw an IllegalStateException.
|
|
||||||
*/
|
|
||||||
public void addFollowingAccountKeys(List<DeterministicKey> followingAccountKeys, int threshold) {
|
|
||||||
lock.lock();
|
|
||||||
try {
|
|
||||||
keychain.addFollowingAccountKeys(followingAccountKeys, threshold);
|
|
||||||
} finally {
|
|
||||||
lock.unlock();
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/** See {@link org.bitcoinj.wallet.DeterministicKeyChain#setLookaheadSize(int)} for more info on this. */
|
/** See {@link org.bitcoinj.wallet.DeterministicKeyChain#setLookaheadSize(int)} for more info on this. */
|
||||||
|
@ -448,4 +448,22 @@ public class DeterministicKey extends ECKey {
|
|||||||
helper.add("creationTimeSeconds", creationTimeSeconds);
|
helper.add("creationTimeSeconds", creationTimeSeconds);
|
||||||
return helper.toString();
|
return helper.toString();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void formatKeyWithAddress(boolean includePrivateKeys, StringBuilder builder, NetworkParameters params) {
|
||||||
|
final Address address = toAddress(params);
|
||||||
|
builder.append(" addr:");
|
||||||
|
builder.append(address.toString());
|
||||||
|
builder.append(" hash160:");
|
||||||
|
builder.append(Utils.HEX.encode(getPubKeyHash()));
|
||||||
|
builder.append(" (");
|
||||||
|
builder.append(getPathAsString());
|
||||||
|
builder.append(")");
|
||||||
|
builder.append("\n");
|
||||||
|
if (includePrivateKeys) {
|
||||||
|
builder.append(" ");
|
||||||
|
builder.append(toStringWithPrivate());
|
||||||
|
builder.append("\n");
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
@ -205,8 +205,6 @@ public class WalletProtobufSerializer {
|
|||||||
walletBuilder.addTransactionSigners(protoSigner);
|
walletBuilder.addTransactionSigners(protoSigner);
|
||||||
}
|
}
|
||||||
|
|
||||||
walletBuilder.setSigsRequiredToSpend(wallet.getSigsRequiredToSpend());
|
|
||||||
|
|
||||||
// Populate the wallet version.
|
// Populate the wallet version.
|
||||||
walletBuilder.setVersion(wallet.getVersion());
|
walletBuilder.setVersion(wallet.getVersion());
|
||||||
|
|
||||||
@ -410,16 +408,14 @@ public class WalletProtobufSerializer {
|
|||||||
if (!walletProto.getNetworkIdentifier().equals(params.getId()))
|
if (!walletProto.getNetworkIdentifier().equals(params.getId()))
|
||||||
throw new UnreadableWalletException.WrongNetwork();
|
throw new UnreadableWalletException.WrongNetwork();
|
||||||
|
|
||||||
int sigsRequiredToSpend = walletProto.getSigsRequiredToSpend();
|
|
||||||
|
|
||||||
// Read the scrypt parameters that specify how encryption and decryption is performed.
|
// Read the scrypt parameters that specify how encryption and decryption is performed.
|
||||||
KeyChainGroup chain;
|
KeyChainGroup chain;
|
||||||
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(params, walletProto.getKeyList(), sigsRequiredToSpend, keyCrypter);
|
chain = KeyChainGroup.fromProtobufEncrypted(params, walletProto.getKeyList(), keyCrypter);
|
||||||
} else {
|
} else {
|
||||||
chain = KeyChainGroup.fromProtobufUnencrypted(params, walletProto.getKeyList(), sigsRequiredToSpend);
|
chain = KeyChainGroup.fromProtobufUnencrypted(params, walletProto.getKeyList());
|
||||||
}
|
}
|
||||||
Wallet wallet = factory.create(params, chain);
|
Wallet wallet = factory.create(params, chain);
|
||||||
|
|
||||||
|
@ -18,10 +18,14 @@ package org.bitcoinj.wallet;
|
|||||||
|
|
||||||
import org.bitcoinj.core.BloomFilter;
|
import org.bitcoinj.core.BloomFilter;
|
||||||
import org.bitcoinj.core.ECKey;
|
import org.bitcoinj.core.ECKey;
|
||||||
|
import org.bitcoinj.core.NetworkParameters;
|
||||||
import org.bitcoinj.core.Utils;
|
import org.bitcoinj.core.Utils;
|
||||||
import org.bitcoinj.crypto.*;
|
import org.bitcoinj.crypto.*;
|
||||||
|
import org.bitcoinj.script.Script;
|
||||||
import org.bitcoinj.store.UnreadableWalletException;
|
import org.bitcoinj.store.UnreadableWalletException;
|
||||||
import org.bitcoinj.utils.Threading;
|
import org.bitcoinj.utils.Threading;
|
||||||
|
|
||||||
|
import com.google.common.base.Joiner;
|
||||||
import com.google.common.collect.ImmutableList;
|
import com.google.common.collect.ImmutableList;
|
||||||
import com.google.protobuf.ByteString;
|
import com.google.protobuf.ByteString;
|
||||||
import org.slf4j.Logger;
|
import org.slf4j.Logger;
|
||||||
@ -37,6 +41,7 @@ import java.util.concurrent.Executor;
|
|||||||
import java.util.concurrent.locks.ReentrantLock;
|
import java.util.concurrent.locks.ReentrantLock;
|
||||||
|
|
||||||
import static com.google.common.base.Preconditions.*;
|
import static com.google.common.base.Preconditions.*;
|
||||||
|
import static com.google.common.collect.Lists.newArrayList;
|
||||||
import static com.google.common.collect.Lists.newLinkedList;
|
import static com.google.common.collect.Lists.newLinkedList;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -87,11 +92,12 @@ import static com.google.common.collect.Lists.newLinkedList;
|
|||||||
* But even when you are requesting the 33rd key, you will still be looking 100 keys ahead.
|
* But even when you are requesting the 33rd key, you will still be looking 100 keys ahead.
|
||||||
* </p>
|
* </p>
|
||||||
*/
|
*/
|
||||||
|
@SuppressWarnings("PublicStaticCollectionField")
|
||||||
public class DeterministicKeyChain implements EncryptableKeyChain {
|
public class DeterministicKeyChain implements EncryptableKeyChain {
|
||||||
private static final Logger log = LoggerFactory.getLogger(DeterministicKeyChain.class);
|
private static final Logger log = LoggerFactory.getLogger(DeterministicKeyChain.class);
|
||||||
public static final String DEFAULT_PASSPHRASE_FOR_MNEMONIC = "";
|
public static final String DEFAULT_PASSPHRASE_FOR_MNEMONIC = "";
|
||||||
|
|
||||||
private final ReentrantLock lock = Threading.lock("DeterministicKeyChain");
|
protected final ReentrantLock lock = Threading.lock("DeterministicKeyChain");
|
||||||
|
|
||||||
private DeterministicHierarchy hierarchy;
|
private DeterministicHierarchy hierarchy;
|
||||||
@Nullable private DeterministicKey rootKey;
|
@Nullable private DeterministicKey rootKey;
|
||||||
@ -114,11 +120,11 @@ public class DeterministicKeyChain implements EncryptableKeyChain {
|
|||||||
// yet. For new chains it's set to whatever the default is, unless overridden by setLookaheadSize. For deserialized
|
// yet. For new chains it's set to whatever the default is, unless overridden by setLookaheadSize. For deserialized
|
||||||
// chains, it will be calculated on demand from the number of loaded keys.
|
// chains, it will be calculated on demand from the number of loaded keys.
|
||||||
private static final int LAZY_CALCULATE_LOOKAHEAD = -1;
|
private static final int LAZY_CALCULATE_LOOKAHEAD = -1;
|
||||||
private int lookaheadSize = 100;
|
protected int lookaheadSize = 100;
|
||||||
// The lookahead threshold causes us to batch up creation of new keys to minimize the frequency of Bloom filter
|
// The lookahead threshold causes us to batch up creation of new keys to minimize the frequency of Bloom filter
|
||||||
// regenerations, which are expensive and will (in future) trigger chain download stalls/retries. One third
|
// regenerations, which are expensive and will (in future) trigger chain download stalls/retries. One third
|
||||||
// is an efficiency tradeoff.
|
// is an efficiency tradeoff.
|
||||||
private int lookaheadThreshold = calcDefaultLookaheadThreshold();
|
protected int lookaheadThreshold = calcDefaultLookaheadThreshold();
|
||||||
|
|
||||||
private int calcDefaultLookaheadThreshold() {
|
private int calcDefaultLookaheadThreshold() {
|
||||||
return lookaheadSize / 3;
|
return lookaheadSize / 3;
|
||||||
@ -144,6 +150,99 @@ public class DeterministicKeyChain implements EncryptableKeyChain {
|
|||||||
// If set this chain is following another chain in a married KeyChainGroup
|
// If set this chain is following another chain in a married KeyChainGroup
|
||||||
private boolean isFollowing;
|
private boolean isFollowing;
|
||||||
|
|
||||||
|
// holds a number of signatures required to spend. It's the N from N-of-M CHECKMULTISIG script for P2SH transactions
|
||||||
|
// and always 1 for other transaction types
|
||||||
|
protected int sigsRequiredToSpend = 1;
|
||||||
|
|
||||||
|
|
||||||
|
public static class Builder<T extends Builder<T>> {
|
||||||
|
protected SecureRandom random;
|
||||||
|
protected int bits = 128;
|
||||||
|
protected String passphrase;
|
||||||
|
protected long seedCreationTimeSecs;
|
||||||
|
protected byte[] entropy;
|
||||||
|
protected DeterministicSeed seed;
|
||||||
|
|
||||||
|
protected Builder() {
|
||||||
|
}
|
||||||
|
|
||||||
|
@SuppressWarnings("unchecked")
|
||||||
|
protected T self() {
|
||||||
|
return (T)this;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Creates a deterministic key chain starting from the given entropy. All keys yielded by this chain will be the same
|
||||||
|
* if the starting entropy is the same. You should provide the creation time in seconds since the UNIX epoch for the
|
||||||
|
* seed: this lets us know from what part of the chain we can expect to see derived keys appear.
|
||||||
|
*/
|
||||||
|
public T entropy(byte[] entropy) {
|
||||||
|
this.entropy = entropy;
|
||||||
|
return self();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Creates a deterministic key chain starting from the given seed. All keys yielded by this chain will be the same
|
||||||
|
* if the starting seed is the same.
|
||||||
|
*/
|
||||||
|
public T seed(DeterministicSeed seed) {
|
||||||
|
this.seed = seed;
|
||||||
|
return self();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Generates a new key chain with entropy selected randomly from the given {@link java.security.SecureRandom}
|
||||||
|
* object and of the requested size in bits. The derived seed is further protected with a user selected passphrase
|
||||||
|
* (see BIP 39).
|
||||||
|
* @param random the random number generator - use new SecureRandom().
|
||||||
|
* @param bits The number of bits of entropy to use when generating entropy. Either 128 (default), 192 or 256.
|
||||||
|
*/
|
||||||
|
public T random(SecureRandom random, int bits) {
|
||||||
|
this.random = random;
|
||||||
|
this.bits = bits;
|
||||||
|
return self();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Generates a new key chain with 128 bits of entropy selected randomly from the given {@link java.security.SecureRandom}
|
||||||
|
* object. The derived seed is further protected with a user selected passphrase
|
||||||
|
* (see BIP 39).
|
||||||
|
* @param random the random number generator - use new SecureRandom().
|
||||||
|
*/
|
||||||
|
public T random(SecureRandom random) {
|
||||||
|
this.random = random;
|
||||||
|
return self();
|
||||||
|
}
|
||||||
|
|
||||||
|
/** The passphrase to use with the generated mnemonic, or null. Currently must be empty. */
|
||||||
|
public T passphrase(String passphrase) {
|
||||||
|
// FIXME support non-empty passphrase
|
||||||
|
this.passphrase = passphrase;
|
||||||
|
return self();
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
public DeterministicKeyChain build() {
|
||||||
|
checkState(random != null || entropy != null || seed != null, "Must provide either entropy or random");
|
||||||
|
checkState(passphrase == null || seed == null, "Passphrase must not be specified with seed");
|
||||||
|
DeterministicKeyChain chain;
|
||||||
|
|
||||||
|
if (random != null) {
|
||||||
|
chain = new DeterministicKeyChain(random, bits, passphrase, seedCreationTimeSecs);
|
||||||
|
} else if (entropy != null) {
|
||||||
|
chain = new DeterministicKeyChain(entropy, passphrase, seedCreationTimeSecs);
|
||||||
|
} else {
|
||||||
|
chain = new DeterministicKeyChain(seed);
|
||||||
|
}
|
||||||
|
|
||||||
|
return chain;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public static Builder<?> builder() {
|
||||||
|
return new Builder();
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Generates a new key chain with entropy selected randomly from the given {@link java.security.SecureRandom}
|
* Generates a new key chain with entropy selected randomly from the given {@link java.security.SecureRandom}
|
||||||
* object and the default entropy size.
|
* object and the default entropy size.
|
||||||
@ -170,7 +269,7 @@ public class DeterministicKeyChain implements EncryptableKeyChain {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Creates a deterministic key chain starting from the given seed. All keys yielded by this chain will be the same
|
* Creates a deterministic key chain starting from the given entropy. All keys yielded by this chain will be the same
|
||||||
* if the starting seed is the same. You should provide the creation time in seconds since the UNIX epoch for the
|
* if the starting seed is the same. You should provide the creation time in seconds since the UNIX epoch for the
|
||||||
* seed: this lets us know from what part of the chain we can expect to see derived keys appear.
|
* seed: this lets us know from what part of the chain we can expect to see derived keys appear.
|
||||||
*/
|
*/
|
||||||
@ -209,7 +308,7 @@ public class DeterministicKeyChain implements EncryptableKeyChain {
|
|||||||
* some other keychain. In a married wallet following keychain represents "spouse's" keychain.</p>
|
* some other keychain. In a married wallet following keychain represents "spouse's" keychain.</p>
|
||||||
* <p>Watch key has to be an account key.</p>
|
* <p>Watch key has to be an account key.</p>
|
||||||
*/
|
*/
|
||||||
private DeterministicKeyChain(DeterministicKey watchKey, boolean isFollowing) {
|
protected DeterministicKeyChain(DeterministicKey watchKey, boolean isFollowing) {
|
||||||
this(watchKey, Utils.currentTimeSeconds());
|
this(watchKey, Utils.currentTimeSeconds());
|
||||||
this.isFollowing = isFollowing;
|
this.isFollowing = isFollowing;
|
||||||
}
|
}
|
||||||
@ -581,49 +680,61 @@ public class DeterministicKeyChain implements EncryptableKeyChain {
|
|||||||
//
|
//
|
||||||
//////////////////////////////////////////////////////////////////////////////////////////////////////////////////
|
//////////////////////////////////////////////////////////////////////////////////////////////////////////////////
|
||||||
|
|
||||||
|
protected void beforeSerializeToProtobuf(List<Protos.Key> result) {
|
||||||
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public List<Protos.Key> serializeToProtobuf() {
|
public List<Protos.Key> serializeToProtobuf() {
|
||||||
|
List<Protos.Key> result = newArrayList();
|
||||||
lock.lock();
|
lock.lock();
|
||||||
try {
|
try {
|
||||||
// Most of the serialization work is delegated to the basic key chain, which will serialize the bulk of the
|
beforeSerializeToProtobuf(result);
|
||||||
// data (handling encryption along the way), and letting us patch it up with the extra data we care about.
|
result.addAll(serializeMyselfToProtobuf());
|
||||||
LinkedList<Protos.Key> entries = newLinkedList();
|
|
||||||
if (seed != null) {
|
|
||||||
Protos.Key.Builder mnemonicEntry = BasicKeyChain.serializeEncryptableItem(seed);
|
|
||||||
mnemonicEntry.setType(Protos.Key.Type.DETERMINISTIC_MNEMONIC);
|
|
||||||
serializeSeedEncryptableItem(seed, mnemonicEntry);
|
|
||||||
entries.add(mnemonicEntry.build());
|
|
||||||
}
|
|
||||||
Map<ECKey, Protos.Key.Builder> keys = basicKeyChain.serializeToEditableProtobufs();
|
|
||||||
for (Map.Entry<ECKey, Protos.Key.Builder> entry : keys.entrySet()) {
|
|
||||||
DeterministicKey key = (DeterministicKey) entry.getKey();
|
|
||||||
Protos.Key.Builder proto = entry.getValue();
|
|
||||||
proto.setType(Protos.Key.Type.DETERMINISTIC_KEY);
|
|
||||||
final Protos.DeterministicKey.Builder detKey = proto.getDeterministicKeyBuilder();
|
|
||||||
detKey.setChainCode(ByteString.copyFrom(key.getChainCode()));
|
|
||||||
for (ChildNumber num : key.getPath())
|
|
||||||
detKey.addPath(num.i());
|
|
||||||
if (key.equals(externalKey)) {
|
|
||||||
detKey.setIssuedSubkeys(issuedExternalKeys);
|
|
||||||
detKey.setLookaheadSize(lookaheadSize);
|
|
||||||
} else if (key.equals(internalKey)) {
|
|
||||||
detKey.setIssuedSubkeys(issuedInternalKeys);
|
|
||||||
detKey.setLookaheadSize(lookaheadSize);
|
|
||||||
}
|
|
||||||
// Flag the very first key of following keychain.
|
|
||||||
if (entries.isEmpty() && isFollowing()) {
|
|
||||||
detKey.setIsFollowing(true);
|
|
||||||
}
|
|
||||||
if (key.getParent() != null) {
|
|
||||||
// HD keys inherit the timestamp of their parent if they have one, so no need to serialize it.
|
|
||||||
proto.clearCreationTimestamp();
|
|
||||||
}
|
|
||||||
entries.add(proto.build());
|
|
||||||
}
|
|
||||||
return entries;
|
|
||||||
} finally {
|
} finally {
|
||||||
lock.unlock();
|
lock.unlock();
|
||||||
}
|
}
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
|
protected List<Protos.Key> serializeMyselfToProtobuf() {
|
||||||
|
// Most of the serialization work is delegated to the basic key chain, which will serialize the bulk of the
|
||||||
|
// data (handling encryption along the way), and letting us patch it up with the extra data we care about.
|
||||||
|
LinkedList<Protos.Key> entries = newLinkedList();
|
||||||
|
if (seed != null) {
|
||||||
|
Protos.Key.Builder mnemonicEntry = BasicKeyChain.serializeEncryptableItem(seed);
|
||||||
|
mnemonicEntry.setType(Protos.Key.Type.DETERMINISTIC_MNEMONIC);
|
||||||
|
serializeSeedEncryptableItem(seed, mnemonicEntry);
|
||||||
|
entries.add(mnemonicEntry.build());
|
||||||
|
}
|
||||||
|
Map<ECKey, Protos.Key.Builder> keys = basicKeyChain.serializeToEditableProtobufs();
|
||||||
|
for (Map.Entry<ECKey, Protos.Key.Builder> entry : keys.entrySet()) {
|
||||||
|
DeterministicKey key = (DeterministicKey) entry.getKey();
|
||||||
|
Protos.Key.Builder proto = entry.getValue();
|
||||||
|
proto.setType(Protos.Key.Type.DETERMINISTIC_KEY);
|
||||||
|
final Protos.DeterministicKey.Builder detKey = proto.getDeterministicKeyBuilder();
|
||||||
|
detKey.setChainCode(ByteString.copyFrom(key.getChainCode()));
|
||||||
|
for (ChildNumber num : key.getPath())
|
||||||
|
detKey.addPath(num.i());
|
||||||
|
if (key.equals(externalKey)) {
|
||||||
|
detKey.setIssuedSubkeys(issuedExternalKeys);
|
||||||
|
detKey.setLookaheadSize(lookaheadSize);
|
||||||
|
detKey.setSigsRequiredToSpend(getSigsRequiredToSpend());
|
||||||
|
} else if (key.equals(internalKey)) {
|
||||||
|
detKey.setIssuedSubkeys(issuedInternalKeys);
|
||||||
|
detKey.setLookaheadSize(lookaheadSize);
|
||||||
|
detKey.setSigsRequiredToSpend(getSigsRequiredToSpend());
|
||||||
|
}
|
||||||
|
// Flag the very first key of following keychain.
|
||||||
|
if (entries.isEmpty() && isFollowing()) {
|
||||||
|
detKey.setIsFollowing(true);
|
||||||
|
}
|
||||||
|
if (key.getParent() != null) {
|
||||||
|
// HD keys inherit the timestamp of their parent if they have one, so no need to serialize it.
|
||||||
|
proto.clearCreationTimestamp();
|
||||||
|
}
|
||||||
|
entries.add(proto.build());
|
||||||
|
}
|
||||||
|
return entries;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -636,12 +747,15 @@ public class DeterministicKeyChain implements EncryptableKeyChain {
|
|||||||
DeterministicKeyChain chain = null;
|
DeterministicKeyChain chain = null;
|
||||||
|
|
||||||
int lookaheadSize = -1;
|
int lookaheadSize = -1;
|
||||||
|
int sigsRequiredToSpend = 1;
|
||||||
|
|
||||||
for (Protos.Key key : keys) {
|
for (Protos.Key key : keys) {
|
||||||
final Protos.Key.Type t = key.getType();
|
final Protos.Key.Type t = key.getType();
|
||||||
if (t == Protos.Key.Type.DETERMINISTIC_MNEMONIC) {
|
if (t == Protos.Key.Type.DETERMINISTIC_MNEMONIC) {
|
||||||
if (chain != null) {
|
if (chain != null) {
|
||||||
checkState(lookaheadSize >= 0);
|
checkState(lookaheadSize >= 0);
|
||||||
chain.setLookaheadSize(lookaheadSize);
|
chain.setLookaheadSize(lookaheadSize);
|
||||||
|
chain.setSigsRequiredToSpend(sigsRequiredToSpend);
|
||||||
chain.maybeLookAhead();
|
chain.maybeLookAhead();
|
||||||
chains.add(chain);
|
chains.add(chain);
|
||||||
chain = null;
|
chain = null;
|
||||||
@ -693,6 +807,7 @@ public class DeterministicKeyChain implements EncryptableKeyChain {
|
|||||||
if (chain != null) {
|
if (chain != null) {
|
||||||
checkState(lookaheadSize >= 0);
|
checkState(lookaheadSize >= 0);
|
||||||
chain.setLookaheadSize(lookaheadSize);
|
chain.setLookaheadSize(lookaheadSize);
|
||||||
|
chain.setSigsRequiredToSpend(sigsRequiredToSpend);
|
||||||
chain.maybeLookAhead();
|
chain.maybeLookAhead();
|
||||||
chains.add(chain);
|
chains.add(chain);
|
||||||
chain = null;
|
chain = null;
|
||||||
@ -701,15 +816,23 @@ public class DeterministicKeyChain implements EncryptableKeyChain {
|
|||||||
isFollowingKey = true;
|
isFollowingKey = true;
|
||||||
}
|
}
|
||||||
if (chain == null) {
|
if (chain == null) {
|
||||||
|
// If this is not a following chain and previous was, this must be married
|
||||||
|
boolean isMarried = !isFollowingKey && !chains.isEmpty() && chains.get(chains.size() - 1).isFollowing();
|
||||||
if (seed == null) {
|
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, isFollowingKey);
|
if (isMarried)
|
||||||
|
chain = new MarriedKeyChain(accountKey);
|
||||||
|
else
|
||||||
|
chain = new DeterministicKeyChain(accountKey, isFollowingKey);
|
||||||
isWatchingAccountKey = true;
|
isWatchingAccountKey = true;
|
||||||
} else {
|
} else {
|
||||||
chain = new DeterministicKeyChain(seed, crypter);
|
if (isMarried)
|
||||||
|
chain = new MarriedKeyChain(seed, crypter);
|
||||||
|
else
|
||||||
|
chain = new DeterministicKeyChain(seed, crypter);
|
||||||
chain.lookaheadSize = LAZY_CALCULATE_LOOKAHEAD;
|
chain.lookaheadSize = LAZY_CALCULATE_LOOKAHEAD;
|
||||||
// If the seed is encrypted, then the chain is incomplete at this point. However, we will load
|
// If the seed is encrypted, then the chain is incomplete at this point. However, we will load
|
||||||
// it up below as we parse in the keys. We just need to check at the end that we've loaded
|
// it up below as we parse in the keys. We just need to check at the end that we've loaded
|
||||||
@ -759,6 +882,7 @@ public class DeterministicKeyChain implements EncryptableKeyChain {
|
|||||||
chain.externalKey = detkey;
|
chain.externalKey = detkey;
|
||||||
chain.issuedExternalKeys = key.getDeterministicKey().getIssuedSubkeys();
|
chain.issuedExternalKeys = key.getDeterministicKey().getIssuedSubkeys();
|
||||||
lookaheadSize = Math.max(lookaheadSize, key.getDeterministicKey().getLookaheadSize());
|
lookaheadSize = Math.max(lookaheadSize, key.getDeterministicKey().getLookaheadSize());
|
||||||
|
sigsRequiredToSpend = key.getDeterministicKey().getSigsRequiredToSpend();
|
||||||
} else if (detkey.getChildNumber().num() == 1) {
|
} else if (detkey.getChildNumber().num() == 1) {
|
||||||
chain.internalKey = detkey;
|
chain.internalKey = detkey;
|
||||||
chain.issuedInternalKeys = key.getDeterministicKey().getIssuedSubkeys();
|
chain.issuedInternalKeys = key.getDeterministicKey().getIssuedSubkeys();
|
||||||
@ -772,6 +896,7 @@ public class DeterministicKeyChain implements EncryptableKeyChain {
|
|||||||
if (chain != null) {
|
if (chain != null) {
|
||||||
checkState(lookaheadSize >= 0);
|
checkState(lookaheadSize >= 0);
|
||||||
chain.setLookaheadSize(lookaheadSize);
|
chain.setLookaheadSize(lookaheadSize);
|
||||||
|
chain.setSigsRequiredToSpend(sigsRequiredToSpend);
|
||||||
chain.maybeLookAhead();
|
chain.maybeLookAhead();
|
||||||
chains.add(chain);
|
chains.add(chain);
|
||||||
}
|
}
|
||||||
@ -1013,6 +1138,10 @@ public class DeterministicKeyChain implements EncryptableKeyChain {
|
|||||||
return result;
|
return result;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/** Housekeeping call to call when lookahead might be needed. Normally called automatically by KeychainGroup. */
|
||||||
|
public void maybeLookAheadScripts() {
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Returns number of keys used on external path. This may be fewer than the number that have been deserialized
|
* Returns number of keys used on external path. This may be fewer than the number that have been deserialized
|
||||||
* or held in memory, because of the lookahead zone.
|
* or held in memory, because of the lookahead zone.
|
||||||
@ -1115,4 +1244,73 @@ public class DeterministicKeyChain implements EncryptableKeyChain {
|
|||||||
lock.unlock();
|
lock.unlock();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Whether the keychain is married. A keychain is married when it vends P2SH addresses
|
||||||
|
* from multiple keychains in a multisig relationship.
|
||||||
|
* @see org.bitcoinj.wallet.MarriedKeyChain
|
||||||
|
*/
|
||||||
|
public boolean isMarried() {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Get redeem data for a key. Only applicable to married keychains. */
|
||||||
|
public RedeemData getRedeemData(DeterministicKey followedKey) {
|
||||||
|
throw new UnsupportedOperationException();
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Create a new key and return the matching output script. Only applicable to married keychains. */
|
||||||
|
public Script freshOutputScript(KeyPurpose purpose) {
|
||||||
|
throw new UnsupportedOperationException();
|
||||||
|
}
|
||||||
|
|
||||||
|
public String toString(boolean includePrivateKeys, NetworkParameters params) {
|
||||||
|
final StringBuilder builder2 = new StringBuilder();
|
||||||
|
if (seed != null) {
|
||||||
|
if (seed.isEncrypted()) {
|
||||||
|
builder2.append(String.format("Seed is encrypted%n"));
|
||||||
|
} else if (includePrivateKeys) {
|
||||||
|
final List<String> words = seed.getMnemonicCode();
|
||||||
|
builder2.append(
|
||||||
|
String.format("Seed as words: %s%nSeed as hex: %s%n", Joiner.on(' ').join(words),
|
||||||
|
seed.toHexString())
|
||||||
|
);
|
||||||
|
}
|
||||||
|
builder2.append(String.format("Seed birthday: %d [%s]%n", seed.getCreationTimeSeconds(), new Date(seed.getCreationTimeSeconds() * 1000)));
|
||||||
|
}
|
||||||
|
final DeterministicKey watchingKey = getWatchingKey();
|
||||||
|
// Don't show if it's been imported from a watching wallet already, because it'd result in a weird/
|
||||||
|
// unintuitive result where the watching key in a watching wallet is not the one it was created with
|
||||||
|
// due to the parent fingerprint being missing/not stored. In future we could store the parent fingerprint
|
||||||
|
// optionally as well to fix this, but it seems unimportant for now.
|
||||||
|
if (watchingKey.getParent() != null) {
|
||||||
|
builder2.append(String.format("Key to watch: %s%n", watchingKey.serializePubB58()));
|
||||||
|
}
|
||||||
|
formatAddresses(includePrivateKeys, params, builder2);
|
||||||
|
return builder2.toString();
|
||||||
|
}
|
||||||
|
|
||||||
|
protected void formatAddresses(boolean includePrivateKeys, NetworkParameters params, StringBuilder builder2) {
|
||||||
|
for (ECKey key : getKeys(false))
|
||||||
|
key.formatKeyWithAddress(includePrivateKeys, builder2, params);
|
||||||
|
}
|
||||||
|
|
||||||
|
/** The number of signatures required to spend coins received by this keychain. */
|
||||||
|
public void setSigsRequiredToSpend(int sigsRequiredToSpend) {
|
||||||
|
this.sigsRequiredToSpend = sigsRequiredToSpend;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns the number of signatures required to spend transactions for this KeyChain. It's the N from
|
||||||
|
* N-of-M CHECKMULTISIG script for P2SH transactions and always 1 for other transaction types.
|
||||||
|
*/
|
||||||
|
public int getSigsRequiredToSpend() {
|
||||||
|
return sigsRequiredToSpend;
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Returns the redeem script by its hash or null if this keychain did not generate the script. */
|
||||||
|
@Nullable
|
||||||
|
public RedeemData findRedeemDataByScriptHash(ByteString bytes) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
@ -26,6 +26,7 @@ import org.bitcoinj.script.ScriptBuilder;
|
|||||||
import org.bitcoinj.store.UnreadableWalletException;
|
import org.bitcoinj.store.UnreadableWalletException;
|
||||||
import org.bitcoinj.utils.ListenerRegistration;
|
import org.bitcoinj.utils.ListenerRegistration;
|
||||||
import org.bitcoinj.utils.Threading;
|
import org.bitcoinj.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.HashMultimap;
|
||||||
import com.google.common.collect.ImmutableList;
|
import com.google.common.collect.ImmutableList;
|
||||||
@ -67,20 +68,9 @@ public class KeyChainGroup implements KeyBag {
|
|||||||
|
|
||||||
private BasicKeyChain basic;
|
private BasicKeyChain basic;
|
||||||
private NetworkParameters params;
|
private NetworkParameters params;
|
||||||
private final List<DeterministicKeyChain> chains;
|
private final LinkedList<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;
|
|
||||||
|
|
||||||
// holds a number of signatures required to spend. It's the N from N-of-M CHECKMULTISIG script for P2SH transactions
|
|
||||||
// and always 1 for other transaction types
|
|
||||||
private int sigsRequiredToSpend;
|
|
||||||
|
|
||||||
// The map holds P2SH redeem script and corresponding ECKeys issued by this KeyChainGroup (including lookahead)
|
|
||||||
// mapped to redeem script hashes.
|
|
||||||
private LinkedHashMap<ByteString, RedeemData> marriedKeysRedeemData;
|
|
||||||
|
|
||||||
private EnumMap<KeyChain.KeyPurpose, Address> currentAddresses;
|
private EnumMap<KeyChain.KeyPurpose, Address> currentAddresses;
|
||||||
@Nullable private KeyCrypter keyCrypter;
|
@Nullable private KeyCrypter keyCrypter;
|
||||||
private int lookaheadSize = -1;
|
private int lookaheadSize = -1;
|
||||||
@ -88,12 +78,12 @@ public class KeyChainGroup implements KeyBag {
|
|||||||
|
|
||||||
/** Creates a keychain group with no basic chain, and a single, lazily created HD chain. */
|
/** Creates a keychain group with no basic chain, and a single, lazily created HD chain. */
|
||||||
public KeyChainGroup(NetworkParameters params) {
|
public KeyChainGroup(NetworkParameters params) {
|
||||||
this(params, null, new ArrayList<DeterministicKeyChain>(1), null, null, 1, null);
|
this(params, null, new ArrayList<DeterministicKeyChain>(1), null, null);
|
||||||
}
|
}
|
||||||
|
|
||||||
/** Creates a keychain group with no basic chain, and an HD chain initialized from the given seed. */
|
/** Creates a keychain group with no basic chain, and an HD chain initialized from the given seed. */
|
||||||
public KeyChainGroup(NetworkParameters params, DeterministicSeed seed) {
|
public KeyChainGroup(NetworkParameters params, DeterministicSeed seed) {
|
||||||
this(params, null, ImmutableList.of(new DeterministicKeyChain(seed)), null, null, 1, null);
|
this(params, null, ImmutableList.of(new DeterministicKeyChain(seed)), null, null);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -101,7 +91,7 @@ public class KeyChainGroup implements KeyBag {
|
|||||||
* This HAS to be an account key as returned by {@link DeterministicKeyChain#getWatchingKey()}.
|
* This HAS to be an account key as returned by {@link DeterministicKeyChain#getWatchingKey()}.
|
||||||
*/
|
*/
|
||||||
public KeyChainGroup(NetworkParameters params, DeterministicKey watchKey) {
|
public KeyChainGroup(NetworkParameters params, DeterministicKey watchKey) {
|
||||||
this(params, null, ImmutableList.of(DeterministicKeyChain.watch(watchKey)), null, null, 1, null);
|
this(params, null, ImmutableList.of(DeterministicKeyChain.watch(watchKey)), null, null);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -110,115 +100,36 @@ public class KeyChainGroup implements KeyBag {
|
|||||||
* This HAS to be an account key as returned by {@link DeterministicKeyChain#getWatchingKey()}.
|
* This HAS to be an account key as returned by {@link DeterministicKeyChain#getWatchingKey()}.
|
||||||
*/
|
*/
|
||||||
public KeyChainGroup(NetworkParameters params, DeterministicKey watchKey, long creationTimeSecondsSecs) {
|
public KeyChainGroup(NetworkParameters params, DeterministicKey watchKey, long creationTimeSecondsSecs) {
|
||||||
this(params, null, ImmutableList.of(DeterministicKeyChain.watch(watchKey, creationTimeSecondsSecs)), null, null, 1, null);
|
this(params, null, ImmutableList.of(DeterministicKeyChain.watch(watchKey, creationTimeSecondsSecs)), 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, int sigsRequiredToSpend) {
|
|
||||||
this(params, seed);
|
|
||||||
|
|
||||||
addFollowingAccountKeys(followingAccountKeys, sigsRequiredToSpend);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* <p>Alias for <code>addFollowingAccountKeys(followingAccountKeys, (followingAccountKeys.size() + 1) / 2 + 1)</code></p>
|
|
||||||
* <p>Creates married keychain requiring majority of keys to spend (2-of-3, 3-of-5 and so on)</p>
|
|
||||||
* <p>IMPORTANT: As of Bitcoin Core 0.9 all multisig transactions which require more than 3 public keys are non-standard
|
|
||||||
* and such spends won't be processed by peers with default settings, essentially making such transactions almost
|
|
||||||
* nonspendable</p>
|
|
||||||
*/
|
|
||||||
public void addFollowingAccountKeys(List<DeterministicKey> followingAccountKeys) {
|
|
||||||
addFollowingAccountKeys(followingAccountKeys, (followingAccountKeys.size() + 1) / 2 + 1);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* <p>Makes given account keys follow the account key of the active keychain. After that active keychain will be
|
|
||||||
* treated as married and you will be able to get P2SH addresses to receive coins to. Given sigsRequiredToSpend value
|
|
||||||
* specifies how many signatures required to spend transactions for this married keychain. This value should not exceed
|
|
||||||
* total number of keys involved (one followed key plus number of following keys), otherwise IllegalArgumentException
|
|
||||||
* will be thrown.</p>
|
|
||||||
* <p>IMPORTANT: As of Bitcoin Core 0.9 all multisig transactions which require more than 3 public keys are non-standard
|
|
||||||
* and such spends won't be processed by peers with default settings, essentially making such transactions almost
|
|
||||||
* nonspendable</p>
|
|
||||||
* <p>This method will throw an IllegalStateException, if active keychain is already married or already has leaf keys
|
|
||||||
* issued. In future this behaviour may be replaced with key rotation.</p>
|
|
||||||
*/
|
|
||||||
public void addFollowingAccountKeys(List<DeterministicKey> followingAccountKeys, int sigsRequiredToSpend) {
|
|
||||||
checkArgument(sigsRequiredToSpend <= followingAccountKeys.size() + 1, "Multisig threshold can't exceed total number of keys");
|
|
||||||
checkState(!isMarried(), "KeyChainGroup is married already");
|
|
||||||
checkState(getActiveKeyChain().numLeafKeysIssued() == 0, "Active keychain already has keys in use");
|
|
||||||
|
|
||||||
this.sigsRequiredToSpend = sigsRequiredToSpend;
|
|
||||||
|
|
||||||
DeterministicKey accountKey = getActiveKeyChain().getWatchingKey();
|
|
||||||
for (DeterministicKey key : followingAccountKeys) {
|
|
||||||
checkArgument(key.getPath().size() == 1, "Following keys have to be account keys");
|
|
||||||
DeterministicKeyChain chain = DeterministicKeyChain.watchAndFollow(key);
|
|
||||||
if (lookaheadSize >= 0)
|
|
||||||
chain.setLookaheadSize(lookaheadSize);
|
|
||||||
if (lookaheadThreshold >= 0)
|
|
||||||
chain.setLookaheadThreshold(lookaheadThreshold);
|
|
||||||
followingKeychains.put(accountKey, chain);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Used for deserialization.
|
// Used for deserialization.
|
||||||
private KeyChainGroup(NetworkParameters params, @Nullable BasicKeyChain basicKeyChain, List<DeterministicKeyChain> chains,
|
private KeyChainGroup(NetworkParameters params, @Nullable BasicKeyChain basicKeyChain, List<DeterministicKeyChain> chains,
|
||||||
@Nullable EnumMap<KeyChain.KeyPurpose, DeterministicKey> currentKeys, Multimap<DeterministicKey,
|
@Nullable EnumMap<KeyChain.KeyPurpose, DeterministicKey> currentKeys, @Nullable KeyCrypter crypter) {
|
||||||
DeterministicKeyChain> followingKeychains, int sigsRequiredToSpend, @Nullable KeyCrypter crypter) {
|
|
||||||
this.params = params;
|
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 LinkedList<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.currentAddresses = new EnumMap<KeyChain.KeyPurpose, Address>(KeyChain.KeyPurpose.class);
|
||||||
this.followingKeychains = HashMultimap.create();
|
|
||||||
if (followingKeychains != null) {
|
|
||||||
this.followingKeychains.putAll(followingKeychains);
|
|
||||||
}
|
|
||||||
this.sigsRequiredToSpend = sigsRequiredToSpend;
|
|
||||||
marriedKeysRedeemData = new LinkedHashMap<ByteString, RedeemData>();
|
|
||||||
maybeLookaheadScripts();
|
maybeLookaheadScripts();
|
||||||
|
|
||||||
if (!this.followingKeychains.isEmpty()) {
|
if (isMarried()) {
|
||||||
DeterministicKey followedWatchKey = getActiveKeyChain().getWatchingKey();
|
|
||||||
for (Map.Entry<KeyChain.KeyPurpose, DeterministicKey> entry : this.currentKeys.entrySet()) {
|
for (Map.Entry<KeyChain.KeyPurpose, DeterministicKey> entry : this.currentKeys.entrySet()) {
|
||||||
Address address = makeP2SHOutputScript(entry.getValue(), followedWatchKey).getToAddress(params);
|
Address address = makeP2SHOutputScript(entry.getValue(), getActiveKeyChain()).getToAddress(params);
|
||||||
currentAddresses.put(entry.getKey(), address);
|
currentAddresses.put(entry.getKey(), address);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* This keeps {@link #marriedKeysRedeemData} in sync with the number of keys issued
|
* This keeps married redeem data in sync with the number of keys issued
|
||||||
*/
|
*/
|
||||||
private void maybeLookaheadScripts() {
|
private void maybeLookaheadScripts() {
|
||||||
if (chains.isEmpty())
|
|
||||||
return;
|
|
||||||
|
|
||||||
int numLeafKeys = 0;
|
|
||||||
for (DeterministicKeyChain chain : chains) {
|
for (DeterministicKeyChain chain : chains) {
|
||||||
numLeafKeys += chain.getLeafKeys().size();
|
chain.maybeLookAheadScripts();
|
||||||
}
|
|
||||||
|
|
||||||
checkState(marriedKeysRedeemData.size() <= numLeafKeys, "Number of scripts is greater than number of leaf keys");
|
|
||||||
if (marriedKeysRedeemData.size() == numLeafKeys)
|
|
||||||
return;
|
|
||||||
|
|
||||||
for (DeterministicKeyChain chain : chains) {
|
|
||||||
if (isMarried(chain)) {
|
|
||||||
chain.maybeLookAhead();
|
|
||||||
for (DeterministicKey followedKey : chain.getLeafKeys()) {
|
|
||||||
RedeemData redeemData = getRedeemData(followedKey, chain.getWatchingKey());
|
|
||||||
Script scriptPubKey = ScriptBuilder.createP2SHOutputScript(redeemData.redeemScript);
|
|
||||||
marriedKeysRedeemData.put(ByteString.copyFrom(scriptPubKey.getPubKeyHash()), redeemData);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -226,6 +137,14 @@ public class KeyChainGroup implements KeyBag {
|
|||||||
public void createAndActivateNewHDChain() {
|
public void createAndActivateNewHDChain() {
|
||||||
// We can't do auto upgrade here because we don't know the rotation time, if any.
|
// We can't do auto upgrade here because we don't know the rotation time, if any.
|
||||||
final DeterministicKeyChain chain = new DeterministicKeyChain(new SecureRandom());
|
final DeterministicKeyChain chain = new DeterministicKeyChain(new SecureRandom());
|
||||||
|
addAndActivateHDChain(chain);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Adds an HD chain to the chains list, and make it the default chain (from which keys are issued).
|
||||||
|
* Useful for adding a complex pre-configured keychain, such as a married wallet.
|
||||||
|
*/
|
||||||
|
public void addAndActivateHDChain(DeterministicKeyChain chain) {
|
||||||
log.info("Creating and activating a new HD chain: {}", chain);
|
log.info("Creating and activating a new HD chain: {}", chain);
|
||||||
for (ListenerRegistration<KeyChainEventListener> registration : basic.getListeners())
|
for (ListenerRegistration<KeyChainEventListener> registration : basic.getListeners())
|
||||||
chain.addEventListener(registration.listener, registration.executor);
|
chain.addEventListener(registration.listener, registration.executor);
|
||||||
@ -249,7 +168,7 @@ public class KeyChainGroup implements KeyBag {
|
|||||||
*/
|
*/
|
||||||
public DeterministicKey currentKey(KeyChain.KeyPurpose purpose) {
|
public DeterministicKey currentKey(KeyChain.KeyPurpose purpose) {
|
||||||
DeterministicKeyChain chain = getActiveKeyChain();
|
DeterministicKeyChain chain = getActiveKeyChain();
|
||||||
if (isMarried(chain)) {
|
if (chain.isMarried()) {
|
||||||
throw new UnsupportedOperationException("Key is not suitable to receive coins for married keychains." +
|
throw new UnsupportedOperationException("Key is not suitable to receive coins for married keychains." +
|
||||||
" Use freshAddress to get P2SH address instead");
|
" Use freshAddress to get P2SH address instead");
|
||||||
}
|
}
|
||||||
@ -266,7 +185,7 @@ public class KeyChainGroup implements KeyBag {
|
|||||||
*/
|
*/
|
||||||
public Address currentAddress(KeyChain.KeyPurpose purpose) {
|
public Address currentAddress(KeyChain.KeyPurpose purpose) {
|
||||||
DeterministicKeyChain chain = getActiveKeyChain();
|
DeterministicKeyChain chain = getActiveKeyChain();
|
||||||
if (isMarried(chain)) {
|
if (chain.isMarried()) {
|
||||||
Address current = currentAddresses.get(purpose);
|
Address current = currentAddresses.get(purpose);
|
||||||
if (current == null) {
|
if (current == null) {
|
||||||
current = freshAddress(purpose);
|
current = freshAddress(purpose);
|
||||||
@ -308,7 +227,7 @@ public class KeyChainGroup implements KeyBag {
|
|||||||
*/
|
*/
|
||||||
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)) {
|
if (chain.isMarried()) {
|
||||||
throw new UnsupportedOperationException("Key is not suitable to receive coins for married keychains." +
|
throw new UnsupportedOperationException("Key is not suitable to receive coins for married keychains." +
|
||||||
" Use freshAddress to get P2SH address instead");
|
" Use freshAddress to get P2SH address instead");
|
||||||
}
|
}
|
||||||
@ -320,10 +239,10 @@ public class KeyChainGroup implements KeyBag {
|
|||||||
*/
|
*/
|
||||||
public Address freshAddress(KeyChain.KeyPurpose purpose) {
|
public Address freshAddress(KeyChain.KeyPurpose purpose) {
|
||||||
DeterministicKeyChain chain = getActiveKeyChain();
|
DeterministicKeyChain chain = getActiveKeyChain();
|
||||||
if (isMarried(chain)) {
|
if (chain.isMarried()) {
|
||||||
List<ECKey> marriedKeys = freshMarriedKeys(purpose, chain);
|
Script outputScript = chain.freshOutputScript(purpose);
|
||||||
Script p2shScript = makeP2SHOutputScript(marriedKeys);
|
checkState(outputScript.isPayToScriptHash()); // Only handle P2SH for now
|
||||||
Address freshAddress = Address.fromP2SHScript(params, p2shScript);
|
Address freshAddress = Address.fromP2SHScript(params, outputScript);
|
||||||
maybeLookaheadScripts();
|
maybeLookaheadScripts();
|
||||||
currentAddresses.put(purpose, freshAddress);
|
currentAddresses.put(purpose, freshAddress);
|
||||||
return freshAddress;
|
return freshAddress;
|
||||||
@ -332,28 +251,6 @@ public class KeyChainGroup implements KeyBag {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private List<ECKey> freshMarriedKeys(KeyChain.KeyPurpose purpose, DeterministicKeyChain followedKeyChain) {
|
|
||||||
DeterministicKey followedKey = followedKeyChain.getKey(purpose);
|
|
||||||
ImmutableList.Builder<ECKey> keys = ImmutableList.<ECKey>builder().add(followedKey);
|
|
||||||
Collection<DeterministicKeyChain> keyChains = followingKeychains.get(followedKeyChain.getWatchingKey());
|
|
||||||
for (DeterministicKeyChain keyChain : keyChains) {
|
|
||||||
DeterministicKey followingKey = keyChain.getKey(purpose);
|
|
||||||
checkState(followedKey.getChildNumber().equals(followingKey.getChildNumber()), "Following keychains should be in sync");
|
|
||||||
keys.add(followingKey);
|
|
||||||
}
|
|
||||||
return keys.build();
|
|
||||||
}
|
|
||||||
|
|
||||||
private List<ECKey> getMarriedKeysWithFollowed(DeterministicKey followedKey, Collection<DeterministicKeyChain> followingChains) {
|
|
||||||
ImmutableList.Builder<ECKey> keys = ImmutableList.builder();
|
|
||||||
for (DeterministicKeyChain keyChain : followingChains) {
|
|
||||||
keyChain.maybeLookAhead();
|
|
||||||
keys.add(keyChain.getKeyByPath(followedKey.getPath()));
|
|
||||||
}
|
|
||||||
keys.add(followedKey);
|
|
||||||
return keys.build();
|
|
||||||
}
|
|
||||||
|
|
||||||
/** Returns the key chain that's used for generation of fresh/current keys. This is always the newest HD chain. */
|
/** 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() {
|
||||||
if (chains.isEmpty()) {
|
if (chains.isEmpty()) {
|
||||||
@ -379,9 +276,6 @@ public class KeyChainGroup implements KeyBag {
|
|||||||
for (DeterministicKeyChain chain : chains) {
|
for (DeterministicKeyChain chain : chains) {
|
||||||
chain.setLookaheadSize(lookaheadSize);
|
chain.setLookaheadSize(lookaheadSize);
|
||||||
}
|
}
|
||||||
for (DeterministicKeyChain chain : followingKeychains.values()) {
|
|
||||||
chain.setLookaheadSize(lookaheadSize);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -456,7 +350,14 @@ public class KeyChainGroup implements KeyBag {
|
|||||||
|
|
||||||
@Nullable
|
@Nullable
|
||||||
public RedeemData findRedeemDataFromScriptHash(byte[] scriptHash) {
|
public RedeemData findRedeemDataFromScriptHash(byte[] scriptHash) {
|
||||||
return marriedKeysRedeemData.get(ByteString.copyFrom(scriptHash));
|
// Iterate in reverse order, since the active keychain is the one most likely to have the hit
|
||||||
|
for (Iterator<DeterministicKeyChain> iter = chains.descendingIterator() ; iter.hasNext() ; ) {
|
||||||
|
DeterministicKeyChain chain = iter.next();
|
||||||
|
RedeemData redeemData = chain.findRedeemDataByScriptHash(ByteString.copyFrom(scriptHash));
|
||||||
|
if (redeemData != null)
|
||||||
|
return redeemData;
|
||||||
|
}
|
||||||
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
@Nullable
|
@Nullable
|
||||||
@ -552,18 +453,12 @@ public class KeyChainGroup implements KeyBag {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Returns true if the given keychain is being followed by at least one another keychain
|
* Whether the active keychain is married. A keychain is married when it vends P2SH addresses
|
||||||
*/
|
* from multiple keychains in a multisig relationship.
|
||||||
public boolean isMarried(DeterministicKeyChain keychain) {
|
* @see org.bitcoinj.wallet.MarriedKeyChain
|
||||||
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() {
|
public boolean isMarried() {
|
||||||
return isMarried(getActiveKeyChain());
|
return !chains.isEmpty() && getActiveKeyChain().isMarried();
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -638,12 +533,7 @@ public class KeyChainGroup implements KeyBag {
|
|||||||
public int getBloomFilterElementCount() {
|
public int getBloomFilterElementCount() {
|
||||||
int result = basic.numBloomFilterEntries();
|
int result = basic.numBloomFilterEntries();
|
||||||
for (DeterministicKeyChain chain : chains) {
|
for (DeterministicKeyChain chain : chains) {
|
||||||
if (isMarried(chain)) {
|
result += chain.numBloomFilterEntries();
|
||||||
chain.maybeLookAhead();
|
|
||||||
result += chain.getLeafKeys().size() * 2;
|
|
||||||
} else {
|
|
||||||
result += chain.numBloomFilterEntries();
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
return result;
|
return result;
|
||||||
}
|
}
|
||||||
@ -653,15 +543,8 @@ public class KeyChainGroup implements KeyBag {
|
|||||||
if (basic.numKeys() > 0)
|
if (basic.numKeys() > 0)
|
||||||
filter.merge(basic.getFilter(size, falsePositiveRate, nTweak));
|
filter.merge(basic.getFilter(size, falsePositiveRate, nTweak));
|
||||||
|
|
||||||
for (Map.Entry<ByteString, RedeemData> entry : marriedKeysRedeemData.entrySet()) {
|
|
||||||
filter.insert(entry.getKey().toByteArray());
|
|
||||||
filter.insert(entry.getValue().redeemScript.getProgram());
|
|
||||||
}
|
|
||||||
|
|
||||||
for (DeterministicKeyChain chain : chains) {
|
for (DeterministicKeyChain chain : chains) {
|
||||||
if (!isMarried(chain)) {
|
filter.merge(chain.getFilter(size, falsePositiveRate, nTweak));
|
||||||
filter.merge(chain.getFilter(size, falsePositiveRate, nTweak));
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
return filter;
|
return filter;
|
||||||
}
|
}
|
||||||
@ -671,22 +554,8 @@ public class KeyChainGroup implements KeyBag {
|
|||||||
throw new UnsupportedOperationException(); // Unused.
|
throw new UnsupportedOperationException(); // Unused.
|
||||||
}
|
}
|
||||||
|
|
||||||
private Script makeP2SHOutputScript(List<ECKey> marriedKeys) {
|
private Script makeP2SHOutputScript(DeterministicKey followedKey, DeterministicKeyChain chain) {
|
||||||
return ScriptBuilder.createP2SHOutputScript(makeRedeemScript(marriedKeys));
|
return ScriptBuilder.createP2SHOutputScript(chain.getRedeemData(followedKey).redeemScript);
|
||||||
}
|
|
||||||
|
|
||||||
private Script makeP2SHOutputScript(DeterministicKey followedKey, DeterministicKey followedAccountKey) {
|
|
||||||
return ScriptBuilder.createP2SHOutputScript(getRedeemData(followedKey, followedAccountKey).redeemScript);
|
|
||||||
}
|
|
||||||
|
|
||||||
private RedeemData getRedeemData(DeterministicKey followedKey, DeterministicKey followedAccountKey) {
|
|
||||||
Collection<DeterministicKeyChain> followingChains = followingKeychains.get(followedAccountKey);
|
|
||||||
List<ECKey> marriedKeys = getMarriedKeysWithFollowed(followedKey, followingChains);
|
|
||||||
return RedeemData.of(marriedKeys, makeRedeemScript(marriedKeys));
|
|
||||||
}
|
|
||||||
|
|
||||||
private Script makeRedeemScript(List<ECKey> marriedKeys) {
|
|
||||||
return ScriptBuilder.createRedeemScript(sigsRequiredToSpend, marriedKeys);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/** Adds a listener for events that are run when keys are added, on the user thread. */
|
/** Adds a listener for events that are run when keys are added, on the user thread. */
|
||||||
@ -719,41 +588,31 @@ public class KeyChainGroup implements KeyBag {
|
|||||||
else
|
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(NetworkParameters params, List<Protos.Key> keys, int sigsRequiredToSpend) throws UnreadableWalletException {
|
public static KeyChainGroup fromProtobufUnencrypted(NetworkParameters params, List<Protos.Key> keys) throws UnreadableWalletException {
|
||||||
checkArgument(sigsRequiredToSpend > 0);
|
|
||||||
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);
|
||||||
Multimap<DeterministicKey, DeterministicKeyChain> followingKeychains = extractFollowingKeychains(chains);
|
extractFollowingKeychains(chains);
|
||||||
if (sigsRequiredToSpend < 2 && followingKeychains.size() > 0)
|
return new KeyChainGroup(params, basicKeyChain, chains, currentKeys, null);
|
||||||
throw new IllegalArgumentException("Married KeyChainGroup requires multiple signatures to spend");
|
|
||||||
return new KeyChainGroup(params, basicKeyChain, chains, currentKeys, followingKeychains, sigsRequiredToSpend, null);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
public static KeyChainGroup fromProtobufEncrypted(NetworkParameters params, List<Protos.Key> keys, int sigsRequiredToSpend, KeyCrypter crypter) throws UnreadableWalletException {
|
public static KeyChainGroup fromProtobufEncrypted(NetworkParameters params, List<Protos.Key> keys, KeyCrypter crypter) throws UnreadableWalletException {
|
||||||
checkArgument(sigsRequiredToSpend > 0);
|
|
||||||
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);
|
||||||
Multimap<DeterministicKey, DeterministicKeyChain> followingKeychains = extractFollowingKeychains(chains);
|
extractFollowingKeychains(chains);
|
||||||
if (sigsRequiredToSpend < 2 && followingKeychains.size() > 0)
|
return new KeyChainGroup(params, basicKeyChain, chains, currentKeys, crypter);
|
||||||
throw new IllegalArgumentException("Married KeyChainGroup requires multiple signatures to spend");
|
|
||||||
return new KeyChainGroup(params, basicKeyChain, chains, currentKeys, followingKeychains, sigsRequiredToSpend, crypter);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -848,111 +707,42 @@ public class KeyChainGroup implements KeyBag {
|
|||||||
return currentKeys;
|
return currentKeys;
|
||||||
}
|
}
|
||||||
|
|
||||||
private static Multimap<DeterministicKey, DeterministicKeyChain> extractFollowingKeychains(List<DeterministicKeyChain> chains) {
|
private static void extractFollowingKeychains(List<DeterministicKeyChain> chains) {
|
||||||
// look for following key chains and map them to the watch keys of followed keychains
|
// look for following key chains and map them to the watch keys of followed keychains
|
||||||
Multimap<DeterministicKey, DeterministicKeyChain> followingKeychains = HashMultimap.create();
|
Multimap<DeterministicKey, DeterministicKeyChain> followingKeychains = HashMultimap.create();
|
||||||
List<DeterministicKeyChain> followingChains = new ArrayList<DeterministicKeyChain>();
|
List<DeterministicKeyChain> followingChains = Lists.newArrayList();
|
||||||
for (Iterator<DeterministicKeyChain> it = chains.iterator(); it.hasNext(); ) {
|
for (Iterator<DeterministicKeyChain> it = chains.iterator(); it.hasNext(); ) {
|
||||||
DeterministicKeyChain chain = it.next();
|
DeterministicKeyChain chain = it.next();
|
||||||
if (chain.isFollowing()) {
|
if (chain.isFollowing()) {
|
||||||
followingChains.add(chain);
|
followingChains.add(chain);
|
||||||
it.remove();
|
it.remove();
|
||||||
} else if (!followingChains.isEmpty()) {
|
} else if (!followingChains.isEmpty()) {
|
||||||
followingKeychains.putAll(chain.getWatchingKey(), followingChains);
|
if (!(chain instanceof MarriedKeyChain))
|
||||||
followingChains.clear();
|
throw new IllegalStateException();
|
||||||
|
((MarriedKeyChain)chain).setFollowingKeyChains(followingChains);
|
||||||
|
followingChains = Lists.newArrayList();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return followingKeychains;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
public String toString(boolean includePrivateKeys) {
|
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(includePrivateKeys, key, builder);
|
key.formatKeyWithAddress(includePrivateKeys, builder, params);
|
||||||
}
|
}
|
||||||
List<String> chainStrs = Lists.newLinkedList();
|
List<String> chainStrs = Lists.newLinkedList();
|
||||||
for (DeterministicKeyChain chain : chains) {
|
for (DeterministicKeyChain chain : chains) {
|
||||||
final StringBuilder builder2 = new StringBuilder();
|
chainStrs.add(chain.toString(includePrivateKeys, params));
|
||||||
DeterministicSeed seed = chain.getSeed();
|
|
||||||
if (seed != null) {
|
|
||||||
if (seed.isEncrypted()) {
|
|
||||||
builder2.append(String.format("Seed is encrypted%n"));
|
|
||||||
} else if (includePrivateKeys) {
|
|
||||||
final List<String> words = seed.getMnemonicCode();
|
|
||||||
builder2.append(
|
|
||||||
String.format("Seed as words: %s%nSeed as hex: %s%n", Joiner.on(' ').join(words),
|
|
||||||
seed.toHexString())
|
|
||||||
);
|
|
||||||
}
|
|
||||||
builder2.append(String.format("Seed birthday: %d [%s]%n", seed.getCreationTimeSeconds(), new Date(seed.getCreationTimeSeconds() * 1000)));
|
|
||||||
}
|
|
||||||
final DeterministicKey watchingKey = chain.getWatchingKey();
|
|
||||||
// Don't show if it's been imported from a watching wallet already, because it'd result in a weird/
|
|
||||||
// unintuitive result where the watching key in a watching wallet is not the one it was created with
|
|
||||||
// due to the parent fingerprint being missing/not stored. In future we could store the parent fingerprint
|
|
||||||
// optionally as well to fix this, but it seems unimportant for now.
|
|
||||||
if (watchingKey.getParent() != null) {
|
|
||||||
builder2.append(String.format("Key to watch: %s%n", watchingKey.serializePubB58()));
|
|
||||||
}
|
|
||||||
if (isMarried(chain)) {
|
|
||||||
Collection<DeterministicKeyChain> followingChains = followingKeychains.get(chain.getWatchingKey());
|
|
||||||
for (DeterministicKeyChain followingChain : followingChains) {
|
|
||||||
builder2.append(String.format("Following chain: %s%n", followingChain.getWatchingKey().serializePubB58()));
|
|
||||||
}
|
|
||||||
builder2.append(String.format("%n"));
|
|
||||||
for (RedeemData redeemData : marriedKeysRedeemData.values())
|
|
||||||
formatScript(ScriptBuilder.createP2SHOutputScript(redeemData.redeemScript), builder2);
|
|
||||||
} else {
|
|
||||||
for (ECKey key : chain.getKeys(false))
|
|
||||||
formatKeyWithAddress(includePrivateKeys, key, builder2);
|
|
||||||
}
|
|
||||||
chainStrs.add(builder2.toString());
|
|
||||||
}
|
}
|
||||||
builder.append(Joiner.on(String.format("%n")).join(chainStrs));
|
builder.append(Joiner.on(String.format("%n")).join(chainStrs));
|
||||||
return builder.toString();
|
return builder.toString();
|
||||||
}
|
}
|
||||||
|
|
||||||
private void formatScript(Script script, StringBuilder builder) {
|
|
||||||
builder.append(" addr:");
|
|
||||||
builder.append(script.getToAddress(params));
|
|
||||||
builder.append(" hash160:");
|
|
||||||
builder.append(Utils.HEX.encode(script.getPubKeyHash()));
|
|
||||||
builder.append("\n");
|
|
||||||
}
|
|
||||||
|
|
||||||
private void formatKeyWithAddress(boolean includePrivateKeys, ECKey key, StringBuilder builder) {
|
|
||||||
final Address address = key.toAddress(params);
|
|
||||||
builder.append(" addr:");
|
|
||||||
builder.append(address.toString());
|
|
||||||
builder.append(" hash160:");
|
|
||||||
builder.append(Utils.HEX.encode(key.getPubKeyHash()));
|
|
||||||
if (key instanceof DeterministicKey) {
|
|
||||||
builder.append(" (");
|
|
||||||
builder.append((((DeterministicKey) key).getPathAsString()));
|
|
||||||
builder.append(")");
|
|
||||||
}
|
|
||||||
builder.append("\n");
|
|
||||||
if (includePrivateKeys) {
|
|
||||||
builder.append(" ");
|
|
||||||
builder.append(key.toStringWithPrivate());
|
|
||||||
builder.append("\n");
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/** Returns a copy of the current list of chains. */
|
/** Returns a copy of the current list of chains. */
|
||||||
public List<DeterministicKeyChain> getDeterministicKeyChains() {
|
public List<DeterministicKeyChain> getDeterministicKeyChains() {
|
||||||
return new ArrayList<DeterministicKeyChain>(chains);
|
return new ArrayList<DeterministicKeyChain>(chains);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* Returns the number of signatures required to spend transactions for this KeyChainGroup. It's the N from
|
|
||||||
* N-of-M CHECKMULTISIG script for P2SH transactions and always 1 for other transaction types.
|
|
||||||
*/
|
|
||||||
public int getSigsRequiredToSpend() {
|
|
||||||
return sigsRequiredToSpend;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Returns a counter that increases (by an arbitrary amount) each time new keys have been calculated due to
|
* Returns a counter that increases (by an arbitrary amount) each time new keys have been calculated due to
|
||||||
* lookahead and thus the Bloom filter that was previously calculated has become stale.
|
* lookahead and thus the Bloom filter that was previously calculated has become stale.
|
||||||
|
286
core/src/main/java/org/bitcoinj/wallet/MarriedKeyChain.java
Normal file
286
core/src/main/java/org/bitcoinj/wallet/MarriedKeyChain.java
Normal file
@ -0,0 +1,286 @@
|
|||||||
|
/**
|
||||||
|
* Copyright 2013 The bitcoinj developers.
|
||||||
|
*
|
||||||
|
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
* you may not use this file except in compliance with the License.
|
||||||
|
* You may obtain a copy of the License at
|
||||||
|
*
|
||||||
|
* http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
*
|
||||||
|
* Unless required by applicable law or agreed to in writing, software
|
||||||
|
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
* See the License for the specific language governing permissions and
|
||||||
|
* limitations under the License.
|
||||||
|
*/
|
||||||
|
|
||||||
|
package org.bitcoinj.wallet;
|
||||||
|
|
||||||
|
import com.google.common.collect.ImmutableList;
|
||||||
|
import com.google.common.collect.Lists;
|
||||||
|
import com.google.protobuf.ByteString;
|
||||||
|
|
||||||
|
import org.bitcoinj.core.BloomFilter;
|
||||||
|
import org.bitcoinj.core.ECKey;
|
||||||
|
import org.bitcoinj.core.NetworkParameters;
|
||||||
|
import org.bitcoinj.core.Utils;
|
||||||
|
import org.bitcoinj.crypto.DeterministicKey;
|
||||||
|
import org.bitcoinj.crypto.KeyCrypter;
|
||||||
|
import org.bitcoinj.script.Script;
|
||||||
|
import org.bitcoinj.script.ScriptBuilder;
|
||||||
|
|
||||||
|
import java.security.SecureRandom;
|
||||||
|
import java.util.LinkedHashMap;
|
||||||
|
import java.util.List;
|
||||||
|
import java.util.Map;
|
||||||
|
|
||||||
|
import javax.annotation.Nullable;
|
||||||
|
|
||||||
|
import static com.google.common.base.Preconditions.checkArgument;
|
||||||
|
import static com.google.common.base.Preconditions.checkNotNull;
|
||||||
|
import static com.google.common.base.Preconditions.checkState;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* <p>A multi-signature keychain using synchronized HD keys (a.k.a HDM)</p>
|
||||||
|
* <p>This keychain keeps track of following keychains that follow the account key of this keychain.
|
||||||
|
* You can get P2SH addresses to receive coins to from this chain. The threshold - sigsRequiredToSpend
|
||||||
|
* specifies how many signatures required to spend transactions for this married keychain. This value should not exceed
|
||||||
|
* total number of keys involved (one followed key plus number of following keys), otherwise IllegalArgumentException
|
||||||
|
* will be thrown.</p>
|
||||||
|
* <p>IMPORTANT: As of Bitcoin Core 0.9 all multisig transactions which require more than 3 public keys are non-standard
|
||||||
|
* and such spends won't be processed by peers with default settings, essentially making such transactions almost
|
||||||
|
* nonspendable</p>
|
||||||
|
* <p>This method will throw an IllegalStateException, if the keychain is already married or already has leaf keys
|
||||||
|
* issued.</p>
|
||||||
|
*/
|
||||||
|
public class MarriedKeyChain extends DeterministicKeyChain {
|
||||||
|
// The map holds P2SH redeem script and corresponding ECKeys issued by this KeyChainGroup (including lookahead)
|
||||||
|
// mapped to redeem script hashes.
|
||||||
|
private LinkedHashMap<ByteString, RedeemData> marriedKeysRedeemData = new LinkedHashMap<ByteString, RedeemData>();
|
||||||
|
|
||||||
|
private List<DeterministicKeyChain> followingKeyChains;
|
||||||
|
|
||||||
|
/** Builds a {@link MarriedKeyChain} */
|
||||||
|
public static class Builder<T extends Builder<T>> extends DeterministicKeyChain.Builder<T> {
|
||||||
|
private List<DeterministicKey> followingKeys;
|
||||||
|
private int threshold;
|
||||||
|
|
||||||
|
protected Builder() {
|
||||||
|
}
|
||||||
|
|
||||||
|
public T followingKeys(List<DeterministicKey> followingKeys) {
|
||||||
|
this.followingKeys = followingKeys;
|
||||||
|
return self();
|
||||||
|
}
|
||||||
|
|
||||||
|
public T followingKeys(DeterministicKey followingKey, DeterministicKey ...followingKeys) {
|
||||||
|
this.followingKeys = Lists.asList(followingKey, followingKeys);
|
||||||
|
return self();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Threshold, or <code>(followingKeys.size() + 1) / 2 + 1)</code> (majority) if unspecified.</p>
|
||||||
|
* <p>IMPORTANT: As of Bitcoin Core 0.9 all multisig transactions which require more than 3 public keys are non-standard
|
||||||
|
* and such spends won't be processed by peers with default settings, essentially making such transactions almost
|
||||||
|
* nonspendable</p>
|
||||||
|
*/
|
||||||
|
public T threshold(int threshold) {
|
||||||
|
this.threshold = threshold;
|
||||||
|
return self();
|
||||||
|
}
|
||||||
|
|
||||||
|
public MarriedKeyChain build() {
|
||||||
|
checkState(random != null || entropy != null || seed != null, "Must provide either entropy or random");
|
||||||
|
checkNotNull(followingKeys, "followingKeys must be provided");
|
||||||
|
MarriedKeyChain chain;
|
||||||
|
if (threshold == 0)
|
||||||
|
threshold = (followingKeys.size() + 1) / 2 + 1;
|
||||||
|
if (random != null) {
|
||||||
|
chain = new MarriedKeyChain(random, bits, passphrase, seedCreationTimeSecs);
|
||||||
|
} else if (entropy != null) {
|
||||||
|
chain = new MarriedKeyChain(entropy, passphrase, seedCreationTimeSecs);
|
||||||
|
} else {
|
||||||
|
chain = new MarriedKeyChain(seed);
|
||||||
|
}
|
||||||
|
chain.addFollowingAccountKeys(followingKeys, threshold);
|
||||||
|
return chain;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public static Builder<?> builder() {
|
||||||
|
return new Builder();
|
||||||
|
}
|
||||||
|
|
||||||
|
// Protobuf deserialization constructors
|
||||||
|
MarriedKeyChain(DeterministicKey accountKey) {
|
||||||
|
super(accountKey, false);
|
||||||
|
}
|
||||||
|
|
||||||
|
MarriedKeyChain(DeterministicSeed seed, KeyCrypter crypter) {
|
||||||
|
super(seed, crypter);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Builder constructors
|
||||||
|
private MarriedKeyChain(SecureRandom random, int bits, String passphrase, long seedCreationTimeSecs) {
|
||||||
|
super(random, bits, passphrase, seedCreationTimeSecs);
|
||||||
|
}
|
||||||
|
|
||||||
|
private MarriedKeyChain(byte[] entropy, String passphrase, long seedCreationTimeSecs) {
|
||||||
|
super(entropy, passphrase, seedCreationTimeSecs);
|
||||||
|
}
|
||||||
|
|
||||||
|
private MarriedKeyChain(DeterministicSeed seed) {
|
||||||
|
super(seed);
|
||||||
|
}
|
||||||
|
|
||||||
|
void setFollowingKeyChains(List<DeterministicKeyChain> followingKeyChains) {
|
||||||
|
checkArgument(!followingKeyChains.isEmpty());
|
||||||
|
this.followingKeyChains = followingKeyChains;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public boolean isMarried() {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Create a new married key and return the matching output script */
|
||||||
|
@Override
|
||||||
|
public Script freshOutputScript(KeyPurpose purpose) {
|
||||||
|
DeterministicKey followedKey = getKey(purpose);
|
||||||
|
ImmutableList.Builder<ECKey> keys = ImmutableList.<ECKey>builder().add(followedKey);
|
||||||
|
for (DeterministicKeyChain keyChain : followingKeyChains) {
|
||||||
|
DeterministicKey followingKey = keyChain.getKey(purpose);
|
||||||
|
checkState(followedKey.getChildNumber().equals(followingKey.getChildNumber()), "Following keychains should be in sync");
|
||||||
|
keys.add(followingKey);
|
||||||
|
}
|
||||||
|
List<ECKey> marriedKeys = keys.build();
|
||||||
|
Script redeemScript = ScriptBuilder.createRedeemScript(sigsRequiredToSpend, marriedKeys);
|
||||||
|
return ScriptBuilder.createP2SHOutputScript(redeemScript);
|
||||||
|
}
|
||||||
|
|
||||||
|
private List<ECKey> getMarriedKeysWithFollowed(DeterministicKey followedKey) {
|
||||||
|
ImmutableList.Builder<ECKey> keys = ImmutableList.builder();
|
||||||
|
for (DeterministicKeyChain keyChain : followingKeyChains) {
|
||||||
|
keyChain.maybeLookAhead();
|
||||||
|
keys.add(keyChain.getKeyByPath(followedKey.getPath()));
|
||||||
|
}
|
||||||
|
keys.add(followedKey);
|
||||||
|
return keys.build();
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Get the redeem data for a key in this married chain */
|
||||||
|
@Override
|
||||||
|
public RedeemData getRedeemData(DeterministicKey followedKey) {
|
||||||
|
checkState(isMarried());
|
||||||
|
List<ECKey> marriedKeys = getMarriedKeysWithFollowed(followedKey);
|
||||||
|
Script redeemScript = ScriptBuilder.createRedeemScript(sigsRequiredToSpend, marriedKeys);
|
||||||
|
return RedeemData.of(marriedKeys, redeemScript);
|
||||||
|
}
|
||||||
|
|
||||||
|
private void addFollowingAccountKeys(List<DeterministicKey> followingAccountKeys, int sigsRequiredToSpend) {
|
||||||
|
checkArgument(sigsRequiredToSpend <= followingAccountKeys.size() + 1, "Multisig threshold can't exceed total number of keys");
|
||||||
|
checkState(numLeafKeysIssued() == 0, "Active keychain already has keys in use");
|
||||||
|
checkState(followingKeyChains == null);
|
||||||
|
|
||||||
|
List<DeterministicKeyChain> followingKeyChains = Lists.newArrayList();
|
||||||
|
|
||||||
|
for (DeterministicKey key : followingAccountKeys) {
|
||||||
|
checkArgument(key.getPath().size() == 1, "Following keys have to be account keys");
|
||||||
|
DeterministicKeyChain chain = DeterministicKeyChain.watchAndFollow(key);
|
||||||
|
if (lookaheadSize >= 0)
|
||||||
|
chain.setLookaheadSize(lookaheadSize);
|
||||||
|
if (lookaheadThreshold >= 0)
|
||||||
|
chain.setLookaheadThreshold(lookaheadThreshold);
|
||||||
|
followingKeyChains.add(chain);
|
||||||
|
}
|
||||||
|
|
||||||
|
this.sigsRequiredToSpend = sigsRequiredToSpend;
|
||||||
|
this.followingKeyChains = followingKeyChains;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void setLookaheadSize(int lookaheadSize) {
|
||||||
|
lock.lock();
|
||||||
|
try {
|
||||||
|
super.setLookaheadSize(lookaheadSize);
|
||||||
|
if (followingKeyChains != null) {
|
||||||
|
for (DeterministicKeyChain followingChain : followingKeyChains) {
|
||||||
|
followingChain.setLookaheadSize(lookaheadSize);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} finally {
|
||||||
|
lock.unlock();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
protected void beforeSerializeToProtobuf(List<Protos.Key> result) {
|
||||||
|
super.beforeSerializeToProtobuf(result);
|
||||||
|
for (DeterministicKeyChain chain : followingKeyChains) {
|
||||||
|
result.addAll(chain.serializeMyselfToProtobuf());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
protected void formatAddresses(boolean includePrivateKeys, NetworkParameters params, StringBuilder builder2) {
|
||||||
|
for (DeterministicKeyChain followingChain : followingKeyChains) {
|
||||||
|
builder2.append(String.format("Following chain: %s%n", followingChain.getWatchingKey().serializePubB58()));
|
||||||
|
}
|
||||||
|
builder2.append(String.format("%n"));
|
||||||
|
for (RedeemData redeemData : marriedKeysRedeemData.values())
|
||||||
|
formatScript(ScriptBuilder.createP2SHOutputScript(redeemData.redeemScript), builder2, params);
|
||||||
|
}
|
||||||
|
|
||||||
|
private void formatScript(Script script, StringBuilder builder, NetworkParameters params) {
|
||||||
|
builder.append(" addr:");
|
||||||
|
builder.append(script.getToAddress(params));
|
||||||
|
builder.append(" hash160:");
|
||||||
|
builder.append(Utils.HEX.encode(script.getPubKeyHash()));
|
||||||
|
builder.append("\n");
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void maybeLookAheadScripts() {
|
||||||
|
super.maybeLookAheadScripts();
|
||||||
|
int numLeafKeys = getLeafKeys().size();
|
||||||
|
|
||||||
|
checkState(marriedKeysRedeemData.size() <= numLeafKeys, "Number of scripts is greater than number of leaf keys");
|
||||||
|
if (marriedKeysRedeemData.size() == numLeafKeys)
|
||||||
|
return;
|
||||||
|
|
||||||
|
maybeLookAhead();
|
||||||
|
for (DeterministicKey followedKey : getLeafKeys()) {
|
||||||
|
RedeemData redeemData = getRedeemData(followedKey);
|
||||||
|
Script scriptPubKey = ScriptBuilder.createP2SHOutputScript(redeemData.redeemScript);
|
||||||
|
marriedKeysRedeemData.put(ByteString.copyFrom(scriptPubKey.getPubKeyHash()), redeemData);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Nullable
|
||||||
|
@Override
|
||||||
|
public RedeemData findRedeemDataByScriptHash(ByteString bytes) {
|
||||||
|
return marriedKeysRedeemData.get(bytes);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public BloomFilter getFilter(int size, double falsePositiveRate, long tweak) {
|
||||||
|
lock.lock();
|
||||||
|
BloomFilter filter;
|
||||||
|
try {
|
||||||
|
filter = new BloomFilter(size, falsePositiveRate, tweak);
|
||||||
|
for (Map.Entry<ByteString, RedeemData> entry : marriedKeysRedeemData.entrySet()) {
|
||||||
|
filter.insert(entry.getKey().toByteArray());
|
||||||
|
filter.insert(entry.getValue().redeemScript.getProgram());
|
||||||
|
}
|
||||||
|
} finally {
|
||||||
|
lock.unlock();
|
||||||
|
}
|
||||||
|
return filter;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public int numBloomFilterEntries() {
|
||||||
|
maybeLookAhead();
|
||||||
|
return getLeafKeys().size() * 2;
|
||||||
|
}
|
||||||
|
}
|
@ -1269,6 +1269,26 @@ public final class Protos {
|
|||||||
* </pre>
|
* </pre>
|
||||||
*/
|
*/
|
||||||
boolean getIsFollowing();
|
boolean getIsFollowing();
|
||||||
|
|
||||||
|
// optional uint32 sigsRequiredToSpend = 6 [default = 1];
|
||||||
|
/**
|
||||||
|
* <code>optional uint32 sigsRequiredToSpend = 6 [default = 1];</code>
|
||||||
|
*
|
||||||
|
* <pre>
|
||||||
|
* Number of signatures required to spend. This field is needed only for married keychains to reconstruct KeyChain
|
||||||
|
* and represents the N value from N-of-M CHECKMULTISIG script. For regular single keychains it will always be 1.
|
||||||
|
* </pre>
|
||||||
|
*/
|
||||||
|
boolean hasSigsRequiredToSpend();
|
||||||
|
/**
|
||||||
|
* <code>optional uint32 sigsRequiredToSpend = 6 [default = 1];</code>
|
||||||
|
*
|
||||||
|
* <pre>
|
||||||
|
* Number of signatures required to spend. This field is needed only for married keychains to reconstruct KeyChain
|
||||||
|
* and represents the N value from N-of-M CHECKMULTISIG script. For regular single keychains it will always be 1.
|
||||||
|
* </pre>
|
||||||
|
*/
|
||||||
|
int getSigsRequiredToSpend();
|
||||||
}
|
}
|
||||||
/**
|
/**
|
||||||
* Protobuf type {@code wallet.DeterministicKey}
|
* Protobuf type {@code wallet.DeterministicKey}
|
||||||
@ -1367,6 +1387,11 @@ public final class Protos {
|
|||||||
isFollowing_ = input.readBool();
|
isFollowing_ = input.readBool();
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
|
case 48: {
|
||||||
|
bitField0_ |= 0x00000010;
|
||||||
|
sigsRequiredToSpend_ = input.readUInt32();
|
||||||
|
break;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
} catch (com.google.protobuf.InvalidProtocolBufferException e) {
|
} catch (com.google.protobuf.InvalidProtocolBufferException e) {
|
||||||
@ -1554,12 +1579,39 @@ public final class Protos {
|
|||||||
return isFollowing_;
|
return isFollowing_;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// optional uint32 sigsRequiredToSpend = 6 [default = 1];
|
||||||
|
public static final int SIGSREQUIREDTOSPEND_FIELD_NUMBER = 6;
|
||||||
|
private int sigsRequiredToSpend_;
|
||||||
|
/**
|
||||||
|
* <code>optional uint32 sigsRequiredToSpend = 6 [default = 1];</code>
|
||||||
|
*
|
||||||
|
* <pre>
|
||||||
|
* Number of signatures required to spend. This field is needed only for married keychains to reconstruct KeyChain
|
||||||
|
* and represents the N value from N-of-M CHECKMULTISIG script. For regular single keychains it will always be 1.
|
||||||
|
* </pre>
|
||||||
|
*/
|
||||||
|
public boolean hasSigsRequiredToSpend() {
|
||||||
|
return ((bitField0_ & 0x00000010) == 0x00000010);
|
||||||
|
}
|
||||||
|
/**
|
||||||
|
* <code>optional uint32 sigsRequiredToSpend = 6 [default = 1];</code>
|
||||||
|
*
|
||||||
|
* <pre>
|
||||||
|
* Number of signatures required to spend. This field is needed only for married keychains to reconstruct KeyChain
|
||||||
|
* and represents the N value from N-of-M CHECKMULTISIG script. For regular single keychains it will always be 1.
|
||||||
|
* </pre>
|
||||||
|
*/
|
||||||
|
public int getSigsRequiredToSpend() {
|
||||||
|
return sigsRequiredToSpend_;
|
||||||
|
}
|
||||||
|
|
||||||
private void initFields() {
|
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;
|
isFollowing_ = false;
|
||||||
|
sigsRequiredToSpend_ = 1;
|
||||||
}
|
}
|
||||||
private byte memoizedIsInitialized = -1;
|
private byte memoizedIsInitialized = -1;
|
||||||
public final boolean isInitialized() {
|
public final boolean isInitialized() {
|
||||||
@ -1592,6 +1644,9 @@ public final class Protos {
|
|||||||
if (((bitField0_ & 0x00000008) == 0x00000008)) {
|
if (((bitField0_ & 0x00000008) == 0x00000008)) {
|
||||||
output.writeBool(5, isFollowing_);
|
output.writeBool(5, isFollowing_);
|
||||||
}
|
}
|
||||||
|
if (((bitField0_ & 0x00000010) == 0x00000010)) {
|
||||||
|
output.writeUInt32(6, sigsRequiredToSpend_);
|
||||||
|
}
|
||||||
getUnknownFields().writeTo(output);
|
getUnknownFields().writeTo(output);
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -1626,6 +1681,10 @@ public final class Protos {
|
|||||||
size += com.google.protobuf.CodedOutputStream
|
size += com.google.protobuf.CodedOutputStream
|
||||||
.computeBoolSize(5, isFollowing_);
|
.computeBoolSize(5, isFollowing_);
|
||||||
}
|
}
|
||||||
|
if (((bitField0_ & 0x00000010) == 0x00000010)) {
|
||||||
|
size += com.google.protobuf.CodedOutputStream
|
||||||
|
.computeUInt32Size(6, sigsRequiredToSpend_);
|
||||||
|
}
|
||||||
size += getUnknownFields().getSerializedSize();
|
size += getUnknownFields().getSerializedSize();
|
||||||
memoizedSerializedSize = size;
|
memoizedSerializedSize = size;
|
||||||
return size;
|
return size;
|
||||||
@ -1757,6 +1816,8 @@ public final class Protos {
|
|||||||
bitField0_ = (bitField0_ & ~0x00000008);
|
bitField0_ = (bitField0_ & ~0x00000008);
|
||||||
isFollowing_ = false;
|
isFollowing_ = false;
|
||||||
bitField0_ = (bitField0_ & ~0x00000010);
|
bitField0_ = (bitField0_ & ~0x00000010);
|
||||||
|
sigsRequiredToSpend_ = 1;
|
||||||
|
bitField0_ = (bitField0_ & ~0x00000020);
|
||||||
return this;
|
return this;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -1806,6 +1867,10 @@ public final class Protos {
|
|||||||
to_bitField0_ |= 0x00000008;
|
to_bitField0_ |= 0x00000008;
|
||||||
}
|
}
|
||||||
result.isFollowing_ = isFollowing_;
|
result.isFollowing_ = isFollowing_;
|
||||||
|
if (((from_bitField0_ & 0x00000020) == 0x00000020)) {
|
||||||
|
to_bitField0_ |= 0x00000010;
|
||||||
|
}
|
||||||
|
result.sigsRequiredToSpend_ = sigsRequiredToSpend_;
|
||||||
result.bitField0_ = to_bitField0_;
|
result.bitField0_ = to_bitField0_;
|
||||||
onBuilt();
|
onBuilt();
|
||||||
return result;
|
return result;
|
||||||
@ -1844,6 +1909,9 @@ public final class Protos {
|
|||||||
if (other.hasIsFollowing()) {
|
if (other.hasIsFollowing()) {
|
||||||
setIsFollowing(other.getIsFollowing());
|
setIsFollowing(other.getIsFollowing());
|
||||||
}
|
}
|
||||||
|
if (other.hasSigsRequiredToSpend()) {
|
||||||
|
setSigsRequiredToSpend(other.getSigsRequiredToSpend());
|
||||||
|
}
|
||||||
this.mergeUnknownFields(other.getUnknownFields());
|
this.mergeUnknownFields(other.getUnknownFields());
|
||||||
return this;
|
return this;
|
||||||
}
|
}
|
||||||
@ -2195,6 +2263,59 @@ public final class Protos {
|
|||||||
return this;
|
return this;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// optional uint32 sigsRequiredToSpend = 6 [default = 1];
|
||||||
|
private int sigsRequiredToSpend_ = 1;
|
||||||
|
/**
|
||||||
|
* <code>optional uint32 sigsRequiredToSpend = 6 [default = 1];</code>
|
||||||
|
*
|
||||||
|
* <pre>
|
||||||
|
* Number of signatures required to spend. This field is needed only for married keychains to reconstruct KeyChain
|
||||||
|
* and represents the N value from N-of-M CHECKMULTISIG script. For regular single keychains it will always be 1.
|
||||||
|
* </pre>
|
||||||
|
*/
|
||||||
|
public boolean hasSigsRequiredToSpend() {
|
||||||
|
return ((bitField0_ & 0x00000020) == 0x00000020);
|
||||||
|
}
|
||||||
|
/**
|
||||||
|
* <code>optional uint32 sigsRequiredToSpend = 6 [default = 1];</code>
|
||||||
|
*
|
||||||
|
* <pre>
|
||||||
|
* Number of signatures required to spend. This field is needed only for married keychains to reconstruct KeyChain
|
||||||
|
* and represents the N value from N-of-M CHECKMULTISIG script. For regular single keychains it will always be 1.
|
||||||
|
* </pre>
|
||||||
|
*/
|
||||||
|
public int getSigsRequiredToSpend() {
|
||||||
|
return sigsRequiredToSpend_;
|
||||||
|
}
|
||||||
|
/**
|
||||||
|
* <code>optional uint32 sigsRequiredToSpend = 6 [default = 1];</code>
|
||||||
|
*
|
||||||
|
* <pre>
|
||||||
|
* Number of signatures required to spend. This field is needed only for married keychains to reconstruct KeyChain
|
||||||
|
* and represents the N value from N-of-M CHECKMULTISIG script. For regular single keychains it will always be 1.
|
||||||
|
* </pre>
|
||||||
|
*/
|
||||||
|
public Builder setSigsRequiredToSpend(int value) {
|
||||||
|
bitField0_ |= 0x00000020;
|
||||||
|
sigsRequiredToSpend_ = value;
|
||||||
|
onChanged();
|
||||||
|
return this;
|
||||||
|
}
|
||||||
|
/**
|
||||||
|
* <code>optional uint32 sigsRequiredToSpend = 6 [default = 1];</code>
|
||||||
|
*
|
||||||
|
* <pre>
|
||||||
|
* Number of signatures required to spend. This field is needed only for married keychains to reconstruct KeyChain
|
||||||
|
* and represents the N value from N-of-M CHECKMULTISIG script. For regular single keychains it will always be 1.
|
||||||
|
* </pre>
|
||||||
|
*/
|
||||||
|
public Builder clearSigsRequiredToSpend() {
|
||||||
|
bitField0_ = (bitField0_ & ~0x00000020);
|
||||||
|
sigsRequiredToSpend_ = 1;
|
||||||
|
onChanged();
|
||||||
|
return this;
|
||||||
|
}
|
||||||
|
|
||||||
// @@protoc_insertion_point(builder_scope:wallet.DeterministicKey)
|
// @@protoc_insertion_point(builder_scope:wallet.DeterministicKey)
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -14291,26 +14412,6 @@ public final class Protos {
|
|||||||
*/
|
*/
|
||||||
org.bitcoinj.wallet.Protos.TransactionSignerOrBuilder getTransactionSignersOrBuilder(
|
org.bitcoinj.wallet.Protos.TransactionSignerOrBuilder getTransactionSignersOrBuilder(
|
||||||
int index);
|
int index);
|
||||||
|
|
||||||
// optional uint32 sigsRequiredToSpend = 18 [default = 1];
|
|
||||||
/**
|
|
||||||
* <code>optional uint32 sigsRequiredToSpend = 18 [default = 1];</code>
|
|
||||||
*
|
|
||||||
* <pre>
|
|
||||||
* Number of signatures required to spend. This field is needed only for married wallets to reconstruct KeyChainGroup
|
|
||||||
* and represents the N value from N-of-M CHECKMULTISIG script. For regular single wallets it will always be 1.
|
|
||||||
* </pre>
|
|
||||||
*/
|
|
||||||
boolean hasSigsRequiredToSpend();
|
|
||||||
/**
|
|
||||||
* <code>optional uint32 sigsRequiredToSpend = 18 [default = 1];</code>
|
|
||||||
*
|
|
||||||
* <pre>
|
|
||||||
* Number of signatures required to spend. This field is needed only for married wallets to reconstruct KeyChainGroup
|
|
||||||
* and represents the N value from N-of-M CHECKMULTISIG script. For regular single wallets it will always be 1.
|
|
||||||
* </pre>
|
|
||||||
*/
|
|
||||||
int getSigsRequiredToSpend();
|
|
||||||
}
|
}
|
||||||
/**
|
/**
|
||||||
* Protobuf type {@code wallet.Wallet}
|
* Protobuf type {@code wallet.Wallet}
|
||||||
@ -14474,11 +14575,6 @@ public final class Protos {
|
|||||||
transactionSigners_.add(input.readMessage(org.bitcoinj.wallet.Protos.TransactionSigner.PARSER, extensionRegistry));
|
transactionSigners_.add(input.readMessage(org.bitcoinj.wallet.Protos.TransactionSigner.PARSER, extensionRegistry));
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
case 144: {
|
|
||||||
bitField0_ |= 0x00000200;
|
|
||||||
sigsRequiredToSpend_ = input.readUInt32();
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
} catch (com.google.protobuf.InvalidProtocolBufferException e) {
|
} catch (com.google.protobuf.InvalidProtocolBufferException e) {
|
||||||
@ -15146,32 +15242,6 @@ public final class Protos {
|
|||||||
return transactionSigners_.get(index);
|
return transactionSigners_.get(index);
|
||||||
}
|
}
|
||||||
|
|
||||||
// optional uint32 sigsRequiredToSpend = 18 [default = 1];
|
|
||||||
public static final int SIGSREQUIREDTOSPEND_FIELD_NUMBER = 18;
|
|
||||||
private int sigsRequiredToSpend_;
|
|
||||||
/**
|
|
||||||
* <code>optional uint32 sigsRequiredToSpend = 18 [default = 1];</code>
|
|
||||||
*
|
|
||||||
* <pre>
|
|
||||||
* Number of signatures required to spend. This field is needed only for married wallets to reconstruct KeyChainGroup
|
|
||||||
* and represents the N value from N-of-M CHECKMULTISIG script. For regular single wallets it will always be 1.
|
|
||||||
* </pre>
|
|
||||||
*/
|
|
||||||
public boolean hasSigsRequiredToSpend() {
|
|
||||||
return ((bitField0_ & 0x00000200) == 0x00000200);
|
|
||||||
}
|
|
||||||
/**
|
|
||||||
* <code>optional uint32 sigsRequiredToSpend = 18 [default = 1];</code>
|
|
||||||
*
|
|
||||||
* <pre>
|
|
||||||
* Number of signatures required to spend. This field is needed only for married wallets to reconstruct KeyChainGroup
|
|
||||||
* and represents the N value from N-of-M CHECKMULTISIG script. For regular single wallets it will always be 1.
|
|
||||||
* </pre>
|
|
||||||
*/
|
|
||||||
public int getSigsRequiredToSpend() {
|
|
||||||
return sigsRequiredToSpend_;
|
|
||||||
}
|
|
||||||
|
|
||||||
private void initFields() {
|
private void initFields() {
|
||||||
networkIdentifier_ = "";
|
networkIdentifier_ = "";
|
||||||
lastSeenBlockHash_ = com.google.protobuf.ByteString.EMPTY;
|
lastSeenBlockHash_ = com.google.protobuf.ByteString.EMPTY;
|
||||||
@ -15188,7 +15258,6 @@ public final class Protos {
|
|||||||
keyRotationTime_ = 0L;
|
keyRotationTime_ = 0L;
|
||||||
tags_ = java.util.Collections.emptyList();
|
tags_ = java.util.Collections.emptyList();
|
||||||
transactionSigners_ = java.util.Collections.emptyList();
|
transactionSigners_ = java.util.Collections.emptyList();
|
||||||
sigsRequiredToSpend_ = 1;
|
|
||||||
}
|
}
|
||||||
private byte memoizedIsInitialized = -1;
|
private byte memoizedIsInitialized = -1;
|
||||||
public final boolean isInitialized() {
|
public final boolean isInitialized() {
|
||||||
@ -15293,9 +15362,6 @@ public final class Protos {
|
|||||||
for (int i = 0; i < transactionSigners_.size(); i++) {
|
for (int i = 0; i < transactionSigners_.size(); i++) {
|
||||||
output.writeMessage(17, transactionSigners_.get(i));
|
output.writeMessage(17, transactionSigners_.get(i));
|
||||||
}
|
}
|
||||||
if (((bitField0_ & 0x00000200) == 0x00000200)) {
|
|
||||||
output.writeUInt32(18, sigsRequiredToSpend_);
|
|
||||||
}
|
|
||||||
getUnknownFields().writeTo(output);
|
getUnknownFields().writeTo(output);
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -15365,10 +15431,6 @@ public final class Protos {
|
|||||||
size += com.google.protobuf.CodedOutputStream
|
size += com.google.protobuf.CodedOutputStream
|
||||||
.computeMessageSize(17, transactionSigners_.get(i));
|
.computeMessageSize(17, transactionSigners_.get(i));
|
||||||
}
|
}
|
||||||
if (((bitField0_ & 0x00000200) == 0x00000200)) {
|
|
||||||
size += com.google.protobuf.CodedOutputStream
|
|
||||||
.computeUInt32Size(18, sigsRequiredToSpend_);
|
|
||||||
}
|
|
||||||
size += getUnknownFields().getSerializedSize();
|
size += getUnknownFields().getSerializedSize();
|
||||||
memoizedSerializedSize = size;
|
memoizedSerializedSize = size;
|
||||||
return size;
|
return size;
|
||||||
@ -15554,8 +15616,6 @@ public final class Protos {
|
|||||||
} else {
|
} else {
|
||||||
transactionSignersBuilder_.clear();
|
transactionSignersBuilder_.clear();
|
||||||
}
|
}
|
||||||
sigsRequiredToSpend_ = 1;
|
|
||||||
bitField0_ = (bitField0_ & ~0x00008000);
|
|
||||||
return this;
|
return this;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -15678,10 +15738,6 @@ public final class Protos {
|
|||||||
} else {
|
} else {
|
||||||
result.transactionSigners_ = transactionSignersBuilder_.build();
|
result.transactionSigners_ = transactionSignersBuilder_.build();
|
||||||
}
|
}
|
||||||
if (((from_bitField0_ & 0x00008000) == 0x00008000)) {
|
|
||||||
to_bitField0_ |= 0x00000200;
|
|
||||||
}
|
|
||||||
result.sigsRequiredToSpend_ = sigsRequiredToSpend_;
|
|
||||||
result.bitField0_ = to_bitField0_;
|
result.bitField0_ = to_bitField0_;
|
||||||
onBuilt();
|
onBuilt();
|
||||||
return result;
|
return result;
|
||||||
@ -15885,9 +15941,6 @@ public final class Protos {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
if (other.hasSigsRequiredToSpend()) {
|
|
||||||
setSigsRequiredToSpend(other.getSigsRequiredToSpend());
|
|
||||||
}
|
|
||||||
this.mergeUnknownFields(other.getUnknownFields());
|
this.mergeUnknownFields(other.getUnknownFields());
|
||||||
return this;
|
return this;
|
||||||
}
|
}
|
||||||
@ -18066,59 +18119,6 @@ public final class Protos {
|
|||||||
return transactionSignersBuilder_;
|
return transactionSignersBuilder_;
|
||||||
}
|
}
|
||||||
|
|
||||||
// optional uint32 sigsRequiredToSpend = 18 [default = 1];
|
|
||||||
private int sigsRequiredToSpend_ = 1;
|
|
||||||
/**
|
|
||||||
* <code>optional uint32 sigsRequiredToSpend = 18 [default = 1];</code>
|
|
||||||
*
|
|
||||||
* <pre>
|
|
||||||
* Number of signatures required to spend. This field is needed only for married wallets to reconstruct KeyChainGroup
|
|
||||||
* and represents the N value from N-of-M CHECKMULTISIG script. For regular single wallets it will always be 1.
|
|
||||||
* </pre>
|
|
||||||
*/
|
|
||||||
public boolean hasSigsRequiredToSpend() {
|
|
||||||
return ((bitField0_ & 0x00008000) == 0x00008000);
|
|
||||||
}
|
|
||||||
/**
|
|
||||||
* <code>optional uint32 sigsRequiredToSpend = 18 [default = 1];</code>
|
|
||||||
*
|
|
||||||
* <pre>
|
|
||||||
* Number of signatures required to spend. This field is needed only for married wallets to reconstruct KeyChainGroup
|
|
||||||
* and represents the N value from N-of-M CHECKMULTISIG script. For regular single wallets it will always be 1.
|
|
||||||
* </pre>
|
|
||||||
*/
|
|
||||||
public int getSigsRequiredToSpend() {
|
|
||||||
return sigsRequiredToSpend_;
|
|
||||||
}
|
|
||||||
/**
|
|
||||||
* <code>optional uint32 sigsRequiredToSpend = 18 [default = 1];</code>
|
|
||||||
*
|
|
||||||
* <pre>
|
|
||||||
* Number of signatures required to spend. This field is needed only for married wallets to reconstruct KeyChainGroup
|
|
||||||
* and represents the N value from N-of-M CHECKMULTISIG script. For regular single wallets it will always be 1.
|
|
||||||
* </pre>
|
|
||||||
*/
|
|
||||||
public Builder setSigsRequiredToSpend(int value) {
|
|
||||||
bitField0_ |= 0x00008000;
|
|
||||||
sigsRequiredToSpend_ = value;
|
|
||||||
onChanged();
|
|
||||||
return this;
|
|
||||||
}
|
|
||||||
/**
|
|
||||||
* <code>optional uint32 sigsRequiredToSpend = 18 [default = 1];</code>
|
|
||||||
*
|
|
||||||
* <pre>
|
|
||||||
* Number of signatures required to spend. This field is needed only for married wallets to reconstruct KeyChainGroup
|
|
||||||
* and represents the N value from N-of-M CHECKMULTISIG script. For regular single wallets it will always be 1.
|
|
||||||
* </pre>
|
|
||||||
*/
|
|
||||||
public Builder clearSigsRequiredToSpend() {
|
|
||||||
bitField0_ = (bitField0_ & ~0x00008000);
|
|
||||||
sigsRequiredToSpend_ = 1;
|
|
||||||
onChanged();
|
|
||||||
return this;
|
|
||||||
}
|
|
||||||
|
|
||||||
// @@protoc_insertion_point(builder_scope:wallet.Wallet)
|
// @@protoc_insertion_point(builder_scope:wallet.Wallet)
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -18995,81 +18995,81 @@ public final class Protos {
|
|||||||
"\nip_address\030\001 \002(\014\022\014\n\004port\030\002 \002(\r\022\020\n\010servi" +
|
"\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\"y\n\020DeterministicKey\022\022\n\nchain_co" +
|
"ey\030\002 \002(\014\"\231\001\n\020DeterministicKey\022\022\n\nchain_c" +
|
||||||
"de\030\001 \002(\014\022\014\n\004path\030\002 \003(\r\022\026\n\016issued_subkeys" +
|
"ode\030\001 \002(\014\022\014\n\004path\030\002 \003(\r\022\026\n\016issued_subkey" +
|
||||||
"\030\003 \001(\r\022\026\n\016lookahead_size\030\004 \001(\r\022\023\n\013isFoll" +
|
"s\030\003 \001(\r\022\026\n\016lookahead_size\030\004 \001(\r\022\023\n\013isFol" +
|
||||||
"owing\030\005 \001(\010\"\232\003\n\003Key\022\036\n\004type\030\001 \002(\0162\020.wall" +
|
"lowing\030\005 \001(\010\022\036\n\023sigsRequiredToSpend\030\006 \001(" +
|
||||||
"et.Key.Type\022\024\n\014secret_bytes\030\002 \001(\014\022-\n\016enc" +
|
"\r:\0011\"\232\003\n\003Key\022\036\n\004type\030\001 \002(\0162\020.wallet.Key." +
|
||||||
"rypted_data\030\006 \001(\0132\025.wallet.EncryptedData",
|
"Type\022\024\n\014secret_bytes\030\002 \001(\014\022-\n\016encrypted_",
|
||||||
"\022\022\n\npublic_key\030\003 \001(\014\022\r\n\005label\030\004 \001(\t\022\032\n\022c" +
|
"data\030\006 \001(\0132\025.wallet.EncryptedData\022\022\n\npub" +
|
||||||
"reation_timestamp\030\005 \001(\003\0223\n\021deterministic" +
|
"lic_key\030\003 \001(\014\022\r\n\005label\030\004 \001(\t\022\032\n\022creation" +
|
||||||
"_key\030\007 \001(\0132\030.wallet.DeterministicKey\022\032\n\022" +
|
"_timestamp\030\005 \001(\003\0223\n\021deterministic_key\030\007 " +
|
||||||
"deterministic_seed\030\010 \001(\014\022;\n\034encrypted_de" +
|
"\001(\0132\030.wallet.DeterministicKey\022\032\n\022determi" +
|
||||||
"terministic_seed\030\t \001(\0132\025.wallet.Encrypte" +
|
"nistic_seed\030\010 \001(\014\022;\n\034encrypted_determini" +
|
||||||
"dData\"a\n\004Type\022\014\n\010ORIGINAL\020\001\022\030\n\024ENCRYPTED" +
|
"stic_seed\030\t \001(\0132\025.wallet.EncryptedData\"a" +
|
||||||
"_SCRYPT_AES\020\002\022\032\n\026DETERMINISTIC_MNEMONIC\020" +
|
"\n\004Type\022\014\n\010ORIGINAL\020\001\022\030\n\024ENCRYPTED_SCRYPT" +
|
||||||
"\003\022\025\n\021DETERMINISTIC_KEY\020\004\"5\n\006Script\022\017\n\007pr" +
|
"_AES\020\002\022\032\n\026DETERMINISTIC_MNEMONIC\020\003\022\025\n\021DE" +
|
||||||
"ogram\030\001 \002(\014\022\032\n\022creation_timestamp\030\002 \002(\003\"" +
|
"TERMINISTIC_KEY\020\004\"5\n\006Script\022\017\n\007program\030\001" +
|
||||||
"\222\001\n\020TransactionInput\022\"\n\032transaction_out_",
|
" \002(\014\022\032\n\022creation_timestamp\030\002 \002(\003\"\222\001\n\020Tra",
|
||||||
"point_hash\030\001 \002(\014\022#\n\033transaction_out_poin" +
|
"nsactionInput\022\"\n\032transaction_out_point_h" +
|
||||||
"t_index\030\002 \002(\r\022\024\n\014script_bytes\030\003 \002(\014\022\020\n\010s" +
|
"ash\030\001 \002(\014\022#\n\033transaction_out_point_index" +
|
||||||
"equence\030\004 \001(\r\022\r\n\005value\030\005 \001(\003\"\177\n\021Transact" +
|
"\030\002 \002(\r\022\024\n\014script_bytes\030\003 \002(\014\022\020\n\010sequence" +
|
||||||
"ionOutput\022\r\n\005value\030\001 \002(\003\022\024\n\014script_bytes" +
|
"\030\004 \001(\r\022\r\n\005value\030\005 \001(\003\"\177\n\021TransactionOutp" +
|
||||||
"\030\002 \002(\014\022!\n\031spent_by_transaction_hash\030\003 \001(" +
|
"ut\022\r\n\005value\030\001 \002(\003\022\024\n\014script_bytes\030\002 \002(\014\022" +
|
||||||
"\014\022\"\n\032spent_by_transaction_index\030\004 \001(\005\"\211\003" +
|
"!\n\031spent_by_transaction_hash\030\003 \001(\014\022\"\n\032sp" +
|
||||||
"\n\025TransactionConfidence\0220\n\004type\030\001 \001(\0162\"." +
|
"ent_by_transaction_index\030\004 \001(\005\"\211\003\n\025Trans" +
|
||||||
"wallet.TransactionConfidence.Type\022\032\n\022app" +
|
"actionConfidence\0220\n\004type\030\001 \001(\0162\".wallet." +
|
||||||
"eared_at_height\030\002 \001(\005\022\036\n\026overriding_tran" +
|
"TransactionConfidence.Type\022\032\n\022appeared_a" +
|
||||||
"saction\030\003 \001(\014\022\r\n\005depth\030\004 \001(\005\022)\n\014broadcas",
|
"t_height\030\002 \001(\005\022\036\n\026overriding_transaction",
|
||||||
"t_by\030\006 \003(\0132\023.wallet.PeerAddress\0224\n\006sourc" +
|
"\030\003 \001(\014\022\r\n\005depth\030\004 \001(\005\022)\n\014broadcast_by\030\006 " +
|
||||||
"e\030\007 \001(\0162$.wallet.TransactionConfidence.S" +
|
"\003(\0132\023.wallet.PeerAddress\0224\n\006source\030\007 \001(\016" +
|
||||||
"ource\"O\n\004Type\022\013\n\007UNKNOWN\020\000\022\014\n\010BUILDING\020\001" +
|
"2$.wallet.TransactionConfidence.Source\"O" +
|
||||||
"\022\013\n\007PENDING\020\002\022\025\n\021NOT_IN_BEST_CHAIN\020\003\022\010\n\004" +
|
"\n\004Type\022\013\n\007UNKNOWN\020\000\022\014\n\010BUILDING\020\001\022\013\n\007PEN" +
|
||||||
"DEAD\020\004\"A\n\006Source\022\022\n\016SOURCE_UNKNOWN\020\000\022\022\n\016" +
|
"DING\020\002\022\025\n\021NOT_IN_BEST_CHAIN\020\003\022\010\n\004DEAD\020\004\"" +
|
||||||
"SOURCE_NETWORK\020\001\022\017\n\013SOURCE_SELF\020\002\"\264\005\n\013Tr" +
|
"A\n\006Source\022\022\n\016SOURCE_UNKNOWN\020\000\022\022\n\016SOURCE_" +
|
||||||
"ansaction\022\017\n\007version\030\001 \002(\005\022\014\n\004hash\030\002 \002(\014" +
|
"NETWORK\020\001\022\017\n\013SOURCE_SELF\020\002\"\264\005\n\013Transacti" +
|
||||||
"\022&\n\004pool\030\003 \001(\0162\030.wallet.Transaction.Pool" +
|
"on\022\017\n\007version\030\001 \002(\005\022\014\n\004hash\030\002 \002(\014\022&\n\004poo" +
|
||||||
"\022\021\n\tlock_time\030\004 \001(\r\022\022\n\nupdated_at\030\005 \001(\003\022" +
|
"l\030\003 \001(\0162\030.wallet.Transaction.Pool\022\021\n\tloc" +
|
||||||
"3\n\021transaction_input\030\006 \003(\0132\030.wallet.Tran",
|
"k_time\030\004 \001(\r\022\022\n\nupdated_at\030\005 \001(\003\0223\n\021tran",
|
||||||
"sactionInput\0225\n\022transaction_output\030\007 \003(\013" +
|
"saction_input\030\006 \003(\0132\030.wallet.Transaction" +
|
||||||
"2\031.wallet.TransactionOutput\022\022\n\nblock_has" +
|
"Input\0225\n\022transaction_output\030\007 \003(\0132\031.wall" +
|
||||||
"h\030\010 \003(\014\022 \n\030block_relativity_offsets\030\013 \003(" +
|
"et.TransactionOutput\022\022\n\nblock_hash\030\010 \003(\014" +
|
||||||
"\005\0221\n\nconfidence\030\t \001(\0132\035.wallet.Transacti" +
|
"\022 \n\030block_relativity_offsets\030\013 \003(\005\0221\n\nco" +
|
||||||
"onConfidence\0225\n\007purpose\030\n \001(\0162\033.wallet.T" +
|
"nfidence\030\t \001(\0132\035.wallet.TransactionConfi" +
|
||||||
"ransaction.Purpose:\007UNKNOWN\022+\n\rexchange_" +
|
"dence\0225\n\007purpose\030\n \001(\0162\033.wallet.Transact" +
|
||||||
"rate\030\014 \001(\0132\024.wallet.ExchangeRate\022\014\n\004memo" +
|
"ion.Purpose:\007UNKNOWN\022+\n\rexchange_rate\030\014 " +
|
||||||
"\030\r \001(\t\"Y\n\004Pool\022\013\n\007UNSPENT\020\004\022\t\n\005SPENT\020\005\022\014" +
|
"\001(\0132\024.wallet.ExchangeRate\022\014\n\004memo\030\r \001(\t\"" +
|
||||||
"\n\010INACTIVE\020\002\022\010\n\004DEAD\020\n\022\013\n\007PENDING\020\020\022\024\n\020P" +
|
"Y\n\004Pool\022\013\n\007UNSPENT\020\004\022\t\n\005SPENT\020\005\022\014\n\010INACT" +
|
||||||
"ENDING_INACTIVE\020\022\"\224\001\n\007Purpose\022\013\n\007UNKNOWN",
|
"IVE\020\002\022\010\n\004DEAD\020\n\022\013\n\007PENDING\020\020\022\024\n\020PENDING_",
|
||||||
"\020\000\022\020\n\014USER_PAYMENT\020\001\022\020\n\014KEY_ROTATION\020\002\022\034" +
|
"INACTIVE\020\022\"\224\001\n\007Purpose\022\013\n\007UNKNOWN\020\000\022\020\n\014U" +
|
||||||
"\n\030ASSURANCE_CONTRACT_CLAIM\020\003\022\035\n\031ASSURANC" +
|
"SER_PAYMENT\020\001\022\020\n\014KEY_ROTATION\020\002\022\034\n\030ASSUR" +
|
||||||
"E_CONTRACT_PLEDGE\020\004\022\033\n\027ASSURANCE_CONTRAC" +
|
"ANCE_CONTRACT_CLAIM\020\003\022\035\n\031ASSURANCE_CONTR" +
|
||||||
"T_STUB\020\005\"N\n\020ScryptParameters\022\014\n\004salt\030\001 \002" +
|
"ACT_PLEDGE\020\004\022\033\n\027ASSURANCE_CONTRACT_STUB\020" +
|
||||||
"(\014\022\020\n\001n\030\002 \001(\003:\00516384\022\014\n\001r\030\003 \001(\005:\0018\022\014\n\001p\030" +
|
"\005\"N\n\020ScryptParameters\022\014\n\004salt\030\001 \002(\014\022\020\n\001n" +
|
||||||
"\004 \001(\005:\0011\"8\n\tExtension\022\n\n\002id\030\001 \002(\t\022\014\n\004dat" +
|
"\030\002 \001(\003:\00516384\022\014\n\001r\030\003 \001(\005:\0018\022\014\n\001p\030\004 \001(\005:\001" +
|
||||||
"a\030\002 \002(\014\022\021\n\tmandatory\030\003 \002(\010\" \n\003Tag\022\013\n\003tag" +
|
"1\"8\n\tExtension\022\n\n\002id\030\001 \002(\t\022\014\n\004data\030\002 \002(\014" +
|
||||||
"\030\001 \002(\t\022\014\n\004data\030\002 \002(\014\"5\n\021TransactionSigne" +
|
"\022\021\n\tmandatory\030\003 \002(\010\" \n\003Tag\022\013\n\003tag\030\001 \002(\t\022" +
|
||||||
"r\022\022\n\nclass_name\030\001 \002(\t\022\014\n\004data\030\002 \001(\014\"\211\005\n\006" +
|
"\014\n\004data\030\002 \002(\014\"5\n\021TransactionSigner\022\022\n\ncl" +
|
||||||
"Wallet\022\032\n\022network_identifier\030\001 \002(\t\022\034\n\024la",
|
"ass_name\030\001 \002(\t\022\014\n\004data\030\002 \001(\014\"\351\004\n\006Wallet\022",
|
||||||
"st_seen_block_hash\030\002 \001(\014\022\036\n\026last_seen_bl" +
|
"\032\n\022network_identifier\030\001 \002(\t\022\034\n\024last_seen" +
|
||||||
"ock_height\030\014 \001(\r\022!\n\031last_seen_block_time" +
|
"_block_hash\030\002 \001(\014\022\036\n\026last_seen_block_hei" +
|
||||||
"_secs\030\016 \001(\003\022\030\n\003key\030\003 \003(\0132\013.wallet.Key\022(\n" +
|
"ght\030\014 \001(\r\022!\n\031last_seen_block_time_secs\030\016" +
|
||||||
"\013transaction\030\004 \003(\0132\023.wallet.Transaction\022" +
|
" \001(\003\022\030\n\003key\030\003 \003(\0132\013.wallet.Key\022(\n\013transa" +
|
||||||
"&\n\016watched_script\030\017 \003(\0132\016.wallet.Script\022" +
|
"ction\030\004 \003(\0132\023.wallet.Transaction\022&\n\016watc" +
|
||||||
"C\n\017encryption_type\030\005 \001(\0162\035.wallet.Wallet" +
|
"hed_script\030\017 \003(\0132\016.wallet.Script\022C\n\017encr" +
|
||||||
".EncryptionType:\013UNENCRYPTED\0227\n\025encrypti" +
|
"yption_type\030\005 \001(\0162\035.wallet.Wallet.Encryp" +
|
||||||
"on_parameters\030\006 \001(\0132\030.wallet.ScryptParam" +
|
"tionType:\013UNENCRYPTED\0227\n\025encryption_para" +
|
||||||
"eters\022\022\n\007version\030\007 \001(\005:\0011\022$\n\textension\030\n" +
|
"meters\030\006 \001(\0132\030.wallet.ScryptParameters\022\022" +
|
||||||
" \003(\0132\021.wallet.Extension\022\023\n\013description\030\013",
|
"\n\007version\030\007 \001(\005:\0011\022$\n\textension\030\n \003(\0132\021.",
|
||||||
" \001(\t\022\031\n\021key_rotation_time\030\r \001(\004\022\031\n\004tags\030" +
|
"wallet.Extension\022\023\n\013description\030\013 \001(\t\022\031\n" +
|
||||||
"\020 \003(\0132\013.wallet.Tag\0226\n\023transaction_signer" +
|
"\021key_rotation_time\030\r \001(\004\022\031\n\004tags\030\020 \003(\0132\013" +
|
||||||
"s\030\021 \003(\0132\031.wallet.TransactionSigner\022\036\n\023si" +
|
".wallet.Tag\0226\n\023transaction_signers\030\021 \003(\013" +
|
||||||
"gsRequiredToSpend\030\022 \001(\r:\0011\";\n\016Encryption" +
|
"2\031.wallet.TransactionSigner\";\n\016Encryptio" +
|
||||||
"Type\022\017\n\013UNENCRYPTED\020\001\022\030\n\024ENCRYPTED_SCRYP" +
|
"nType\022\017\n\013UNENCRYPTED\020\001\022\030\n\024ENCRYPTED_SCRY" +
|
||||||
"T_AES\020\002\"R\n\014ExchangeRate\022\022\n\ncoin_value\030\001 " +
|
"PT_AES\020\002\"R\n\014ExchangeRate\022\022\n\ncoin_value\030\001" +
|
||||||
"\002(\003\022\022\n\nfiat_value\030\002 \002(\003\022\032\n\022fiat_currency" +
|
" \002(\003\022\022\n\nfiat_value\030\002 \002(\003\022\032\n\022fiat_currenc" +
|
||||||
"_code\030\003 \002(\tB\035\n\023org.bitcoinj.walletB\006Prot" +
|
"y_code\030\003 \002(\tB\035\n\023org.bitcoinj.walletB\006Pro" +
|
||||||
"os"
|
"tos"
|
||||||
};
|
};
|
||||||
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() {
|
||||||
@ -19093,7 +19093,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", "IsFollowing", });
|
new java.lang.String[] { "ChainCode", "Path", "IssuedSubkeys", "LookaheadSize", "IsFollowing", "SigsRequiredToSpend", });
|
||||||
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
|
||||||
@ -19159,7 +19159,7 @@ public final class Protos {
|
|||||||
internal_static_wallet_Wallet_fieldAccessorTable = new
|
internal_static_wallet_Wallet_fieldAccessorTable = new
|
||||||
com.google.protobuf.GeneratedMessage.FieldAccessorTable(
|
com.google.protobuf.GeneratedMessage.FieldAccessorTable(
|
||||||
internal_static_wallet_Wallet_descriptor,
|
internal_static_wallet_Wallet_descriptor,
|
||||||
new java.lang.String[] { "NetworkIdentifier", "LastSeenBlockHash", "LastSeenBlockHeight", "LastSeenBlockTimeSecs", "Key", "Transaction", "WatchedScript", "EncryptionType", "EncryptionParameters", "Version", "Extension", "Description", "KeyRotationTime", "Tags", "TransactionSigners", "SigsRequiredToSpend", });
|
new java.lang.String[] { "NetworkIdentifier", "LastSeenBlockHash", "LastSeenBlockHeight", "LastSeenBlockTimeSecs", "Key", "Transaction", "WatchedScript", "EncryptionType", "EncryptionParameters", "Version", "Extension", "Description", "KeyRotationTime", "Tags", "TransactionSigners", });
|
||||||
internal_static_wallet_ExchangeRate_descriptor =
|
internal_static_wallet_ExchangeRate_descriptor =
|
||||||
getDescriptor().getMessageTypes().get(14);
|
getDescriptor().getMessageTypes().get(14);
|
||||||
internal_static_wallet_ExchangeRate_fieldAccessorTable = new
|
internal_static_wallet_ExchangeRate_fieldAccessorTable = new
|
||||||
|
@ -111,7 +111,11 @@ public class WalletTest extends TestWithWallet {
|
|||||||
wallet.addTransactionSigner(new KeyChainTransactionSigner(keyChain));
|
wallet.addTransactionSigner(new KeyChainTransactionSigner(keyChain));
|
||||||
}
|
}
|
||||||
|
|
||||||
wallet.addFollowingAccountKeys(followingKeys, threshold);
|
MarriedKeyChain chain = MarriedKeyChain.builder()
|
||||||
|
.random(new SecureRandom())
|
||||||
|
.followingKeys(followingKeys)
|
||||||
|
.threshold(threshold).build();
|
||||||
|
wallet.addAndActivateHDChain(chain);
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
@ -2652,7 +2656,11 @@ public class WalletTest extends TestWithWallet {
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
wallet.addTransactionSigner(signer);
|
wallet.addTransactionSigner(signer);
|
||||||
wallet.addFollowingAccountKeys(ImmutableList.of(partnerKey));
|
MarriedKeyChain chain = MarriedKeyChain.builder()
|
||||||
|
.random(new SecureRandom())
|
||||||
|
.followingKeys(partnerKey)
|
||||||
|
.build();
|
||||||
|
wallet.addAndActivateHDChain(chain);
|
||||||
|
|
||||||
myAddress = wallet.currentAddress(KeyChain.KeyPurpose.RECEIVE_FUNDS);
|
myAddress = wallet.currentAddress(KeyChain.KeyPurpose.RECEIVE_FUNDS);
|
||||||
sendMoneyToWallet(wallet, COIN, myAddress, AbstractBlockChain.NewBlockType.BEST_CHAIN);
|
sendMoneyToWallet(wallet, COIN, myAddress, AbstractBlockChain.NewBlockType.BEST_CHAIN);
|
||||||
|
@ -31,6 +31,8 @@ import org.bitcoinj.wallet.DeterministicKeyChain;
|
|||||||
import org.bitcoinj.wallet.KeyChain;
|
import org.bitcoinj.wallet.KeyChain;
|
||||||
import com.google.common.collect.ImmutableList;
|
import com.google.common.collect.ImmutableList;
|
||||||
import com.google.protobuf.ByteString;
|
import com.google.protobuf.ByteString;
|
||||||
|
|
||||||
|
import org.bitcoinj.wallet.MarriedKeyChain;
|
||||||
import org.bitcoinj.wallet.Protos;
|
import org.bitcoinj.wallet.Protos;
|
||||||
import org.junit.Before;
|
import org.junit.Before;
|
||||||
import org.junit.Test;
|
import org.junit.Test;
|
||||||
@ -287,16 +289,20 @@ public class WalletProtobufSerializerTest {
|
|||||||
public void testRoundTripMarriedWallet() throws Exception {
|
public void testRoundTripMarriedWallet() throws Exception {
|
||||||
// create 2-of-2 married wallet
|
// create 2-of-2 married wallet
|
||||||
myWallet = new Wallet(params);
|
myWallet = new Wallet(params);
|
||||||
final DeterministicKeyChain keyChain = new DeterministicKeyChain(new SecureRandom());
|
final DeterministicKeyChain partnerChain = new DeterministicKeyChain(new SecureRandom());
|
||||||
DeterministicKey partnerKey = DeterministicKey.deserializeB58(null, keyChain.getWatchingKey().serializePubB58());
|
DeterministicKey partnerKey = DeterministicKey.deserializeB58(null, partnerChain.getWatchingKey().serializePubB58());
|
||||||
|
MarriedKeyChain chain = MarriedKeyChain.builder()
|
||||||
|
.random(new SecureRandom())
|
||||||
|
.followingKeys(partnerKey)
|
||||||
|
.threshold(2).build();
|
||||||
|
myWallet.addAndActivateHDChain(chain);
|
||||||
|
|
||||||
myWallet.addFollowingAccountKeys(ImmutableList.of(partnerKey), 2);
|
|
||||||
myAddress = myWallet.currentAddress(KeyChain.KeyPurpose.RECEIVE_FUNDS);
|
myAddress = myWallet.currentAddress(KeyChain.KeyPurpose.RECEIVE_FUNDS);
|
||||||
|
|
||||||
Wallet wallet1 = roundTrip(myWallet);
|
Wallet wallet1 = roundTrip(myWallet);
|
||||||
assertEquals(0, wallet1.getTransactions(true).size());
|
assertEquals(0, wallet1.getTransactions(true).size());
|
||||||
assertEquals(Coin.ZERO, wallet1.getBalance());
|
assertEquals(Coin.ZERO, wallet1.getBalance());
|
||||||
assertEquals(2, wallet1.getSigsRequiredToSpend());
|
assertEquals(2, wallet1.getActiveKeychain().getSigsRequiredToSpend());
|
||||||
assertEquals(myAddress, wallet1.currentAddress(KeyChain.KeyPurpose.RECEIVE_FUNDS));
|
assertEquals(myAddress, wallet1.currentAddress(KeyChain.KeyPurpose.RECEIVE_FUNDS));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -56,14 +56,24 @@ public class KeyChainGroupTest {
|
|||||||
}
|
}
|
||||||
|
|
||||||
private KeyChainGroup createMarriedKeyChainGroup() {
|
private KeyChainGroup createMarriedKeyChainGroup() {
|
||||||
byte[] entropy = Sha256Hash.create("don't use a seed like this in real life".getBytes()).getBytes();
|
KeyChainGroup group = new KeyChainGroup(params);
|
||||||
DeterministicSeed seed = new DeterministicSeed(entropy, "", MnemonicCode.BIP39_STANDARDISATION_TIME_SECS);
|
DeterministicKeyChain chain = createMarriedKeyChain();
|
||||||
KeyChainGroup group = new KeyChainGroup(params, seed, ImmutableList.of(watchingAccountKey), 2);
|
group.addAndActivateHDChain(chain);
|
||||||
group.setLookaheadSize(LOOKAHEAD_SIZE);
|
group.setLookaheadSize(LOOKAHEAD_SIZE);
|
||||||
group.getActiveKeyChain();
|
group.getActiveKeyChain();
|
||||||
return group;
|
return group;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private MarriedKeyChain createMarriedKeyChain() {
|
||||||
|
byte[] entropy = Sha256Hash.create("don't use a seed like this in real life".getBytes()).getBytes();
|
||||||
|
DeterministicSeed seed = new DeterministicSeed(entropy, "", MnemonicCode.BIP39_STANDARDISATION_TIME_SECS);
|
||||||
|
MarriedKeyChain chain = MarriedKeyChain.builder()
|
||||||
|
.seed(seed)
|
||||||
|
.followingKeys(watchingAccountKey)
|
||||||
|
.threshold(2).build();
|
||||||
|
return chain;
|
||||||
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
public void freshCurrentKeys() throws Exception {
|
public void freshCurrentKeys() throws Exception {
|
||||||
int numKeys = ((group.getLookaheadSize() + group.getLookaheadThreshold()) * 2) // * 2 because of internal/external
|
int numKeys = ((group.getLookaheadSize() + group.getLookaheadThreshold()) * 2) // * 2 because of internal/external
|
||||||
@ -400,7 +410,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(params, group.serializeToProtobuf(), 1);
|
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);
|
||||||
@ -411,13 +421,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(params, protoKeys1, 1);
|
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(params, protoKeys2, 1);
|
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));
|
||||||
@ -426,7 +436,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(params, protoKeys3, 1, 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);
|
||||||
@ -443,7 +453,7 @@ public class KeyChainGroupTest {
|
|||||||
group.getBloomFilterElementCount(); // Force lookahead.
|
group.getBloomFilterElementCount(); // Force lookahead.
|
||||||
List<Protos.Key> protoKeys1 = group.serializeToProtobuf();
|
List<Protos.Key> protoKeys1 = group.serializeToProtobuf();
|
||||||
assertEquals(3 + (group.getLookaheadSize() + group.getLookaheadThreshold() + 1) * 2, protoKeys1.size());
|
assertEquals(3 + (group.getLookaheadSize() + group.getLookaheadThreshold() + 1) * 2, protoKeys1.size());
|
||||||
group = KeyChainGroup.fromProtobufUnencrypted(params, protoKeys1, 1);
|
group = KeyChainGroup.fromProtobufUnencrypted(params, protoKeys1);
|
||||||
assertEquals(3 + (group.getLookaheadSize() + group.getLookaheadThreshold() + 1) * 2, group.serializeToProtobuf().size());
|
assertEquals(3 + (group.getLookaheadSize() + group.getLookaheadThreshold() + 1) * 2, group.serializeToProtobuf().size());
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -452,12 +462,12 @@ public class KeyChainGroupTest {
|
|||||||
group = createMarriedKeyChainGroup();
|
group = createMarriedKeyChainGroup();
|
||||||
Address address1 = group.currentAddress(KeyChain.KeyPurpose.RECEIVE_FUNDS);
|
Address address1 = group.currentAddress(KeyChain.KeyPurpose.RECEIVE_FUNDS);
|
||||||
assertTrue(group.isMarried());
|
assertTrue(group.isMarried());
|
||||||
assertEquals(2, group.getSigsRequiredToSpend());
|
assertEquals(2, group.getActiveKeyChain().getSigsRequiredToSpend());
|
||||||
|
|
||||||
List<Protos.Key> protoKeys = group.serializeToProtobuf();
|
List<Protos.Key> protoKeys = group.serializeToProtobuf();
|
||||||
KeyChainGroup group2 = KeyChainGroup.fromProtobufUnencrypted(params, protoKeys, 2);
|
KeyChainGroup group2 = KeyChainGroup.fromProtobufUnencrypted(params, protoKeys);
|
||||||
assertTrue(group2.isMarried());
|
assertTrue(group2.isMarried());
|
||||||
assertEquals(2, group.getSigsRequiredToSpend());
|
assertEquals(2, group.getActiveKeyChain().getSigsRequiredToSpend());
|
||||||
Address address2 = group2.currentAddress(KeyChain.KeyPurpose.RECEIVE_FUNDS);
|
Address address2 = group2.currentAddress(KeyChain.KeyPurpose.RECEIVE_FUNDS);
|
||||||
assertEquals(address1, address2);
|
assertEquals(address1, address2);
|
||||||
}
|
}
|
||||||
@ -465,23 +475,10 @@ public class KeyChainGroupTest {
|
|||||||
@Test
|
@Test
|
||||||
public void addFollowingAccounts() throws Exception {
|
public void addFollowingAccounts() throws Exception {
|
||||||
assertFalse(group.isMarried());
|
assertFalse(group.isMarried());
|
||||||
group.addFollowingAccountKeys(ImmutableList.of(watchingAccountKey));
|
group.addAndActivateHDChain(createMarriedKeyChain());
|
||||||
assertTrue(group.isMarried());
|
assertTrue(group.isMarried());
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test (expected = IllegalStateException.class)
|
|
||||||
public void addFollowingAccountsTwiceShouldFail() {
|
|
||||||
ImmutableList<DeterministicKey> followingKeys = ImmutableList.of(watchingAccountKey);
|
|
||||||
group.addFollowingAccountKeys(followingKeys);
|
|
||||||
group.addFollowingAccountKeys(followingKeys);
|
|
||||||
}
|
|
||||||
|
|
||||||
@Test (expected = IllegalStateException.class)
|
|
||||||
public void addFollowingAccountsForUsedKeychainShouldFail() {
|
|
||||||
group.freshAddress(KeyChain.KeyPurpose.RECEIVE_FUNDS);
|
|
||||||
group.addFollowingAccountKeys(ImmutableList.of(watchingAccountKey));
|
|
||||||
}
|
|
||||||
|
|
||||||
@Test
|
@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);
|
||||||
@ -519,7 +516,7 @@ public class KeyChainGroupTest {
|
|||||||
DeterministicSeed seed1 = group.getActiveKeyChain().getSeed();
|
DeterministicSeed seed1 = group.getActiveKeyChain().getSeed();
|
||||||
assertNotNull(seed1);
|
assertNotNull(seed1);
|
||||||
|
|
||||||
group = KeyChainGroup.fromProtobufUnencrypted(params, protobufs, 1);
|
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();
|
||||||
|
@ -28,6 +28,7 @@ deterministic_key {
|
|||||||
path: 0
|
path: 0
|
||||||
issued_subkeys: 2
|
issued_subkeys: 2
|
||||||
lookahead_size: 10
|
lookahead_size: 10
|
||||||
|
sigsRequiredToSpend: 1
|
||||||
}
|
}
|
||||||
|
|
||||||
type: DETERMINISTIC_KEY
|
type: DETERMINISTIC_KEY
|
||||||
@ -39,6 +40,7 @@ deterministic_key {
|
|||||||
path: 1
|
path: 1
|
||||||
issued_subkeys: 1
|
issued_subkeys: 1
|
||||||
lookahead_size: 10
|
lookahead_size: 10
|
||||||
|
sigsRequiredToSpend: 1
|
||||||
}
|
}
|
||||||
|
|
||||||
type: DETERMINISTIC_KEY
|
type: DETERMINISTIC_KEY
|
||||||
|
@ -14,6 +14,7 @@ deterministic_key {
|
|||||||
path: 0
|
path: 0
|
||||||
issued_subkeys: 2
|
issued_subkeys: 2
|
||||||
lookahead_size: 10
|
lookahead_size: 10
|
||||||
|
sigsRequiredToSpend: 1
|
||||||
}
|
}
|
||||||
|
|
||||||
type: DETERMINISTIC_KEY
|
type: DETERMINISTIC_KEY
|
||||||
@ -24,6 +25,7 @@ deterministic_key {
|
|||||||
path: 1
|
path: 1
|
||||||
issued_subkeys: 1
|
issued_subkeys: 1
|
||||||
lookahead_size: 10
|
lookahead_size: 10
|
||||||
|
sigsRequiredToSpend: 1
|
||||||
}
|
}
|
||||||
|
|
||||||
type: DETERMINISTIC_KEY
|
type: DETERMINISTIC_KEY
|
||||||
|
@ -66,6 +66,10 @@ message DeterministicKey {
|
|||||||
* a single P2SH multisignature address
|
* a single P2SH multisignature address
|
||||||
*/
|
*/
|
||||||
optional bool isFollowing = 5;
|
optional bool isFollowing = 5;
|
||||||
|
|
||||||
|
// Number of signatures required to spend. This field is needed only for married keychains to reconstruct KeyChain
|
||||||
|
// and represents the N value from N-of-M CHECKMULTISIG script. For regular single keychains it will always be 1.
|
||||||
|
optional uint32 sigsRequiredToSpend = 6 [default = 1];
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -379,11 +383,7 @@ message Wallet {
|
|||||||
// transaction signers added to the wallet
|
// transaction signers added to the wallet
|
||||||
repeated TransactionSigner transaction_signers = 17;
|
repeated TransactionSigner transaction_signers = 17;
|
||||||
|
|
||||||
// Number of signatures required to spend. This field is needed only for married wallets to reconstruct KeyChainGroup
|
// Next tag: 18
|
||||||
// and represents the N value from N-of-M CHECKMULTISIG script. For regular single wallets it will always be 1.
|
|
||||||
optional uint32 sigsRequiredToSpend = 18 [default = 1];
|
|
||||||
|
|
||||||
// Next tag: 19
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/** An exchange rate between Bitcoin and some fiat currency. */
|
/** An exchange rate between Bitcoin and some fiat currency. */
|
||||||
|
@ -48,6 +48,8 @@ import joptsimple.OptionParser;
|
|||||||
import joptsimple.OptionSet;
|
import joptsimple.OptionSet;
|
||||||
import joptsimple.OptionSpec;
|
import joptsimple.OptionSpec;
|
||||||
import joptsimple.util.DateConverter;
|
import joptsimple.util.DateConverter;
|
||||||
|
|
||||||
|
import org.bitcoinj.wallet.MarriedKeyChain;
|
||||||
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;
|
||||||
@ -59,6 +61,7 @@ import java.io.*;
|
|||||||
import java.math.BigInteger;
|
import java.math.BigInteger;
|
||||||
import java.net.InetAddress;
|
import java.net.InetAddress;
|
||||||
import java.net.UnknownHostException;
|
import java.net.UnknownHostException;
|
||||||
|
import java.security.SecureRandom;
|
||||||
import java.text.ParseException;
|
import java.text.ParseException;
|
||||||
import java.util.Date;
|
import java.util.Date;
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
@ -428,7 +431,11 @@ public class WalletTool {
|
|||||||
for (String xpubkey : xpubkeys) {
|
for (String xpubkey : xpubkeys) {
|
||||||
keys.add(DeterministicKey.deserializeB58(null, xpubkey.trim()));
|
keys.add(DeterministicKey.deserializeB58(null, xpubkey.trim()));
|
||||||
}
|
}
|
||||||
wallet.addFollowingAccountKeys(keys.build());
|
MarriedKeyChain chain = MarriedKeyChain.builder()
|
||||||
|
.random(new SecureRandom())
|
||||||
|
.followingKeys(keys.build())
|
||||||
|
.build();
|
||||||
|
wallet.addAndActivateHDChain(chain);
|
||||||
}
|
}
|
||||||
|
|
||||||
private static void rotate() throws BlockStoreException {
|
private static void rotate() throws BlockStoreException {
|
||||||
|
Loading…
Reference in New Issue
Block a user