3
0
mirror of https://github.com/Qortal/altcoinj.git synced 2025-02-13 10:45:51 +00:00

Don't discard hierarchy data when deserializing an HD key.

Fingerprints are now ints rather than four-byte arrays.
This commit is contained in:
Adam Mackler 2015-01-30 02:21:25 -05:00 committed by Mike Hearn
parent eed4901a6e
commit 7ef5ab9abf
2 changed files with 129 additions and 22 deletions

View File

@ -42,6 +42,8 @@ public class DeterministicKey extends ECKey {
private final DeterministicKey parent;
private final ImmutableList<ChildNumber> 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<ChildNumber> 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<ChildNumber> 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<ChildNumber> 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<ChildNumber> path = childNumberPath.subList(cursor.getDepth(), childNumberPath.size());
ImmutableList<ChildNumber> 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<ChildNumber> 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);
}
}

View File

@ -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());
}