org.bitcoinj.wallet.AnyDeterministicKeyChain Maven / Gradle / Ivy
/*
* Copyright by the original author or authors.
* Copyright 2023 Dash Core Group
*
* 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.base.MoreObjects;
import com.google.common.base.Stopwatch;
import com.google.common.collect.ImmutableList;
import com.google.common.collect.Iterators;
import com.google.common.collect.PeekingIterator;
import com.google.protobuf.ByteString;
import org.bitcoinj.core.BloomFilter;
import org.bitcoinj.core.NetworkParameters;
import org.bitcoinj.core.Utils;
import org.bitcoinj.crypto.ChildNumber;
import org.bitcoinj.crypto.DeterministicKey;
import org.bitcoinj.crypto.EncryptedData;
import org.bitcoinj.crypto.ExtendedChildNumber;
import org.bitcoinj.crypto.HDKeyDerivation;
import org.bitcoinj.crypto.HDUtils;
import org.bitcoinj.crypto.IDeterministicKey;
import org.bitcoinj.crypto.IKey;
import org.bitcoinj.crypto.KeyCrypter;
import org.bitcoinj.crypto.KeyCrypterException;
import org.bitcoinj.crypto.KeyCrypterScrypt;
import org.bitcoinj.crypto.KeyType;
import org.bitcoinj.crypto.MnemonicCode;
import org.bitcoinj.crypto.AnyDeterministicHierarchy;
import org.bitcoinj.crypto.factory.ECKeyFactory;
import org.bitcoinj.crypto.factory.KeyFactory;
import org.bitcoinj.script.Script;
import org.bitcoinj.utils.ListenerRegistration;
import org.bitcoinj.utils.Threading;
import org.bitcoinj.wallet.listeners.KeyChainEventListener;
import org.bouncycastle.crypto.params.KeyParameter;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import javax.annotation.Nullable;
import java.security.SecureRandom;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Iterator;
import java.util.LinkedList;
import java.util.List;
import java.util.Locale;
import java.util.Map;
import java.util.concurrent.Executor;
import java.util.concurrent.locks.ReentrantLock;
import static com.google.common.base.Preconditions.checkArgument;
import static com.google.common.base.Preconditions.checkNotNull;
import static com.google.common.base.Preconditions.checkState;
import static com.google.common.collect.Lists.newArrayList;
import static com.google.common.collect.Lists.newLinkedList;
/**
* A deterministic key chain is a {@link KeyChain} that uses the
* BIP 32 standard, as implemented by
* {@link AnyDeterministicHierarchy}, to derive all the keys in the keychain from a master seed.
* This type of wallet is extremely convenient and flexible. Although backing up full wallet files is always a good
* idea, to recover money only the root seed needs to be preserved and that is a number small enough that it can be
* written down on paper or, when represented using a BIP 39 {@link MnemonicCode},
* dictated over the phone (possibly even memorized).
*
* Deterministic key chains have other advantages: parts of the key tree can be selectively revealed to allow
* for auditing, and new public keys can be generated without access to the private keys, yielding a highly secure
* configuration for web servers which can accept payments into a wallet but not spend from them. This does not work
* quite how you would expect due to a quirk of elliptic curve mathematics and the techniques used to deal with it.
* A watching wallet is not instantiated using the public part of the master key as you may imagine. Instead, you
* need to take the account key (first child of the master key) and provide the public part of that to the watching
* wallet instead. You can do this by calling {@link #getWatchingKey()} and then serializing it with
* {@link IDeterministicKey#serializePubB58(NetworkParameters)}. The resulting "xpub..." string encodes
* sufficient information about the account key to create a watching chain via
* {@link KeyFactory#deserializeB58(IDeterministicKey, String, NetworkParameters)}
* (with null as the first parameter) and then
* {@link Builder#watch(IDeterministicKey)}.
*
* This class builds on {@link AnyDeterministicHierarchy} and
* {@link IDeterministicKey} by adding support for serialization to and from protobufs,
* and encryption of parts of the key tree. Internally it arranges itself as per the BIP 32 spec, with the seed being
* used to derive a master key, which is then used to derive an account key, the account key is used to derive two
* child keys called the internal and external parent keys (for change and handing out addresses respectively)
* and finally the actual leaf keys that users use hanging off the end. The leaf keys are special in that they don't
* internally store the private part at all, instead choosing to rederive the private key from the parent when
* needed for signing. This simplifies the design for encrypted key chains.
*
* The key chain manages a lookahead zone. This zone is required because when scanning the chain, you don't
* know exactly which keys might receive payments. The user may have handed out several addresses and received payments
* on them, but for latency reasons the block chain is requested from remote peers in bulk, meaning you must
* "look ahead" when calculating keys to put in the Bloom filter. The default lookahead zone is 100 keys, meaning if
* the user hands out more than 100 addresses and receives payment on them before the chain is next scanned, some
* transactions might be missed. 100 is a reasonable choice for consumer wallets running on CPU constrained devices.
* For industrial wallets that are receiving keys all the time, a higher value is more appropriate. Ideally DKC and the
* wallet would know how to adjust this value automatically, but that's not implemented at the moment.
*
* In fact the real size of the lookahead zone is larger than requested, by default, it's one third larger. This
* is because the act of deriving new keys means recalculating the Bloom filters and this is an expensive operation.
* Thus, to ensure we don't have to recalculate on every single new key/address requested or seen we add more buffer
* space and only extend the lookahead zone when that buffer is exhausted. For example with a lookahead zone of 100
* keys, you can request 33 keys before more keys will be calculated and the Bloom filter rebuilt and rebroadcast.
* But even when you are requesting the 33rd key, you will still be looking 100 keys ahead.
*
*
* @author Andreas Schildbach
*/
@SuppressWarnings("PublicStaticCollectionField")
public class AnyDeterministicKeyChain implements IEncryptableKeyChain {
private static final Logger log = LoggerFactory.getLogger(AnyDeterministicKeyChain.class);
public static final String DEFAULT_PASSPHRASE_FOR_MNEMONIC = "";
protected final ReentrantLock lock = Threading.lock("AnyDeterministicKeyChain");
protected AnyDeterministicHierarchy hierarchy;
@Nullable private IDeterministicKey rootKey;
@Nullable private DeterministicSeed seed;
private final Script.ScriptType outputScriptType;
private final ImmutableList accountPath;
protected final KeyFactory keyFactory;
protected final boolean hardenedKeysOnly;
protected boolean externalKeysOnly;
// Paths through the key tree. External keys are ones that are communicated to other parties. Internal keys are
// keys created for change addresses, coinbases, mixing, etc - anything that isn't communicated. The distinction
// is somewhat arbitrary but can be useful for audits. The first number is the "account number" but we don't use
// that feature yet. In future we might hand out different accounts for cases where we wish to hand payers
// a payment request that can generate lots of addresses independently.
// The account path may be overridden by subclasses.
// m / 0'
public static final ImmutableList ACCOUNT_ZERO_PATH = ImmutableList.of(ChildNumber.ZERO_HARDENED);
// m / 1'
public static final ImmutableList ACCOUNT_ONE_PATH = ImmutableList.of(ChildNumber.ONE_HARDENED);
// m / 44' / 0' / 0'
public static final ImmutableList BIP44_ACCOUNT_ZERO_PATH = ImmutableList.of(new ChildNumber(44, true),
ChildNumber.FIVE_HARDENED, ChildNumber.ZERO_HARDENED);
public static final ImmutableList BIP44_ACCOUNT_ZERO_PATH_TESTNET = ImmutableList.of(new ChildNumber(44, true),
ChildNumber.ONE_HARDENED, ChildNumber.ZERO_HARDENED);
// m / 9' / 5' / 3' / 0' - 1000 DASH for masternode
public static final ImmutableList MASTERNODE_HOLDINGS_PATH = ImmutableList.of(new ChildNumber(9, true),
ChildNumber.FIVE_HARDENED, new ChildNumber(3, true), ChildNumber.ZERO_HARDENED);
public static final ImmutableList MASTERNODE_HOLDINGS_PATH_TESTNET = ImmutableList.of(new ChildNumber(9, true),
ChildNumber.ONE_HARDENED, new ChildNumber(3, true), ChildNumber.ZERO_HARDENED);
// m / 9' / 5' / 3' / 1' - Masternode Voting Path
public static final ImmutableList PROVIDER_VOTING_PATH = ImmutableList.of(new ChildNumber(9, true),
ChildNumber.FIVE_HARDENED, new ChildNumber(3, true), ChildNumber.ONE_HARDENED);
public static final ImmutableList PROVIDER_VOTING_PATH_TESTNET = ImmutableList.of(new ChildNumber(9, true),
ChildNumber.ONE_HARDENED, new ChildNumber(3, true), ChildNumber.ONE_HARDENED);
// m / 9' / 5' / 3' / 2' - Masternode Owner Path
public static final ImmutableList PROVIDER_OWNER_PATH = ImmutableList.of(new ChildNumber(9, true),
ChildNumber.FIVE_HARDENED, new ChildNumber(3, true), new ChildNumber(2, true));
public static final ImmutableList PROVIDER_OWNER_PATH_TESTNET = ImmutableList.of(new ChildNumber(9, true),
ChildNumber.ONE_HARDENED, new ChildNumber(3, true), new ChildNumber(2, true));
// m / 9' / 5' / 3' / 3' - Masternode Operator Path
public static final ImmutableList PROVIDER_OPERATOR_PATH = ImmutableList.of(new ChildNumber(9, true),
ChildNumber.FIVE_HARDENED, new ChildNumber(3, true), new ChildNumber(3, true));
public static final ImmutableList PROVIDER_OPERATOR_PATH_TESTNET = ImmutableList.of(new ChildNumber(9, true),
ChildNumber.ONE_HARDENED, new ChildNumber(3, true), new ChildNumber(3, true));
// m / 9' / 5' / 5' / 0' - Blockchain User Path
public static final ImmutableList BLOCKCHAIN_USER_PATH = ImmutableList.of(new ChildNumber(9, true),
ChildNumber.FIVE_HARDENED, new ChildNumber(5, true), new ChildNumber(0, true));
public static final ImmutableList BLOCKCHAIN_USER_PATH_TESTNET = ImmutableList.of(new ChildNumber(9, true),
ChildNumber.ONE_HARDENED, new ChildNumber(5, true), new ChildNumber(0, true));
public static final ImmutableList EXTERNAL_SUBPATH = ImmutableList.of(ChildNumber.ZERO);
public static final ImmutableList INTERNAL_SUBPATH = ImmutableList.of(ChildNumber.ONE);
public static final ImmutableList EXTERNAL_SUBPATH_HARDENED = ImmutableList.of(ChildNumber.ZERO_HARDENED);
public static final ImmutableList INTERNAL_SUBPATH_HARDENED = ImmutableList.of(ChildNumber.ONE_HARDENED);
// We try to ensure we have at least this many keys ready and waiting to be handed out via getKey().
// See docs for getLookaheadSize() for more info on what this is for. The -1 value means it hasn't been calculated
// 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;
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.
protected int lookaheadThreshold = calcDefaultLookaheadThreshold();
private int calcDefaultLookaheadThreshold() {
return lookaheadSize / 3;
}
// The parent keys for external keys (handed out to other people) and internal keys (used for change addresses).
private IDeterministicKey externalParentKey, internalParentKey;
// How many keys on each path have actually been used. This may be fewer than the number that have been deserialized
// or held in memory, because of the lookahead zone.
protected int issuedExternalKeys, issuedInternalKeys;
// A counter that is incremented each time a key in the lookahead threshold zone is marked as used and lookahead
// is triggered. The Wallet/KCG reads these counters and combines them so it can tell the Peer whether to throw
// away the current block (and any future blocks in the same download batch) and restart chain sync once a new
// filter has been calculated. This field isn't persisted to the wallet as it's only relevant within a network
// session.
protected int keyLookaheadEpoch;
// We simplify by wrapping a basic key chain and that way we get some functionality like key lookup and event
// listeners "for free". All keys in the key tree appear here, even if they aren't meant to be used for receiving
// money.
protected final AnyBasicKeyChain basicKeyChain;
// If set this chain is following another chain in a married KeyChainGroup
protected 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> {
protected SecureRandom random;
protected int bits = DeterministicSeed.DEFAULT_SEED_ENTROPY_BITS;
protected String passphrase;
protected long creationTimeSecs = 0;
protected byte[] entropy;
protected DeterministicSeed seed;
protected Script.ScriptType outputScriptType = Script.ScriptType.P2PKH;
protected IDeterministicKey watchingKey = null;
protected boolean isFollowing = false;
protected IDeterministicKey spendingKey = null;
protected ImmutableList accountPath = null;
protected KeyFactory keyFactory = ECKeyFactory.get();
protected boolean hardenedKeysOnly = false;
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, long creationTimeSecs) {
this.entropy = entropy;
this.creationTimeSecs = creationTimeSecs;
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 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 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();
}
/**
* Creates a key chain that watches the given account key.
*/
public T watch(IDeterministicKey accountKey) {
checkState(accountPath == null, "either watch or accountPath");
this.watchingKey = accountKey;
this.isFollowing = false;
this.keyFactory = accountKey.getKeyFactory();
return self();
}
/**
* Creates a deterministic key chain with the given watch key and that follows some other keychain. In a married
* wallet following keychain represents "spouse". Watch key has to be an account key.
*/
public T watchAndFollow(IDeterministicKey accountKey) {
checkState(accountPath == null, "either watchAndFollow or accountPath");
this.watchingKey = accountKey;
this.isFollowing = true;
this.keyFactory = accountKey.getKeyFactory();
return self();
}
/**
* Creates a key chain that can spend from the given account key.
*/
public T spend(IDeterministicKey accountKey) {
checkState(accountPath == null, "either spend or accountPath");
this.spendingKey = accountKey;
this.isFollowing = false;
return self();
}
public T outputScriptType(Script.ScriptType outputScriptType) {
this.outputScriptType = outputScriptType;
return self();
}
/** The passphrase to use with the generated mnemonic, or null if you would like to use the default empty string. Currently must be the empty string. */
public T passphrase(String passphrase) {
// FIXME support non-empty passphrase
this.passphrase = passphrase;
return self();
}
/**
* Use an account path other than the default {@link AnyDeterministicKeyChain#ACCOUNT_ZERO_PATH}.
*/
public T accountPath(ImmutableList accountPath) {
checkState(watchingKey == null, "either watch or accountPath");
this.accountPath = checkNotNull(accountPath);
return self();
}
public T keyFactory(KeyFactory keyFactory) {
this.keyFactory = keyFactory;
return self();
}
public T hardenedKeysOnly(boolean hardenedKeysOnly) {
this.hardenedKeysOnly = hardenedKeysOnly;
return self();
}
public AnyDeterministicKeyChain build() {
checkState(passphrase == null || seed == null, "Passphrase must not be specified with seed");
if (accountPath == null)
accountPath = ACCOUNT_ZERO_PATH;
if (random != null)
// Default passphrase to "" if not specified
return new AnyDeterministicKeyChain(new DeterministicSeed(random, bits, getPassphrase()), null,
outputScriptType, accountPath, keyFactory, hardenedKeysOnly, false);
else if (entropy != null)
return new AnyDeterministicKeyChain(new DeterministicSeed(entropy, getPassphrase(), creationTimeSecs),
null, outputScriptType, accountPath, keyFactory, hardenedKeysOnly, false);
else if (seed != null)
return new AnyDeterministicKeyChain(seed, null, outputScriptType, accountPath, keyFactory, hardenedKeysOnly, false);
else if (watchingKey != null)
return new AnyDeterministicKeyChain(watchingKey, isFollowing, true, outputScriptType, hardenedKeysOnly, false);
else if (spendingKey != null)
return new AnyDeterministicKeyChain(spendingKey, false, false, outputScriptType, hardenedKeysOnly, false);
else
throw new IllegalStateException();
}
protected String getPassphrase() {
return passphrase != null ? passphrase : DEFAULT_PASSPHRASE_FOR_MNEMONIC;
}
}
public static Builder> builder() {
return new Builder();
}
/**
*
* Creates a deterministic key chain from a watched or spendable account key. If {@code isWatching} flag is set,
* then creates a deterministic key chain that watches the given (public only) root key. You can use this to
* calculate balances and generally follow along, but spending is not possible with such a chain. If it is not set,
* then this creates a deterministic key chain that allows spending. If {@code isFollowing} flag is set(only allowed
* if {@code isWatching} is set) then this keychain follows some other keychain. In a married wallet following
* keychain represents "spouse's" keychain.
*
*
*
* This constructor is not stable across releases! If you need a stable API, use {@link #builder()} to use a
* {@link Builder}.
*
*/
public AnyDeterministicKeyChain(IDeterministicKey key, boolean isFollowing, boolean isWatching,
Script.ScriptType outputScriptType, boolean hardenedKeysOnly, boolean externalKeysOnly) {
this.hardenedKeysOnly = hardenedKeysOnly;
this.externalKeysOnly = externalKeysOnly;
if (isWatching)
checkArgument(key.isPubKeyOnly(), "Private subtrees not currently supported for watching keys: if you got this key from DKC.getWatchingKey() then use .dropPrivate().dropParent() on it first.");
else
checkArgument(key.hasPrivKey(), "Private subtrees are required.");
checkArgument(isWatching || !isFollowing, "Can only follow a key that is watched");
this.keyFactory = key.getKeyFactory();
basicKeyChain = new AnyBasicKeyChain(keyFactory);
this.seed = null;
this.rootKey = null;
basicKeyChain.importKey(key);
hierarchy = new AnyDeterministicHierarchy(key);
this.accountPath = key.getPath();
this.outputScriptType = outputScriptType;
initializeHierarchyUnencrypted(key);
this.isFollowing = isFollowing;
}
public AnyDeterministicKeyChain(IDeterministicKey key, boolean isFollowing, boolean isWatching,
Script.ScriptType outputScriptType, ImmutableList accountPath,
boolean hardenedKeysOnly, boolean externalKeysOnly) {
this.hardenedKeysOnly = hardenedKeysOnly;
this.externalKeysOnly = externalKeysOnly;
if (isWatching)
checkArgument(key.isPubKeyOnly(), "Private subtrees not currently supported for watching keys: if you got this key from DKC.getWatchingKey() then use .dropPrivate().dropParent() on it first.");
else
checkArgument(key.hasPrivKey(), "Private subtrees are required.");
checkArgument(isWatching || !isFollowing, "Can only follow a key that is watched");
this.keyFactory = key.getKeyFactory();
basicKeyChain = new AnyBasicKeyChain(keyFactory);
this.seed = null;
this.rootKey = null;
basicKeyChain.importKey(key);
hierarchy = new AnyDeterministicHierarchy(key);
this.accountPath = accountPath;
this.outputScriptType = outputScriptType;
initializeHierarchyUnencrypted(key);
this.isFollowing = isFollowing;
}
/**
*
* Creates a deterministic key chain with an encrypted deterministic seed using the provided account path. Using
* {@link KeyCrypter KeyCrypter} to decrypt.
*
*
*
* This constructor is not stable across releases! If you need a stable API, use {@link #builder()} to use a
* {@link Builder}.
*
*/
protected AnyDeterministicKeyChain(DeterministicSeed seed, @Nullable KeyCrypter crypter,
Script.ScriptType outputScriptType, ImmutableList accountPath,
KeyFactory keyFactory, boolean hardenedKeysOnly, boolean externalKeysOnly) {
this.hardenedKeysOnly = hardenedKeysOnly;
this.externalKeysOnly = externalKeysOnly;
checkArgument(outputScriptType == null || outputScriptType == Script.ScriptType.P2PKH,
"Only P2PKH is allowed.");
this.outputScriptType = outputScriptType != null ? outputScriptType : Script.ScriptType.P2PKH;
this.accountPath = accountPath;
this.seed = seed;
this.keyFactory = keyFactory;
basicKeyChain = new AnyBasicKeyChain(crypter, keyFactory);
if (!seed.isEncrypted()) {
rootKey = keyFactory.createMasterPrivateKey(checkNotNull(seed.getSeedBytes()));
rootKey.setCreationTimeSeconds(seed.getCreationTimeSeconds());
basicKeyChain.importKey(rootKey);
hierarchy = new AnyDeterministicHierarchy(rootKey);
for (int i = 1; i <= getAccountPath().size(); i++) {
basicKeyChain.importKey(hierarchy.get(getAccountPath().subList(0, i), false, true));
}
initializeHierarchyUnencrypted(rootKey);
}
// Else...
// We can't initialize ourselves with just an encrypted seed, so we expected deserialization code to do the
// rest of the setup (loading the root key).
}
/**
* For use in encryption when {@link #toEncrypted(KeyCrypter, KeyParameter)} is called, so that
* subclasses can override that method and create an instance of the right class.
*
* See also {@link #makeKeyChainFromSeed(DeterministicSeed, ImmutableList, Script.ScriptType)}
*/
protected AnyDeterministicKeyChain(KeyCrypter crypter, KeyParameter aesKey, AnyDeterministicKeyChain chain,
boolean hardenedKeysOnly, boolean externalKeysOnly) {
this.hardenedKeysOnly = hardenedKeysOnly;
this.externalKeysOnly = externalKeysOnly;
// Can't encrypt a watching chain.
checkNotNull(chain.rootKey);
checkNotNull(chain.seed);
checkArgument(!chain.rootKey.isEncrypted(), "Chain already encrypted");
this.accountPath = chain.getAccountPath();
this.outputScriptType = chain.outputScriptType;
this.issuedExternalKeys = chain.issuedExternalKeys;
this.issuedInternalKeys = chain.issuedInternalKeys;
this.lookaheadSize = chain.lookaheadSize;
this.lookaheadThreshold = chain.lookaheadThreshold;
this.seed = chain.seed.encrypt(crypter, aesKey);
this.keyFactory = chain.keyFactory;
basicKeyChain = new AnyBasicKeyChain(crypter, keyFactory);
// The first number is the "account number" but we don't use that feature.
rootKey = chain.rootKey.encrypt(crypter, aesKey, null);
hierarchy = new AnyDeterministicHierarchy(rootKey);
basicKeyChain.importKey(rootKey);
for (int i = 1; i < getAccountPath().size(); i++) {
IDeterministicKey parent = hierarchy.get(getAccountPath().subList(0, i - 1), false, false);
encryptNonLeaf(aesKey, chain, parent, getAccountPath().subList(0, i));
}
IDeterministicKey accountParent = hierarchy.get(getAccountPath().subList(0, getAccountPath().size() - 1), false, false);
IDeterministicKey account = encryptNonLeaf(aesKey, chain, accountParent, getAccountPath());
externalParentKey = encryptNonLeaf(aesKey, chain, account, HDUtils.concat(getAccountPath(), getExternalPath()));
internalParentKey = encryptNonLeaf(aesKey, chain, account, HDUtils.concat(getAccountPath(), getInternalPath()));
// Now copy the (pubkey only) leaf keys across to avoid rederiving them. The private key bytes are missing
// anyway so there's nothing to encrypt.
for (IKey eckey : chain.basicKeyChain.getKeys()) {
IDeterministicKey key = (IDeterministicKey) eckey;
if (key.getPath().size() != getAccountPath().size() + (externalKeysOnly ? 1 : 2)) continue; // Not a leaf key.
IDeterministicKey parent = hierarchy.get(checkNotNull(key.getParent()).getPath(), false, false);
// Clone the key to the new encrypted hierarchy.
key = keyFactory.fromChildAndParent(key.dropPrivateBytes(), parent);
hierarchy.putKey(key);
basicKeyChain.importKey(key);
}
for (ListenerRegistration listener : chain.basicKeyChain.getListeners()) {
basicKeyChain.addEventListener(listener.listener);
}
}
private ImmutableList getExternalPath() {
return hardenedKeysOnly ? EXTERNAL_SUBPATH_HARDENED : EXTERNAL_SUBPATH;
}
private ImmutableList getInternalPath() {
return hardenedKeysOnly ? INTERNAL_SUBPATH_HARDENED : INTERNAL_SUBPATH;
}
public ImmutableList getAccountPath() {
return accountPath;
}
public Script.ScriptType getOutputScriptType() {
return outputScriptType;
}
private IDeterministicKey encryptNonLeaf(KeyParameter aesKey, AnyDeterministicKeyChain chain,
IDeterministicKey parent, ImmutableList path) {
IDeterministicKey key = chain.hierarchy.get(path, false, false);
key = key.encrypt(checkNotNull(basicKeyChain.getKeyCrypter()), aesKey, parent);
hierarchy.putKey(key);
basicKeyChain.importKey(key);
return key;
}
// Derives the account path keys and inserts them into the basic key chain. This is important to preserve their
// order for serialization, amongst other things.
private void initializeHierarchyUnencrypted(IDeterministicKey baseKey) {
externalParentKey = hierarchy.deriveChild(getAccountPath(), false, false, hardenedKeysOnly ? ChildNumber.ZERO_HARDENED : ChildNumber.ZERO);
internalParentKey = hierarchy.deriveChild(getAccountPath(), false, false, hardenedKeysOnly ? ChildNumber.ONE_HARDENED : ChildNumber.ONE);
basicKeyChain.importKey(externalParentKey);
basicKeyChain.importKey(internalParentKey);
}
/** Returns a freshly derived key that has not been returned by this method before. */
@Override
public IDeterministicKey getKey(KeyChain.KeyPurpose purpose) {
return getKeys(purpose, 1).get(0);
}
/** Returns freshly derived key/s that have not been returned by this method before. */
@Override
public List getKeys(KeyChain.KeyPurpose purpose, int numberOfKeys) {
checkArgument(numberOfKeys > 0);
lock.lock();
try {
IDeterministicKey parentKey;
int index;
switch (purpose) {
// Map both REFUND and RECEIVE_KEYS to the same branch for now. Refunds are a feature of the BIP 70
// payment protocol. Later we may wish to map it to a different branch (in a new wallet version?).
// This would allow a watching wallet to only be able to see inbound payments, but not change
// (i.e. spends) or refunds. Might be useful for auditing ...
case RECEIVE_FUNDS:
case REFUND:
issuedExternalKeys += numberOfKeys;
index = issuedExternalKeys;
parentKey = externalParentKey;
break;
case AUTHENTICATION:
case CHANGE:
issuedInternalKeys += numberOfKeys;
index = issuedInternalKeys;
parentKey = internalParentKey;
break;
default:
throw new UnsupportedOperationException();
}
// Optimization: potentially do a very quick key generation for just the number of keys we need if we
// didn't already create them, ignoring the configured lookahead size. This ensures we'll be able to
// retrieve the keys in the following loop, but if we're totally fresh and didn't get a chance to
// calculate the lookahead keys yet, this will not block waiting to calculate 100+ EC point multiplies.
// On slow/crappy Android phones looking ahead 100 keys can take ~5 seconds but the OS will kill us
// if we block for just one second on the UI thread. Because UI threads may need an address in order
// to render the screen, we need getKeys to be fast even if the wallet is totally brand new and lookahead
// didn't happen yet.
//
// It's safe to do this because when a network thread tries to calculate a Bloom filter, we'll go ahead
// and calculate the full lookahead zone there, so network requests will always use the right amount.
List lookahead = maybeLookAhead(parentKey, index, 0, 0);
basicKeyChain.importKeys(lookahead);
List keys = new ArrayList<>(numberOfKeys);
for (int i = 0; i < numberOfKeys; i++) {
ImmutableList path = HDUtils.append(parentKey.getPath(), new ChildNumber(index - numberOfKeys + i, hardenedKeysOnly));
IDeterministicKey k = hierarchy.get(path, false, false);
if (k.getKeyFactory().getKeyType() == KeyType.ECDSA && !hardenedKeysOnly) {
// Just a last minute sanity check before we hand the key out to the app for usage. This isn't inspired
// by any real problem reports from bitcoinj users, but I've heard of cases via the grapevine of
// places that lost money due to bitflips causing addresses to not match keys. Of course in an
// environment with flaky RAM there's no real way to always win: bitflips could be introduced at any
// other layer. But as we're potentially retrieving from long term storage here, check anyway.
checkForBitFlip((DeterministicKey) k);
}
keys.add(k);
}
return keys;
} finally {
lock.unlock();
}
}
protected void checkForBitFlip(DeterministicKey k) {
DeterministicKey parent = checkNotNull(k.getParent());
byte[] rederived = HDKeyDerivation.deriveChildKeyBytesFromPublic(parent, k.getChildNumber(), HDKeyDerivation.PublicDeriveMode.WITH_INVERSION).keyBytes;
byte[] actual = k.getPubKey();
if (!Arrays.equals(rederived, actual))
throw new IllegalStateException(String.format(Locale.US, "Bit-flip check failed: %s vs %s", Arrays.toString(rederived), Arrays.toString(actual)));
}
public boolean hasHardenedKeysOnly() {
return hardenedKeysOnly;
}
/**
* Mark the IDeterministicKey as used.
* Also correct the issued{Internal|External}Keys counter, because all lower children seem to be requested already.
* If the counter was updated, we also might trigger lookahead.
*/
public IDeterministicKey markKeyAsUsed(IDeterministicKey k) {
int numChildren = k.getChildNumber().i() + 1;
if (k.getParent() == internalParentKey) {
if (issuedInternalKeys < numChildren) {
issuedInternalKeys = numChildren;
maybeLookAhead();
}
} else if (k.getParent() == externalParentKey) {
if (issuedExternalKeys < numChildren) {
issuedExternalKeys = numChildren;
maybeLookAhead();
}
}
return k;
}
public IDeterministicKey findKeyFromPubHash(byte[] pubkeyHash) {
lock.lock();
try {
return (IDeterministicKey) basicKeyChain.findKeyFromPubHash(pubkeyHash);
} finally {
lock.unlock();
}
}
public IDeterministicKey findKeyFromPubKey(byte[] pubkey) {
lock.lock();
try {
return (IDeterministicKey) basicKeyChain.findKeyFromPubKey(pubkey);
} finally {
lock.unlock();
}
}
/**
* Mark the DeterministicKeys as used, if they match the pubkeyHash
* See {@link AnyDeterministicKeyChain#markKeyAsUsed(IDeterministicKey)} for more info on this.
*/
@Nullable
public IDeterministicKey markPubHashAsUsed(byte[] pubkeyHash) {
lock.lock();
try {
IDeterministicKey k = (IDeterministicKey) basicKeyChain.findKeyFromPubHash(pubkeyHash);
if (k != null)
markKeyAsUsed(k);
return k;
} finally {
lock.unlock();
}
}
/**
* Mark the DeterministicKeys as used, if they match the pubkey
* See {@link AnyDeterministicKeyChain#markKeyAsUsed(IDeterministicKey)} for more info on this.
*/
@Nullable
public IDeterministicKey markPubKeyAsUsed(byte[] pubkey) {
lock.lock();
try {
IDeterministicKey k = (IDeterministicKey) basicKeyChain.findKeyFromPubKey(pubkey);
if (k != null)
markKeyAsUsed(k);
return k;
} finally {
lock.unlock();
}
}
@Override
public boolean hasKey(IKey key) {
lock.lock();
try {
return basicKeyChain.hasKey(key);
} finally {
lock.unlock();
}
}
/** Returns the deterministic key for the given absolute path in the hierarchy. */
protected IDeterministicKey getKeyByPath(ChildNumber... path) {
return getKeyByPath(ImmutableList.copyOf(path));
}
/** Returns the deterministic key for the given absolute path in the hierarchy. */
public IDeterministicKey getKeyByPath(List path) {
return getKeyByPath(path, false);
}
/** Returns the deterministic key for the given absolute path in the hierarchy, optionally creating it */
public IDeterministicKey getKeyByPath(List path, boolean create) {
return hierarchy.get(path, false, create);
}
/**
* An alias for {@code getKeyByPath(getAccountPath())}.
*
* Use this when you would like to create a watching key chain that follows this one, but can't spend money from it.
* The returned key can be serialized and then passed into {@link Builder#watch(IDeterministicKey)}
* on another system to watch the hierarchy.
*
* Note that the returned key is not pubkey only unless this key chain already is: the returned key can still
* be used for signing etc if the private key bytes are available.
*/
public IDeterministicKey getWatchingKey() {
return getKeyByPath(getAccountPath());
}
/** Returns true if this chain is watch only, meaning it has public keys but no private key. */
public boolean isWatching() {
return getWatchingKey().isWatching();
}
@Override
public int numKeys() {
// We need to return here the total number of keys including the lookahead zone, not the number of keys we
// have issued via getKey/freshReceiveKey.
lock.lock();
try {
maybeLookAhead();
return basicKeyChain.numKeys();
} finally {
lock.unlock();
}
}
/**
* Returns number of leaf keys used including both internal and external paths. This may be fewer than the number
* that have been deserialized or held in memory, because of the lookahead zone.
*/
public int numLeafKeysIssued() {
lock.lock();
try {
return issuedExternalKeys + issuedInternalKeys;
} finally {
lock.unlock();
}
}
@Override
public long getEarliestKeyCreationTime() {
if (seed != null)
return seed.getCreationTimeSeconds();
else
return getWatchingKey().getCreationTimeSeconds();
}
@Override
public void addEventListener(KeyChainEventListener listener) {
basicKeyChain.addEventListener(listener);
}
@Override
public void addEventListener(KeyChainEventListener listener, Executor executor) {
basicKeyChain.addEventListener(listener, executor);
}
@Override
public boolean removeEventListener(KeyChainEventListener listener) {
return basicKeyChain.removeEventListener(listener);
}
/** Returns a list of words that represent the seed or null if this chain is a watching chain. */
@Nullable
public List getMnemonicCode() {
if (seed == null) return null;
lock.lock();
try {
return seed.getMnemonicCode();
} finally {
lock.unlock();
}
}
/**
* Return true if this keychain is following another keychain
*/
public boolean isFollowing() {
return isFollowing;
}
//////////////////////////////////////////////////////////////////////////////////////////////////////////////////
//
// Serialization support
//
//////////////////////////////////////////////////////////////////////////////////////////////////////////////////
@Override
public List serializeToProtobuf() {
List result = newArrayList();
lock.lock();
try {
result.addAll(serializeMyselfToProtobuf());
} finally {
lock.unlock();
}
return result;
}
protected List 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 entries = newLinkedList();
if (seed != null) {
Protos.Key.Builder mnemonicEntry = AnyBasicKeyChain.serializeEncryptableItem(seed);
mnemonicEntry.setType(Protos.Key.Type.DETERMINISTIC_MNEMONIC);
serializeSeedEncryptableItem(seed, mnemonicEntry);
/*for (ChildNumber childNumber : getAccountPath()) {
mnemonicEntry.addAccountPath(childNumber.i());
}*/
setPathOrExtendedPath(accountPath, mnemonicEntry);
entries.add(mnemonicEntry.build());
}
Map keys = basicKeyChain.serializeToEditableProtobufs();
for (Map.Entry entry : keys.entrySet()) {
IDeterministicKey key = (IDeterministicKey) entry.getKey();
Protos.Key.Builder proto = entry.getValue();
proto.setType(Protos.Key.Type.DETERMINISTIC_KEY);
final Protos.DeterministicKey.Builder detKey = proto.getDeterministicKey().toBuilder();
detKey.setChainCode(ByteString.copyFrom(key.getChainCode()));
setPathOrExtendedPath(key, detKey);
if (key.equals(externalParentKey)) {
detKey.setIssuedSubkeys(issuedExternalKeys);
detKey.setLookaheadSize(lookaheadSize);
detKey.setSigsRequiredToSpend(getSigsRequiredToSpend());
} else if (key.equals(internalParentKey)) {
detKey.setIssuedSubkeys(issuedInternalKeys);
detKey.setLookaheadSize(lookaheadSize);
detKey.setSigsRequiredToSpend(getSigsRequiredToSpend());
}
// Flag the very first key of following keychain.
if (entries.isEmpty() && isFollowing()) {
detKey.setIsFollowing(true);
}
proto.setDeterministicKey(detKey);
if (key.getParent() != null) {
// HD keys inherit the timestamp of their parent if they have one, so no need to serialize it.
proto.clearCreationTimestamp();
} else {
proto.setOutputScriptType(Protos.Key.OutputScriptType.valueOf(outputScriptType.name()));
}
entries.add(proto.build());
}
return entries;
}
private void setPathOrExtendedPath(IDeterministicKey key, Protos.DeterministicKey.Builder detKey) {
ImmutableList path = key.getPath();
setPathOrExtendedPath(path, detKey);
}
private void setPathOrExtendedPath(ImmutableList path, Protos.DeterministicKey.Builder detKey) {
if(!pathHasExtendedChildren(path)) {
for (ChildNumber num : path)
detKey.addPath(num.i());
} else for (ChildNumber num : path) {
boolean simple = num instanceof ExtendedChildNumber == false;
Protos.ExtendedChildNumber.Builder builder = Protos.ExtendedChildNumber.newBuilder();
builder.setSimple(simple);
if(simple)
builder.setI(num.i());
else {
ExtendedChildNumber extendedChildNumber = (ExtendedChildNumber)num;
builder.setHardened(extendedChildNumber.isHardened());
builder.setSize(extendedChildNumber.bi().toByteArray().length);
builder.setBi(ByteString.copyFrom(extendedChildNumber.bi().toByteArray()));
}
detKey.addExtendedPath(builder.build());
}
}
private void setPathOrExtendedPath(ImmutableList path, Protos.Key.Builder detKey) {
if(!pathHasExtendedChildren(path)) {
for (ChildNumber num : path)
detKey.addAccountPath(num.i());
} else for (ChildNumber num : path) {
boolean simple = num instanceof ExtendedChildNumber == false;
Protos.ExtendedChildNumber.Builder builder = Protos.ExtendedChildNumber.newBuilder();
builder.setSimple(simple);
if(simple)
builder.setI(num.i());
else {
ExtendedChildNumber extendedChildNumber = (ExtendedChildNumber)num;
builder.setHardened(extendedChildNumber.isHardened());
builder.setSize(extendedChildNumber.bi().toByteArray().length);
builder.setBi(ByteString.copyFrom(extendedChildNumber.bi().toByteArray()));
}
detKey.addExtendedPath(builder.build());
}
}
protected boolean pathHasExtendedChildren(IDeterministicKey key) {
return pathHasExtendedChildren(key.getPath());
}
protected boolean pathHasExtendedChildren(ImmutableList path) {
boolean hasExtendedChildren = false;
for (ChildNumber num : path) {
if (num instanceof ExtendedChildNumber) {
hasExtendedChildren = true;
break;
}
}
return hasExtendedChildren;
}
static List fromProtobuf(List keys, @Nullable KeyCrypter crypter, KeyFactory keyFactory, boolean hardenedKeysOnly) throws UnreadableWalletException {
return fromProtobuf(keys, crypter, new AnyDefaultKeyChainFactory(), keyFactory, hardenedKeysOnly);
}
/**
* Returns all the key chains found in the given list of keys. Typically there will only be one, but in the case of
* key rotation it can happen that there are multiple chains found.
*/
public static List fromProtobuf(List keys, @Nullable KeyCrypter crypter, AnyKeyChainFactory factory, KeyFactory keyFactory, boolean hardenedKeysOnly) throws UnreadableWalletException {
List chains = newLinkedList();
DeterministicSeed seed = null;
AnyDeterministicKeyChain chain = null;
int lookaheadSize = -1;
int sigsRequiredToSpend = 1;
List accountPath = newArrayList();
boolean simple = true;
Script.ScriptType outputScriptType = Script.ScriptType.P2PKH;
PeekingIterator iter = Iterators.peekingIterator(keys.iterator());
while (iter.hasNext()) {
Protos.Key key = iter.next();
final Protos.Key.Type t = key.getType();
if (t == Protos.Key.Type.DETERMINISTIC_MNEMONIC) {
accountPath = newArrayList();
for (int i : key.getAccountPathList()) {
accountPath.add(new ChildNumber(i));
}
if (accountPath.isEmpty()) {
for(Protos.ExtendedChildNumber j : key.getExtendedPathList()) {
if(j.getSimple())
accountPath.add(new ChildNumber(j.getI()));
else {
accountPath.add(new ExtendedChildNumber(j.getBi().toByteArray(), j.getHardened()));
simple = false;
}
}
if(accountPath.isEmpty())
accountPath = ACCOUNT_ZERO_PATH;
}
if (chain != null) {
checkState(lookaheadSize >= 0);
chain.setLookaheadSize(lookaheadSize);
chain.setSigsRequiredToSpend(sigsRequiredToSpend);
chain.maybeLookAhead();
chains.add(chain);
chain = null;
}
long timestamp = key.getCreationTimestamp() / 1000;
String passphrase = DEFAULT_PASSPHRASE_FOR_MNEMONIC; // FIXME allow non-empty passphrase
if (key.hasSecretBytes()) {
if (key.hasEncryptedDeterministicSeed())
throw new UnreadableWalletException("Malformed key proto: " + key.toString());
byte[] seedBytes = null;
if (key.hasDeterministicSeed()) {
seedBytes = key.getDeterministicSeed().toByteArray();
}
seed = new DeterministicSeed(key.getSecretBytes().toStringUtf8(), seedBytes, passphrase, timestamp);
} else if (key.hasEncryptedData()) {
if (key.hasDeterministicSeed())
throw new UnreadableWalletException("Malformed key proto: " + key.toString());
EncryptedData data = new EncryptedData(key.getEncryptedData().getInitialisationVector().toByteArray(),
key.getEncryptedData().getEncryptedPrivateKey().toByteArray());
EncryptedData encryptedSeedBytes = null;
if (key.hasEncryptedDeterministicSeed()) {
Protos.EncryptedData encryptedSeed = key.getEncryptedDeterministicSeed();
encryptedSeedBytes = new EncryptedData(encryptedSeed.getInitialisationVector().toByteArray(),
encryptedSeed.getEncryptedPrivateKey().toByteArray());
}
seed = new DeterministicSeed(data, encryptedSeedBytes, timestamp);
} else {
throw new UnreadableWalletException("Malformed key proto: " + key.toString());
}
if (log.isDebugEnabled())
log.debug("Deserializing: DETERMINISTIC_MNEMONIC: {}", seed);
} else if (t == Protos.Key.Type.DETERMINISTIC_KEY) {
if (!key.hasDeterministicKey())
throw new UnreadableWalletException("Deterministic key missing extra data: " + key.toString());
byte[] chainCode = key.getDeterministicKey().getChainCode().toByteArray();
// Deserialize the path through the tree.
LinkedList path = newLinkedList();
for (int i : key.getDeterministicKey().getPathList())
path.add(new ChildNumber(i));
//load the extended list
if(path.isEmpty()) {
for (Protos.ExtendedChildNumber j : key.getDeterministicKey().getExtendedPathList()) {
if(j.getSimple())
path.add(new ChildNumber(j.getI()));
else {
path.add(new ExtendedChildNumber(j.getBi().toByteArray(), j.getHardened()));
simple = false;
}
}
}
// Deserialize the public key and path.
byte [] pubkey = key.getPublicKey().toByteArray();
final ImmutableList immutablePath = ImmutableList.copyOf(path);
if (key.hasOutputScriptType())
outputScriptType = Script.ScriptType.valueOf(key.getOutputScriptType().name());
// Possibly create the chain, if we didn't already do so yet.
boolean isWatchingAccountKey = false;
boolean isFollowingKey = false;
boolean isSpendingKey = false;
// save previous chain if any if the key is marked as following. Current key and the next ones are to be
// placed in new following key chain
if (key.getDeterministicKey().getIsFollowing()) {
if (chain != null) {
checkState(lookaheadSize >= 0);
chain.setLookaheadSize(lookaheadSize);
chain.setSigsRequiredToSpend(sigsRequiredToSpend);
chain.maybeLookAhead();
chains.add(chain);
chain = null;
seed = null;
}
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 this has a private key but no seed, then all we know is the spending key H
if (seed == null && key.hasSecretBytes()) {
IDeterministicKey accountKey = keyFactory.fromExtended(immutablePath, chainCode, pubkey, key.getSecretBytes().toByteArray(), null);
accountKey.setCreationTimeSeconds(key.getCreationTimestamp() / 1000);
chain = factory.makeSpendingKeyChain(key, iter.peek(), accountKey, isMarried, outputScriptType, hardenedKeysOnly);
isSpendingKey = true;
} else if (seed == null) {
IDeterministicKey accountKey = keyFactory.fromExtended(immutablePath, chainCode, pubkey, null, null);
accountKey.setCreationTimeSeconds(key.getCreationTimestamp() / 1000);
if (simple) {
chain = factory.makeWatchingKeyChain(key, iter.peek(), accountKey, isFollowingKey, isMarried,
outputScriptType);
} else {
chain = factory.makeWatchingFriendKeyChain(accountKey, immutablePath);
}
isWatchingAccountKey = true;
} else {
if (simple)
chain = factory.makeKeyChain(key, iter.peek(), seed, crypter, isMarried,
outputScriptType, ImmutableList. builder().addAll(accountPath).build(), keyFactory, hardenedKeysOnly);
else
chain = factory.makeSpendingFriendKeyChain(key, iter.peek(), seed, crypter, isMarried,
ImmutableList. builder().addAll(accountPath).build(), keyFactory, hardenedKeysOnly);
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
// everything afterwards.
}
}
// Find the parent key assuming this is not the root key, and not an account key for a watching chain.
IDeterministicKey parent = null;
if (!path.isEmpty() && !isWatchingAccountKey && !isSpendingKey) {
ChildNumber index = path.removeLast();
parent = chain.hierarchy.get(path, false, false);
path.add(index);
}
IDeterministicKey detkey;
if (key.hasSecretBytes()) {
// Not encrypted: private key is available.
final byte[] priv = key.getSecretBytes().toByteArray();
detkey = keyFactory.fromExtended(immutablePath, chainCode, pubkey, priv, parent);
} else {
if (key.hasEncryptedData()) {
Protos.EncryptedData proto = key.getEncryptedData();
EncryptedData data = new EncryptedData(proto.getInitialisationVector().toByteArray(),
proto.getEncryptedPrivateKey().toByteArray());
checkNotNull(crypter, "Encountered an encrypted key but no key crypter provided");
detkey = keyFactory.fromExtendedEncrypted(immutablePath, chainCode, crypter, pubkey, data, parent);
} else {
// No secret key bytes and key is not encrypted: either a watching key or private key bytes
// will be rederived on the fly from the parent.
detkey = keyFactory.fromExtended(immutablePath, chainCode, pubkey, null, parent);
}
}
if (key.hasCreationTimestamp())
detkey.setCreationTimeSeconds(key.getCreationTimestamp() / 1000);
if (log.isDebugEnabled())
log.debug("Deserializing: DETERMINISTIC_KEY: {}", detkey);
if (!isWatchingAccountKey) {
// If the non-encrypted case, the non-leaf keys (account, internal, external) have already
// been rederived and inserted at this point. In the encrypted case though,
// we can't rederive and we must reinsert, potentially building the hierarchy object
// if need be.
if (path.isEmpty()) {
// Master key.
if (chain.rootKey == null) {
chain.rootKey = detkey;
chain.hierarchy = new AnyDeterministicHierarchy(detkey);
}
} else if ((path.size() == chain.getAccountPath().size() + 1) || isSpendingKey) {
// Constant 0 is used for external chain and constant 1 for internal chain
// (also known as change addresses). https://github.com/bitcoin/bips/blob/master/bip-0044.mediawiki
if (detkey.getChildNumber().num() == 0) {
// External chain is used for addresses that are meant to be visible outside of the wallet
// (e.g. for receiving payments).
chain.externalParentKey = detkey;
chain.issuedExternalKeys = key.getDeterministicKey().getIssuedSubkeys();
lookaheadSize = Math.max(lookaheadSize, key.getDeterministicKey().getLookaheadSize());
sigsRequiredToSpend = key.getDeterministicKey().getSigsRequiredToSpend();
} else if (detkey.getChildNumber().num() == 1) {
// Internal chain is used for addresses which are not meant to be visible outside of the
// wallet and is used for return transaction change.
chain.internalParentKey = detkey;
chain.issuedInternalKeys = key.getDeterministicKey().getIssuedSubkeys();
}
}
}
chain.hierarchy.putKey(detkey);
chain.basicKeyChain.importKey(detkey);
}
}
if (chain != null) {
checkState(lookaheadSize >= 0);
chain.setLookaheadSize(hardenedKeysOnly ? 0 : lookaheadSize);
chain.setSigsRequiredToSpend(sigsRequiredToSpend);
chain.maybeLookAhead();
chains.add(chain);
}
return chains;
}
//////////////////////////////////////////////////////////////////////////////////////////////////////////////////
//
// Encryption support
//
//////////////////////////////////////////////////////////////////////////////////////////////////////////////////
@Override
public AnyDeterministicKeyChain toEncrypted(CharSequence password) {
checkNotNull(password);
checkArgument(password.length() > 0);
checkState(seed != null, "Attempt to encrypt a watching chain.");
checkState(!seed.isEncrypted());
KeyCrypter scrypt = new KeyCrypterScrypt();
KeyParameter derivedKey = scrypt.deriveKey(password);
return toEncrypted(scrypt, derivedKey);
}
@Override
public AnyDeterministicKeyChain toEncrypted(KeyCrypter keyCrypter, KeyParameter aesKey) {
return new AnyDeterministicKeyChain(keyCrypter, aesKey, this, hardenedKeysOnly, false);
}
@Override
public AnyDeterministicKeyChain toDecrypted(CharSequence password) {
checkNotNull(password);
checkArgument(password.length() > 0);
KeyCrypter crypter = getKeyCrypter();
checkState(crypter != null, "Chain not encrypted");
KeyParameter derivedKey = crypter.deriveKey(password);
return toDecrypted(derivedKey);
}
@Override
public AnyDeterministicKeyChain toDecrypted(KeyParameter aesKey) {
checkState(getKeyCrypter() != null, "Key chain not encrypted");
checkState(seed != null, "Can't decrypt a watching chain");
checkState(seed.isEncrypted());
String passphrase = DEFAULT_PASSPHRASE_FOR_MNEMONIC; // FIXME allow non-empty passphrase
DeterministicSeed decSeed = seed.decrypt(getKeyCrypter(), passphrase, aesKey);
AnyDeterministicKeyChain chain = makeKeyChainFromSeed(decSeed, getAccountPath(), outputScriptType);
// Now double check that the keys match to catch the case where the key is wrong but padding didn't catch it.
if (!chain.getWatchingKey().getPubKeyObject().equals(getWatchingKey().getPubKeyObject()) &&
!Arrays.equals(chain.getWatchingKey().getPubKey(), getWatchingKey().getPubKey()))
throw new KeyCrypterException.PublicPrivateMismatch("Provided AES key is wrong");
chain.lookaheadSize = lookaheadSize;
// Now copy the (pubkey only) leaf keys across to avoid rederiving them. The private key bytes are missing
// anyway so there's nothing to decrypt.
for (IKey eckey : basicKeyChain.getKeys()) {
IDeterministicKey key = (IDeterministicKey) eckey;
if (key.getPath().size() != getAccountPath().size() + 2) continue; // Not a leaf key.
checkState(key.isEncrypted());
IDeterministicKey parent = chain.hierarchy.get(checkNotNull(key.getParent()).getPath(), false, false);
// Clone the key to the new decrypted hierarchy.
key = keyFactory.fromChildAndParent(key.dropPrivateBytes(), parent);
chain.hierarchy.putKey(key);
chain.basicKeyChain.importKey(key);
}
chain.issuedExternalKeys = issuedExternalKeys;
chain.issuedInternalKeys = issuedInternalKeys;
for (ListenerRegistration listener : basicKeyChain.getListeners()) {
chain.basicKeyChain.addEventListener(listener);
}
return chain;
}
/**
* Factory method to create a key chain from a seed.
* Subclasses should override this to create an instance of the subclass instead of a plain DKC.
* This is used in encryption/decryption.
*/
protected AnyDeterministicKeyChain makeKeyChainFromSeed(DeterministicSeed seed, ImmutableList accountPath,
Script.ScriptType outputScriptType) {
return new AnyDeterministicKeyChain(seed, null, outputScriptType, accountPath, keyFactory, hardenedKeysOnly, false);
}
@Override
public boolean checkPassword(CharSequence password) {
checkNotNull(password);
checkState(getKeyCrypter() != null, "Key chain not encrypted");
return checkAESKey(getKeyCrypter().deriveKey(password));
}
@Override
public boolean checkAESKey(KeyParameter aesKey) {
checkState(rootKey != null, "Can't check password for a watching chain");
checkNotNull(aesKey);
checkState(getKeyCrypter() != null, "Key chain not encrypted");
try {
return rootKey.decrypt(aesKey).getPubKeyObject().equals(rootKey.getPubKeyObject());
} catch (KeyCrypterException e) {
return false;
}
}
@Nullable
@Override
public KeyCrypter getKeyCrypter() {
return basicKeyChain.getKeyCrypter();
}
//////////////////////////////////////////////////////////////////////////////////////////////////////////////////
//
// Bloom filtering support
//
//////////////////////////////////////////////////////////////////////////////////////////////////////////////////
@Override
public int numBloomFilterEntries() {
return numKeys() * 2;
}
@Override
public BloomFilter getFilter(int size, double falsePositiveRate, long tweak) {
lock.lock();
try {
checkArgument(size >= numBloomFilterEntries());
maybeLookAhead();
return basicKeyChain.getFilter(size, falsePositiveRate, tweak);
} finally {
lock.unlock();
}
}
/**
* The number of public keys we should pre-generate on each path before they are requested by the app. This is
* required so that when scanning through the chain given only a seed, we can give enough keys to the remote node
* via the Bloom filter such that we see transactions that are "from the future", for example transactions created
* by a different app that's sharing the same seed, or transactions we made before but we're replaying the chain
* given just the seed. The default is 100.
*/
public int getLookaheadSize() {
lock.lock();
try {
return lookaheadSize;
} finally {
lock.unlock();
}
}
/**
* Sets a new lookahead size. See {@link #getLookaheadSize()} for details on what this is. Setting a new size
* that's larger than the current size will return immediately and the new size will only take effect next time
* a fresh filter is requested (e.g. due to a new peer being connected). So you should set this before starting
* to sync the chain, if you want to modify it. If you haven't modified the lookahead threshold manually then
* it will be automatically set to be a third of the new size.
*/
public void setLookaheadSize(int lookaheadSize) {
lock.lock();
try {
boolean readjustThreshold = this.lookaheadThreshold == calcDefaultLookaheadThreshold();
this.lookaheadSize = lookaheadSize;
if (readjustThreshold)
this.lookaheadThreshold = calcDefaultLookaheadThreshold();
} finally {
lock.unlock();
}
}
/**
* Sets the threshold for the key pre-generation. This is used to avoid adding new keys and thus
* re-calculating Bloom filters every time a new key is calculated. Without a lookahead threshold, every time we
* received a relevant transaction we'd extend the lookahead zone and generate a new filter, which is inefficient.
*/
public void setLookaheadThreshold(int num) {
lock.lock();
try {
if (num >= lookaheadSize)
throw new IllegalArgumentException("Threshold larger or equal to the lookaheadSize");
this.lookaheadThreshold = num;
} finally {
lock.unlock();
}
}
/**
* Gets the threshold for the key pre-generation. See {@link #setLookaheadThreshold(int)} for details on what this
* is. The default is a third of the lookahead size (100 / 3 == 33). If you don't modify it explicitly then this
* value will always be one third of the lookahead size.
*/
public int getLookaheadThreshold() {
lock.lock();
try {
if (lookaheadThreshold >= lookaheadSize)
return 0;
return lookaheadThreshold;
} finally {
lock.unlock();
}
}
/**
* Pre-generate enough keys to reach the lookahead size. You can call this if you need to explicitly invoke
* the lookahead procedure, but it's normally unnecessary as it will be done automatically when needed.
*/
public void maybeLookAhead() {
lock.lock();
try {
List keys = maybeLookAhead(externalParentKey, issuedExternalKeys);
keys.addAll(maybeLookAhead(internalParentKey, issuedInternalKeys));
if (keys.isEmpty())
return;
keyLookaheadEpoch++;
// Batch add all keys at once so there's only one event listener invocation, as this will be listened to
// by the wallet and used to rebuild/broadcast the Bloom filter. That's expensive so we don't want to do
// it more often than necessary.
basicKeyChain.importKeys(keys);
} finally {
lock.unlock();
}
}
private List maybeLookAhead(IDeterministicKey parent, int issued) {
checkState(lock.isHeldByCurrentThread());
return maybeLookAhead(parent, issued, getLookaheadSize(), getLookaheadThreshold());
}
/**
* Pre-generate enough keys to reach the lookahead size, but only if there are more than the lookaheadThreshold to
* be generated, so that the Bloom filter does not have to be regenerated that often.
*
* The returned mutable list of keys must be inserted into the basic key chain.
*/
private List maybeLookAhead(IDeterministicKey parent, int issued, int lookaheadSize, int lookaheadThreshold) {
checkState(lock.isHeldByCurrentThread());
final int numChildren = hierarchy.getNumChildren(parent.getPath());
final int needed = issued + lookaheadSize + lookaheadThreshold - numChildren;
if (needed <= lookaheadThreshold)
return new ArrayList<>();
log.info("{} keys needed for {} = {} issued + {} lookahead size + {} lookahead threshold - {} num children",
needed, parent.getPathAsString(), issued, lookaheadSize, lookaheadThreshold, numChildren);
List result = new ArrayList<>(needed);
final Stopwatch watch = Stopwatch.createStarted();
int nextChild = numChildren;
for (int i = 0; i < needed; i++) {
IDeterministicKey key = parent.deriveThisOrNextChildKey(nextChild | (hardenedKeysOnly ? ChildNumber.HARDENED_BIT : 0));
key = key.dropPrivateBytes();
hierarchy.putKey(key);
result.add(key);
nextChild = key.getChildNumber().num() + 1;
}
watch.stop();
log.info("Took {}", watch);
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.
*/
public int getIssuedExternalKeys() {
lock.lock();
try {
return issuedExternalKeys;
} finally {
lock.unlock();
}
}
/**
* Returns number of keys used on internal path. This may be fewer than the number that have been deserialized
* or held in memory, because of the lookahead zone.
*/
public int getIssuedInternalKeys() {
lock.lock();
try {
return issuedInternalKeys;
} finally {
lock.unlock();
}
}
/** Returns the seed or null if this chain is a watching chain. */
@Nullable
public DeterministicSeed getSeed() {
lock.lock();
try {
return seed;
} finally {
lock.unlock();
}
}
// For internal usage only
/* package */ List getKeys(boolean includeLookahead, boolean includeParents) {
List keys = basicKeyChain.getKeys();
List result = new LinkedList<>();
if (!includeLookahead) {
int treeSize = internalParentKey.getPath().size();
for (IKey key : keys) {
IDeterministicKey detkey = (IDeterministicKey) key;
IDeterministicKey parent = detkey.getParent();
if (!includeParents && parent == null) continue;
if (!includeParents && detkey.getPath().size() <= treeSize) continue;
if (internalParentKey.equals(parent) && detkey.getChildNumber().i() >= issuedInternalKeys) continue;
if (externalParentKey.equals(parent) && detkey.getChildNumber().i() >= issuedExternalKeys) continue;
result.add(detkey);
}
} else {
for (IKey key : keys)
result.add((IDeterministicKey) key);
// TODO includeParents is ignored here
}
return result;
}
/**
* Returns only the external keys that have been issued by this chain, lookahead not included.
*/
public List getIssuedReceiveKeys() {
final List keys = new ArrayList<>(getKeys(false, false));
for (Iterator i = keys.iterator(); i.hasNext();) {
IDeterministicKey parent = i.next().getParent();
if (parent == null || !externalParentKey.equals(parent))
i.remove();
}
return keys;
}
/**
* Returns leaf keys issued by this chain (including lookahead zone)
*/
public List getLeafKeys() {
ImmutableList.Builder keys = ImmutableList.builder();
for (IKey key : getKeys(true, false)) {
IDeterministicKey dKey = (IDeterministicKey) key;
if (dKey.getPath().size() == getAccountPath().size() + 2) {
keys.add(dKey);
}
}
return keys.build();
}
/*package*/ static void serializeSeedEncryptableItem(DeterministicSeed seed, Protos.Key.Builder proto) {
// The seed can be missing if we have not derived it yet from the mnemonic.
// This will not normally happen once all the wallets are on the latest code that caches
// the seed.
if (seed.isEncrypted() && seed.getEncryptedSeedData() != null) {
EncryptedData data = seed.getEncryptedSeedData();
proto.setEncryptedDeterministicSeed(proto.getEncryptedDeterministicSeed().toBuilder()
.setEncryptedPrivateKey(ByteString.copyFrom(data.encryptedBytes))
.setInitialisationVector(ByteString.copyFrom(data.initialisationVector)));
// We don't allow mixing of encryption types at the moment.
checkState(seed.getEncryptionType() == Protos.Wallet.EncryptionType.ENCRYPTED_SCRYPT_AES);
} else {
final byte[] secret = seed.getSeedBytes();
if (secret != null)
proto.setDeterministicSeed(ByteString.copyFrom(secret));
}
}
/**
* Returns a counter that is incremented each time new keys are generated due to lookahead. Used by the network
* code to learn whether to discard the current block and await calculation of a new filter.
*/
public int getKeyLookaheadEpoch() {
lock.lock();
try {
return keyLookaheadEpoch;
} finally {
lock.unlock();
}
}
/**
* Whether the keychain is married. A keychain is married when it vends P2SH addresses
* from multiple keychains in a multisig relationship.
* @see MarriedKeyChain
*/
public boolean isMarried() {
return false;
}
/** Get redeem data for a key. Only applicable to married keychains. */
public RedeemData getRedeemData(IDeterministicKey followedKey) {
throw new UnsupportedOperationException();
}
/** Create a new key and return the matching output script. Only applicable to married keychains. */
public Script freshOutputScript(KeyChain.KeyPurpose purpose) {
throw new UnsupportedOperationException();
}
@Override
public String toString() {
MoreObjects.ToStringHelper helper = MoreObjects.toStringHelper(this).omitNullValues();
helper.addValue(keyFactory.getKeyType());
helper.addValue(outputScriptType);
helper.add("accountPath", accountPath);
helper.add("lookaheadSize", lookaheadSize);
helper.add("lookaheadThreshold", lookaheadThreshold);
if (isFollowing)
helper.addValue("following");
return helper.toString();
}
public String toString(boolean includeLookahead, boolean includePrivateKeys, @Nullable KeyParameter aesKey, NetworkParameters params) {
final IDeterministicKey watchingKey = getWatchingKey();
final StringBuilder builder = new StringBuilder();
if (seed != null) {
if (includePrivateKeys) {
DeterministicSeed decryptedSeed = seed.isEncrypted()
? seed.decrypt(getKeyCrypter(), DEFAULT_PASSPHRASE_FOR_MNEMONIC, aesKey)
: seed;
final List words = decryptedSeed.getMnemonicCode();
builder.append("Seed as words: ").append(Utils.SPACE_JOINER.join(words)).append('\n');
builder.append("Seed as hex: ").append(decryptedSeed.toHexString()).append('\n');
} else {
if (seed.isEncrypted())
builder.append("Seed is encrypted\n");
}
builder.append("Seed birthday: ").append(seed.getCreationTimeSeconds()).append(" [")
.append(Utils.dateTimeFormat(seed.getCreationTimeSeconds() * 1000)).append("]\n");
} else {
builder.append("Key birthday: ").append(watchingKey.getCreationTimeSeconds()).append(" [")
.append(Utils.dateTimeFormat(watchingKey.getCreationTimeSeconds() * 1000)).append("]\n");
}
builder.append("Ouput script type: ").append(outputScriptType).append('\n');
builder.append("Key to watch: ").append(watchingKey.serializePubB58(params, outputScriptType))
.append('\n');
builder.append("Lookahead siz/thr: ").append(lookaheadSize).append('/').append(lookaheadThreshold).append('\n');
builder.append("Key Type: ").append(keyFactory.getKeyType());
formatAddresses(includeLookahead, includePrivateKeys, aesKey, params, builder);
return builder.toString();
}
protected void formatAddresses(boolean includeLookahead, boolean includePrivateKeys, @Nullable KeyParameter aesKey,
NetworkParameters params, StringBuilder builder) {
for (IDeterministicKey key : getKeys(includeLookahead, true)) {
String comment = null;
if (key.equals(rootKey))
comment = "root";
else if (key.equals(getWatchingKey()))
comment = "account";
else if (key.equals(internalParentKey))
comment = "internal";
else if (key.equals(externalParentKey))
comment = "external";
else if (internalParentKey.equals(key.getParent()) && key.getChildNumber().i() >= issuedInternalKeys)
comment = "*";
else if (externalParentKey.equals(key.getParent()) && key.getChildNumber().i() >= issuedExternalKeys)
comment = "*";
key.formatKeyWithAddress(includePrivateKeys, aesKey, builder, params, outputScriptType, comment);
}
}
/** 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 IRedeemData findRedeemDataByScriptHash(ByteString bytes) {
return null;
}
public KeyFactory getKeyFactory() {
return keyFactory;
}
}