Skip to content
This repository has been archived by the owner on Jun 6, 2024. It is now read-only.

keytree: add serialization for xpub, xprv #228

Merged
merged 10 commits into from
Mar 19, 2019
Merged
142 changes: 142 additions & 0 deletions keytree/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -50,6 +50,39 @@ impl Xprv {
precompressed_pubkey: self.precompressed_pubkey,
}
}

/// Serializes this Xprv to a sequence of bytes.
pub fn to_bytes(&self) -> [u8; 64] {
let mut buf = [0u8; 64];
buf[..32].copy_from_slice(&self.scalar.to_bytes());
buf[32..].copy_from_slice(&self.dk);
buf
}

/// Decodes an Xprv from a 64-byte array, and fails if the provided array is not
/// exactly 64 bytes.
pub fn from_bytes(bytes: &[u8]) -> Option<Self> {
if bytes.len() != 64 {
return None;
}

let (scslice, dkslice) = bytes.split_at(32);
tessr marked this conversation as resolved.
Show resolved Hide resolved
let mut scalar_bytes = [0u8; 32];
scalar_bytes.copy_from_slice(&scslice[..]);
tessr marked this conversation as resolved.
Show resolved Hide resolved
let scalar = match Scalar::from_canonical_bytes(scalar_bytes) {
Some(x) => x,
None => return None,
};
let mut dk = [0u8; 32];
dk.copy_from_slice(&dkslice[..]);
let precompressed_pubkey = (scalar * &constants::RISTRETTO_BASEPOINT_POINT).compress();

return Some(Xprv {
scalar,
dk,
precompressed_pubkey,
});
}
}

impl Xpub {
Expand Down Expand Up @@ -78,6 +111,37 @@ impl Xpub {
precompressed_pubkey: child_point.compress(),
}
}

/// Serializes this Xpub to a sequence of bytes.
pub fn to_bytes(&self) -> [u8; 64] {
let mut buf = [0u8; 64];
buf[..32].copy_from_slice(self.precompressed_pubkey.as_bytes());
buf[32..].copy_from_slice(&self.dk);
buf
}

/// Decodes an Xpub from a 64-byte array, and fails if the provided array is not
/// exactly 64 bytes, or if the compressed point fails to decompress.
pub fn from_bytes(bytes: &[u8]) -> Option<Self> {
if bytes.len() != 64 {
return None;
}

let (pkslice, dkslice) = bytes.split_at(32);
let precompressed_pubkey = CompressedRistretto::from_slice(&pkslice[..]);
let mut dk = [0u8; 32];
dk.copy_from_slice(&dkslice[..]);

let point = match precompressed_pubkey.decompress() {
Some(p) => p,
None => return None,
};
Some(Xpub {
point,
dk,
precompressed_pubkey,
})
}
}

#[cfg(test)]
Expand Down Expand Up @@ -106,6 +170,42 @@ mod tests {
assert_eq!(expected_scalar, xprv.scalar);
}

