/// Interoperability test between cmdruid/frost (TypeScript) and frost-taproot (Rust). /// /// The fixture was generated by `ref/frost/gen_fixture.mjs`, which runs the full /// cmdruid/frost signing protocol with deterministic inputs and records every /// intermediate value. This test re-runs the same steps in Rust and asserts that /// every output matches the TypeScript reference exactly. use frost_taproot::{ commit::create_commit_pkg, context::get_group_signing_ctx, group::create_dealer_set, sign::{combine_partial_sigs, sign_msg, verify_final_sig, verify_partial_sig}, types::{PublicNonce, SecretNonce, SecretShare, ShareSignature}, }; // ── Fixture helpers ─────────────────────────────────────────────────────────── fn hex_to_32(s: &str) -> [u8; 32] { let b = hex::decode(s).expect("invalid hex"); b.try_into().expect("expected 32 bytes") } fn hex_to_33(s: &str) -> [u8; 33] { let b = hex::decode(s).expect("invalid hex"); b.try_into().expect("expected 33 bytes") } fn hex_to_vec(s: &str) -> Vec { hex::decode(s).expect("invalid hex") } fn to_hex(b: &[u8]) -> String { hex::encode(b) } // ── Fixture (generated by gen_fixture.mjs) ──────────────────────────────────── struct Fixture { secrets: [&'static str; 2], threshold: usize, share_max: u32, message: &'static str, hidden_seed: &'static str, binder_seed: &'static str, tweaks: [&'static str; 2], // Expected dealer outputs group_pk: &'static str, vss_commits: [&'static str; 2], shares: [(u32, &'static str); 3], // Expected nonce commitments for the signing subset (idx 1 and 2) commits: [(u32, &'static str, &'static str, &'static str, &'static str); 2], // Expected signing context values ctx_group_pk: &'static str, ctx_group_pn: &'static str, ctx_bind_prefix: &'static str, ctx_bind_factors: [(u32, &'static str); 2], ctx_challenge: &'static str, // Expected partial signatures psigs: [(u32, &'static str, &'static str); 2], // Expected final signature signature: &'static str, } const FX: Fixture = Fixture { secrets: [ "0070ca75929ca1ec4cd70ac34f46079bdfdd87f9d0c0bf4275f3882f7b462d0f", "0e0376a7180253cdb8b6020bff0eda529760da65e715663e2152e4be2fc7b443", ], threshold: 2, share_max: 3, message: "68656c6c6f20776f726c6421", hidden_seed: "0070ca75929ca1ec4cd70ac34f46079bdfdd87f9d0c0bf4275f3882f7b462d0f", binder_seed: "0e0376a7180253cdb8b6020bff0eda529760da65e715663e2152e4be2fc7b443", tweaks: [ "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa", "bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb", ], group_pk: "021ae63bc9ddaffe52d44c3018e83115bfb22195bd8112fcad112310714e6fd5ec", vss_commits: [ "021ae63bc9ddaffe52d44c3018e83115bfb22195bd8112fcad112310714e6fd5ec", "024f75a5478deda1102eba931e19425e59c1750533a54218ce215ced343fbfb6cf", ], shares: [ (1, "0e74411caa9ef5ba058d0ccf4e54e1ee773e625fb7d6258097466cedab0de152"), (2, "1c77b7c3c2a14987be430edb4d63bc410e9f3cc59eeb8bbeb89951abdad59595"), (3, "2a7b2e6adaa39d5576f910e74c729693a600172b8600f1fcd9ec366a0a9d49d8"), ], // (idx, hidden_sn, binder_sn, hidden_pn, binder_pn) commits: [ (1, "189aeb1bf3a453673cb144a459f0b644183ff02808cad807b672067da4f33357", "162f3098066a9407c7ce156cb0c49c58ab34b6e195b6435fa4be759e827b9b4c", "024d837d707dfa4b56be26da22b9ff5cb0fd220d011351ba79334003f16871801c", "0263c0d31a58799213f5210685b8bc2ce4539819a90c09c216a983e8f8c67a12f5"), (2, "1c48193192f4a7ba98b04f21246da1925fdacd387ac79bb9708062c705f37a17", "f3ede9cd66b93ce18af27792521c929c10cf21a45d72892db7d8a5088bd2ea2e", "034bc9f2ef5cc5eb741cc00d763e1077e8bc624df82d198781c71a0757617d8d44", "03a1e7d63fd0665b9255df5f6d781762f7e7298a2c42ee6d67cfd287780fb3c2a6"), ], ctx_group_pk: "025731d4d57552d12877e3db13061d7f6ca09198963e003d1d6e0960d6651e42d3", ctx_group_pn: "03e76328e49c27c12392a117d39ef9f5def368590d5e72438907fb63c1006fd589", ctx_bind_prefix: "025731d4d57552d12877e3db13061d7f6ca09198963e003d1d6e0960d6651e42d3c00982b3526dcd6b7bcb4f685ddb41c7d00fecd032aa479f5df03601701bf5232ba4662301918443c019abba4a752cdcb8d4f572ad78a88ec416f17b60bb866c", ctx_bind_factors: [ (1, "de9fa47304afaa64b5baddfccf4a8da6705edd162201ce55e1f9a478e6ec2a57"), (2, "97aa7e9649ea9086359b7ba8fe815f54d98a5956ad63d2cf670d465d3b5d0f1f"), ], ctx_challenge: "99e6637f68e223b0f6b4caa36b48cc277bf036ece4f14bab657200b43ecb0d55", // (idx, pubkey, psig) psigs: [ (1, "0278f55809a11a1016d13ec4f54674810abe4a6fec8b586e14f90d0c1f80de33eb", "89ce878d8aa2f6c6565e963c6bbe99c45af811a2892c402b4c4e3f9ad972a48b"), (2, "02f00d2b4d3b761ed6317310ed791234dfcad643c00000690e9601adc412b1a22d", "67488a00fc37386e12eddfd8eee2dcf997fc01255e7ea66f605db448c86363b1"), ], signature: "e76328e49c27c12392a117d39ef9f5def368590d5e72438907fb63c1006fd5891d715fa750b5840610aaf531949f633c4555ac20caf290c3f22cc0771f074447", }; // ── Tests ───────────────────────────────────────────────────────────────────── #[test] fn test_dealer_set_matches_ts() { let secrets: Vec<[u8; 32]> = FX.secrets.iter().map(|s| hex_to_32(s)).collect(); let group = create_dealer_set(FX.threshold, FX.share_max, &secrets).expect("create_dealer_set failed"); assert_eq!(to_hex(&group.group_pk), FX.group_pk, "group_pk mismatch"); for (i, expected) in FX.vss_commits.iter().enumerate() { assert_eq!( to_hex(&group.vss_commits[i]), *expected, "vss_commit[{i}] mismatch" ); } for (idx, expected_seckey) in &FX.shares { let share = group.shares.iter().find(|s| s.idx == *idx).unwrap(); assert_eq!( to_hex(&share.seckey), *expected_seckey, "share[{idx}].seckey mismatch" ); } } #[test] fn test_nonce_commitments_match_ts() { for (idx, hidden_sn, binder_sn, hidden_pn, binder_pn) in &FX.commits { let share = SecretShare { idx: *idx, seckey: hex_to_32(FX.shares[(*idx as usize) - 1].1), }; let hidden_seed = hex_to_32(FX.hidden_seed); let binder_seed = hex_to_32(FX.binder_seed); let commit = create_commit_pkg(&share, Some(&hidden_seed), Some(&binder_seed)); assert_eq!( to_hex(&commit.hidden_sn), *hidden_sn, "hidden_sn mismatch for idx {idx}" ); assert_eq!( to_hex(&commit.binder_sn), *binder_sn, "binder_sn mismatch for idx {idx}" ); assert_eq!( to_hex(&commit.hidden_pn), *hidden_pn, "hidden_pn mismatch for idx {idx}" ); assert_eq!( to_hex(&commit.binder_pn), *binder_pn, "binder_pn mismatch for idx {idx}" ); } } #[test] fn test_signing_context_matches_ts() { let pnonces: Vec = FX .commits .iter() .map(|(idx, _, _, hidden_pn, binder_pn)| PublicNonce { idx: *idx, hidden_pn: hex_to_33(hidden_pn), binder_pn: hex_to_33(binder_pn), }) .collect(); let tweaks: Vec<[u8; 32]> = FX.tweaks.iter().map(|t| hex_to_32(t)).collect(); let message = hex_to_vec(FX.message); let group_pk_bytes = hex_to_33(FX.group_pk); let ctx = get_group_signing_ctx(&group_pk_bytes, &pnonces, &message, &tweaks) .expect("get_group_signing_ctx failed"); assert_eq!( to_hex(&ctx.group_pk), FX.ctx_group_pk, "ctx.group_pk mismatch" ); assert_eq!( to_hex(&ctx.group_pn), FX.ctx_group_pn, "ctx.group_pn mismatch" ); assert_eq!( to_hex(&ctx.bind_prefix), FX.ctx_bind_prefix, "ctx.bind_prefix mismatch" ); for (idx, expected_factor) in &FX.ctx_bind_factors { let bf = ctx.bind_factors.iter().find(|b| b.idx == *idx).unwrap(); assert_eq!( to_hex(&bf.factor), *expected_factor, "bind_factor[{idx}] mismatch" ); } use frost_taproot::ecc::util::scalar_to_bytes; assert_eq!( to_hex(&scalar_to_bytes(&ctx.challenge)), FX.ctx_challenge, "challenge mismatch" ); } #[test] fn test_partial_signatures_match_ts() { let pnonces: Vec = FX .commits .iter() .map(|(idx, _, _, hidden_pn, binder_pn)| PublicNonce { idx: *idx, hidden_pn: hex_to_33(hidden_pn), binder_pn: hex_to_33(binder_pn), }) .collect(); let tweaks: Vec<[u8; 32]> = FX.tweaks.iter().map(|t| hex_to_32(t)).collect(); let message = hex_to_vec(FX.message); let group_pk_bytes = hex_to_33(FX.group_pk); let ctx = get_group_signing_ctx(&group_pk_bytes, &pnonces, &message, &tweaks) .expect("get_group_signing_ctx failed"); for (idx, expected_pubkey, expected_psig) in &FX.psigs { let share = SecretShare { idx: *idx, seckey: hex_to_32(FX.shares[(*idx as usize) - 1].1), }; let (_, hidden_sn, binder_sn, _, _) = FX.commits[(*idx as usize) - 1]; let snonce = SecretNonce { idx: *idx, hidden_sn: hex_to_32(hidden_sn), binder_sn: hex_to_32(binder_sn), }; let sig = sign_msg(&ctx, &share, &snonce).expect("sign_msg failed"); assert_eq!( to_hex(&sig.pubkey), *expected_pubkey, "psig[{idx}].pubkey mismatch" ); assert_eq!( to_hex(&sig.psig), *expected_psig, "psig[{idx}].psig mismatch" ); } } #[test] fn test_partial_sig_verification() { let pnonces: Vec = FX .commits .iter() .map(|(idx, _, _, hidden_pn, binder_pn)| PublicNonce { idx: *idx, hidden_pn: hex_to_33(hidden_pn), binder_pn: hex_to_33(binder_pn), }) .collect(); let tweaks: Vec<[u8; 32]> = FX.tweaks.iter().map(|t| hex_to_32(t)).collect(); let message = hex_to_vec(FX.message); let group_pk_bytes = hex_to_33(FX.group_pk); let ctx = get_group_signing_ctx(&group_pk_bytes, &pnonces, &message, &tweaks) .expect("get_group_signing_ctx failed"); for (idx, expected_pubkey, expected_psig) in &FX.psigs { let pnonce = pnonces.iter().find(|p| p.idx == *idx).unwrap(); let share_pk = hex_to_33(expected_pubkey); let psig = hex_to_32(expected_psig); let ok = verify_partial_sig(&ctx, pnonce, &share_pk, &psig).expect("verify_partial_sig failed"); assert!(ok, "partial sig verification failed for idx {idx}"); } } #[test] fn test_final_signature_matches_ts() { let pnonces: Vec = FX .commits .iter() .map(|(idx, _, _, hidden_pn, binder_pn)| PublicNonce { idx: *idx, hidden_pn: hex_to_33(hidden_pn), binder_pn: hex_to_33(binder_pn), }) .collect(); let tweaks: Vec<[u8; 32]> = FX.tweaks.iter().map(|t| hex_to_32(t)).collect(); let message = hex_to_vec(FX.message); let group_pk_bytes = hex_to_33(FX.group_pk); let ctx = get_group_signing_ctx(&group_pk_bytes, &pnonces, &message, &tweaks) .expect("get_group_signing_ctx failed"); let psigs: Vec = FX .psigs .iter() .map(|(idx, pubkey, psig)| ShareSignature { idx: *idx, pubkey: hex_to_33(pubkey), psig: hex_to_32(psig), }) .collect(); let sig = combine_partial_sigs(&ctx, &psigs).expect("combine_partial_sigs failed"); assert_eq!(to_hex(&sig), FX.signature, "final signature mismatch"); let key_ctx = ctx.key_context(); let valid = verify_final_sig(&key_ctx, &message, &sig).expect("verify_final_sig failed"); assert!(valid, "final signature failed BIP340 verification"); }