358 lines
12 KiB
Rust
358 lines
12 KiB
Rust
/// 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<u8> {
|
|
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<PublicNonce> = 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<PublicNonce> = 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<PublicNonce> = 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<PublicNonce> = 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<ShareSignature> = 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");
|
|
}
|