Skip to content

Commit

Permalink
Add stark test vectors
Browse files Browse the repository at this point in the history
Signed-off-by: Denis Varlakov <[email protected]>
  • Loading branch information
survived committed Nov 25, 2024
1 parent f152108 commit 9bd5655
Show file tree
Hide file tree
Showing 8 changed files with 466 additions and 10 deletions.
20 changes: 15 additions & 5 deletions Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -35,28 +35,38 @@ curve-secp256k1 = ["generic-ec/curve-secp256k1", "slip10"]
curve-secp256r1 = ["generic-ec/curve-secp256r1", "slip10"]
# Enables Edwards-specific derivation
curve-ed25519 = ["generic-ec/curve-ed25519", "edwards"]
# Enables Stark-specific derivation
curve-stark = ["generic-ec/curve-stark", "stark"]
all-curves = ["curve-secp256k1", "curve-secp256r1", "curve-ed25519"]
serde = ["dep:serde", "generic-ec/serde"]

# Enables Slip10 derivation
slip10 = ["hmac", "sha2", "generic-array", "subtle"]
slip10 = ["subtle", "hmac", "sha2", "generic-array"]
# Enables Edwards-specific derivation
edwards = ["hmac", "sha2", "generic-array", "curve-ed25519"]
edwards = ["curve-ed25519", "hmac", "sha2", "generic-array"]
# Enables Stark-specific derivation
stark = []
stark = ["curve-stark", "hmac", "sha2", "generic-array"]

[[test]]
name = "slip10_test_vector"
required-features = ["curve-secp256k1", "curve-secp256r1"]
required-features = ["slip10", "curve-secp256k1", "curve-secp256r1"]

[[test]]
name = "edwards_test_vector"
required-features = ["curve-ed25519"]
required-features = ["edwards"]

[[test]]
name = "stark_test_vector"
required-features = ["stark"]

[[example]]
name = "curves_analysis"
required-features = ["all-curves"]

[[example]]
name = "generate_test_vectors"
required-features = ["all-curves"]

[package.metadata.docs.rs]
all-features = true
rustdoc-args = ["--cfg", "docsrs", "--html-in-header", "katex-header.html"]
103 changes: 103 additions & 0 deletions examples/generate_test_vectors.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,103 @@
use generic_ec::{Curve, Point, SecretScalar};
use rand::Rng;

fn main() {
generate_test_vectors::<generic_ec::curves::Stark, hd_wallet::Stark>();
}

fn generate_test_vectors<E: Curve, Hd: hd_wallet::HdWallet<E>>() {
let mut rng = rand::thread_rng();

let sk = SecretScalar::<E>::random(&mut rng);
let pk = Point::generator() * &sk;
let chain_code: hd_wallet::ChainCode = rng.gen();

println!("sk: {}", hex::encode(sk.as_ref().to_be_bytes()));
println!("pk: {}", hex::encode(pk.to_bytes(true)));
println!("chain code: {}", hex::encode(chain_code));

let paths: &[&[u32]] = &[
// Non-hardened derivation
&[0],
&[1],
&[2],
&[
rng.gen_range(0..hd_wallet::H),
rng.gen_range(0..hd_wallet::H),
rng.gen_range(0..hd_wallet::H),
rng.gen_range(0..hd_wallet::H),
],
&[
rng.gen_range(0..hd_wallet::H),
rng.gen_range(0..hd_wallet::H),
rng.gen_range(0..hd_wallet::H),
rng.gen_range(0..hd_wallet::H),
],
// Hardened derivation
&[0 + hd_wallet::H],
&[1 + hd_wallet::H],
&[2 + hd_wallet::H],
// Mixed hardened and non-hardened derivation
&[
rng.gen_range(0..hd_wallet::H),
rng.gen_range(hd_wallet::H..=u32::MAX),
rng.gen_range(0..hd_wallet::H),
rng.gen_range(hd_wallet::H..=u32::MAX),
],
&[
rng.gen_range(0..hd_wallet::H),
rng.gen_range(0..hd_wallet::H),
rng.gen_range(hd_wallet::H..=u32::MAX),
rng.gen_range(hd_wallet::H..=u32::MAX),
],
];

let key = hd_wallet::ExtendedSecretKey {
secret_key: sk,
chain_code,
};
let key = hd_wallet::ExtendedKeyPair::from(key);
assert_eq!(key.public_key().public_key, pk);

for path in paths {
println!("{:?}", PathFmt(path));
let child_key = Hd::derive_child_key_pair_with_path(&key, path.iter().copied());
println!(
"child secret key: {}",
hex::encode(child_key.secret_key().secret_key.as_ref().to_be_bytes())
);
println!(
"child public key: {}",
hex::encode(child_key.public_key().public_key.to_bytes(true))
);
println!("child chain code: {}", hex::encode(child_key.chain_code()));
println!();
}
}

