Add integration test
This commit is contained in:
Generated
+7
@@ -135,6 +135,7 @@ dependencies = [
|
|||||||
name = "frost-taproot"
|
name = "frost-taproot"
|
||||||
version = "0.1.0"
|
version = "0.1.0"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
|
"hex",
|
||||||
"k256",
|
"k256",
|
||||||
"rand",
|
"rand",
|
||||||
"sha2",
|
"sha2",
|
||||||
@@ -175,6 +176,12 @@ dependencies = [
|
|||||||
"subtle",
|
"subtle",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "hex"
|
||||||
|
version = "0.4.3"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "7f24254aa9a54b5c858eaee2f5bccdb46aaf0e486a595ed5fd8f86ba55232a70"
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "hmac"
|
name = "hmac"
|
||||||
version = "0.12.1"
|
version = "0.12.1"
|
||||||
|
|||||||
@@ -9,3 +9,10 @@ sha2 = "0.10"
|
|||||||
rand = "0.8"
|
rand = "0.8"
|
||||||
thiserror = "1"
|
thiserror = "1"
|
||||||
signature = "2"
|
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.
|
/// 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 {
|
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];
|
let mut uniform = [0u8; 48];
|
||||||
ExpandMsgXmd::<Sha256>::expand_message(&[msg], &[dst_bytes], 48)
|
ExpandMsgXmd::<Sha256>::expand_message(&[msg], &[dst_bytes], 48)
|
||||||
.expect("expand_message failed")
|
.expect("expand_message failed")
|
||||||
.fill_bytes(&mut uniform);
|
.fill_bytes(&mut uniform);
|
||||||
|
|
||||||
// Interpret as a 384-bit integer mod N.
|
// Step 2 & 3: interpret 48 bytes as big-endian integer, reduce mod N.
|
||||||
// We take the low 256 bits after reduction — use U256 from the 48-byte big-endian value.
|
// Split into high 16 bytes and low 32 bytes.
|
||||||
// Noble does: interpret as big-endian integer, then mod N.
|
// value = hi * 2^256 + lo (mod N)
|
||||||
// We replicate by taking the full 48 bytes as a big integer mod N.
|
let (hi16, lo32_slice) = uniform.split_at(16);
|
||||||
// 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);
|
|
||||||
|
|
||||||
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));
|
let lo_scalar = mod_n(U256::from_be_slice(&lo32));
|
||||||
|
|
||||||
// 2^128 mod N as a scalar
|
// 2^256 mod N: since 2^256 = N + (2^256 - N), we compute it as a scalar.
|
||||||
let shift = {
|
// 2^256 - N = 0x14551231950b75fc4402da1732fc9bebf
|
||||||
let mut s = [0u8; 32];
|
// Represented as 32 bytes (fits in 17 bytes):
|
||||||
s[16] = 1; // 2^128
|
let two256_mod_n = {
|
||||||
mod_n(U256::from_be_slice(&s))
|
// 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.
|
/// H1: rho — binding factor hash.
|
||||||
|
|||||||
@@ -124,18 +124,20 @@ pub fn verify_partial_sig(
|
|||||||
|
|
||||||
/// Verify a final aggregated BIP340 Schnorr signature.
|
/// Verify a final aggregated BIP340 Schnorr signature.
|
||||||
/// Mirrors `verify_final_sig` in the TS implementation.
|
/// 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(
|
pub fn verify_final_sig(
|
||||||
ctx: &GroupKeyContext,
|
ctx: &GroupKeyContext,
|
||||||
message: &[u8],
|
message: &[u8],
|
||||||
signature: &[u8; 64],
|
signature: &[u8; 64],
|
||||||
) -> Result<bool, Error> {
|
) -> Result<bool, Error> {
|
||||||
use k256::schnorr::{Signature, VerifyingKey};
|
use k256::schnorr::{Signature, VerifyingKey};
|
||||||
use signature::Verifier;
|
|
||||||
|
|
||||||
// group_pk is 33-byte compressed; BIP340 uses x-only (32 bytes).
|
// 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 pk_bytes: [u8; 32] = ctx.group_pk[1..].try_into().unwrap();
|
||||||
let vk = VerifyingKey::from_bytes(&pk_bytes).map_err(|_| Error::InvalidPoint)?;
|
let vk = VerifyingKey::from_bytes(&pk_bytes).map_err(|_| Error::InvalidPoint)?;
|
||||||
let sig = Signature::try_from(signature.as_slice()).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