diff --git a/core/src/main/java/org/bitcoinj/crypto/DeterministicKey.java b/core/src/main/java/org/bitcoinj/crypto/DeterministicKey.java index b3e6b056..3dd7fa89 100644 --- a/core/src/main/java/org/bitcoinj/crypto/DeterministicKey.java +++ b/core/src/main/java/org/bitcoinj/crypto/DeterministicKey.java @@ -42,6 +42,8 @@ public class DeterministicKey extends ECKey { private final DeterministicKey parent; private final ImmutableList childNumberPath; + private final int depth; + private final int parentFingerprint; // 0 if this key is root node of key hierarchy /** 32 bytes */ private final byte[] chainCode; @@ -57,6 +59,8 @@ public class DeterministicKey extends ECKey { this.parent = parent; this.childNumberPath = checkNotNull(childNumberPath); this.chainCode = Arrays.copyOf(chainCode, chainCode.length); + this.depth = this.childNumberPath.size(); + this.parentFingerprint = (parent != null) ? parent.getFingerprint() : 0; } public DeterministicKey(ImmutableList childNumberPath, @@ -77,6 +81,8 @@ public class DeterministicKey extends ECKey { this.parent = parent; this.childNumberPath = checkNotNull(childNumberPath); this.chainCode = Arrays.copyOf(chainCode, chainCode.length); + this.depth = this.childNumberPath.size(); + this.parentFingerprint = (parent != null) ? parent.getFingerprint() : 0; } /** Constructs a key from its components. This is not normally something you should use. */ @@ -91,6 +97,63 @@ public class DeterministicKey extends ECKey { this.keyCrypter = checkNotNull(crypter); } + /** + * Return the fingerprint of this key's parent as an int value, or zero if this key is the + * root node of the key hierarchy. Raise an exception if the arguments are inconsistent. + * This method exists to avoid code repetition in the constructors. + */ + private int ascertainParentFingerprint(DeterministicKey parentKey, int parentFingerprint) + throws IllegalArgumentException { + if (parentFingerprint != 0) { + if (parent != null) + checkArgument(parent.getFingerprint() == parentFingerprint, + "parent fingerprint mismatch", + Integer.toHexString(parent.getFingerprint()), Integer.toHexString(parentFingerprint)); + return parentFingerprint; + } else return 0; + } + + /** + * Constructs a key from its components, including its public key data and possibly-redundant + * information about its parent key. Invoked when deserializing, but otherwise not something that + * you normally should use. + */ + private DeterministicKey(ImmutableList childNumberPath, + byte[] chainCode, + LazyECPoint publicAsPoint, + @Nullable DeterministicKey parent, + int depth, + int parentFingerprint) { + super(null, compressPoint(checkNotNull(publicAsPoint))); + checkArgument(chainCode.length == 32); + this.parent = parent; + this.childNumberPath = checkNotNull(childNumberPath); + this.chainCode = Arrays.copyOf(chainCode, chainCode.length); + this.depth = depth; + this.parentFingerprint = ascertainParentFingerprint(parent, parentFingerprint); + } + + /** + * Constructs a key from its components, including its private key data and possibly-redundant + * information about its parent key. Invoked when deserializing, but otherwise not something that + * you normally should use. + */ + private DeterministicKey(ImmutableList childNumberPath, + byte[] chainCode, + BigInteger priv, + @Nullable DeterministicKey parent, + int depth, + int parentFingerprint) { + super(priv, compressPoint(ECKey.CURVE.getG().multiply(priv))); + checkArgument(chainCode.length == 32); + this.parent = parent; + this.childNumberPath = checkNotNull(childNumberPath); + this.chainCode = Arrays.copyOf(chainCode, chainCode.length); + this.depth = depth; + this.parentFingerprint = ascertainParentFingerprint(parent, parentFingerprint); + } + + /** Clones the key */ public DeterministicKey(DeterministicKey keyToClone, DeterministicKey newParent) { super(keyToClone.priv, keyToClone.pub.get()); @@ -98,6 +161,8 @@ public class DeterministicKey extends ECKey { this.childNumberPath = keyToClone.childNumberPath; this.chainCode = keyToClone.chainCode; this.encryptedPrivateKey = keyToClone.encryptedPrivateKey; + this.depth = this.childNumberPath.size(); + this.parentFingerprint = this.parent.getFingerprint(); } /** @@ -116,13 +181,18 @@ public class DeterministicKey extends ECKey { return HDUtils.formatPath(getPath()); } - private int getDepth() { - return childNumberPath.size(); + /** + * Return this key's depth in the hierarchy, where the root node is at depth zero. + * This may be different than the number of segments in the path if this key was + * deserialized without access to its parent. + */ + public int getDepth() { + return depth; } /** Returns the last element of the path returned by {@link DeterministicKey#getPath()} */ public ChildNumber getChildNumber() { - return getDepth() == 0 ? ChildNumber.ZERO : childNumberPath.get(childNumberPath.size() - 1); + return childNumberPath.size() == 0 ? ChildNumber.ZERO : childNumberPath.get(childNumberPath.size() - 1); } /** @@ -140,9 +210,9 @@ public class DeterministicKey extends ECKey { } /** Returns the first 32 bits of the result of {@link #getIdentifier()}. */ - public byte[] getFingerprint() { + public int getFingerprint() { // TODO: why is this different than armory's fingerprint? BIP 32: "The first 32 bits of the identifier are called the fingerprint." - return Arrays.copyOfRange(getIdentifier(), 0, 4); + return ByteBuffer.wrap(Arrays.copyOfRange(getIdentifier(), 0, 4)).getInt(); } @Nullable @@ -150,6 +220,14 @@ public class DeterministicKey extends ECKey { return parent; } + /** + * Return the fingerprint of the key from which this key was derived, if this is a + * child key, or else an array of four zero-value bytes. + */ + public int getParentFingerprint() { + return parentFingerprint; + } + /** * Returns private key bytes, padded with zeros to 33 bytes. * @throws java.lang.IllegalStateException if the private key bytes are missing. @@ -293,7 +371,7 @@ public class DeterministicKey extends ECKey { cursor.pub, new BigInteger(1, parentalPrivateKeyBytes), cursor.parent); // Now we have to rederive the keys along the path back to ourselves. That path can be found by just truncating // our path with the length of the parents path. - ImmutableList path = childNumberPath.subList(cursor.getDepth(), childNumberPath.size()); + ImmutableList path = childNumberPath.subList(cursor.getPath().size(), childNumberPath.size()); for (ChildNumber num : path) { downCursor = HDKeyDerivation.deriveChildKey(downCursor, num); } @@ -335,11 +413,7 @@ public class DeterministicKey extends ECKey { ByteBuffer ser = ByteBuffer.allocate(78); ser.putInt(pub ? params.getBip32HeaderPub() : params.getBip32HeaderPriv()); ser.put((byte) getDepth()); - if (parent == null) { - ser.putInt(0); - } else { - ser.put(parent.getFingerprint()); - } + ser.putInt(getParentFingerprint()); ser.putInt(getChildNumber().i()); ser.put(getChainCode()); ser.put(pub ? getPubKey() : getPrivKeyBytes33()); @@ -393,26 +467,25 @@ public class DeterministicKey extends ECKey { if (header != params.getBip32HeaderPriv() && header != params.getBip32HeaderPub()) throw new IllegalArgumentException("Unknown header bytes: " + toBase58(serializedKey).substring(0, 4)); boolean pub = header == params.getBip32HeaderPub(); - byte depth = buffer.get(); - byte[] parentFingerprint = new byte[4]; - buffer.get(parentFingerprint); + int depth = buffer.get() & 0xFF; // convert signed byte to positive int since depth cannot be negative + final int parentFingerprint = buffer.getInt(); final int i = buffer.getInt(); final ChildNumber childNumber = new ChildNumber(i); ImmutableList path; if (parent != null) { - if (Arrays.equals(parentFingerprint, HDUtils.longTo4ByteArray(0))) + if (parentFingerprint == 0) throw new IllegalArgumentException("Parent was provided but this key doesn't have one"); - if (!Arrays.equals(parent.getFingerprint(), parentFingerprint)) + if (parent.getFingerprint() != parentFingerprint) throw new IllegalArgumentException("Parent fingerprints don't match"); path = HDUtils.append(parent.getPath(), childNumber); if (path.size() != depth) throw new IllegalArgumentException("Depth does not match"); } else { if (depth >= 1) - // We have been given a key that is not a root key, yet we lack any object representing the parent. - // This can happen when deserializing an account key for a watching wallet. In this case, we assume that - // the client wants to conceal the key's position in the hierarchy. The parent is deemed to be the - // root of the hierarchy. + // We have been given a key that is not a root key, yet we lack the object representing the parent. + // This can happen when deserializing an account key for a watching wallet. In this case, we assume that + // the client wants to conceal the key's position in the hierarchy. The path is truncated at the + // parent's node. path = ImmutableList.of(childNumber); else path = ImmutableList.of(); } @@ -423,9 +496,9 @@ public class DeterministicKey extends ECKey { checkArgument(!buffer.hasRemaining(), "Found unexpected data in key"); if (pub) { ECPoint point = ECKey.CURVE.getCurve().decodePoint(data); - return new DeterministicKey(path, chainCode, new LazyECPoint(point), null, parent); + return new DeterministicKey(path, chainCode, new LazyECPoint(point), parent, depth, parentFingerprint); } else { - return new DeterministicKey(path, chainCode, new BigInteger(1, data), parent); + return new DeterministicKey(path, chainCode, new BigInteger(1, data), parent, depth, parentFingerprint); } } diff --git a/core/src/test/java/org/bitcoinj/crypto/ChildKeyDerivationTest.java b/core/src/test/java/org/bitcoinj/crypto/ChildKeyDerivationTest.java index e7ed68b6..5606eccd 100644 --- a/core/src/test/java/org/bitcoinj/crypto/ChildKeyDerivationTest.java +++ b/core/src/test/java/org/bitcoinj/crypto/ChildKeyDerivationTest.java @@ -244,6 +244,40 @@ public class ChildKeyDerivationTest { assertEquals(DeterministicKey.deserialize(params, key4.serializePrivate(params)).getPath().size(), 1); } + /** Reserializing a deserialized key should yield the original input */ + @Test + public void reserialization() { + // This is the public encoding of the key with path m/0H/1/2H from BIP32 published test vector 1: + // https://en.bitcoin.it/wiki/BIP_0032_TestVectors + String encoded = + "xpub6D4BDPcP2GT577Vvch3R8wDkScZWzQzMMUm3PWbmWvVJrZwQY4VUNgqFJPMM3No2dFDFGTsxxpG5uJh7n7epu4trkrX7x7DogT5Uv6fcLW5"; + DeterministicKey key = DeterministicKey.deserializeB58(encoded, MainNetParams.get()); + assertEquals("Reserialized parentless private HD key is wrong", key.serializePubB58(MainNetParams.get()), encoded); + assertEquals("Depth of deserialized parentless public HD key is wrong", key.getDepth(), 3); + assertEquals("Path size of deserialized parentless public HD key is wrong", key.getPath().size(), 1); + assertEquals("Parent fingerprint of deserialized parentless public HD key is wrong", + key.getParentFingerprint(), 0xbef5a2f9); + + // This encoding is the same key but including its private data: + encoded = + "xprv9z4pot5VBttmtdRTWfWQmoH1taj2axGVzFqSb8C9xaxKymcFzXBDptWmT7FwuEzG3ryjH4ktypQSAewRiNMjANTtpgP4mLTj34bhnZX7UiM"; + key = DeterministicKey.deserializeB58(encoded, MainNetParams.get()); + assertEquals("Reserialized parentless private HD key is wrong", key.serializePrivB58(MainNetParams.get()), encoded); + assertEquals("Depth of deserialized parentless private HD key is wrong", key.getDepth(), 3); + assertEquals("Path size of deserialized parentless private HD key is wrong", key.getPath().size(), 1); + assertEquals("Parent fingerprint of deserialized parentless private HD key is wrong", + key.getParentFingerprint(), 0xbef5a2f9); + + // These encodings are of the the root key of that hierarchy + assertEquals("Parent fingerprint of root node public HD key should be zero", + DeterministicKey.deserializeB58("xpub661MyMwAqRbcFW31YEwpkMuc5THy2PSt5bDMsktWQcFF8syAmRUapSCGu8ED9W6oDMSgv6Zz8idoc4a6mr8BDzTJY47LJhkJ8UB7WEGuduB", MainNetParams.get()).getParentFingerprint(), + 0); + assertEquals("Parent fingerprint of root node private HD key should be zero", + DeterministicKey.deserializeB58("xprv9s21ZrQH143K31xYSDQpPDxsXRTUcvj2iNHm5NUtrGiGG5e2DtALGdso3pGz6ssrdK4PFmM8NSpSBHNqPqm55Qn3LqFtT2emdEXVYsCzC2U", MainNetParams.get()).getParentFingerprint(), + 0); + + } + private static String hexEncodePub(DeterministicKey pubKey) { return HEX.encode(pubKey.getPubKey()); }