Skip to content

Commit

Permalink
test: wip adding test vectors for BIP32 key derivation
Browse files Browse the repository at this point in the history
  • Loading branch information
redshiftzero committed Sep 11, 2023
1 parent ff45f82 commit fb9883b
Show file tree
Hide file tree
Showing 3 changed files with 88 additions and 13 deletions.
10 changes: 10 additions & 0 deletions Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

1 change: 1 addition & 0 deletions crates/core/keys/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -62,6 +62,7 @@ proptest = "1"
serde_json = "1"
frost377 = { version = "0.2", default-features=false }
num-traits = "0.2"
bs58 = "0.5"

[features]
default = []
Expand Down
90 changes: 77 additions & 13 deletions crates/core/keys/src/keys/bip44.rs
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@ const PENUMBRA_COIN_TYPE: u32 = 0x0001984;
/// BIP43: https://github.com/bitcoin/bips/blob/master/bip-0043.mediawiki
/// BIP44: https://github.com/bitcoin/bips/blob/master/bip-0044.mediawiki
pub struct Bip44Path {
purpose: u32,
coin_type: u32,
account: u32,
change: u32,
Expand All @@ -24,16 +25,34 @@ impl Bip44Path {
/// Create a new BIP44 path for Penumbra.
pub fn new(account: u32, change: u32, address_index: u32) -> Self {
Self {
purpose: 0x8000002C,
coin_type: PENUMBRA_COIN_TYPE,
account,
change,
address_index,
}
}

/// Per BIP43, purpose is a constant set to 44' or 0x8000002C.
/// Create a new generic BIP44 path.
pub fn new_generic(
purpose: u32,
coin_type: u32,
account: u32,
change: u32,
address_index: u32,
) -> Self {
Self {
purpose,
coin_type,
account,
change,
address_index,
}
}

/// Per BIP43, purpose is typically a constant set to 44' or 0x8000002C.
pub fn purpose(&self) -> u32 {
0x8000002C
self.purpose
}

/// Per BIP44, coin type is a constant set for each currency.
Expand Down Expand Up @@ -67,17 +86,6 @@ impl Bip44Path {
}
}

#[cfg(test)]
mod tests {
use super::*;

#[test]
fn test_bip44_path() {
let path = Bip44Path::new(0, 0, 0);
assert_eq!(path.path(), "m/44'/6532'/0'/0/0");
}
}

#[allow(non_snake_case)]
pub fn ckd_priv(k_par: [u8; 32], c_par: [u8; 32], i: u32) -> ([u8; 32], [u8; 32]) {
let mut hmac = Hmac::<sha2::Sha512>::new_from_slice(&c_par).expect("can create hmac");
Expand Down Expand Up @@ -122,6 +130,10 @@ pub fn ckd_priv(k_par: [u8; 32], c_par: [u8; 32], i: u32) -> ([u8; 32], [u8; 32]
// Finally, we need to check if i_L ≥ n or k_i = 0, as the resulting key is invalid
if k_i == Fsecp256k1::zero() || i_L_mod_n_bytes != i_L.to_vec() {
// Key is invalid, proceed with the next value for i
dbg!("this should only occur with w/ low probability");
dbg!(k_i);
dbg!(i_L_mod_n_bytes);
dbg!(i_L.to_vec());
return ckd_priv(k_par, c_par, i + 1);
}

Expand All @@ -130,3 +142,55 @@ pub fn ckd_priv(k_par: [u8; 32], c_par: [u8; 32], i: u32) -> ([u8; 32], [u8; 32]
.expect("can serialize");
(k_i_bytes.try_into().expect("result fits in 32 bytes"), c_i)
}

#[cfg(test)]
mod tests {
use bs58;

use super::*;

#[test]
fn test_bip44_path() {
let path = Bip44Path::new(0, 0, 0);
assert_eq!(path.path(), "m/44'/6532'/0'/0/0");
}

/// The below test vectors are from: https://github.com/bitcoin/bips/blob/master/bip-0032.mediawiki#test-vector-1
#[test]
fn bip44_child_derivation() {
// Test vector 1
let hex_seed = "000102030405060708090a0b0c0d0e0f".to_string();
let seed = hex::decode(hex_seed).expect("can decode test vector");
let mut seed_arr = [0u8; 32];
seed_arr[0..16].copy_from_slice(&seed[..]);

let path = Bip44Path::new_generic(0, 1, 2, 2, 1000000000);

// Chain m
let data = "xprv9s21ZrQH143K3QTDL4LXw2F7HEK3wJUD2nW2nRk4stbPy6cq3jPPqjiChkVvvNKmPGJxWUtg6LnF5kejMRNNU3TGtRBeJgk33yuGBxrMPHi".to_string();
let decoded_bytes = bs58::decode(data)
.into_vec()
.expect("all test vectors should be valid base58");

// Per BIP43, each address has the prefix 0x0488B21E for public and 0x0488ADE4 for private nodes.
// All our test vectors are private nodes since we only implemented the private key derivation:
assert_eq!(decoded_bytes[0..4], [0x04, 0x88, 0xAD, 0xE4]);

// Extended keys are 82 bytes long, the final 4 bytes being a checksum, and the preceding 33 bytes being the
// actual key material. Public keys are 33 bytes, and private keys get a 0x00 appended since they are 32 bytes.
let priv_key_material = &decoded_bytes[46..78];

// First level is hardened derivation in test vector 1.
let i = 2147483648u32; // 2^31
let mut purpose_bytes = [0u8; 32];
let purpose = path.purpose();
let purpose_arr = path.purpose().to_le_bytes();
purpose_bytes[0..4].copy_from_slice(&purpose_arr[..]);
let (k_1, _) = ckd_priv(seed_arr, purpose_bytes, i);

assert_eq!(
k_1, priv_key_material,
"first level derivation should match"
);
}
}

0 comments on commit fb9883b

Please sign in to comment.