allow DKC derivation path to be overridden

This commit is contained in:
Devrandom
2014-12-26 12:58:27 -08:00
committed by Mike Hearn
parent 4713c76a29
commit 80ed15f253
8 changed files with 36 additions and 30 deletions

View File

@@ -16,8 +16,9 @@
package org.bitcoinj.wallet;
import org.bitcoinj.crypto.DeterministicKey;
import org.bitcoinj.crypto.KeyCrypter;
import com.google.common.collect.ImmutableList;
import org.bitcoinj.crypto.*;
import org.bitcoinj.store.UnreadableWalletException;
/**
* Default factory for creating keychains while de-serializing.
@@ -34,7 +35,11 @@ public class DefaultKeyChainFactory implements KeyChainFactory {
}
@Override
public DeterministicKeyChain makeWatchingKeyChain(Protos.Key key, Protos.Key firstSubKey, DeterministicKey accountKey, boolean isFollowingKey, boolean isMarried) {
public DeterministicKeyChain makeWatchingKeyChain(Protos.Key key, Protos.Key firstSubKey, DeterministicKey accountKey,
boolean isFollowingKey, boolean isMarried) throws UnreadableWalletException {
if (!accountKey.getPath().equals(DeterministicKeyChain.ACCOUNT_ZERO_PATH))
throw new UnreadableWalletException("Expecting account key but found key with path: " +
HDUtils.formatPath(accountKey.getPath()));
DeterministicKeyChain chain;
if (isMarried)
chain = new MarriedKeyChain(accountKey);

View File

@@ -61,9 +61,9 @@ import static com.google.common.collect.Lists.newLinkedList;
* 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 org.bitcoinj.crypto.DeterministicKey#serializePubB58()}. The resulting "xpub..." string encodes
* {@link org.bitcoinj.crypto.DeterministicKey#serializePubB58(org.bitcoinj.core.NetworkParameters)}. The resulting "xpub..." string encodes
* sufficient information about the account key to create a watching chain via
* {@link org.bitcoinj.crypto.DeterministicKey#deserializeB58(org.bitcoinj.crypto.DeterministicKey, String)}
* {@link org.bitcoinj.crypto.DeterministicKey#deserializeB58(org.bitcoinj.crypto.DeterministicKey, String, org.bitcoinj.core.NetworkParameters)}
* (with null as the first parameter) and then
* {@link DeterministicKeyChain#DeterministicKeyChain(org.bitcoinj.crypto.DeterministicKey)}.</p>
*
@@ -113,8 +113,10 @@ public class DeterministicKeyChain implements EncryptableKeyChain {
// 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.
public static final ImmutableList<ChildNumber> ACCOUNT_ZERO_PATH = ImmutableList.of(ChildNumber.ZERO_HARDENED);
public static final ImmutableList<ChildNumber> EXTERNAL_PATH = ImmutableList.of(ChildNumber.ZERO);
public static final ImmutableList<ChildNumber> INTERNAL_PATH = ImmutableList.of(ChildNumber.ONE);
public static final ImmutableList<ChildNumber> EXTERNAL_SUBPATH = ImmutableList.of(ChildNumber.ZERO);
public static final ImmutableList<ChildNumber> INTERNAL_SUBPATH = ImmutableList.of(ChildNumber.ONE);
// m / 44' / 0' / 0'
public static final ImmutableList<ChildNumber> BIP44_ACCOUNT_ZERO_PATH = ImmutableList.of(new ChildNumber(44, true), ChildNumber.ZERO_HARDENED, ChildNumber.ZERO_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
@@ -311,7 +313,7 @@ public class DeterministicKeyChain implements EncryptableKeyChain {
*/
public DeterministicKeyChain(DeterministicKey watchingKey, long creationTimeSeconds) {
checkArgument(watchingKey.isPubKeyOnly(), "Private subtrees not currently supported: if you got this key from DKC.getWatchingKey() then use .dropPrivate().dropParent() on it first.");
checkArgument(watchingKey.getPath().size() == 1, "You can only watch an account key currently");
checkArgument(watchingKey.getPath().size() == getAccountPath().size(), "You can only watch an account key currently");
basicKeyChain = new BasicKeyChain();
this.creationTimeSeconds = creationTimeSeconds;
this.seed = null;
@@ -404,8 +406,8 @@ public class DeterministicKeyChain implements EncryptableKeyChain {
encryptNonLeaf(aesKey, chain, rootKey, getAccountPath().subList(0, i));
}
DeterministicKey account = encryptNonLeaf(aesKey, chain, rootKey, getAccountPath());
externalKey = encryptNonLeaf(aesKey, chain, account, ImmutableList.<ChildNumber>builder().addAll(getAccountPath()).addAll(EXTERNAL_PATH).build());
internalKey = encryptNonLeaf(aesKey, chain, account, ImmutableList.<ChildNumber>builder().addAll(getAccountPath()).addAll(INTERNAL_PATH).build());
externalKey = encryptNonLeaf(aesKey, chain, account, ImmutableList.<ChildNumber>builder().addAll(getAccountPath()).addAll(EXTERNAL_SUBPATH).build());
internalKey = encryptNonLeaf(aesKey, chain, account, ImmutableList.<ChildNumber>builder().addAll(getAccountPath()).addAll(INTERNAL_SUBPATH).build());
// 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.
@@ -852,9 +854,6 @@ public class DeterministicKeyChain implements EncryptableKeyChain {
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 = factory.makeWatchingKeyChain(key, iter.peek(), accountKey, isFollowingKey, isMarried);
isWatchingAccountKey = true;
} else {
@@ -979,7 +978,7 @@ public class DeterministicKeyChain implements EncryptableKeyChain {
// anyway so there's nothing to decrypt.
for (ECKey eckey : basicKeyChain.getKeys()) {
DeterministicKey key = (DeterministicKey) eckey;
if (key.getPath().size() != 3) continue; // Not a leaf key.
if (key.getPath().size() != getAccountPath().size() + 2) continue; // Not a leaf key.
checkState(key.isEncrypted());
DeterministicKey parent = chain.hierarchy.get(checkNotNull(key.getParent()).getPath(), false, false);
// Clone the key to the new decrypted hierarchy.

View File

@@ -16,8 +16,11 @@
package org.bitcoinj.wallet;
import com.google.common.collect.ImmutableList;
import org.bitcoinj.crypto.ChildNumber;
import org.bitcoinj.crypto.DeterministicKey;
import org.bitcoinj.crypto.KeyCrypter;
import org.bitcoinj.store.UnreadableWalletException;
/**
* Factory interface for creation keychains while de-serializing a wallet.
@@ -45,5 +48,5 @@ public interface KeyChainFactory {
* @param isFollowingKey whether the keychain is following in a marriage
* @param isMarried whether the keychain is leading in a marriage
*/
DeterministicKeyChain makeWatchingKeyChain(Protos.Key key, Protos.Key firstSubKey, DeterministicKey accountKey, boolean isFollowingKey, boolean isMarried);
DeterministicKeyChain makeWatchingKeyChain(Protos.Key key, Protos.Key firstSubKey, DeterministicKey accountKey, boolean isFollowingKey, boolean isMarried) throws UnreadableWalletException;
}

View File

@@ -764,14 +764,14 @@ public class KeyChainGroup implements KeyBag {
// kinds of KeyPurpose are introduced.
if (activeChain.getIssuedExternalKeys() > 0) {
DeterministicKey currentExternalKey = activeChain.getKeyByPath(
ImmutableList.of(ChildNumber.ZERO_HARDENED, ChildNumber.ZERO, new ChildNumber(activeChain.getIssuedExternalKeys() - 1))
Lists.newArrayList(ImmutableList.<ChildNumber>builder().addAll(activeChain.getAccountPath()).add(ChildNumber.ZERO, new ChildNumber(activeChain.getIssuedExternalKeys() - 1)).build())
);
currentKeys.put(KeyChain.KeyPurpose.RECEIVE_FUNDS, currentExternalKey);
}
if (activeChain.getIssuedInternalKeys() > 0) {
DeterministicKey currentInternalKey = activeChain.getKeyByPath(
ImmutableList.of(ChildNumber.ZERO_HARDENED, new ChildNumber(1), new ChildNumber(activeChain.getIssuedInternalKeys() - 1))
Lists.newArrayList(ImmutableList.<ChildNumber>builder().addAll(activeChain.getAccountPath()).add(new ChildNumber(1), new ChildNumber(activeChain.getIssuedInternalKeys() - 1)).build())
);
currentKeys.put(KeyChain.KeyPurpose.CHANGE, currentInternalKey);
}

View File

@@ -192,7 +192,7 @@ public class MarriedKeyChain extends DeterministicKeyChain {
List<DeterministicKeyChain> followingKeyChains = Lists.newArrayList();
for (DeterministicKey key : followingAccountKeys) {
checkArgument(key.getPath().size() == 1, "Following keys have to be account keys");
checkArgument(key.getPath().size() == getAccountPath().size(), "Following keys have to be account keys");
DeterministicKeyChain chain = DeterministicKeyChain.watchAndFollow(key);
if (lookaheadSize >= 0)
chain.setLookaheadSize(lookaheadSize);

View File

@@ -3078,7 +3078,8 @@ public class WalletTest extends TestWithWallet {
public boolean signInputs(ProposedTransaction propTx, KeyBag keyBag) {
assertEquals(propTx.partialTx.getInputs().size(), propTx.keyPaths.size());
List<ChildNumber> externalZeroLeaf = ImmutableList.<ChildNumber>builder()
.addAll(DeterministicKeyChain.EXTERNAL_PATH).add(ChildNumber.ZERO).build();
.addAll(DeterministicKeyChain.ACCOUNT_ZERO_PATH)
.addAll(DeterministicKeyChain.EXTERNAL_SUBPATH).add(ChildNumber.ZERO).build();
for (TransactionInput input : propTx.partialTx.getInputs()) {
List<ChildNumber> keypath = propTx.keyPaths.get(input.getConnectedOutput().getScriptPubKey());
assertNotNull(keypath);

View File

@@ -17,11 +17,8 @@
package org.bitcoinj.wallet;
import org.bitcoinj.core.*;
import org.bitcoinj.crypto.ChildNumber;
import org.bitcoinj.crypto.DeterministicHierarchy;
import org.bitcoinj.crypto.DeterministicKey;
import org.bitcoinj.crypto.*;
import org.bitcoinj.params.MainNetParams;
import org.bitcoinj.crypto.KeyCrypter;
import org.bitcoinj.params.UnitTestParams;
import org.bitcoinj.store.UnreadableWalletException;
import org.bitcoinj.utils.BriefLogFormatter;

View File

@@ -78,7 +78,7 @@ public class KeyChainGroupTest {
public void freshCurrentKeys() throws Exception {
int numKeys = ((group.getLookaheadSize() + group.getLookaheadThreshold()) * 2) // * 2 because of internal/external
+ 1 // keys issued
+ 3 /* account key + int/ext parent keys */;
+ group.getActiveKeyChain().getAccountPath().size() + 2 /* account key + int/ext parent keys */;
assertEquals(numKeys, group.numKeys());
assertEquals(2 * numKeys, group.getBloomFilterElementCount());
ECKey r1 = group.currentKey(KeyChain.KeyPurpose.RECEIVE_FUNDS);
@@ -186,7 +186,7 @@ public class KeyChainGroupTest {
group.getBloomFilterElementCount();
assertEquals(((group.getLookaheadSize() + group.getLookaheadThreshold()) * 2) // * 2 because of internal/external
+ (2 - group.getLookaheadThreshold()) // keys issued
+ 4 /* master, account, int, ext */, group.numKeys());
+ group.getActiveKeyChain().getAccountPath().size() + 3 /* master, account, int, ext */, group.numKeys());
Address a3 = group.currentAddress(KeyChain.KeyPurpose.RECEIVE_FUNDS);
assertEquals(a2, a3);
@@ -409,26 +409,27 @@ public class KeyChainGroupTest {
@Test
public void serialization() throws Exception {
assertEquals(INITIAL_KEYS + 1 /* for the seed */, group.serializeToProtobuf().size());
int initialKeys = INITIAL_KEYS + group.getActiveKeyChain().getAccountPath().size() - 1;
assertEquals(initialKeys + 1 /* for the seed */, group.serializeToProtobuf().size());
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);
group.getBloomFilterElementCount();
List<Protos.Key> protoKeys1 = group.serializeToProtobuf();
assertEquals(INITIAL_KEYS + ((LOOKAHEAD_SIZE + 1) * 2) + 1 /* for the seed */ + 1, protoKeys1.size());
assertEquals(initialKeys + ((LOOKAHEAD_SIZE + 1) * 2) + 1 /* for the seed */ + 1, protoKeys1.size());
group.importKeys(new ECKey());
List<Protos.Key> protoKeys2 = group.serializeToProtobuf();
assertEquals(INITIAL_KEYS + ((LOOKAHEAD_SIZE + 1) * 2) + 1 /* for the seed */ + 2, protoKeys2.size());
assertEquals(initialKeys + ((LOOKAHEAD_SIZE + 1) * 2) + 1 /* for the seed */ + 2, protoKeys2.size());
group = KeyChainGroup.fromProtobufUnencrypted(params, protoKeys1);
assertEquals(INITIAL_KEYS + ((LOOKAHEAD_SIZE + 1) * 2) + 1 /* for the seed */ + 1, protoKeys1.size());
assertEquals(initialKeys + ((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);
assertEquals(INITIAL_KEYS + ((LOOKAHEAD_SIZE + 1) * 2) + 1 /* for the seed */ + 2, protoKeys2.size());
assertEquals(initialKeys + ((LOOKAHEAD_SIZE + 1) * 2) + 1 /* for the seed */ + 2, protoKeys2.size());
assertTrue(group.hasKey(key1));
assertTrue(group.hasKey(key2));