Commit 6637f05a authored by Nelson MELINA's avatar Nelson MELINA Committed by bitcoinj-sv

Allow to create wallets with an arbitrary account path. Adds a test for BIP44...

Allow to create wallets with an arbitrary account path. Adds a test for BIP44 wallets (account zero only).

Authors: Nelson MELINA <nelson.melina@mycelium.com>, Giuseppe Magnotta <giuseppe.magnotta@gmail.com>
parent dc60674c
......@@ -81,11 +81,21 @@ public final class HDKeyDerivation {
/**
* @throws HDDerivationException if privKeyBytes is invalid (0 or >= n).
*/
public static DeterministicKey createMasterPrivKeyFromBytes(byte[] privKeyBytes, byte[] chainCode) throws HDDerivationException {
public static DeterministicKey createMasterPrivKeyFromBytes(byte[] privKeyBytes, byte[] chainCode)
throws HDDerivationException {
// childNumberPath is an empty list because we are creating the root key.
return createMasterPrivKeyFromBytes(privKeyBytes, chainCode, ImmutableList.<ChildNumber> of());
}
/**
* @throws HDDerivationException if privKeyBytes is invalid (0 or >= n).
*/
public static DeterministicKey createMasterPrivKeyFromBytes(byte[] privKeyBytes, byte[] chainCode,
ImmutableList<ChildNumber> childNumberPath) throws HDDerivationException {
BigInteger priv = new BigInteger(1, privKeyBytes);
assertNonZero(priv, "Generated master key is invalid.");
assertLessThanN(priv, "Generated master key is invalid.");
return new DeterministicKey(ImmutableList.<ChildNumber>of(), chainCode, priv, null);
return new DeterministicKey(childNumberPath, chainCode, priv, null);
}
public static DeterministicKey createMasterPubKeyFromBytes(byte[] pubKeyBytes, byte[] chainCode) {
......
......@@ -18,6 +18,8 @@ package org.bitcoinj.wallet;
import org.bitcoinj.crypto.*;
import com.google.common.collect.ImmutableList;
/**
* Default factory for creating keychains while de-serializing.
*/
......@@ -32,17 +34,25 @@ public class DefaultKeyChainFactory implements KeyChainFactory {
return chain;
}
@Override
public DeterministicKeyChain makeKeyChain(Protos.Key key, Protos.Key firstSubKey, DeterministicSeed seed,
KeyCrypter crypter, boolean isMarried, ImmutableList<ChildNumber> accountPath) {
DeterministicKeyChain chain;
if (isMarried)
chain = new MarriedKeyChain(seed, crypter);
else
chain = new DeterministicKeyChain(seed, crypter, accountPath);
return chain;
}
@Override
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);
else
chain = new DeterministicKeyChain(accountKey, isFollowingKey);
chain = new DeterministicKeyChain(accountKey, isFollowingKey, accountKey.getPath());
return chain;
}
}
......@@ -16,6 +16,8 @@
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;
......@@ -34,6 +36,20 @@ public interface KeyChainFactory {
*/
DeterministicKeyChain makeKeyChain(Protos.Key key, Protos.Key firstSubKey, DeterministicSeed seed, KeyCrypter crypter, boolean isMarried);
/**
* Make a keychain (but not a watching one) with the specified account path
*
* @param key the protobuf for the root key
* @param firstSubKey the protobuf for the first child key (normally the parent of the external subchain)
* @param seed the seed
* @param crypter the encrypted/decrypter
* @param isMarried whether the keychain is leading in a marriage
* @param accountPath the specified account path
*/
DeterministicKeyChain makeKeyChain(Protos.Key key, Protos.Key firstSubKey, DeterministicSeed seed,
KeyCrypter crypter, boolean isMarried,
ImmutableList<ChildNumber> accountPath);
/**
* Make a watching keychain.
*
......
......@@ -84,6 +84,14 @@ public class KeyChainGroup implements KeyBag {
this(params, null, ImmutableList.of(new DeterministicKeyChain(seed)), null, null);
}
/**
* Creates a keychain group with no basic chain, and an HD chain initialized from the given seed. Account path is
* provided.
*/
public KeyChainGroup(NetworkParameters params, DeterministicSeed seed, ImmutableList<ChildNumber> accountPath) {
this(params, null, ImmutableList.of(new DeterministicKeyChain(seed, accountPath)), null, null);
}
/**
* Creates a keychain group with no basic chain, and an HD chain that is watching the given watching key.
* This HAS to be an account key as returned by {@link DeterministicKeyChain#getWatchingKey()}.
......@@ -92,6 +100,14 @@ public class KeyChainGroup implements KeyBag {
this(params, null, ImmutableList.of(DeterministicKeyChain.watch(watchKey)), null, null);
}
/**
* Creates a keychain group with no basic chain, and an HD chain that is watching the given watching key.
* This HAS to be an account key as returned by {@link DeterministicKeyChain#getWatchingKey()}.
*/
public KeyChainGroup(NetworkParameters params, DeterministicKey watchKey, ImmutableList<ChildNumber> accountPath) {
this(params, null, ImmutableList.of(DeterministicKeyChain.watch(watchKey, accountPath)), null, null);
}
// Used for deserialization.
private KeyChainGroup(NetworkParameters params, @Nullable BasicKeyChain basicKeyChain, List<DeterministicKeyChain> chains,
@Nullable EnumMap<KeyChain.KeyPurpose, DeterministicKey> currentKeys, @Nullable KeyCrypter crypter) {
......
......@@ -267,10 +267,26 @@ public class Wallet extends BaseTaggableObject
this(context, new KeyChainGroup(context.getParams()));
}
/**
* @param params network parameters
* @param seed deterministic seed
* @return a wallet from a deterministic seed with a
* {@link org.bitcoinj.wallet.DeterministicKeyChain#ACCOUNT_ZERO_PATH 0 hardened path}
*/
public static Wallet fromSeed(NetworkParameters params, DeterministicSeed seed) {
return new Wallet(params, new KeyChainGroup(params, seed));
}
/**
* @param params network parameters
* @param seed deterministic seed
* @param accountPath account path
* @return an instance of a wallet from a deterministic seed.
*/
public static Wallet fromSeed(NetworkParameters params, DeterministicSeed seed, ImmutableList<ChildNumber> accountPath) {
return new Wallet(params, new KeyChainGroup(params, seed, accountPath));
}
/**
* Creates a wallet that tracks payments to and from the HD key hierarchy rooted by the given watching key. A
* watching key corresponds to account zero in the recommended BIP32 key hierarchy.
......@@ -279,6 +295,13 @@ public class Wallet extends BaseTaggableObject
return new Wallet(params, new KeyChainGroup(params, watchKey));
}
/**
* Creates a wallet that tracks payments to and from the HD key hierarchy rooted by the given watching key. The account path is specified.
*/
public static Wallet fromWatchingKey(NetworkParameters params, DeterministicKey watchKey, ImmutableList<ChildNumber> accountPath) {
return new Wallet(params, new KeyChainGroup(params, watchKey, accountPath));
}
/**
* Creates a wallet that tracks payments to and from the HD key hierarchy rooted by the given watching key. A
* watching key corresponds to account zero in the recommended BIP32 key hierarchy. The key is specified in base58
......@@ -291,6 +314,18 @@ public class Wallet extends BaseTaggableObject
return fromWatchingKey(params, watchKey);
}
/**
* Creates a wallet that tracks payments to and from the HD key hierarchy rooted by the given watching key. The
* account path is specified. The key is specified in base58 notation and the creation time of the key. If you don't
* know the creation time, you can pass {@link DeterministicHierarchy#BIP32_STANDARDISATION_TIME_SECS}.
*/
public static Wallet fromWatchingKeyB58(NetworkParameters params, String watchKeyB58, long creationTimeSeconds,
ImmutableList<ChildNumber> accountPath) {
final DeterministicKey watchKey = DeterministicKey.deserializeB58(null, watchKeyB58, params);
watchKey.setCreationTimeSeconds(creationTimeSeconds);
return fromWatchingKey(params, watchKey, accountPath);
}
/**
* Creates a wallet containing a given set of keys. All further keys will be derived from the oldest key.
*/
......
......@@ -39,6 +39,7 @@ import static org.junit.Assert.*;
public class DeterministicKeyChainTest {
private DeterministicKeyChain chain;
private DeterministicKeyChain bip44chain;
private final byte[] ENTROPY = Sha256Hash.hash("don't use a string seed like this in real life".getBytes());
@Before
......@@ -50,6 +51,11 @@ public class DeterministicKeyChainTest {
chain = new DeterministicKeyChain(ENTROPY, "", secs);
chain.setLookaheadSize(10);
assertEquals(secs, checkNotNull(chain.getSeed()).getCreationTimeSeconds());
bip44chain = new DeterministicKeyChain(new DeterministicSeed(ENTROPY, "", secs),
ImmutableList.of(new ChildNumber(44, true), new ChildNumber(1, true), ChildNumber.ZERO_HARDENED));
bip44chain.setLookaheadSize(10);
assertEquals(secs, checkNotNull(bip44chain.getSeed()).getCreationTimeSeconds());
}
@Test
......@@ -131,6 +137,12 @@ public class DeterministicKeyChainTest {
List<Protos.Key> keys = chain1.serializeToProtobuf();
KeyChainFactory factory = new KeyChainFactory() {
@Override
public DeterministicKeyChain makeKeyChain(Protos.Key key, Protos.Key firstSubKey, DeterministicSeed seed,
KeyCrypter crypter, boolean isMarried, ImmutableList<ChildNumber> accountPath) {
return new AccountOneChain(crypter, seed);
}
@Override
public DeterministicKeyChain makeKeyChain(Protos.Key key, Protos.Key firstSubKey, DeterministicSeed seed, KeyCrypter crypter, boolean isMarried) {
return new AccountOneChain(crypter, seed);
......@@ -248,6 +260,43 @@ public class DeterministicKeyChainTest {
assertEquals(oldLookaheadSize, chain.getLookaheadSize());
}
@Test
public void serializeUnencryptedBIP44() throws UnreadableWalletException {
bip44chain.maybeLookAhead();
DeterministicKey key1 = bip44chain.getKey(KeyChain.KeyPurpose.RECEIVE_FUNDS);
DeterministicKey key2 = bip44chain.getKey(KeyChain.KeyPurpose.RECEIVE_FUNDS);
DeterministicKey key3 = bip44chain.getKey(KeyChain.KeyPurpose.CHANGE);
List<Protos.Key> keys = bip44chain.serializeToProtobuf();
// 1 mnemonic/seed, 1 master key, 1 account key, 2 internal keys, 3 derived, 20 lookahead and 5 lookahead
// threshold.
int numItems = 3 // mnemonic/seed
+ 1 // master key
+ 1 // account key
+ 2 // ext/int parent keys
+ (bip44chain.getLookaheadSize() + bip44chain.getLookaheadThreshold()) * 2 // lookahead zone on each chain
;
assertEquals(numItems, keys.size());
// Get another key that will be lost during round-tripping, to ensure we can derive it again.
DeterministicKey key4 = bip44chain.getKey(KeyChain.KeyPurpose.CHANGE);
final String EXPECTED_SERIALIZATION = checkSerialization(keys, "deterministic-wallet-bip44-serialization.txt");
// Round trip the data back and forth to check it is preserved.
int oldLookaheadSize = bip44chain.getLookaheadSize();
bip44chain = DeterministicKeyChain.fromProtobuf(keys, null).get(0);
assertEquals(EXPECTED_SERIALIZATION, protoToString(bip44chain.serializeToProtobuf()));
assertEquals(key1, bip44chain.findKeyFromPubHash(key1.getPubKeyHash()));
assertEquals(key2, bip44chain.findKeyFromPubHash(key2.getPubKeyHash()));
assertEquals(key3, bip44chain.findKeyFromPubHash(key3.getPubKeyHash()));
assertEquals(key4, bip44chain.getKey(KeyChain.KeyPurpose.CHANGE));
key1.sign(Sha256Hash.ZERO_HASH);
key2.sign(Sha256Hash.ZERO_HASH);
key3.sign(Sha256Hash.ZERO_HASH);
key4.sign(Sha256Hash.ZERO_HASH);
assertEquals(oldLookaheadSize, bip44chain.getLookaheadSize());
}
@Test(expected = IllegalStateException.class)
public void notEncrypted() {
chain.toDecrypted("fail");
......
......@@ -2,6 +2,7 @@ type: DETERMINISTIC_MNEMONIC
secret_bytes: "aerobic toe save section draw warm cute upon raccoon mother priority pilot taste sweet next traffic fatal sword dentist original crisp team caution rebel"
creation_timestamp: 1389353062000
deterministic_seed: "E\032\356\206\230,\275\263\364=\334^f\307\037\350\321X7R\262z\205\3564\371tp\2639R\342\027 J\266\253\250\320\022\031\233\271~O$\330\260\214\fz\231tI\353\215*\037\355\205\213.\224?"
account_path: 2147483648
type: DETERMINISTIC_KEY
secret_bytes: "\270E0\202(\362b\023\276\264\347\226E2\360\221\347\325\233L\203\3276\272\213\2436&\304\373\221\025"
......
......@@ -132,6 +132,9 @@ message Key {
// Encrypted version of the seed
optional EncryptedData encrypted_deterministic_seed = 9;
// The path to the root. Only applicable to a DETERMINISTIC_MNEMONIC key entry.
repeated uint32 account_path = 10 [packed = true];
}
message Script {
......
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment