From 336a151198d35767edd24a222d1b88b6ca1393b4 Mon Sep 17 00:00:00 2001 From: Jon Staab Date: Thu, 19 Feb 2026 18:05:07 -0800 Subject: [PATCH] Add unit tests --- frost-taproot/src/ecc/mod.rs | 3 + frost-taproot/src/ecc/tests.rs | 457 +++++++++++++++++++++++++++++ frost-taproot/src/ecdh_tests.rs | 108 +++++++ frost-taproot/src/group_tests.rs | 94 ++++++ frost-taproot/src/helpers_tests.rs | 194 ++++++++++++ frost-taproot/src/lib.rs | 17 ++ frost-taproot/src/poly_tests.rs | 159 ++++++++++ frost-taproot/src/recover_tests.rs | 106 +++++++ frost-taproot/src/refresh_tests.rs | 120 ++++++++ frost-taproot/src/shares_tests.rs | 207 +++++++++++++ frost-taproot/src/vss_tests.rs | 106 +++++++ 11 files changed, 1571 insertions(+) create mode 100644 frost-taproot/src/ecc/tests.rs create mode 100644 frost-taproot/src/ecdh_tests.rs create mode 100644 frost-taproot/src/group_tests.rs create mode 100644 frost-taproot/src/helpers_tests.rs create mode 100644 frost-taproot/src/poly_tests.rs create mode 100644 frost-taproot/src/recover_tests.rs create mode 100644 frost-taproot/src/refresh_tests.rs create mode 100644 frost-taproot/src/shares_tests.rs create mode 100644 frost-taproot/src/vss_tests.rs diff --git a/frost-taproot/src/ecc/mod.rs b/frost-taproot/src/ecc/mod.rs index e51319f..021a00e 100644 --- a/frost-taproot/src/ecc/mod.rs +++ b/frost-taproot/src/ecc/mod.rs @@ -2,3 +2,6 @@ pub mod group; pub mod hash; pub mod state; pub mod util; + +#[cfg(test)] +mod tests; diff --git a/frost-taproot/src/ecc/tests.rs b/frost-taproot/src/ecc/tests.rs new file mode 100644 index 0000000..6c2b004 --- /dev/null +++ b/frost-taproot/src/ecc/tests.rs @@ -0,0 +1,457 @@ +// Unit tests for the ecc module. + +#[cfg(test)] +mod util_tests { + use crate::ecc::util::*; + use k256::{ProjectivePoint, Scalar, U256}; + + fn s(hex: &str) -> [u8; 32] { + let b = hex::decode(hex).unwrap(); + b.try_into().unwrap() + } + + fn p(hex: &str) -> [u8; 33] { + let b = hex::decode(hex).unwrap(); + b.try_into().unwrap() + } + + // ── mod_n ──────────────────────────────────────────────────────────────── + + #[test] + fn mod_n_zero_is_zero() { + assert_eq!(mod_n(U256::ZERO), Scalar::ZERO); + } + + #[test] + fn mod_n_one_is_one() { + assert_eq!(mod_n(U256::ONE), Scalar::ONE); + } + + #[test] + fn mod_n_reduces_n_to_zero() { + // N mod N = 0 + assert_eq!(mod_n(N), Scalar::ZERO); + } + + #[test] + fn mod_n_reduces_n_plus_one_to_one() { + let n_plus_one = N.wrapping_add(&U256::ONE); + assert_eq!(mod_n(n_plus_one), Scalar::ONE); + } + + // ── scalar_from_bytes / scalar_to_bytes ────────────────────────────────── + + #[test] + fn scalar_roundtrip() { + let bytes = s("0e74411caa9ef5ba058d0ccf4e54e1ee773e625fb7d6258097466cedab0de152"); + let scalar = scalar_from_bytes(&bytes); + assert_eq!(scalar_to_bytes(&scalar), bytes); + } + + #[test] + fn scalar_from_zero_bytes() { + let bytes = [0u8; 32]; + assert_eq!(scalar_from_bytes(&bytes), Scalar::ZERO); + } + + // ── pow_n ──────────────────────────────────────────────────────────────── + + #[test] + fn pow_n_exp_zero_is_one() { + assert_eq!(pow_n(5, 0), Scalar::ONE); + assert_eq!(pow_n(0, 0), Scalar::ONE); + } + + #[test] + fn pow_n_exp_one_is_base() { + let three = scalar_from_bytes(&{ + let mut b = [0u8; 32]; + b[31] = 3; + b + }); + assert_eq!(pow_n(3, 1), three); + } + + #[test] + fn pow_n_three_to_four() { + // 3^4 = 81 = 0x51 + let expected = scalar_from_bytes(&{ + let mut b = [0u8; 32]; + b[31] = 81; + b + }); + assert_eq!(pow_n(3, 4), expected); + } + + #[test] + fn pow_n_two_to_eight() { + // 2^8 = 256 = 0x0100 + let expected = scalar_from_bytes(&{ + let mut b = [0u8; 32]; + b[30] = 1; + b[31] = 0; + b + }); + assert_eq!(pow_n(2, 8), expected); + } + + // ── lift_x ─────────────────────────────────────────────────────────────── + + #[test] + fn lift_x_32_bytes_gives_even_y_point() { + // x-only: strip prefix from known compressed point + let x_only = + hex::decode("1ae63bc9ddaffe52d44c3018e83115bfb22195bd8112fcad112310714e6fd5ec") + .unwrap(); + let pt = lift_x(&x_only).unwrap(); + assert!(has_even_y(&pt)); + let serialized = serialize_point(&pt); + assert_eq!(serialized[0], 0x02); + } + + #[test] + fn lift_x_33_bytes_compressed() { + let compressed = p("021ae63bc9ddaffe52d44c3018e83115bfb22195bd8112fcad112310714e6fd5ec"); + let pt = lift_x(&compressed).unwrap(); + assert!(has_even_y(&pt)); + } + + #[test] + fn lift_x_odd_prefix_still_works() { + // 0x03 prefix = odd Y; lift_x should still decode it + let odd = p("034bc9f2ef5cc5eb741cc00d763e1077e8bc624df82d198781c71a0757617d8d44"); + let pt = lift_x(&odd).unwrap(); + // The point itself has odd Y + assert!(!has_even_y(&pt)); + } + + #[test] + fn lift_x_invalid_bytes_errors() { + assert!(lift_x(&[0u8; 31]).is_err()); + assert!(lift_x(&[0u8; 34]).is_err()); + // All-zero 32 bytes is not a valid x-coordinate + assert!(lift_x(&[0u8; 32]).is_err()); + } + + // ── serialize / deserialize ────────────────────────────────────────────── + + #[test] + fn serialize_deserialize_roundtrip() { + let pt = ProjectivePoint::GENERATOR; + let bytes = serialize_point(&pt); + let recovered = deserialize_point(&bytes).unwrap(); + assert_eq!(pt, recovered); + } + + #[test] + fn generator_serializes_correctly() { + let pt = ProjectivePoint::GENERATOR; + let bytes = serialize_point(&pt); + assert_eq!( + hex::encode(bytes), + "0279be667ef9dcbbac55a06295ce870b07029bfcdb2dce28d959f2815b16f81798" + ); + } + + // ── has_even_y / negate_point ──────────────────────────────────────────── + + #[test] + fn generator_has_even_y() { + assert!(has_even_y(&ProjectivePoint::GENERATOR)); + } + + #[test] + fn negate_flips_parity() { + let pt = ProjectivePoint::GENERATOR; + let neg = negate_point(&pt); + assert!(!has_even_y(&neg)); + // Double negation is identity + assert_eq!(negate_point(&neg), pt); + } + + // ── scalar_invert ──────────────────────────────────────────────────────── + + #[test] + fn scalar_invert_of_one_is_one() { + let inv = scalar_invert(&Scalar::ONE).unwrap(); + assert_eq!(inv, Scalar::ONE); + } + + #[test] + fn scalar_invert_of_zero_errors() { + assert!(scalar_invert(&Scalar::ZERO).is_err()); + } + + #[test] + fn scalar_invert_roundtrip() { + let a = scalar_from_bytes(&s( + "0e74411caa9ef5ba058d0ccf4e54e1ee773e625fb7d6258097466cedab0de152", + )); + let inv = scalar_invert(&a).unwrap(); + assert_eq!(a * inv, Scalar::ONE); + } +} + +#[cfg(test)] +mod group_tests { + use crate::ecc::group::*; + use k256::Scalar; + + fn p(hex: &str) -> [u8; 33] { + let b = hex::decode(hex).unwrap(); + b.try_into().unwrap() + } + + // ── scalar_base_multi ──────────────────────────────────────────────────── + + #[test] + fn scalar_base_multi_one_is_generator() { + let g = scalar_base_multi(&Scalar::ONE); + assert_eq!( + hex::encode(serialize_element(&g)), + "0279be667ef9dcbbac55a06295ce870b07029bfcdb2dce28d959f2815b16f81798" + ); + } + + #[test] + fn scalar_base_multi_known_value() { + // 7*G + let seven = crate::ecc::util::scalar_from_bytes(&{ + let mut b = [0u8; 32]; + b[31] = 7; + b + }); + let pt = scalar_base_multi(&seven); + assert_eq!( + hex::encode(serialize_element(&pt)), + "025cbdf0646e5db4eaa398f365f2ea7a0e3d419b7e0330e39ce92bddedcac4f9bc" + ); + } + + // ── element_add ────────────────────────────────────────────────────────── + + #[test] + fn element_add_2g_plus_3g_equals_5g() { + let two = crate::ecc::util::scalar_from_bytes(&{ + let mut b = [0u8; 32]; + b[31] = 2; + b + }); + let three = crate::ecc::util::scalar_from_bytes(&{ + let mut b = [0u8; 32]; + b[31] = 3; + b + }); + let five = crate::ecc::util::scalar_from_bytes(&{ + let mut b = [0u8; 32]; + b[31] = 5; + b + }); + let a = scalar_base_multi(&two); + let b = scalar_base_multi(&three); + let c = element_add(Some(a), Some(b)).unwrap(); + let expected = scalar_base_multi(&five); + assert_eq!( + hex::encode(serialize_element(&c)), + hex::encode(serialize_element(&expected)) + ); + } + + #[test] + fn element_add_none_left_returns_right() { + let g = scalar_base_multi(&Scalar::ONE); + let result = element_add(None, Some(g)).unwrap(); + assert_eq!(serialize_element(&result), serialize_element(&g)); + } + + #[test] + fn element_add_none_right_returns_left() { + let g = scalar_base_multi(&Scalar::ONE); + let result = element_add(Some(g), None).unwrap(); + assert_eq!(serialize_element(&result), serialize_element(&g)); + } + + #[test] + fn element_add_both_none_errors() { + assert!(element_add(None, None).is_err()); + } + + // ── scalar_multi ───────────────────────────────────────────────────────── + + #[test] + fn scalar_multi_matches_base_multi() { + let seven = crate::ecc::util::scalar_from_bytes(&{ + let mut b = [0u8; 32]; + b[31] = 7; + b + }); + let g = scalar_base_multi(&Scalar::ONE); + let result = scalar_multi(&g, &seven); + let expected = scalar_base_multi(&seven); + assert_eq!(serialize_element(&result), serialize_element(&expected)); + } + + // ── serialize_scalar_u32 ───────────────────────────────────────────────── + + #[test] + fn serialize_scalar_u32_zero() { + assert_eq!(serialize_scalar_u32(0), [0u8; 32]); + } + + #[test] + fn serialize_scalar_u32_one() { + let mut expected = [0u8; 32]; + expected[31] = 1; + assert_eq!(serialize_scalar_u32(1), expected); + } + + #[test] + fn serialize_scalar_u32_large() { + let mut expected = [0u8; 32]; + expected[28..].copy_from_slice(&0x01020304u32.to_be_bytes()); + assert_eq!(serialize_scalar_u32(0x01020304), expected); + } +} + +#[cfg(test)] +mod hash_tests { + use crate::ecc::hash::*; + + // All expected values verified against the cmdruid/frost TypeScript implementation. + + #[test] + fn h1_empty_input() { + let result = h1(&[]); + assert_eq!( + hex::encode(result), + "28d6cedb3fba18f85dbb373d8b01328464bf020f6ad651d877998f70f2980fd0" + ); + } + + #[test] + fn h2_empty_input() { + let result = h2(&[]); + assert_eq!( + hex::encode(result), + "ac6482b13a7f3ae0cbb38157c557f1857bf480c77c63e19e73e8070d98c86fc3" + ); + } + + #[test] + fn h3_empty_input() { + let result = h3(&[]); + assert_eq!( + hex::encode(result), + "2da9f94fe1dde8b15546272af0cad7c8ba7c4a58e8ec7087521055161c8fbc03" + ); + } + + #[test] + fn h3_known_input() { + // generate_nonce(share[1].seckey, hidden_seed) from the fixture + let input = hex::decode( + "0070ca75929ca1ec4cd70ac34f46079bdfdd87f9d0c0bf4275f3882f7b462d0f\ + 0e74411caa9ef5ba058d0ccf4e54e1ee773e625fb7d6258097466cedab0de152", + ) + .unwrap(); + assert_eq!( + hex::encode(h3(&input)), + "189aeb1bf3a453673cb144a459f0b644183ff02808cad807b672067da4f33357" + ); + } + + #[test] + fn h4_empty_input() { + let result = h4(&[]); + assert_eq!( + hex::encode(result), + "578967b1c52aeeb9d8c64aa02fbe2cf7f171b58f2547dbeb349be5eff9ba1549" + ); + } + + #[test] + fn h4_known_input() { + // "test" in hex = 74657374 + let input = hex::decode("74657374").unwrap(); + assert_eq!( + hex::encode(h4(&input)), + "ff9b5210ffbb3c07a73a7c8935be4a8c62cf015f6cf7ade6efac09a6513540fc" + ); + } + + #[test] + fn h5_known_input() { + // Vector from hash.json + let input = hex::decode( + "000000000000000000000000000000000000000000000000000000000000000103c699af97d26bb4d3f05232ec5e1938c12f1e6ae97643c8f8f11c9820303f190402fa2aaccd51b948c9dc1a325d77226e98a5a3fe65fe9ba213761a60123040a45e000000000000000000000000000000000000000000000000000000000000000303077507ba327fc074d2793955ef3410ee3f03b82b4cdc2370f71d865beb926ef602ad53031ddfbbacfc5fbda3d3b0c2445c8e3e99cbc4ca2db2aa283fa68525b135", + ).unwrap(); + assert_eq!( + hex::encode(h5(&input)), + "3f5a816aaebc2114a811a415d7a55db7c5cbc1cf27183e79dd9def941b5d4801" + ); + } + + #[test] + fn h1_h2_h3_differ_for_same_input() { + // Different DSTs must produce different outputs + let input = b"test"; + let r1 = h1(input); + let r2 = h2(input); + let r3 = h3(input); + assert_ne!(r1, r2); + assert_ne!(r2, r3); + assert_ne!(r1, r3); + } + + #[test] + fn h4_h5_differ_for_same_input() { + let input = b"test"; + assert_ne!(h4(input), h5(input)); + } +} + +#[cfg(test)] +mod state_tests { + use crate::ecc::{group::serialize_element, state::get_point_state, util::lift_x}; + + fn h32(hex: &str) -> [u8; 32] { + hex::decode(hex).unwrap().try_into().unwrap() + } + + #[test] + fn no_tweaks_returns_identity_state() { + let pt = lift_x( + &hex::decode("1ae63bc9ddaffe52d44c3018e83115bfb22195bd8112fcad112310714e6fd5ec") + .unwrap(), + ) + .unwrap(); + let state = get_point_state(pt, &[]).unwrap(); + // No tweaks: point unchanged, parity = +1 (even Y), state = +1, tweak = 0 + use k256::Scalar; + assert_eq!(state.parity, Scalar::ONE); + assert_eq!(state.state, Scalar::ONE); + assert_eq!(state.tweak, Scalar::ZERO); + assert_eq!( + hex::encode(serialize_element(&state.point)), + "021ae63bc9ddaffe52d44c3018e83115bfb22195bd8112fcad112310714e6fd5ec" + ); + } + + #[test] + fn tweaked_group_pk_matches_fixture() { + // From the integration fixture: group_pk tweaked with aa...aa and bb...bb + let group_pk = + hex::decode("021ae63bc9ddaffe52d44c3018e83115bfb22195bd8112fcad112310714e6fd5ec") + .unwrap(); + let pt = lift_x(&group_pk).unwrap(); + let tweaks = [ + h32("aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa"), + h32("bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb"), + ]; + let state = get_point_state(pt, &tweaks).unwrap(); + assert_eq!( + hex::encode(serialize_element(&state.point)), + "025731d4d57552d12877e3db13061d7f6ca09198963e003d1d6e0960d6651e42d3" + ); + } +} diff --git a/frost-taproot/src/ecdh_tests.rs b/frost-taproot/src/ecdh_tests.rs new file mode 100644 index 0000000..b872538 --- /dev/null +++ b/frost-taproot/src/ecdh_tests.rs @@ -0,0 +1,108 @@ +#[cfg(test)] +mod ecdh_tests { + use crate::ecdh::*; + use crate::group::create_dealer_set; + use crate::helpers::{generate_seckey, get_pubkey, tweak_pubkey}; + + fn s32(hex: &str) -> [u8; 32] { + hex::decode(hex).unwrap().try_into().unwrap() + } + + fn p33(hex: &str) -> [u8; 33] { + hex::decode(hex).unwrap().try_into().unwrap() + } + + const S0: &str = "0070ca75929ca1ec4cd70ac34f46079bdfdd87f9d0c0bf4275f3882f7b462d0f"; + const S1: &str = "0e0376a7180253cdb8b6020bff0eda529760da65e715663e2152e4be2fc7b443"; + + // ── create_ecdh_share ──────────────────────────────────────────────────── + + #[test] + fn create_ecdh_share_matches_fixture() { + let secrets = [s32(S0), s32(S1)]; + let group = create_dealer_set(2, 3, &secrets).unwrap(); + + // demo_seckey = generate_seckey(Some(aa...aa)) + let demo_aux = s32("aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa"); + let demo_seckey = generate_seckey(Some(&demo_aux)); + let demo_pubkey = get_pubkey(&demo_seckey); + + let members = [1u32, 3u32]; + let ecdh1 = create_ecdh_share(&members, &group.shares[0], &demo_pubkey).unwrap(); + let ecdh3 = create_ecdh_share(&members, &group.shares[2], &demo_pubkey).unwrap(); + + assert_eq!(ecdh1.idx, 1); + assert_eq!(ecdh3.idx, 3); + assert_eq!( + hex::encode(ecdh1.pubkey), + "0386c5b0f4bace78ef17d02b09e339b5a39f659dbbf1f3f531b9825df6836cfea9" + ); + assert_eq!( + hex::encode(ecdh3.pubkey), + "023edaf055945d35006e1c52dd7a388e0c10b36eb55aa9d117853af87903cb54c0" + ); + } + + // ── derive_ecdh_secret ─────────────────────────────────────────────────── + + #[test] + fn derive_ecdh_secret_matches_master_secret() { + // The FROST ECDH shared secret must equal secret_key * demo_pubkey + // = demo_seckey * group_pubkey (commutativity of scalar mult). + let secrets = [s32(S0), s32(S1)]; + let group = create_dealer_set(2, 3, &secrets).unwrap(); + + let demo_aux = s32("aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa"); + let demo_seckey = generate_seckey(Some(&demo_aux)); + let demo_pubkey = get_pubkey(&demo_seckey); + + let members = [1u32, 3u32]; + let ecdh1 = create_ecdh_share(&members, &group.shares[0], &demo_pubkey).unwrap(); + let ecdh3 = create_ecdh_share(&members, &group.shares[2], &demo_pubkey).unwrap(); + + let frost_secret = derive_ecdh_secret(&[ecdh1, ecdh3]).unwrap(); + + // master_shared_secret = group_pk * demo_seckey = tweak_pubkey(demo_pubkey, s0) + let master_secret = tweak_pubkey(&demo_pubkey, &s32(S0)).unwrap(); + + assert_eq!( + hex::encode(frost_secret), + hex::encode(master_secret), + "FROST ECDH secret must match master shared secret" + ); + } + + #[test] + fn derive_ecdh_secret_matches_fixture() { + let frost_secret = derive_ecdh_secret(&[ + crate::types::PublicShare { + idx: 1, + pubkey: p33("0386c5b0f4bace78ef17d02b09e339b5a39f659dbbf1f3f531b9825df6836cfea9"), + }, + crate::types::PublicShare { + idx: 3, + pubkey: p33("023edaf055945d35006e1c52dd7a388e0c10b36eb55aa9d117853af87903cb54c0"), + }, + ]) + .unwrap(); + assert_eq!( + hex::encode(frost_secret), + "020b6417cef5530ed4b82681945d4565ea7027f423a97b60247d07386ca3619585" + ); + } + + #[test] + fn derive_ecdh_secret_empty_errors() { + assert!(derive_ecdh_secret(&[]).is_err()); + } + + #[test] + fn create_ecdh_share_invalid_pubkey_errors() { + let share = crate::types::SecretShare { + idx: 1, + seckey: s32("0e74411caa9ef5ba058d0ccf4e54e1ee773e625fb7d6258097466cedab0de152"), + }; + // All-zero pubkey is invalid + assert!(create_ecdh_share(&[1u32, 2u32], &share, &[0u8; 33]).is_err()); + } +} diff --git a/frost-taproot/src/group_tests.rs b/frost-taproot/src/group_tests.rs new file mode 100644 index 0000000..a804225 --- /dev/null +++ b/frost-taproot/src/group_tests.rs @@ -0,0 +1,94 @@ +#[cfg(test)] +mod group_tests { + use crate::group::*; + use crate::shares::verify_share; + + fn s32(hex: &str) -> [u8; 32] { + hex::decode(hex).unwrap().try_into().unwrap() + } + + const S0: &str = "0070ca75929ca1ec4cd70ac34f46079bdfdd87f9d0c0bf4275f3882f7b462d0f"; + const S1: &str = "0e0376a7180253cdb8b6020bff0eda529760da65e715663e2152e4be2fc7b443"; + + // ── create_share_set ───────────────────────────────────────────────────── + + #[test] + fn create_share_set_correct_counts() { + let secrets = [s32(S0), s32(S1)]; + let set = create_share_set(2, 3, &secrets).unwrap(); + assert_eq!(set.shares.len(), 3); + assert_eq!(set.vss_commits.len(), 2); + } + + #[test] + fn create_share_set_shares_are_valid() { + let secrets = [s32(S0), s32(S1)]; + let set = create_share_set(2, 3, &secrets).unwrap(); + for share in &set.shares { + assert!( + verify_share(&set.vss_commits, share, 2).unwrap(), + "share {} should verify", + share.idx + ); + } + } + + #[test] + fn create_share_set_vss_commits_match_fixture() { + let secrets = [s32(S0), s32(S1)]; + let set = create_share_set(2, 3, &secrets).unwrap(); + assert_eq!( + hex::encode(set.vss_commits[0]), + "021ae63bc9ddaffe52d44c3018e83115bfb22195bd8112fcad112310714e6fd5ec" + ); + assert_eq!( + hex::encode(set.vss_commits[1]), + "024f75a5478deda1102eba931e19425e59c1750533a54218ce215ced343fbfb6cf" + ); + } + + // ── create_dealer_set ──────────────────────────────────────────────────── + + #[test] + fn create_dealer_set_group_pk_is_first_commit() { + let secrets = [s32(S0), s32(S1)]; + let set = create_dealer_set(2, 3, &secrets).unwrap(); + assert_eq!(set.group_pk, set.vss_commits[0]); + } + + #[test] + fn create_dealer_set_group_pk_matches_fixture() { + let secrets = [s32(S0), s32(S1)]; + let set = create_dealer_set(2, 3, &secrets).unwrap(); + assert_eq!( + hex::encode(set.group_pk), + "021ae63bc9ddaffe52d44c3018e83115bfb22195bd8112fcad112310714e6fd5ec" + ); + } + + #[test] + fn create_dealer_set_shares_match_fixture() { + let secrets = [s32(S0), s32(S1)]; + let set = create_dealer_set(2, 3, &secrets).unwrap(); + assert_eq!( + hex::encode(set.shares[0].seckey), + "0e74411caa9ef5ba058d0ccf4e54e1ee773e625fb7d6258097466cedab0de152" + ); + assert_eq!( + hex::encode(set.shares[1].seckey), + "1c77b7c3c2a14987be430edb4d63bc410e9f3cc59eeb8bbeb89951abdad59595" + ); + assert_eq!( + hex::encode(set.shares[2].seckey), + "2a7b2e6adaa39d5576f910e74c729693a600172b8600f1fcd9ec366a0a9d49d8" + ); + } + + #[test] + fn create_dealer_set_no_secrets_is_random() { + let a = create_dealer_set(2, 3, &[]).unwrap(); + let b = create_dealer_set(2, 3, &[]).unwrap(); + // Random secrets → different group keys + assert_ne!(a.group_pk, b.group_pk); + } +} diff --git a/frost-taproot/src/helpers_tests.rs b/frost-taproot/src/helpers_tests.rs new file mode 100644 index 0000000..a1cbae4 --- /dev/null +++ b/frost-taproot/src/helpers_tests.rs @@ -0,0 +1,194 @@ +#[cfg(test)] +mod helpers_tests { + use crate::helpers::*; + + fn s32(hex: &str) -> [u8; 32] { + hex::decode(hex).unwrap().try_into().unwrap() + } + + // ── generate_seckey ────────────────────────────────────────────────────── + + #[test] + fn generate_seckey_deterministic_with_aux() { + let aux = s32("0070ca75929ca1ec4cd70ac34f46079bdfdd87f9d0c0bf4275f3882f7b462d0f"); + let a = generate_seckey(Some(&aux)); + let b = generate_seckey(Some(&aux)); + assert_eq!(a, b); + } + + #[test] + fn generate_seckey_random_without_aux() { + // Two calls without aux should (overwhelmingly) differ + let a = generate_seckey(None); + let b = generate_seckey(None); + assert_ne!(a, b); + } + + #[test] + fn generate_seckey_is_h3_of_aux() { + let aux = s32("0070ca75929ca1ec4cd70ac34f46079bdfdd87f9d0c0bf4275f3882f7b462d0f"); + let result = generate_seckey(Some(&aux)); + let expected = crate::ecc::hash::h3(&aux); + assert_eq!(result, expected); + } + + // ── generate_nonce ─────────────────────────────────────────────────────── + + #[test] + fn generate_nonce_deterministic_with_seed() { + let secret = s32("0e74411caa9ef5ba058d0ccf4e54e1ee773e625fb7d6258097466cedab0de152"); + let seed = s32("0070ca75929ca1ec4cd70ac34f46079bdfdd87f9d0c0bf4275f3882f7b462d0f"); + let a = generate_nonce(&secret, Some(&seed)); + let b = generate_nonce(&secret, Some(&seed)); + assert_eq!(a, b); + } + + #[test] + fn generate_nonce_matches_fixture() { + // hidden_sn for share[1]: generate_nonce(share1.seckey, hidden_seed) + let secret = s32("0e74411caa9ef5ba058d0ccf4e54e1ee773e625fb7d6258097466cedab0de152"); + let seed = s32("0070ca75929ca1ec4cd70ac34f46079bdfdd87f9d0c0bf4275f3882f7b462d0f"); + let nonce = generate_nonce(&secret, Some(&seed)); + assert_eq!( + hex::encode(nonce), + "189aeb1bf3a453673cb144a459f0b644183ff02808cad807b672067da4f33357" + ); + } + + #[test] + fn generate_nonce_random_without_seed() { + let secret = s32("0e74411caa9ef5ba058d0ccf4e54e1ee773e625fb7d6258097466cedab0de152"); + let a = generate_nonce(&secret, None); + let b = generate_nonce(&secret, None); + assert_ne!(a, b); + } + + // ── get_pubkey ─────────────────────────────────────────────────────────── + + #[test] + fn get_pubkey_matches_fixture() { + let seckey = s32("0e74411caa9ef5ba058d0ccf4e54e1ee773e625fb7d6258097466cedab0de152"); + let pubkey = get_pubkey(&seckey); + assert_eq!( + hex::encode(pubkey), + "0278f55809a11a1016d13ec4f54674810abe4a6fec8b586e14f90d0c1f80de33eb" + ); + } + + #[test] + fn get_pubkey_is_compressed_33_bytes() { + let seckey = s32("0e74411caa9ef5ba058d0ccf4e54e1ee773e625fb7d6258097466cedab0de152"); + let pubkey = get_pubkey(&seckey); + assert!(pubkey[0] == 0x02 || pubkey[0] == 0x03); + } + + // ── tweak_seckey ───────────────────────────────────────────────────────── + + #[test] + fn tweak_seckey_by_one_is_identity() { + let seckey = s32("0e74411caa9ef5ba058d0ccf4e54e1ee773e625fb7d6258097466cedab0de152"); + let one = s32("0000000000000000000000000000000000000000000000000000000000000001"); + assert_eq!(tweak_seckey(&seckey, &one), seckey); + } + + #[test] + fn tweak_seckey_doubles_with_two() { + use crate::ecc::util::{scalar_from_bytes, scalar_to_bytes}; + let seckey = s32("0e74411caa9ef5ba058d0ccf4e54e1ee773e625fb7d6258097466cedab0de152"); + let two = s32("0000000000000000000000000000000000000000000000000000000000000002"); + let tweaked = tweak_seckey(&seckey, &two); + let expected = scalar_to_bytes(&(scalar_from_bytes(&seckey) + scalar_from_bytes(&seckey))); + assert_eq!(tweaked, expected); + } + + // ── tweak_pubkey ───────────────────────────────────────────────────────── + + #[test] + fn tweak_pubkey_by_one_is_identity() { + let pubkey = + hex::decode("021ae63bc9ddaffe52d44c3018e83115bfb22195bd8112fcad112310714e6fd5ec") + .unwrap(); + let one = s32("0000000000000000000000000000000000000000000000000000000000000001"); + let tweaked = tweak_pubkey(&pubkey, &one).unwrap(); + assert_eq!(hex::encode(tweaked), hex::encode(&pubkey)); + } + + #[test] + fn tweak_pubkey_invalid_input_errors() { + let one = s32("0000000000000000000000000000000000000000000000000000000000000001"); + assert!(tweak_pubkey(&[0u8; 31], &one).is_err()); + } + + // ── get_challenge ──────────────────────────────────────────────────────── + + #[test] + fn get_challenge_matches_fixture() { + use crate::ecc::util::scalar_to_bytes; + // From the fixture signing context + let group_pn = + hex::decode("03e76328e49c27c12392a117d39ef9f5def368590d5e72438907fb63c1006fd589") + .unwrap(); + let group_pk = + hex::decode("025731d4d57552d12877e3db13061d7f6ca09198963e003d1d6e0960d6651e42d3") + .unwrap(); + let message = hex::decode("68656c6c6f20776f726c6421").unwrap(); + let challenge = get_challenge(&group_pn, &group_pk, &message).unwrap(); + assert_eq!( + hex::encode(scalar_to_bytes(&challenge)), + "99e6637f68e223b0f6b4caa36b48cc277bf036ece4f14bab657200b43ecb0d55" + ); + } + + #[test] + fn get_challenge_invalid_pubkey_errors() { + assert!(get_challenge(&[0u8; 31], &[0u8; 33], &[]).is_err()); + assert!(get_challenge(&[0u8; 33], &[0u8; 31], &[]).is_err()); + } + + // ── convert_pubkey_to_bip340 / convert_pubkey_to_ecdsa ────────────────── + + #[test] + fn convert_to_bip340_strips_prefix() { + let compressed = + hex::decode("021ae63bc9ddaffe52d44c3018e83115bfb22195bd8112fcad112310714e6fd5ec") + .unwrap(); + let bip340 = convert_pubkey_to_bip340(&compressed).unwrap(); + assert_eq!(bip340.len(), 32); + assert_eq!(bip340, compressed[1..]); + } + + #[test] + fn convert_to_bip340_passthrough_32() { + let x_only = + hex::decode("1ae63bc9ddaffe52d44c3018e83115bfb22195bd8112fcad112310714e6fd5ec") + .unwrap(); + let bip340 = convert_pubkey_to_bip340(&x_only).unwrap(); + assert_eq!(bip340, x_only); + } + + #[test] + fn convert_to_bip340_invalid_length_errors() { + assert!(convert_pubkey_to_bip340(&[0u8; 31]).is_err()); + assert!(convert_pubkey_to_bip340(&[0u8; 34]).is_err()); + } + + #[test] + fn convert_to_ecdsa_prepends_prefix() { + let x_only = + hex::decode("1ae63bc9ddaffe52d44c3018e83115bfb22195bd8112fcad112310714e6fd5ec") + .unwrap(); + let ecdsa = convert_pubkey_to_ecdsa(&x_only).unwrap(); + assert_eq!(ecdsa.len(), 33); + assert_eq!(ecdsa[0], 0x02); + assert_eq!(&ecdsa[1..], x_only.as_slice()); + } + + #[test] + fn convert_to_ecdsa_passthrough_33() { + let compressed = + hex::decode("021ae63bc9ddaffe52d44c3018e83115bfb22195bd8112fcad112310714e6fd5ec") + .unwrap(); + let ecdsa = convert_pubkey_to_ecdsa(&compressed).unwrap(); + assert_eq!(ecdsa, compressed); + } +} diff --git a/frost-taproot/src/lib.rs b/frost-taproot/src/lib.rs index 4c9820e..76e831e 100644 --- a/frost-taproot/src/lib.rs +++ b/frost-taproot/src/lib.rs @@ -14,6 +14,23 @@ pub mod shares; pub mod sign; pub mod vss; +#[cfg(test)] +mod ecdh_tests; +#[cfg(test)] +mod group_tests; +#[cfg(test)] +mod helpers_tests; +#[cfg(test)] +mod poly_tests; +#[cfg(test)] +mod recover_tests; +#[cfg(test)] +mod refresh_tests; +#[cfg(test)] +mod shares_tests; +#[cfg(test)] +mod vss_tests; + use thiserror::Error; #[derive(Debug, Error)] diff --git a/frost-taproot/src/poly_tests.rs b/frost-taproot/src/poly_tests.rs new file mode 100644 index 0000000..f4993c9 --- /dev/null +++ b/frost-taproot/src/poly_tests.rs @@ -0,0 +1,159 @@ +#[cfg(test)] +mod poly_tests { + use crate::ecc::util::scalar_from_bytes; + use crate::poly::*; + use k256::Scalar; + + fn sc(n: u8) -> Scalar { + scalar_from_bytes(&{ + let mut b = [0u8; 32]; + b[31] = n; + b + }) + } + + fn sc32(hex: &str) -> Scalar { + scalar_from_bytes(&hex::decode(hex).unwrap().try_into().unwrap()) + } + + // ── evaluate_x ─────────────────────────────────────────────────────────── + + #[test] + fn evaluate_x_constant_polynomial() { + // f(x) = 7 → f(3) = 7 + let coeffs = [sc(7)]; + assert_eq!(evaluate_x(&coeffs, sc(3)).unwrap(), sc(7)); + } + + #[test] + fn evaluate_x_linear() { + // f(x) = 3 + 2x → f(1) = 5, f(2) = 7 + let coeffs = [sc(3), sc(2)]; + assert_eq!(evaluate_x(&coeffs, sc(1)).unwrap(), sc(5)); + assert_eq!(evaluate_x(&coeffs, sc(2)).unwrap(), sc(7)); + } + + #[test] + fn evaluate_x_quadratic() { + // f(x) = 3 + 2x + x^2 → f(1) = 6, f(2) = 11 + let coeffs = [sc(3), sc(2), sc(1)]; + assert_eq!(evaluate_x(&coeffs, sc(1)).unwrap(), sc(6)); + assert_eq!(evaluate_x(&coeffs, sc(2)).unwrap(), sc(11)); + } + + #[test] + fn evaluate_x_at_zero_errors() { + let coeffs = [sc(1), sc(2)]; + assert!(evaluate_x(&coeffs, Scalar::ZERO).is_err()); + } + + #[test] + fn evaluate_x_matches_fixture_shares() { + // From the fixture: coeffs = [s0, s1], share[1].seckey and share[2].seckey + let s0 = sc32("0070ca75929ca1ec4cd70ac34f46079bdfdd87f9d0c0bf4275f3882f7b462d0f"); + let s1 = sc32("0e0376a7180253cdb8b6020bff0eda529760da65e715663e2152e4be2fc7b443"); + let coeffs = [s0, s1]; + + let share1 = evaluate_x(&coeffs, index_to_scalar(1)).unwrap(); + let share2 = evaluate_x(&coeffs, index_to_scalar(2)).unwrap(); + + assert_eq!( + hex::encode(share1.to_bytes()), + "0e74411caa9ef5ba058d0ccf4e54e1ee773e625fb7d6258097466cedab0de152" + ); + assert_eq!( + hex::encode(share2.to_bytes()), + "1c77b7c3c2a14987be430edb4d63bc410e9f3cc59eeb8bbeb89951abdad59595" + ); + } + + // ── interpolate_root ───────────────────────────────────────────────────── + + #[test] + fn interpolate_root_recovers_secret() { + // secret=7, f(x) = 7 + 3x → f(1)=10, f(2)=13 + let points = [(sc(1), sc(10)), (sc(2), sc(13))]; + let secret = interpolate_root(&points).unwrap(); + assert_eq!(secret, sc(7)); + } + + #[test] + fn interpolate_root_single_point() { + // f(x) = 5 (constant) → f(1) = 5 + let points = [(sc(1), sc(5))]; + let secret = interpolate_root(&points).unwrap(); + assert_eq!(secret, sc(5)); + } + + #[test] + fn interpolate_root_recovers_fixture_secret() { + // From the fixture: shares[0] and shares[1] should recover s0 + let s0 = sc32("0070ca75929ca1ec4cd70ac34f46079bdfdd87f9d0c0bf4275f3882f7b462d0f"); + let share1 = sc32("0e74411caa9ef5ba058d0ccf4e54e1ee773e625fb7d6258097466cedab0de152"); + let share2 = sc32("1c77b7c3c2a14987be430edb4d63bc410e9f3cc59eeb8bbeb89951abdad59595"); + let points = [(index_to_scalar(1), share1), (index_to_scalar(2), share2)]; + assert_eq!(interpolate_root(&points).unwrap(), s0); + } + + // ── interpolate_x ──────────────────────────────────────────────────────── + + #[test] + fn interpolate_x_not_in_set_errors() { + let l = [sc(1), sc(2)]; + assert!(interpolate_x(&l, sc(3)).is_err()); + } + + #[test] + fn interpolate_x_duplicate_errors() { + let l = [sc(1), sc(1)]; + assert!(interpolate_x(&l, sc(1)).is_err()); + } + + #[test] + fn interpolate_x_single_element() { + // L = {1}, x = 1: numerator = 1 (empty product), denominator = 1 → result = 1 + let l = [sc(1)]; + assert_eq!(interpolate_x(&l, sc(1)).unwrap(), Scalar::ONE); + } + + // ── calc_lagrange_coeff ────────────────────────────────────────────────── + + #[test] + fn calc_lagrange_coeff_duplicate_errors() { + let l = [sc(1), sc(1)]; + assert!(calc_lagrange_coeff(&l, sc(1), Scalar::ZERO).is_err()); + } + + #[test] + fn calc_lagrange_coeff_two_party_at_zero() { + // L = {1, 2}, P = 1, x = 0 + // numerator = (0 - 2) = -2 + // denominator = (1 - 2) = -1 + // result = (-2)/(-1) = 2 + let l = [sc(1), sc(2)]; + let coeff = calc_lagrange_coeff(&l, sc(1), Scalar::ZERO).unwrap(); + assert_eq!(coeff, sc(2)); + } + + // ── index_to_scalar ────────────────────────────────────────────────────── + + #[test] + fn index_to_scalar_one() { + assert_eq!(index_to_scalar(1), Scalar::ONE); + } + + #[test] + fn index_to_scalar_zero() { + assert_eq!(index_to_scalar(0), Scalar::ZERO); + } + + #[test] + fn index_to_scalar_large() { + let expected = scalar_from_bytes(&{ + let mut b = [0u8; 32]; + b[28..].copy_from_slice(&255u32.to_be_bytes()); + b + }); + assert_eq!(index_to_scalar(255), expected); + } +} diff --git a/frost-taproot/src/recover_tests.rs b/frost-taproot/src/recover_tests.rs new file mode 100644 index 0000000..90e22d7 --- /dev/null +++ b/frost-taproot/src/recover_tests.rs @@ -0,0 +1,106 @@ +#[cfg(test)] +mod recover_tests { + use crate::group::create_dealer_set; + use crate::recover::*; + use crate::types::SecretShare; + + fn s32(hex: &str) -> [u8; 32] { + hex::decode(hex).unwrap().try_into().unwrap() + } + + const S0: &str = "0070ca75929ca1ec4cd70ac34f46079bdfdd87f9d0c0bf4275f3882f7b462d0f"; + const S1: &str = "0e0376a7180253cdb8b6020bff0eda529760da65e715663e2152e4be2fc7b443"; + + // ── gen_recovery_shares ────────────────────────────────────────────────── + + #[test] + fn gen_recovery_shares_correct_structure() { + let secrets = [s32(S0), s32(S1)]; + let group = create_dealer_set(2, 3, &secrets).unwrap(); + let members = [2u32, 3u32]; + let pkg = gen_recovery_shares(&members, &group.shares[1], 1, 2, &[]).unwrap(); + assert_eq!(pkg.idx, 2); // originating share's idx + assert_eq!(pkg.shares.len(), 2); // one per member + // vss_commits = rand_coeffs (threshold-1) + repair_coeff = threshold total + assert_eq!(pkg.vss_commits.len(), 2); + } + + #[test] + fn gen_recovery_shares_not_enough_members_errors() { + let secrets = [s32(S0), s32(S1)]; + let group = create_dealer_set(2, 3, &secrets).unwrap(); + // Only 1 member but threshold=2 + assert!(gen_recovery_shares(&[2u32], &group.shares[1], 1, 2, &[]).is_err()); + } + + // ── recover_share ──────────────────────────────────────────────────────── + + #[test] + fn recover_share_reconstructs_lost_share() { + // Members 2 and 3 help recover share for target=1. + // Protocol: each helper generates recovery shares, then target sums + // the aggregated shares it receives. + let secrets = [s32(S0), s32(S1)]; + let group = create_dealer_set(2, 3, &secrets).unwrap(); + let members = [2u32, 3u32]; + + let pkg2 = gen_recovery_shares(&members, &group.shares[1], 1, 2, &[s32(S0)]).unwrap(); + let pkg3 = gen_recovery_shares(&members, &group.shares[2], 1, 2, &[s32(S1)]).unwrap(); + + // Each member aggregates the shares they received from others at their own index. + // Member 2 receives: pkg2.shares[idx=2] + pkg3.shares[idx=2] + // Member 3 receives: pkg2.shares[idx=3] + pkg3.shares[idx=3] + use crate::ecc::util::{scalar_from_bytes, scalar_to_bytes}; + + let agg2 = { + let a = scalar_from_bytes(&pkg2.shares.iter().find(|s| s.idx == 2).unwrap().seckey); + let b = scalar_from_bytes(&pkg3.shares.iter().find(|s| s.idx == 2).unwrap().seckey); + SecretShare { + idx: 2, + seckey: scalar_to_bytes(&(a + b)), + } + }; + let agg3 = { + let a = scalar_from_bytes(&pkg2.shares.iter().find(|s| s.idx == 3).unwrap().seckey); + let b = scalar_from_bytes(&pkg3.shares.iter().find(|s| s.idx == 3).unwrap().seckey); + SecretShare { + idx: 3, + seckey: scalar_to_bytes(&(a + b)), + } + }; + + let repaired = recover_share(&[agg2, agg3], 1); + assert_eq!(repaired.idx, 1); + assert_eq!( + hex::encode(repaired.seckey), + hex::encode(group.shares[0].seckey), + "recovered share must match original" + ); + } + + #[test] + fn recover_share_sums_scalars() { + // recover_share simply sums the provided shares. + let a = SecretShare { + idx: 1, + seckey: s32("0000000000000000000000000000000000000000000000000000000000000003"), + }; + let b = SecretShare { + idx: 1, + seckey: s32("0000000000000000000000000000000000000000000000000000000000000004"), + }; + let result = recover_share(&[a, b], 5); + assert_eq!(result.idx, 5); + assert_eq!(result.seckey[31], 7); + } + + #[test] + fn recover_share_single_input() { + let a = SecretShare { + idx: 2, + seckey: s32("0e74411caa9ef5ba058d0ccf4e54e1ee773e625fb7d6258097466cedab0de152"), + }; + let result = recover_share(&[a.clone()], 2); + assert_eq!(result.seckey, a.seckey); + } +} diff --git a/frost-taproot/src/refresh_tests.rs b/frost-taproot/src/refresh_tests.rs new file mode 100644 index 0000000..2edbd2f --- /dev/null +++ b/frost-taproot/src/refresh_tests.rs @@ -0,0 +1,120 @@ +#[cfg(test)] +mod refresh_tests { + use crate::group::create_dealer_set; + use crate::refresh::*; + use crate::shares::derive_shares_secret; + use crate::types::SecretShare; + + fn s32(hex: &str) -> [u8; 32] { + hex::decode(hex).unwrap().try_into().unwrap() + } + + const S0: &str = "0070ca75929ca1ec4cd70ac34f46079bdfdd87f9d0c0bf4275f3882f7b462d0f"; + const S1: &str = "0e0376a7180253cdb8b6020bff0eda529760da65e715663e2152e4be2fc7b443"; + const R0: &str = "1111111111111111111111111111111111111111111111111111111111111111"; + const R1: &str = "2222222222222222222222222222222222222222222222222222222222222222"; + const R2: &str = "3333333333333333333333333333333333333333333333333333333333333333"; + + // ── gen_refresh_shares ─────────────────────────────────────────────────── + + #[test] + fn gen_refresh_shares_correct_counts() { + let pkg = gen_refresh_shares(1, 2, 3, &[s32(R0)]).unwrap(); + assert_eq!(pkg.shares.len(), 3); + assert_eq!(pkg.vss_commits.len(), 1); // threshold - 1 = 1 + assert_eq!(pkg.idx, 1); + } + + #[test] + fn gen_refresh_shares_polynomial_has_zero_constant_term() { + // The refresh polynomial f has f(0) = 0. + // Verify: sum of all refresh shares from one participant, interpolated at 0, equals 0. + use crate::ecc::util::scalar_from_bytes; + use crate::poly::index_to_scalar; + use crate::poly::interpolate_root; + + let pkg = gen_refresh_shares(1, 2, 3, &[s32(R0)]).unwrap(); + let points: Vec<_> = pkg + .shares + .iter() + .map(|s| (index_to_scalar(s.idx), scalar_from_bytes(&s.seckey))) + .collect(); + let root = interpolate_root(&points).unwrap(); + assert_eq!( + root, + k256::Scalar::ZERO, + "refresh polynomial must have f(0) = 0" + ); + } + + #[test] + fn gen_refresh_shares_deterministic_with_secrets() { + let a = gen_refresh_shares(1, 2, 3, &[s32(R0)]).unwrap(); + let b = gen_refresh_shares(1, 2, 3, &[s32(R0)]).unwrap(); + for (sa, sb) in a.shares.iter().zip(b.shares.iter()) { + assert_eq!(sa.seckey, sb.seckey); + } + } + + // ── refresh_share ──────────────────────────────────────────────────────── + + #[test] + fn refresh_share_preserves_secret() { + // Full 3-participant refresh: secret must be unchanged after refresh. + let secrets = [s32(S0), s32(S1)]; + let group = create_dealer_set(2, 3, &secrets).unwrap(); + + let rp1 = gen_refresh_shares(1, 2, 3, &[s32(R0)]).unwrap(); + let rp2 = gen_refresh_shares(2, 2, 3, &[s32(R1)]).unwrap(); + let rp3 = gen_refresh_shares(3, 2, 3, &[s32(R2)]).unwrap(); + + // Each participant collects all refresh shares addressed to them (including own). + let agg1: Vec = [&rp1, &rp2, &rp3] + .iter() + .map(|pkg| pkg.shares.iter().find(|s| s.idx == 1).unwrap().clone()) + .collect(); + let agg2: Vec = [&rp1, &rp2, &rp3] + .iter() + .map(|pkg| pkg.shares.iter().find(|s| s.idx == 2).unwrap().clone()) + .collect(); + + let new1 = refresh_share(&agg1, &group.shares[0]).unwrap(); + let new2 = refresh_share(&agg2, &group.shares[1]).unwrap(); + + let recovered = derive_shares_secret(&[new1, new2]).unwrap(); + assert_eq!(recovered, s32(S0), "secret must be unchanged after refresh"); + } + + #[test] + fn refresh_share_changes_share_values() { + let secrets = [s32(S0), s32(S1)]; + let group = create_dealer_set(2, 3, &secrets).unwrap(); + let rp1 = gen_refresh_shares(1, 2, 3, &[s32(R0)]).unwrap(); + let rp2 = gen_refresh_shares(2, 2, 3, &[s32(R1)]).unwrap(); + let rp3 = gen_refresh_shares(3, 2, 3, &[s32(R2)]).unwrap(); + + let agg1: Vec = [&rp1, &rp2, &rp3] + .iter() + .map(|pkg| pkg.shares.iter().find(|s| s.idx == 1).unwrap().clone()) + .collect(); + + let new1 = refresh_share(&agg1, &group.shares[0]).unwrap(); + // The refreshed share should differ from the original + assert_ne!(new1.seckey, group.shares[0].seckey); + assert_eq!(new1.idx, group.shares[0].idx); + } + + #[test] + fn refresh_share_mismatched_idx_errors() { + let current = SecretShare { + idx: 1, + seckey: s32("0e74411caa9ef5ba058d0ccf4e54e1ee773e625fb7d6258097466cedab0de152"), + }; + let wrong_idx = SecretShare { + idx: 2, // different idx + seckey: s32("0000000000000000000000000000000000000000000000000000000000000001"), + }; + // combine_set will fail because indices differ + assert!(refresh_share(&[wrong_idx], ¤t).is_err()); + } +} diff --git a/frost-taproot/src/shares_tests.rs b/frost-taproot/src/shares_tests.rs new file mode 100644 index 0000000..f399a89 --- /dev/null +++ b/frost-taproot/src/shares_tests.rs @@ -0,0 +1,207 @@ +#[cfg(test)] +mod shares_tests { + use crate::ecc::util::scalar_from_bytes; + use crate::shares::*; + use crate::types::SecretShare; + use crate::vss::create_share_coeffs; + + fn s32(hex: &str) -> [u8; 32] { + hex::decode(hex).unwrap().try_into().unwrap() + } + + fn share(idx: u32, seckey_hex: &str) -> SecretShare { + SecretShare { + idx, + seckey: s32(seckey_hex), + } + } + + // ── create_shares ──────────────────────────────────────────────────────── + + #[test] + fn create_shares_count_and_indices() { + let s0 = s32("0070ca75929ca1ec4cd70ac34f46079bdfdd87f9d0c0bf4275f3882f7b462d0f"); + let s1 = s32("0e0376a7180253cdb8b6020bff0eda529760da65e715663e2152e4be2fc7b443"); + let coeffs = create_share_coeffs(&[s0, s1], 2); + let shares = create_shares(&coeffs, 3).unwrap(); + assert_eq!(shares.len(), 3); + assert_eq!(shares[0].idx, 1); + assert_eq!(shares[1].idx, 2); + assert_eq!(shares[2].idx, 3); + } + + #[test] + fn create_shares_matches_fixture() { + let s0 = s32("0070ca75929ca1ec4cd70ac34f46079bdfdd87f9d0c0bf4275f3882f7b462d0f"); + let s1 = s32("0e0376a7180253cdb8b6020bff0eda529760da65e715663e2152e4be2fc7b443"); + let coeffs = create_share_coeffs(&[s0, s1], 2); + let shares = create_shares(&coeffs, 3).unwrap(); + assert_eq!( + hex::encode(shares[0].seckey), + "0e74411caa9ef5ba058d0ccf4e54e1ee773e625fb7d6258097466cedab0de152" + ); + assert_eq!( + hex::encode(shares[1].seckey), + "1c77b7c3c2a14987be430edb4d63bc410e9f3cc59eeb8bbeb89951abdad59595" + ); + assert_eq!( + hex::encode(shares[2].seckey), + "2a7b2e6adaa39d5576f910e74c729693a600172b8600f1fcd9ec366a0a9d49d8" + ); + } + + // ── combine_shares ─────────────────────────────────────────────────────── + + #[test] + fn combine_shares_sums_scalars() { + let a = share( + 1, + "0000000000000000000000000000000000000000000000000000000000000003", + ); + let b = share( + 2, + "0000000000000000000000000000000000000000000000000000000000000004", + ); + let result = combine_shares(&[a, b]); + let expected = scalar_from_bytes(&{ + let mut b = [0u8; 32]; + b[31] = 7; + b + }); + assert_eq!(scalar_from_bytes(&result), expected); + } + + #[test] + fn combine_shares_single() { + let a = share( + 1, + "0e74411caa9ef5ba058d0ccf4e54e1ee773e625fb7d6258097466cedab0de152", + ); + let result = combine_shares(&[a.clone()]); + assert_eq!(result, a.seckey); + } + + // ── combine_set ────────────────────────────────────────────────────────── + + #[test] + fn combine_set_same_idx() { + let a = share( + 1, + "0000000000000000000000000000000000000000000000000000000000000003", + ); + let b = share( + 1, + "0000000000000000000000000000000000000000000000000000000000000004", + ); + let result = combine_set(&[a, b]).unwrap(); + assert_eq!(result.idx, 1); + assert_eq!(result.seckey[31], 7); + } + + #[test] + fn combine_set_mismatched_idx_errors() { + let a = share( + 1, + "0000000000000000000000000000000000000000000000000000000000000003", + ); + let b = share( + 2, + "0000000000000000000000000000000000000000000000000000000000000004", + ); + assert!(combine_set(&[a, b]).is_err()); + } + + // ── merge_shares ───────────────────────────────────────────────────────── + + #[test] + fn merge_shares_combines_matching_indices() { + let a1 = share( + 1, + "0000000000000000000000000000000000000000000000000000000000000003", + ); + let a2 = share( + 2, + "0000000000000000000000000000000000000000000000000000000000000005", + ); + let b1 = share( + 1, + "0000000000000000000000000000000000000000000000000000000000000004", + ); + let b2 = share( + 2, + "0000000000000000000000000000000000000000000000000000000000000006", + ); + let merged = merge_shares(&[a1, a2], &[b1, b2]).unwrap(); + assert_eq!(merged[0].idx, 1); + assert_eq!(merged[0].seckey[31], 7); + assert_eq!(merged[1].idx, 2); + assert_eq!(merged[1].seckey[31], 11); + } + + #[test] + fn merge_shares_mismatched_lengths_errors() { + let a = vec![share( + 1, + "0000000000000000000000000000000000000000000000000000000000000001", + )]; + let b: Vec = vec![]; + assert!(merge_shares(&a, &b).is_err()); + } + + // ── verify_share ───────────────────────────────────────────────────────── + + #[test] + fn verify_share_valid_shares() { + let s0 = s32("0070ca75929ca1ec4cd70ac34f46079bdfdd87f9d0c0bf4275f3882f7b462d0f"); + let s1 = s32("0e0376a7180253cdb8b6020bff0eda529760da65e715663e2152e4be2fc7b443"); + let coeffs = create_share_coeffs(&[s0, s1], 2); + let shares = create_shares(&coeffs, 3).unwrap(); + let commits: Vec<[u8; 33]> = crate::vss::get_share_commits(&coeffs); + for s in &shares { + assert!( + verify_share(&commits, s, 2).unwrap(), + "share {} should be valid", + s.idx + ); + } + } + + #[test] + fn verify_share_tampered_share_fails() { + let s0 = s32("0070ca75929ca1ec4cd70ac34f46079bdfdd87f9d0c0bf4275f3882f7b462d0f"); + let s1 = s32("0e0376a7180253cdb8b6020bff0eda529760da65e715663e2152e4be2fc7b443"); + let coeffs = create_share_coeffs(&[s0, s1], 2); + let mut shares = create_shares(&coeffs, 3).unwrap(); + let commits = crate::vss::get_share_commits(&coeffs); + // Corrupt share[0] + shares[0].seckey[0] ^= 0xff; + assert!(!verify_share(&commits, &shares[0], 2).unwrap()); + } + + // ── derive_shares_secret ───────────────────────────────────────────────── + + #[test] + fn derive_shares_secret_recovers_root() { + let s0 = s32("0070ca75929ca1ec4cd70ac34f46079bdfdd87f9d0c0bf4275f3882f7b462d0f"); + let s1 = s32("0e0376a7180253cdb8b6020bff0eda529760da65e715663e2152e4be2fc7b443"); + let coeffs = create_share_coeffs(&[s0, s1], 2); + let shares = create_shares(&coeffs, 3).unwrap(); + // Any 2-of-3 subset should recover s0 + let secret_12 = derive_shares_secret(&shares[..2]).unwrap(); + let secret_13 = derive_shares_secret(&[shares[0].clone(), shares[2].clone()]).unwrap(); + let secret_23 = derive_shares_secret(&shares[1..]).unwrap(); + assert_eq!(secret_12, s0); + assert_eq!(secret_13, s0); + assert_eq!(secret_23, s0); + } + + #[test] + fn derive_shares_secret_all_three_also_recovers() { + let s0 = s32("0070ca75929ca1ec4cd70ac34f46079bdfdd87f9d0c0bf4275f3882f7b462d0f"); + let s1 = s32("0e0376a7180253cdb8b6020bff0eda529760da65e715663e2152e4be2fc7b443"); + let coeffs = create_share_coeffs(&[s0, s1], 2); + let shares = create_shares(&coeffs, 3).unwrap(); + let secret = derive_shares_secret(&shares).unwrap(); + assert_eq!(secret, s0); + } +} diff --git a/frost-taproot/src/vss_tests.rs b/frost-taproot/src/vss_tests.rs new file mode 100644 index 0000000..23a324b --- /dev/null +++ b/frost-taproot/src/vss_tests.rs @@ -0,0 +1,106 @@ +#[cfg(test)] +mod vss_tests { + use crate::ecc::group::{scalar_base_multi, serialize_element}; + use crate::ecc::util::scalar_from_bytes; + use crate::vss::*; + use k256::Scalar; + + fn s32(hex: &str) -> [u8; 32] { + hex::decode(hex).unwrap().try_into().unwrap() + } + + fn p33(hex: &str) -> [u8; 33] { + hex::decode(hex).unwrap().try_into().unwrap() + } + + // ── create_share_coeffs ────────────────────────────────────────────────── + + #[test] + fn create_share_coeffs_uses_provided_secrets() { + let s0 = s32("0070ca75929ca1ec4cd70ac34f46079bdfdd87f9d0c0bf4275f3882f7b462d0f"); + let s1 = s32("0e0376a7180253cdb8b6020bff0eda529760da65e715663e2152e4be2fc7b443"); + let coeffs = create_share_coeffs(&[s0, s1], 2); + assert_eq!(coeffs.len(), 2); + assert_eq!(coeffs[0], scalar_from_bytes(&s0)); + assert_eq!(coeffs[1], scalar_from_bytes(&s1)); + } + + #[test] + fn create_share_coeffs_fills_random_when_short() { + let s0 = s32("0070ca75929ca1ec4cd70ac34f46079bdfdd87f9d0c0bf4275f3882f7b462d0f"); + let coeffs = create_share_coeffs(&[s0], 3); + assert_eq!(coeffs.len(), 3); + // First coeff is deterministic + assert_eq!(coeffs[0], scalar_from_bytes(&s0)); + // Remaining two are random (non-zero with overwhelming probability) + // We can't assert their values, but we can check they're not zero + assert_ne!(coeffs[1], Scalar::ZERO); + assert_ne!(coeffs[2], Scalar::ZERO); + } + + #[test] + fn create_share_coeffs_empty_secrets_all_random() { + let coeffs = create_share_coeffs(&[], 3); + assert_eq!(coeffs.len(), 3); + } + + #[test] + fn create_share_coeffs_threshold_zero_is_empty() { + let coeffs = create_share_coeffs(&[], 0); + assert!(coeffs.is_empty()); + } + + // ── get_share_commits ──────────────────────────────────────────────────── + + #[test] + fn get_share_commits_matches_fixture() { + let s0 = s32("0070ca75929ca1ec4cd70ac34f46079bdfdd87f9d0c0bf4275f3882f7b462d0f"); + let s1 = s32("0e0376a7180253cdb8b6020bff0eda529760da65e715663e2152e4be2fc7b443"); + let coeffs = create_share_coeffs(&[s0, s1], 2); + let commits = get_share_commits(&coeffs); + assert_eq!(commits.len(), 2); + assert_eq!( + hex::encode(commits[0]), + "021ae63bc9ddaffe52d44c3018e83115bfb22195bd8112fcad112310714e6fd5ec" + ); + assert_eq!( + hex::encode(commits[1]), + "024f75a5478deda1102eba931e19425e59c1750533a54218ce215ced343fbfb6cf" + ); + } + + #[test] + fn get_share_commits_commit_is_scalar_times_generator() { + let s0 = s32("0070ca75929ca1ec4cd70ac34f46079bdfdd87f9d0c0bf4275f3882f7b462d0f"); + let coeffs = create_share_coeffs(&[s0], 1); + let commits = get_share_commits(&coeffs); + let expected = serialize_element(&scalar_base_multi(&coeffs[0])); + assert_eq!(commits[0], expected); + } + + // ── merge_share_commits ────────────────────────────────────────────────── + + #[test] + fn merge_share_commits_adds_points() { + let s0 = s32("0070ca75929ca1ec4cd70ac34f46079bdfdd87f9d0c0bf4275f3882f7b462d0f"); + let s1 = s32("0e0376a7180253cdb8b6020bff0eda529760da65e715663e2152e4be2fc7b443"); + let coeffs_a = create_share_coeffs(&[s0], 1); + let coeffs_b = create_share_coeffs(&[s1], 1); + let commits_a = get_share_commits(&coeffs_a); + let commits_b = get_share_commits(&coeffs_b); + let merged = merge_share_commits(&commits_a, &commits_b).unwrap(); + // merged[0] should be (s0 + s1) * G + let combined = scalar_from_bytes(&s0) + scalar_from_bytes(&s1); + let expected = serialize_element(&scalar_base_multi(&combined)); + assert_eq!(merged[0], expected); + } + + #[test] + fn merge_share_commits_mismatched_lengths_errors() { + let a = vec![p33( + "021ae63bc9ddaffe52d44c3018e83115bfb22195bd8112fcad112310714e6fd5ec", + )]; + let b: Vec<[u8; 33]> = vec![]; + assert!(merge_share_commits(&a, &b).is_err()); + } +}