struct PathFmt<'a>(&'a [u32]);

impl<'a> std::fmt::Debug for PathFmt<'a> {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
write!(f, "[")?;
for (is_first, index) in std::iter::once(true)
.chain(std::iter::repeat(false))
.zip(self.0)
{
if !is_first {
write!(f, ", ")?;
}

if *index < hd_wallet::H {
write!(f, "{index}")?;
} else {
write!(
f,
"{} + hd_wallet::H",
index.checked_sub(hd_wallet::H).unwrap()
)?;
}
}
write!(f, "]")
}
}
2 changes: 0 additions & 2 deletions src/edwards.rs
Original file line number Diff line number Diff line change
Expand Up @@ -49,7 +49,6 @@ impl DeriveShift<curves::Ed25519> for Edwards {
let hmac = HmacSha512::new_from_slice(&parent_public_key.chain_code)
.expect("this never fails: hmac can handle keys of any size");
let i = hmac
.clone()
.chain_update(parent_public_key.public_key.to_bytes(true))
// we append 0 byte to the public key for compatibility with other libs
.chain_update([0x00])
Expand All @@ -66,7 +65,6 @@ impl DeriveShift<curves::Ed25519> for Edwards {
let hmac = HmacSha512::new_from_slice(parent_key.chain_code())
.expect("this never fails: hmac can handle keys of any size");
let i = hmac
.clone()
.chain_update([0x00])
.chain_update(parent_key.secret_key.secret_key.as_ref().to_be_bytes())
.chain_update(child_index.to_be_bytes())
Expand Down
4 changes: 4 additions & 0 deletions src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -80,11 +80,15 @@ pub mod edwards;
pub mod errors;
#[cfg(feature = "slip10")]
pub mod slip10;
#[cfg(feature = "stark")]
pub mod stark;

#[cfg(feature = "edwards")]
pub use edwards::Edwards;
#[cfg(feature = "slip10")]
pub use slip10::Slip10;
#[cfg(feature = "stark")]
pub use stark::Stark;

/// Beginning of hardened child indexes
///
Expand Down
8 changes: 5 additions & 3 deletions src/slip10.rs
Original file line number Diff line number Diff line change
Expand Up @@ -33,11 +33,13 @@ type HmacSha512 = hmac::Hmac<sha2::Sha512>;
/// secret keys are represented as scalars and public keys as points, see [`ExtendedSecretKey`]
/// and [`ExtendedPublicKey`].
///
/// If you need HD derivation on Ed25519 curve, we recommend using [`Edwards`] HD derivation,
/// which supports both hardened and non-hardened derivation.
/// [`ExtendedSecretKey`]: crate::ExtendedSecretKey
///
/// If you need HD derivation on Ed25519 curve, we recommend using [`Edwards`](crate::Edwards) HD
/// derivation, which supports both hardened and non-hardened derivation.
///
/// ## Master key derivation from the seed
/// [`slip10::derive_master_key`] can be used to derive a master key from the seed as defined
/// [`derive_master_key`] can be used to derive a master key from the seed as defined
/// in the spec.
///
/// ## Example
Expand Down
132 changes: 132 additions & 0 deletions src/stark.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,132 @@
//! Stark HD derivation
//!
//! This module provides [`Stark`] derivation as well as aliases for calling
//! `<Stark as HdWallet<_>>::*` methods for convenience when you don't need to support
//! generic HD derivation algorithm.
//!
//! See [`Stark`] docs to learn more about the derivation method.
use generic_ec::{curves, Point, Scalar};
use hmac::Mac;

use crate::{
DeriveShift, DerivedShift, ExtendedKeyPair, ExtendedPublicKey, HardenedIndex, NonHardenedIndex,
};

type HmacSha512 = hmac::Hmac<sha2::Sha512>;

/// HD derivation for stark curve
///
/// ## Algorithm
/// The algorithm is a modification of BIP32:
///
/// ```text
/// def derive_child_key(parent_public_key[, parent_secret_key], parent_chain_code, child_index):
/// if is_hardened(child_index):
/// i = HMAC_SHA512(key = parent_chain_code, 0x00 || 0x00 || parent_secret_key || child_index)
/// || HMAC_SHA512(key = parent_chain_code, 0x01 || 0x00 || parent_secret_key || child_index)
/// else:
/// i = HMAC_SHA512(key = parent_chain_code, 0x00 || parent_public_key || child_index)
/// || HMAC_SHA512(key = parent_chain_code, 0x01 || parent_public_key || child_index)
/// shift = i[..96] mod order
/// child_secret_key = parent_secret_key + shift and/or child_public_key = parent_public_key + shift G
/// child_chain_code = i[N..]
/// return child_public_key[, child_secret_key], child_chain_code
/// ```
///
/// ## Other known methods for stark HD derivation
/// There's another known method for HD derivation on stark curve implemented in
/// [argent-x], which basically derives secp256k1 child key from a seed, and then
/// uses grinding function to deterministically convert it into stark key.
///
/// We decided not to implement it due to its cons:
/// * No support for non-hardened derivation
/// * Grinding is a probabilistic algorithm which does a lot of hashing (32 hashes
/// on average, but in worst case can be 1000+).
/// * In general, it's strange to derive secp256k1 key and then convert it to stark key
///
/// Our derivation algorithm addresses these flaws: it yields a stark key right away (without
/// any intermediate secp256k1 keys), supports non-hardened derivation, does only 2 hashes per
/// derivation.
///
/// [argent-x]: https://github.com/argentlabs/argent-x/blob/13142607d83fea10b297d6a23452e810605784d1/packages/extension/src/shared/signer/ArgentSigner.ts#L14-L25,
pub struct Stark;

impl DeriveShift<curves::Stark> for Stark {
fn derive_public_shift(
parent_public_key: &ExtendedPublicKey<curves::Stark>,
child_index: NonHardenedIndex,
) -> DerivedShift<curves::Stark> {
let hmac = HmacSha512::new_from_slice(&parent_public_key.chain_code)
.expect("this never fails: hmac can handle keys of any size");
let i0 = hmac
.clone()
.chain_update([0x00])
.chain_update(parent_public_key.public_key.to_bytes(true))
.chain_update(child_index.to_be_bytes())
.finalize()
.into_bytes();
let i1 = hmac
.chain_update([0x01])
.chain_update(parent_public_key.public_key.to_bytes(true))
.chain_update(child_index.to_be_bytes())
.finalize()
.into_bytes();
Self::calculate_shift(parent_public_key, i0, i1)
}

fn derive_hardened_shift(
parent_key: &ExtendedKeyPair<curves::Stark>,
child_index: HardenedIndex,
) -> DerivedShift<curves::Stark> {
let hmac = HmacSha512::new_from_slice(parent_key.chain_code())
.expect("this never fails: hmac can handle keys of any size");
let i0 = hmac
.clone()
.chain_update([0x00])
.chain_update([0x00])
.chain_update(parent_key.secret_key.secret_key.as_ref().to_be_bytes())
.chain_update(child_index.to_be_bytes())
.finalize()
.into_bytes();
let i1 = hmac
.chain_update([0x01])
.chain_update([0x00])
.chain_update(parent_key.secret_key.secret_key.as_ref().to_be_bytes())
.chain_update(child_index.to_be_bytes())
.finalize()
.into_bytes();
Self::calculate_shift(&parent_key.public_key, i0, i1)
}
}

impl Stark {
fn calculate_shift(
parent_public_key: &ExtendedPublicKey<curves::Stark>,
i0: hmac::digest::Output<HmacSha512>,
i1: hmac::digest::Output<HmacSha512>,
) -> DerivedShift<curves::Stark> {
let i = generic_array::sequence::Concat::concat(i0, i1);
let (shift, chain_code) = split(&i);

let shift = Scalar::from_be_bytes_mod_order(shift);
let child_pk = parent_public_key.public_key + Point::generator() * shift;

DerivedShift {
shift,
child_public_key: ExtendedPublicKey {
public_key: child_pk,
chain_code: (*chain_code).into(),
},
}
}
}

fn split(
i: &generic_array::GenericArray<u8, generic_array::typenum::U128>,
) -> (
&generic_array::GenericArray<u8, generic_array::typenum::U96>,
&generic_array::GenericArray<u8, generic_array::typenum::U32>,
) {
generic_array::sequence::Split::split(i)
}
2 changes: 2 additions & 0 deletions tests/edwards_test_vector.rs
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,8 @@ struct Derivation {
expected_public_key: [u8; 32],
}

/// These test vectors were obtained by running HD derivation in another library
/// that implements edwards HD derivation
const TEST_VECTORS: &[TestVector] = &[TestVector {
root_secret_key: hex!("09ba1ad29fabe87a0cf23fec142db2adfb8f9e7089928000dcba5714e08236ec"),
root_public_key: hex!("6fa093b0e855f5fdb40d77f6efe9b67b709092a71d73f35de6afc70cac40d57a"),
Expand Down
Loading

0 comments on commit 9bd5655

Please sign in to comment.