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());
|
||||
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
|
||||
* 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.
|
||||
* Gets the active keychain via {@link KeyChainGroup#getActiveKeyChain()}
|
||||
*/
|
||||
public int getSigsRequiredToSpend() {
|
||||
lock.lock();
|
||||
try {
|
||||
return keychain.getSigsRequiredToSpend();
|
||||
} finally {
|
||||
lock.unlock();
|
||||
}
|
||||
public DeterministicKeyChain getActiveKeychain() {
|
||||
return keychain.getActiveKeyChain();
|
||||
}
|
||||
|
||||
/**
|
||||
@ -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>
|
||||
* <p>Creates married wallet 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>
|
||||
* Add a pre-configured keychain to the wallet. Useful for setting up a complex keychain,
|
||||
* such as for a married wallet. For example:
|
||||
* <pre>
|
||||
* MarriedKeyChain chain = MarriedKeyChain.builder()
|
||||
* .random(new SecureRandom())
|
||||
* .followingKeys(followingKeys)
|
||||
* .threshold(2).build();
|
||||
* wallet.addAndActivateHDChain(chain);
|
||||
* </p>
|
||||
*/
|
||||
public void addFollowingAccountKeys(List<DeterministicKey> followingAccountKeys) {
|
||||
lock.lock();
|
||||
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();
|
||||
}
|
||||
public void addAndActivateHDChain(DeterministicKeyChain chain) {
|
||||
keychain.addAndActivateHDChain(chain);
|
||||
}
|
||||
|
||||
/** 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);
|
||||
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.setSigsRequiredToSpend(wallet.getSigsRequiredToSpend());
|
||||
|
||||
// Populate the wallet version.
|
||||
walletBuilder.setVersion(wallet.getVersion());
|
||||
|
||||
@ -410,16 +408,14 @@ public class WalletProtobufSerializer {
|
||||
if (!walletProto.getNetworkIdentifier().equals(params.getId()))
|
||||
throw new UnreadableWalletException.WrongNetwork();
|
||||
|
||||
int sigsRequiredToSpend = walletProto.getSigsRequiredToSpend();
|
||||
|
||||
// Read the scrypt parameters that specify how encryption and decryption is performed.
|
||||
KeyChainGroup chain;
|
||||
if (walletProto.hasEncryptionParameters()) {
|
||||
Protos.ScryptParameters encryptionParameters = walletProto.getEncryptionParameters();
|
||||
final KeyCrypterScrypt keyCrypter = new KeyCrypterScrypt(encryptionParameters);
|
||||
chain = KeyChainGroup.fromProtobufEncrypted(params, walletProto.getKeyList(), sigsRequiredToSpend, keyCrypter);
|
||||
chain = KeyChainGroup.fromProtobufEncrypted(params, walletProto.getKeyList(), keyCrypter);
|
||||
} else {
|
||||
chain = KeyChainGroup.fromProtobufUnencrypted(params, walletProto.getKeyList(), sigsRequiredToSpend);
|
||||
chain = KeyChainGroup.fromProtobufUnencrypted(params, walletProto.getKeyList());
|
||||
}
|
||||
Wallet wallet = factory.create(params, chain);
|
||||
|
||||
|
@ -18,10 +18,14 @@ package org.bitcoinj.wallet;
|
||||
|
||||
import org.bitcoinj.core.BloomFilter;
|
||||
import org.bitcoinj.core.ECKey;
|
||||
import org.bitcoinj.core.NetworkParameters;
|
||||
import org.bitcoinj.core.Utils;
|
||||
import org.bitcoinj.crypto.*;
|
||||
import org.bitcoinj.script.Script;
|
||||
import org.bitcoinj.store.UnreadableWalletException;
|
||||
import org.bitcoinj.utils.Threading;
|
||||
|
||||
import com.google.common.base.Joiner;
|
||||
import com.google.common.collect.ImmutableList;
|
||||
import com.google.protobuf.ByteString;
|
||||
import org.slf4j.Logger;
|
||||
@ -37,6 +41,7 @@ import java.util.concurrent.Executor;
|
||||
import java.util.concurrent.locks.ReentrantLock;
|
||||
|
||||
import static com.google.common.base.Preconditions.*;
|
||||
import static com.google.common.collect.Lists.newArrayList;
|
||||
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.
|
||||
* </p>
|
||||
*/
|
||||
@SuppressWarnings("PublicStaticCollectionField")
|
||||
public class DeterministicKeyChain implements EncryptableKeyChain {
|
||||
private static final Logger log = LoggerFactory.getLogger(DeterministicKeyChain.class);
|
||||
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;
|
||||
@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
|
||||
// chains, it will be calculated on demand from the number of loaded keys.
|
||||
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
|
||||
// regenerations, which are expensive and will (in future) trigger chain download stalls/retries. One third
|
||||
// is an efficiency tradeoff.
|
||||
private int lookaheadThreshold = calcDefaultLookaheadThreshold();
|
||||
protected int lookaheadThreshold = calcDefaultLookaheadThreshold();
|
||||
|
||||
private int calcDefaultLookaheadThreshold() {
|
||||
return lookaheadSize / 3;
|
||||
@ -144,6 +150,99 @@ public class DeterministicKeyChain implements EncryptableKeyChain {
|
||||
// If set this chain is following another chain in a married KeyChainGroup
|
||||
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}
|
||||
* 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
|
||||
* 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>
|
||||
* <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.isFollowing = isFollowing;
|
||||
}
|
||||
@ -581,49 +680,61 @@ public class DeterministicKeyChain implements EncryptableKeyChain {
|
||||
//
|
||||
//////////////////////////////////////////////////////////////////////////////////////////////////////////////////
|
||||
|
||||
protected void beforeSerializeToProtobuf(List<Protos.Key> result) {
|
||||
}
|
||||
|
||||
@Override
|
||||
public List<Protos.Key> serializeToProtobuf() {
|
||||
List<Protos.Key> result = newArrayList();
|
||||
lock.lock();
|
||||
try {
|
||||
// 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);
|
||||
} 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;
|
||||
beforeSerializeToProtobuf(result);
|
||||
result.addAll(serializeMyselfToProtobuf());
|
||||
} finally {
|
||||
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;
|
||||
|
||||
int lookaheadSize = -1;
|
||||
int sigsRequiredToSpend = 1;
|
||||
|
||||
for (Protos.Key key : keys) {
|
||||
final Protos.Key.Type t = key.getType();
|
||||
if (t == Protos.Key.Type.DETERMINISTIC_MNEMONIC) {
|
||||
if (chain != null) {
|
||||
checkState(lookaheadSize >= 0);
|
||||
chain.setLookaheadSize(lookaheadSize);
|
||||
chain.setSigsRequiredToSpend(sigsRequiredToSpend);
|
||||
chain.maybeLookAhead();
|
||||
chains.add(chain);
|
||||
chain = null;
|
||||
@ -693,6 +807,7 @@ public class DeterministicKeyChain implements EncryptableKeyChain {
|
||||
if (chain != null) {
|
||||
checkState(lookaheadSize >= 0);
|
||||
chain.setLookaheadSize(lookaheadSize);
|
||||
chain.setSigsRequiredToSpend(sigsRequiredToSpend);
|
||||
chain.maybeLookAhead();
|
||||
chains.add(chain);
|
||||
chain = null;
|
||||
@ -701,15 +816,23 @@ public class DeterministicKeyChain implements EncryptableKeyChain {
|
||||
isFollowingKey = true;
|
||||
}
|
||||
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) {
|
||||
DeterministicKey accountKey = new DeterministicKey(immutablePath, chainCode, pubkey, null, null);
|
||||
if (!accountKey.getPath().equals(ACCOUNT_ZERO_PATH))
|
||||
throw new UnreadableWalletException("Expecting account key but found key with path: " +
|
||||
HDUtils.formatPath(accountKey.getPath()));
|
||||
chain = new DeterministicKeyChain(accountKey, isFollowingKey);
|
||||
if (isMarried)
|
||||
chain = new MarriedKeyChain(accountKey);
|
||||
else
|
||||
chain = new DeterministicKeyChain(accountKey, isFollowingKey);
|
||||
isWatchingAccountKey = true;
|
||||
} else {
|
||||
chain = new DeterministicKeyChain(seed, crypter);
|
||||
if (isMarried)
|
||||
chain = new MarriedKeyChain(seed, crypter);
|
||||
else
|
||||
chain = new DeterministicKeyChain(seed, crypter);
|
||||
chain.lookaheadSize = LAZY_CALCULATE_LOOKAHEAD;
|
||||
// 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
|
||||
@ -759,6 +882,7 @@ public class DeterministicKeyChain implements EncryptableKeyChain {
|
||||
chain.externalKey = detkey;
|
||||
chain.issuedExternalKeys = key.getDeterministicKey().getIssuedSubkeys();
|
||||
lookaheadSize = Math.max(lookaheadSize, key.getDeterministicKey().getLookaheadSize());
|
||||
sigsRequiredToSpend = key.getDeterministicKey().getSigsRequiredToSpend();
|
||||
} else if (detkey.getChildNumber().num() == 1) {
|
||||
chain.internalKey = detkey;
|
||||
chain.issuedInternalKeys = key.getDeterministicKey().getIssuedSubkeys();
|
||||
@ -772,6 +896,7 @@ public class DeterministicKeyChain implements EncryptableKeyChain {
|
||||
if (chain != null) {
|
||||
checkState(lookaheadSize >= 0);
|
||||
chain.setLookaheadSize(lookaheadSize);
|
||||
chain.setSigsRequiredToSpend(sigsRequiredToSpend);
|
||||
chain.maybeLookAhead();
|
||||
chains.add(chain);
|
||||
}
|
||||
@ -1013,6 +1138,10 @@ public class DeterministicKeyChain implements EncryptableKeyChain {
|
||||
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
|
||||
* or held in memory, because of the lookahead zone.
|
||||
@ -1115,4 +1244,73 @@ public class DeterministicKeyChain implements EncryptableKeyChain {
|
||||
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.utils.ListenerRegistration;
|
||||
import org.bitcoinj.utils.Threading;
|
||||
|
||||
import com.google.common.base.Joiner;
|
||||
import com.google.common.collect.HashMultimap;
|
||||
import com.google.common.collect.ImmutableList;
|
||||
@ -67,20 +68,9 @@ public class KeyChainGroup implements KeyBag {
|
||||
|
||||
private BasicKeyChain basic;
|
||||
private NetworkParameters params;
|
||||
private final List<DeterministicKeyChain> chains;
|
||||
private final LinkedList<DeterministicKeyChain> chains;
|
||||
private final EnumMap<KeyChain.KeyPurpose, DeterministicKey> currentKeys;
|
||||
|
||||
// The map keys are the watching keys of the followed chains and values are the following chains
|
||||
private Multimap<DeterministicKey, DeterministicKeyChain> followingKeychains;
|
||||
|
||||
// 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;
|
||||
@Nullable private KeyCrypter keyCrypter;
|
||||
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. */
|
||||
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. */
|
||||
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()}.
|
||||
*/
|
||||
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()}.
|
||||
*/
|
||||
public KeyChainGroup(NetworkParameters params, DeterministicKey watchKey, long creationTimeSecondsSecs) {
|
||||
this(params, null, ImmutableList.of(DeterministicKeyChain.watch(watchKey, creationTimeSecondsSecs)), null, null, 1, 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);
|
||||
}
|
||||
this(params, null, ImmutableList.of(DeterministicKeyChain.watch(watchKey, creationTimeSecondsSecs)), null, null);
|
||||
}
|
||||
|
||||
// Used for deserialization.
|
||||
private KeyChainGroup(NetworkParameters params, @Nullable BasicKeyChain basicKeyChain, List<DeterministicKeyChain> chains,
|
||||
@Nullable EnumMap<KeyChain.KeyPurpose, DeterministicKey> currentKeys, Multimap<DeterministicKey,
|
||||
DeterministicKeyChain> followingKeychains, int sigsRequiredToSpend, @Nullable KeyCrypter crypter) {
|
||||
@Nullable EnumMap<KeyChain.KeyPurpose, DeterministicKey> currentKeys, @Nullable KeyCrypter crypter) {
|
||||
this.params = params;
|
||||
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.currentKeys = currentKeys == null
|
||||
? new EnumMap<KeyChain.KeyPurpose, DeterministicKey>(KeyChain.KeyPurpose.class)
|
||||
: currentKeys;
|
||||
this.currentAddresses = new EnumMap<KeyChain.KeyPurpose, Address>(KeyChain.KeyPurpose.class);
|
||||
this.followingKeychains = HashMultimap.create();
|
||||
if (followingKeychains != null) {
|
||||
this.followingKeychains.putAll(followingKeychains);
|
||||
}
|
||||
this.sigsRequiredToSpend = sigsRequiredToSpend;
|
||||
marriedKeysRedeemData = new LinkedHashMap<ByteString, RedeemData>();
|
||||
maybeLookaheadScripts();
|
||||
|
||||
if (!this.followingKeychains.isEmpty()) {
|
||||
DeterministicKey followedWatchKey = getActiveKeyChain().getWatchingKey();
|
||||
if (isMarried()) {
|
||||
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);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 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() {
|
||||
if (chains.isEmpty())
|
||||
return;
|
||||
|
||||
int numLeafKeys = 0;
|
||||
for (DeterministicKeyChain chain : chains) {
|
||||
numLeafKeys += chain.getLeafKeys().size();
|
||||
}
|
||||
|
||||
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);
|
||||
}
|
||||
}
|
||||
chain.maybeLookAheadScripts();
|
||||
}
|
||||
}
|
||||
|
||||
@ -226,6 +137,14 @@ public class KeyChainGroup implements KeyBag {
|
||||
public void createAndActivateNewHDChain() {
|
||||
// We can't do auto upgrade here because we don't know the rotation time, if any.
|
||||
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);
|
||||
for (ListenerRegistration<KeyChainEventListener> registration : basic.getListeners())
|
||||
chain.addEventListener(registration.listener, registration.executor);
|
||||
@ -249,7 +168,7 @@ public class KeyChainGroup implements KeyBag {
|
||||
*/
|
||||
public DeterministicKey currentKey(KeyChain.KeyPurpose purpose) {
|
||||
DeterministicKeyChain chain = getActiveKeyChain();
|
||||
if (isMarried(chain)) {
|
||||
if (chain.isMarried()) {
|
||||
throw new UnsupportedOperationException("Key is not suitable to receive coins for married keychains." +
|
||||
" Use freshAddress to get P2SH address instead");
|
||||
}
|
||||
@ -266,7 +185,7 @@ public class KeyChainGroup implements KeyBag {
|
||||
*/
|
||||
public Address currentAddress(KeyChain.KeyPurpose purpose) {
|
||||
DeterministicKeyChain chain = getActiveKeyChain();
|
||||
if (isMarried(chain)) {
|
||||
if (chain.isMarried()) {
|
||||
Address current = currentAddresses.get(purpose);
|
||||
if (current == null) {
|
||||
current = freshAddress(purpose);
|
||||
@ -308,7 +227,7 @@ public class KeyChainGroup implements KeyBag {
|
||||
*/
|
||||
public List<DeterministicKey> freshKeys(KeyChain.KeyPurpose purpose, int numberOfKeys) {
|
||||
DeterministicKeyChain chain = getActiveKeyChain();
|
||||
if (isMarried(chain)) {
|
||||
if (chain.isMarried()) {
|
||||
throw new UnsupportedOperationException("Key is not suitable to receive coins for married keychains." +
|
||||
" Use freshAddress to get P2SH address instead");
|
||||
}
|
||||
@ -320,10 +239,10 @@ public class KeyChainGroup implements KeyBag {
|
||||
*/
|
||||
public Address freshAddress(KeyChain.KeyPurpose purpose) {
|
||||
DeterministicKeyChain chain = getActiveKeyChain();
|
||||
if (isMarried(chain)) {
|
||||
List<ECKey> marriedKeys = freshMarriedKeys(purpose, chain);
|
||||
Script p2shScript = makeP2SHOutputScript(marriedKeys);
|
||||
Address freshAddress = Address.fromP2SHScript(params, p2shScript);
|
||||
if (chain.isMarried()) {
|
||||
Script outputScript = chain.freshOutputScript(purpose);
|
||||
checkState(outputScript.isPayToScriptHash()); // Only handle P2SH for now
|
||||
Address freshAddress = Address.fromP2SHScript(params, outputScript);
|
||||
maybeLookaheadScripts();
|
||||
currentAddresses.put(purpose, 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. */
|
||||
public DeterministicKeyChain getActiveKeyChain() {
|
||||
if (chains.isEmpty()) {
|
||||
@ -379,9 +276,6 @@ public class KeyChainGroup implements KeyBag {
|
||||
for (DeterministicKeyChain chain : chains) {
|
||||
chain.setLookaheadSize(lookaheadSize);
|
||||
}
|
||||
for (DeterministicKeyChain chain : followingKeychains.values()) {
|
||||
chain.setLookaheadSize(lookaheadSize);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
@ -456,7 +350,14 @@ public class KeyChainGroup implements KeyBag {
|
||||
|
||||
@Nullable
|
||||
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
|
||||
@ -552,18 +453,12 @@ public class KeyChainGroup implements KeyBag {
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns true if the given keychain is being followed by at least one another keychain
|
||||
*/
|
||||
public boolean isMarried(DeterministicKeyChain keychain) {
|
||||
DeterministicKey watchingKey = keychain.getWatchingKey();
|
||||
return followingKeychains.containsKey(watchingKey) && followingKeychains.get(watchingKey).size() > 0;
|
||||
}
|
||||
|
||||
/**
|
||||
* An alias for {@link #isMarried(DeterministicKeyChain)} called for the active keychain
|
||||
* Whether the active 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 isMarried(getActiveKeyChain());
|
||||
return !chains.isEmpty() && getActiveKeyChain().isMarried();
|
||||
}
|
||||
|
||||
/**
|
||||
@ -638,12 +533,7 @@ public class KeyChainGroup implements KeyBag {
|
||||
public int getBloomFilterElementCount() {
|
||||
int result = basic.numBloomFilterEntries();
|
||||
for (DeterministicKeyChain chain : chains) {
|
||||
if (isMarried(chain)) {
|
||||
chain.maybeLookAhead();
|
||||
result += chain.getLeafKeys().size() * 2;
|
||||
} else {
|
||||
result += chain.numBloomFilterEntries();
|
||||
}
|
||||
result += chain.numBloomFilterEntries();
|
||||
}
|
||||
return result;
|
||||
}
|
||||
@ -653,15 +543,8 @@ public class KeyChainGroup implements KeyBag {
|
||||
if (basic.numKeys() > 0)
|
||||
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) {
|
||||
if (!isMarried(chain)) {
|
||||
filter.merge(chain.getFilter(size, falsePositiveRate, nTweak));
|
||||
}
|
||||
filter.merge(chain.getFilter(size, falsePositiveRate, nTweak));
|
||||
}
|
||||
return filter;
|
||||
}
|
||||
@ -671,22 +554,8 @@ public class KeyChainGroup implements KeyBag {
|
||||
throw new UnsupportedOperationException(); // Unused.
|
||||
}
|
||||
|
||||
private Script makeP2SHOutputScript(List<ECKey> marriedKeys) {
|
||||
return ScriptBuilder.createP2SHOutputScript(makeRedeemScript(marriedKeys));
|
||||
}
|
||||
|
||||
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);
|
||||
private Script makeP2SHOutputScript(DeterministicKey followedKey, DeterministicKeyChain chain) {
|
||||
return ScriptBuilder.createP2SHOutputScript(chain.getRedeemData(followedKey).redeemScript);
|
||||
}
|
||||
|
||||
/** 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
|
||||
result = Lists.newArrayList();
|
||||
for (DeterministicKeyChain chain : chains) {
|
||||
// prepend each chain with it's following chains if any
|
||||
for (DeterministicKeyChain followingChain : followingKeychains.get(chain.getWatchingKey())) {
|
||||
result.addAll(followingChain.serializeToProtobuf());
|
||||
}
|
||||
List<Protos.Key> protos = chain.serializeToProtobuf();
|
||||
result.addAll(protos);
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
public static KeyChainGroup fromProtobufUnencrypted(NetworkParameters params, List<Protos.Key> keys, int sigsRequiredToSpend) throws UnreadableWalletException {
|
||||
checkArgument(sigsRequiredToSpend > 0);
|
||||
public static KeyChainGroup fromProtobufUnencrypted(NetworkParameters params, List<Protos.Key> keys) throws UnreadableWalletException {
|
||||
BasicKeyChain basicKeyChain = BasicKeyChain.fromProtobufUnencrypted(keys);
|
||||
List<DeterministicKeyChain> chains = DeterministicKeyChain.fromProtobuf(keys, null);
|
||||
EnumMap<KeyChain.KeyPurpose, DeterministicKey> currentKeys = null;
|
||||
if (!chains.isEmpty())
|
||||
currentKeys = createCurrentKeysMap(chains);
|
||||
Multimap<DeterministicKey, DeterministicKeyChain> followingKeychains = extractFollowingKeychains(chains);
|
||||
if (sigsRequiredToSpend < 2 && followingKeychains.size() > 0)
|
||||
throw new IllegalArgumentException("Married KeyChainGroup requires multiple signatures to spend");
|
||||
return new KeyChainGroup(params, basicKeyChain, chains, currentKeys, followingKeychains, sigsRequiredToSpend, null);
|
||||
extractFollowingKeychains(chains);
|
||||
return new KeyChainGroup(params, basicKeyChain, chains, currentKeys, null);
|
||||
}
|
||||
|
||||
public static KeyChainGroup fromProtobufEncrypted(NetworkParameters params, List<Protos.Key> keys, int sigsRequiredToSpend, KeyCrypter crypter) throws UnreadableWalletException {
|
||||
checkArgument(sigsRequiredToSpend > 0);
|
||||
public static KeyChainGroup fromProtobufEncrypted(NetworkParameters params, List<Protos.Key> keys, KeyCrypter crypter) throws UnreadableWalletException {
|
||||
checkNotNull(crypter);
|
||||
BasicKeyChain basicKeyChain = BasicKeyChain.fromProtobufEncrypted(keys, crypter);
|
||||
List<DeterministicKeyChain> chains = DeterministicKeyChain.fromProtobuf(keys, crypter);
|
||||
EnumMap<KeyChain.KeyPurpose, DeterministicKey> currentKeys = null;
|
||||
if (!chains.isEmpty())
|
||||
currentKeys = createCurrentKeysMap(chains);
|
||||
Multimap<DeterministicKey, DeterministicKeyChain> followingKeychains = extractFollowingKeychains(chains);
|
||||
if (sigsRequiredToSpend < 2 && followingKeychains.size() > 0)
|
||||
throw new IllegalArgumentException("Married KeyChainGroup requires multiple signatures to spend");
|
||||
return new KeyChainGroup(params, basicKeyChain, chains, currentKeys, followingKeychains, sigsRequiredToSpend, crypter);
|
||||
extractFollowingKeychains(chains);
|
||||
return new KeyChainGroup(params, basicKeyChain, chains, currentKeys, crypter);
|
||||
}
|
||||
|
||||
/**
|
||||
@ -848,111 +707,42 @@ public class KeyChainGroup implements KeyBag {
|
||||
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
|
||||
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(); ) {
|
||||
DeterministicKeyChain chain = it.next();
|
||||
if (chain.isFollowing()) {
|
||||
followingChains.add(chain);
|
||||
it.remove();
|
||||
} else if (!followingChains.isEmpty()) {
|
||||
followingKeychains.putAll(chain.getWatchingKey(), followingChains);
|
||||
followingChains.clear();
|
||||
if (!(chain instanceof MarriedKeyChain))
|
||||
throw new IllegalStateException();
|
||||
((MarriedKeyChain)chain).setFollowingKeyChains(followingChains);
|
||||
followingChains = Lists.newArrayList();
|
||||
}
|
||||
}
|
||||
return followingKeychains;
|
||||
}
|
||||
|
||||
public String toString(boolean includePrivateKeys) {
|
||||
final StringBuilder builder = new StringBuilder();
|
||||
if (basic != null) {
|
||||
for (ECKey key : basic.getKeys())
|
||||
formatKeyWithAddress(includePrivateKeys, key, builder);
|
||||
key.formatKeyWithAddress(includePrivateKeys, builder, params);
|
||||
}
|
||||
List<String> chainStrs = Lists.newLinkedList();
|
||||
for (DeterministicKeyChain chain : chains) {
|
||||
final StringBuilder builder2 = new StringBuilder();
|
||||
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());
|
||||
chainStrs.add(chain.toString(includePrivateKeys, params));
|
||||
}
|
||||
builder.append(Joiner.on(String.format("%n")).join(chainStrs));
|
||||
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. */
|
||||
public List<DeterministicKeyChain> getDeterministicKeyChains() {
|
||||
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
|
||||
* 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>
|
||||
*/
|
||||
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}
|
||||
@ -1367,6 +1387,11 @@ public final class Protos {
|
||||
isFollowing_ = input.readBool();
|
||||
break;
|
||||
}
|
||||
case 48: {
|
||||
bitField0_ |= 0x00000010;
|
||||
sigsRequiredToSpend_ = input.readUInt32();
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
} catch (com.google.protobuf.InvalidProtocolBufferException e) {
|
||||
@ -1554,12 +1579,39 @@ public final class Protos {
|
||||
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() {
|
||||
chainCode_ = com.google.protobuf.ByteString.EMPTY;
|
||||
path_ = java.util.Collections.emptyList();
|
||||
issuedSubkeys_ = 0;
|
||||
lookaheadSize_ = 0;
|
||||
isFollowing_ = false;
|
||||
sigsRequiredToSpend_ = 1;
|
||||
}
|
||||
private byte memoizedIsInitialized = -1;
|
||||
public final boolean isInitialized() {
|
||||
@ -1592,6 +1644,9 @@ public final class Protos {
|
||||
if (((bitField0_ & 0x00000008) == 0x00000008)) {
|
||||
output.writeBool(5, isFollowing_);
|
||||
}
|
||||
if (((bitField0_ & 0x00000010) == 0x00000010)) {
|
||||
output.writeUInt32(6, sigsRequiredToSpend_);
|
||||
}
|
||||
getUnknownFields().writeTo(output);
|
||||
}
|
||||
|
||||
@ -1626,6 +1681,10 @@ public final class Protos {
|
||||
size += com.google.protobuf.CodedOutputStream
|
||||
.computeBoolSize(5, isFollowing_);
|
||||
}
|
||||
if (((bitField0_ & 0x00000010) == 0x00000010)) {
|
||||
size += com.google.protobuf.CodedOutputStream
|
||||
.computeUInt32Size(6, sigsRequiredToSpend_);
|
||||
}
|
||||
size += getUnknownFields().getSerializedSize();
|
||||
memoizedSerializedSize = size;
|
||||
return size;
|
||||
@ -1757,6 +1816,8 @@ public final class Protos {
|
||||
bitField0_ = (bitField0_ & ~0x00000008);
|
||||
isFollowing_ = false;
|
||||
bitField0_ = (bitField0_ & ~0x00000010);
|
||||
sigsRequiredToSpend_ = 1;
|
||||
bitField0_ = (bitField0_ & ~0x00000020);
|
||||
return this;
|
||||
}
|
||||
|
||||
@ -1806,6 +1867,10 @@ public final class Protos {
|
||||
to_bitField0_ |= 0x00000008;
|
||||
}
|
||||
result.isFollowing_ = isFollowing_;
|
||||
if (((from_bitField0_ & 0x00000020) == 0x00000020)) {
|
||||
to_bitField0_ |= 0x00000010;
|
||||
}
|
||||
result.sigsRequiredToSpend_ = sigsRequiredToSpend_;
|
||||
result.bitField0_ = to_bitField0_;
|
||||
onBuilt();
|
||||
return result;
|
||||
@ -1844,6 +1909,9 @@ public final class Protos {
|
||||
if (other.hasIsFollowing()) {
|
||||
setIsFollowing(other.getIsFollowing());
|
||||
}
|
||||
if (other.hasSigsRequiredToSpend()) {
|
||||
setSigsRequiredToSpend(other.getSigsRequiredToSpend());
|
||||
}
|
||||
this.mergeUnknownFields(other.getUnknownFields());
|
||||
return this;
|
||||
}
|
||||
@ -2195,6 +2263,59 @@ public final class Protos {
|
||||
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)
|
||||
}
|
||||
|
||||
@ -14291,26 +14412,6 @@ public final class Protos {
|
||||
*/
|
||||
org.bitcoinj.wallet.Protos.TransactionSignerOrBuilder getTransactionSignersOrBuilder(
|
||||
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}
|
||||
@ -14474,11 +14575,6 @@ public final class Protos {
|
||||
transactionSigners_.add(input.readMessage(org.bitcoinj.wallet.Protos.TransactionSigner.PARSER, extensionRegistry));
|
||||
break;
|
||||
}
|
||||
case 144: {
|
||||
bitField0_ |= 0x00000200;
|
||||
sigsRequiredToSpend_ = input.readUInt32();
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
} catch (com.google.protobuf.InvalidProtocolBufferException e) {
|
||||
@ -15146,32 +15242,6 @@ public final class Protos {
|
||||
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() {
|
||||
networkIdentifier_ = "";
|
||||
lastSeenBlockHash_ = com.google.protobuf.ByteString.EMPTY;
|
||||
@ -15188,7 +15258,6 @@ public final class Protos {
|
||||
keyRotationTime_ = 0L;
|
||||
tags_ = java.util.Collections.emptyList();
|
||||
transactionSigners_ = java.util.Collections.emptyList();
|
||||
sigsRequiredToSpend_ = 1;
|
||||
}
|
||||
private byte memoizedIsInitialized = -1;
|
||||
public final boolean isInitialized() {
|
||||
@ -15293,9 +15362,6 @@ public final class Protos {
|
||||
for (int i = 0; i < transactionSigners_.size(); i++) {
|
||||
output.writeMessage(17, transactionSigners_.get(i));
|
||||
}
|
||||
if (((bitField0_ & 0x00000200) == 0x00000200)) {
|
||||
output.writeUInt32(18, sigsRequiredToSpend_);
|
||||
}
|
||||
getUnknownFields().writeTo(output);
|
||||
}
|
||||
|
||||
@ -15365,10 +15431,6 @@ public final class Protos {
|
||||
size += com.google.protobuf.CodedOutputStream
|
||||
.computeMessageSize(17, transactionSigners_.get(i));
|
||||
}
|
||||
if (((bitField0_ & 0x00000200) == 0x00000200)) {
|
||||
size += com.google.protobuf.CodedOutputStream
|
||||
.computeUInt32Size(18, sigsRequiredToSpend_);
|
||||
}
|
||||
size += getUnknownFields().getSerializedSize();
|
||||
memoizedSerializedSize = size;
|
||||
return size;
|
||||
@ -15554,8 +15616,6 @@ public final class Protos {
|
||||
} else {
|
||||
transactionSignersBuilder_.clear();
|
||||
}
|
||||
sigsRequiredToSpend_ = 1;
|
||||
bitField0_ = (bitField0_ & ~0x00008000);
|
||||
return this;
|
||||
}
|
||||
|
||||
@ -15678,10 +15738,6 @@ public final class Protos {
|
||||
} else {
|
||||
result.transactionSigners_ = transactionSignersBuilder_.build();
|
||||
}
|
||||
if (((from_bitField0_ & 0x00008000) == 0x00008000)) {
|
||||
to_bitField0_ |= 0x00000200;
|
||||
}
|
||||
result.sigsRequiredToSpend_ = sigsRequiredToSpend_;
|
||||
result.bitField0_ = to_bitField0_;
|
||||
onBuilt();
|
||||
return result;
|
||||
@ -15885,9 +15941,6 @@ public final class Protos {
|
||||
}
|
||||
}
|
||||
}
|
||||
if (other.hasSigsRequiredToSpend()) {
|
||||
setSigsRequiredToSpend(other.getSigsRequiredToSpend());
|
||||
}
|
||||
this.mergeUnknownFields(other.getUnknownFields());
|
||||
return this;
|
||||
}
|
||||
@ -18066,59 +18119,6 @@ public final class Protos {
|
||||
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)
|
||||
}
|
||||
|
||||
@ -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" +
|
||||
"ces\030\003 \002(\004\"M\n\rEncryptedData\022\035\n\025initialisa" +
|
||||
"tion_vector\030\001 \002(\014\022\035\n\025encrypted_private_k" +
|
||||
"ey\030\002 \002(\014\"y\n\020DeterministicKey\022\022\n\nchain_co" +
|
||||
"de\030\001 \002(\014\022\014\n\004path\030\002 \003(\r\022\026\n\016issued_subkeys" +
|
||||
"\030\003 \001(\r\022\026\n\016lookahead_size\030\004 \001(\r\022\023\n\013isFoll" +
|
||||
"owing\030\005 \001(\010\"\232\003\n\003Key\022\036\n\004type\030\001 \002(\0162\020.wall" +
|
||||
"et.Key.Type\022\024\n\014secret_bytes\030\002 \001(\014\022-\n\016enc" +
|
||||
"rypted_data\030\006 \001(\0132\025.wallet.EncryptedData",
|
||||
"\022\022\n\npublic_key\030\003 \001(\014\022\r\n\005label\030\004 \001(\t\022\032\n\022c" +
|
||||
"reation_timestamp\030\005 \001(\003\0223\n\021deterministic" +
|
||||
"_key\030\007 \001(\0132\030.wallet.DeterministicKey\022\032\n\022" +
|
||||
"deterministic_seed\030\010 \001(\014\022;\n\034encrypted_de" +
|
||||
"terministic_seed\030\t \001(\0132\025.wallet.Encrypte" +
|
||||
"dData\"a\n\004Type\022\014\n\010ORIGINAL\020\001\022\030\n\024ENCRYPTED" +
|
||||
"_SCRYPT_AES\020\002\022\032\n\026DETERMINISTIC_MNEMONIC\020" +
|
||||
"\003\022\025\n\021DETERMINISTIC_KEY\020\004\"5\n\006Script\022\017\n\007pr" +
|
||||
"ogram\030\001 \002(\014\022\032\n\022creation_timestamp\030\002 \002(\003\"" +
|
||||
"\222\001\n\020TransactionInput\022\"\n\032transaction_out_",
|
||||
"point_hash\030\001 \002(\014\022#\n\033transaction_out_poin" +
|
||||
"t_index\030\002 \002(\r\022\024\n\014script_bytes\030\003 \002(\014\022\020\n\010s" +
|
||||
"equence\030\004 \001(\r\022\r\n\005value\030\005 \001(\003\"\177\n\021Transact" +
|
||||
"ionOutput\022\r\n\005value\030\001 \002(\003\022\024\n\014script_bytes" +
|
||||
"\030\002 \002(\014\022!\n\031spent_by_transaction_hash\030\003 \001(" +
|
||||
"\014\022\"\n\032spent_by_transaction_index\030\004 \001(\005\"\211\003" +
|
||||
"\n\025TransactionConfidence\0220\n\004type\030\001 \001(\0162\"." +
|
||||
"wallet.TransactionConfidence.Type\022\032\n\022app" +
|
||||
"eared_at_height\030\002 \001(\005\022\036\n\026overriding_tran" +
|
||||
"saction\030\003 \001(\014\022\r\n\005depth\030\004 \001(\005\022)\n\014broadcas",
|
||||
"t_by\030\006 \003(\0132\023.wallet.PeerAddress\0224\n\006sourc" +
|
||||
"e\030\007 \001(\0162$.wallet.TransactionConfidence.S" +
|
||||
"ource\"O\n\004Type\022\013\n\007UNKNOWN\020\000\022\014\n\010BUILDING\020\001" +
|
||||
"\022\013\n\007PENDING\020\002\022\025\n\021NOT_IN_BEST_CHAIN\020\003\022\010\n\004" +
|
||||
"DEAD\020\004\"A\n\006Source\022\022\n\016SOURCE_UNKNOWN\020\000\022\022\n\016" +
|
||||
"SOURCE_NETWORK\020\001\022\017\n\013SOURCE_SELF\020\002\"\264\005\n\013Tr" +
|
||||
"ansaction\022\017\n\007version\030\001 \002(\005\022\014\n\004hash\030\002 \002(\014" +
|
||||
"\022&\n\004pool\030\003 \001(\0162\030.wallet.Transaction.Pool" +
|
||||
"\022\021\n\tlock_time\030\004 \001(\r\022\022\n\nupdated_at\030\005 \001(\003\022" +
|
||||
"3\n\021transaction_input\030\006 \003(\0132\030.wallet.Tran",
|
||||
"sactionInput\0225\n\022transaction_output\030\007 \003(\013" +
|
||||
"2\031.wallet.TransactionOutput\022\022\n\nblock_has" +
|
||||
"h\030\010 \003(\014\022 \n\030block_relativity_offsets\030\013 \003(" +
|
||||
"\005\0221\n\nconfidence\030\t \001(\0132\035.wallet.Transacti" +
|
||||
"onConfidence\0225\n\007purpose\030\n \001(\0162\033.wallet.T" +
|
||||
"ransaction.Purpose:\007UNKNOWN\022+\n\rexchange_" +
|
||||
"rate\030\014 \001(\0132\024.wallet.ExchangeRate\022\014\n\004memo" +
|
||||
"\030\r \001(\t\"Y\n\004Pool\022\013\n\007UNSPENT\020\004\022\t\n\005SPENT\020\005\022\014" +
|
||||
"\n\010INACTIVE\020\002\022\010\n\004DEAD\020\n\022\013\n\007PENDING\020\020\022\024\n\020P" +
|
||||
"ENDING_INACTIVE\020\022\"\224\001\n\007Purpose\022\013\n\007UNKNOWN",
|
||||
"\020\000\022\020\n\014USER_PAYMENT\020\001\022\020\n\014KEY_ROTATION\020\002\022\034" +
|
||||
"\n\030ASSURANCE_CONTRACT_CLAIM\020\003\022\035\n\031ASSURANC" +
|
||||
"E_CONTRACT_PLEDGE\020\004\022\033\n\027ASSURANCE_CONTRAC" +
|
||||
"T_STUB\020\005\"N\n\020ScryptParameters\022\014\n\004salt\030\001 \002" +
|
||||
"(\014\022\020\n\001n\030\002 \001(\003:\00516384\022\014\n\001r\030\003 \001(\005:\0018\022\014\n\001p\030" +
|
||||
"\004 \001(\005:\0011\"8\n\tExtension\022\n\n\002id\030\001 \002(\t\022\014\n\004dat" +
|
||||
"a\030\002 \002(\014\022\021\n\tmandatory\030\003 \002(\010\" \n\003Tag\022\013\n\003tag" +
|
||||
"\030\001 \002(\t\022\014\n\004data\030\002 \002(\014\"5\n\021TransactionSigne" +
|
||||
"r\022\022\n\nclass_name\030\001 \002(\t\022\014\n\004data\030\002 \001(\014\"\211\005\n\006" +
|
||||
"Wallet\022\032\n\022network_identifier\030\001 \002(\t\022\034\n\024la",
|
||||
"st_seen_block_hash\030\002 \001(\014\022\036\n\026last_seen_bl" +
|
||||
"ock_height\030\014 \001(\r\022!\n\031last_seen_block_time" +
|
||||
"_secs\030\016 \001(\003\022\030\n\003key\030\003 \003(\0132\013.wallet.Key\022(\n" +
|
||||
"\013transaction\030\004 \003(\0132\023.wallet.Transaction\022" +
|
||||
"&\n\016watched_script\030\017 \003(\0132\016.wallet.Script\022" +
|
||||
"C\n\017encryption_type\030\005 \001(\0162\035.wallet.Wallet" +
|
||||
".EncryptionType:\013UNENCRYPTED\0227\n\025encrypti" +
|
||||
"on_parameters\030\006 \001(\0132\030.wallet.ScryptParam" +
|
||||
"eters\022\022\n\007version\030\007 \001(\005:\0011\022$\n\textension\030\n" +
|
||||
" \003(\0132\021.wallet.Extension\022\023\n\013description\030\013",
|
||||
" \001(\t\022\031\n\021key_rotation_time\030\r \001(\004\022\031\n\004tags\030" +
|
||||
"\020 \003(\0132\013.wallet.Tag\0226\n\023transaction_signer" +
|
||||
"s\030\021 \003(\0132\031.wallet.TransactionSigner\022\036\n\023si" +
|
||||
"gsRequiredToSpend\030\022 \001(\r:\0011\";\n\016Encryption" +
|
||||
"Type\022\017\n\013UNENCRYPTED\020\001\022\030\n\024ENCRYPTED_SCRYP" +
|
||||
"T_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" +
|
||||
"_code\030\003 \002(\tB\035\n\023org.bitcoinj.walletB\006Prot" +
|
||||
"os"
|
||||
"ey\030\002 \002(\014\"\231\001\n\020DeterministicKey\022\022\n\nchain_c" +
|
||||
"ode\030\001 \002(\014\022\014\n\004path\030\002 \003(\r\022\026\n\016issued_subkey" +
|
||||
"s\030\003 \001(\r\022\026\n\016lookahead_size\030\004 \001(\r\022\023\n\013isFol" +
|
||||
"lowing\030\005 \001(\010\022\036\n\023sigsRequiredToSpend\030\006 \001(" +
|
||||
"\r:\0011\"\232\003\n\003Key\022\036\n\004type\030\001 \002(\0162\020.wallet.Key." +
|
||||
"Type\022\024\n\014secret_bytes\030\002 \001(\014\022-\n\016encrypted_",
|
||||
"data\030\006 \001(\0132\025.wallet.EncryptedData\022\022\n\npub" +
|
||||
"lic_key\030\003 \001(\014\022\r\n\005label\030\004 \001(\t\022\032\n\022creation" +
|
||||
"_timestamp\030\005 \001(\003\0223\n\021deterministic_key\030\007 " +
|
||||
"\001(\0132\030.wallet.DeterministicKey\022\032\n\022determi" +
|
||||
"nistic_seed\030\010 \001(\014\022;\n\034encrypted_determini" +
|
||||
"stic_seed\030\t \001(\0132\025.wallet.EncryptedData\"a" +
|
||||
"\n\004Type\022\014\n\010ORIGINAL\020\001\022\030\n\024ENCRYPTED_SCRYPT" +
|
||||
"_AES\020\002\022\032\n\026DETERMINISTIC_MNEMONIC\020\003\022\025\n\021DE" +
|
||||
"TERMINISTIC_KEY\020\004\"5\n\006Script\022\017\n\007program\030\001" +
|
||||
" \002(\014\022\032\n\022creation_timestamp\030\002 \002(\003\"\222\001\n\020Tra",
|
||||
"nsactionInput\022\"\n\032transaction_out_point_h" +
|
||||
"ash\030\001 \002(\014\022#\n\033transaction_out_point_index" +
|
||||
"\030\002 \002(\r\022\024\n\014script_bytes\030\003 \002(\014\022\020\n\010sequence" +
|
||||
"\030\004 \001(\r\022\r\n\005value\030\005 \001(\003\"\177\n\021TransactionOutp" +
|
||||
"ut\022\r\n\005value\030\001 \002(\003\022\024\n\014script_bytes\030\002 \002(\014\022" +
|
||||
"!\n\031spent_by_transaction_hash\030\003 \001(\014\022\"\n\032sp" +
|
||||
"ent_by_transaction_index\030\004 \001(\005\"\211\003\n\025Trans" +
|
||||
"actionConfidence\0220\n\004type\030\001 \001(\0162\".wallet." +
|
||||
"TransactionConfidence.Type\022\032\n\022appeared_a" +
|
||||
"t_height\030\002 \001(\005\022\036\n\026overriding_transaction",
|
||||
"\030\003 \001(\014\022\r\n\005depth\030\004 \001(\005\022)\n\014broadcast_by\030\006 " +
|
||||
"\003(\0132\023.wallet.PeerAddress\0224\n\006source\030\007 \001(\016" +
|
||||
"2$.wallet.TransactionConfidence.Source\"O" +
|
||||
"\n\004Type\022\013\n\007UNKNOWN\020\000\022\014\n\010BUILDING\020\001\022\013\n\007PEN" +
|
||||
"DING\020\002\022\025\n\021NOT_IN_BEST_CHAIN\020\003\022\010\n\004DEAD\020\004\"" +
|
||||
"A\n\006Source\022\022\n\016SOURCE_UNKNOWN\020\000\022\022\n\016SOURCE_" +
|
||||
"NETWORK\020\001\022\017\n\013SOURCE_SELF\020\002\"\264\005\n\013Transacti" +
|
||||
"on\022\017\n\007version\030\001 \002(\005\022\014\n\004hash\030\002 \002(\014\022&\n\004poo" +
|
||||
"l\030\003 \001(\0162\030.wallet.Transaction.Pool\022\021\n\tloc" +
|
||||
"k_time\030\004 \001(\r\022\022\n\nupdated_at\030\005 \001(\003\0223\n\021tran",
|
||||
"saction_input\030\006 \003(\0132\030.wallet.Transaction" +
|
||||
"Input\0225\n\022transaction_output\030\007 \003(\0132\031.wall" +
|
||||
"et.TransactionOutput\022\022\n\nblock_hash\030\010 \003(\014" +
|
||||
"\022 \n\030block_relativity_offsets\030\013 \003(\005\0221\n\nco" +
|
||||
"nfidence\030\t \001(\0132\035.wallet.TransactionConfi" +
|
||||
"dence\0225\n\007purpose\030\n \001(\0162\033.wallet.Transact" +
|
||||
"ion.Purpose:\007UNKNOWN\022+\n\rexchange_rate\030\014 " +
|
||||
"\001(\0132\024.wallet.ExchangeRate\022\014\n\004memo\030\r \001(\t\"" +
|
||||
"Y\n\004Pool\022\013\n\007UNSPENT\020\004\022\t\n\005SPENT\020\005\022\014\n\010INACT" +
|
||||
"IVE\020\002\022\010\n\004DEAD\020\n\022\013\n\007PENDING\020\020\022\024\n\020PENDING_",
|
||||
"INACTIVE\020\022\"\224\001\n\007Purpose\022\013\n\007UNKNOWN\020\000\022\020\n\014U" +
|
||||
"SER_PAYMENT\020\001\022\020\n\014KEY_ROTATION\020\002\022\034\n\030ASSUR" +
|
||||
"ANCE_CONTRACT_CLAIM\020\003\022\035\n\031ASSURANCE_CONTR" +
|
||||
"ACT_PLEDGE\020\004\022\033\n\027ASSURANCE_CONTRACT_STUB\020" +
|
||||
"\005\"N\n\020ScryptParameters\022\014\n\004salt\030\001 \002(\014\022\020\n\001n" +
|
||||
"\030\002 \001(\003:\00516384\022\014\n\001r\030\003 \001(\005:\0018\022\014\n\001p\030\004 \001(\005:\001" +
|
||||
"1\"8\n\tExtension\022\n\n\002id\030\001 \002(\t\022\014\n\004data\030\002 \002(\014" +
|
||||
"\022\021\n\tmandatory\030\003 \002(\010\" \n\003Tag\022\013\n\003tag\030\001 \002(\t\022" +
|
||||
"\014\n\004data\030\002 \002(\014\"5\n\021TransactionSigner\022\022\n\ncl" +
|
||||
"ass_name\030\001 \002(\t\022\014\n\004data\030\002 \001(\014\"\351\004\n\006Wallet\022",
|
||||
"\032\n\022network_identifier\030\001 \002(\t\022\034\n\024last_seen" +
|
||||
"_block_hash\030\002 \001(\014\022\036\n\026last_seen_block_hei" +
|
||||
"ght\030\014 \001(\r\022!\n\031last_seen_block_time_secs\030\016" +
|
||||
" \001(\003\022\030\n\003key\030\003 \003(\0132\013.wallet.Key\022(\n\013transa" +
|
||||
"ction\030\004 \003(\0132\023.wallet.Transaction\022&\n\016watc" +
|
||||
"hed_script\030\017 \003(\0132\016.wallet.Script\022C\n\017encr" +
|
||||
"yption_type\030\005 \001(\0162\035.wallet.Wallet.Encryp" +
|
||||
"tionType:\013UNENCRYPTED\0227\n\025encryption_para" +
|
||||
"meters\030\006 \001(\0132\030.wallet.ScryptParameters\022\022" +
|
||||
"\n\007version\030\007 \001(\005:\0011\022$\n\textension\030\n \003(\0132\021.",
|
||||
"wallet.Extension\022\023\n\013description\030\013 \001(\t\022\031\n" +
|
||||
"\021key_rotation_time\030\r \001(\004\022\031\n\004tags\030\020 \003(\0132\013" +
|
||||
".wallet.Tag\0226\n\023transaction_signers\030\021 \003(\013" +
|
||||
"2\031.wallet.TransactionSigner\";\n\016Encryptio" +
|
||||
"nType\022\017\n\013UNENCRYPTED\020\001\022\030\n\024ENCRYPTED_SCRY" +
|
||||
"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_currenc" +
|
||||
"y_code\030\003 \002(\tB\035\n\023org.bitcoinj.walletB\006Pro" +
|
||||
"tos"
|
||||
};
|
||||
com.google.protobuf.Descriptors.FileDescriptor.InternalDescriptorAssigner assigner =
|
||||
new com.google.protobuf.Descriptors.FileDescriptor.InternalDescriptorAssigner() {
|
||||
@ -19093,7 +19093,7 @@ public final class Protos {
|
||||
internal_static_wallet_DeterministicKey_fieldAccessorTable = new
|
||||
com.google.protobuf.GeneratedMessage.FieldAccessorTable(
|
||||
internal_static_wallet_DeterministicKey_descriptor,
|
||||
new java.lang.String[] { "ChainCode", "Path", "IssuedSubkeys", "LookaheadSize", "IsFollowing", });
|
||||
new java.lang.String[] { "ChainCode", "Path", "IssuedSubkeys", "LookaheadSize", "IsFollowing", "SigsRequiredToSpend", });
|
||||
internal_static_wallet_Key_descriptor =
|
||||
getDescriptor().getMessageTypes().get(3);
|
||||
internal_static_wallet_Key_fieldAccessorTable = new
|
||||
@ -19159,7 +19159,7 @@ public final class Protos {
|
||||
internal_static_wallet_Wallet_fieldAccessorTable = new
|
||||
com.google.protobuf.GeneratedMessage.FieldAccessorTable(
|
||||
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 =
|
||||
getDescriptor().getMessageTypes().get(14);
|
||||
internal_static_wallet_ExchangeRate_fieldAccessorTable = new
|
||||
|
@ -111,7 +111,11 @@ public class WalletTest extends TestWithWallet {
|
||||
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
|
||||
@ -2652,7 +2656,11 @@ public class WalletTest extends TestWithWallet {
|
||||
}
|
||||
};
|
||||
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);
|
||||
sendMoneyToWallet(wallet, COIN, myAddress, AbstractBlockChain.NewBlockType.BEST_CHAIN);
|
||||
|
@ -31,6 +31,8 @@ import org.bitcoinj.wallet.DeterministicKeyChain;
|
||||
import org.bitcoinj.wallet.KeyChain;
|
||||
import com.google.common.collect.ImmutableList;
|
||||
import com.google.protobuf.ByteString;
|
||||
|
||||
import org.bitcoinj.wallet.MarriedKeyChain;
|
||||
import org.bitcoinj.wallet.Protos;
|
||||
import org.junit.Before;
|
||||
import org.junit.Test;
|
||||
@ -287,16 +289,20 @@ public class WalletProtobufSerializerTest {
|
||||
public void testRoundTripMarriedWallet() throws Exception {
|
||||
// create 2-of-2 married wallet
|
||||
myWallet = new Wallet(params);
|
||||
final DeterministicKeyChain keyChain = new DeterministicKeyChain(new SecureRandom());
|
||||
DeterministicKey partnerKey = DeterministicKey.deserializeB58(null, keyChain.getWatchingKey().serializePubB58());
|
||||
final DeterministicKeyChain partnerChain = new DeterministicKeyChain(new SecureRandom());
|
||||
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);
|
||||
|
||||
Wallet wallet1 = roundTrip(myWallet);
|
||||
assertEquals(0, wallet1.getTransactions(true).size());
|
||||
assertEquals(Coin.ZERO, wallet1.getBalance());
|
||||
assertEquals(2, wallet1.getSigsRequiredToSpend());
|
||||
assertEquals(2, wallet1.getActiveKeychain().getSigsRequiredToSpend());
|
||||
assertEquals(myAddress, wallet1.currentAddress(KeyChain.KeyPurpose.RECEIVE_FUNDS));
|
||||
}
|
||||
|
||||
|
@ -56,14 +56,24 @@ public class KeyChainGroupTest {
|
||||
}
|
||||
|
||||
private KeyChainGroup createMarriedKeyChainGroup() {
|
||||
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);
|
||||
KeyChainGroup group = new KeyChainGroup(params, seed, ImmutableList.of(watchingAccountKey), 2);
|
||||
KeyChainGroup group = new KeyChainGroup(params);
|
||||
DeterministicKeyChain chain = createMarriedKeyChain();
|
||||
group.addAndActivateHDChain(chain);
|
||||
group.setLookaheadSize(LOOKAHEAD_SIZE);
|
||||
group.getActiveKeyChain();
|
||||
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
|
||||
public void freshCurrentKeys() throws Exception {
|
||||
int numKeys = ((group.getLookaheadSize() + group.getLookaheadThreshold()) * 2) // * 2 because of internal/external
|
||||
@ -400,7 +410,7 @@ public class KeyChainGroupTest {
|
||||
@Test
|
||||
public void serialization() throws Exception {
|
||||
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);
|
||||
DeterministicKey key1 = group.freshKey(KeyChain.KeyPurpose.RECEIVE_FUNDS);
|
||||
DeterministicKey key2 = group.freshKey(KeyChain.KeyPurpose.CHANGE);
|
||||
@ -411,13 +421,13 @@ public class KeyChainGroupTest {
|
||||
List<Protos.Key> protoKeys2 = group.serializeToProtobuf();
|
||||
assertEquals(INITIAL_KEYS + ((LOOKAHEAD_SIZE + 1) * 2) + 1 /* for the seed */ + 2, protoKeys2.size());
|
||||
|
||||
group = KeyChainGroup.fromProtobufUnencrypted(params, protoKeys1, 1);
|
||||
group = KeyChainGroup.fromProtobufUnencrypted(params, protoKeys1);
|
||||
assertEquals(INITIAL_KEYS + ((LOOKAHEAD_SIZE + 1) * 2) + 1 /* for the seed */ + 1, protoKeys1.size());
|
||||
assertTrue(group.hasKey(key1));
|
||||
assertTrue(group.hasKey(key2));
|
||||
assertEquals(key2, group.currentKey(KeyChain.KeyPurpose.CHANGE));
|
||||
assertEquals(key1, group.currentKey(KeyChain.KeyPurpose.RECEIVE_FUNDS));
|
||||
group = KeyChainGroup.fromProtobufUnencrypted(params, protoKeys2, 1);
|
||||
group = KeyChainGroup.fromProtobufUnencrypted(params, protoKeys2);
|
||||
assertEquals(INITIAL_KEYS + ((LOOKAHEAD_SIZE + 1) * 2) + 1 /* for the seed */ + 2, protoKeys2.size());
|
||||
assertTrue(group.hasKey(key1));
|
||||
assertTrue(group.hasKey(key2));
|
||||
@ -426,7 +436,7 @@ public class KeyChainGroupTest {
|
||||
final KeyParameter aesKey = scrypt.deriveKey("password");
|
||||
group.encrypt(scrypt, aesKey);
|
||||
List<Protos.Key> protoKeys3 = group.serializeToProtobuf();
|
||||
group = KeyChainGroup.fromProtobufEncrypted(params, protoKeys3, 1, scrypt);
|
||||
group = KeyChainGroup.fromProtobufEncrypted(params, protoKeys3, scrypt);
|
||||
assertTrue(group.isEncrypted());
|
||||
assertTrue(group.checkPassword("password"));
|
||||
group.decrypt(aesKey);
|
||||
@ -443,7 +453,7 @@ public class KeyChainGroupTest {
|
||||
group.getBloomFilterElementCount(); // Force lookahead.
|
||||
List<Protos.Key> protoKeys1 = group.serializeToProtobuf();
|
||||
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());
|
||||
}
|
||||
|
||||
@ -452,12 +462,12 @@ public class KeyChainGroupTest {
|
||||
group = createMarriedKeyChainGroup();
|
||||
Address address1 = group.currentAddress(KeyChain.KeyPurpose.RECEIVE_FUNDS);
|
||||
assertTrue(group.isMarried());
|
||||
assertEquals(2, group.getSigsRequiredToSpend());
|
||||
assertEquals(2, group.getActiveKeyChain().getSigsRequiredToSpend());
|
||||
|
||||
List<Protos.Key> protoKeys = group.serializeToProtobuf();
|
||||
KeyChainGroup group2 = KeyChainGroup.fromProtobufUnencrypted(params, protoKeys, 2);
|
||||
KeyChainGroup group2 = KeyChainGroup.fromProtobufUnencrypted(params, protoKeys);
|
||||
assertTrue(group2.isMarried());
|
||||
assertEquals(2, group.getSigsRequiredToSpend());
|
||||
assertEquals(2, group.getActiveKeyChain().getSigsRequiredToSpend());
|
||||
Address address2 = group2.currentAddress(KeyChain.KeyPurpose.RECEIVE_FUNDS);
|
||||
assertEquals(address1, address2);
|
||||
}
|
||||
@ -465,23 +475,10 @@ public class KeyChainGroupTest {
|
||||
@Test
|
||||
public void addFollowingAccounts() throws Exception {
|
||||
assertFalse(group.isMarried());
|
||||
group.addFollowingAccountKeys(ImmutableList.of(watchingAccountKey));
|
||||
group.addAndActivateHDChain(createMarriedKeyChain());
|
||||
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
|
||||
public void constructFromSeed() throws Exception {
|
||||
ECKey key1 = group.freshKey(KeyChain.KeyPurpose.RECEIVE_FUNDS);
|
||||
@ -519,7 +516,7 @@ public class KeyChainGroupTest {
|
||||
DeterministicSeed seed1 = group.getActiveKeyChain().getSeed();
|
||||
assertNotNull(seed1);
|
||||
|
||||
group = KeyChainGroup.fromProtobufUnencrypted(params, protobufs, 1);
|
||||
group = KeyChainGroup.fromProtobufUnencrypted(params, protobufs);
|
||||
group.upgradeToDeterministic(0, null); // Should give same result as last time.
|
||||
DeterministicKey dkey2 = group.freshKey(KeyChain.KeyPurpose.RECEIVE_FUNDS);
|
||||
DeterministicSeed seed2 = group.getActiveKeyChain().getSeed();
|
||||
|
@ -28,6 +28,7 @@ deterministic_key {
|
||||
path: 0
|
||||
issued_subkeys: 2
|
||||
lookahead_size: 10
|
||||
sigsRequiredToSpend: 1
|
||||
}
|
||||
|
||||
type: DETERMINISTIC_KEY
|
||||
@ -39,6 +40,7 @@ deterministic_key {
|
||||
path: 1
|
||||
issued_subkeys: 1
|
||||
lookahead_size: 10
|
||||
sigsRequiredToSpend: 1
|
||||
}
|
||||
|
||||
type: DETERMINISTIC_KEY
|
||||
|
@ -14,6 +14,7 @@ deterministic_key {
|
||||
path: 0
|
||||
issued_subkeys: 2
|
||||
lookahead_size: 10
|
||||
sigsRequiredToSpend: 1
|
||||
}
|
||||
|
||||
type: DETERMINISTIC_KEY
|
||||
@ -24,6 +25,7 @@ deterministic_key {
|
||||
path: 1
|
||||
issued_subkeys: 1
|
||||
lookahead_size: 10
|
||||
sigsRequiredToSpend: 1
|
||||
}
|
||||
|
||||
type: DETERMINISTIC_KEY
|
||||
|
@ -66,6 +66,10 @@ message DeterministicKey {
|
||||
* a single P2SH multisignature address
|
||||
*/
|
||||
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
|
||||
repeated TransactionSigner transaction_signers = 17;
|
||||
|
||||
// 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.
|
||||
optional uint32 sigsRequiredToSpend = 18 [default = 1];
|
||||
|
||||
// Next tag: 19
|
||||
// Next tag: 18
|
||||
}
|
||||
|
||||
/** An exchange rate between Bitcoin and some fiat currency. */
|
||||
|
@ -48,6 +48,8 @@ import joptsimple.OptionParser;
|
||||
import joptsimple.OptionSet;
|
||||
import joptsimple.OptionSpec;
|
||||
import joptsimple.util.DateConverter;
|
||||
|
||||
import org.bitcoinj.wallet.MarriedKeyChain;
|
||||
import org.bitcoinj.wallet.Protos;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
@ -59,6 +61,7 @@ import java.io.*;
|
||||
import java.math.BigInteger;
|
||||
import java.net.InetAddress;
|
||||
import java.net.UnknownHostException;
|
||||
import java.security.SecureRandom;
|
||||
import java.text.ParseException;
|
||||
import java.util.Date;
|
||||
import java.util.List;
|
||||
@ -428,7 +431,11 @@ public class WalletTool {
|
||||
for (String xpubkey : xpubkeys) {
|
||||
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 {
|
||||
|
Loading…
Reference in New Issue
Block a user