#[test]
fn serialize_xprv_test() {
let seed = [0u8; 32];
let mut rng = ChaChaRng::from_seed(seed);
let xprv = Xprv::random(&mut rng);
let xprv_bytes = xprv.to_bytes();

// hardcoded, but happens to be expected_scalar concatenated with expected_dk
let expected_bytes = [
Copy link
Contributor

@oleganza oleganza Mar 18, 2019

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

there's hex crate with hex::encode method so we can write a more readable test (and then copy this over to the spec as Test Vectors):

assert_eq!(hex::encode(&xprv.to_bytes()[..]), "df0da2b88d88de1ad7678d8f012...");

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

hex is only needed in dev-dependencies in Cargo.toml. So you'd include it only under the mod test { }, not in the top of lib.rs.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I just tried this out, and it's neat, but I'm not sure it actually produces a more readable test, especially since all the other tests in this package (for creating xprvs and xpubs) use non-hex encoded byte arrays. When I switched over to a hex string, I could no longer eyeball that these were the same.

WDYT?

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

i think all have to be hex, across the board. But if it's a PITA to update, we can do that in a separate PR later.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

we'd definitely want that to post the test vectors in the spec in hex. But before that we need to implement derivation for both xpub and xprv.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'll do it all in another PR, then. (It's fine to do, I should just change them all together.)

74, 83, 195, 251, 188, 89, 151, 14, 229, 248, 90, 248, 19, 135, 93, 255, 193, 58, 144,
74, 46, 83, 174, 126, 101, 250, 13, 234, 110, 98, 201, 1, 159, 7, 231, 190, 85, 81, 56,
122, 152, 186, 151, 124, 115, 45, 8, 13, 203, 15, 41, 160, 72, 227, 101, 105, 18, 198,
83, 62, 50, 238, 122, 237,
];
assert_eq!(&xprv_bytes[..], &expected_bytes[..]);
}

#[test]
fn deserialize_xprv_test() {
let xprv_bytes = [
74, 83, 195, 251, 188, 89, 151, 14, 229, 248, 90, 248, 19, 135, 93, 255, 193, 58, 144,
74, 46, 83, 174, 126, 101, 250, 13, 234, 110, 98, 201, 1, 159, 7, 231, 190, 85, 81, 56,
122, 152, 186, 151, 124, 115, 45, 8, 13, 203, 15, 41, 160, 72, 227, 101, 105, 18, 198,
83, 62, 50, 238, 122, 237,
];

let xprv = Xprv::from_bytes(&xprv_bytes).unwrap();

let seed = [0u8; 32];
let mut rng = ChaChaRng::from_seed(seed);
let expected_xprv = Xprv::random(&mut rng);

assert_eq!(xprv.dk, expected_xprv.dk);
assert_eq!(xprv.scalar, expected_xprv.scalar);
}

#[test]
fn random_xpub_test() {
let seed = [0u8; 32];
Expand All @@ -129,6 +229,48 @@ mod tests {
assert_eq!(xpub.precompressed_pubkey, expected_compressed_point);
}

#[test]
fn serialize_xpub_test() {
let seed = [0u8; 32];
let mut rng = ChaChaRng::from_seed(seed);
let xprv = Xprv::random(&mut rng);
let xpub = xprv.to_xpub();

let xpub_bytes = xpub.to_bytes();

// hardcoded, but happens to be expected_scalar concatenated with expected_compressed_point
let expected_bytes = [
156, 102, 163, 57, 200, 52, 79, 146, 47, 195, 32, 108, 181, 218, 232, 20, 165, 148,
192, 23, 125, 211, 35, 92, 37, 77, 156, 64, 154, 101, 184, 8, 159, 7, 231, 190, 85, 81,
56, 122, 152, 186, 151, 124, 115, 45, 8, 13, 203, 15, 41, 160, 72, 227, 101, 105, 18,
198, 83, 62, 50, 238, 122, 237,
];
assert_eq!(&xpub_bytes[..], &expected_bytes[..]);
}

#[test]
fn deserialize_xpub_test() {
let xpub_bytes = [
156, 102, 163, 57, 200, 52, 79, 146, 47, 195, 32, 108, 181, 218, 232, 20, 165, 148,
192, 23, 125, 211, 35, 92, 37, 77, 156, 64, 154, 101, 184, 8, 159, 7, 231, 190, 85, 81,
56, 122, 152, 186, 151, 124, 115, 45, 8, 13, 203, 15, 41, 160, 72, 227, 101, 105, 18,
198, 83, 62, 50, 238, 122, 237,
];
let xpub = Xpub::from_bytes(&xpub_bytes).unwrap();

let seed = [0u8; 32];
let mut rng = ChaChaRng::from_seed(seed);
let expected_xprv = Xprv::random(&mut rng);
let expected_xpub = expected_xprv.to_xpub();

assert_eq!(xpub.dk, expected_xpub.dk);
assert_eq!(xpub.point, expected_xpub.point);
assert_eq!(
xpub.precompressed_pubkey,
expected_xpub.precompressed_pubkey
);
}

#[test]
fn random_xpub_derivation_test() {
let seed = [0u8; 32];
Expand Down