Add unit tests

This commit is contained in:
Jon Staab
2026-02-19 18:05:07 -08:00
parent 1f9670e6df
commit 336a151198
11 changed files with 1571 additions and 0 deletions
+3
View File
@@ -2,3 +2,6 @@ pub mod group;
pub mod hash;
pub mod state;
pub mod util;
#[cfg(test)]
mod tests;
+457
View File
@@ -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"
);
}
}
+108
View File
@@ -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());
}
}
+94
View File
@@ -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);
}
}
+194
View File
@@ -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);
}
}
+17
View File
@@ -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)]
+159
View File
@@ -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);
}
}
+106
View File
@@ -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);
}
}
+120
View File
@@ -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<SecretShare> = [&rp1, &rp2, &rp3]
.iter()
.map(|pkg| pkg.shares.iter().find(|s| s.idx == 1).unwrap().clone())
.collect();
let agg2: Vec<SecretShare> = [&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<SecretShare> = [&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], &current).is_err());
}
}
+207
View File
@@ -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<SecretShare> = 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);
}
}
+106
View File
@@ -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());
}
}