Add integration test
This commit is contained in:
Generated
+7
@@ -135,6 +135,7 @@ dependencies = [
|
||||
name = "frost-taproot"
|
||||
version = "0.1.0"
|
||||
dependencies = [
|
||||
"hex",
|
||||
"k256",
|
||||
"rand",
|
||||
"sha2",
|
||||
@@ -175,6 +176,12 @@ dependencies = [
|
||||
"subtle",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "hex"
|
||||
version = "0.4.3"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "7f24254aa9a54b5c858eaee2f5bccdb46aaf0e486a595ed5fd8f86ba55232a70"
|
||||
|
||||
[[package]]
|
||||
name = "hmac"
|
||||
version = "0.12.1"
|
||||
|
||||
@@ -9,3 +9,10 @@ sha2 = "0.10"
|
||||
rand = "0.8"
|
||||
thiserror = "1"
|
||||
signature = "2"
|
||||
|
||||
[dev-dependencies]
|
||||
hex = "0.4"
|
||||
|
||||
[[test]]
|
||||
name = "interop"
|
||||
path = "tests/integration/interop.rs"
|
||||
|
||||
@@ -19,37 +19,58 @@ fn dst(sub: &str) -> Vec<u8> {
|
||||
}
|
||||
|
||||
/// hash_to_field using XMD with SHA-256, outputting one field element mod N.
|
||||
/// Mirrors `hash_to_field(msg, 1, { m:1, p:N, k:128, expand:'xmd', hash:sha256, DST })`.
|
||||
///
|
||||
/// Mirrors noble's `hash_to_field(msg, 1, { m:1, p:N, k:128, expand:'xmd', hash:sha256, DST })`:
|
||||
/// 1. expand_message_xmd → 48 uniform bytes
|
||||
/// 2. interpret as a big-endian 384-bit integer
|
||||
/// 3. reduce mod N
|
||||
///
|
||||
/// The 384-bit → mod-N reduction is done as:
|
||||
/// value = hi_128_bits * 2^256 + lo_256_bits (mod N)
|
||||
fn hash_to_field_n(msg: &[u8], dst_bytes: &[u8]) -> Scalar {
|
||||
// expand_message_xmd with len_in_bytes = 48 (ceil((log2(N)+k)/8) = ceil((256+128)/8) = 48)
|
||||
// Step 1: expand_message_xmd → 48 bytes
|
||||
let mut uniform = [0u8; 48];
|
||||
ExpandMsgXmd::<Sha256>::expand_message(&[msg], &[dst_bytes], 48)
|
||||
.expect("expand_message failed")
|
||||
.fill_bytes(&mut uniform);
|
||||
|
||||
// Interpret as a 384-bit integer mod N.
|
||||
// We take the low 256 bits after reduction — use U256 from the 48-byte big-endian value.
|
||||
// Noble does: interpret as big-endian integer, then mod N.
|
||||
// We replicate by taking the full 48 bytes as a big integer mod N.
|
||||
// k256 U256 is only 256 bits, so we do two-step reduction:
|
||||
// high 16 bytes * 2^256 + low 32 bytes, all mod N.
|
||||
let (hi, lo) = uniform.split_at(16);
|
||||
let mut hi32 = [0u8; 32];
|
||||
hi32[16..].copy_from_slice(hi);
|
||||
let mut lo32 = [0u8; 32];
|
||||
lo32.copy_from_slice(lo);
|
||||
// Step 2 & 3: interpret 48 bytes as big-endian integer, reduce mod N.
|
||||
// Split into high 16 bytes and low 32 bytes.
|
||||
// value = hi * 2^256 + lo (mod N)
|
||||
let (hi16, lo32_slice) = uniform.split_at(16);
|
||||
|
||||
let hi_scalar = mod_n(U256::from_be_slice(&hi32));
|
||||
let mut lo32 = [0u8; 32];
|
||||
lo32.copy_from_slice(lo32_slice);
|
||||
let lo_scalar = mod_n(U256::from_be_slice(&lo32));
|
||||
|
||||
// 2^128 mod N as a scalar
|
||||
let shift = {
|
||||
let mut s = [0u8; 32];
|
||||
s[16] = 1; // 2^128
|
||||
mod_n(U256::from_be_slice(&s))
|
||||
// 2^256 mod N: since 2^256 = N + (2^256 - N), we compute it as a scalar.
|
||||
// 2^256 - N = 0x14551231950b75fc4402da1732fc9bebf
|
||||
// Represented as 32 bytes (fits in 17 bytes):
|
||||
let two256_mod_n = {
|
||||
// 2^256 mod N = 2^256 - N (since 2^256 > N)
|
||||
// = 0x014551231950b75fc4402da1732fc9bebf (17 bytes)
|
||||
// As 32-byte big-endian:
|
||||
let bytes =
|
||||
hex_literal_32("000000000000000000000000000000014551231950b75fc4402da1732fc9bebf");
|
||||
mod_n(U256::from_be_slice(&bytes))
|
||||
};
|
||||
|
||||
hi_scalar * shift + lo_scalar
|
||||
// hi as a scalar (16 bytes → pad to 32)
|
||||
let mut hi32 = [0u8; 32];
|
||||
hi32[16..].copy_from_slice(hi16);
|
||||
let hi_scalar = mod_n(U256::from_be_slice(&hi32));
|
||||
|
||||
hi_scalar * two256_mod_n + lo_scalar
|
||||
}
|
||||
|
||||
/// Parse a hex literal into a 32-byte array (must be exactly 64 hex chars).
|
||||
fn hex_literal_32(s: &str) -> [u8; 32] {
|
||||
// Pad or truncate to 64 hex chars (32 bytes), right-aligned.
|
||||
let padded = format!("{:0>64}", s);
|
||||
let b = (0..32)
|
||||
.map(|i| u8::from_str_radix(&padded[i * 2..i * 2 + 2], 16).unwrap())
|
||||
.collect::<Vec<_>>();
|
||||
b.try_into().unwrap()
|
||||
}
|
||||
|
||||
/// H1: rho — binding factor hash.
|
||||
|
||||
@@ -124,18 +124,20 @@ pub fn verify_partial_sig(
|
||||
|
||||
/// Verify a final aggregated BIP340 Schnorr signature.
|
||||
/// Mirrors `verify_final_sig` in the TS implementation.
|
||||
///
|
||||
/// Uses `verify_raw` because noble's `schnorr.verify` passes the message directly
|
||||
/// into the BIP340 challenge hash without pre-hashing it with SHA-256.
|
||||
pub fn verify_final_sig(
|
||||
ctx: &GroupKeyContext,
|
||||
message: &[u8],
|
||||
signature: &[u8; 64],
|
||||
) -> Result<bool, Error> {
|
||||
use k256::schnorr::{Signature, VerifyingKey};
|
||||
use signature::Verifier;
|
||||
|
||||
// group_pk is 33-byte compressed; BIP340 uses x-only (32 bytes).
|
||||
let pk_bytes: [u8; 32] = ctx.group_pk[1..].try_into().unwrap();
|
||||
let vk = VerifyingKey::from_bytes(&pk_bytes).map_err(|_| Error::InvalidPoint)?;
|
||||
let sig = Signature::try_from(signature.as_slice()).map_err(|_| Error::InvalidPoint)?;
|
||||
|
||||
Ok(vk.verify(message, &sig).is_ok())
|
||||
Ok(vk.verify_raw(message, &sig).is_ok())
|
||||
}
|
||||
|
||||
@@ -0,0 +1,84 @@
|
||||
{
|
||||
"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": [
|
||||
{
|
||||
"idx": 1,
|
||||
"seckey": "0e74411caa9ef5ba058d0ccf4e54e1ee773e625fb7d6258097466cedab0de152"
|
||||
},
|
||||
{
|
||||
"idx": 2,
|
||||
"seckey": "1c77b7c3c2a14987be430edb4d63bc410e9f3cc59eeb8bbeb89951abdad59595"
|
||||
},
|
||||
{
|
||||
"idx": 3,
|
||||
"seckey": "2a7b2e6adaa39d5576f910e74c729693a600172b8600f1fcd9ec366a0a9d49d8"
|
||||
}
|
||||
],
|
||||
"commits": [
|
||||
{
|
||||
"idx": 1,
|
||||
"hidden_sn": "189aeb1bf3a453673cb144a459f0b644183ff02808cad807b672067da4f33357",
|
||||
"binder_sn": "162f3098066a9407c7ce156cb0c49c58ab34b6e195b6435fa4be759e827b9b4c",
|
||||
"hidden_pn": "024d837d707dfa4b56be26da22b9ff5cb0fd220d011351ba79334003f16871801c",
|
||||
"binder_pn": "0263c0d31a58799213f5210685b8bc2ce4539819a90c09c216a983e8f8c67a12f5"
|
||||
},
|
||||
{
|
||||
"idx": 2,
|
||||
"hidden_sn": "1c48193192f4a7ba98b04f21246da1925fdacd387ac79bb9708062c705f37a17",
|
||||
"binder_sn": "f3ede9cd66b93ce18af27792521c929c10cf21a45d72892db7d8a5088bd2ea2e",
|
||||
"hidden_pn": "034bc9f2ef5cc5eb741cc00d763e1077e8bc624df82d198781c71a0757617d8d44",
|
||||
"binder_pn": "03a1e7d63fd0665b9255df5f6d781762f7e7298a2c42ee6d67cfd287780fb3c2a6"
|
||||
}
|
||||
],
|
||||
"ctx": {
|
||||
"group_pk": "025731d4d57552d12877e3db13061d7f6ca09198963e003d1d6e0960d6651e42d3",
|
||||
"group_pn": "03e76328e49c27c12392a117d39ef9f5def368590d5e72438907fb63c1006fd589",
|
||||
"bind_prefix": "025731d4d57552d12877e3db13061d7f6ca09198963e003d1d6e0960d6651e42d3c00982b3526dcd6b7bcb4f685ddb41c7d00fecd032aa479f5df03601701bf5232ba4662301918443c019abba4a752cdcb8d4f572ad78a88ec416f17b60bb866c",
|
||||
"bind_factors": [
|
||||
{
|
||||
"idx": 1,
|
||||
"factor": "de9fa47304afaa64b5baddfccf4a8da6705edd162201ce55e1f9a478e6ec2a57"
|
||||
},
|
||||
{
|
||||
"idx": 2,
|
||||
"factor": "97aa7e9649ea9086359b7ba8fe815f54d98a5956ad63d2cf670d465d3b5d0f1f"
|
||||
}
|
||||
],
|
||||
"challenge": "99e6637f68e223b0f6b4caa36b48cc277bf036ece4f14bab657200b43ecb0d55",
|
||||
"indexes": [
|
||||
1,
|
||||
2
|
||||
],
|
||||
"message": "68656c6c6f20776f726c6421"
|
||||
},
|
||||
"psigs": [
|
||||
{
|
||||
"idx": 1,
|
||||
"pubkey": "0278f55809a11a1016d13ec4f54674810abe4a6fec8b586e14f90d0c1f80de33eb",
|
||||
"psig": "89ce878d8aa2f6c6565e963c6bbe99c45af811a2892c402b4c4e3f9ad972a48b"
|
||||
},
|
||||
{
|
||||
"idx": 2,
|
||||
"pubkey": "02f00d2b4d3b761ed6317310ed791234dfcad643c00000690e9601adc412b1a22d",
|
||||
"psig": "67488a00fc37386e12eddfd8eee2dcf997fc01255e7ea66f605db448c86363b1"
|
||||
}
|
||||
],
|
||||
"signature": "e76328e49c27c12392a117d39ef9f5def368590d5e72438907fb63c1006fd5891d715fa750b5840610aaf531949f633c4555ac20caf290c3f22cc0771f074447"
|
||||
}
|
||||
@@ -0,0 +1,357 @@
|
||||
/// 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");
|
||||
}
|
||||
Reference in New Issue
Block a user