From 498a459f55e8413b9de5895d9b0ba46a792714af Mon Sep 17 00:00:00 2001 From: Kittywhiskers Van Gogh <63189531+kwvg@users.noreply.github.com> Date: Sun, 9 Aug 2026 12:50:00 +0530 Subject: [PATCH 01/14] sdk%lint(codeql): expand secret type classification by shape --- contrib/codeql/zeroize.ql | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/contrib/codeql/zeroize.ql b/contrib/codeql/zeroize.ql index cea1256d..aba222b6 100644 --- a/contrib/codeql/zeroize.ql +++ b/contrib/codeql/zeroize.ql @@ -299,7 +299,7 @@ predicate variableTimeSecretTest(Function f, string how) { * this query exists to examine unchecked. */ predicate secretType(TypeItem t) { - isSecretType(t) and + (isSecretType(t) or wipesSelf(t)) and fileOf(t).fromSource() and not isTestCode(t) } From dec05047514733bf4da04444b56489fcae5cf761 Mon Sep 17 00:00:00 2001 From: Kittywhiskers Van Gogh <63189531+kwvg@users.noreply.github.com> Date: Sun, 9 Aug 2026 00:10:11 +0530 Subject: [PATCH 02/14] pkc%refac: drop `hex-literal` in favour of `hex-conservative` --- Cargo.lock | 1 - pkgs/pkc/Cargo.toml | 11 +++-- pkgs/pkc/src/bls/chia_h2c.rs | 74 ++++++++++------------------ pkgs/pkc/src/bls/scheme_chia.rs | 9 ++-- pkgs/pkc/src/bls/scheme_ietf.rs | 17 +++---- pkgs/pkc/src/bls/tests.rs | 2 +- pkgs/pkc/tests/bls_ietf_aggregate.rs | 2 +- pkgs/pkc/tests/bls_ietf_sign.rs | 2 +- pkgs/pkc/tests/common/mod.rs | 5 +- 9 files changed, 45 insertions(+), 78 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index df546635..086aa04d 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -549,7 +549,6 @@ dependencies = [ "dash-types", "divan", "hex-conservative 0.3.2", - "hex-literal", "k256", "rand_core 0.6.4", "rayon", diff --git a/pkgs/pkc/Cargo.toml b/pkgs/pkc/Cargo.toml index 18c4068b..3de631b6 100644 --- a/pkgs/pkc/Cargo.toml +++ b/pkgs/pkc/Cargo.toml @@ -12,8 +12,9 @@ blst = { version = "0.3", default-features = false, optional = true } cfg-if = "1" dash-num = { version = "0.0.0", path = "../num" } dash-types = { version = "0.0.0", path = "../types", default-features = false } -hex-conservative = { version = "0.3", optional = true } -hex-literal = "0.4" +hex-conservative = { version = "0.3", default-features = false, features = [ + "alloc", +] } k256 = { version = "0.13", default-features = false, features = [ "arithmetic", "ecdsa", @@ -51,10 +52,10 @@ std = [ "rand_core/getrandom", ] bls = ["dep:blst", "dep:sha2"] -ecdsa = ["dep:k256", "dep:hex-conservative"] -serde = ["dep:serde", "dep:hex-conservative", "dash-num/serde", "dash-types/serde"] +ecdsa = ["dep:k256"] +serde = ["dep:serde", "dash-num/serde", "dash-types/serde"] full = ["ecdsa", "bls", "serde", "std", "tests"] -tests = ["std", "dep:hex-conservative", "dep:rstest"] +tests = ["std", "dep:rstest"] [lints] workspace = true diff --git a/pkgs/pkc/src/bls/chia_h2c.rs b/pkgs/pkc/src/bls/chia_h2c.rs index 9d07de3c..6bf9b5e5 100644 --- a/pkgs/pkc/src/bls/chia_h2c.rs +++ b/pkgs/pkc/src/bls/chia_h2c.rs @@ -8,62 +8,41 @@ use super::blst_ffi::{Fp, Fp2, G2Affine, Point, G2}; -use hex_literal::hex; +use hex_conservative::hex; use sha2::{Digest, Sha256}; -// sqrt(-3) mod p (big-endian, 48 bytes, left-padded from 40-byte hex -// B12_P381_S3). -const S3: [u8; 48] = hex!( - "00000000 00000000 be32ce5f beed9ca3" - "74d38c0e d41eefd5 bb675277 cdf12d11" - "bc2fb026 c4140004 5c03ffff fffdfffd" -); - -// (sqrt(-3) - 1) / 2 mod p (big-endian, 48 bytes, left-padded from 40-byte hex -// B12_P381_S32). -const S32: [u8; 48] = hex!( - "00000000 00000000 5f19672f df76ce51" - "ba69c607 6a0f77ea ddb3a93b e6f89688" - "de17d813 620a0002 2e01ffff fffefffe" -); +// sqrt(-3) mod p (big-endian, left-padded from 40-byte B12_P381_S3). +const S3: [u8; 48] = + hex!("0000000000000000be32ce5fbeed9ca374d38c0ed41eefd5bb675277cdf12d11bc2fb026c41400045c03fffffffdfffd"); + +// (sqrt(-3) - 1) / 2 mod p (big-endian, left-padded from 40-byte B12_P381_S32). +const S32: [u8; 48] = + hex!("00000000000000005f19672fdf76ce51ba69c6076a0f77eaddb3a93be6f89688de17d813620a00022e01fffffffefffe"); // BLS12-381 curve parameter |x| in little-endian byte order. -// x = -(2^63 + 2^62 + 2^60 + 2^57 + 2^48 + 2^16) +// x = -(2^63 + 2^62 + 2^60 + 2^57 + 2^48 + 2^16) // |x| = 0xD201000000010000 -const BLS_X_LE: [u8; 8] = hex!("00000100 000001d2"); +const BLS_X_LE: [u8; 8] = hex!("00000100000001d2"); const BLS_X_BITS: usize = 64; // Frobenius endomorphism constants for the BLS12-381 M-type twist. // psi(x,y) = (conj(x)*PSI_COEFF_X, conj(y)*PSI_COEFF_Y) -// PSI_COEFF_X = (0, 0x1a0111ea397fe699ec02408663d4de85aa0d857d89759ad4 -// 897d29650fb85f9b409427eb4f49fffd8bfd00000000aaad) -const PSI_COEFF_X_C1: [u8; 48] = hex!( - "1a0111ea 397fe699 ec024086 63d4de85" - "aa0d857d 89759ad4 897d2965 0fb85f9b" - "409427eb 4f49fffd 8bfd0000 0000aaad" -); +// PSI_COEFF_X.c0 is zero, so only c1 is carried. +const PSI_COEFF_X_C1: [u8; 48] = + hex!("1a0111ea397fe699ec02408663d4de85aa0d857d89759ad4897d29650fb85f9b409427eb4f49fffd8bfd00000000aaad"); // PSI_COEFF_Y.c0 -const PSI_COEFF_Y_C0: [u8; 48] = hex!( - "135203e6 0180a68e e2e9c448 d77a2cd9" - "1c3dedd9 30b1cf60 ef396489 f61eb45e" - "304466cf 3e67fa0a f1ee7b04 121bdea2" -); +const PSI_COEFF_Y_C0: [u8; 48] = + hex!("135203e60180a68ee2e9c448d77a2cd91c3dedd930b1cf60ef396489f61eb45e304466cf3e67fa0af1ee7b04121bdea2"); // PSI_COEFF_Y.c1 -const PSI_COEFF_Y_C1: [u8; 48] = hex!( - "06af0e04 37ff400b 6831e36d 6bd17ffe" - "48395dab c2d3435e 77f76e17 009241c5" - "ee67992f 72ec05f4 c81084fb ede3cc09" -); - -// 2^384 mod p for BLS12-381 (big-endian, 48 bytes). Used in wide reduction. -const R_MOD_P: [u8; 48] = hex!( - "15f65ec3 fa80e493 5c071a97 a256ec6d" - "77ce5853 70525745 5f489857 53c758ba" - "ebf4000b c40c0002 76090000 0002fffd" -); +const PSI_COEFF_Y_C1: [u8; 48] = + hex!("06af0e0437ff400b6831e36d6bd17ffe48395dabc2d3435e77f76e17009241c5ee67992f72ec05f4c81084fbede3cc09"); + +// 2^384 mod p for BLS12-381 (big-endian). Used in wide reduction. +const R_MOD_P: [u8; 48] = + hex!("15f65ec3fa80e4935c071a97a256ec6d77ce5853705257455f48985753c758baebf4000bc40c0002760900000002fffd"); // The 'b' coefficient for BLS12-381 twist curve: y^2 = x^3 + 4(1+i). fn curve_b() -> Fp2 { @@ -95,9 +74,9 @@ pub(crate) fn hash_to_g2(msg: &[u8; 32]) -> G2 { /// Cofactor clearing via the Budroni-Pintore method. /// -/// Computes `(x^2-x-1)*P + psi((x-1)*P) + psi^2(2*P)` -/// where `x` is the BLS12-381 curve parameter and `psi` -/// is the Frobenius endomorphism on the twist. +/// Computes `(x^2-x-1)*P + psi((x-1)*P) + psi^2(2*P)` where `x` is the +/// BLS12-381 curve parameter and `psi` is the Frobenius endomorphism on the +/// twist. fn mul_cof_b12(p: &G2) -> G2 { // t0 = x·P (x is negative, so negate after multiplying by |x|) let t0 = -p.mul_scalar(&BLS_X_LE, BLS_X_BITS); @@ -121,9 +100,8 @@ fn mul_cof_b12(p: &G2) -> G2 { /// Frobenius endomorphism psi on E'(Fp2). /// -/// `psi(x, y) = (conj(x) * PSI_COEFF_X, conj(y) * PSI_COEFF_Y)` -/// -/// where `conj(a + b*u) = a - b*u`. +/// `psi(x, y) = (conj(x) * PSI_COEFF_X, conj(y) * PSI_COEFF_Y)` where +/// `conj(a + b*u) = a - b*u`. fn psi(p: &G2Affine) -> G2Affine { // Conjugate x and y (negate the c1 component of each). let x = p.x().with_c1(-p.x().c1()); diff --git a/pkgs/pkc/src/bls/scheme_chia.rs b/pkgs/pkc/src/bls/scheme_chia.rs index 806459ab..fd7e9657 100644 --- a/pkgs/pkc/src/bls/scheme_chia.rs +++ b/pkgs/pkc/src/bls/scheme_chia.rs @@ -13,16 +13,13 @@ use super::scheme_ops::BlsScheme; use super::schemes::BlsScChia; use blst::min_pk; -use hex_literal::hex; +use hex_conservative::hex; use zeroize::Zeroize; /// y.c1 > (p-1)/2, matching the legacy sign convention. fn y_c1_is_larger(y_c1: &[u8]) -> bool { - const HALF_P: [u8; 48] = hex!( - "0d0088f5 1cbff34d 258dd3db 21a5d66b" - "b23ba5c2 79c2895f b3986950 7b587b12" - "0f55ffff 58a9ffff dcff7fff ffffd555" - ); + const HALF_P: [u8; 48] = + hex!("0d0088f51cbff34d258dd3db21a5d66bb23ba5c279c2895fb39869507b587b120f55ffff58a9ffffdcff7fffffffd555"); y_c1.len() >= 48 && y_c1[..48] > HALF_P[..] } diff --git a/pkgs/pkc/src/bls/scheme_ietf.rs b/pkgs/pkc/src/bls/scheme_ietf.rs index ccf85f8b..2ceb73a9 100644 --- a/pkgs/pkc/src/bls/scheme_ietf.rs +++ b/pkgs/pkc/src/bls/scheme_ietf.rs @@ -160,8 +160,8 @@ mod tests { use crate::prelude::*; use dash_dev::{arr_from_hex, vec_from_hex, Corpus}; + use hex_conservative::hex; use hex_conservative::DisplayHex; - use hex_literal::hex; use serde::Deserialize; #[derive(Deserialize)] @@ -194,19 +194,14 @@ mod tests { #[test] fn pyecc_signature_matches() { let sk = BlsScIetf::sk_from_bytes(&hex!( - "0101010101010101010101010101010101" - "010101010101010101010101010101" + "0101010101010101010101010101010101010101010101010101010101010101" )) .unwrap(); let msg = hex!("030104010509"); - let expected = hex!( - "96ba34fac33c7f129d602a0bc8a3d43f" - "9abc014eceaab7359146b4b150e57b80" - "8645738f35671e9e10e0d862a30cab70" - "074eb5831d13e6a5b162d01eebe687d0" - "164adbd0a864370a7c222a2768d7704d" - "a254f1bf1823665bc2361f9dd8c00e99" - ); + let expected = hex!(concat!( + "96ba34fac33c7f129d602a0bc8a3d43f9abc014eceaab7359146b4b150e57b808645738f35671e9e10e0d862a30cab70", + "074eb5831d13e6a5b162d01eebe687d0164adbd0a864370a7c222a2768d7704da254f1bf1823665bc2361f9dd8c00e99" + )); let sig = BlsScIetf::sign(&sk, &msg); assert_eq!(BlsScIetf::sig_to_bytes(&sig), expected); assert!(BlsScIetf::verify(&sig, &msg, &BlsScIetf::derive_pk(&sk)).is_ok()); diff --git a/pkgs/pkc/src/bls/tests.rs b/pkgs/pkc/src/bls/tests.rs index fd06b84b..702347dd 100644 --- a/pkgs/pkc/src/bls/tests.rs +++ b/pkgs/pkc/src/bls/tests.rs @@ -10,7 +10,7 @@ use crate::bls_chia::SecretKey as ChiaSk; use crate::bls_ietf::SecretKey as IetfSk; use crate::prelude::*; -use hex_literal::hex; +use hex_conservative::hex; use rstest::*; /// IKM producing the first deterministic test key. diff --git a/pkgs/pkc/tests/bls_ietf_aggregate.rs b/pkgs/pkc/tests/bls_ietf_aggregate.rs index 53a834c5..949840c0 100644 --- a/pkgs/pkc/tests/bls_ietf_aggregate.rs +++ b/pkgs/pkc/tests/bls_ietf_aggregate.rs @@ -13,7 +13,7 @@ use dash_pkc::bls::tests as common; use dash_pkc::bls_ietf::{ aggregate_pk, aggregate_sig, fast_verify_aggregates, verify_aggregates, PublicKey, SecretKey, Signature, }; -use hex_literal::hex; +use hex_conservative::hex; use rstest::*; /// Aggregated public key serializes to 48 bytes. diff --git a/pkgs/pkc/tests/bls_ietf_sign.rs b/pkgs/pkc/tests/bls_ietf_sign.rs index 079f6c3a..2735491d 100644 --- a/pkgs/pkc/tests/bls_ietf_sign.rs +++ b/pkgs/pkc/tests/bls_ietf_sign.rs @@ -12,7 +12,7 @@ use common::*; #[cfg(feature = "serde")] use dash_dev::assert_json_rt; use dash_pkc::{bls::tests as common, bls_ietf::SecretKey, bls_ietf::Signature}; -use hex_literal::hex; +use hex_conservative::hex; use rstest::*; /// Sign then verify with a generated key succeeds and is diff --git a/pkgs/pkc/tests/common/mod.rs b/pkgs/pkc/tests/common/mod.rs index 17efc246..b6c9e9a0 100644 --- a/pkgs/pkc/tests/common/mod.rs +++ b/pkgs/pkc/tests/common/mod.rs @@ -9,7 +9,4 @@ #![allow(dead_code, reason = "usage dependent on build flags")] /// Shared test constants. -pub const MSG_DEADBEEF: [u8; 32] = hex_literal::hex!( - "deadbeefdeadbeefdeadbeefdeadbeef" - "cafebabecafebabecafebabecafebabe" -); +pub const MSG_DEADBEEF: [u8; 32] = hex_conservative::hex!("deadbeefdeadbeefdeadbeefdeadbeefcafebabecafebabecafebabecafebabe"); From 7d66ab6e280d6d7c4aaf78accdf0009ef3ad8294 Mon Sep 17 00:00:00 2001 From: Kittywhiskers Van Gogh <63189531+kwvg@users.noreply.github.com> Date: Sun, 9 Aug 2026 13:50:33 +0530 Subject: [PATCH 03/14] pkc%refac(bls): re-home the scheme layer's verify helper and DSTs --- pkgs/pkc/src/bls/mod.rs | 2 +- pkgs/pkc/src/bls/scheme_ietf.rs | 25 ++++++++++--------------- pkgs/pkc/src/bls/scheme_ops.rs | 10 ++++++++++ pkgs/pkc/src/bls_ietf/agg.rs | 13 ++++--------- pkgs/pkc/src/bls_ietf/mod.rs | 6 ------ pkgs/pkc/src/bls_ietf/pk.rs | 13 ++++--------- pkgs/pkc/src/bls_ietf/sig.rs | 15 +++++---------- pkgs/pkc/src/bls_ietf/sk.rs | 4 ++-- 8 files changed, 36 insertions(+), 52 deletions(-) diff --git a/pkgs/pkc/src/bls/mod.rs b/pkgs/pkc/src/bls/mod.rs index 28f62c80..0df88c11 100644 --- a/pkgs/pkc/src/bls/mod.rs +++ b/pkgs/pkc/src/bls/mod.rs @@ -23,10 +23,10 @@ pub use sig_id::BlsSigId; cfg_if::cfg_if! { if #[cfg(feature = "bls")] { mod scheme_chia; - mod scheme_ietf; #[expect(unsafe_code, reason = "blst C FFI")] pub(crate) mod blst_ffi; pub(crate) mod chia_h2c; + pub(crate) mod scheme_ietf; pub(crate) mod scheme_ops; #[cfg(feature = "tests")] diff --git a/pkgs/pkc/src/bls/scheme_ietf.rs b/pkgs/pkc/src/bls/scheme_ietf.rs index 2ceb73a9..cb7f7690 100644 --- a/pkgs/pkc/src/bls/scheme_ietf.rs +++ b/pkgs/pkc/src/bls/scheme_ietf.rs @@ -8,12 +8,17 @@ use super::blst_ffi::{G1Affine, G2Affine, G1, G2}; use super::error::BlsError; -use super::scheme_ops::BlsScheme; +use super::scheme_ops::{verify_ok, BlsScheme}; use super::schemes::BlsScIetf; -use crate::bls_ietf::DST_BASIC; use blst::min_pk::{AggregatePublicKey, AggregateSignature, PublicKey, SecretKey, Signature}; -use blst::BLST_ERROR; + +/// Domain separation tag for the basic (NUL) signature scheme. +pub(crate) const DST_BASIC: &[u8] = b"BLS_SIG_BLS12381G2_XMD:SHA-256_SSWU_RO_NUL_"; +/// Domain separation tag for signatures in the proof-of-possession scheme. +pub(crate) const DST_POP: &[u8] = b"BLS_SIG_BLS12381G2_XMD:SHA-256_SSWU_RO_POP_"; +/// Domain separation tag for proofs of possession. +pub(crate) const DST_POP_PROVE: &[u8] = b"BLS_POP_BLS12381G2_XMD:SHA-256_SSWU_RO_POP_"; impl BlsScheme for BlsScIetf { type InnerSk = SecretKey; @@ -112,12 +117,7 @@ impl BlsScheme for BlsScIetf { /// Verify against the basic-scheme DST. fn verify(sig: &Self::InnerSig, msg: &Self::Msg, pk: &Self::InnerPk) -> Result<(), BlsError> { - let result = sig.verify(true, msg, DST_BASIC, &[], pk, true); - if result == BLST_ERROR::BLST_SUCCESS { - Ok(()) - } else { - Err(BlsError::VerifyFailed) - } + verify_ok(sig.verify(true, msg, DST_BASIC, &[], pk, true)) } /// Aggregate the public keys via blst. @@ -143,12 +143,7 @@ impl BlsScheme for BlsScIetf { if pks.is_empty() { return Err(BlsError::EmptyAggregation); } - let result = sig.fast_aggregate_verify(true, msg, DST_BASIC, pks); - if result == BLST_ERROR::BLST_SUCCESS { - Ok(()) - } else { - Err(BlsError::VerifyFailed) - } + verify_ok(sig.fast_aggregate_verify(true, msg, DST_BASIC, pks)) } } diff --git a/pkgs/pkc/src/bls/scheme_ops.rs b/pkgs/pkc/src/bls/scheme_ops.rs index 1bd440a6..519e9799 100644 --- a/pkgs/pkc/src/bls/scheme_ops.rs +++ b/pkgs/pkc/src/bls/scheme_ops.rs @@ -11,6 +11,7 @@ use super::error::BlsError; use super::schemes::BlsSchemeId; use crate::prelude::*; +use blst::BLST_ERROR; use dash_num::Hash256; use sha2::{Digest, Sha256}; use zeroize::{Zeroize, Zeroizing}; @@ -23,6 +24,15 @@ use core::fmt::Debug; /// rather than [`blst_ffi::FR_BITS`]. const WEIGHT_BITS: usize = 256; +/// Map a blst verification outcome onto a [`BlsError`]. +pub(crate) fn verify_ok(result: BLST_ERROR) -> Result<(), BlsError> { + if result == BLST_ERROR::BLST_SUCCESS { + Ok(()) + } else { + Err(BlsError::VerifyFailed) + } +} + /// BLS operations tied to a specific scheme. pub(crate) trait BlsScheme: BlsSchemeId { /// Inner secret key representation. diff --git a/pkgs/pkc/src/bls_ietf/agg.rs b/pkgs/pkc/src/bls_ietf/agg.rs index eb348a32..c5756dbc 100644 --- a/pkgs/pkc/src/bls_ietf/agg.rs +++ b/pkgs/pkc/src/bls_ietf/agg.rs @@ -9,12 +9,12 @@ use super::pk::PublicKey; use super::sig::Signature; use super::sk::SecretKey; -use super::DST; -use crate::bls::scheme_ops::BlsScheme; +use crate::bls::scheme_ietf::DST_BASIC; +use crate::bls::scheme_ops::{verify_ok, BlsScheme}; use crate::bls::{BlsError, BlsScIetf}; use crate::prelude::*; -use blst::{min_pk, BLST_ERROR}; +use blst::min_pk; /// Aggregate multiple public keys into one. pub fn aggregate_pk(keys: &[&PublicKey]) -> Result { @@ -43,12 +43,7 @@ pub fn verify_aggregates(sig: &Signature, msgs: &[&[u8]], pks: &[&PublicKey]) -> return Err(BlsError::EmptyAggregation); } let inner_pks: Vec<&min_pk::PublicKey> = pks.iter().map(|k| &k.0).collect(); - let result = sig.0.aggregate_verify(true, msgs, DST, &inner_pks, true); - if result == BLST_ERROR::BLST_SUCCESS { - Ok(()) - } else { - Err(BlsError::VerifyFailed) - } + verify_ok(sig.0.aggregate_verify(true, msgs, DST_BASIC, &inner_pks, true)) } /// Securely aggregate and verify signatures with public-key weighting. diff --git a/pkgs/pkc/src/bls_ietf/mod.rs b/pkgs/pkc/src/bls_ietf/mod.rs index 980fb782..0bc37377 100644 --- a/pkgs/pkc/src/bls_ietf/mod.rs +++ b/pkgs/pkc/src/bls_ietf/mod.rs @@ -22,12 +22,6 @@ pub use pk::PublicKey; pub use sig::Signature; pub use sk::{Scheme, SecretKey}; -pub(crate) const DST_BASIC: &[u8] = b"BLS_SIG_BLS12381G2_XMD:SHA-256_SSWU_RO_NUL_"; -const DST_POP: &[u8] = b"BLS_SIG_BLS12381G2_XMD:SHA-256_SSWU_RO_POP_"; -const DST_POP_PROVE: &[u8] = b"BLS_POP_BLS12381G2_XMD:SHA-256_SSWU_RO_POP_"; - -const DST: &[u8] = DST_BASIC; - // Compile-time contract: if any of these methods are // removed or their signatures change, this block fails. const _: () = { diff --git a/pkgs/pkc/src/bls_ietf/pk.rs b/pkgs/pkc/src/bls_ietf/pk.rs index 3516137f..9935f8d5 100644 --- a/pkgs/pkc/src/bls_ietf/pk.rs +++ b/pkgs/pkc/src/bls_ietf/pk.rs @@ -8,11 +8,11 @@ use super::sig::Signature; use super::sk::SecretKey; -use super::DST_POP_PROVE; -use crate::bls::scheme_ops::BlsScheme; +use crate::bls::scheme_ietf::DST_POP_PROVE; +use crate::bls::scheme_ops::{verify_ok, BlsScheme}; use crate::bls::{BlsError, BlsPkBytes, BlsScIetf}; -use blst::{min_pk, BLST_ERROR}; +use blst::min_pk; use dash_types::Unencodable; /// A BLS public key (48-byte compressed G1 point). @@ -61,12 +61,7 @@ impl PublicKey { /// Returns [`BlsError::VerifyFailed`] if the proof does not verify. pub fn verify_possession(&self, pop: &Signature) -> Result<(), BlsError> { let pk_bytes = self.to_bytes(); - let result = pop.0.verify(true, &pk_bytes, DST_POP_PROVE, &[], &self.0, true); - if result == BLST_ERROR::BLST_SUCCESS { - Ok(()) - } else { - Err(BlsError::VerifyFailed) - } + verify_ok(pop.0.verify(true, &pk_bytes, DST_POP_PROVE, &[], &self.0, true)) } } diff --git a/pkgs/pkc/src/bls_ietf/sig.rs b/pkgs/pkc/src/bls_ietf/sig.rs index 535c9700..346b195c 100644 --- a/pkgs/pkc/src/bls_ietf/sig.rs +++ b/pkgs/pkc/src/bls_ietf/sig.rs @@ -8,11 +8,11 @@ use super::pk::PublicKey; use super::sk::Scheme; -use super::{DST, DST_POP}; -use crate::bls::scheme_ops::BlsScheme; +use crate::bls::scheme_ietf::{DST_BASIC, DST_POP}; +use crate::bls::scheme_ops::{verify_ok, BlsScheme}; use crate::bls::{BlsError, BlsScIetf, BlsSigBytes}; -use blst::{min_pk, BLST_ERROR}; +use blst::min_pk; use dash_types::Unencodable; /// A BLS signature (96-byte compressed G2 point). @@ -60,19 +60,14 @@ impl Signature { /// Returns [`BlsError::VerifyFailed`] if the signature does not verify. pub fn verify_with(&self, msg: &[u8], pk: &PublicKey, scheme: Scheme) -> Result<(), BlsError> { let dst = match scheme { - Scheme::Basic => DST, + Scheme::Basic => DST_BASIC, Scheme::ProofOfPossession => DST_POP, }; self.verify_raw(msg, pk, dst) } fn verify_raw(&self, msg: &[u8], pk: &PublicKey, dst: &[u8]) -> Result<(), BlsError> { - let result = self.0.verify(true, msg, dst, &[], &pk.0, true); - if result == BLST_ERROR::BLST_SUCCESS { - Ok(()) - } else { - Err(BlsError::VerifyFailed) - } + verify_ok(self.0.verify(true, msg, dst, &[], &pk.0, true)) } } diff --git a/pkgs/pkc/src/bls_ietf/sk.rs b/pkgs/pkc/src/bls_ietf/sk.rs index f5b11ff0..77804b5b 100644 --- a/pkgs/pkc/src/bls_ietf/sk.rs +++ b/pkgs/pkc/src/bls_ietf/sk.rs @@ -8,7 +8,7 @@ use super::pk::PublicKey; use super::sig::Signature; -use super::{DST, DST_POP, DST_POP_PROVE}; +use crate::bls::scheme_ietf::{DST_BASIC, DST_POP, DST_POP_PROVE}; use crate::bls::scheme_ops::BlsScheme; use crate::bls::{BlsError, BlsScIetf, BlsSkBytes, BLS_SK_LEN}; @@ -74,7 +74,7 @@ impl SecretKey { /// Sign with a specific scheme. pub fn sign_with(&self, msg: &[u8], scheme: Scheme) -> Signature { let dst = match scheme { - Scheme::Basic => DST, + Scheme::Basic => DST_BASIC, Scheme::ProofOfPossession => DST_POP, }; Signature::from_inner(self.0.sign(msg, dst, &[])) From a700cfe203f3316a8eee3c40496bdd4b5b3dd7ad Mon Sep 17 00:00:00 2001 From: Kittywhiskers Van Gogh <63189531+kwvg@users.noreply.github.com> Date: Thu, 6 Aug 2026 06:30:43 +0530 Subject: [PATCH 04/14] pkc%refac(bls): move DH exchange onto the secret key --- pkgs/pkc/src/bls_chia/mod.rs | 6 +++--- pkgs/pkc/src/bls_chia/pk.rs | 11 ----------- pkgs/pkc/src/bls_chia/sk.rs | 10 ++++++++++ pkgs/pkc/src/bls_ietf/mod.rs | 6 +++--- pkgs/pkc/src/bls_ietf/pk.rs | 11 ----------- pkgs/pkc/src/bls_ietf/sk.rs | 10 ++++++++++ pkgs/pkc/src/common/bls/contract.rs | 2 +- pkgs/pkc/tests/bls_chia_dh.rs | 8 ++++---- pkgs/pkc/tests/bls_ietf_dh.rs | 8 ++++---- 9 files changed, 35 insertions(+), 37 deletions(-) diff --git a/pkgs/pkc/src/bls_chia/mod.rs b/pkgs/pkc/src/bls_chia/mod.rs index edc236ef..1523a391 100644 --- a/pkgs/pkc/src/bls_chia/mod.rs +++ b/pkgs/pkc/src/bls_chia/mod.rs @@ -45,6 +45,9 @@ const _: () = { fn sign(&self, msg: &[u8; 32]) -> Signature { self.sign(msg) } + fn dh_exchange(&self, peer_pk: &PublicKey) -> Result { + self.dh_exchange(peer_pk) + } } impl BlsPublicKey for PublicKey { type Error = BlsError; @@ -55,9 +58,6 @@ const _: () = { fn to_bytes(&self) -> [u8; 48] { self.to_bytes() } - fn dh_exchange(sk: &SecretKey, pk: &Self) -> Result { - PublicKey::dh_exchange(sk, pk) - } } impl BlsSignature for Signature { type Error = BlsError; diff --git a/pkgs/pkc/src/bls_chia/pk.rs b/pkgs/pkc/src/bls_chia/pk.rs index a68ce6b3..03e5e4fe 100644 --- a/pkgs/pkc/src/bls_chia/pk.rs +++ b/pkgs/pkc/src/bls_chia/pk.rs @@ -6,7 +6,6 @@ //! Legacy BLS public key (48-byte G1 point, legacy serialization). -use super::sk::SecretKey; use crate::bls::blst_ffi::G1Affine; use crate::bls::scheme_ops::BlsScheme; use crate::bls::{BlsError, BlsPkBytes, BlsScChia}; @@ -41,16 +40,6 @@ impl PublicKey { pub fn to_bytes(&self) -> [u8; 48] { BlsScChia::pk_to_bytes(&self.0) } - - /// Compute a DH shared key: `sk * peer_pk`. - /// - /// # Errors - /// - /// Infallible for the legacy scheme; the `Result` mirrors the shared - /// scheme signature and always returns `Ok`. - pub fn dh_exchange(sk: &SecretKey, peer_pk: &PublicKey) -> Result { - BlsScChia::dh_exchange(&sk.0, &peer_pk.0).map(Self) - } } crate::common::bls::impl_hash_via_bytes!(PublicKey); diff --git a/pkgs/pkc/src/bls_chia/sk.rs b/pkgs/pkc/src/bls_chia/sk.rs index 772baa8c..040b7a9c 100644 --- a/pkgs/pkc/src/bls_chia/sk.rs +++ b/pkgs/pkc/src/bls_chia/sk.rs @@ -65,6 +65,16 @@ impl SecretKey { pub fn sign(&self, msg: &[u8; 32]) -> Signature { Signature::from_inner(BlsScChia::sign(&self.0, msg)) } + + /// Compute a DH shared key: `self * peer_pk`. + /// + /// # Errors + /// + /// Infallible for the legacy scheme; the `Result` mirrors the shared + /// scheme signature and always returns `Ok`. + pub fn dh_exchange(&self, peer_pk: &PublicKey) -> Result { + BlsScChia::dh_exchange(&self.0, &peer_pk.0).map(PublicKey::from_inner) + } } impl Drop for SecretKey { diff --git a/pkgs/pkc/src/bls_ietf/mod.rs b/pkgs/pkc/src/bls_ietf/mod.rs index 0bc37377..0d6becae 100644 --- a/pkgs/pkc/src/bls_ietf/mod.rs +++ b/pkgs/pkc/src/bls_ietf/mod.rs @@ -46,6 +46,9 @@ const _: () = { fn sign(&self, msg: &[u8]) -> Signature { self.sign(msg) } + fn dh_exchange(&self, peer_pk: &PublicKey) -> Result { + self.dh_exchange(peer_pk) + } } impl BlsPublicKey for PublicKey { type Error = BlsError; @@ -56,9 +59,6 @@ const _: () = { fn to_bytes(&self) -> [u8; 48] { self.to_bytes() } - fn dh_exchange(sk: &SecretKey, pk: &Self) -> Result { - PublicKey::dh_exchange(sk, pk) - } } impl BlsSignature for Signature { type Error = BlsError; diff --git a/pkgs/pkc/src/bls_ietf/pk.rs b/pkgs/pkc/src/bls_ietf/pk.rs index 9935f8d5..0d4bdaec 100644 --- a/pkgs/pkc/src/bls_ietf/pk.rs +++ b/pkgs/pkc/src/bls_ietf/pk.rs @@ -7,7 +7,6 @@ //! IETF BLS public key (48-byte compressed G1 point). use super::sig::Signature; -use super::sk::SecretKey; use crate::bls::scheme_ietf::DST_POP_PROVE; use crate::bls::scheme_ops::{verify_ok, BlsScheme}; use crate::bls::{BlsError, BlsPkBytes, BlsScIetf}; @@ -44,16 +43,6 @@ impl PublicKey { BlsScIetf::pk_to_bytes(&self.0) } - /// Compute a DH shared key: `sk * peer_pk`. - /// - /// # Errors - /// - /// Returns [`BlsError::InvalidPublicKey`] when `peer_pk` or the resulting - /// point is not a valid public key. - pub fn dh_exchange(sk: &SecretKey, peer_pk: &PublicKey) -> Result { - BlsScIetf::dh_exchange(&sk.0, &peer_pk.0).map(Self) - } - /// Verify a proof of possession against this key. /// /// # Errors diff --git a/pkgs/pkc/src/bls_ietf/sk.rs b/pkgs/pkc/src/bls_ietf/sk.rs index 77804b5b..af6f719c 100644 --- a/pkgs/pkc/src/bls_ietf/sk.rs +++ b/pkgs/pkc/src/bls_ietf/sk.rs @@ -80,6 +80,16 @@ impl SecretKey { Signature::from_inner(self.0.sign(msg, dst, &[])) } + /// Compute a DH shared key: `self * peer_pk`. + /// + /// # Errors + /// + /// Returns [`BlsError::InvalidPublicKey`] when `peer_pk` or the resulting + /// point is not a valid public key. + pub fn dh_exchange(&self, peer_pk: &PublicKey) -> Result { + BlsScIetf::dh_exchange(&self.0, &peer_pk.0).map(PublicKey::from_inner) + } + /// Produce a proof of possession by signing the serialized public key with /// the PoP DST. pub fn prove_possession(&self) -> Signature { diff --git a/pkgs/pkc/src/common/bls/contract.rs b/pkgs/pkc/src/common/bls/contract.rs index 4ed0e874..291b9f34 100644 --- a/pkgs/pkc/src/common/bls/contract.rs +++ b/pkgs/pkc/src/common/bls/contract.rs @@ -26,6 +26,7 @@ pub(crate) trait BlsSecretKey: Clone + Sized { fn to_bytes(&self) -> [u8; 32]; fn public_key(&self) -> Self::PublicKey; fn sign(&self, msg: &Self::Msg) -> Self::Signature; + fn dh_exchange(&self, peer_pk: &Self::PublicKey) -> Result; } /// Marker trait asserting the minimum BLS public key API. @@ -35,7 +36,6 @@ pub(crate) trait BlsPublicKey: Clone + Sized { fn from_bytes(bytes: &[u8; 48]) -> Result; fn to_bytes(&self) -> [u8; 48]; - fn dh_exchange(sk: &Self::SecretKey, peer_pk: &Self) -> Result; } /// Marker trait asserting the minimum BLS signature API. diff --git a/pkgs/pkc/tests/bls_chia_dh.rs b/pkgs/pkc/tests/bls_chia_dh.rs index b56b0831..3f3e808c 100644 --- a/pkgs/pkc/tests/bls_chia_dh.rs +++ b/pkgs/pkc/tests/bls_chia_dh.rs @@ -19,15 +19,15 @@ fn dh_exchange_roundtrip(chia_sk0: SecretKey, chia_sk1: SecretKey) { let pk0 = chia_sk0.public_key(); let pk1 = chia_sk1.public_key(); // sk0 * pk1 == sk1 * pk0 - let shared_a = PublicKey::dh_exchange(&chia_sk0, &pk1).unwrap(); - let shared_b = PublicKey::dh_exchange(&chia_sk1, &pk0).unwrap(); + let shared_a = chia_sk0.dh_exchange(&pk1).unwrap(); + let shared_b = chia_sk1.dh_exchange(&pk0).unwrap(); assert_eq!(shared_a.to_bytes(), shared_b.to_bytes()); } /// Reference vectors through the public wrapper. /// /// The scheme-level KAT pins `dh_exchange` on the trait; this pins that -/// `PublicKey::dh_exchange` is still wired to it. +/// `SecretKey::dh_exchange` is still wired to it. mod kat { use dash_dev::{arr_from_hex, Corpus}; use hex_conservative::DisplayHex; @@ -46,7 +46,7 @@ mod kat { for v in corpus.vectors::("dh_exchange") { let sk = super::SecretKey::from_bytes(&arr_from_hex(&v.sk)).unwrap(); let peer = super::PublicKey::from_bytes(&arr_from_hex(&v.peer_pk)).unwrap(); - let shared = super::PublicKey::dh_exchange(&sk, &peer).unwrap(); + let shared = sk.dh_exchange(&peer).unwrap(); assert_eq!(shared.to_bytes().to_lower_hex_string(), v.shared); } } diff --git a/pkgs/pkc/tests/bls_ietf_dh.rs b/pkgs/pkc/tests/bls_ietf_dh.rs index 6cf57891..c96fc25e 100644 --- a/pkgs/pkc/tests/bls_ietf_dh.rs +++ b/pkgs/pkc/tests/bls_ietf_dh.rs @@ -19,15 +19,15 @@ fn dh_exchange_roundtrip(ietf_sk0: SecretKey, ietf_sk1: SecretKey) { let pk0 = ietf_sk0.public_key(); let pk1 = ietf_sk1.public_key(); // sk0 * pk1 == sk1 * pk0 - let shared_a = PublicKey::dh_exchange(&ietf_sk0, &pk1).unwrap(); - let shared_b = PublicKey::dh_exchange(&ietf_sk1, &pk0).unwrap(); + let shared_a = ietf_sk0.dh_exchange(&pk1).unwrap(); + let shared_b = ietf_sk1.dh_exchange(&pk0).unwrap(); assert_eq!(shared_a.to_bytes(), shared_b.to_bytes()); } /// Reference vectors through the public wrapper. /// /// The scheme-level KAT pins `dh_exchange` on the trait; this pins that -/// `PublicKey::dh_exchange` is still wired to it. +/// `SecretKey::dh_exchange` is still wired to it. mod kat { use dash_dev::{arr_from_hex, Corpus}; use hex_conservative::DisplayHex; @@ -46,7 +46,7 @@ mod kat { for v in corpus.vectors::("dh_exchange") { let sk = super::SecretKey::from_bytes(&arr_from_hex(&v.sk)).unwrap(); let peer = super::PublicKey::from_bytes(&arr_from_hex(&v.peer_pk)).unwrap(); - let shared = super::PublicKey::dh_exchange(&sk, &peer).unwrap(); + let shared = sk.dh_exchange(&peer).unwrap(); assert_eq!(shared.to_bytes().to_lower_hex_string(), v.shared); } } From a16ed539c0a000076a11cb170d2600d9c4553a74 Mon Sep 17 00:00:00 2001 From: Kittywhiskers Van Gogh <63189531+kwvg@users.noreply.github.com> Date: Sun, 9 Aug 2026 12:48:45 +0530 Subject: [PATCH 05/14] pkc%refac(bls): open the scheme layer to generic wrappers --- pkgs/pkc/src/bls/blst_ffi.rs | 8 ++++---- pkgs/pkc/src/bls/scheme_ops.rs | 2 +- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/pkgs/pkc/src/bls/blst_ffi.rs b/pkgs/pkc/src/bls/blst_ffi.rs index 411dea95..14aa1731 100644 --- a/pkgs/pkc/src/bls/blst_ffi.rs +++ b/pkgs/pkc/src/bls/blst_ffi.rs @@ -342,7 +342,7 @@ pub(crate) trait Point: Copy + Default + Add { /// A point of the G1 group (over `Fp`) in projective coordinates, /// suitable for accumulation before a single conversion to affine. #[derive(Clone, Copy, Debug, Default, Unencodable)] -pub(crate) struct G1(blst_p1); +pub struct G1(blst_p1); impl G1 { /// Convert to affine coordinates. @@ -381,7 +381,7 @@ type_cvrt!(From for G1, |raw| Self(*raw)); /// A point of the G1 group in affine coordinates, the canonical form /// used for serialization and pairing inputs. #[derive(Clone, Copy, Debug, Default, Eq, PartialEq, Unencodable)] -pub(crate) struct G1Affine(blst_p1_affine); +pub struct G1Affine(blst_p1_affine); impl G1Affine { /// The conventional G1 generator. @@ -426,7 +426,7 @@ type_cvrt!(From for G1Affine, |raw| Self(*raw)); /// A point of the G2 group (over `Fp2`) in projective coordinates, /// suitable for accumulation before a single conversion to affine. #[derive(Clone, Copy, Debug, Default, Unencodable)] -pub(crate) struct G2(blst_p2); +pub struct G2(blst_p2); impl G2 { /// The conventional G2 generator. @@ -487,7 +487,7 @@ type_cvrt!(From for blst_p2, |g| g.0); /// A point of the G2 group in affine coordinates, the canonical form /// used for serialization and pairing inputs. #[derive(Clone, Copy, Debug, Default, Eq, PartialEq, Unencodable)] -pub(crate) struct G2Affine(blst_p2_affine); +pub struct G2Affine(blst_p2_affine); impl G2Affine { /// Construct from affine `x` and `y` coordinates in `Fp2`. diff --git a/pkgs/pkc/src/bls/scheme_ops.rs b/pkgs/pkc/src/bls/scheme_ops.rs index 519e9799..009e4f5c 100644 --- a/pkgs/pkc/src/bls/scheme_ops.rs +++ b/pkgs/pkc/src/bls/scheme_ops.rs @@ -34,7 +34,7 @@ pub(crate) fn verify_ok(result: BLST_ERROR) -> Result<(), BlsError> { } /// BLS operations tied to a specific scheme. -pub(crate) trait BlsScheme: BlsSchemeId { +pub trait BlsScheme: BlsSchemeId { /// Inner secret key representation. type InnerSk: Clone; /// Inner public key representation. From 62b567e7b62db3797814fb0a03dfedd9d586e1a4 Mon Sep 17 00:00:00 2001 From: Kittywhiskers Van Gogh <63189531+kwvg@users.noreply.github.com> Date: Mon, 10 Aug 2026 13:13:35 +0530 Subject: [PATCH 06/14] pkc%feat(bls): migrate public key operations to `BlsPublicKey` --- pkgs/pkc/bench/bls_chia.rs | 8 +- pkgs/pkc/bench/bls_ietf.rs | 10 +- pkgs/pkc/src/bls/mod.rs | 4 + pkgs/pkc/src/bls/public_ops.rs | 212 +++++++++++++++++++++++++++ pkgs/pkc/src/bls_chia/agg.rs | 8 +- pkgs/pkc/src/bls_chia/mod.rs | 19 +-- pkgs/pkc/src/bls_chia/pk.rs | 59 -------- pkgs/pkc/src/bls_chia/sig.rs | 2 +- pkgs/pkc/src/bls_chia/sk.rs | 2 +- pkgs/pkc/src/bls_chia/threshold.rs | 2 +- pkgs/pkc/src/bls_ietf/agg.rs | 8 +- pkgs/pkc/src/bls_ietf/mod.rs | 19 +-- pkgs/pkc/src/bls_ietf/pk.rs | 71 --------- pkgs/pkc/src/bls_ietf/sig.rs | 16 +- pkgs/pkc/src/bls_ietf/sk.rs | 2 +- pkgs/pkc/src/bls_ietf/threshold.rs | 2 +- pkgs/pkc/src/common/bls/contract.rs | 9 -- pkgs/pkc/tests/bls_chia_aggregate.rs | 30 +--- pkgs/pkc/tests/bls_chia_llmq.rs | 4 +- pkgs/pkc/tests/bls_ietf_aggregate.rs | 34 +---- pkgs/pkc/tests/bls_ietf_llmq.rs | 4 +- 21 files changed, 262 insertions(+), 263 deletions(-) create mode 100644 pkgs/pkc/src/bls/public_ops.rs delete mode 100644 pkgs/pkc/src/bls_chia/pk.rs delete mode 100644 pkgs/pkc/src/bls_ietf/pk.rs diff --git a/pkgs/pkc/bench/bls_chia.rs b/pkgs/pkc/bench/bls_chia.rs index 83a49d28..9f63a78a 100644 --- a/pkgs/pkc/bench/bls_chia.rs +++ b/pkgs/pkc/bench/bls_chia.rs @@ -8,7 +8,7 @@ use common::*; use dash_pkc::bls::tests as common; -use dash_pkc::bls_chia::{aggregate_pk, aggregate_sig, verify_aggregates, PublicKey, SecretKey, Signature}; +use dash_pkc::bls_chia::{aggregate_sig, verify_aggregates, PublicKey, SecretKey, Signature}; /// Single signature creation (legacy hash-to-G2). #[divan::bench] @@ -40,7 +40,7 @@ fn aggregate_pk_n(bencher: divan::Bencher, n: usize) { let pk_refs: Vec<_> = pks.iter().collect(); bencher .counter(divan::counter::ItemsCount::new(n)) - .bench(|| aggregate_pk(&pk_refs)); + .bench(|| PublicKey::aggregate(&pk_refs)); } /// Signature aggregation at various batch sizes. @@ -151,7 +151,7 @@ fn recover_threshold(bencher: divan::Bencher, t: usize) { #[cfg(feature = "std")] mod worker_benches { - use dash_pkc::bls_chia::{aggregate_pk, PublicKey, SecretKey, Signature}; + use dash_pkc::bls_chia::{PublicKey, SecretKey, Signature}; use dash_pkc::worker; fn setup_sigs(n: usize) -> Vec<(Signature, PublicKey, [u8; 32])> { @@ -181,6 +181,6 @@ mod worker_benches { .collect(); bencher .counter(divan::counter::ItemsCount::new(n)) - .bench(|| worker::par_reduce(pks.clone(), |a, b| aggregate_pk(&[&a, &b]).unwrap())); + .bench(|| worker::par_reduce(pks.clone(), |a, b| PublicKey::aggregate(&[&a, &b]).unwrap())); } } diff --git a/pkgs/pkc/bench/bls_ietf.rs b/pkgs/pkc/bench/bls_ietf.rs index ae02e334..b6cc036b 100644 --- a/pkgs/pkc/bench/bls_ietf.rs +++ b/pkgs/pkc/bench/bls_ietf.rs @@ -8,9 +8,7 @@ use common::*; use dash_pkc::bls::tests as common; -use dash_pkc::bls_ietf::{ - aggregate_pk, aggregate_sig, fast_verify_aggregates, verify_aggregates, PublicKey, SecretKey, Signature, -}; +use dash_pkc::bls_ietf::{aggregate_sig, fast_verify_aggregates, verify_aggregates, PublicKey, SecretKey, Signature}; /// Single signature creation. #[divan::bench] @@ -42,7 +40,7 @@ fn aggregate_pk_n(bencher: divan::Bencher, n: usize) { let pk_refs: Vec<_> = pks.iter().collect(); bencher .counter(divan::counter::ItemsCount::new(n)) - .bench(|| aggregate_pk(&pk_refs)); + .bench(|| PublicKey::aggregate(&pk_refs)); } /// Signature aggregation at various batch sizes. @@ -187,7 +185,7 @@ fn verify_pop(bencher: divan::Bencher) { #[cfg(feature = "std")] mod worker_benches { - use dash_pkc::bls_ietf::{aggregate_pk, PublicKey, SecretKey, Signature}; + use dash_pkc::bls_ietf::{PublicKey, SecretKey, Signature}; use dash_pkc::worker; fn setup_sigs(n: usize) -> Vec<(Signature, PublicKey, Vec)> { @@ -217,6 +215,6 @@ mod worker_benches { .collect(); bencher .counter(divan::counter::ItemsCount::new(n)) - .bench(|| worker::par_reduce(pks.clone(), |a, b| aggregate_pk(&[&a, &b]).unwrap())); + .bench(|| worker::par_reduce(pks.clone(), |a, b| PublicKey::aggregate(&[&a, &b]).unwrap())); } } diff --git a/pkgs/pkc/src/bls/mod.rs b/pkgs/pkc/src/bls/mod.rs index 0df88c11..ccaa698b 100644 --- a/pkgs/pkc/src/bls/mod.rs +++ b/pkgs/pkc/src/bls/mod.rs @@ -22,6 +22,7 @@ pub use sig_id::BlsSigId; cfg_if::cfg_if! { if #[cfg(feature = "bls")] { + mod public_ops; mod scheme_chia; #[expect(unsafe_code, reason = "blst C FFI")] pub(crate) mod blst_ffi; @@ -33,5 +34,8 @@ cfg_if::cfg_if! { #[doc(hidden)] #[expect(clippy::unwrap_used, reason = "test support code")] pub mod tests; + + pub use public_ops::BlsPublicKey; + pub use scheme_ops::BlsScheme; } } diff --git a/pkgs/pkc/src/bls/public_ops.rs b/pkgs/pkc/src/bls/public_ops.rs new file mode 100644 index 00000000..26532410 --- /dev/null +++ b/pkgs/pkc/src/bls/public_ops.rs @@ -0,0 +1,212 @@ +// +// Copyright (c) 2026-present, The Dash Core developers +// SPDX-License-Identifier: MIT +// See the accompanying file LICENSE or https://opensource.org/license/MIT +// + +//! Scheme-generic BLS public key. + +use super::error::BlsError; +use super::scheme_ops::BlsScheme; +use super::{BlsPkBytes, BLS_PK_LEN}; +use crate::prelude::*; + +use dash_num::Hash256; +use dash_types::codec::TypeId; +use dash_types::{dlgt_codec, qtypestr, type_cvrt}; +use hex_conservative::DisplayHex; + +use core::any::type_name; +use core::fmt::{Debug, Formatter, Result as FmtResult}; +use core::hash::{Hash, Hasher}; + +/// A BLS public key (48-byte compressed G1 point) +#[cfg_attr(feature = "serde", derive(::serde::Serialize, ::serde::Deserialize))] +#[cfg_attr(feature = "serde", serde(into = "BlsPkBytes", try_from = "BlsPkBytes",))] +#[cfg_attr(feature = "serde", serde(bound(serialize = "", deserialize = "")))] +pub struct BlsPublicKey(pub(crate) S::InnerPk); + +dlgt_codec!(for[S: BlsScheme] BlsPublicKey => BlsPkBytes, Hash256, BlsError, BLS_PK_LEN); + +impl BlsPublicKey { + /// Deserialize from 48 bytes. + /// + /// # Errors + /// + /// Returns `InvalidPublicKey` when the bytes are not a valid point. + pub fn from_bytes(bytes: &[u8; 48]) -> Result { + S::pk_from_bytes(bytes).map(Self) + } + + /// Serialize to 48 bytes. + pub fn to_bytes(&self) -> [u8; 48] { + S::pk_to_bytes(&self.0) + } + + /// Aggregate multiple public keys into one. + /// + /// # Errors + /// + /// Returns `EmptyAggregation` when no keys are given, or `InvalidPublicKey` + /// when a key fails to aggregate. + pub fn aggregate(keys: &[&Self]) -> Result { + let inner_refs: Vec<&S::InnerPk> = keys.iter().map(|k| &k.0).collect(); + S::aggregate_pk(&inner_refs).map(Self::from_inner) + } + + pub(crate) fn from_inner(inner: S::InnerPk) -> Self { + Self(inner) + } +} + +impl Clone for BlsPublicKey { + fn clone(&self) -> Self { + Self(self.0.clone()) + } +} + +impl Debug for BlsPublicKey { + fn fmt(&self, f: &mut Formatter<'_>) -> FmtResult { + qtypestr(f, type_name::())?; + write!(f, "({})", self.to_bytes().as_hex()) + } +} + +impl Eq for BlsPublicKey {} + +impl Hash for BlsPublicKey { + fn hash(&self, state: &mut H) { + self.to_bytes().hash(state); + } +} + +impl PartialEq for BlsPublicKey { + fn eq(&self, other: &Self) -> bool { + self.0 == other.0 + } +} + +impl TypeId for BlsPublicKey { + const TYPE_ID: u32 = S::PK_TYPE_ID; +} + +type_cvrt!(for[S: BlsScheme] From> for BlsPkBytes, |pk| { + Self::from_bytes(pk.to_bytes()) +}); + +type_cvrt!(for[S: BlsScheme] TryFrom> for BlsPublicKey, BlsError, |bytes| { + Self::from_bytes(bytes.as_bytes()) +}); + +#[cfg(all(test, feature = "tests"))] +#[expect(clippy::unwrap_used, reason = "test code")] +mod tests { + use super::*; + use crate::bls::tests::SEED_0; + use crate::bls::{BlsScChia, BlsScIetf}; + + use cfg_if::cfg_if; + use dash_dev::{arr_from_hex, Corpus}; + use hex_conservative::DisplayHex; + use rstest::rstest; + use serde::Deserialize; + + #[derive(Deserialize)] + struct PkSerVec { + pk_legacy: String, + pk_ietf: String, + } + + #[derive(Deserialize)] + struct AggPkVec { + pks: Vec, + agg_pk: String, + } + + fn assert_pk_roundtrip() { + let pk = BlsPublicKey::::from_inner(S::derive_pk(&S::generate(&SEED_0).unwrap())); + let bytes = pk.to_bytes(); + assert_eq!(BlsPublicKey::::from_bytes(&bytes).unwrap().to_bytes(), bytes); + } + + #[rstest] + #[case::chia(assert_pk_roundtrip::)] + #[case::ietf(assert_pk_roundtrip::)] + fn serialization_roundtrip(#[case] assertion: fn()) { + assertion(); + } + + /// The legacy decoder rejects the infinity marker rather than yielding an + /// identity public key. + #[rstest] + fn chia_rejects_identity_public_key() { + let mut infinity = [0u8; 48]; + infinity[0] = 0xc0; + assert!(BlsPublicKey::::from_bytes(&infinity).is_err()); + } + + /// The legacy decoder normalizes stray high bits, so a mutated encoding + /// round-trips back to its canonical form. + #[rstest] + fn chia_masks_stray_public_key_bits() { + let clean = BlsScChia::pk_to_bytes(&BlsScChia::derive_pk(&BlsScChia::generate(&SEED_0).unwrap())); + + let mut mutated = clean; + mutated[0] |= 0x20; + let decoded = BlsPublicKey::::from_bytes(&mutated).unwrap(); + assert_eq!(decoded.to_bytes(), clean); + } + + /// The same G1 point encodes differently under the two schemes, and the + /// legacy encoding must round-trip through the wrapper. + #[rstest] + fn serialization_formats_match_vectors() { + let corpus = Corpus::open(env!("CARGO_MANIFEST_DIR"), "bls_chia_ser_internals"); + let vecs: Vec = corpus.vectors("pk_serialization"); + + for v in &vecs { + let legacy = BlsPublicKey::::from_bytes(&arr_from_hex(&v.pk_legacy)).unwrap(); + assert_eq!(legacy.to_bytes().to_lower_hex_string(), v.pk_legacy); + + assert_ne!(v.pk_legacy, v.pk_ietf, "legacy and ietf should differ"); + } + } + + fn assert_aggregate_vectors(corpus: &str) { + let corpus = Corpus::open(env!("CARGO_MANIFEST_DIR"), corpus); + let vecs: Vec = corpus.vectors("aggregate_pk"); + + for v in &vecs { + let pks: Vec> = v + .pks + .iter() + .map(|pk| BlsPublicKey::::from_bytes(&arr_from_hex(pk)).unwrap()) + .collect(); + let refs: Vec<&BlsPublicKey> = pks.iter().collect(); + let agg = BlsPublicKey::::aggregate(&refs).unwrap(); + assert_eq!(agg.to_bytes().to_lower_hex_string(), v.agg_pk); + } + } + + #[rstest] + #[case::chia(assert_aggregate_vectors::, "bls_chia_aggregate")] + #[case::ietf(assert_aggregate_vectors::, "bls_ietf_aggregate")] + fn aggregate_matches_vectors(#[case] assertion: fn(&str), #[case] corpus: &str) { + assertion(corpus); + } + + cfg_if! { + if #[cfg(feature = "serde")] { + use dash_dev::assert_json_rt; + + #[rstest] + fn serde_roundtrip() { + let corpus = Corpus::open(env!("CARGO_MANIFEST_DIR"), "bls_chia_ser_internals"); + let v = corpus.vectors::("pk_serialization").swap_remove(0); + + assert_json_rt(&BlsPublicKey::::from_bytes(&arr_from_hex(&v.pk_legacy)).unwrap()); + assert_json_rt(&BlsPublicKey::::from_bytes(&arr_from_hex(&v.pk_ietf)).unwrap()); + } + } + } +} diff --git a/pkgs/pkc/src/bls_chia/agg.rs b/pkgs/pkc/src/bls_chia/agg.rs index c5e23a42..7ff8b9df 100644 --- a/pkgs/pkc/src/bls_chia/agg.rs +++ b/pkgs/pkc/src/bls_chia/agg.rs @@ -6,19 +6,13 @@ //! Aggregation and secure verification for legacy BLS. -use super::pk::PublicKey; use super::sig::Signature; use super::sk::SecretKey; +use super::PublicKey; use crate::bls::scheme_ops::BlsScheme; use crate::bls::{BlsError, BlsScChia}; use crate::prelude::*; -/// Aggregate multiple legacy BLS public keys (simple point addition in G1). -pub fn aggregate_pk(keys: &[&PublicKey]) -> Result { - let inner: Vec<_> = keys.iter().map(|key| &key.0).collect(); - BlsScChia::aggregate_pk(&inner).map(PublicKey::from_inner) -} - /// Aggregate multiple legacy BLS signatures (simple point addition in G2). pub fn aggregate_sig(sigs: &[&Signature]) -> Result { let inner: Vec<_> = sigs.iter().map(|sig| &sig.0).collect(); diff --git a/pkgs/pkc/src/bls_chia/mod.rs b/pkgs/pkc/src/bls_chia/mod.rs index 1523a391..a9c035c3 100644 --- a/pkgs/pkc/src/bls_chia/mod.rs +++ b/pkgs/pkc/src/bls_chia/mod.rs @@ -7,7 +7,6 @@ //! Legacy BLS signatures (non-standard hash-to-G2, min-pubkey-size). mod agg; -mod pk; mod sig; mod sk; @@ -15,13 +14,13 @@ pub mod threshold; pub use crate::bls::BlsError; -pub use agg::{ - aggregate_pk, aggregate_sig, aggregate_sk, fast_verify_aggregates, secure_verify_aggregates, verify_aggregates, -}; -pub use pk::PublicKey; +pub use agg::{aggregate_sig, aggregate_sk, fast_verify_aggregates, secure_verify_aggregates, verify_aggregates}; pub use sig::Signature; pub use sk::SecretKey; +/// A legacy BLS public key (48-byte G1 point in legacy serialization). +pub type PublicKey = crate::bls::BlsPublicKey; + // Compile-time contract: must match bls_ietf's shared API surface. const _: () = { use crate::common::bls::contract::*; @@ -49,16 +48,6 @@ const _: () = { self.dh_exchange(peer_pk) } } - impl BlsPublicKey for PublicKey { - type Error = BlsError; - type SecretKey = SecretKey; - fn from_bytes(b: &[u8; 48]) -> Result { - PublicKey::from_bytes(b) - } - fn to_bytes(&self) -> [u8; 48] { - self.to_bytes() - } - } impl BlsSignature for Signature { type Error = BlsError; type PublicKey = PublicKey; diff --git a/pkgs/pkc/src/bls_chia/pk.rs b/pkgs/pkc/src/bls_chia/pk.rs deleted file mode 100644 index 03e5e4fe..00000000 --- a/pkgs/pkc/src/bls_chia/pk.rs +++ /dev/null @@ -1,59 +0,0 @@ -// -// Copyright (c) 2026-present, The Dash Core developers -// SPDX-License-Identifier: MIT -// See the accompanying file LICENSE or https://opensource.org/license/MIT -// - -//! Legacy BLS public key (48-byte G1 point, legacy serialization). - -use crate::bls::blst_ffi::G1Affine; -use crate::bls::scheme_ops::BlsScheme; -use crate::bls::{BlsError, BlsPkBytes, BlsScChia}; - -use dash_types::Unencodable; - -/// A legacy BLS public key (48-byte G1 point in legacy serialization). -#[derive(Clone, Debug, Eq, PartialEq, Unencodable)] -#[cfg_attr(feature = "serde", derive(::serde::Serialize, ::serde::Deserialize))] -#[cfg_attr( - feature = "serde", - serde(into = "BlsPkBytes", try_from = "BlsPkBytes",) -)] -pub struct PublicKey(pub(super) G1Affine); - -impl PublicKey { - pub(super) fn from_inner(inner: G1Affine) -> Self { - Self(inner) - } - - /// Deserialize from 48 legacy-format bytes. - /// - /// # Errors - /// - /// Returns [`BlsError::InvalidPublicKey`] when the bytes do not decode to a - /// valid public key (identity marker, all-zero buffer, or malformed input). - pub fn from_bytes(bytes: &[u8; 48]) -> Result { - BlsScChia::pk_from_bytes(bytes).map(Self) - } - - /// Serialize to 48 legacy-format bytes. - pub fn to_bytes(&self) -> [u8; 48] { - BlsScChia::pk_to_bytes(&self.0) - } -} - -crate::common::bls::impl_hash_via_bytes!(PublicKey); - -impl From for BlsPkBytes { - fn from(pk: PublicKey) -> Self { - Self::from_bytes(pk.to_bytes()) - } -} - -impl TryFrom> for PublicKey { - type Error = BlsError; - - fn try_from(bytes: BlsPkBytes) -> Result { - Self::from_bytes(bytes.as_bytes()) - } -} diff --git a/pkgs/pkc/src/bls_chia/sig.rs b/pkgs/pkc/src/bls_chia/sig.rs index bc82cb86..fbe7ada8 100644 --- a/pkgs/pkc/src/bls_chia/sig.rs +++ b/pkgs/pkc/src/bls_chia/sig.rs @@ -6,7 +6,7 @@ //! Legacy BLS signature (96-byte G2 point, legacy serialization). -use super::pk::PublicKey; +use super::PublicKey; use crate::bls::blst_ffi::G2Affine; use crate::bls::scheme_ops::BlsScheme; use crate::bls::{BlsError, BlsScChia, BlsSigBytes}; diff --git a/pkgs/pkc/src/bls_chia/sk.rs b/pkgs/pkc/src/bls_chia/sk.rs index 040b7a9c..31bbf253 100644 --- a/pkgs/pkc/src/bls_chia/sk.rs +++ b/pkgs/pkc/src/bls_chia/sk.rs @@ -6,8 +6,8 @@ //! Legacy BLS secret key. -use super::pk::PublicKey; use super::sig::Signature; +use super::PublicKey; use crate::bls::scheme_ops::BlsScheme; use crate::bls::{BlsError, BlsScChia, BlsSkBytes, BLS_SK_LEN}; diff --git a/pkgs/pkc/src/bls_chia/threshold.rs b/pkgs/pkc/src/bls_chia/threshold.rs index 98bc06dd..2a5ba0ed 100644 --- a/pkgs/pkc/src/bls_chia/threshold.rs +++ b/pkgs/pkc/src/bls_chia/threshold.rs @@ -6,9 +6,9 @@ //! Thresholds for legacy scheme (m-of-n secret sharing and signature recovery). -use super::pk::PublicKey; use super::sig::Signature; use super::sk::SecretKey; +use super::PublicKey; use crate::bls::scheme_ops::BlsScheme; use crate::bls::{BlsError, BlsScChia}; use crate::prelude::*; diff --git a/pkgs/pkc/src/bls_ietf/agg.rs b/pkgs/pkc/src/bls_ietf/agg.rs index c5756dbc..351a5e19 100644 --- a/pkgs/pkc/src/bls_ietf/agg.rs +++ b/pkgs/pkc/src/bls_ietf/agg.rs @@ -6,9 +6,9 @@ //! Aggregation and batch verification for IETF BLS. -use super::pk::PublicKey; use super::sig::Signature; use super::sk::SecretKey; +use super::PublicKey; use crate::bls::scheme_ietf::DST_BASIC; use crate::bls::scheme_ops::{verify_ok, BlsScheme}; use crate::bls::{BlsError, BlsScIetf}; @@ -16,12 +16,6 @@ use crate::prelude::*; use blst::min_pk; -/// Aggregate multiple public keys into one. -pub fn aggregate_pk(keys: &[&PublicKey]) -> Result { - let inner: Vec<_> = keys.iter().map(|key| &key.0).collect(); - BlsScIetf::aggregate_pk(&inner).map(PublicKey::from_inner) -} - /// Aggregate multiple signatures into one. pub fn aggregate_sig(sigs: &[&Signature]) -> Result { let inner: Vec<_> = sigs.iter().map(|sig| &sig.0).collect(); diff --git a/pkgs/pkc/src/bls_ietf/mod.rs b/pkgs/pkc/src/bls_ietf/mod.rs index 0d6becae..437d0ead 100644 --- a/pkgs/pkc/src/bls_ietf/mod.rs +++ b/pkgs/pkc/src/bls_ietf/mod.rs @@ -7,7 +7,6 @@ //! IETF BLS12-381 signatures (basic scheme, min-pubkey-size). mod agg; -mod pk; mod sig; mod sk; @@ -15,13 +14,13 @@ pub mod threshold; pub use crate::bls::BlsError; -pub use agg::{ - aggregate_pk, aggregate_sig, aggregate_sk, fast_verify_aggregates, secure_verify_aggregates, verify_aggregates, -}; -pub use pk::PublicKey; +pub use agg::{aggregate_sig, aggregate_sk, fast_verify_aggregates, secure_verify_aggregates, verify_aggregates}; pub use sig::Signature; pub use sk::{Scheme, SecretKey}; +/// An IETF BLS public key (48-byte compressed G1 point). +pub type PublicKey = crate::bls::BlsPublicKey; + // Compile-time contract: if any of these methods are // removed or their signatures change, this block fails. const _: () = { @@ -50,16 +49,6 @@ const _: () = { self.dh_exchange(peer_pk) } } - impl BlsPublicKey for PublicKey { - type Error = BlsError; - type SecretKey = SecretKey; - fn from_bytes(b: &[u8; 48]) -> Result { - PublicKey::from_bytes(b) - } - fn to_bytes(&self) -> [u8; 48] { - self.to_bytes() - } - } impl BlsSignature for Signature { type Error = BlsError; type PublicKey = PublicKey; diff --git a/pkgs/pkc/src/bls_ietf/pk.rs b/pkgs/pkc/src/bls_ietf/pk.rs deleted file mode 100644 index 0d4bdaec..00000000 --- a/pkgs/pkc/src/bls_ietf/pk.rs +++ /dev/null @@ -1,71 +0,0 @@ -// -// Copyright (c) 2026-present, The Dash Core developers -// SPDX-License-Identifier: MIT -// See the accompanying file LICENSE or https://opensource.org/license/MIT -// - -//! IETF BLS public key (48-byte compressed G1 point). - -use super::sig::Signature; -use crate::bls::scheme_ietf::DST_POP_PROVE; -use crate::bls::scheme_ops::{verify_ok, BlsScheme}; -use crate::bls::{BlsError, BlsPkBytes, BlsScIetf}; - -use blst::min_pk; -use dash_types::Unencodable; - -/// A BLS public key (48-byte compressed G1 point). -#[derive(Clone, Debug, Eq, PartialEq, Unencodable)] -#[cfg_attr(feature = "serde", derive(::serde::Serialize, ::serde::Deserialize))] -#[cfg_attr( - feature = "serde", - serde(into = "BlsPkBytes", try_from = "BlsPkBytes",) -)] -pub struct PublicKey(pub(super) min_pk::PublicKey); - -impl PublicKey { - pub(super) fn from_inner(inner: min_pk::PublicKey) -> Self { - Self(inner) - } - - /// Deserialize from 48 compressed bytes. - /// - /// # Errors - /// - /// Returns [`BlsError::InvalidPublicKey`] when the bytes are not a valid - /// encoding or the point fails `validate` (identity or non-prime-order). - pub fn from_bytes(bytes: &[u8; 48]) -> Result { - BlsScIetf::pk_from_bytes(bytes).map(Self) - } - - /// Serialize to 48 compressed bytes. - pub fn to_bytes(&self) -> [u8; 48] { - BlsScIetf::pk_to_bytes(&self.0) - } - - /// Verify a proof of possession against this key. - /// - /// # Errors - /// - /// Returns [`BlsError::VerifyFailed`] if the proof does not verify. - pub fn verify_possession(&self, pop: &Signature) -> Result<(), BlsError> { - let pk_bytes = self.to_bytes(); - verify_ok(pop.0.verify(true, &pk_bytes, DST_POP_PROVE, &[], &self.0, true)) - } -} - -crate::common::bls::impl_hash_via_bytes!(PublicKey); - -impl From for BlsPkBytes { - fn from(pk: PublicKey) -> Self { - Self::from_bytes(pk.to_bytes()) - } -} - -impl TryFrom> for PublicKey { - type Error = BlsError; - - fn try_from(bytes: BlsPkBytes) -> Result { - Self::from_bytes(bytes.as_bytes()) - } -} diff --git a/pkgs/pkc/src/bls_ietf/sig.rs b/pkgs/pkc/src/bls_ietf/sig.rs index 346b195c..023cd5f1 100644 --- a/pkgs/pkc/src/bls_ietf/sig.rs +++ b/pkgs/pkc/src/bls_ietf/sig.rs @@ -6,9 +6,9 @@ //! IETF BLS signature (96-byte compressed G2 point). -use super::pk::PublicKey; use super::sk::Scheme; -use crate::bls::scheme_ietf::{DST_BASIC, DST_POP}; +use super::PublicKey; +use crate::bls::scheme_ietf::{DST_BASIC, DST_POP, DST_POP_PROVE}; use crate::bls::scheme_ops::{verify_ok, BlsScheme}; use crate::bls::{BlsError, BlsScIetf, BlsSigBytes}; @@ -71,6 +71,18 @@ impl Signature { } } +impl PublicKey { + /// Verify a proof of possession against this key. + /// + /// # Errors + /// + /// Returns [`BlsError::VerifyFailed`] if the proof does not verify. + pub fn verify_possession(&self, pop: &Signature) -> Result<(), BlsError> { + let pk_bytes = self.to_bytes(); + verify_ok(pop.0.verify(true, &pk_bytes, DST_POP_PROVE, &[], &self.0, true)) + } +} + crate::common::bls::impl_hash_via_bytes!(Signature); impl From for BlsSigBytes { diff --git a/pkgs/pkc/src/bls_ietf/sk.rs b/pkgs/pkc/src/bls_ietf/sk.rs index af6f719c..ea317bc4 100644 --- a/pkgs/pkc/src/bls_ietf/sk.rs +++ b/pkgs/pkc/src/bls_ietf/sk.rs @@ -6,8 +6,8 @@ //! IETF BLS secret key. -use super::pk::PublicKey; use super::sig::Signature; +use super::PublicKey; use crate::bls::scheme_ietf::{DST_BASIC, DST_POP, DST_POP_PROVE}; use crate::bls::scheme_ops::BlsScheme; use crate::bls::{BlsError, BlsScIetf, BlsSkBytes, BLS_SK_LEN}; diff --git a/pkgs/pkc/src/bls_ietf/threshold.rs b/pkgs/pkc/src/bls_ietf/threshold.rs index cf42038c..0416be42 100644 --- a/pkgs/pkc/src/bls_ietf/threshold.rs +++ b/pkgs/pkc/src/bls_ietf/threshold.rs @@ -6,9 +6,9 @@ //! Thresholds for IETF scheme (m-of-n secret sharing and signature recovery). -use super::pk::PublicKey; use super::sig::Signature; use super::sk::SecretKey; +use super::PublicKey; use crate::bls::scheme_ops::BlsScheme; use crate::bls::{BlsError, BlsScIetf}; use crate::prelude::*; diff --git a/pkgs/pkc/src/common/bls/contract.rs b/pkgs/pkc/src/common/bls/contract.rs index 291b9f34..4e22dff5 100644 --- a/pkgs/pkc/src/common/bls/contract.rs +++ b/pkgs/pkc/src/common/bls/contract.rs @@ -29,15 +29,6 @@ pub(crate) trait BlsSecretKey: Clone + Sized { fn dh_exchange(&self, peer_pk: &Self::PublicKey) -> Result; } -/// Marker trait asserting the minimum BLS public key API. -pub(crate) trait BlsPublicKey: Clone + Sized { - type Error; - type SecretKey; - - fn from_bytes(bytes: &[u8; 48]) -> Result; - fn to_bytes(&self) -> [u8; 48]; -} - /// Marker trait asserting the minimum BLS signature API. pub(crate) trait BlsSignature: Clone + Sized { type Error; diff --git a/pkgs/pkc/tests/bls_chia_aggregate.rs b/pkgs/pkc/tests/bls_chia_aggregate.rs index bb461fc8..259efc6f 100644 --- a/pkgs/pkc/tests/bls_chia_aggregate.rs +++ b/pkgs/pkc/tests/bls_chia_aggregate.rs @@ -10,7 +10,7 @@ use common::*; use dash_pkc::bls::tests as common; -use dash_pkc::bls_chia::{aggregate_pk, aggregate_sig, verify_aggregates, PublicKey, SecretKey, Signature}; +use dash_pkc::bls_chia::{aggregate_sig, verify_aggregates, PublicKey, SecretKey, Signature}; use rstest::*; /// Aggregate then verify over a shared message. @@ -29,7 +29,7 @@ fn aggregate_and_verify(chia_sk0: SecretKey, chia_sk1: SecretKey) { #[rstest] fn aggregate_empty_fails() { let empty_pk: Vec<&dash_pkc::bls_chia::PublicKey> = vec![]; - assert!(aggregate_pk(&empty_pk).is_err()); + assert!(PublicKey::aggregate(&empty_pk).is_err()); let empty_sig: Vec<&Signature> = vec![]; assert!(aggregate_sig(&empty_sig).is_err()); } @@ -112,12 +112,6 @@ mod kat { use hex_conservative::DisplayHex; use serde::Deserialize; - #[derive(Deserialize)] - struct AggregatePkVector { - pks: Vec, - agg_pk: String, - } - #[derive(Deserialize)] struct AggregateSigVector { sigs: Vec, @@ -133,26 +127,6 @@ mod kat { agg_sig_secure: String, } - #[test] - fn kat_aggregate_pk() { - let corpus = Corpus::open(env!("CARGO_MANIFEST_DIR"), "bls_chia_aggregate"); - let vecs: Vec = corpus.vectors("aggregate_pk"); - - for v in &vecs { - let pks: Vec = v - .pks - .iter() - .map(|h| { - let b: [u8; 48] = arr_from_hex(h); - dash_pkc::bls_chia::PublicKey::from_bytes(&b).unwrap() - }) - .collect(); - let pk_refs: Vec<_> = pks.iter().collect(); - let agg = dash_pkc::bls_chia::aggregate_pk(&pk_refs).unwrap(); - assert_eq!(agg.to_bytes().to_lower_hex_string(), v.agg_pk); - } - } - #[test] fn kat_aggregate_sig() { let corpus = Corpus::open(env!("CARGO_MANIFEST_DIR"), "bls_chia_aggregate"); diff --git a/pkgs/pkc/tests/bls_chia_llmq.rs b/pkgs/pkc/tests/bls_chia_llmq.rs index 5008e3a8..3b8d5413 100644 --- a/pkgs/pkc/tests/bls_chia_llmq.rs +++ b/pkgs/pkc/tests/bls_chia_llmq.rs @@ -11,7 +11,7 @@ use dash_dev::{arr_from_hex, Corpus}; use dash_num::Hash256; use dash_pkc::bls::tests as common; -use dash_pkc::bls_chia::{aggregate_pk, aggregate_sig, threshold, PublicKey, SecretKey, Signature}; +use dash_pkc::bls_chia::{aggregate_sig, threshold, PublicKey, SecretKey, Signature}; use hex_conservative::DisplayHex; #[test] @@ -114,7 +114,7 @@ fn llmq_commit_quorum_key() { .map(|c| PublicKey::from_bytes(&arr_from_hex(c["vvec"][0].as_str().unwrap())).unwrap()) .collect(); let pk_refs: Vec<&PublicKey> = member_pks.iter().collect(); - let agg_pk = aggregate_pk(&pk_refs).unwrap(); + let agg_pk = PublicKey::aggregate(&pk_refs).unwrap(); assert_eq!(agg_pk.to_bytes().to_lower_hex_string(), expected_qpk); } diff --git a/pkgs/pkc/tests/bls_ietf_aggregate.rs b/pkgs/pkc/tests/bls_ietf_aggregate.rs index 949840c0..17d5565d 100644 --- a/pkgs/pkc/tests/bls_ietf_aggregate.rs +++ b/pkgs/pkc/tests/bls_ietf_aggregate.rs @@ -10,9 +10,7 @@ use common::*; use dash_pkc::bls::tests as common; -use dash_pkc::bls_ietf::{ - aggregate_pk, aggregate_sig, fast_verify_aggregates, verify_aggregates, PublicKey, SecretKey, Signature, -}; +use dash_pkc::bls_ietf::{aggregate_sig, fast_verify_aggregates, verify_aggregates, PublicKey, SecretKey, Signature}; use hex_conservative::hex; use rstest::*; @@ -21,7 +19,7 @@ use rstest::*; fn aggregate_pk_roundtrip(ietf_sk0: SecretKey, ietf_sk1: SecretKey) { let pk1 = ietf_sk0.public_key(); let pk2 = ietf_sk1.public_key(); - let agg = aggregate_pk(&[&pk1, &pk2]).unwrap(); + let agg = PublicKey::aggregate(&[&pk1, &pk2]).unwrap(); assert_eq!(agg.to_bytes().len(), 48); } @@ -29,7 +27,7 @@ fn aggregate_pk_roundtrip(ietf_sk0: SecretKey, ietf_sk1: SecretKey) { #[rstest] fn aggregate_empty_fails() { let empty_pk: Vec<&dash_pkc::bls_ietf::PublicKey> = vec![]; - assert!(aggregate_pk(&empty_pk).is_err()); + assert!(PublicKey::aggregate(&empty_pk).is_err()); let empty_sig: Vec<&Signature> = vec![]; assert!(aggregate_sig(&empty_sig).is_err()); } @@ -134,12 +132,6 @@ mod kat { use hex_conservative::DisplayHex; use serde::Deserialize; - #[derive(Deserialize)] - struct AggregatePkVector { - pks: Vec, - agg_pk: String, - } - #[derive(Deserialize)] struct AggregateSigVector { sigs: Vec, @@ -161,26 +153,6 @@ mod kat { agg_sig_secure: String, } - #[test] - fn kat_aggregate_pk() { - let corpus = Corpus::open(env!("CARGO_MANIFEST_DIR"), "bls_ietf_aggregate"); - let vecs: Vec = corpus.vectors("aggregate_pk"); - - for v in &vecs { - let pks: Vec = v - .pks - .iter() - .map(|h| { - let b: [u8; 48] = arr_from_hex(h); - dash_pkc::bls_ietf::PublicKey::from_bytes(&b).unwrap() - }) - .collect(); - let pk_refs: Vec<_> = pks.iter().collect(); - let agg = dash_pkc::bls_ietf::aggregate_pk(&pk_refs).unwrap(); - assert_eq!(agg.to_bytes().to_lower_hex_string(), v.agg_pk); - } - } - #[test] fn kat_aggregate_sig() { let corpus = Corpus::open(env!("CARGO_MANIFEST_DIR"), "bls_ietf_aggregate"); diff --git a/pkgs/pkc/tests/bls_ietf_llmq.rs b/pkgs/pkc/tests/bls_ietf_llmq.rs index f6292a08..afa4a86a 100644 --- a/pkgs/pkc/tests/bls_ietf_llmq.rs +++ b/pkgs/pkc/tests/bls_ietf_llmq.rs @@ -15,7 +15,7 @@ use dash_dev::{arr_from_hex, Corpus}; use dash_num::Hash256; use dash_pkc::bls::tests as common; -use dash_pkc::bls_ietf::{aggregate_pk, aggregate_sig, threshold, PublicKey, SecretKey, Signature}; +use dash_pkc::bls_ietf::{aggregate_sig, threshold, PublicKey, SecretKey, Signature}; use hex_conservative::DisplayHex; #[test] @@ -143,7 +143,7 @@ fn llmq_commit_quorum_key() { .map(|c| PublicKey::from_bytes(&arr_from_hex(c["vvec"][0].as_str().unwrap())).unwrap()) .collect(); let pk_refs: Vec<&PublicKey> = member_pks.iter().collect(); - let agg_pk = aggregate_pk(&pk_refs).unwrap(); + let agg_pk = PublicKey::aggregate(&pk_refs).unwrap(); assert_eq!(agg_pk.to_bytes().to_lower_hex_string(), expected_qpk,); } From 70170206178d08cbc89bcd070d7ee39059f64f49 Mon Sep 17 00:00:00 2001 From: Kittywhiskers Van Gogh <63189531+kwvg@users.noreply.github.com> Date: Mon, 10 Aug 2026 14:16:39 +0530 Subject: [PATCH 07/14] pkc%feat(bls): migrate secret key operations to `BlsSecretKey` --- pkgs/pkc/Cargo.toml | 16 -- pkgs/pkc/src/bls/mod.rs | 2 + pkgs/pkc/src/bls/public_ops.rs | 53 ++++++- pkgs/pkc/src/bls/secret_ops.rs | 227 +++++++++++++++++++++++++++ pkgs/pkc/src/bls_chia/agg.rs | 7 - pkgs/pkc/src/bls_chia/mod.rs | 31 +--- pkgs/pkc/src/bls_chia/sig.rs | 10 +- pkgs/pkc/src/bls_chia/sk.rs | 98 ------------ pkgs/pkc/src/bls_chia/threshold.rs | 3 +- pkgs/pkc/src/bls_ietf/agg.rs | 7 - pkgs/pkc/src/bls_ietf/mod.rs | 33 +--- pkgs/pkc/src/bls_ietf/sig.rs | 26 ++- pkgs/pkc/src/bls_ietf/sk.rs | 113 ------------- pkgs/pkc/src/bls_ietf/threshold.rs | 3 +- pkgs/pkc/src/common/bls/contract.rs | 15 -- pkgs/pkc/tests/bls_chia_dh.rs | 53 ------- pkgs/pkc/tests/bls_chia_keygen.rs | 98 ------------ pkgs/pkc/tests/bls_chia_llmq.rs | 2 +- pkgs/pkc/tests/bls_ietf_aggregate.rs | 26 --- pkgs/pkc/tests/bls_ietf_dh.rs | 53 ------- pkgs/pkc/tests/bls_ietf_keygen.rs | 85 ---------- pkgs/pkc/tests/bls_ietf_llmq.rs | 2 +- 22 files changed, 325 insertions(+), 638 deletions(-) create mode 100644 pkgs/pkc/src/bls/secret_ops.rs delete mode 100644 pkgs/pkc/src/bls_chia/sk.rs delete mode 100644 pkgs/pkc/src/bls_ietf/sk.rs delete mode 100644 pkgs/pkc/tests/bls_chia_dh.rs delete mode 100644 pkgs/pkc/tests/bls_chia_keygen.rs delete mode 100644 pkgs/pkc/tests/bls_ietf_dh.rs delete mode 100644 pkgs/pkc/tests/bls_ietf_keygen.rs diff --git a/pkgs/pkc/Cargo.toml b/pkgs/pkc/Cargo.toml index 3de631b6..c8de1abf 100644 --- a/pkgs/pkc/Cargo.toml +++ b/pkgs/pkc/Cargo.toml @@ -73,14 +73,6 @@ required-features = ["tests"] name = "bls_chia_aggregate" required-features = ["bls", "tests"] -[[test]] -name = "bls_chia_dh" -required-features = ["bls", "tests"] - -[[test]] -name = "bls_chia_keygen" -required-features = ["bls", "tests"] - [[test]] name = "bls_chia_llmq" required-features = ["bls", "tests"] @@ -101,14 +93,6 @@ required-features = ["bls", "tests"] name = "bls_ietf_aggregate" required-features = ["bls", "tests"] -[[test]] -name = "bls_ietf_dh" -required-features = ["bls", "tests"] - -[[test]] -name = "bls_ietf_keygen" -required-features = ["bls", "tests"] - [[test]] name = "bls_ietf_llmq" required-features = ["bls", "tests"] diff --git a/pkgs/pkc/src/bls/mod.rs b/pkgs/pkc/src/bls/mod.rs index ccaa698b..c6be136c 100644 --- a/pkgs/pkc/src/bls/mod.rs +++ b/pkgs/pkc/src/bls/mod.rs @@ -24,6 +24,7 @@ cfg_if::cfg_if! { if #[cfg(feature = "bls")] { mod public_ops; mod scheme_chia; + mod secret_ops; #[expect(unsafe_code, reason = "blst C FFI")] pub(crate) mod blst_ffi; pub(crate) mod chia_h2c; @@ -37,5 +38,6 @@ cfg_if::cfg_if! { pub use public_ops::BlsPublicKey; pub use scheme_ops::BlsScheme; + pub use secret_ops::BlsSecretKey; } } diff --git a/pkgs/pkc/src/bls/public_ops.rs b/pkgs/pkc/src/bls/public_ops.rs index 26532410..eb3ec052 100644 --- a/pkgs/pkc/src/bls/public_ops.rs +++ b/pkgs/pkc/src/bls/public_ops.rs @@ -102,8 +102,8 @@ type_cvrt!(for[S: BlsScheme] TryFrom> for BlsPublicKey, BlsErro #[expect(clippy::unwrap_used, reason = "test code")] mod tests { use super::*; - use crate::bls::tests::SEED_0; - use crate::bls::{BlsScChia, BlsScIetf}; + use crate::bls::tests::{SEED_0, SEED_1}; + use crate::bls::{BlsScChia, BlsScIetf, BlsSecretKey}; use cfg_if::cfg_if; use dash_dev::{arr_from_hex, Corpus}; @@ -123,8 +123,50 @@ mod tests { agg_pk: String, } + #[derive(Deserialize)] + struct DhVec { + sk: String, + peer_pk: String, + shared: String, + } + + fn assert_dh_matches_vectors(corpus: &str) { + let corpus = Corpus::open(env!("CARGO_MANIFEST_DIR"), corpus); + let vecs: Vec = corpus.vectors("dh_exchange"); + + for v in &vecs { + let sk = BlsSecretKey::::from_bytes(&arr_from_hex(&v.sk)).unwrap(); + let peer = BlsPublicKey::::from_bytes(&arr_from_hex(&v.peer_pk)).unwrap(); + let shared = sk.dh_exchange(&peer).unwrap(); + assert_eq!(shared.to_bytes().to_lower_hex_string(), v.shared); + } + } + + #[rstest] + #[case::chia(assert_dh_matches_vectors::, "bls_chia_dh")] + #[case::ietf(assert_dh_matches_vectors::, "bls_ietf_dh")] + fn dh_exchange_matches_vectors(#[case] assertion: fn(&str), #[case] corpus: &str) { + assertion(corpus); + } + + fn assert_dh_roundtrip() { + let sk_a = BlsSecretKey::::generate(&SEED_0).unwrap(); + let sk_b = BlsSecretKey::::generate(&SEED_1).unwrap(); + + let shared_ab = sk_a.dh_exchange(&sk_b.public_key()).unwrap(); + let shared_ba = sk_b.dh_exchange(&sk_a.public_key()).unwrap(); + assert_eq!(shared_ab.to_bytes(), shared_ba.to_bytes()); + } + + #[rstest] + #[case::chia(assert_dh_roundtrip::)] + #[case::ietf(assert_dh_roundtrip::)] + fn dh_exchange_roundtrip(#[case] assertion: fn()) { + assertion(); + } + fn assert_pk_roundtrip() { - let pk = BlsPublicKey::::from_inner(S::derive_pk(&S::generate(&SEED_0).unwrap())); + let pk = BlsSecretKey::::generate(&SEED_0).unwrap().public_key(); let bytes = pk.to_bytes(); assert_eq!(BlsPublicKey::::from_bytes(&bytes).unwrap().to_bytes(), bytes); } @@ -149,7 +191,10 @@ mod tests { /// round-trips back to its canonical form. #[rstest] fn chia_masks_stray_public_key_bits() { - let clean = BlsScChia::pk_to_bytes(&BlsScChia::derive_pk(&BlsScChia::generate(&SEED_0).unwrap())); + let clean = BlsSecretKey::::generate(&SEED_0) + .unwrap() + .public_key() + .to_bytes(); let mut mutated = clean; mutated[0] |= 0x20; diff --git a/pkgs/pkc/src/bls/secret_ops.rs b/pkgs/pkc/src/bls/secret_ops.rs new file mode 100644 index 00000000..bf661aff --- /dev/null +++ b/pkgs/pkc/src/bls/secret_ops.rs @@ -0,0 +1,227 @@ +// +// Copyright (c) 2026-present, The Dash Core developers +// SPDX-License-Identifier: MIT +// See the accompanying file LICENSE or https://opensource.org/license/MIT +// + +//! Scheme-generic BLS secret key. + +use super::error::BlsError; +use super::public_ops::BlsPublicKey; +use super::scheme_ops::BlsScheme; +use super::{BlsSkBytes, BLS_SK_LEN}; +use crate::prelude::*; + +use dash_num::Hash256; +use dash_types::{dlgt_scodec, qtypestr, type_cvrt}; +use zeroize::Zeroizing; + +use core::fmt::{Debug, Formatter, Result as FmtResult}; + +/// A BLS secret key (32-byte scalar). +pub struct BlsSecretKey(pub(crate) S::InnerSk); + +dlgt_scodec!(for[S: BlsScheme] BlsSecretKey => BlsSkBytes, Hash256, BlsError, BLS_SK_LEN); + +impl BlsSecretKey { + /// Derive a secret key from input keying material (>= 32 bytes). + /// + /// # Errors + /// + /// Returns `InvalidKeyMaterial` or `InvalidSecretKey` when `ikm` + /// is shorter than 32 bytes. + pub fn generate(ikm: &[u8]) -> Result { + S::generate(ikm).map(Self) + } + + /// Parse from a 32-byte big-endian scalar. + /// + /// # Errors + /// + /// Returns `InvalidSecretKey` when the bytes are not a valid scalar. + pub fn from_bytes(bytes: &[u8; 32]) -> Result { + S::sk_from_bytes(bytes).map(Self) + } + + /// Serialize to 32 bytes. + pub fn to_bytes(&self) -> [u8; 32] { + S::sk_to_bytes(&self.0) + } + + /// Derive the corresponding public key. + pub fn public_key(&self) -> BlsPublicKey { + BlsPublicKey(S::derive_pk(&self.0)) + } + + /// Compute a DH shared key: `self * peer_pk`. + /// + /// The result is secret material despite its [`BlsPublicKey`] type: it is a + /// shared secret, so it must not be published, logged, or compared + /// non-uniformly the way a real public key may be. + /// + /// # Errors + /// + /// Returns `InvalidPublicKey` when the peer key or the product point + /// is invalid. + pub fn dh_exchange(&self, peer_pk: &BlsPublicKey) -> Result, BlsError> { + S::dh_exchange(&self.0, &peer_pk.0).map(BlsPublicKey::from_inner) + } + + /// Sum multiple secret keys (mod group order). + /// + /// # Errors + /// + /// Returns `EmptyAggregation` when no keys are given, or `InvalidSecretKey` + /// when the sum is not a valid scalar. + pub fn aggregate(keys: &[&Self]) -> Result { + let inner_refs: Vec<&S::InnerSk> = keys.iter().map(|k| &k.0).collect(); + S::aggregate_sk(&inner_refs).map(Self::from_inner) + } + + pub(crate) fn from_inner(inner: S::InnerSk) -> Self { + Self(inner) + } +} + +impl Clone for BlsSecretKey { + fn clone(&self) -> Self { + Self(self.0.clone()) + } +} + +impl Drop for BlsSecretKey { + fn drop(&mut self) { + S::zeroize_sk(&mut self.0); + } +} + +impl Debug for BlsSecretKey { + fn fmt(&self, f: &mut Formatter<'_>) -> FmtResult { + qtypestr(f, core::any::type_name::())?; + f.write_str("(..)") + } +} + +type_cvrt!(for[S: BlsScheme] From> for BlsSkBytes, |sk| { + Self::from_bytes(*Zeroizing::new(sk.to_bytes())) +}); + +type_cvrt!(for[S: BlsScheme] TryFrom> for BlsSecretKey, BlsError, |bytes| { + Self::from_bytes(bytes.as_bytes()) +}); + +#[cfg(all(test, feature = "tests"))] +#[expect(clippy::unwrap_used, reason = "test code")] +mod tests { + use super::*; + use crate::bls::tests::{RSEED, SEED_0}; + use crate::bls::{BlsScChia, BlsScIetf}; + + use dash_dev::{arr_from_hex, Corpus}; + use hex_conservative::DisplayHex; + use rstest::rstest; + use serde::Deserialize; + + #[derive(Deserialize)] + struct KeygenVec { + sk: String, + pk: String, + } + + #[derive(Deserialize)] + struct AggSkVec { + sks: Vec, + agg_sk: String, + } + + fn assert_roundtrip() { + let sk = BlsSecretKey::::generate(&SEED_0).unwrap(); + let bytes = sk.to_bytes(); + let decoded = BlsSecretKey::::from_bytes(&bytes).unwrap(); + assert_eq!(decoded.to_bytes(), bytes); + } + + #[rstest] + #[case::chia(assert_roundtrip::)] + #[case::ietf(assert_roundtrip::)] + fn serialization_roundtrip(#[case] assertion: fn()) { + assertion(); + } + + fn assert_derive_pk(corpus: &str) { + let corpus = Corpus::open(env!("CARGO_MANIFEST_DIR"), corpus); + let vecs: Vec = corpus.vectors("derive_pk"); + for v in &vecs { + let sk = BlsSecretKey::::from_bytes(&arr_from_hex(&v.sk)).unwrap(); + assert_eq!(sk.public_key().to_bytes().to_lower_hex_string(), v.pk); + } + } + + #[rstest] + #[case::chia(assert_derive_pk::, "bls_chia_keygen")] + #[case::ietf(assert_derive_pk::, "bls_ietf_keygen")] + fn derive_public_key_matches_vectors(#[case] assertion: fn(&str), #[case] corpus: &str) { + assertion(corpus); + } + + /// Key generation follows the KeyGen of draft-irtf-cfrg-bls-signature-03 + /// for both schemes; another variant would change these bytes. + fn assert_keygen_draft03(ikm: &[u8], expected: &str) { + let sk = BlsSecretKey::::generate(ikm).unwrap(); + assert_eq!(sk.to_bytes().to_lower_hex_string(), expected); + } + + #[rstest] + #[case::seed0(&RSEED[0], "4a353be3dac091a0a7e640620372f5e1e2e4401717c1e79cac6ffba8f6905604")] + #[case::seed1(&RSEED[1], "6fc9d9a2b05fd1f0e51bc91041a03be8657081f272ec281aff731624f0d1c220")] + #[case::seed2(&RSEED[2], "01433a85a09ef4c9f7a2cd973c007c1150631a35a1d0e199eca4364e051809bb")] + fn keygen_uses_draft03_variant(#[case] ikm: &[u8], #[case] expected: &str) { + assert_keygen_draft03::(ikm, expected); + assert_keygen_draft03::(ikm, expected); + } + + /// The keygen variant requires at least 32 bytes of input key material. + fn assert_short_ikm_rejected() { + assert!(BlsSecretKey::::generate(&[0u8; 31]).is_err()); + } + + #[rstest] + #[case::chia(assert_short_ikm_rejected::)] + #[case::ietf(assert_short_ikm_rejected::)] + fn generate_rejects_short_ikm(#[case] assertion: fn()) { + assertion(); + } + + /// One secret scalar derives two differently encoded public keys, so a + /// scheme mix-up cannot go unnoticed. + #[rstest] + fn public_key_formats_differ() { + let chia = BlsSecretKey::::generate(&SEED_0).unwrap(); + let ietf = BlsSecretKey::::from_bytes(&chia.to_bytes()).unwrap(); + assert_ne!(chia.public_key().to_bytes(), ietf.public_key().to_bytes()); + } + + /// Summing scalars is scheme-independent, so one corpus serves both. + fn assert_aggregate_vectors() { + let corpus = Corpus::open(env!("CARGO_MANIFEST_DIR"), "bls_aggregate"); + let vecs: Vec = corpus.vectors("aggregate_sk"); + + for v in &vecs { + let sks: Vec> = v + .sks + .iter() + .map(|sk| BlsSecretKey::::from_bytes(&arr_from_hex(sk)).unwrap()) + .collect(); + let refs: Vec<&BlsSecretKey> = sks.iter().collect(); + let agg = BlsSecretKey::::aggregate(&refs).unwrap(); + assert_eq!(agg.to_bytes().to_lower_hex_string(), v.agg_sk); + } + } + + #[rstest] + #[case::chia(assert_aggregate_vectors::)] + #[case::ietf(assert_aggregate_vectors::)] + fn aggregate_matches_vectors(#[case] assertion: fn()) { + assertion(); + } +} diff --git a/pkgs/pkc/src/bls_chia/agg.rs b/pkgs/pkc/src/bls_chia/agg.rs index 7ff8b9df..27201246 100644 --- a/pkgs/pkc/src/bls_chia/agg.rs +++ b/pkgs/pkc/src/bls_chia/agg.rs @@ -7,7 +7,6 @@ //! Aggregation and secure verification for legacy BLS. use super::sig::Signature; -use super::sk::SecretKey; use super::PublicKey; use crate::bls::scheme_ops::BlsScheme; use crate::bls::{BlsError, BlsScChia}; @@ -50,9 +49,3 @@ pub fn secure_verify_aggregates(sig: &Signature, msg: &[u8; 32], pks: &[&PublicK let inner: Vec<_> = pks.iter().map(|pk| &pk.0).collect(); BlsScChia::secure_verify_aggregates(&sig.0, msg, &inner) } - -/// Sum multiple secret keys (mod group order). -pub fn aggregate_sk(keys: &[&SecretKey]) -> Result { - let inner: Vec<_> = keys.iter().map(|key| &key.0).collect(); - BlsScChia::aggregate_sk(&inner).map(SecretKey::from_inner) -} diff --git a/pkgs/pkc/src/bls_chia/mod.rs b/pkgs/pkc/src/bls_chia/mod.rs index a9c035c3..eb1b3618 100644 --- a/pkgs/pkc/src/bls_chia/mod.rs +++ b/pkgs/pkc/src/bls_chia/mod.rs @@ -8,46 +8,23 @@ mod agg; mod sig; -mod sk; pub mod threshold; pub use crate::bls::BlsError; -pub use agg::{aggregate_sig, aggregate_sk, fast_verify_aggregates, secure_verify_aggregates, verify_aggregates}; +pub use agg::{aggregate_sig, fast_verify_aggregates, secure_verify_aggregates, verify_aggregates}; pub use sig::Signature; -pub use sk::SecretKey; /// A legacy BLS public key (48-byte G1 point in legacy serialization). pub type PublicKey = crate::bls::BlsPublicKey; +/// A legacy BLS secret key (32-byte scalar). +pub type SecretKey = crate::bls::BlsSecretKey; + // Compile-time contract: must match bls_ietf's shared API surface. const _: () = { use crate::common::bls::contract::*; - impl BlsSecretKey for SecretKey { - type Error = BlsError; - type PublicKey = PublicKey; - type Signature = Signature; - type Msg = [u8; 32]; - fn generate(ikm: &[u8]) -> Result { - SecretKey::generate(ikm) - } - fn from_bytes(b: &[u8; 32]) -> Result { - SecretKey::from_bytes(b) - } - fn to_bytes(&self) -> [u8; 32] { - self.to_bytes() - } - fn public_key(&self) -> PublicKey { - self.public_key() - } - fn sign(&self, msg: &[u8; 32]) -> Signature { - self.sign(msg) - } - fn dh_exchange(&self, peer_pk: &PublicKey) -> Result { - self.dh_exchange(peer_pk) - } - } impl BlsSignature for Signature { type Error = BlsError; type PublicKey = PublicKey; diff --git a/pkgs/pkc/src/bls_chia/sig.rs b/pkgs/pkc/src/bls_chia/sig.rs index fbe7ada8..44f2de03 100644 --- a/pkgs/pkc/src/bls_chia/sig.rs +++ b/pkgs/pkc/src/bls_chia/sig.rs @@ -6,7 +6,7 @@ //! Legacy BLS signature (96-byte G2 point, legacy serialization). -use super::PublicKey; +use super::{PublicKey, SecretKey}; use crate::bls::blst_ffi::G2Affine; use crate::bls::scheme_ops::BlsScheme; use crate::bls::{BlsError, BlsScChia, BlsSigBytes}; @@ -44,6 +44,14 @@ impl Signature { } } +impl SecretKey { + /// Sign a 32-byte message hash using the legacy scheme (no DST, Shallue-van + /// de Woestijne hash-to-G2). + pub fn sign(&self, msg: &[u8; 32]) -> Signature { + Signature::from_inner(BlsScChia::sign(&self.0, msg)) + } +} + crate::common::bls::impl_hash_via_bytes!(Signature); impl From for BlsSigBytes { diff --git a/pkgs/pkc/src/bls_chia/sk.rs b/pkgs/pkc/src/bls_chia/sk.rs deleted file mode 100644 index 31bbf253..00000000 --- a/pkgs/pkc/src/bls_chia/sk.rs +++ /dev/null @@ -1,98 +0,0 @@ -// -// Copyright (c) 2026-present, The Dash Core developers -// SPDX-License-Identifier: MIT -// See the accompanying file LICENSE or https://opensource.org/license/MIT -// - -//! Legacy BLS secret key. - -use super::sig::Signature; -use super::PublicKey; -use crate::bls::scheme_ops::BlsScheme; -use crate::bls::{BlsError, BlsScChia, BlsSkBytes, BLS_SK_LEN}; - -use dash_num::Hash256; -use dash_types::{dlgt_scodec, type_cvrt}; -use zeroize::Zeroizing; - -use core::fmt; - -/// A legacy BLS secret key (32-byte scalar). -#[derive(Clone)] -pub struct SecretKey(pub(super) blst::blst_scalar); - -dlgt_scodec!(SecretKey => BlsSkBytes, Hash256, BlsError, BLS_SK_LEN); - -impl SecretKey { - pub(super) fn from_inner(inner: blst::blst_scalar) -> Self { - Self(inner) - } - - /// Derive a secret key from input keying material (>= 32 bytes). Uses the - /// same IETF key generation as standard BLS, only the signing scheme - /// differs. - /// - /// # Errors - /// - /// Returns `InvalidSecretKey` when `ikm` is shorter than 32 bytes or the - /// derived scalar is not a valid secret key. - pub fn generate(ikm: &[u8]) -> Result { - BlsScChia::generate(ikm).map(Self) - } - - /// Parse from 32-byte big-endian scalar. - /// - /// # Errors - /// - /// Returns `InvalidSecretKey` when the scalar is zero or not less than the - /// group order. - pub fn from_bytes(bytes: &[u8; 32]) -> Result { - BlsScChia::sk_from_bytes(bytes).map(Self) - } - - /// Serialize to 32 bytes. - pub fn to_bytes(&self) -> [u8; 32] { - BlsScChia::sk_to_bytes(&self.0) - } - - /// Derive the corresponding public key (G1 point). - pub fn public_key(&self) -> PublicKey { - PublicKey::from_inner(BlsScChia::derive_pk(&self.0)) - } - - /// Sign a 32-byte message hash using the legacy scheme (no DST, Shallue-van - /// de Woestijne hash-to-G2). - pub fn sign(&self, msg: &[u8; 32]) -> Signature { - Signature::from_inner(BlsScChia::sign(&self.0, msg)) - } - - /// Compute a DH shared key: `self * peer_pk`. - /// - /// # Errors - /// - /// Infallible for the legacy scheme; the `Result` mirrors the shared - /// scheme signature and always returns `Ok`. - pub fn dh_exchange(&self, peer_pk: &PublicKey) -> Result { - BlsScChia::dh_exchange(&self.0, &peer_pk.0).map(PublicKey::from_inner) - } -} - -impl Drop for SecretKey { - fn drop(&mut self) { - BlsScChia::zeroize_sk(&mut self.0); - } -} - -impl fmt::Debug for SecretKey { - fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { - write!(f, "SecretKey(..)") - } -} - -type_cvrt!(From for BlsSkBytes, |sk| { - Self::from_bytes(*Zeroizing::new(sk.to_bytes())) -}); - -type_cvrt!(TryFrom> for SecretKey, BlsError, |bytes| { - Self::from_bytes(bytes.as_bytes()) -}); diff --git a/pkgs/pkc/src/bls_chia/threshold.rs b/pkgs/pkc/src/bls_chia/threshold.rs index 2a5ba0ed..54525a14 100644 --- a/pkgs/pkc/src/bls_chia/threshold.rs +++ b/pkgs/pkc/src/bls_chia/threshold.rs @@ -7,8 +7,7 @@ //! Thresholds for legacy scheme (m-of-n secret sharing and signature recovery). use super::sig::Signature; -use super::sk::SecretKey; -use super::PublicKey; +use super::{PublicKey, SecretKey}; use crate::bls::scheme_ops::BlsScheme; use crate::bls::{BlsError, BlsScChia}; use crate::prelude::*; diff --git a/pkgs/pkc/src/bls_ietf/agg.rs b/pkgs/pkc/src/bls_ietf/agg.rs index 351a5e19..775985dd 100644 --- a/pkgs/pkc/src/bls_ietf/agg.rs +++ b/pkgs/pkc/src/bls_ietf/agg.rs @@ -7,7 +7,6 @@ //! Aggregation and batch verification for IETF BLS. use super::sig::Signature; -use super::sk::SecretKey; use super::PublicKey; use crate::bls::scheme_ietf::DST_BASIC; use crate::bls::scheme_ops::{verify_ok, BlsScheme}; @@ -53,9 +52,3 @@ pub fn secure_verify_aggregates(sig: &Signature, msg: &[u8], pks: &[&PublicKey]) let inner: Vec<_> = pks.iter().map(|pk| &pk.0).collect(); BlsScIetf::secure_verify_aggregates(&sig.0, msg, &inner) } - -/// Sum multiple secret keys (mod group order). -pub fn aggregate_sk(keys: &[&SecretKey]) -> Result { - let inner: Vec<_> = keys.iter().map(|key| &key.0).collect(); - BlsScIetf::aggregate_sk(&inner).map(SecretKey::from_inner) -} diff --git a/pkgs/pkc/src/bls_ietf/mod.rs b/pkgs/pkc/src/bls_ietf/mod.rs index 437d0ead..b68af4b4 100644 --- a/pkgs/pkc/src/bls_ietf/mod.rs +++ b/pkgs/pkc/src/bls_ietf/mod.rs @@ -8,47 +8,26 @@ mod agg; mod sig; -mod sk; pub mod threshold; pub use crate::bls::BlsError; +/// BLS signature scheme (determines the DST). +pub use crate::bls::BlsSigId as Scheme; -pub use agg::{aggregate_sig, aggregate_sk, fast_verify_aggregates, secure_verify_aggregates, verify_aggregates}; +pub use agg::{aggregate_sig, fast_verify_aggregates, secure_verify_aggregates, verify_aggregates}; pub use sig::Signature; -pub use sk::{Scheme, SecretKey}; /// An IETF BLS public key (48-byte compressed G1 point). pub type PublicKey = crate::bls::BlsPublicKey; +/// An IETF BLS secret key (32-byte scalar). +pub type SecretKey = crate::bls::BlsSecretKey; + // Compile-time contract: if any of these methods are // removed or their signatures change, this block fails. const _: () = { use crate::common::bls::contract::*; - impl BlsSecretKey for SecretKey { - type Error = BlsError; - type PublicKey = PublicKey; - type Signature = Signature; - type Msg = [u8]; - fn generate(ikm: &[u8]) -> Result { - SecretKey::generate(ikm) - } - fn from_bytes(b: &[u8; 32]) -> Result { - SecretKey::from_bytes(b) - } - fn to_bytes(&self) -> [u8; 32] { - self.to_bytes() - } - fn public_key(&self) -> PublicKey { - self.public_key() - } - fn sign(&self, msg: &[u8]) -> Signature { - self.sign(msg) - } - fn dh_exchange(&self, peer_pk: &PublicKey) -> Result { - self.dh_exchange(peer_pk) - } - } impl BlsSignature for Signature { type Error = BlsError; type PublicKey = PublicKey; diff --git a/pkgs/pkc/src/bls_ietf/sig.rs b/pkgs/pkc/src/bls_ietf/sig.rs index 023cd5f1..2cc574b4 100644 --- a/pkgs/pkc/src/bls_ietf/sig.rs +++ b/pkgs/pkc/src/bls_ietf/sig.rs @@ -6,8 +6,7 @@ //! IETF BLS signature (96-byte compressed G2 point). -use super::sk::Scheme; -use super::PublicKey; +use super::{PublicKey, Scheme, SecretKey}; use crate::bls::scheme_ietf::{DST_BASIC, DST_POP, DST_POP_PROVE}; use crate::bls::scheme_ops::{verify_ok, BlsScheme}; use crate::bls::{BlsError, BlsScIetf, BlsSigBytes}; @@ -71,6 +70,29 @@ impl Signature { } } +impl SecretKey { + /// Sign with the Basic scheme. + pub fn sign(&self, msg: &[u8]) -> Signature { + Signature::from_inner(BlsScIetf::sign(&self.0, msg)) + } + + /// Sign with a specific scheme. + pub fn sign_with(&self, msg: &[u8], scheme: Scheme) -> Signature { + let dst = match scheme { + Scheme::Basic => DST_BASIC, + Scheme::ProofOfPossession => DST_POP, + }; + Signature::from_inner(self.0.sign(msg, dst, &[])) + } + + /// Produce a proof of possession by signing the serialized public key with + /// the PoP DST. + pub fn prove_possession(&self) -> Signature { + let pk_bytes = self.public_key().to_bytes(); + Signature::from_inner(self.0.sign(&pk_bytes, DST_POP_PROVE, &[])) + } +} + impl PublicKey { /// Verify a proof of possession against this key. /// diff --git a/pkgs/pkc/src/bls_ietf/sk.rs b/pkgs/pkc/src/bls_ietf/sk.rs deleted file mode 100644 index ea317bc4..00000000 --- a/pkgs/pkc/src/bls_ietf/sk.rs +++ /dev/null @@ -1,113 +0,0 @@ -// -// Copyright (c) 2026-present, The Dash Core developers -// SPDX-License-Identifier: MIT -// See the accompanying file LICENSE or https://opensource.org/license/MIT -// - -//! IETF BLS secret key. - -use super::sig::Signature; -use super::PublicKey; -use crate::bls::scheme_ietf::{DST_BASIC, DST_POP, DST_POP_PROVE}; -use crate::bls::scheme_ops::BlsScheme; -use crate::bls::{BlsError, BlsScIetf, BlsSkBytes, BLS_SK_LEN}; - -use blst::min_pk; -use dash_num::Hash256; -use dash_types::{dlgt_scodec, type_cvrt, Unencodable}; -use zeroize::Zeroizing; - -use core::fmt; - -/// BLS signature scheme (determines the DST). -#[derive(Clone, Copy, Debug, Eq, Hash, PartialEq, Unencodable)] -#[cfg_attr(feature = "serde", derive(::serde::Serialize, ::serde::Deserialize))] -pub enum Scheme { - /// Basic scheme (NUL augmentation). - Basic, - /// Proof of Possession scheme. - ProofOfPossession, -} - -/// A BLS secret key (32-byte scalar). -/// -/// Zeroised on drop by the blst crate. -#[derive(Clone)] -pub struct SecretKey(pub(super) min_pk::SecretKey); - -dlgt_scodec!(SecretKey => BlsSkBytes, Hash256, BlsError, BLS_SK_LEN); - -impl SecretKey { - pub(super) fn from_inner(inner: min_pk::SecretKey) -> Self { - Self(inner) - } - - /// Derive a secret key from input keying material. - /// - /// # Errors - /// - /// Returns `InvalidKeyMaterial` when `ikm` is shorter than 32 bytes. - pub fn generate(ikm: &[u8]) -> Result { - BlsScIetf::generate(ikm).map(Self) - } - - /// Parse from a 32-byte big-endian scalar. - pub fn from_bytes(bytes: &[u8; 32]) -> Result { - BlsScIetf::sk_from_bytes(bytes).map(Self) - } - - /// Serialize to 32 bytes. - pub fn to_bytes(&self) -> [u8; 32] { - BlsScIetf::sk_to_bytes(&self.0) - } - - /// Derive the corresponding public key. - pub fn public_key(&self) -> PublicKey { - PublicKey::from_inner(BlsScIetf::derive_pk(&self.0)) - } - - /// Sign with the Basic scheme. - pub fn sign(&self, msg: &[u8]) -> Signature { - Signature::from_inner(BlsScIetf::sign(&self.0, msg)) - } - - /// Sign with a specific scheme. - pub fn sign_with(&self, msg: &[u8], scheme: Scheme) -> Signature { - let dst = match scheme { - Scheme::Basic => DST_BASIC, - Scheme::ProofOfPossession => DST_POP, - }; - Signature::from_inner(self.0.sign(msg, dst, &[])) - } - - /// Compute a DH shared key: `self * peer_pk`. - /// - /// # Errors - /// - /// Returns [`BlsError::InvalidPublicKey`] when `peer_pk` or the resulting - /// point is not a valid public key. - pub fn dh_exchange(&self, peer_pk: &PublicKey) -> Result { - BlsScIetf::dh_exchange(&self.0, &peer_pk.0).map(PublicKey::from_inner) - } - - /// Produce a proof of possession by signing the serialized public key with - /// the PoP DST. - pub fn prove_possession(&self) -> Signature { - let pk_bytes = self.public_key().to_bytes(); - Signature::from_inner(self.0.sign(&pk_bytes, DST_POP_PROVE, &[])) - } -} - -impl fmt::Debug for SecretKey { - fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { - write!(f, "SecretKey(..)") - } -} - -type_cvrt!(From for BlsSkBytes, |sk| { - Self::from_bytes(*Zeroizing::new(sk.to_bytes())) -}); - -type_cvrt!(TryFrom> for SecretKey, BlsError, |bytes| { - Self::from_bytes(bytes.as_bytes()) -}); diff --git a/pkgs/pkc/src/bls_ietf/threshold.rs b/pkgs/pkc/src/bls_ietf/threshold.rs index 0416be42..e937547c 100644 --- a/pkgs/pkc/src/bls_ietf/threshold.rs +++ b/pkgs/pkc/src/bls_ietf/threshold.rs @@ -7,8 +7,7 @@ //! Thresholds for IETF scheme (m-of-n secret sharing and signature recovery). use super::sig::Signature; -use super::sk::SecretKey; -use super::PublicKey; +use super::{PublicKey, SecretKey}; use crate::bls::scheme_ops::BlsScheme; use crate::bls::{BlsError, BlsScIetf}; use crate::prelude::*; diff --git a/pkgs/pkc/src/common/bls/contract.rs b/pkgs/pkc/src/common/bls/contract.rs index 4e22dff5..833c1d99 100644 --- a/pkgs/pkc/src/common/bls/contract.rs +++ b/pkgs/pkc/src/common/bls/contract.rs @@ -14,21 +14,6 @@ //! `Hash256` participant IDs (big-endian, reduced mod the scalar field //! order). -/// Marker trait asserting the minimum BLS secret key API. -pub(crate) trait BlsSecretKey: Clone + Sized { - type Error; - type PublicKey; - type Signature; - type Msg: ?Sized; - - fn generate(ikm: &[u8]) -> Result; - fn from_bytes(bytes: &[u8; 32]) -> Result; - fn to_bytes(&self) -> [u8; 32]; - fn public_key(&self) -> Self::PublicKey; - fn sign(&self, msg: &Self::Msg) -> Self::Signature; - fn dh_exchange(&self, peer_pk: &Self::PublicKey) -> Result; -} - /// Marker trait asserting the minimum BLS signature API. pub(crate) trait BlsSignature: Clone + Sized { type Error; diff --git a/pkgs/pkc/tests/bls_chia_dh.rs b/pkgs/pkc/tests/bls_chia_dh.rs deleted file mode 100644 index 3f3e808c..00000000 --- a/pkgs/pkc/tests/bls_chia_dh.rs +++ /dev/null @@ -1,53 +0,0 @@ -// -// Copyright (c) 2026-present, The Dash Core developers -// SPDX-License-Identifier: MIT -// See the accompanying file LICENSE or https://opensource.org/license/MIT -// - -//! Diffie-Hellman exchange tests for bls_chia. - -#![expect(clippy::unwrap_used, reason = "test code")] - -use common::*; -use dash_pkc::bls::tests as common; -use dash_pkc::bls_chia::{PublicKey, SecretKey}; -use rstest::*; - -/// DH exchange produces a shared secret. -#[rstest] -fn dh_exchange_roundtrip(chia_sk0: SecretKey, chia_sk1: SecretKey) { - let pk0 = chia_sk0.public_key(); - let pk1 = chia_sk1.public_key(); - // sk0 * pk1 == sk1 * pk0 - let shared_a = chia_sk0.dh_exchange(&pk1).unwrap(); - let shared_b = chia_sk1.dh_exchange(&pk0).unwrap(); - assert_eq!(shared_a.to_bytes(), shared_b.to_bytes()); -} - -/// Reference vectors through the public wrapper. -/// -/// The scheme-level KAT pins `dh_exchange` on the trait; this pins that -/// `SecretKey::dh_exchange` is still wired to it. -mod kat { - use dash_dev::{arr_from_hex, Corpus}; - use hex_conservative::DisplayHex; - use serde::Deserialize; - - #[derive(Deserialize)] - struct DhVector { - sk: String, - peer_pk: String, - shared: String, - } - - #[test] - fn public_api_dh_matches_vectors() { - let corpus = Corpus::open(env!("CARGO_MANIFEST_DIR"), "bls_chia_dh"); - for v in corpus.vectors::("dh_exchange") { - let sk = super::SecretKey::from_bytes(&arr_from_hex(&v.sk)).unwrap(); - let peer = super::PublicKey::from_bytes(&arr_from_hex(&v.peer_pk)).unwrap(); - let shared = sk.dh_exchange(&peer).unwrap(); - assert_eq!(shared.to_bytes().to_lower_hex_string(), v.shared); - } - } -} diff --git a/pkgs/pkc/tests/bls_chia_keygen.rs b/pkgs/pkc/tests/bls_chia_keygen.rs deleted file mode 100644 index 514cf907..00000000 --- a/pkgs/pkc/tests/bls_chia_keygen.rs +++ /dev/null @@ -1,98 +0,0 @@ -// -// Copyright (c) 2026-present, The Dash Core developers -// SPDX-License-Identifier: MIT -// See the accompanying file LICENSE or https://opensource.org/license/MIT -// - -//! Key generation and public key serialization tests for -//! bls_chia. - -#![expect(clippy::unwrap_used, reason = "test code")] - -use common::*; -#[cfg(feature = "serde")] -use dash_dev::assert_json_rt; -use dash_pkc::{bls::tests as common, bls_chia::SecretKey}; -use rstest::*; - -/// Secret key serialization round-trips. -#[rstest] -fn sk_roundtrip(chia_sk0: SecretKey) { - let bytes = chia_sk0.to_bytes(); - let restored = SecretKey::from_bytes(&bytes).unwrap(); - assert_eq!(restored.public_key().to_bytes(), chia_sk0.public_key().to_bytes()); -} - -/// IKM shorter than 32 bytes is rejected. -#[rstest] -fn sk_generate_rejects_short_ikm() { - assert!(SecretKey::generate(&[0u8; 31]).is_err()); -} - -/// Keys derived using draft-03 (blst `key_gen_v3`) -#[rstest] -#[case(&RSEED[0], "4a353be3dac091a0a7e640620372f5e1e2e4401717c1e79cac6ffba8f6905604")] -#[case(&RSEED[1], "6fc9d9a2b05fd1f0e51bc91041a03be8657081f272ec281aff731624f0d1c220")] -#[case(&RSEED[2], "01433a85a09ef4c9f7a2cd973c007c1150631a35a1d0e199eca4364e051809bb")] -fn keygen_uses_eip2333_variant(#[case] ikm: &[u8], #[case] expected: &str) { - use hex_conservative::DisplayHex; - let hex = SecretKey::generate(ikm).unwrap().to_bytes().to_lower_hex_string(); - assert_eq!(hex, expected, "got {hex}"); -} - -/// Legacy public key round-trips (48 bytes). -#[rstest] -fn pk_roundtrip(chia_sk0: SecretKey) { - let pk = chia_sk0.public_key(); - let bytes = pk.to_bytes(); - assert_eq!(bytes.len(), 48); - let restored = dash_pkc::bls_chia::PublicKey::from_bytes(&bytes).unwrap(); - assert_eq!(restored, pk); -} - -/// Serde round-trip for PublicKey. -#[cfg(feature = "serde")] -#[rstest] -fn serde_pk_roundtrip(chia_sk0: SecretKey) { - let pk = chia_sk0.public_key(); - assert_json_rt(&pk); -} - -/// Same key serialized under legacy and IETF formats must produce -/// different bytes. -#[rstest] -fn cross_format_pk_differs(chia_sk0: SecretKey) { - let legacy_bytes = chia_sk0.public_key().to_bytes(); - let ietf_sk = dash_pkc::bls_ietf::SecretKey::from_bytes(&chia_sk0.to_bytes()).unwrap(); - let ietf_bytes = ietf_sk.public_key().to_bytes(); - assert_ne!(legacy_bytes, ietf_bytes, "same point must serialize differently"); -} - -mod kat { - use dash_dev::{arr_from_hex, Corpus}; - use hex_conservative::DisplayHex; - use serde::Deserialize; - - #[derive(Deserialize)] - struct KeygenVector { - sk: String, - pk: String, - } - - #[test] - fn kat_derive_pk() { - let corpus = Corpus::open(env!("CARGO_MANIFEST_DIR"), "bls_chia_keygen"); - let vecs: Vec = corpus.vectors("derive_pk"); - - for v in &vecs { - let sk_bytes: [u8; 32] = arr_from_hex(&v.sk); - let sk = dash_pkc::bls_chia::SecretKey::from_bytes(&sk_bytes).unwrap(); - assert_eq!( - sk.public_key().to_bytes().to_lower_hex_string(), - v.pk, - "pk mismatch for sk {}", - v.sk - ); - } - } -} diff --git a/pkgs/pkc/tests/bls_chia_llmq.rs b/pkgs/pkc/tests/bls_chia_llmq.rs index 3b8d5413..ff9f7b55 100644 --- a/pkgs/pkc/tests/bls_chia_llmq.rs +++ b/pkgs/pkc/tests/bls_chia_llmq.rs @@ -132,7 +132,7 @@ fn llmq_commit_sk_share() { } let refs: Vec<&SecretKey> = received.iter().collect(); - let agg = dash_pkc::bls_chia::aggregate_sk(&refs).unwrap(); + let agg = SecretKey::aggregate(&refs).unwrap(); assert_eq!(agg.to_bytes().to_lower_hex_string(), expected_share); } } diff --git a/pkgs/pkc/tests/bls_ietf_aggregate.rs b/pkgs/pkc/tests/bls_ietf_aggregate.rs index 17d5565d..2e8f9f44 100644 --- a/pkgs/pkc/tests/bls_ietf_aggregate.rs +++ b/pkgs/pkc/tests/bls_ietf_aggregate.rs @@ -138,12 +138,6 @@ mod kat { agg_sig: String, } - #[derive(Deserialize)] - struct AggregateSkVector { - sks: Vec, - agg_sk: String, - } - #[derive(Deserialize)] #[expect(dead_code, reason = "deserialized from corpus JSON")] struct SecureAggVector { @@ -173,26 +167,6 @@ mod kat { } } - #[test] - fn kat_aggregate_sk() { - let corpus = Corpus::open(env!("CARGO_MANIFEST_DIR"), "bls_aggregate"); - let vecs: Vec = corpus.vectors("aggregate_sk"); - - for v in &vecs { - let sks: Vec = v - .sks - .iter() - .map(|h| { - let b: [u8; 32] = arr_from_hex(h); - dash_pkc::bls_ietf::SecretKey::from_bytes(&b).unwrap() - }) - .collect(); - let sk_refs: Vec<_> = sks.iter().collect(); - let agg = dash_pkc::bls_ietf::aggregate_sk(&sk_refs).unwrap(); - assert_eq!(agg.to_bytes().to_lower_hex_string(), v.agg_sk); - } - } - #[test] fn kat_secure_verify_aggregates() { let corpus = Corpus::open(env!("CARGO_MANIFEST_DIR"), "bls_ietf_secure_aggregate"); diff --git a/pkgs/pkc/tests/bls_ietf_dh.rs b/pkgs/pkc/tests/bls_ietf_dh.rs deleted file mode 100644 index c96fc25e..00000000 --- a/pkgs/pkc/tests/bls_ietf_dh.rs +++ /dev/null @@ -1,53 +0,0 @@ -// -// Copyright (c) 2026-present, The Dash Core developers -// SPDX-License-Identifier: MIT -// See the accompanying file LICENSE or https://opensource.org/license/MIT -// - -//! Diffie-Hellman exchange tests for bls_ietf. - -#![expect(clippy::unwrap_used, reason = "test code")] - -use common::*; -use dash_pkc::bls::tests as common; -use dash_pkc::bls_ietf::{PublicKey, SecretKey}; -use rstest::*; - -/// DH exchange produces a shared secret. -#[rstest] -fn dh_exchange_roundtrip(ietf_sk0: SecretKey, ietf_sk1: SecretKey) { - let pk0 = ietf_sk0.public_key(); - let pk1 = ietf_sk1.public_key(); - // sk0 * pk1 == sk1 * pk0 - let shared_a = ietf_sk0.dh_exchange(&pk1).unwrap(); - let shared_b = ietf_sk1.dh_exchange(&pk0).unwrap(); - assert_eq!(shared_a.to_bytes(), shared_b.to_bytes()); -} - -/// Reference vectors through the public wrapper. -/// -/// The scheme-level KAT pins `dh_exchange` on the trait; this pins that -/// `SecretKey::dh_exchange` is still wired to it. -mod kat { - use dash_dev::{arr_from_hex, Corpus}; - use hex_conservative::DisplayHex; - use serde::Deserialize; - - #[derive(Deserialize)] - struct DhVector { - sk: String, - peer_pk: String, - shared: String, - } - - #[test] - fn public_api_dh_matches_vectors() { - let corpus = Corpus::open(env!("CARGO_MANIFEST_DIR"), "bls_ietf_dh"); - for v in corpus.vectors::("dh_exchange") { - let sk = super::SecretKey::from_bytes(&arr_from_hex(&v.sk)).unwrap(); - let peer = super::PublicKey::from_bytes(&arr_from_hex(&v.peer_pk)).unwrap(); - let shared = sk.dh_exchange(&peer).unwrap(); - assert_eq!(shared.to_bytes().to_lower_hex_string(), v.shared); - } - } -} diff --git a/pkgs/pkc/tests/bls_ietf_keygen.rs b/pkgs/pkc/tests/bls_ietf_keygen.rs deleted file mode 100644 index 8e30bf8a..00000000 --- a/pkgs/pkc/tests/bls_ietf_keygen.rs +++ /dev/null @@ -1,85 +0,0 @@ -// -// Copyright (c) 2026-present, The Dash Core developers -// SPDX-License-Identifier: MIT -// See the accompanying file LICENSE or https://opensource.org/license/MIT -// - -//! Key generation and public key serialization tests for bls_ietf. - -#![expect(clippy::unwrap_used, reason = "test code")] - -use common::*; -#[cfg(feature = "serde")] -use dash_dev::assert_json_rt; -use dash_pkc::{bls::tests as common, bls_ietf::SecretKey}; -use rstest::*; - -/// Secret key serialization round-trips. -#[rstest] -fn sk_roundtrip(ietf_sk0: SecretKey) { - let bytes = ietf_sk0.to_bytes(); - let restored = SecretKey::from_bytes(&bytes).unwrap(); - assert_eq!(restored.public_key().to_bytes(), ietf_sk0.public_key().to_bytes()); -} - -/// IKM shorter than 32 bytes is rejected. -#[rstest] -fn sk_generate_rejects_short_ikm() { - assert!(SecretKey::generate(&[0u8; 31]).is_err()); -} - -/// Compressed public key round-trips (48 bytes). -#[rstest] -fn pk_roundtrip(ietf_sk0: SecretKey) { - let pk = ietf_sk0.public_key(); - let bytes = pk.to_bytes(); - assert_eq!(bytes.len(), 48); - let restored = dash_pkc::bls_ietf::PublicKey::from_bytes(&bytes).unwrap(); - assert_eq!(restored, pk); -} - -/// Serde round-trip for PublicKey. -#[cfg(feature = "serde")] -#[rstest] -fn serde_pk_roundtrip(ietf_sk0: SecretKey) { - let pk = ietf_sk0.public_key(); - assert_json_rt(&pk); -} - -/// Same key serialized under IETF and legacy formats must differ. -#[rstest] -fn cross_format_pk_differs(ietf_sk0: SecretKey) { - let ietf_bytes = ietf_sk0.public_key().to_bytes(); - let legacy_sk = dash_pkc::bls_chia::SecretKey::from_bytes(&ietf_sk0.to_bytes()).unwrap(); - let legacy_bytes = legacy_sk.public_key().to_bytes(); - assert_ne!(ietf_bytes, legacy_bytes, "same point must serialize differently"); -} - -mod kat { - use dash_dev::{arr_from_hex, Corpus}; - use hex_conservative::DisplayHex; - use serde::Deserialize; - - #[derive(Deserialize)] - struct KeygenVector { - sk: String, - pk: String, - } - - #[test] - fn kat_derive_pk() { - let corpus = Corpus::open(env!("CARGO_MANIFEST_DIR"), "bls_ietf_keygen"); - let vecs: Vec = corpus.vectors("derive_pk"); - - for v in &vecs { - let sk_bytes: [u8; 32] = arr_from_hex(&v.sk); - let sk = dash_pkc::bls_ietf::SecretKey::from_bytes(&sk_bytes).unwrap(); - assert_eq!( - sk.public_key().to_bytes().to_lower_hex_string(), - v.pk, - "pk mismatch for sk {}", - v.sk - ); - } - } -} diff --git a/pkgs/pkc/tests/bls_ietf_llmq.rs b/pkgs/pkc/tests/bls_ietf_llmq.rs index afa4a86a..76f6d2e5 100644 --- a/pkgs/pkc/tests/bls_ietf_llmq.rs +++ b/pkgs/pkc/tests/bls_ietf_llmq.rs @@ -165,7 +165,7 @@ fn llmq_commit_sk_share() { } let refs: Vec<&SecretKey> = received.iter().collect(); - let agg = dash_pkc::bls_ietf::aggregate_sk(&refs).unwrap(); + let agg = SecretKey::aggregate(&refs).unwrap(); assert_eq!( agg.to_bytes().to_lower_hex_string(), expected_share, From e4f0168cbe08effdaf3c96fa69b844fd1ac9ac7b Mon Sep 17 00:00:00 2001 From: Kittywhiskers Van Gogh <63189531+kwvg@users.noreply.github.com> Date: Mon, 10 Aug 2026 13:44:35 +0530 Subject: [PATCH 08/14] pkc%feat(bls): propagate `zeroize` and `subtle` use in `BlsSecretKey` --- pkgs/pkc/src/bls/scheme_ietf.rs | 8 +++++--- pkgs/pkc/src/bls/secret_ops.rs | 34 +++++++++++++++++++++++++++------ 2 files changed, 33 insertions(+), 9 deletions(-) diff --git a/pkgs/pkc/src/bls/scheme_ietf.rs b/pkgs/pkc/src/bls/scheme_ietf.rs index cb7f7690..7860e404 100644 --- a/pkgs/pkc/src/bls/scheme_ietf.rs +++ b/pkgs/pkc/src/bls/scheme_ietf.rs @@ -46,9 +46,11 @@ impl BlsScheme for BlsScIetf { sk.sk_to_pk() } - /// No-op; `blst` zeroizes the key on drop. - fn zeroize_sk(_sk: &mut Self::InnerSk) { - // blst::min_pk::SecretKey zeroizes on drop internally. + /// Wipe the scalar limbs. + fn zeroize_sk(sk: &mut Self::InnerSk) { + // blst's SecretKey wipes itself on drop but exposes no in-place wipe, + // so assign over it. The old value drops, dropping is the wipe. + *sk = SecretKey::default(); } /// Decode the compressed G1 point and run `validate`. diff --git a/pkgs/pkc/src/bls/secret_ops.rs b/pkgs/pkc/src/bls/secret_ops.rs index bf661aff..53a0d067 100644 --- a/pkgs/pkc/src/bls/secret_ops.rs +++ b/pkgs/pkc/src/bls/secret_ops.rs @@ -13,8 +13,9 @@ use super::{BlsSkBytes, BLS_SK_LEN}; use crate::prelude::*; use dash_num::Hash256; +use dash_types::codec::TypeId; use dash_types::{dlgt_scodec, qtypestr, type_cvrt}; -use zeroize::Zeroizing; +use zeroize::{Zeroize, ZeroizeOnDrop, Zeroizing}; use core::fmt::{Debug, Formatter, Result as FmtResult}; @@ -43,9 +44,9 @@ impl BlsSecretKey { S::sk_from_bytes(bytes).map(Self) } - /// Serialize to 32 bytes. - pub fn to_bytes(&self) -> [u8; 32] { - S::sk_to_bytes(&self.0) + /// Serialize to 32 bytes, wiped when the returned value drops. + pub fn to_bytes(&self) -> Zeroizing<[u8; 32]> { + Zeroizing::new(S::sk_to_bytes(&self.0)) } /// Derive the corresponding public key. @@ -91,7 +92,7 @@ impl Clone for BlsSecretKey { impl Drop for BlsSecretKey { fn drop(&mut self) { - S::zeroize_sk(&mut self.0); + self.zeroize(); } } @@ -102,8 +103,29 @@ impl Debug for BlsSecretKey { } } +impl Eq for BlsSecretKey {} + +impl PartialEq for BlsSecretKey { + fn eq(&self, other: &Self) -> bool { + use subtle::ConstantTimeEq; + (*self.to_bytes()).ct_eq(&*other.to_bytes()).into() + } +} + +impl Zeroize for BlsSecretKey { + fn zeroize(&mut self) { + S::zeroize_sk(&mut self.0); + } +} + +impl ZeroizeOnDrop for BlsSecretKey {} + +impl TypeId for BlsSecretKey { + const TYPE_ID: u32 = S::SK_TYPE_ID; +} + type_cvrt!(for[S: BlsScheme] From> for BlsSkBytes, |sk| { - Self::from_bytes(*Zeroizing::new(sk.to_bytes())) + Self::from_bytes(*sk.to_bytes()) }); type_cvrt!(for[S: BlsScheme] TryFrom> for BlsSecretKey, BlsError, |bytes| { From c0310b4ba80a6c85aae510b851aad893203c88bd Mon Sep 17 00:00:00 2001 From: Kittywhiskers Van Gogh <63189531+kwvg@users.noreply.github.com> Date: Mon, 10 Aug 2026 13:47:00 +0530 Subject: [PATCH 09/14] pkc%feat(bls): migrate basic signing to `BlsSignature` --- pkgs/pkc/Cargo.toml | 12 -- pkgs/pkc/src/bls/mod.rs | 2 + pkgs/pkc/src/bls/scheme_chia.rs | 7 +- pkgs/pkc/src/bls/scheme_ietf.rs | 32 +++- pkgs/pkc/src/bls/scheme_ops.rs | 3 + pkgs/pkc/src/bls/secret_ops.rs | 15 +- pkgs/pkc/src/bls/sig_basic.rs | 288 +++++++++++++++++++++++++++++ pkgs/pkc/src/bls_chia/agg.rs | 3 +- pkgs/pkc/src/bls_chia/mod.rs | 5 +- pkgs/pkc/src/bls_chia/sig.rs | 69 ------- pkgs/pkc/src/bls_chia/threshold.rs | 3 +- pkgs/pkc/src/bls_ietf/agg.rs | 3 +- pkgs/pkc/src/bls_ietf/mod.rs | 4 +- pkgs/pkc/src/bls_ietf/sig.rs | 97 +--------- pkgs/pkc/src/bls_ietf/threshold.rs | 3 +- pkgs/pkc/src/common/bls/mod.rs | 12 -- pkgs/pkc/tests/bls_chia_ser.rs | 118 ------------ pkgs/pkc/tests/bls_chia_sign.rs | 126 ------------- pkgs/pkc/tests/bls_ietf_sign.rs | 114 ------------ 19 files changed, 358 insertions(+), 558 deletions(-) create mode 100644 pkgs/pkc/src/bls/sig_basic.rs delete mode 100644 pkgs/pkc/src/bls_chia/sig.rs delete mode 100644 pkgs/pkc/tests/bls_chia_ser.rs delete mode 100644 pkgs/pkc/tests/bls_chia_sign.rs delete mode 100644 pkgs/pkc/tests/bls_ietf_sign.rs diff --git a/pkgs/pkc/Cargo.toml b/pkgs/pkc/Cargo.toml index c8de1abf..d0f55343 100644 --- a/pkgs/pkc/Cargo.toml +++ b/pkgs/pkc/Cargo.toml @@ -77,14 +77,6 @@ required-features = ["bls", "tests"] name = "bls_chia_llmq" required-features = ["bls", "tests"] -[[test]] -name = "bls_chia_ser" -required-features = ["bls", "tests"] - -[[test]] -name = "bls_chia_sign" -required-features = ["bls", "tests"] - [[test]] name = "bls_chia_threshold" required-features = ["bls", "tests"] @@ -101,10 +93,6 @@ required-features = ["bls", "tests"] name = "bls_ietf_pop" required-features = ["bls", "tests"] -[[test]] -name = "bls_ietf_sign" -required-features = ["bls", "tests"] - [[test]] name = "bls_ietf_threshold" required-features = ["bls", "tests"] diff --git a/pkgs/pkc/src/bls/mod.rs b/pkgs/pkc/src/bls/mod.rs index c6be136c..7784e7ab 100644 --- a/pkgs/pkc/src/bls/mod.rs +++ b/pkgs/pkc/src/bls/mod.rs @@ -25,6 +25,7 @@ cfg_if::cfg_if! { mod public_ops; mod scheme_chia; mod secret_ops; + mod sig_basic; #[expect(unsafe_code, reason = "blst C FFI")] pub(crate) mod blst_ffi; pub(crate) mod chia_h2c; @@ -39,5 +40,6 @@ cfg_if::cfg_if! { pub use public_ops::BlsPublicKey; pub use scheme_ops::BlsScheme; pub use secret_ops::BlsSecretKey; + pub use sig_basic::BlsSignature; } } diff --git a/pkgs/pkc/src/bls/scheme_chia.rs b/pkgs/pkc/src/bls/scheme_chia.rs index fd7e9657..fce56512 100644 --- a/pkgs/pkc/src/bls/scheme_chia.rs +++ b/pkgs/pkc/src/bls/scheme_chia.rs @@ -138,7 +138,7 @@ impl BlsScheme for BlsScChia { // After swizzling, byte 48 (top of `x.c1`) sits in the IETF flag byte, // where blst reads flags instead of range-checking, so reject its stray - // high bits here: the reference feeds them to relic as `x >= p`. + // high bits here: kept, they would make `x.c1 >= p`. if b[48] & 0xe0 != 0 { return Err(BlsError::InvalidSignature); } @@ -223,6 +223,11 @@ impl BlsScheme for BlsScChia { } } + /// The Chia message type is already a fixed 32-byte array. + fn msg_ref(m: &[u8; 32]) -> &Self::Msg { + m + } + /// Sum the public keys in G1. fn aggregate_pk(pks: &[&Self::InnerPk]) -> Result { if pks.is_empty() { diff --git a/pkgs/pkc/src/bls/scheme_ietf.rs b/pkgs/pkc/src/bls/scheme_ietf.rs index 7860e404..04f0d11b 100644 --- a/pkgs/pkc/src/bls/scheme_ietf.rs +++ b/pkgs/pkc/src/bls/scheme_ietf.rs @@ -10,13 +10,14 @@ use super::blst_ffi::{G1Affine, G2Affine, G1, G2}; use super::error::BlsError; use super::scheme_ops::{verify_ok, BlsScheme}; use super::schemes::BlsScIetf; +use super::sig_id::BlsSigId; use blst::min_pk::{AggregatePublicKey, AggregateSignature, PublicKey, SecretKey, Signature}; /// Domain separation tag for the basic (NUL) signature scheme. pub(crate) const DST_BASIC: &[u8] = b"BLS_SIG_BLS12381G2_XMD:SHA-256_SSWU_RO_NUL_"; /// Domain separation tag for signatures in the proof-of-possession scheme. -pub(crate) const DST_POP: &[u8] = b"BLS_SIG_BLS12381G2_XMD:SHA-256_SSWU_RO_POP_"; +const DST_POP: &[u8] = b"BLS_SIG_BLS12381G2_XMD:SHA-256_SSWU_RO_POP_"; /// Domain separation tag for proofs of possession. pub(crate) const DST_POP_PROVE: &[u8] = b"BLS_POP_BLS12381G2_XMD:SHA-256_SSWU_RO_POP_"; @@ -122,6 +123,11 @@ impl BlsScheme for BlsScIetf { verify_ok(sig.verify(true, msg, DST_BASIC, &[], pk, true)) } + /// IETF messages are unsized slices; a fixed array reborrows as one. + fn msg_ref(m: &[u8; 32]) -> &Self::Msg { + m + } + /// Aggregate the public keys via blst. fn aggregate_pk(pks: &[&Self::InnerPk]) -> Result { if pks.is_empty() { @@ -149,6 +155,30 @@ impl BlsScheme for BlsScIetf { } } +impl BlsScIetf { + /// Sign under the DST selected by `id`. + pub(crate) fn sign_with(sk: &SecretKey, msg: &[u8], id: BlsSigId) -> Signature { + let dst = match id { + BlsSigId::Basic => DST_BASIC, + BlsSigId::ProofOfPossession => DST_POP, + }; + sk.sign(msg, dst, &[]) + } + + /// Verify under the DST selected by `id`. + /// + /// # Errors + /// + /// Returns `VerifyFailed` when the pairing check does not hold. + pub(crate) fn verify_with(sig: &Signature, msg: &[u8], pk: &PublicKey, id: BlsSigId) -> Result<(), BlsError> { + let dst = match id { + BlsSigId::Basic => DST_BASIC, + BlsSigId::ProofOfPossession => DST_POP, + }; + verify_ok(sig.verify(true, msg, dst, &[], pk, true)) + } +} + #[cfg(all(test, feature = "tests"))] #[expect(clippy::unwrap_used, reason = "test code")] mod tests { diff --git a/pkgs/pkc/src/bls/scheme_ops.rs b/pkgs/pkc/src/bls/scheme_ops.rs index 009e4f5c..b75d324d 100644 --- a/pkgs/pkc/src/bls/scheme_ops.rs +++ b/pkgs/pkc/src/bls/scheme_ops.rs @@ -126,6 +126,9 @@ pub trait BlsScheme: BlsSchemeId { /// Returns `VerifyFailed` when the pairing check does not hold. fn verify(sig: &Self::InnerSig, msg: &Self::Msg, pk: &Self::InnerPk) -> Result<(), BlsError>; + /// Reborrow a fixed 32-byte message as the scheme's message type. + fn msg_ref(m: &[u8; 32]) -> &Self::Msg; + /// Compute the Diffie-Hellman shared key `sk * peer_pk`. /// /// # Errors diff --git a/pkgs/pkc/src/bls/secret_ops.rs b/pkgs/pkc/src/bls/secret_ops.rs index 53a0d067..efc77e20 100644 --- a/pkgs/pkc/src/bls/secret_ops.rs +++ b/pkgs/pkc/src/bls/secret_ops.rs @@ -9,7 +9,8 @@ use super::error::BlsError; use super::public_ops::BlsPublicKey; use super::scheme_ops::BlsScheme; -use super::{BlsSkBytes, BLS_SK_LEN}; +use super::sig_basic::BlsSignature; +use super::{BlsScIetf, BlsSigId, BlsSkBytes, BLS_SK_LEN}; use crate::prelude::*; use dash_num::Hash256; @@ -54,6 +55,11 @@ impl BlsSecretKey { BlsPublicKey(S::derive_pk(&self.0)) } + /// Sign a message of the scheme's message type. + pub fn sign(&self, msg: &S::Msg) -> BlsSignature { + BlsSignature::from_inner(S::sign(&self.0, msg)) + } + /// Compute a DH shared key: `self * peer_pk`. /// /// The result is secret material despite its [`BlsPublicKey`] type: it is a @@ -84,6 +90,13 @@ impl BlsSecretKey { } } +impl BlsSecretKey { + /// Sign under the domain separation tag selected by `scheme`. + pub fn sign_with(&self, msg: &[u8], scheme: BlsSigId) -> BlsSignature { + BlsSignature::from_inner(BlsScIetf::sign_with(&self.0, msg, scheme)) + } +} + impl Clone for BlsSecretKey { fn clone(&self) -> Self { Self(self.0.clone()) diff --git a/pkgs/pkc/src/bls/sig_basic.rs b/pkgs/pkc/src/bls/sig_basic.rs new file mode 100644 index 00000000..f4cc8262 --- /dev/null +++ b/pkgs/pkc/src/bls/sig_basic.rs @@ -0,0 +1,288 @@ +// +// Copyright (c) 2026-present, The Dash Core developers +// SPDX-License-Identifier: MIT +// See the accompanying file LICENSE or https://opensource.org/license/MIT +// + +//! Scheme-generic BLS signature. + +use super::error::BlsError; +use super::public_ops::BlsPublicKey; +use super::scheme_ops::BlsScheme; +use super::{BlsScIetf, BlsSigBytes, BlsSigId, BLS_SIG_LEN}; + +use dash_num::Hash256; +use dash_types::codec::TypeId; +use dash_types::{dlgt_codec, qtypestr, type_cvrt}; +use hex_conservative::DisplayHex; + +use core::fmt::{Debug, Formatter, Result as FmtResult}; +use core::hash::{Hash, Hasher}; + +/// A BLS signature (96-byte compressed G2 point) +#[cfg_attr(feature = "serde", derive(::serde::Serialize, ::serde::Deserialize))] +#[cfg_attr(feature = "serde", serde(into = "BlsSigBytes", try_from = "BlsSigBytes"))] +#[cfg_attr(feature = "serde", serde(bound(serialize = "", deserialize = "")))] +pub struct BlsSignature(pub(crate) S::InnerSig); + +dlgt_codec!(for[S: BlsScheme] BlsSignature => BlsSigBytes, Hash256, BlsError, BLS_SIG_LEN); + +impl BlsSignature { + /// Deserialize from 96 bytes. + /// + /// # Errors + /// + /// Returns `InvalidSignature` when the bytes are not a valid point. + pub fn from_bytes(bytes: &[u8; 96]) -> Result { + S::sig_from_bytes(bytes).map(Self) + } + + /// Serialize to 96 bytes. + pub fn to_bytes(&self) -> [u8; 96] { + S::sig_to_bytes(&self.0) + } + + /// Verify over a message of the scheme's message type. + /// + /// # Errors + /// + /// Returns `VerifyFailed` when the pairing check does not hold. + pub fn verify(&self, msg: &S::Msg, pk: &BlsPublicKey) -> Result<(), BlsError> { + S::verify(&self.0, msg, &pk.0) + } + + pub(crate) fn from_inner(inner: S::InnerSig) -> Self { + Self(inner) + } +} + +impl BlsSignature { + /// Verify under the domain separation tag selected by `scheme`. + /// + /// # Errors + /// + /// Returns `VerifyFailed` when the pairing check does not hold. + pub fn verify_with(&self, msg: &[u8], pk: &BlsPublicKey, scheme: BlsSigId) -> Result<(), BlsError> { + BlsScIetf::verify_with(&self.0, msg, &pk.0, scheme) + } +} + +impl Clone for BlsSignature { + fn clone(&self) -> Self { + Self(self.0.clone()) + } +} + +impl Debug for BlsSignature { + fn fmt(&self, f: &mut Formatter<'_>) -> FmtResult { + qtypestr(f, core::any::type_name::())?; + write!(f, "({})", self.to_bytes().as_hex()) + } +} + +impl PartialEq for BlsSignature { + fn eq(&self, other: &Self) -> bool { + self.0 == other.0 + } +} + +impl Eq for BlsSignature {} + +impl Hash for BlsSignature { + fn hash(&self, state: &mut H) { + self.to_bytes().hash(state); + } +} + +impl TypeId for BlsSignature { + const TYPE_ID: u32 = S::SIG_TYPE_ID; +} + +type_cvrt!(for[S: BlsScheme] From> for BlsSigBytes, |sig| { + Self::from_bytes(sig.to_bytes()) +}); + +type_cvrt!(for[S: BlsScheme] TryFrom> for BlsSignature, BlsError, |bytes| { + Self::from_bytes(bytes.as_bytes()) +}); + +#[cfg(all(test, feature = "tests"))] +#[expect(clippy::unwrap_used, reason = "test code")] +mod tests { + use super::*; + use crate::bls::secret_ops::BlsSecretKey; + use crate::bls::tests::{MSG_DEADBEEF, SEED_0, SEED_1}; + use crate::bls::{BlsScChia, BlsScIetf}; + use crate::prelude::*; + + use cfg_if::cfg_if; + use dash_dev::{arr_from_hex, Corpus}; + use hex_conservative::DisplayHex; + use rstest::rstest; + use serde::Deserialize; + + #[derive(Deserialize)] + struct SigSerVec { + sig_legacy: String, + sig_ietf: String, + } + + #[derive(Deserialize)] + struct SignVec { + sk: String, + msg: String, + sig: String, + } + + fn assert_sign_verify() { + let sk = BlsSecretKey::::generate(&SEED_0).unwrap(); + let pk = sk.public_key(); + let sig = sk.sign(S::msg_ref(&MSG_DEADBEEF)); + + assert!(sig.verify(S::msg_ref(&MSG_DEADBEEF), &pk).is_ok()); + assert!(sig.verify(S::msg_ref(&[0x42; 32]), &pk).is_err()); + + let other_pk = BlsSecretKey::::generate(&SEED_1).unwrap().public_key(); + assert!(sig.verify(S::msg_ref(&MSG_DEADBEEF), &other_pk).is_err()); + } + + #[rstest] + #[case::chia(assert_sign_verify::)] + #[case::ietf(assert_sign_verify::)] + fn signing_roundtrip_and_rejections(#[case] assertion: fn()) { + assertion(); + } + + /// BLS signing draws no randomness, so the same key over the same message + /// yields the same signature every time. + fn assert_sign_is_deterministic() { + let sk = BlsSecretKey::::generate(&SEED_0).unwrap(); + let msg = S::msg_ref(&MSG_DEADBEEF); + assert_eq!(sk.sign(msg), sk.sign(msg)); + } + + #[rstest] + #[case::chia(assert_sign_is_deterministic::)] + #[case::ietf(assert_sign_is_deterministic::)] + fn signing_is_deterministic(#[case] assertion: fn()) { + assertion(); + } + + fn assert_sig_roundtrip() { + let sk = BlsSecretKey::::generate(&SEED_0).unwrap(); + let bytes = sk.sign(S::msg_ref(&MSG_DEADBEEF)).to_bytes(); + assert_eq!(BlsSignature::::from_bytes(&bytes).unwrap().to_bytes(), bytes); + } + + #[rstest] + #[case::chia(assert_sig_roundtrip::)] + #[case::ietf(assert_sig_roundtrip::)] + fn serialization_roundtrip(#[case] assertion: fn()) { + assertion(); + } + + /// The legacy decoder rejects the all-zero encoding and the infinity marker + /// rather than yielding an identity signature. + #[rstest] + fn chia_rejects_identity_signature() { + assert!(BlsSignature::::from_bytes(&[0u8; 96]).is_err()); + + let mut infinity = [0u8; 96]; + infinity[0] = 0xc0; + assert!(BlsSignature::::from_bytes(&infinity).is_err()); + } + + /// Only bit 7 of byte 0 is the legacy sign flag, and unlike G1 the legacy + /// G2 decoder rejects stray high bits rather than masking them, at the sign + /// byte (index 0) and the swizzled `x.c1` byte (index 48) alike. + /// + /// Rejecting at the decoder is stricter than decoding the bits into an + /// out-of-range point and failing later, but a point so decoded never + /// verifies, so the two agree on every observable outcome. + #[rstest] + #[case::sign_byte(0, 0x20)] + #[case::swizzled_byte(48, 0x40)] + fn chia_rejects_stray_signature_bits(#[case] index: usize, #[case] mask: u8) { + let clean = BlsSecretKey::::generate(&SEED_0) + .unwrap() + .sign(&MSG_DEADBEEF) + .to_bytes(); + + let mut mutated = clean; + mutated[index] |= mask; + assert!(BlsSignature::::from_bytes(&mutated).is_err()); + } + + /// The IETF decoder runs `validate`, which rejects the identity even though + /// its encoding is canonical. + #[rstest] + fn ietf_rejects_identity_signature() { + let mut infinity = [0u8; 96]; + infinity[0] = 0xc0; + assert!(BlsSignature::::from_bytes(&infinity).is_err()); + assert!(BlsSignature::::from_bytes(&[0u8; 96]).is_err()); + } + + /// The same G2 point encodes differently under the two schemes, and each + /// encoding must round-trip through the wrapper of its own scheme. + #[rstest] + fn serialization_formats_match_vectors() { + let corpus = Corpus::open(env!("CARGO_MANIFEST_DIR"), "bls_chia_ser_internals"); + let vecs: Vec = corpus.vectors("sig_serialization"); + + for v in &vecs { + let legacy = BlsSignature::::from_bytes(&arr_from_hex(&v.sig_legacy)).unwrap(); + assert_eq!(legacy.to_bytes().to_lower_hex_string(), v.sig_legacy); + + let ietf = BlsSignature::::from_bytes(&arr_from_hex(&v.sig_ietf)).unwrap(); + assert_eq!(ietf.to_bytes().to_lower_hex_string(), v.sig_ietf); + + assert_ne!(v.sig_legacy, v.sig_ietf, "legacy and ietf should differ"); + } + } + + /// The scheme-level KAT pins `BlsScheme::sign`; this pins that the wrapper's + /// byte-oriented bridge is still wired to it, message length checks included. + fn assert_signing_matches_vectors(corpus: &str) { + let corpus = Corpus::open(env!("CARGO_MANIFEST_DIR"), corpus); + let vecs: Vec = corpus.vectors("sign"); + + for v in &vecs { + let sk = BlsSecretKey::::from_bytes(&arr_from_hex(&v.sk)).unwrap(); + let sig = sk.sign(S::msg_ref(&arr_from_hex(&v.msg))); + assert_eq!(sig.to_bytes().to_lower_hex_string(), v.sig); + } + } + + #[rstest] + #[case::chia(assert_signing_matches_vectors::, "bls_chia_sign")] + #[case::ietf(assert_signing_matches_vectors::, "bls_ietf_sign")] + fn signing_matches_vectors(#[case] assertion: fn(&str), #[case] corpus: &str) { + assertion(corpus); + } + + /// One secret scalar over one message yields two different signatures, so a + /// scheme mix-up cannot go unnoticed. + #[rstest] + fn signatures_differ_across_schemes() { + let chia = BlsSecretKey::::generate(&SEED_0).unwrap(); + let ietf = BlsSecretKey::::from_bytes(&chia.to_bytes()).unwrap(); + assert_ne!(chia.sign(&MSG_DEADBEEF).to_bytes(), ietf.sign(&MSG_DEADBEEF).to_bytes()); + } + + cfg_if! { + if #[cfg(feature = "serde")] { + use dash_dev::assert_json_rt; + + /// The wrapper serializes through the byte bag, so the round-trip is + /// pinned per scheme. + #[rstest] + fn serde_roundtrip() { + let chia = BlsSecretKey::::generate(&SEED_0).unwrap(); + assert_json_rt(&chia.sign(&MSG_DEADBEEF)); + let ietf = BlsSecretKey::::generate(&SEED_0).unwrap(); + assert_json_rt(&ietf.sign(&MSG_DEADBEEF)); + } + } + } +} diff --git a/pkgs/pkc/src/bls_chia/agg.rs b/pkgs/pkc/src/bls_chia/agg.rs index 27201246..70444e5f 100644 --- a/pkgs/pkc/src/bls_chia/agg.rs +++ b/pkgs/pkc/src/bls_chia/agg.rs @@ -6,8 +6,7 @@ //! Aggregation and secure verification for legacy BLS. -use super::sig::Signature; -use super::PublicKey; +use super::{PublicKey, Signature}; use crate::bls::scheme_ops::BlsScheme; use crate::bls::{BlsError, BlsScChia}; use crate::prelude::*; diff --git a/pkgs/pkc/src/bls_chia/mod.rs b/pkgs/pkc/src/bls_chia/mod.rs index eb1b3618..2deaea32 100644 --- a/pkgs/pkc/src/bls_chia/mod.rs +++ b/pkgs/pkc/src/bls_chia/mod.rs @@ -7,14 +7,12 @@ //! Legacy BLS signatures (non-standard hash-to-G2, min-pubkey-size). mod agg; -mod sig; pub mod threshold; pub use crate::bls::BlsError; pub use agg::{aggregate_sig, fast_verify_aggregates, secure_verify_aggregates, verify_aggregates}; -pub use sig::Signature; /// A legacy BLS public key (48-byte G1 point in legacy serialization). pub type PublicKey = crate::bls::BlsPublicKey; @@ -22,6 +20,9 @@ pub type PublicKey = crate::bls::BlsPublicKey; /// A legacy BLS secret key (32-byte scalar). pub type SecretKey = crate::bls::BlsSecretKey; +/// A legacy BLS signature (96-byte G2 point in legacy serialization). +pub type Signature = crate::bls::BlsSignature; + // Compile-time contract: must match bls_ietf's shared API surface. const _: () = { use crate::common::bls::contract::*; diff --git a/pkgs/pkc/src/bls_chia/sig.rs b/pkgs/pkc/src/bls_chia/sig.rs deleted file mode 100644 index 44f2de03..00000000 --- a/pkgs/pkc/src/bls_chia/sig.rs +++ /dev/null @@ -1,69 +0,0 @@ -// -// Copyright (c) 2026-present, The Dash Core developers -// SPDX-License-Identifier: MIT -// See the accompanying file LICENSE or https://opensource.org/license/MIT -// - -//! Legacy BLS signature (96-byte G2 point, legacy serialization). - -use super::{PublicKey, SecretKey}; -use crate::bls::blst_ffi::G2Affine; -use crate::bls::scheme_ops::BlsScheme; -use crate::bls::{BlsError, BlsScChia, BlsSigBytes}; - -use dash_types::Unencodable; - -/// A legacy BLS signature (96-byte G2 point in legacy serialization). -#[derive(Clone, Debug, Eq, PartialEq, Unencodable)] -#[cfg_attr(feature = "serde", derive(::serde::Serialize, ::serde::Deserialize))] -#[cfg_attr( - feature = "serde", - serde(into = "BlsSigBytes", try_from = "BlsSigBytes",) -)] -pub struct Signature(pub(super) G2Affine); - -impl Signature { - pub(super) fn from_inner(inner: G2Affine) -> Self { - Self(inner) - } - - /// Deserialize from 96 legacy-format bytes. - pub fn from_bytes(bytes: &[u8; 96]) -> Result { - BlsScChia::sig_from_bytes(bytes).map(Self) - } - - /// Serialize to 96 legacy-format bytes. - pub fn to_bytes(&self) -> [u8; 96] { - BlsScChia::sig_to_bytes(&self.0) - } - - /// Verify against a 32-byte message and public key via pairing check: - /// e(sig, G1) == e(H(msg), pk). - pub fn verify(&self, msg: &[u8; 32], pk: &PublicKey) -> Result<(), BlsError> { - BlsScChia::verify(&self.0, msg, &pk.0) - } -} - -impl SecretKey { - /// Sign a 32-byte message hash using the legacy scheme (no DST, Shallue-van - /// de Woestijne hash-to-G2). - pub fn sign(&self, msg: &[u8; 32]) -> Signature { - Signature::from_inner(BlsScChia::sign(&self.0, msg)) - } -} - -crate::common::bls::impl_hash_via_bytes!(Signature); - -impl From for BlsSigBytes { - fn from(sig: Signature) -> Self { - Self::from_bytes(sig.to_bytes()) - } -} - -impl TryFrom> for Signature { - type Error = BlsError; - - fn try_from(bytes: BlsSigBytes) -> Result { - Self::from_bytes(bytes.as_bytes()) - } -} diff --git a/pkgs/pkc/src/bls_chia/threshold.rs b/pkgs/pkc/src/bls_chia/threshold.rs index 54525a14..d9b865e6 100644 --- a/pkgs/pkc/src/bls_chia/threshold.rs +++ b/pkgs/pkc/src/bls_chia/threshold.rs @@ -6,8 +6,7 @@ //! Thresholds for legacy scheme (m-of-n secret sharing and signature recovery). -use super::sig::Signature; -use super::{PublicKey, SecretKey}; +use super::{PublicKey, SecretKey, Signature}; use crate::bls::scheme_ops::BlsScheme; use crate::bls::{BlsError, BlsScChia}; use crate::prelude::*; diff --git a/pkgs/pkc/src/bls_ietf/agg.rs b/pkgs/pkc/src/bls_ietf/agg.rs index 775985dd..c549bb41 100644 --- a/pkgs/pkc/src/bls_ietf/agg.rs +++ b/pkgs/pkc/src/bls_ietf/agg.rs @@ -6,8 +6,7 @@ //! Aggregation and batch verification for IETF BLS. -use super::sig::Signature; -use super::PublicKey; +use super::{PublicKey, Signature}; use crate::bls::scheme_ietf::DST_BASIC; use crate::bls::scheme_ops::{verify_ok, BlsScheme}; use crate::bls::{BlsError, BlsScIetf}; diff --git a/pkgs/pkc/src/bls_ietf/mod.rs b/pkgs/pkc/src/bls_ietf/mod.rs index b68af4b4..14bf594c 100644 --- a/pkgs/pkc/src/bls_ietf/mod.rs +++ b/pkgs/pkc/src/bls_ietf/mod.rs @@ -16,7 +16,6 @@ pub use crate::bls::BlsError; pub use crate::bls::BlsSigId as Scheme; pub use agg::{aggregate_sig, fast_verify_aggregates, secure_verify_aggregates, verify_aggregates}; -pub use sig::Signature; /// An IETF BLS public key (48-byte compressed G1 point). pub type PublicKey = crate::bls::BlsPublicKey; @@ -24,6 +23,9 @@ pub type PublicKey = crate::bls::BlsPublicKey; /// An IETF BLS secret key (32-byte scalar). pub type SecretKey = crate::bls::BlsSecretKey; +/// An IETF BLS signature (96-byte compressed G2 point). +pub type Signature = crate::bls::BlsSignature; + // Compile-time contract: if any of these methods are // removed or their signatures change, this block fails. const _: () = { diff --git a/pkgs/pkc/src/bls_ietf/sig.rs b/pkgs/pkc/src/bls_ietf/sig.rs index 2cc574b4..e3217406 100644 --- a/pkgs/pkc/src/bls_ietf/sig.rs +++ b/pkgs/pkc/src/bls_ietf/sig.rs @@ -6,85 +6,12 @@ //! IETF BLS signature (96-byte compressed G2 point). -use super::{PublicKey, Scheme, SecretKey}; -use crate::bls::scheme_ietf::{DST_BASIC, DST_POP, DST_POP_PROVE}; -use crate::bls::scheme_ops::{verify_ok, BlsScheme}; -use crate::bls::{BlsError, BlsScIetf, BlsSigBytes}; - -use blst::min_pk; -use dash_types::Unencodable; - -/// A BLS signature (96-byte compressed G2 point). -#[derive(Clone, Debug, Eq, PartialEq, Unencodable)] -#[cfg_attr(feature = "serde", derive(::serde::Serialize, ::serde::Deserialize))] -#[cfg_attr( - feature = "serde", - serde(into = "BlsSigBytes", try_from = "BlsSigBytes",) -)] -pub struct Signature(pub(super) min_pk::Signature); - -impl Signature { - pub(super) fn from_inner(inner: min_pk::Signature) -> Self { - Self(inner) - } - - /// Deserialize from 96 compressed bytes. - /// - /// # Errors - /// - /// Returns [`BlsError::InvalidSignature`] when the bytes are not a valid - /// encoding or the point fails `validate` (identity or non-prime-order). - pub fn from_bytes(bytes: &[u8; 96]) -> Result { - BlsScIetf::sig_from_bytes(bytes).map(Self) - } - - /// Serialize to 96 compressed bytes. - pub fn to_bytes(&self) -> [u8; 96] { - BlsScIetf::sig_to_bytes(&self.0) - } - - /// Verify with the Basic scheme. - /// - /// # Errors - /// - /// Returns [`BlsError::VerifyFailed`] if the signature does not verify. - pub fn verify(&self, msg: &[u8], pk: &PublicKey) -> Result<(), BlsError> { - BlsScIetf::verify(&self.0, msg, &pk.0) - } - - /// Verify with a specific scheme. - /// - /// # Errors - /// - /// Returns [`BlsError::VerifyFailed`] if the signature does not verify. - pub fn verify_with(&self, msg: &[u8], pk: &PublicKey, scheme: Scheme) -> Result<(), BlsError> { - let dst = match scheme { - Scheme::Basic => DST_BASIC, - Scheme::ProofOfPossession => DST_POP, - }; - self.verify_raw(msg, pk, dst) - } - - fn verify_raw(&self, msg: &[u8], pk: &PublicKey, dst: &[u8]) -> Result<(), BlsError> { - verify_ok(self.0.verify(true, msg, dst, &[], &pk.0, true)) - } -} +use super::{PublicKey, SecretKey, Signature}; +use crate::bls::scheme_ietf::DST_POP_PROVE; +use crate::bls::scheme_ops::verify_ok; +use crate::bls::BlsError; impl SecretKey { - /// Sign with the Basic scheme. - pub fn sign(&self, msg: &[u8]) -> Signature { - Signature::from_inner(BlsScIetf::sign(&self.0, msg)) - } - - /// Sign with a specific scheme. - pub fn sign_with(&self, msg: &[u8], scheme: Scheme) -> Signature { - let dst = match scheme { - Scheme::Basic => DST_BASIC, - Scheme::ProofOfPossession => DST_POP, - }; - Signature::from_inner(self.0.sign(msg, dst, &[])) - } - /// Produce a proof of possession by signing the serialized public key with /// the PoP DST. pub fn prove_possession(&self) -> Signature { @@ -104,19 +31,3 @@ impl PublicKey { verify_ok(pop.0.verify(true, &pk_bytes, DST_POP_PROVE, &[], &self.0, true)) } } - -crate::common::bls::impl_hash_via_bytes!(Signature); - -impl From for BlsSigBytes { - fn from(sig: Signature) -> Self { - Self::from_bytes(sig.to_bytes()) - } -} - -impl TryFrom> for Signature { - type Error = BlsError; - - fn try_from(bytes: BlsSigBytes) -> Result { - Self::from_bytes(bytes.as_bytes()) - } -} diff --git a/pkgs/pkc/src/bls_ietf/threshold.rs b/pkgs/pkc/src/bls_ietf/threshold.rs index e937547c..b6b7ec1e 100644 --- a/pkgs/pkc/src/bls_ietf/threshold.rs +++ b/pkgs/pkc/src/bls_ietf/threshold.rs @@ -6,8 +6,7 @@ //! Thresholds for IETF scheme (m-of-n secret sharing and signature recovery). -use super::sig::Signature; -use super::{PublicKey, SecretKey}; +use super::{PublicKey, SecretKey, Signature}; use crate::bls::scheme_ops::BlsScheme; use crate::bls::{BlsError, BlsScIetf}; use crate::prelude::*; diff --git a/pkgs/pkc/src/common/bls/mod.rs b/pkgs/pkc/src/common/bls/mod.rs index 0c89cc67..eb8353da 100644 --- a/pkgs/pkc/src/common/bls/mod.rs +++ b/pkgs/pkc/src/common/bls/mod.rs @@ -8,15 +8,3 @@ #[expect(dead_code, reason = "compile-time contracts, unused at runtime")] pub(crate) mod contract; - -/// Implement Hash via to_bytes() for a BLS type. -macro_rules! impl_hash_via_bytes { - ($ty:ty) => { - impl ::core::hash::Hash for $ty { - fn hash(&self, state: &mut H) { - self.to_bytes().hash(state); - } - } - }; -} -pub(crate) use impl_hash_via_bytes; diff --git a/pkgs/pkc/tests/bls_chia_ser.rs b/pkgs/pkc/tests/bls_chia_ser.rs deleted file mode 100644 index 84a77e7f..00000000 --- a/pkgs/pkc/tests/bls_chia_ser.rs +++ /dev/null @@ -1,118 +0,0 @@ -// -// Copyright (c) 2026-present, The Dash Core developers -// SPDX-License-Identifier: MIT -// See the accompanying file LICENSE or https://opensource.org/license/MIT -// - -//! Serialization format KAT tests for bls_chia. - -#![expect(clippy::unwrap_used, reason = "test code")] - -mod kat { - use dash_dev::{arr_from_hex, Corpus}; - use hex_conservative::DisplayHex; - use serde::Deserialize; - - #[derive(Deserialize)] - struct SerInternalVector { - pk_legacy: String, - pk_ietf: String, - } - - /// Validate that the same G1 point serializes differently - /// under legacy vs IETF formats. - #[test] - fn kat_ser_pk_formats() { - let corpus = Corpus::open(env!("CARGO_MANIFEST_DIR"), "bls_chia_ser_internals"); - let vecs: Vec = corpus.vectors("pk_serialization"); - - for v in &vecs { - // Legacy bytes should deserialize and re-serialize - // identically. - let legacy_bytes: [u8; 48] = arr_from_hex(&v.pk_legacy); - let pk = dash_pkc::bls_chia::PublicKey::from_bytes(&legacy_bytes).unwrap(); - assert_eq!( - pk.to_bytes().to_lower_hex_string(), - v.pk_legacy, - "legacy pk roundtrip mismatch" - ); - - // The two formats must differ for the same point. - assert_ne!(v.pk_legacy, v.pk_ietf, "legacy and ietf should differ"); - } - } -} - -/// The identity passes the subgroup check yet verifies any message, so the -/// decoder rejects the canonical infinity encoding for both public keys and -/// signatures. -#[test] -fn rejects_identity_public_key() { - let mut bytes = [0u8; 48]; - bytes[0] = 0xc0; // compressed identity marker - assert!(dash_pkc::bls_chia::PublicKey::from_bytes(&bytes).is_err()); -} - -#[test] -fn rejects_identity_signature() { - let mut bytes = [0u8; 96]; - bytes[0] = 0xc0; // compressed identity marker - assert!(dash_pkc::bls_chia::Signature::from_bytes(&bytes).is_err()); -} - -/// Only bit 7 of byte 0 is the legacy sign flag. Stray high bits are -/// masked in G1 (the reference masks them too) but rejected in G2, where -/// the reference reads them as an out-of-range `x >= p` coordinate. -#[test] -fn masks_g1_and_rejects_g2_stray_high_bits() { - let sk = dash_pkc::bls_chia::SecretKey::generate(&[7u8; 32]).unwrap(); - - let clean = sk.public_key().to_bytes(); - let mut pk = clean; - pk[0] |= 0x20; - let decoded = dash_pkc::bls_chia::PublicKey::from_bytes(&pk).unwrap(); - assert_eq!(decoded.to_bytes(), clean); - - let sig = sk.sign(&[0x55u8; 32]).to_bytes(); - for (index, mask) in [(0, 0x20), (48, 0x40)] { - let mut bytes = sig; - bytes[index] |= mask; - assert!(dash_pkc::bls_chia::Signature::from_bytes(&bytes).is_err()); - } -} - -/// The y-sign convention (c1 compared against `(p-1)/2`) must be stable -/// across a serialize/parse/serialize round-trip. -#[test] -fn signature_serialization_is_idempotent() { - let sk = dash_pkc::bls_chia::SecretKey::generate(&[9u8; 32]).unwrap(); - let once = sk.sign(&[0x44u8; 32]).to_bytes(); - let twice = dash_pkc::bls_chia::Signature::from_bytes(&once).unwrap().to_bytes(); - assert_eq!(once, twice); -} - -/// Legacy G2 round-trip through the public wrapper. -/// -/// The scheme-level KAT pins `BlsScChia::sig_{from,to}_bytes`; this pins -/// that `Signature::{from,to}_bytes` is still wired to it. -mod kat_sig { - use dash_dev::{arr_from_hex, Corpus}; - use hex_conservative::DisplayHex; - use serde::Deserialize; - - #[derive(Deserialize)] - struct SigSerVector { - sig_legacy: String, - sig_ietf: String, - } - - #[test] - fn public_api_legacy_sig_roundtrips() { - let corpus = Corpus::open(env!("CARGO_MANIFEST_DIR"), "bls_chia_ser_internals"); - for v in corpus.vectors::("sig_serialization") { - let sig = dash_pkc::bls_chia::Signature::from_bytes(&arr_from_hex(&v.sig_legacy)).unwrap(); - assert_eq!(sig.to_bytes().to_lower_hex_string(), v.sig_legacy); - assert_ne!(v.sig_legacy, v.sig_ietf, "legacy and ietf should differ"); - } - } -} diff --git a/pkgs/pkc/tests/bls_chia_sign.rs b/pkgs/pkc/tests/bls_chia_sign.rs deleted file mode 100644 index ed327e7e..00000000 --- a/pkgs/pkc/tests/bls_chia_sign.rs +++ /dev/null @@ -1,126 +0,0 @@ -// -// Copyright (c) 2026-present, The Dash Core developers -// SPDX-License-Identifier: MIT -// See the accompanying file LICENSE or https://opensource.org/license/MIT -// - -//! Signing and verification tests for bls_chia. - -#![expect(clippy::unwrap_used, reason = "test code")] - -use common::*; -#[cfg(feature = "serde")] -use dash_dev::assert_json_rt; -use dash_pkc::{bls::tests as common, bls_chia::SecretKey, bls_chia::Signature}; -use rstest::*; - -/// Sign then verify round-trips. -#[rstest] -fn sign_verify_roundtrip(chia_sk0: SecretKey, msg32: [u8; 32]) { - let sig = chia_sk0.sign(&msg32); - let pk = chia_sk0.public_key(); - assert!(sig.verify(&msg32, &pk).is_ok()); -} - -/// Verification rejects a tampered message. -#[rstest] -fn verify_rejects_wrong_message(chia_sk0: SecretKey, msg32: [u8; 32]) { - let sig = chia_sk0.sign(&msg32); - let mut bad = msg32; - bad[0] ^= 0xff; - assert!(sig.verify(&bad, &chia_sk0.public_key()).is_err()); -} - -/// Verification rejects a different signer's key. -#[rstest] -fn verify_rejects_wrong_key(chia_sk0: SecretKey, chia_sk1: SecretKey, msg32: [u8; 32]) { - let sig = chia_sk0.sign(&msg32); - assert!(sig.verify(&msg32, &chia_sk1.public_key()).is_err()); -} - -/// Legacy BLS signing is deterministic. -#[rstest] -fn sign_is_deterministic(chia_sk0: SecretKey, msg32: [u8; 32]) { - let sig1 = chia_sk0.sign(&msg32); - let sig2 = chia_sk0.sign(&msg32); - assert_eq!(sig1, sig2); -} - -/// Legacy signature round-trips (96 bytes). -#[rstest] -fn sig_roundtrip(chia_sk0: SecretKey, msg32: [u8; 32]) { - let sig = chia_sk0.sign(&msg32); - let bytes = sig.to_bytes(); - assert_eq!(bytes.len(), 96); - let restored = Signature::from_bytes(&bytes).unwrap(); - assert_eq!(restored, sig); -} - -/// Serde round-trip for Signature. -#[cfg(feature = "serde")] -#[rstest] -fn serde_sig_roundtrip(chia_sk0: SecretKey, msg32: [u8; 32]) { - let sig = chia_sk0.sign(&msg32); - assert_json_rt(&sig); -} - -/// Same signature serialized under legacy and IETF formats -/// must produce different bytes. -#[rstest] -fn cross_format_sig_differs(chia_sk0: SecretKey, msg32: [u8; 32]) { - let legacy_sig = chia_sk0.sign(&msg32).to_bytes(); - let ietf_sk = dash_pkc::bls_ietf::SecretKey::from_bytes(&chia_sk0.to_bytes()).unwrap(); - let ietf_sig = ietf_sk.sign(&msg32).to_bytes(); - assert_ne!(legacy_sig, ietf_sig, "same point must serialize differently"); -} - -/// Same key material produces different signatures under legacy -/// and IETF schemes (different hash-to-G2). -#[rstest] -fn legacy_sig_differs_from_ietf() { - let ikm = [0u8; 32]; - let legacy_sk = dash_pkc::bls_chia::SecretKey::generate(&ikm).unwrap(); - let ietf_sk = dash_pkc::bls_ietf::SecretKey::generate(&ikm).unwrap(); - assert_eq!(legacy_sk.to_bytes(), ietf_sk.to_bytes()); - - let msg = [0x42u8; 32]; - let legacy_sig = legacy_sk.sign(&msg); - let ietf_sig = ietf_sk.sign(&msg); - assert_ne!(legacy_sig.to_bytes(), ietf_sig.to_bytes()); -} - -/// Same curve point, different wire format. -#[rstest] -fn legacy_pk_serialization_differs_from_ietf() { - let ikm = [0u8; 32]; - let legacy_pk = dash_pkc::bls_chia::SecretKey::generate(&ikm).unwrap().public_key(); - let ietf_pk = dash_pkc::bls_ietf::SecretKey::generate(&ikm).unwrap().public_key(); - assert_ne!(legacy_pk.to_bytes(), ietf_pk.to_bytes()); -} - -/// Reference vectors through the public wrapper. -/// -/// The scheme-level KAT in `bls::scheme_chia` pins `BlsScChia::sign`; this -/// pins that `SecretKey::sign` is still wired to it. -mod kat { - use dash_dev::{arr_from_hex, Corpus}; - use hex_conservative::DisplayHex; - use serde::Deserialize; - - #[derive(Deserialize)] - struct SignVector { - sk: String, - msg: String, - sig: String, - } - - #[test] - fn public_api_signing_matches_vectors() { - let corpus = Corpus::open(env!("CARGO_MANIFEST_DIR"), "bls_chia_sign"); - for v in corpus.vectors::("sign") { - let sk = dash_pkc::bls_chia::SecretKey::from_bytes(&arr_from_hex(&v.sk)).unwrap(); - let msg: [u8; 32] = arr_from_hex(&v.msg); - assert_eq!(sk.sign(&msg).to_bytes().to_lower_hex_string(), v.sig); - } - } -} diff --git a/pkgs/pkc/tests/bls_ietf_sign.rs b/pkgs/pkc/tests/bls_ietf_sign.rs deleted file mode 100644 index 2735491d..00000000 --- a/pkgs/pkc/tests/bls_ietf_sign.rs +++ /dev/null @@ -1,114 +0,0 @@ -// -// Copyright (c) 2026-present, The Dash Core developers -// SPDX-License-Identifier: MIT -// See the accompanying file LICENSE or https://opensource.org/license/MIT -// - -//! Signing and verification tests for bls_ietf. - -#![expect(clippy::unwrap_used, reason = "test code")] - -use common::*; -#[cfg(feature = "serde")] -use dash_dev::assert_json_rt; -use dash_pkc::{bls::tests as common, bls_ietf::SecretKey, bls_ietf::Signature}; -use hex_conservative::hex; -use rstest::*; - -/// Sign then verify with a generated key succeeds and is -/// deterministic. -#[rstest] -fn sign_verify_known_key() { - let sk = SecretKey::generate(&RSEED[0]).unwrap(); - let msg = hex!("070809"); - let sig = sk.sign(&msg); - assert!(sig.verify(&msg, &sk.public_key()).is_ok()); - assert_eq!(sk.sign(&msg).to_bytes(), sig.to_bytes()); -} - -/// Sign then verify round-trips. -#[rstest] -fn sign_verify_roundtrip(ietf_sk0: SecretKey) { - let msg = b"hello dash"; - let sig = ietf_sk0.sign(msg); - assert!(sig.verify(msg, &ietf_sk0.public_key()).is_ok()); -} - -/// Verification rejects a tampered message. -#[rstest] -fn verify_rejects_wrong_message(ietf_sk0: SecretKey) { - let sig = ietf_sk0.sign(b"right"); - assert!(sig.verify(b"wrong", &ietf_sk0.public_key()).is_err()); -} - -/// Verification rejects a different signer's key. -#[rstest] -fn verify_rejects_wrong_key(ietf_sk0: SecretKey, ietf_sk1: SecretKey) { - let sig = ietf_sk0.sign(b"msg"); - assert!(sig.verify(b"msg", &ietf_sk1.public_key()).is_err()); -} - -/// Compressed signature round-trips (96 bytes). -#[rstest] -fn sig_roundtrip(ietf_sk0: SecretKey) { - let sig = ietf_sk0.sign(b"test"); - let bytes = sig.to_bytes(); - assert_eq!(bytes.len(), 96); - let restored = Signature::from_bytes(&bytes).unwrap(); - assert_eq!(restored, sig); -} - -/// BLS signing is deterministic. -#[rstest] -fn sign_is_deterministic(ietf_sk0: SecretKey) { - let msg = b"determinism check"; - let sig1 = ietf_sk0.sign(msg); - let sig2 = ietf_sk0.sign(msg); - assert_eq!(sig1, sig2); -} - -/// Serde round-trip for Signature. -#[cfg(feature = "serde")] -#[rstest] -fn serde_sig_roundtrip(ietf_sk0: SecretKey) { - let sig = ietf_sk0.sign(b"serde test"); - assert_json_rt(&sig); -} - -/// Same signature under IETF and legacy formats must differ. -#[rstest] -fn cross_format_sig_differs(ietf_sk0: SecretKey) { - // Sign the same 32-byte message on both paths; only the format differs. - let msg = [0x42u8; 32]; - let ietf_sig = ietf_sk0.sign(&msg).to_bytes(); - let legacy_sk = dash_pkc::bls_chia::SecretKey::from_bytes(&ietf_sk0.to_bytes()).unwrap(); - let legacy_sig = legacy_sk.sign(&msg).to_bytes(); - assert_ne!(ietf_sig, legacy_sig, "same key must produce different sigs"); -} - -/// Reference vectors through the public wrapper. -/// -/// The scheme-level KAT in `bls::scheme_ietf` pins `BlsScIetf::sign`; this -/// pins that `SecretKey::sign` is still wired to it, DST included. -mod kat { - use dash_dev::{arr_from_hex, vec_from_hex, Corpus}; - use hex_conservative::DisplayHex; - use serde::Deserialize; - - #[derive(Deserialize)] - struct SignVector { - sk: String, - msg: String, - sig: String, - } - - #[test] - fn public_api_signing_matches_vectors() { - let corpus = Corpus::open(env!("CARGO_MANIFEST_DIR"), "bls_ietf_sign"); - for v in corpus.vectors::("sign") { - let sk = dash_pkc::bls_ietf::SecretKey::from_bytes(&arr_from_hex(&v.sk)).unwrap(); - let sig = sk.sign(&vec_from_hex(&v.msg)); - assert_eq!(sig.to_bytes().to_lower_hex_string(), v.sig); - } - } -} From 2a2c6c317af2d1a7e1a9932900b71fc66002a681 Mon Sep 17 00:00:00 2001 From: Kittywhiskers Van Gogh <63189531+kwvg@users.noreply.github.com> Date: Mon, 10 Aug 2026 13:49:31 +0530 Subject: [PATCH 10/14] pkc%feat(bls): migrate signature aggregation to `BlsSignature` --- pkgs/pkc/Cargo.toml | 8 - pkgs/pkc/bench/bls_chia.rs | 8 +- pkgs/pkc/bench/bls_ietf.rs | 12 +- pkgs/pkc/src/bls/mod.rs | 1 + pkgs/pkc/src/bls/scheme_ietf.rs | 20 +- pkgs/pkc/src/bls/sig_aggregate.rs | 272 +++++++++++++++++++++++++++ pkgs/pkc/src/bls_chia/agg.rs | 50 ----- pkgs/pkc/src/bls_chia/mod.rs | 23 --- pkgs/pkc/src/bls_ietf/agg.rs | 53 ------ pkgs/pkc/src/bls_ietf/mod.rs | 23 --- pkgs/pkc/src/common/bls/contract.rs | 26 --- pkgs/pkc/src/common/bls/mod.rs | 10 - pkgs/pkc/src/common/mod.rs | 10 - pkgs/pkc/src/lib.rs | 2 - pkgs/pkc/tests/bls_chia_aggregate.rs | 177 ----------------- pkgs/pkc/tests/bls_chia_llmq.rs | 6 +- pkgs/pkc/tests/bls_ietf_aggregate.rs | 199 -------------------- pkgs/pkc/tests/bls_ietf_llmq.rs | 6 +- 18 files changed, 308 insertions(+), 598 deletions(-) create mode 100644 pkgs/pkc/src/bls/sig_aggregate.rs delete mode 100644 pkgs/pkc/src/bls_chia/agg.rs delete mode 100644 pkgs/pkc/src/bls_ietf/agg.rs delete mode 100644 pkgs/pkc/src/common/bls/contract.rs delete mode 100644 pkgs/pkc/src/common/bls/mod.rs delete mode 100644 pkgs/pkc/src/common/mod.rs delete mode 100644 pkgs/pkc/tests/bls_chia_aggregate.rs delete mode 100644 pkgs/pkc/tests/bls_ietf_aggregate.rs diff --git a/pkgs/pkc/Cargo.toml b/pkgs/pkc/Cargo.toml index d0f55343..18db8ac6 100644 --- a/pkgs/pkc/Cargo.toml +++ b/pkgs/pkc/Cargo.toml @@ -69,10 +69,6 @@ path = "bench/main.rs" harness = false required-features = ["tests"] -[[test]] -name = "bls_chia_aggregate" -required-features = ["bls", "tests"] - [[test]] name = "bls_chia_llmq" required-features = ["bls", "tests"] @@ -81,10 +77,6 @@ required-features = ["bls", "tests"] name = "bls_chia_threshold" required-features = ["bls", "tests"] -[[test]] -name = "bls_ietf_aggregate" -required-features = ["bls", "tests"] - [[test]] name = "bls_ietf_llmq" required-features = ["bls", "tests"] diff --git a/pkgs/pkc/bench/bls_chia.rs b/pkgs/pkc/bench/bls_chia.rs index 9f63a78a..4717e147 100644 --- a/pkgs/pkc/bench/bls_chia.rs +++ b/pkgs/pkc/bench/bls_chia.rs @@ -8,7 +8,7 @@ use common::*; use dash_pkc::bls::tests as common; -use dash_pkc::bls_chia::{aggregate_sig, verify_aggregates, PublicKey, SecretKey, Signature}; +use dash_pkc::bls_chia::{PublicKey, SecretKey, Signature}; /// Single signature creation (legacy hash-to-G2). #[divan::bench] @@ -57,7 +57,7 @@ fn aggregate_sig_n(bencher: divan::Bencher, n: usize) { let sig_refs: Vec<&Signature> = sigs.iter().collect(); bencher .counter(divan::counter::ItemsCount::new(n)) - .bench(|| aggregate_sig(&sig_refs)); + .bench(|| Signature::aggregate(&sig_refs)); } /// N individual verifications in a loop. @@ -86,11 +86,11 @@ fn verify_aggregated_block(bencher: divan::Bencher, n: usize) { let pks: Vec<_> = keys.iter().map(|k| k.public_key()).collect(); let sigs: Vec<_> = keys.iter().map(|k| k.sign(&msg)).collect(); let sig_refs: Vec<&Signature> = sigs.iter().collect(); - let agg_sig = aggregate_sig(&sig_refs).unwrap(); + let agg_sig = Signature::aggregate(&sig_refs).unwrap(); let pk_refs: Vec<_> = pks.iter().collect(); bencher .counter(divan::counter::ItemsCount::new(n)) - .bench(|| verify_aggregates(&agg_sig, &msg, &pk_refs)); + .bench(|| agg_sig.fast_verify_aggregates(&msg, &pk_refs)); } /// Public key serialization (legacy format). diff --git a/pkgs/pkc/bench/bls_ietf.rs b/pkgs/pkc/bench/bls_ietf.rs index b6cc036b..ac117d28 100644 --- a/pkgs/pkc/bench/bls_ietf.rs +++ b/pkgs/pkc/bench/bls_ietf.rs @@ -8,7 +8,7 @@ use common::*; use dash_pkc::bls::tests as common; -use dash_pkc::bls_ietf::{aggregate_sig, fast_verify_aggregates, verify_aggregates, PublicKey, SecretKey, Signature}; +use dash_pkc::bls_ietf::{PublicKey, SecretKey, Signature}; /// Single signature creation. #[divan::bench] @@ -57,7 +57,7 @@ fn aggregate_sig_n(bencher: divan::Bencher, n: usize) { let sig_refs: Vec<&Signature> = sigs.iter().collect(); bencher .counter(divan::counter::ItemsCount::new(n)) - .bench(|| aggregate_sig(&sig_refs)); + .bench(|| Signature::aggregate(&sig_refs)); } /// N individual verifications in a loop. @@ -86,12 +86,12 @@ fn verify_aggregated_block(bencher: divan::Bencher, n: usize) { let pks: Vec<_> = keys.iter().map(|k| k.public_key()).collect(); let sigs: Vec<_> = keys.iter().zip(msgs.iter()).map(|(k, m)| k.sign(m)).collect(); let sig_refs: Vec<&Signature> = sigs.iter().collect(); - let agg_sig = aggregate_sig(&sig_refs).unwrap(); + let agg_sig = Signature::aggregate(&sig_refs).unwrap(); let pk_refs: Vec<_> = pks.iter().collect(); let msg_slices: Vec<&[u8]> = msgs.iter().map(|m| m.as_slice()).collect(); bencher .counter(divan::counter::ItemsCount::new(n)) - .bench(|| verify_aggregates(&agg_sig, &msg_slices, &pk_refs)); + .bench(|| agg_sig.verify_aggregates(&msg_slices, &pk_refs)); } /// Fast aggregate verify (same message, N signers). @@ -104,11 +104,11 @@ fn fast_verify_n(bencher: divan::Bencher, n: usize) { let pks: Vec<_> = keys.iter().map(|k| k.public_key()).collect(); let sigs: Vec<_> = keys.iter().map(|k| k.sign(&msg)).collect(); let sig_refs: Vec<&Signature> = sigs.iter().collect(); - let agg_sig = aggregate_sig(&sig_refs).unwrap(); + let agg_sig = Signature::aggregate(&sig_refs).unwrap(); let pk_refs: Vec<_> = pks.iter().collect(); bencher .counter(divan::counter::ItemsCount::new(n)) - .bench(|| fast_verify_aggregates(&agg_sig, &msg, &pk_refs)); + .bench(|| agg_sig.fast_verify_aggregates(&msg, &pk_refs)); } /// Public key serialization (compress). diff --git a/pkgs/pkc/src/bls/mod.rs b/pkgs/pkc/src/bls/mod.rs index 7784e7ab..1ebf4096 100644 --- a/pkgs/pkc/src/bls/mod.rs +++ b/pkgs/pkc/src/bls/mod.rs @@ -25,6 +25,7 @@ cfg_if::cfg_if! { mod public_ops; mod scheme_chia; mod secret_ops; + mod sig_aggregate; mod sig_basic; #[expect(unsafe_code, reason = "blst C FFI")] pub(crate) mod blst_ffi; diff --git a/pkgs/pkc/src/bls/scheme_ietf.rs b/pkgs/pkc/src/bls/scheme_ietf.rs index 04f0d11b..ee32739a 100644 --- a/pkgs/pkc/src/bls/scheme_ietf.rs +++ b/pkgs/pkc/src/bls/scheme_ietf.rs @@ -15,7 +15,7 @@ use super::sig_id::BlsSigId; use blst::min_pk::{AggregatePublicKey, AggregateSignature, PublicKey, SecretKey, Signature}; /// Domain separation tag for the basic (NUL) signature scheme. -pub(crate) const DST_BASIC: &[u8] = b"BLS_SIG_BLS12381G2_XMD:SHA-256_SSWU_RO_NUL_"; +const DST_BASIC: &[u8] = b"BLS_SIG_BLS12381G2_XMD:SHA-256_SSWU_RO_NUL_"; /// Domain separation tag for signatures in the proof-of-possession scheme. const DST_POP: &[u8] = b"BLS_SIG_BLS12381G2_XMD:SHA-256_SSWU_RO_POP_"; /// Domain separation tag for proofs of possession. @@ -177,6 +177,24 @@ impl BlsScIetf { }; verify_ok(sig.verify(true, msg, dst, &[], pk, true)) } + + /// Verify an aggregated signature where each signer signed a distinct + /// message. + /// + /// # Errors + /// + /// Returns `CountMismatch` when the message and key counts differ, + /// `EmptyAggregation` when no keys are given, or `VerifyFailed` on + /// mismatch. + pub(crate) fn verify_aggregates(sig: &Signature, msgs: &[&[u8]], pks: &[&PublicKey]) -> Result<(), BlsError> { + if pks.len() != msgs.len() { + return Err(BlsError::CountMismatch); + } + if pks.is_empty() { + return Err(BlsError::EmptyAggregation); + } + verify_ok(sig.aggregate_verify(true, msgs, DST_BASIC, pks, true)) + } } #[cfg(all(test, feature = "tests"))] diff --git a/pkgs/pkc/src/bls/sig_aggregate.rs b/pkgs/pkc/src/bls/sig_aggregate.rs new file mode 100644 index 00000000..a3e60c12 --- /dev/null +++ b/pkgs/pkc/src/bls/sig_aggregate.rs @@ -0,0 +1,272 @@ +// +// Copyright (c) 2026-present, The Dash Core developers +// SPDX-License-Identifier: MIT +// See the accompanying file LICENSE or https://opensource.org/license/MIT +// + +//! BLS signature aggregation and aggregate verification. + +use super::error::BlsError; +use super::public_ops::BlsPublicKey; +use super::scheme_ops::BlsScheme; +use super::sig_basic::BlsSignature; +use super::BlsScIetf; +use crate::prelude::*; + +impl BlsSignature { + /// Aggregate multiple signatures into one. + /// + /// # Errors + /// + /// Returns `EmptyAggregation` when no signatures are given, or + /// `InvalidSignature` when a signature fails to aggregate. + pub fn aggregate(sigs: &[&Self]) -> Result { + let inner_refs: Vec<&S::InnerSig> = sigs.iter().map(|s| &s.0).collect(); + S::aggregate_sig(&inner_refs).map(Self::from_inner) + } + + /// Verify an aggregated signature where every signer signed + /// the same message. + /// + /// Binds only the sum of `pks`, so a key chosen after seeing an honest one + /// can cancel it out. Every key must already be bound to its holder, by a + /// proof of possession or by provenance. + /// + /// Without such a binding, use [`Self::secure_verify_aggregates`], which + /// weights each key. + /// + /// # Errors + /// + /// Returns `EmptyAggregation` when no keys are given, or `VerifyFailed` on + /// mismatch. + pub fn fast_verify_aggregates(&self, msg: &S::Msg, pks: &[&BlsPublicKey]) -> Result<(), BlsError> { + let inner_pks: Vec<&S::InnerPk> = pks.iter().map(|k| &k.0).collect(); + S::fast_verify_aggregates(&self.0, msg, &inner_pks) + } + + /// Securely aggregate and verify signatures with public-key + /// weighting. + /// + /// # Errors + /// + /// Returns `EmptyAggregation` when no keys are given, `InvalidPublicKey` + /// when a key fails to decode, or `VerifyFailed` on mismatch. + pub fn secure_verify_aggregates(&self, msg: &S::Msg, pks: &[&BlsPublicKey]) -> Result<(), BlsError> { + let inner_pks: Vec<&S::InnerPk> = pks.iter().map(|k| &k.0).collect(); + S::secure_verify_aggregates(&self.0, msg, &inner_pks) + } +} + +impl BlsSignature { + /// Verify an aggregated signature where each signer signed a distinct + /// message. + /// + /// # Errors + /// + /// Returns `CountMismatch` when the message and key counts differ, + /// `EmptyAggregation` when no keys are given, or `VerifyFailed` on + /// mismatch. + pub fn verify_aggregates(&self, msgs: &[&[u8]], pks: &[&BlsPublicKey]) -> Result<(), BlsError> { + let inner_pks: Vec<_> = pks.iter().map(|k| &k.0).collect(); + BlsScIetf::verify_aggregates(&self.0, msgs, &inner_pks) + } +} + +#[cfg(all(test, feature = "tests"))] +#[expect(clippy::unwrap_used, reason = "test code")] +mod tests { + use super::*; + use crate::bls::secret_ops::BlsSecretKey; + use crate::bls::tests::{MSG_DEADBEEF, SEED_0, SEED_1}; + use crate::bls::{BlsScChia, BlsScIetf}; + + use dash_dev::{arr_from_hex, Corpus}; + use hex_conservative::DisplayHex; + use rstest::rstest; + use serde::Deserialize; + + #[derive(Deserialize)] + struct SecureVec { + msg: String, + pks: Vec, + agg_sig_secure: String, + } + + #[derive(Deserialize)] + struct AggSigVec { + sigs: Vec, + agg_sig: String, + } + + fn assert_aggregate_same_message() { + let sk1 = BlsSecretKey::::generate(&SEED_0).unwrap(); + let sk2 = BlsSecretKey::::generate(&SEED_1).unwrap(); + let sig1 = sk1.sign(S::msg_ref(&MSG_DEADBEEF)); + let sig2 = sk2.sign(S::msg_ref(&MSG_DEADBEEF)); + + let agg = BlsSignature::::aggregate(&[&sig1, &sig2]).unwrap(); + let pk1 = sk1.public_key(); + let pk2 = sk2.public_key(); + + let msg = S::msg_ref(&MSG_DEADBEEF); + assert!(agg.fast_verify_aggregates(msg, &[&pk1, &pk2]).is_ok()); + // A key not in the set must make verification fail. + let pk3 = BlsSecretKey::::generate(&[9u8; 32]).unwrap().public_key(); + assert!(agg.fast_verify_aggregates(msg, &[&pk1, &pk3]).is_err()); + // Rogue-key resistance: a naive aggregate must not pass weighted verify. + assert!(agg.secure_verify_aggregates(msg, &[&pk1, &pk2]).is_err()); + } + + #[rstest] + #[case::chia(assert_aggregate_same_message::)] + #[case::ietf(assert_aggregate_same_message::)] + fn aggregate_and_verify_same_message(#[case] assertion: fn()) { + assertion(); + } + + fn assert_secure_verify(corpus: &str) { + let corpus = Corpus::open(env!("CARGO_MANIFEST_DIR"), corpus); + let vecs: Vec = corpus.vectors("secure_verify_aggregates"); + for v in &vecs { + let pks: Vec> = v + .pks + .iter() + .map(|pk| BlsPublicKey::::from_bytes(&arr_from_hex(pk)).unwrap()) + .collect(); + let pk_refs: Vec<&BlsPublicKey> = pks.iter().collect(); + let agg = BlsSignature::::from_bytes(&arr_from_hex(&v.agg_sig_secure)).unwrap(); + let msg: [u8; 32] = arr_from_hex(&v.msg); + assert!(agg.secure_verify_aggregates(S::msg_ref(&msg), &pk_refs).is_ok()); + } + } + + #[rstest] + #[case::chia(assert_secure_verify::, "bls_chia_secure_aggregate")] + #[case::ietf(assert_secure_verify::, "bls_ietf_secure_aggregate")] + fn secure_verify_matches_vectors(#[case] assertion: fn(&str), #[case] corpus: &str) { + assertion(corpus); + } + + #[rstest] + fn ietf_verify_distinct_messages() { + let sk1 = BlsSecretKey::::generate(&SEED_0).unwrap(); + let sk2 = BlsSecretKey::::generate(&SEED_1).unwrap(); + + let msg1: &[u8] = b"first message"; + let msg2: &[u8] = b"second message"; + let sig1 = sk1.sign(msg1); + let sig2 = sk2.sign(msg2); + let agg = BlsSignature::aggregate(&[&sig1, &sig2]).unwrap(); + + let pk1 = sk1.public_key(); + let pk2 = sk2.public_key(); + assert!(agg.verify_aggregates(&[msg1, msg2], &[&pk1, &pk2]).is_ok()); + assert!(agg.verify_aggregates(&[msg2, msg1], &[&pk1, &pk2]).is_err()); + } + + /// An empty aggregate has no signers to bind, so both aggregation entry + /// points reject it rather than returning the identity. + fn assert_empty_aggregation_rejected() { + let pks: [&BlsPublicKey; 0] = []; + let sigs: [&BlsSignature; 0] = []; + assert!(BlsPublicKey::::aggregate(&pks).is_err()); + assert!(BlsSignature::::aggregate(&sigs).is_err()); + } + + #[rstest] + #[case::chia(assert_empty_aggregation_rejected::)] + #[case::ietf(assert_empty_aggregation_rejected::)] + fn aggregate_empty_fails(#[case] assertion: fn()) { + assertion(); + } + + /// Aggregation is a group sum, so neither the aggregate nor the verification + /// may depend on the order the caller supplies. + fn assert_order_independent() { + let sks: Vec> = [SEED_0, SEED_1, [2u8; 32]] + .iter() + .map(|seed| BlsSecretKey::::generate(seed).unwrap()) + .collect(); + let sigs: Vec> = sks.iter().map(|sk| sk.sign(S::msg_ref(&MSG_DEADBEEF))).collect(); + let pks: Vec> = sks.iter().map(BlsSecretKey::public_key).collect(); + + let straight = BlsSignature::::aggregate(&[&sigs[0], &sigs[1], &sigs[2]]).unwrap(); + let rotated = BlsSignature::::aggregate(&[&sigs[2], &sigs[0], &sigs[1]]).unwrap(); + assert_eq!(straight, rotated); + + let msg = S::msg_ref(&MSG_DEADBEEF); + assert!(straight + .fast_verify_aggregates(msg, &[&pks[0], &pks[1], &pks[2]]) + .is_ok()); + assert!(straight + .fast_verify_aggregates(msg, &[&pks[2], &pks[0], &pks[1]]) + .is_ok()); + } + + #[rstest] + #[case::chia(assert_order_independent::)] + #[case::ietf(assert_order_independent::)] + fn aggregate_order_independent(#[case] assertion: fn()) { + assertion(); + } + + fn assert_aggregate_vectors(corpus: &str) { + let corpus = Corpus::open(env!("CARGO_MANIFEST_DIR"), corpus); + let vecs: Vec = corpus.vectors("aggregate_sig"); + + for v in &vecs { + let sigs: Vec> = v + .sigs + .iter() + .map(|sig| BlsSignature::::from_bytes(&arr_from_hex(sig)).unwrap()) + .collect(); + let refs: Vec<&BlsSignature> = sigs.iter().collect(); + let agg = BlsSignature::::aggregate(&refs).unwrap(); + assert_eq!(agg.to_bytes().to_lower_hex_string(), v.agg_sig); + } + } + + #[rstest] + #[case::chia(assert_aggregate_vectors::, "bls_chia_aggregate")] + #[case::ietf(assert_aggregate_vectors::, "bls_ietf_aggregate")] + fn aggregate_matches_vectors(#[case] assertion: fn(&str), #[case] corpus: &str) { + assertion(corpus); + } + + /// Aggregating a point with its own negation cancels to the identity, which + /// then verifies against any message at all. The legacy scheme accepts that, + /// and consensus depends on it continuing to; the IETF scheme rejects it. + /// The sign bit sits at bit 7 for legacy and bit 5 for IETF. + fn assert_identity_cancellation(sign_bit: u8, accepted: bool) { + let sk = BlsSecretKey::::generate(&SEED_0).unwrap(); + let signed = [0x11u8; 32]; + let sig = sk.sign(S::msg_ref(&signed)); + let pk = sk.public_key(); + + let mut neg_sig_bytes = sig.to_bytes(); + neg_sig_bytes[0] ^= sign_bit; + let neg_sig = BlsSignature::::from_bytes(&neg_sig_bytes).unwrap(); + + let mut neg_pk_bytes = pk.to_bytes(); + neg_pk_bytes[0] ^= sign_bit; + let neg_pk = BlsPublicKey::::from_bytes(&neg_pk_bytes).unwrap(); + + let identity = BlsSignature::::aggregate(&[&sig, &neg_sig]).unwrap(); + // The outcome must not depend on the message, signed or otherwise. + for msg in [signed, MSG_DEADBEEF] { + let res = identity.fast_verify_aggregates(S::msg_ref(&msg), &[&pk, &neg_pk]); + assert_eq!(res.is_ok(), accepted, "identity aggregate over {msg:?}"); + } + } + + #[rstest] + #[case::chia(assert_identity_cancellation::, 0x80, true)] + #[case::ietf(assert_identity_cancellation::, 0x20, false)] + fn identity_cancellation_follows_scheme( + #[case] assertion: fn(u8, bool), + #[case] sign_bit: u8, + #[case] accepted: bool, + ) { + assertion(sign_bit, accepted); + } +} diff --git a/pkgs/pkc/src/bls_chia/agg.rs b/pkgs/pkc/src/bls_chia/agg.rs deleted file mode 100644 index 70444e5f..00000000 --- a/pkgs/pkc/src/bls_chia/agg.rs +++ /dev/null @@ -1,50 +0,0 @@ -// -// Copyright (c) 2026-present, The Dash Core developers -// SPDX-License-Identifier: MIT -// See the accompanying file LICENSE or https://opensource.org/license/MIT -// - -//! Aggregation and secure verification for legacy BLS. - -use super::{PublicKey, Signature}; -use crate::bls::scheme_ops::BlsScheme; -use crate::bls::{BlsError, BlsScChia}; -use crate::prelude::*; - -/// Aggregate multiple legacy BLS signatures (simple point addition in G2). -pub fn aggregate_sig(sigs: &[&Signature]) -> Result { - let inner: Vec<_> = sigs.iter().map(|sig| &sig.0).collect(); - BlsScChia::aggregate_sig(&inner).map(Signature::from_inner) -} - -/// Verify an aggregated legacy BLS signature over one message and multiple -/// public keys. -/// -/// An aggregate that cancels to the identity (e.g. `pk + (-pk)`) is not -/// rejected here, for backwards compatibility: it is a bare pairing check -/// that treats infinity as valid (see `identity_cancellation_is_not_rejected`). -pub fn verify_aggregates(sig: &Signature, msg: &[u8; 32], pks: &[&PublicKey]) -> Result<(), BlsError> { - let inner: Vec<_> = pks.iter().map(|pk| &pk.0).collect(); - BlsScChia::fast_verify_aggregates(&sig.0, msg, &inner) -} - -/// Verify an aggregated legacy BLS signature where every signer signed the -/// same message. Equivalent to `verify_aggregates` for the legacy scheme. -pub fn fast_verify_aggregates(sig: &Signature, msg: &[u8; 32], pks: &[&PublicKey]) -> Result<(), BlsError> { - let inner: Vec<_> = pks.iter().map(|pk| &pk.0).collect(); - BlsScChia::fast_verify_aggregates(&sig.0, msg, &inner) -} - -/// Securely aggregate and verify signatures with public-key weighting. -/// -/// Algorithm: -/// 1. Sort public keys by serialized (legacy) bytes -/// 2. Compute `pk_hash = SHA256(pk1 || pk2 || ... || pkN)` (sorted order) -/// 3. For each sorted pk at index i: `weight_i = SHA256(i_as_4_bytes || -/// pk_hash) mod order` -/// 4. Compute weighted public key: `agg_pk = sum(weight_i * pk_i)` -/// 5. Verify the aggregate signature against `agg_pk` and the message -pub fn secure_verify_aggregates(sig: &Signature, msg: &[u8; 32], pks: &[&PublicKey]) -> Result<(), BlsError> { - let inner: Vec<_> = pks.iter().map(|pk| &pk.0).collect(); - BlsScChia::secure_verify_aggregates(&sig.0, msg, &inner) -} diff --git a/pkgs/pkc/src/bls_chia/mod.rs b/pkgs/pkc/src/bls_chia/mod.rs index 2deaea32..0cb42c36 100644 --- a/pkgs/pkc/src/bls_chia/mod.rs +++ b/pkgs/pkc/src/bls_chia/mod.rs @@ -6,14 +6,10 @@ //! Legacy BLS signatures (non-standard hash-to-G2, min-pubkey-size). -mod agg; - pub mod threshold; pub use crate::bls::BlsError; -pub use agg::{aggregate_sig, fast_verify_aggregates, secure_verify_aggregates, verify_aggregates}; - /// A legacy BLS public key (48-byte G1 point in legacy serialization). pub type PublicKey = crate::bls::BlsPublicKey; @@ -22,22 +18,3 @@ pub type SecretKey = crate::bls::BlsSecretKey; /// A legacy BLS signature (96-byte G2 point in legacy serialization). pub type Signature = crate::bls::BlsSignature; - -// Compile-time contract: must match bls_ietf's shared API surface. -const _: () = { - use crate::common::bls::contract::*; - impl BlsSignature for Signature { - type Error = BlsError; - type PublicKey = PublicKey; - type Msg = [u8; 32]; - fn from_bytes(b: &[u8; 96]) -> Result { - Signature::from_bytes(b) - } - fn to_bytes(&self) -> [u8; 96] { - self.to_bytes() - } - fn verify(&self, msg: &[u8; 32], pk: &PublicKey) -> Result<(), BlsError> { - self.verify(msg, pk) - } - } -}; diff --git a/pkgs/pkc/src/bls_ietf/agg.rs b/pkgs/pkc/src/bls_ietf/agg.rs deleted file mode 100644 index c549bb41..00000000 --- a/pkgs/pkc/src/bls_ietf/agg.rs +++ /dev/null @@ -1,53 +0,0 @@ -// -// Copyright (c) 2026-present, The Dash Core developers -// SPDX-License-Identifier: MIT -// See the accompanying file LICENSE or https://opensource.org/license/MIT -// - -//! Aggregation and batch verification for IETF BLS. - -use super::{PublicKey, Signature}; -use crate::bls::scheme_ietf::DST_BASIC; -use crate::bls::scheme_ops::{verify_ok, BlsScheme}; -use crate::bls::{BlsError, BlsScIetf}; -use crate::prelude::*; - -use blst::min_pk; - -/// Aggregate multiple signatures into one. -pub fn aggregate_sig(sigs: &[&Signature]) -> Result { - let inner: Vec<_> = sigs.iter().map(|sig| &sig.0).collect(); - BlsScIetf::aggregate_sig(&inner).map(Signature::from_inner) -} - -/// Verify an aggregated signature where every signer signed the same message. -pub fn fast_verify_aggregates(sig: &Signature, msg: &[u8], pks: &[&PublicKey]) -> Result<(), BlsError> { - let inner: Vec<_> = pks.iter().map(|pk| &pk.0).collect(); - BlsScIetf::fast_verify_aggregates(&sig.0, msg, &inner) -} - -/// Verify an aggregated signature where each signer signed a distinct message. -pub fn verify_aggregates(sig: &Signature, msgs: &[&[u8]], pks: &[&PublicKey]) -> Result<(), BlsError> { - if pks.len() != msgs.len() { - return Err(BlsError::CountMismatch); - } - if pks.is_empty() { - return Err(BlsError::EmptyAggregation); - } - let inner_pks: Vec<&min_pk::PublicKey> = pks.iter().map(|k| &k.0).collect(); - verify_ok(sig.0.aggregate_verify(true, msgs, DST_BASIC, &inner_pks, true)) -} - -/// Securely aggregate and verify signatures with public-key weighting. -/// -/// Algorithm: -/// 1. Sort public keys by serialized (compressed) bytes -/// 2. Compute `pk_hash = SHA256(pk1 || pk2 || ... || pkN)` (sorted order) -/// 3. For each sorted pk at index i: `weight_i = SHA256(i_as_4_bytes || -/// pk_hash) mod order` -/// 4. Compute weighted public key: `agg_pk = sum(weight_i * pk_i)` -/// 5. Verify the aggregate signature against `agg_pk` and the message -pub fn secure_verify_aggregates(sig: &Signature, msg: &[u8], pks: &[&PublicKey]) -> Result<(), BlsError> { - let inner: Vec<_> = pks.iter().map(|pk| &pk.0).collect(); - BlsScIetf::secure_verify_aggregates(&sig.0, msg, &inner) -} diff --git a/pkgs/pkc/src/bls_ietf/mod.rs b/pkgs/pkc/src/bls_ietf/mod.rs index 14bf594c..737c33b3 100644 --- a/pkgs/pkc/src/bls_ietf/mod.rs +++ b/pkgs/pkc/src/bls_ietf/mod.rs @@ -6,7 +6,6 @@ //! IETF BLS12-381 signatures (basic scheme, min-pubkey-size). -mod agg; mod sig; pub mod threshold; @@ -15,8 +14,6 @@ pub use crate::bls::BlsError; /// BLS signature scheme (determines the DST). pub use crate::bls::BlsSigId as Scheme; -pub use agg::{aggregate_sig, fast_verify_aggregates, secure_verify_aggregates, verify_aggregates}; - /// An IETF BLS public key (48-byte compressed G1 point). pub type PublicKey = crate::bls::BlsPublicKey; @@ -25,23 +22,3 @@ pub type SecretKey = crate::bls::BlsSecretKey; /// An IETF BLS signature (96-byte compressed G2 point). pub type Signature = crate::bls::BlsSignature; - -// Compile-time contract: if any of these methods are -// removed or their signatures change, this block fails. -const _: () = { - use crate::common::bls::contract::*; - impl BlsSignature for Signature { - type Error = BlsError; - type PublicKey = PublicKey; - type Msg = [u8]; - fn from_bytes(b: &[u8; 96]) -> Result { - Signature::from_bytes(b) - } - fn to_bytes(&self) -> [u8; 96] { - self.to_bytes() - } - fn verify(&self, msg: &[u8], pk: &PublicKey) -> Result<(), BlsError> { - self.verify(msg, pk) - } - } -}; diff --git a/pkgs/pkc/src/common/bls/contract.rs b/pkgs/pkc/src/common/bls/contract.rs deleted file mode 100644 index 833c1d99..00000000 --- a/pkgs/pkc/src/common/bls/contract.rs +++ /dev/null @@ -1,26 +0,0 @@ -// -// Copyright (c) 2026-present, The Dash Core developers -// SPDX-License-Identifier: MIT -// See the accompanying file LICENSE or https://opensource.org/license/MIT -// - -//! Compile-time contract enforcing that both BLS modules expose the same core -//! API surface. -//! -//! This module is never used at runtime, it exists solely so that adding a -//! method to one module without the other triggers a build failure. -//! -//! Threshold operations (`split_sk`, `recover_sig`, `derive_pk_share`) use -//! `Hash256` participant IDs (big-endian, reduced mod the scalar field -//! order). - -/// Marker trait asserting the minimum BLS signature API. -pub(crate) trait BlsSignature: Clone + Sized { - type Error; - type PublicKey; - type Msg: ?Sized; - - fn from_bytes(bytes: &[u8; 96]) -> Result; - fn to_bytes(&self) -> [u8; 96]; - fn verify(&self, msg: &Self::Msg, pk: &Self::PublicKey) -> Result<(), Self::Error>; -} diff --git a/pkgs/pkc/src/common/bls/mod.rs b/pkgs/pkc/src/common/bls/mod.rs deleted file mode 100644 index eb8353da..00000000 --- a/pkgs/pkc/src/common/bls/mod.rs +++ /dev/null @@ -1,10 +0,0 @@ -// -// Copyright (c) 2026-present, The Dash Core developers -// SPDX-License-Identifier: MIT -// See the accompanying file LICENSE or https://opensource.org/license/MIT -// - -//! BLS primitives shared between bls_ietf and bls_chia. - -#[expect(dead_code, reason = "compile-time contracts, unused at runtime")] -pub(crate) mod contract; diff --git a/pkgs/pkc/src/common/mod.rs b/pkgs/pkc/src/common/mod.rs deleted file mode 100644 index 4fb9999a..00000000 --- a/pkgs/pkc/src/common/mod.rs +++ /dev/null @@ -1,10 +0,0 @@ -// -// Copyright (c) 2026-present, The Dash Core developers -// SPDX-License-Identifier: MIT -// See the accompanying file LICENSE or https://opensource.org/license/MIT -// - -//! Shared internals used by multiple BLS modules. - -#[cfg(feature = "bls")] -pub(crate) mod bls; diff --git a/pkgs/pkc/src/lib.rs b/pkgs/pkc/src/lib.rs index f3c145a2..2f9325df 100644 --- a/pkgs/pkc/src/lib.rs +++ b/pkgs/pkc/src/lib.rs @@ -27,8 +27,6 @@ pub mod __private { cfg_if::cfg_if! { if #[cfg(feature = "bls")] { - mod common; - pub mod bls_chia; pub mod bls_ietf; } diff --git a/pkgs/pkc/tests/bls_chia_aggregate.rs b/pkgs/pkc/tests/bls_chia_aggregate.rs deleted file mode 100644 index 259efc6f..00000000 --- a/pkgs/pkc/tests/bls_chia_aggregate.rs +++ /dev/null @@ -1,177 +0,0 @@ -// -// Copyright (c) 2026-present, The Dash Core developers -// SPDX-License-Identifier: MIT -// See the accompanying file LICENSE or https://opensource.org/license/MIT -// - -//! Aggregation and secure verification tests for bls_chia. - -#![expect(clippy::unwrap_used, reason = "test code")] - -use common::*; -use dash_pkc::bls::tests as common; -use dash_pkc::bls_chia::{aggregate_sig, verify_aggregates, PublicKey, SecretKey, Signature}; -use rstest::*; - -/// Aggregate then verify over a shared message. -#[rstest] -fn aggregate_and_verify(chia_sk0: SecretKey, chia_sk1: SecretKey) { - let msg = [0xabu8; 32]; - let sig1 = chia_sk0.sign(&msg); - let sig2 = chia_sk1.sign(&msg); - let agg = aggregate_sig(&[&sig1, &sig2]).unwrap(); - let pk1 = chia_sk0.public_key(); - let pk2 = chia_sk1.public_key(); - assert!(verify_aggregates(&agg, &msg, &[&pk1, &pk2]).is_ok()); -} - -/// Empty aggregation is rejected. -#[rstest] -fn aggregate_empty_fails() { - let empty_pk: Vec<&dash_pkc::bls_chia::PublicKey> = vec![]; - assert!(PublicKey::aggregate(&empty_pk).is_err()); - let empty_sig: Vec<&Signature> = vec![]; - assert!(aggregate_sig(&empty_sig).is_err()); -} - -/// Secure aggregation with weighted coefficients. -#[rstest] -fn secure_verify_aggregates_roundtrip(chia_sk0: SecretKey, chia_sk1: SecretKey) { - use dash_pkc::bls_chia::secure_verify_aggregates; - let msg = [0xabu8; 32]; - let sig1 = chia_sk0.sign(&msg); - let sig2 = chia_sk1.sign(&msg); - let agg = aggregate_sig(&[&sig1, &sig2]).unwrap(); - let pk1 = chia_sk0.public_key(); - let pk2 = chia_sk1.public_key(); - // Simple aggregation should pass verify_aggregates - assert!(verify_aggregates(&agg, &msg, &[&pk1, &pk2]).is_ok()); - // But secure_verify uses different weighting, so naively - // aggregated sigs should fail (wrong weighting). - assert!(secure_verify_aggregates(&agg, &msg, &[&pk1, &pk2]).is_err()); -} - -/// Secure aggregation is order-independent: shuffling the input -/// public keys produces the same result. -#[rstest] -fn secure_aggregate_order_independent() { - let sk1 = SecretKey::generate(&RSEED[1]).unwrap(); - let sk2 = SecretKey::generate(&RSEED[2]).unwrap(); - let sk3 = SecretKey::generate(&RSEED[3]).unwrap(); - - let msg = [0xffu8; 32]; - let pk1 = sk1.public_key(); - let pk2 = sk2.public_key(); - let pk3 = sk3.public_key(); - let sig1 = sk1.sign(&msg); - let sig2 = sk2.sign(&msg); - let sig3 = sk3.sign(&msg); - - // Aggregate in order [1,2,3] and [3,1,2], secure verify - // should accept both with the same pks set. - let agg_a = aggregate_sig(&[&sig1, &sig2, &sig3]).unwrap(); - let agg_b = aggregate_sig(&[&sig3, &sig1, &sig2]).unwrap(); - - // Both aggregates are simple sums, so they should be - // identical (addition is commutative). - assert_eq!(agg_a.to_bytes(), agg_b.to_bytes()); - - // verify_aggregates (non-secure) accepts both. - assert!(verify_aggregates(&agg_a, &msg, &[&pk1, &pk2, &pk3]).is_ok()); - assert!(verify_aggregates(&agg_a, &msg, &[&pk3, &pk1, &pk2]).is_ok()); -} - -/// Cancelling keys and signatures to the identity (`P + (-P)`) yields a -/// point that satisfies the pairing check for any message. The primitives -/// accept it for backwards compatibility: the signed message and an -/// unrelated one both verify. -#[rstest] -#[case::signed_message([0x11u8; 32])] -#[case::unrelated_message([0x22u8; 32])] -fn identity_cancellation_is_not_rejected(chia_sk0: SecretKey, #[case] verify_msg: [u8; 32]) { - let sig = chia_sk0.sign(&[0x11u8; 32]); - let pk = chia_sk0.public_key(); - - let mut neg_sig_bytes = sig.to_bytes(); - neg_sig_bytes[0] ^= 0x80; - let neg_sig = Signature::from_bytes(&neg_sig_bytes).unwrap(); - - let mut neg_pk_bytes = pk.to_bytes(); - neg_pk_bytes[0] ^= 0x80; - let neg_pk = PublicKey::from_bytes(&neg_pk_bytes).unwrap(); - - // sig + (-sig) = identity signature; pk + (-pk) = identity key. - let identity_sig = aggregate_sig(&[&sig, &neg_sig]).unwrap(); - - // Verifies regardless of the message. - assert!(verify_aggregates(&identity_sig, &verify_msg, &[&pk, &neg_pk]).is_ok()); -} - -mod kat { - use dash_dev::{arr_from_hex, Corpus}; - use hex_conservative::DisplayHex; - use serde::Deserialize; - - #[derive(Deserialize)] - struct AggregateSigVector { - sigs: Vec, - agg_sig: String, - } - - #[derive(Deserialize)] - #[expect(dead_code, reason = "deserialized from corpus JSON")] - struct SecureAggVector { - msg: String, - pks: Vec, - sigs: Vec, - agg_sig_secure: String, - } - - #[test] - fn kat_aggregate_sig() { - let corpus = Corpus::open(env!("CARGO_MANIFEST_DIR"), "bls_chia_aggregate"); - let vecs: Vec = corpus.vectors("aggregate_sig"); - - for v in &vecs { - let sigs: Vec = v - .sigs - .iter() - .map(|h| { - let b: [u8; 96] = arr_from_hex(h); - dash_pkc::bls_chia::Signature::from_bytes(&b).unwrap() - }) - .collect(); - let sig_refs: Vec<_> = sigs.iter().collect(); - let agg = dash_pkc::bls_chia::aggregate_sig(&sig_refs).unwrap(); - assert_eq!(agg.to_bytes().to_lower_hex_string(), v.agg_sig); - } - } - - #[test] - fn kat_secure_verify_aggregates() { - let corpus = Corpus::open(env!("CARGO_MANIFEST_DIR"), "bls_chia_secure_aggregate"); - let vecs: Vec = corpus.vectors("secure_verify_aggregates"); - - for v in &vecs { - let msg: [u8; 32] = arr_from_hex(&v.msg); - let pks: Vec = v - .pks - .iter() - .map(|h| { - let b: [u8; 48] = arr_from_hex(h); - dash_pkc::bls_chia::PublicKey::from_bytes(&b).unwrap() - }) - .collect(); - - let expected_agg: [u8; 96] = arr_from_hex(&v.agg_sig_secure); - let agg_sig = dash_pkc::bls_chia::Signature::from_bytes(&expected_agg).unwrap(); - let pk_refs: Vec<_> = pks.iter().collect(); - - assert!( - dash_pkc::bls_chia::secure_verify_aggregates(&agg_sig, &msg, &pk_refs).is_ok(), - "secure verify failed for n={}", - v.pks.len() - ); - } - } -} diff --git a/pkgs/pkc/tests/bls_chia_llmq.rs b/pkgs/pkc/tests/bls_chia_llmq.rs index ff9f7b55..fdb476c9 100644 --- a/pkgs/pkc/tests/bls_chia_llmq.rs +++ b/pkgs/pkc/tests/bls_chia_llmq.rs @@ -11,7 +11,7 @@ use dash_dev::{arr_from_hex, Corpus}; use dash_num::Hash256; use dash_pkc::bls::tests as common; -use dash_pkc::bls_chia::{aggregate_sig, threshold, PublicKey, SecretKey, Signature}; +use dash_pkc::bls_chia::{threshold, PublicKey, SecretKey, Signature}; use hex_conservative::DisplayHex; #[test] @@ -264,7 +264,7 @@ fn llmq_finalize_aggregated_member_sigs() { .collect(); let sig_refs: Vec<&Signature> = member_sigs.iter().collect(); - let agg_sig = aggregate_sig(&sig_refs).unwrap(); + let agg_sig = Signature::aggregate(&sig_refs).unwrap(); let member_pks: Vec = commits .iter() @@ -276,7 +276,7 @@ fn llmq_finalize_aggregated_member_sigs() { let pk_refs: Vec<&PublicKey> = member_pks.iter().collect(); assert!( - dash_pkc::bls_chia::fast_verify_aggregates(&agg_sig, &commitment_hash, &pk_refs,).is_ok(), + agg_sig.fast_verify_aggregates(&commitment_hash, &pk_refs).is_ok(), "aggregated member sigs failed fast_verify" ); } diff --git a/pkgs/pkc/tests/bls_ietf_aggregate.rs b/pkgs/pkc/tests/bls_ietf_aggregate.rs deleted file mode 100644 index 2e8f9f44..00000000 --- a/pkgs/pkc/tests/bls_ietf_aggregate.rs +++ /dev/null @@ -1,199 +0,0 @@ -// -// Copyright (c) 2026-present, The Dash Core developers -// SPDX-License-Identifier: MIT -// See the accompanying file LICENSE or https://opensource.org/license/MIT -// - -//! Aggregation and secure verification tests for bls_ietf. - -#![expect(clippy::unwrap_used, reason = "test code")] - -use common::*; -use dash_pkc::bls::tests as common; -use dash_pkc::bls_ietf::{aggregate_sig, fast_verify_aggregates, verify_aggregates, PublicKey, SecretKey, Signature}; -use hex_conservative::hex; -use rstest::*; - -/// Aggregated public key serializes to 48 bytes. -#[rstest] -fn aggregate_pk_roundtrip(ietf_sk0: SecretKey, ietf_sk1: SecretKey) { - let pk1 = ietf_sk0.public_key(); - let pk2 = ietf_sk1.public_key(); - let agg = PublicKey::aggregate(&[&pk1, &pk2]).unwrap(); - assert_eq!(agg.to_bytes().len(), 48); -} - -/// Empty aggregation is rejected. -#[rstest] -fn aggregate_empty_fails() { - let empty_pk: Vec<&dash_pkc::bls_ietf::PublicKey> = vec![]; - assert!(PublicKey::aggregate(&empty_pk).is_err()); - let empty_sig: Vec<&Signature> = vec![]; - assert!(aggregate_sig(&empty_sig).is_err()); -} - -/// Aggregate verify over two distinct messages. -#[rstest] -fn aggregate_two_distinct_messages() { - let sk1 = SecretKey::generate(&RSEED[0]).unwrap(); - let sk2 = SecretKey::generate(&RSEED[1]).unwrap(); - - let msg1 = hex!("070809"); - let msg2 = hex!("0a0b0c"); - let sig1 = sk1.sign(&msg1); - let sig2 = sk2.sign(&msg2); - let agg = aggregate_sig(&[&sig1, &sig2]).unwrap(); - - let pk1 = sk1.public_key(); - let pk2 = sk2.public_key(); - let msgs: Vec<&[u8]> = vec![msg1.as_slice(), msg2.as_slice()]; - assert!(verify_aggregates(&agg, &msgs, &[&pk1, &pk2]).is_ok()); -} - -/// Fast aggregate verify with a shared message. -#[rstest] -fn fast_verify_same_message(ietf_sk0: SecretKey, ietf_sk1: SecretKey) { - let msg = b"same message for both signers"; - let sig1 = ietf_sk0.sign(msg); - let sig2 = ietf_sk1.sign(msg); - let agg = aggregate_sig(&[&sig1, &sig2]).unwrap(); - let pk1 = ietf_sk0.public_key(); - let pk2 = ietf_sk1.public_key(); - assert!(fast_verify_aggregates(&agg, msg, &[&pk1, &pk2]).is_ok()); -} - -/// Fast aggregate verify is order-independent. -#[rstest] -fn fast_verify_order_independent() { - let sk1 = SecretKey::generate(&RSEED[1]).unwrap(); - let sk2 = SecretKey::generate(&RSEED[2]).unwrap(); - let sk3 = SecretKey::generate(&RSEED[3]).unwrap(); - let msg = b"order test"; - let pk1 = sk1.public_key(); - let pk2 = sk2.public_key(); - let pk3 = sk3.public_key(); - let sig1 = sk1.sign(msg); - let sig2 = sk2.sign(msg); - let sig3 = sk3.sign(msg); - let agg = aggregate_sig(&[&sig1, &sig2, &sig3]).unwrap(); - - // Both orderings of pks verify the same aggregate. - assert!(fast_verify_aggregates(&agg, msg, &[&pk1, &pk2, &pk3]).is_ok()); - assert!(fast_verify_aggregates(&agg, msg, &[&pk3, &pk1, &pk2]).is_ok()); -} - -/// Secure verify rejects naively aggregated signatures. -#[rstest] -fn secure_verify_rejects_naive_aggregate(ietf_sk0: SecretKey, ietf_sk1: SecretKey) { - use dash_pkc::bls_ietf::secure_verify_aggregates; - - let msg = b"secure test"; - let sig1 = ietf_sk0.sign(msg); - let sig2 = ietf_sk1.sign(msg); - let agg = aggregate_sig(&[&sig1, &sig2]).unwrap(); - let pk1 = ietf_sk0.public_key(); - let pk2 = ietf_sk1.public_key(); - - // Fast (non-secure) verify should succeed. - assert!(fast_verify_aggregates(&agg, msg, &[&pk1, &pk2]).is_ok()); - // Secure verify uses different weighting, so naively - // aggregated sigs should fail. - assert!(secure_verify_aggregates(&agg, msg, &[&pk1, &pk2]).is_err()); -} - -/// Cancelling keys and signatures to the identity (`P + (-P)`) is rejected -/// under the IETF scheme, unlike the lenient legacy scheme: blst refuses an -/// infinity aggregate key, so verification fails for any message. -#[rstest] -#[case::signed_message([0x11u8; 32])] -#[case::unrelated_message([0x22u8; 32])] -fn identity_cancellation_is_rejected(ietf_sk0: SecretKey, #[case] verify_msg: [u8; 32]) { - let sig = ietf_sk0.sign(&[0x11u8; 32]); - let pk = ietf_sk0.public_key(); - - // Flip the IETF sign bit (bit 5 of byte 0) to negate each point. - let mut neg_sig_bytes = sig.to_bytes(); - neg_sig_bytes[0] ^= 0x20; - let neg_sig = Signature::from_bytes(&neg_sig_bytes).unwrap(); - - let mut neg_pk_bytes = pk.to_bytes(); - neg_pk_bytes[0] ^= 0x20; - let neg_pk = PublicKey::from_bytes(&neg_pk_bytes).unwrap(); - - // sig + (-sig) = identity signature; pk + (-pk) = identity key. - let identity_sig = aggregate_sig(&[&sig, &neg_sig]).unwrap(); - - // Verification fails regardless of the message. - assert!(fast_verify_aggregates(&identity_sig, &verify_msg, &[&pk, &neg_pk]).is_err()); -} - -mod kat { - use dash_dev::{arr_from_hex, vec_from_hex, Corpus}; - use hex_conservative::DisplayHex; - use serde::Deserialize; - - #[derive(Deserialize)] - struct AggregateSigVector { - sigs: Vec, - agg_sig: String, - } - - #[derive(Deserialize)] - #[expect(dead_code, reason = "deserialized from corpus JSON")] - struct SecureAggVector { - msg: String, - pks: Vec, - sigs: Vec, - agg_sig_secure: String, - } - - #[test] - fn kat_aggregate_sig() { - let corpus = Corpus::open(env!("CARGO_MANIFEST_DIR"), "bls_ietf_aggregate"); - let vecs: Vec = corpus.vectors("aggregate_sig"); - - for v in &vecs { - let sigs: Vec = v - .sigs - .iter() - .map(|h| { - let b: [u8; 96] = arr_from_hex(h); - dash_pkc::bls_ietf::Signature::from_bytes(&b).unwrap() - }) - .collect(); - let sig_refs: Vec<_> = sigs.iter().collect(); - let agg = dash_pkc::bls_ietf::aggregate_sig(&sig_refs).unwrap(); - assert_eq!(agg.to_bytes().to_lower_hex_string(), v.agg_sig); - } - } - - #[test] - fn kat_secure_verify_aggregates() { - let corpus = Corpus::open(env!("CARGO_MANIFEST_DIR"), "bls_ietf_secure_aggregate"); - let vecs: Vec = corpus.vectors("secure_verify_aggregates"); - - for v in &vecs { - let msg = vec_from_hex(&v.msg); - let pks: Vec = v - .pks - .iter() - .map(|h| { - let b: [u8; 48] = arr_from_hex(h); - dash_pkc::bls_ietf::PublicKey::from_bytes(&b).unwrap() - }) - .collect(); - - let expected_agg: [u8; 96] = arr_from_hex(&v.agg_sig_secure); - let agg_sig = dash_pkc::bls_ietf::Signature::from_bytes(&expected_agg).unwrap(); - let pk_refs: Vec<_> = pks.iter().collect(); - - // The securely aggregated signature must pass - // secure_verify_aggregates. - assert!( - dash_pkc::bls_ietf::secure_verify_aggregates(&agg_sig, &msg, &pk_refs).is_ok(), - "secure verify failed for n={}", - v.pks.len() - ); - } - } -} diff --git a/pkgs/pkc/tests/bls_ietf_llmq.rs b/pkgs/pkc/tests/bls_ietf_llmq.rs index 76f6d2e5..c63ec36f 100644 --- a/pkgs/pkc/tests/bls_ietf_llmq.rs +++ b/pkgs/pkc/tests/bls_ietf_llmq.rs @@ -15,7 +15,7 @@ use dash_dev::{arr_from_hex, Corpus}; use dash_num::Hash256; use dash_pkc::bls::tests as common; -use dash_pkc::bls_ietf::{aggregate_sig, threshold, PublicKey, SecretKey, Signature}; +use dash_pkc::bls_ietf::{threshold, PublicKey, SecretKey, Signature}; use hex_conservative::DisplayHex; #[test] @@ -307,7 +307,7 @@ fn llmq_finalize_aggregated_member_sigs() { .collect(); let sig_refs: Vec<&Signature> = member_sigs.iter().collect(); - let agg_sig = aggregate_sig(&sig_refs).unwrap(); + let agg_sig = Signature::aggregate(&sig_refs).unwrap(); // Verify the aggregated member sig against the // commitment hash using each member's public key. @@ -321,7 +321,7 @@ fn llmq_finalize_aggregated_member_sigs() { let pk_refs: Vec<&PublicKey> = member_pks.iter().collect(); assert!( - dash_pkc::bls_ietf::fast_verify_aggregates(&agg_sig, &commitment_hash, &pk_refs,).is_ok(), + agg_sig.fast_verify_aggregates(&commitment_hash, &pk_refs).is_ok(), "aggregated member sigs failed fast_verify" ); } From d7b07bbc608cc59c0fd5963bfab23053af348f87 Mon Sep 17 00:00:00 2001 From: Kittywhiskers Van Gogh <63189531+kwvg@users.noreply.github.com> Date: Wed, 5 Aug 2026 10:17:00 +0530 Subject: [PATCH 11/14] pkc%feat(bls): migrate proof of possession to `bls` module --- pkgs/pkc/Cargo.toml | 4 --- pkgs/pkc/src/bls/mod.rs | 3 +- pkgs/pkc/src/bls/scheme_ietf.rs | 16 ++++++++- pkgs/pkc/src/bls/sig_pop.rs | 61 +++++++++++++++++++++++++++++++++ pkgs/pkc/src/bls_ietf/mod.rs | 2 -- pkgs/pkc/src/bls_ietf/sig.rs | 33 ------------------ pkgs/pkc/tests/bls_ietf_pop.rs | 28 --------------- 7 files changed, 78 insertions(+), 69 deletions(-) create mode 100644 pkgs/pkc/src/bls/sig_pop.rs delete mode 100644 pkgs/pkc/src/bls_ietf/sig.rs delete mode 100644 pkgs/pkc/tests/bls_ietf_pop.rs diff --git a/pkgs/pkc/Cargo.toml b/pkgs/pkc/Cargo.toml index 18db8ac6..91a38cc3 100644 --- a/pkgs/pkc/Cargo.toml +++ b/pkgs/pkc/Cargo.toml @@ -81,10 +81,6 @@ required-features = ["bls", "tests"] name = "bls_ietf_llmq" required-features = ["bls", "tests"] -[[test]] -name = "bls_ietf_pop" -required-features = ["bls", "tests"] - [[test]] name = "bls_ietf_threshold" required-features = ["bls", "tests"] diff --git a/pkgs/pkc/src/bls/mod.rs b/pkgs/pkc/src/bls/mod.rs index 1ebf4096..c02bb1f7 100644 --- a/pkgs/pkc/src/bls/mod.rs +++ b/pkgs/pkc/src/bls/mod.rs @@ -24,13 +24,14 @@ cfg_if::cfg_if! { if #[cfg(feature = "bls")] { mod public_ops; mod scheme_chia; + mod scheme_ietf; mod secret_ops; mod sig_aggregate; mod sig_basic; + mod sig_pop; #[expect(unsafe_code, reason = "blst C FFI")] pub(crate) mod blst_ffi; pub(crate) mod chia_h2c; - pub(crate) mod scheme_ietf; pub(crate) mod scheme_ops; #[cfg(feature = "tests")] diff --git a/pkgs/pkc/src/bls/scheme_ietf.rs b/pkgs/pkc/src/bls/scheme_ietf.rs index ee32739a..ab6223ae 100644 --- a/pkgs/pkc/src/bls/scheme_ietf.rs +++ b/pkgs/pkc/src/bls/scheme_ietf.rs @@ -19,7 +19,7 @@ const DST_BASIC: &[u8] = b"BLS_SIG_BLS12381G2_XMD:SHA-256_SSWU_RO_NUL_"; /// Domain separation tag for signatures in the proof-of-possession scheme. const DST_POP: &[u8] = b"BLS_SIG_BLS12381G2_XMD:SHA-256_SSWU_RO_POP_"; /// Domain separation tag for proofs of possession. -pub(crate) const DST_POP_PROVE: &[u8] = b"BLS_POP_BLS12381G2_XMD:SHA-256_SSWU_RO_POP_"; +const DST_POP_PROVE: &[u8] = b"BLS_POP_BLS12381G2_XMD:SHA-256_SSWU_RO_POP_"; impl BlsScheme for BlsScIetf { type InnerSk = SecretKey; @@ -178,6 +178,20 @@ impl BlsScIetf { verify_ok(sig.verify(true, msg, dst, &[], pk, true)) } + /// Prove possession by signing the public key under the PoP-prove DST. + pub(crate) fn prove_possession(sk: &SecretKey, pk: &PublicKey) -> Signature { + sk.sign(&pk.compress(), DST_POP_PROVE, &[]) + } + + /// Verify a proof of possession under the PoP-prove DST. + /// + /// # Errors + /// + /// Returns `VerifyFailed` when the proof does not match the key. + pub(crate) fn verify_possession(pk: &PublicKey, pop: &Signature) -> Result<(), BlsError> { + verify_ok(pop.verify(true, &pk.compress(), DST_POP_PROVE, &[], pk, true)) + } + /// Verify an aggregated signature where each signer signed a distinct /// message. /// diff --git a/pkgs/pkc/src/bls/sig_pop.rs b/pkgs/pkc/src/bls/sig_pop.rs new file mode 100644 index 00000000..8e075b45 --- /dev/null +++ b/pkgs/pkc/src/bls/sig_pop.rs @@ -0,0 +1,61 @@ +// +// Copyright (c) 2026-present, The Dash Core developers +// SPDX-License-Identifier: MIT +// See the accompanying file LICENSE or https://opensource.org/license/MIT +// + +//! Proof of Possession operations for BLS keys. + +use super::error::BlsError; +use super::public_ops::BlsPublicKey; +use super::secret_ops::BlsSecretKey; +use super::sig_basic::BlsSignature; +use super::BlsScIetf; + +impl BlsSecretKey { + /// Produce a proof of possession by signing the serialized public key. + /// + /// IETF only: the legacy scheme has no proof-of-possession domain + /// separation tag, so there is no such method to call on it. + pub fn prove_possession(&self) -> BlsSignature { + let pk = self.public_key(); + BlsSignature::from_inner(BlsScIetf::prove_possession(&self.0, &pk.0)) + } +} + +impl BlsPublicKey { + /// Verify a proof of possession against this key. + /// + /// IETF only, mirroring [`BlsSecretKey::prove_possession`]. + /// + /// # Errors + /// + /// Returns `VerifyFailed` on mismatch. + pub fn verify_possession(&self, pop: &BlsSignature) -> Result<(), BlsError> { + BlsScIetf::verify_possession(&self.0, &pop.0) + } +} + +#[cfg(all(test, feature = "tests"))] +#[expect(clippy::unwrap_used, reason = "test code")] +mod tests { + use super::*; + use crate::bls::tests::{SEED_0, SEED_1}; + + use rstest::rstest; + + #[rstest] + fn ietf_proof_of_possession_roundtrip() { + let sk = BlsSecretKey::::generate(&SEED_0).unwrap(); + let proof = sk.prove_possession(); + assert!(sk.public_key().verify_possession(&proof).is_ok()); + } + + #[rstest] + fn ietf_proof_of_possession_rejects_wrong_key() { + let sk0 = BlsSecretKey::::generate(&SEED_0).unwrap(); + let sk1 = BlsSecretKey::::generate(&SEED_1).unwrap(); + let proof = sk0.prove_possession(); + assert!(sk1.public_key().verify_possession(&proof).is_err()); + } +} diff --git a/pkgs/pkc/src/bls_ietf/mod.rs b/pkgs/pkc/src/bls_ietf/mod.rs index 737c33b3..6659c271 100644 --- a/pkgs/pkc/src/bls_ietf/mod.rs +++ b/pkgs/pkc/src/bls_ietf/mod.rs @@ -6,8 +6,6 @@ //! IETF BLS12-381 signatures (basic scheme, min-pubkey-size). -mod sig; - pub mod threshold; pub use crate::bls::BlsError; diff --git a/pkgs/pkc/src/bls_ietf/sig.rs b/pkgs/pkc/src/bls_ietf/sig.rs deleted file mode 100644 index e3217406..00000000 --- a/pkgs/pkc/src/bls_ietf/sig.rs +++ /dev/null @@ -1,33 +0,0 @@ -// -// Copyright (c) 2026-present, The Dash Core developers -// SPDX-License-Identifier: MIT -// See the accompanying file LICENSE or https://opensource.org/license/MIT -// - -//! IETF BLS signature (96-byte compressed G2 point). - -use super::{PublicKey, SecretKey, Signature}; -use crate::bls::scheme_ietf::DST_POP_PROVE; -use crate::bls::scheme_ops::verify_ok; -use crate::bls::BlsError; - -impl SecretKey { - /// Produce a proof of possession by signing the serialized public key with - /// the PoP DST. - pub fn prove_possession(&self) -> Signature { - let pk_bytes = self.public_key().to_bytes(); - Signature::from_inner(self.0.sign(&pk_bytes, DST_POP_PROVE, &[])) - } -} - -impl PublicKey { - /// Verify a proof of possession against this key. - /// - /// # Errors - /// - /// Returns [`BlsError::VerifyFailed`] if the proof does not verify. - pub fn verify_possession(&self, pop: &Signature) -> Result<(), BlsError> { - let pk_bytes = self.to_bytes(); - verify_ok(pop.0.verify(true, &pk_bytes, DST_POP_PROVE, &[], &self.0, true)) - } -} diff --git a/pkgs/pkc/tests/bls_ietf_pop.rs b/pkgs/pkc/tests/bls_ietf_pop.rs deleted file mode 100644 index 66fc03b2..00000000 --- a/pkgs/pkc/tests/bls_ietf_pop.rs +++ /dev/null @@ -1,28 +0,0 @@ -// -// Copyright (c) 2026-present, The Dash Core developers -// SPDX-License-Identifier: MIT -// See the accompanying file LICENSE or https://opensource.org/license/MIT -// - -//! Proof of possession tests for bls_ietf. - -use common::*; -use dash_pkc::bls::tests as common; -use dash_pkc::bls_ietf::SecretKey; -use rstest::*; - -/// Proof of possession round-trips. -#[rstest] -fn pop_prove_verify(ietf_sk0: SecretKey) { - let pop = ietf_sk0.prove_possession(); - let pk = ietf_sk0.public_key(); - assert!(pk.verify_possession(&pop).is_ok()); -} - -/// PoP from a different key is rejected. -#[rstest] -fn pop_rejects_wrong_key(ietf_sk0: SecretKey, ietf_sk1: SecretKey) { - let pop = ietf_sk0.prove_possession(); - let wrong_pk = ietf_sk1.public_key(); - assert!(wrong_pk.verify_possession(&pop).is_err()); -} From 84109451cb63151e0bd2d267035e273417a08b31 Mon Sep 17 00:00:00 2001 From: Kittywhiskers Van Gogh <63189531+kwvg@users.noreply.github.com> Date: Mon, 10 Aug 2026 14:20:12 +0530 Subject: [PATCH 12/14] pkc%feat(bls): migrate threshold sharing to `bls` module --- contrib/codeql/lib/policy.qll | 4 +- pkgs/pkc/Cargo.toml | 16 - pkgs/pkc/bench/bls_chia.rs | 19 +- pkgs/pkc/bench/bls_ietf.rs | 19 +- pkgs/pkc/src/bls/mod.rs | 3 + pkgs/pkc/src/bls/share_ops.rs | 582 +++++++++++++++++++++++++++ pkgs/pkc/src/bls/sig_threshold.rs | 122 ++++++ pkgs/pkc/src/bls/tests.rs | 26 -- pkgs/pkc/src/bls_chia/mod.rs | 20 - pkgs/pkc/src/bls_chia/threshold.rs | 141 ------- pkgs/pkc/src/bls_ietf/mod.rs | 22 - pkgs/pkc/src/bls_ietf/threshold.rs | 143 ------- pkgs/pkc/src/lib.rs | 7 - pkgs/pkc/src/prelude.rs | 2 +- pkgs/pkc/tests/bls_chia_llmq.rs | 282 ------------- pkgs/pkc/tests/bls_chia_threshold.rs | 93 ----- pkgs/pkc/tests/bls_ietf_llmq.rs | 327 --------------- pkgs/pkc/tests/bls_ietf_threshold.rs | 93 ----- pkgs/pkc/tests/common/mod.rs | 12 - 19 files changed, 732 insertions(+), 1201 deletions(-) create mode 100644 pkgs/pkc/src/bls/share_ops.rs create mode 100644 pkgs/pkc/src/bls/sig_threshold.rs delete mode 100644 pkgs/pkc/src/bls_chia/mod.rs delete mode 100644 pkgs/pkc/src/bls_chia/threshold.rs delete mode 100644 pkgs/pkc/src/bls_ietf/mod.rs delete mode 100644 pkgs/pkc/src/bls_ietf/threshold.rs delete mode 100644 pkgs/pkc/tests/bls_chia_llmq.rs delete mode 100644 pkgs/pkc/tests/bls_chia_threshold.rs delete mode 100644 pkgs/pkc/tests/bls_ietf_llmq.rs delete mode 100644 pkgs/pkc/tests/bls_ietf_threshold.rs delete mode 100644 pkgs/pkc/tests/common/mod.rs diff --git a/contrib/codeql/lib/policy.qll b/contrib/codeql/lib/policy.qll index ac7aebb8..1d55e00e 100644 --- a/contrib/codeql/lib/policy.qll +++ b/contrib/codeql/lib/policy.qll @@ -52,9 +52,9 @@ predicate isSecretType(TypeItem t) { t.getName().getText() = "Fr" ) and // A share *of a signature* is published, so it holds nothing to protect. Excluded by - // exact name because `SecretKeyShare` and `RawShare` match the same Share substring + // exact name because `BlsSkShare` and `RawShare` match the same Share substring // and do carry secret scalars. - not t.getName().getText() = "SignatureShare" and + not t.getName().getText() = "BlsSigShare" and // Serde artifact to deserialize a tagged enum. not t.getName().getText() = "__Seed" } diff --git a/pkgs/pkc/Cargo.toml b/pkgs/pkc/Cargo.toml index 91a38cc3..ba23aee4 100644 --- a/pkgs/pkc/Cargo.toml +++ b/pkgs/pkc/Cargo.toml @@ -69,19 +69,3 @@ path = "bench/main.rs" harness = false required-features = ["tests"] -[[test]] -name = "bls_chia_llmq" -required-features = ["bls", "tests"] - -[[test]] -name = "bls_chia_threshold" -required-features = ["bls", "tests"] - -[[test]] -name = "bls_ietf_llmq" -required-features = ["bls", "tests"] - -[[test]] -name = "bls_ietf_threshold" -required-features = ["bls", "tests"] - diff --git a/pkgs/pkc/bench/bls_chia.rs b/pkgs/pkc/bench/bls_chia.rs index 4717e147..bb2cfa70 100644 --- a/pkgs/pkc/bench/bls_chia.rs +++ b/pkgs/pkc/bench/bls_chia.rs @@ -8,7 +8,11 @@ use common::*; use dash_pkc::bls::tests as common; -use dash_pkc::bls_chia::{PublicKey, SecretKey, Signature}; +use dash_pkc::bls::{BlsPublicKey, BlsScChia, BlsSecretKey, BlsSignature}; + +type PublicKey = BlsPublicKey; +type SecretKey = BlsSecretKey; +type Signature = BlsSignature; /// Single signature creation (legacy hash-to-G2). #[divan::bench] @@ -124,34 +128,33 @@ fn deser_sig(bencher: divan::Bencher) { /// Threshold secret key splitting at various quorum sizes. #[divan::bench(args = [5, 10, 50])] fn split_threshold(bencher: divan::Bencher, n: usize) { - use dash_pkc::bls_chia::threshold; let sk = SecretKey::generate(&test_ikm(1)).unwrap(); let t = n.div_ceil(2); let ids = sequential_ids(n); bencher .counter(divan::counter::ItemsCount::new(n)) - .bench(|| threshold::split_sk(&sk, t, &ids, &mut rand_core::OsRng)); + .bench(|| sk.split(t, &ids, &mut rand_core::OsRng)); } /// Threshold signature recovery via Lagrange interpolation. #[divan::bench(args = [3, 5, 10])] fn recover_threshold(bencher: divan::Bencher, t: usize) { - use dash_pkc::bls_chia::threshold; let sk = SecretKey::generate(&test_ikm(1)).unwrap(); let n = t * 2; let ids = sequential_ids(n); - let shares = threshold::split_sk(&sk, t, &ids, &mut rand_core::OsRng).unwrap(); + let shares = sk.split(t, &ids, &mut rand_core::OsRng).unwrap(); let msg = test_msg(42); let sig_shares: Vec<_> = shares.iter().map(|s| s.sign(&msg)).collect(); - let subset: Vec<&threshold::SignatureShare> = sig_shares.iter().take(t).collect(); + let subset: Vec<&dash_pkc::bls::BlsSigShare> = sig_shares.iter().take(t).collect(); bencher .counter(divan::counter::ItemsCount::new(t)) - .bench(|| threshold::recover_sig(&subset)); + .bench(|| Signature::recover(&subset)); } #[cfg(feature = "std")] mod worker_benches { - use dash_pkc::bls_chia::{PublicKey, SecretKey, Signature}; + use super::{PublicKey, SecretKey, Signature}; + use dash_pkc::worker; fn setup_sigs(n: usize) -> Vec<(Signature, PublicKey, [u8; 32])> { diff --git a/pkgs/pkc/bench/bls_ietf.rs b/pkgs/pkc/bench/bls_ietf.rs index ac117d28..c17a5caf 100644 --- a/pkgs/pkc/bench/bls_ietf.rs +++ b/pkgs/pkc/bench/bls_ietf.rs @@ -8,7 +8,11 @@ use common::*; use dash_pkc::bls::tests as common; -use dash_pkc::bls_ietf::{PublicKey, SecretKey, Signature}; +use dash_pkc::bls::{BlsPublicKey, BlsScIetf, BlsSecretKey, BlsSignature}; + +type PublicKey = BlsPublicKey; +type SecretKey = BlsSecretKey; +type Signature = BlsSignature; /// Single signature creation. #[divan::bench] @@ -142,29 +146,27 @@ fn deser_sig(bencher: divan::Bencher) { /// Threshold secret key splitting at various quorum sizes. #[divan::bench(args = [5, 10, 50])] fn split_threshold(bencher: divan::Bencher, n: usize) { - use dash_pkc::bls_ietf::threshold; let sk = SecretKey::generate(&test_ikm(1)).unwrap(); let t = n.div_ceil(2); let ids = sequential_ids(n); bencher .counter(divan::counter::ItemsCount::new(n)) - .bench(|| threshold::split_sk(&sk, t, &ids, &mut rand_core::OsRng)); + .bench(|| sk.split(t, &ids, &mut rand_core::OsRng)); } /// Threshold signature recovery via Lagrange interpolation. #[divan::bench(args = [3, 5, 10])] fn recover_threshold(bencher: divan::Bencher, t: usize) { - use dash_pkc::bls_ietf::threshold; let sk = SecretKey::generate(&test_ikm(1)).unwrap(); let n = t * 2; let ids = sequential_ids(n); - let shares = threshold::split_sk(&sk, t, &ids, &mut rand_core::OsRng).unwrap(); + let shares = sk.split(t, &ids, &mut rand_core::OsRng).unwrap(); let msg = test_msg(42); let sig_shares: Vec<_> = shares.iter().map(|s| s.sign(&msg)).collect(); - let subset: Vec<&threshold::SignatureShare> = sig_shares.iter().take(t).collect(); + let subset: Vec<&dash_pkc::bls::BlsSigShare> = sig_shares.iter().take(t).collect(); bencher .counter(divan::counter::ItemsCount::new(t)) - .bench(|| threshold::recover_sig(&subset)); + .bench(|| Signature::recover(&subset)); } /// Proof of possession creation. @@ -185,7 +187,8 @@ fn verify_pop(bencher: divan::Bencher) { #[cfg(feature = "std")] mod worker_benches { - use dash_pkc::bls_ietf::{PublicKey, SecretKey, Signature}; + use super::{PublicKey, SecretKey, Signature}; + use dash_pkc::worker; fn setup_sigs(n: usize) -> Vec<(Signature, PublicKey, Vec)> { diff --git a/pkgs/pkc/src/bls/mod.rs b/pkgs/pkc/src/bls/mod.rs index c02bb1f7..0bf532c4 100644 --- a/pkgs/pkc/src/bls/mod.rs +++ b/pkgs/pkc/src/bls/mod.rs @@ -26,9 +26,11 @@ cfg_if::cfg_if! { mod scheme_chia; mod scheme_ietf; mod secret_ops; + mod share_ops; mod sig_aggregate; mod sig_basic; mod sig_pop; + mod sig_threshold; #[expect(unsafe_code, reason = "blst C FFI")] pub(crate) mod blst_ffi; pub(crate) mod chia_h2c; @@ -42,6 +44,7 @@ cfg_if::cfg_if! { pub use public_ops::BlsPublicKey; pub use scheme_ops::BlsScheme; pub use secret_ops::BlsSecretKey; + pub use share_ops::{BlsSigShare, BlsSkShare}; pub use sig_basic::BlsSignature; } } diff --git a/pkgs/pkc/src/bls/share_ops.rs b/pkgs/pkc/src/bls/share_ops.rs new file mode 100644 index 00000000..81c4d977 --- /dev/null +++ b/pkgs/pkc/src/bls/share_ops.rs @@ -0,0 +1,582 @@ +// +// Copyright (c) 2026-present, The Dash Core developers +// SPDX-License-Identifier: MIT +// See the accompanying file LICENSE or https://opensource.org/license/MIT +// + +//! Threshold share types and secret-key splitting. + +use super::error::BlsError; +use super::public_ops::BlsPublicKey; +use super::scheme_ops::BlsScheme; +use super::secret_ops::BlsSecretKey; +use super::sig_basic::BlsSignature; +use crate::prelude::*; + +use dash_num::Hash256; +use dash_types::{qtypestr, Unencodable}; +use rand_core::CryptoRngCore; + +use core::fmt::{Debug, Formatter, Result as FmtResult}; +use core::hash::{Hash, Hasher}; + +/// Secret key share for threshold signing. +#[derive(Unencodable)] +pub struct BlsSkShare { + id: Hash256, + sk: BlsSecretKey, +} + +impl BlsSkShare { + /// Construct a secret key share from an ID and a secret key. + pub fn new(id: Hash256, sk: BlsSecretKey) -> Self { + Self { id, sk } + } + + /// Participant identifier (32-byte hash). + pub fn id(&self) -> &Hash256 { + &self.id + } + + /// Sign a message of the scheme's message type, producing a signature share. + pub fn sign(&self, msg: &S::Msg) -> BlsSigShare { + BlsSigShare { + id: self.id, + sig: self.sk.sign(msg), + } + } + + /// The underlying secret key. + pub fn secret_key(&self) -> &BlsSecretKey { + &self.sk + } +} + +impl Clone for BlsSkShare { + fn clone(&self) -> Self { + Self { + id: self.id, + sk: self.sk.clone(), + } + } +} + +impl Debug for BlsSkShare { + fn fmt(&self, f: &mut Formatter<'_>) -> FmtResult { + qtypestr(f, core::any::type_name::())?; + write!(f, "(id={:?})", self.id) + } +} + +/// Signature share from a threshold participant. +#[derive(Unencodable)] +#[cfg_attr(feature = "serde", derive(::serde::Serialize, ::serde::Deserialize))] +#[cfg_attr(feature = "serde", serde(bound(serialize = "", deserialize = "")))] +pub struct BlsSigShare { + id: Hash256, + sig: BlsSignature, +} + +impl BlsSigShare { + /// Construct a signature share from an ID and a signature. + pub fn new(id: Hash256, sig: BlsSignature) -> Self { + Self { id, sig } + } + + /// Participant identifier (32-byte hash). + pub fn id(&self) -> &Hash256 { + &self.id + } + + /// The underlying signature. + pub fn signature(&self) -> &BlsSignature { + &self.sig + } +} + +impl Clone for BlsSigShare { + fn clone(&self) -> Self { + Self { + id: self.id, + sig: self.sig.clone(), + } + } +} + +impl Debug for BlsSigShare { + fn fmt(&self, f: &mut Formatter<'_>) -> FmtResult { + qtypestr(f, core::any::type_name::())?; + write!(f, "(id={:?})", self.id) + } +} + +impl PartialEq for BlsSigShare { + fn eq(&self, other: &Self) -> bool { + self.id == other.id && self.sig == other.sig + } +} + +impl Eq for BlsSigShare {} + +impl Hash for BlsSigShare { + fn hash(&self, state: &mut H) { + self.id.hash(state); + state.write(&self.sig.to_bytes()); + } +} + +impl BlsSecretKey { + /// Split this secret key into shares for the given participant IDs, requiring + /// `threshold` shares to recover. + /// + /// # Errors + /// + /// Returns `ThresholdTooLarge` if `threshold` is below 2 or exceeds the + /// number of ids, `InvalidShareId` if any id reduces to zero, + /// `DuplicateShareId` if two ids collide mod the group order, or + /// `InvalidSecretKey` if share generation fails. + pub fn split( + &self, + threshold: usize, + ids: &[Hash256], + rng: &mut impl CryptoRngCore, + ) -> Result>, BlsError> { + S::split_sk(&self.0, threshold, ids, rng, |id, inner| { + BlsSkShare::new(id, BlsSecretKey::from_inner(inner)) + }) + } +} + +impl BlsPublicKey { + /// Derive a public key share by evaluating the master public key polynomial + /// at the given participant id. + /// + /// # Errors + /// + /// Returns `InvalidVerificationVector` when fewer than two master keys are + /// given, `InvalidShareId` on a zero-reducing id, or `InvalidPublicKey` + /// when a coefficient or the result fails to decode. + pub fn derive_share(master_pks: &[&Self], id: &Hash256) -> Result { + let inner_refs: Vec<&S::InnerPk> = master_pks.iter().map(|pk| &pk.0).collect(); + S::derive_pk_share(&inner_refs, id).map(Self::from_inner) + } +} + +#[cfg(all(test, feature = "tests"))] +#[expect(clippy::unwrap_used, reason = "test code")] +mod tests { + use super::*; + use crate::bls::tests::{hash_from_hex, make_id, sequential_ids, GROUP_ORDER, SEED_0}; + use crate::bls::{BlsScChia, BlsScIetf}; + + use dash_dev::{arr_from_hex, Corpus, Value}; + use hex_conservative::DisplayHex; + use rand_core::OsRng; + use rstest::rstest; + + /// The scalar-field order `r + 1`, congruent to `1` mod `r`. + fn group_order_plus_one() -> Hash256 { + let mut bytes = GROUP_ORDER; + for b in bytes.iter_mut().rev() { + let (v, carry) = b.overflowing_add(1); + *b = v; + if !carry { + break; + } + } + Hash256::from_bytes(bytes) + } + + /// A 1-of-n split hands the master key to every participant, so a `threshold` + /// below 2 is rejected; one above the participant count yields a quorum that + /// can never sign. + fn assert_invalid_thresholds_rejected() { + let sk = BlsSecretKey::::generate(&SEED_0).unwrap(); + let ids = sequential_ids(5); + for threshold in [0, 1, ids.len() + 1] { + assert!(matches!( + sk.split(threshold, &ids, &mut OsRng), + Err(BlsError::ThresholdTooLarge) + )); + } + assert!(matches!(sk.split(2, &[], &mut OsRng), Err(BlsError::ThresholdTooLarge))); + } + + #[rstest] + #[case::chia(assert_invalid_thresholds_rejected::)] + #[case::ietf(assert_invalid_thresholds_rejected::)] + fn split_rejects_invalid_thresholds(#[case] assertion: fn()) { + assertion(); + } + + /// An id congruent to zero mod `r` would make the share equal the master key, + /// so both the zero hash and the group order are rejected. + fn assert_zero_reducing_id_rejected() { + let sk = BlsSecretKey::::generate(&SEED_0).unwrap(); + + let zero = Hash256::from_bytes([0u8; 32]); + let ids = [make_id(1), zero]; + assert!(matches!(sk.split(2, &ids, &mut OsRng), Err(BlsError::InvalidShareId))); + + let order = Hash256::from_bytes(GROUP_ORDER); + let ids = [make_id(1), order]; + assert!(matches!(sk.split(2, &ids, &mut OsRng), Err(BlsError::InvalidShareId))); + } + + #[rstest] + #[case::chia(assert_zero_reducing_id_rejected::)] + #[case::ietf(assert_zero_reducing_id_rejected::)] + fn split_rejects_zero_reducing_ids(#[case] assertion: fn()) { + assertion(); + } + + /// Two ids congruent mod `r` collide during interpolation, and a raw-byte + /// duplicate check would miss `1` and `r + 1`. + fn assert_congruent_ids_rejected() { + let sk = BlsSecretKey::::generate(&SEED_0).unwrap(); + let ids = [make_id(1), group_order_plus_one()]; + assert!(matches!(sk.split(2, &ids, &mut OsRng), Err(BlsError::DuplicateShareId))); + } + + #[rstest] + #[case::chia(assert_congruent_ids_rejected::)] + #[case::ietf(assert_congruent_ids_rejected::)] + fn split_rejects_congruent_ids(#[case] assertion: fn()) { + assertion(); + } + + /// Evaluating the verification-vector polynomial needs at least two + /// coefficients, so a single master key is rejected. + fn assert_derive_share_rejects_short_vv() { + let pk = BlsSecretKey::::generate(&SEED_0).unwrap().public_key(); + assert!(matches!( + BlsPublicKey::::derive_share(&[&pk], &make_id(1)), + Err(BlsError::InvalidVerificationVector) + )); + } + + #[rstest] + #[case::chia(assert_derive_share_rejects_short_vv::)] + #[case::ietf(assert_derive_share_rejects_short_vv::)] + fn derive_share_rejects_short_verification_vector(#[case] assertion: fn()) { + assertion(); + } + + /// End-to-end quorum DKG validation against reference vectors, exercising the + /// full flow: contribute -> verify -> commit -> finalize. + fn assert_llmq_contribute_vvec(corpus: &str) { + let f: Value = Corpus::open(env!("CARGO_MANIFEST_DIR"), corpus).into_value(); + let t = f["inputs"]["t"].as_u64().unwrap() as usize; + + for c in f["contribute"].as_array().unwrap() { + let vvec: Vec<&str> = c["vvec"] + .as_array() + .unwrap() + .iter() + .map(|v| v.as_str().unwrap()) + .collect(); + assert_eq!(vvec.len(), t, "vvec must hold one coefficient per threshold"); + + for pk_hex in &vvec { + assert!(BlsPublicKey::::from_bytes(&arr_from_hex(pk_hex)).is_ok()); + } + } + } + + #[rstest] + #[case::chia("bls_chia_llmq_100", assert_llmq_contribute_vvec::)] + #[case::ietf("bls_ietf_llmq_100", assert_llmq_contribute_vvec::)] + fn llmq_contribute_vvec(#[case] corpus: &str, #[case] assertion: fn(&str)) { + assertion(corpus); + } + + fn assert_llmq_contribute_sk_shares(corpus: &str) { + let f: Value = Corpus::open(env!("CARGO_MANIFEST_DIR"), corpus).into_value(); + let n = f["inputs"]["n"].as_u64().unwrap() as usize; + + for c in f["contribute"].as_array().unwrap() { + let shares = c["sk_shares"].as_array().unwrap(); + assert_eq!(shares.len(), n); + for s in shares { + assert!(BlsSecretKey::::from_bytes(&arr_from_hex(s.as_str().unwrap())).is_ok()); + } + } + } + + #[rstest] + #[case::chia("bls_chia_llmq_100", assert_llmq_contribute_sk_shares::)] + #[case::ietf("bls_ietf_llmq_100", assert_llmq_contribute_sk_shares::)] + fn llmq_contribute_sk_shares(#[case] corpus: &str, #[case] assertion: fn(&str)) { + assertion(corpus); + } + + fn assert_llmq_verify_contributions(corpus: &str) { + let f: Value = Corpus::open(env!("CARGO_MANIFEST_DIR"), corpus).into_value(); + let member_ids: Vec = f["inputs"]["member_ids"] + .as_array() + .unwrap() + .iter() + .map(|v| v.as_str().unwrap().to_string()) + .collect(); + + for v in f["verify"].as_array().unwrap() { + let member_idx = v["member_idx"].as_u64().unwrap() as usize; + let received_vvecs = v["received_vvecs"].as_array().unwrap(); + let received_sks = v["received_sk_contributions"].as_array().unwrap(); + let results = v["verification_results"].as_array().unwrap(); + + for (contrib_idx, ((vvec_arr, sk_hex), expected)) in received_vvecs + .iter() + .zip(received_sks.iter()) + .zip(results.iter()) + .enumerate() + { + let vvec: Vec> = vvec_arr + .as_array() + .unwrap() + .iter() + .map(|v| BlsPublicKey::::from_bytes(&arr_from_hex(v.as_str().unwrap())).unwrap()) + .collect(); + let vvec_refs: Vec<&BlsPublicKey> = vvec.iter().collect(); + + let sk_share = BlsSecretKey::::from_bytes(&arr_from_hex(sk_hex.as_str().unwrap())).unwrap(); + let pk_from_share = sk_share.public_key(); + + let member_id = hash_from_hex(&member_ids[member_idx]); + let pk_from_vvec = BlsPublicKey::derive_share(&vvec_refs, &member_id).unwrap(); + + let matches = pk_from_share.to_bytes() == pk_from_vvec.to_bytes(); + assert_eq!( + matches, + expected.as_bool().unwrap(), + "verification mismatch for member {} from contributor {}", + member_idx, + contrib_idx, + ); + } + } + } + + #[rstest] + #[case::chia("bls_chia_llmq_100", assert_llmq_verify_contributions::)] + #[case::ietf("bls_ietf_llmq_100", assert_llmq_verify_contributions::)] + fn llmq_verify_contributions(#[case] corpus: &str, #[case] assertion: fn(&str)) { + assertion(corpus); + } + + fn assert_llmq_commit_quorum_key(corpus: &str) { + let f: Value = Corpus::open(env!("CARGO_MANIFEST_DIR"), corpus).into_value(); + + let commits = f["commit"].as_array().unwrap(); + let expected_qpk = commits[0]["quorum_public_key"].as_str().unwrap(); + + for c in commits { + assert_eq!( + c["quorum_public_key"].as_str().unwrap(), + expected_qpk, + "quorum pk disagreement at member {}", + c["member_idx"], + ); + let qvvec = c["quorum_vvec"].as_array().unwrap(); + assert_eq!(qvvec[0].as_str().unwrap(), expected_qpk); + } + + // Reconstruct the quorum pk by aggregating each member's vvec[0]. + let contributions = f["contribute"].as_array().unwrap(); + let member_pks: Vec> = contributions + .iter() + .map(|c| BlsPublicKey::::from_bytes(&arr_from_hex(c["vvec"][0].as_str().unwrap())).unwrap()) + .collect(); + let pk_refs: Vec<&BlsPublicKey> = member_pks.iter().collect(); + let agg_pk = BlsPublicKey::aggregate(&pk_refs).unwrap(); + assert_eq!(agg_pk.to_bytes().to_lower_hex_string(), expected_qpk); + } + + #[rstest] + #[case::chia("bls_chia_llmq_100", assert_llmq_commit_quorum_key::)] + #[case::ietf("bls_ietf_llmq_100", assert_llmq_commit_quorum_key::)] + fn llmq_commit_quorum_key(#[case] corpus: &str, #[case] assertion: fn(&str)) { + assertion(corpus); + } + + fn assert_llmq_commit_sk_share(corpus: &str) { + let f: Value = Corpus::open(env!("CARGO_MANIFEST_DIR"), corpus).into_value(); + + // Each member's committed sk_share is the sum of the sk_contributions it + // received from every contributor. + for (member_idx, c) in f["commit"].as_array().unwrap().iter().enumerate() { + let expected_share = c["sk_share"].as_str().unwrap(); + + let mut received: Vec> = Vec::new(); + for contrib in f["contribute"].as_array().unwrap() { + let sk_hex = contrib["sk_shares"][member_idx].as_str().unwrap(); + received.push(BlsSecretKey::::from_bytes(&arr_from_hex(sk_hex)).unwrap()); + } + + let refs: Vec<&BlsSecretKey> = received.iter().collect(); + let agg = BlsSecretKey::aggregate(&refs).unwrap(); + assert_eq!( + agg.to_bytes().to_lower_hex_string(), + expected_share, + "sk_share mismatch for member {}", + member_idx, + ); + } + } + + #[rstest] + #[case::chia("bls_chia_llmq_100", assert_llmq_commit_sk_share::)] + #[case::ietf("bls_ietf_llmq_100", assert_llmq_commit_sk_share::)] + fn llmq_commit_sk_share(#[case] corpus: &str, #[case] assertion: fn(&str)) { + assertion(corpus); + } + + fn assert_llmq_commit_sig(corpus: &str, hash_field: &str, label: &str) { + let f: Value = Corpus::open(env!("CARGO_MANIFEST_DIR"), corpus).into_value(); + + for c in f["commit"].as_array().unwrap() { + let sk_share = BlsSecretKey::::from_bytes(&arr_from_hex(c["sk_share"].as_str().unwrap())).unwrap(); + let msg: [u8; 32] = arr_from_hex(c[hash_field].as_str().unwrap()); + + let sig = sk_share.sign(S::msg_ref(&msg)); + let pk = sk_share.public_key(); + assert!( + sig.verify(S::msg_ref(&msg), &pk).is_ok(), + "{} failed self-verification at member {}", + label, + c["member_idx"], + ); + } + } + + #[rstest] + #[case::chia_member("bls_chia_llmq_100", assert_llmq_commit_sig::, "commitment_hash", "member_sig")] + #[case::chia_quorum("bls_chia_llmq_100", assert_llmq_commit_sig::, "quorum_hash", "quorum_sig_share")] + #[case::ietf_member("bls_ietf_llmq_100", assert_llmq_commit_sig::, "commitment_hash", "member_sig")] + #[case::ietf_quorum("bls_ietf_llmq_100", assert_llmq_commit_sig::, "quorum_hash", "quorum_sig_share")] + fn llmq_commit_sig( + #[case] corpus: &str, + #[case] assertion: fn(&str, &str, &str), + #[case] hash_field: &str, + #[case] label: &str, + ) { + assertion(corpus, hash_field, label); + } + + fn assert_llmq_finalize_recover_quorum_sig(corpus: &str) { + let f: Value = Corpus::open(env!("CARGO_MANIFEST_DIR"), corpus).into_value(); + let fin = &f["finalize"]; + let commits = f["commit"].as_array().unwrap(); + + let member_ids: Vec = f["inputs"]["member_ids"] + .as_array() + .unwrap() + .iter() + .map(|v| v.as_str().unwrap().to_string()) + .collect(); + + let signer_ids: Vec = fin["signer_ids"] + .as_array() + .unwrap() + .iter() + .map(|v| v.as_str().unwrap().to_string()) + .collect(); + + let quorum_hash: [u8; 32] = arr_from_hex(fin["quorum_hash"].as_str().unwrap()); + + // signer_ids are in internal byte order; byte-reverse to match the + // display-order member_ids. + let sig_shares: Vec> = signer_ids + .iter() + .map(|sid| { + let sid_bytes = arr_from_hex::<32>(sid); + let sid_display = sid_bytes + .iter() + .copied() + .rev() + .collect::>() + .to_lower_hex_string(); + let idx = member_ids.iter().position(|m| *m == sid_display).unwrap(); + let sk = BlsSecretKey::::from_bytes(&arr_from_hex(commits[idx]["sk_share"].as_str().unwrap())).unwrap(); + let member_id = hash_from_hex(&sid_display); + BlsSkShare::new(member_id, sk).sign(S::msg_ref(&quorum_hash)) + }) + .collect(); + + let share_refs: Vec<&BlsSigShare> = sig_shares.iter().collect(); + let recovered = BlsSignature::recover(&share_refs).unwrap(); + + let quorum_pk = + BlsPublicKey::::from_bytes(&arr_from_hex(commits[0]["quorum_public_key"].as_str().unwrap())).unwrap(); + assert!( + recovered.verify(S::msg_ref(&quorum_hash), &quorum_pk).is_ok(), + "recovered quorum sig failed verification" + ); + + // Cross-check: recovery from all members should match the subset recovery. + let all_ids: Vec = member_ids.iter().map(|mid| hash_from_hex(mid)).collect(); + let all_shares: Vec> = commits + .iter() + .zip(all_ids.iter()) + .map(|(c, id)| { + let sk = BlsSecretKey::::from_bytes(&arr_from_hex(c["sk_share"].as_str().unwrap())).unwrap(); + BlsSkShare::new(*id, sk).sign(S::msg_ref(&quorum_hash)) + }) + .collect(); + let all_refs: Vec<&BlsSigShare> = all_shares.iter().collect(); + let recovered_all = BlsSignature::recover(&all_refs).unwrap(); + assert_eq!( + recovered.to_bytes(), + recovered_all.to_bytes(), + "recovery from subset and full set differ" + ); + } + + #[rstest] + #[case::chia("bls_chia_llmq_100", assert_llmq_finalize_recover_quorum_sig::)] + #[case::ietf("bls_ietf_llmq_100", assert_llmq_finalize_recover_quorum_sig::)] + fn llmq_finalize_recover_quorum_sig(#[case] corpus: &str, #[case] assertion: fn(&str)) { + assertion(corpus); + } + + fn assert_llmq_finalize_aggregated_member_sigs(corpus: &str) { + let f: Value = Corpus::open(env!("CARGO_MANIFEST_DIR"), corpus).into_value(); + let commits = f["commit"].as_array().unwrap(); + + // Re-sign the commitment hash with each member's sk_share, then aggregate. + let commitment_hash: [u8; 32] = arr_from_hex(commits[0]["commitment_hash"].as_str().unwrap()); + + let member_sigs: Vec> = commits + .iter() + .map(|c| { + let sk = BlsSecretKey::::from_bytes(&arr_from_hex(c["sk_share"].as_str().unwrap())).unwrap(); + sk.sign(S::msg_ref(&commitment_hash)) + }) + .collect(); + let sig_refs: Vec<&BlsSignature> = member_sigs.iter().collect(); + let agg_sig = BlsSignature::aggregate(&sig_refs).unwrap(); + + let member_pks: Vec> = commits + .iter() + .map(|c| { + let sk = BlsSecretKey::::from_bytes(&arr_from_hex(c["sk_share"].as_str().unwrap())).unwrap(); + sk.public_key() + }) + .collect(); + let pk_refs: Vec<&BlsPublicKey> = member_pks.iter().collect(); + + assert!( + agg_sig + .fast_verify_aggregates(S::msg_ref(&commitment_hash), &pk_refs) + .is_ok(), + "aggregated member sigs failed fast_verify" + ); + } + + #[rstest] + #[case::chia("bls_chia_llmq_100", assert_llmq_finalize_aggregated_member_sigs::)] + #[case::ietf("bls_ietf_llmq_100", assert_llmq_finalize_aggregated_member_sigs::)] + fn llmq_finalize_aggregated_member_sigs(#[case] corpus: &str, #[case] assertion: fn(&str)) { + assertion(corpus); + } +} diff --git a/pkgs/pkc/src/bls/sig_threshold.rs b/pkgs/pkc/src/bls/sig_threshold.rs new file mode 100644 index 00000000..3777dc2b --- /dev/null +++ b/pkgs/pkc/src/bls/sig_threshold.rs @@ -0,0 +1,122 @@ +// +// Copyright (c) 2026-present, The Dash Core developers +// SPDX-License-Identifier: MIT +// See the accompanying file LICENSE or https://opensource.org/license/MIT +// + +//! Threshold signature recovery via Lagrange interpolation. + +use super::error::BlsError; +use super::scheme_ops::BlsScheme; +use super::share_ops::BlsSigShare; +use super::sig_basic::BlsSignature; +use crate::prelude::*; + +use dash_num::Hash256; + +impl BlsSignature { + /// Recover a full signature from threshold signature shares via Lagrange + /// interpolation in G2. + /// + /// # Errors + /// + /// Returns `InsufficientShares` if fewer than 2 shares are provided, + /// `InvalidShareId`/`DuplicateShareId` on bad ids, or `InvalidSignature` + /// when a share fails to decode. + pub fn recover(shares: &[&BlsSigShare]) -> Result { + let ids: Vec<&Hash256> = shares.iter().map(|s| s.id()).collect(); + let sigs: Vec<&S::InnerSig> = shares.iter().map(|s| &s.signature().0).collect(); + + S::recover_sig_shares(&ids, &sigs).map(BlsSignature::from_inner) + } +} + +#[cfg(all(test, feature = "tests"))] +#[expect(clippy::unwrap_used, reason = "test code")] +mod tests { + use crate::bls::scheme_ops::BlsScheme; + use crate::bls::tests::{sequential_ids, MSG_DEADBEEF, SEED_0}; + use crate::bls::{BlsError, BlsScChia, BlsScIetf, BlsSecretKey, BlsSigShare, BlsSignature}; + use crate::prelude::*; + + use rand_core::OsRng; + use rstest::rstest; + + fn assert_threshold_split_recover() { + let sk = BlsSecretKey::::generate(&SEED_0).unwrap(); + let pk = sk.public_key(); + let ids = sequential_ids(5); + + let shares = sk.split(3, &ids, &mut OsRng).unwrap(); + assert_eq!(shares.len(), 5); + + // Any threshold-sized subset recovers the master signature. Comparing + // against the master's own signature, not just verifying, is what pins + // interpolation to the right point rather than to a self-consistent one. + let msg = S::msg_ref(&MSG_DEADBEEF); + let sig_shares: Vec> = shares[..3].iter().map(|s| s.sign(msg)).collect(); + let refs: Vec<&BlsSigShare> = sig_shares.iter().collect(); + let recovered = BlsSignature::::recover(&refs).unwrap(); + assert!(recovered.verify(msg, &pk).is_ok()); + assert_eq!(recovered.to_bytes(), sk.sign(msg).to_bytes()); + + // A different subset recovers the identical signature. + let sig_shares2: Vec> = shares[2..5].iter().map(|s| s.sign(msg)).collect(); + let refs2: Vec<&BlsSigShare> = sig_shares2.iter().collect(); + let recovered2 = BlsSignature::::recover(&refs2).unwrap(); + assert_eq!(recovered.to_bytes(), recovered2.to_bytes()); + } + + #[rstest] + #[case::chia(assert_threshold_split_recover::)] + #[case::ietf(assert_threshold_split_recover::)] + fn threshold_split_and_recover(#[case] assertion: fn()) { + assertion(); + } + + /// Interpolating fewer than `threshold` shares still yields a point, so the + /// guard against a short quorum is that the result fails verification. + fn assert_sub_threshold_does_not_verify() { + let sk = BlsSecretKey::::generate(&SEED_0).unwrap(); + let pk = sk.public_key(); + let shares = sk.split(3, &sequential_ids(5), &mut OsRng).unwrap(); + let msg = S::msg_ref(&MSG_DEADBEEF); + let signed: Vec> = shares.iter().map(|s| s.sign(msg)).collect(); + + let below = BlsSignature::::recover(&[&signed[0], &signed[1]]).unwrap(); + assert!(below.verify(msg, &pk).is_err()); + + let at = BlsSignature::::recover(&[&signed[0], &signed[2], &signed[4]]).unwrap(); + assert!(at.verify(msg, &pk).is_ok()); + } + + #[rstest] + #[case::chia(assert_sub_threshold_does_not_verify::)] + #[case::ietf(assert_sub_threshold_does_not_verify::)] + fn sub_threshold_recovery_does_not_verify(#[case] assertion: fn()) { + assertion(); + } + + fn assert_insufficient_shares_rejected() { + assert!(matches!( + BlsSignature::::recover(&[]), + Err(BlsError::InsufficientShares) + )); + + let sk = BlsSecretKey::::generate(&SEED_0).unwrap(); + let ids = sequential_ids(3); + let shares = sk.split(2, &ids, &mut OsRng).unwrap(); + let one = shares[0].sign(S::msg_ref(&MSG_DEADBEEF)); + assert!(matches!( + BlsSignature::::recover(&[&one]), + Err(BlsError::InsufficientShares) + )); + } + + #[rstest] + #[case::chia(assert_insufficient_shares_rejected::)] + #[case::ietf(assert_insufficient_shares_rejected::)] + fn recover_rejects_insufficient_shares(#[case] assertion: fn()) { + assertion(); + } +} diff --git a/pkgs/pkc/src/bls/tests.rs b/pkgs/pkc/src/bls/tests.rs index 702347dd..793e2ba7 100644 --- a/pkgs/pkc/src/bls/tests.rs +++ b/pkgs/pkc/src/bls/tests.rs @@ -6,8 +6,6 @@ //! Shared test fixtures and constants. -use crate::bls_chia::SecretKey as ChiaSk; -use crate::bls_ietf::SecretKey as IetfSk; use crate::prelude::*; use hex_conservative::hex; @@ -51,30 +49,6 @@ pub fn msg32() -> [u8; 32] { MSG_DEADBEEF } -/// Key derived from all-zero IKM. -#[fixture] -pub fn chia_sk0() -> ChiaSk { - ChiaSk::generate(&RSEED[0]).unwrap() -} - -/// Key derived from all-zero IKM. -#[fixture] -pub fn ietf_sk0() -> IetfSk { - IetfSk::generate(&RSEED[0]).unwrap() -} - -/// Key derived from all-one IKM. -#[fixture] -pub fn chia_sk1() -> ChiaSk { - ChiaSk::generate(&RSEED[1]).unwrap() -} - -/// Key derived from all-one IKM. -#[fixture] -pub fn ietf_sk1() -> IetfSk { - IetfSk::generate(&RSEED[1]).unwrap() -} - /// Build a distinct 32-byte IKM from an index, for multi-signer tests. pub fn test_ikm(i: u8) -> [u8; 32] { let mut ikm = [0u8; 32]; diff --git a/pkgs/pkc/src/bls_chia/mod.rs b/pkgs/pkc/src/bls_chia/mod.rs deleted file mode 100644 index 0cb42c36..00000000 --- a/pkgs/pkc/src/bls_chia/mod.rs +++ /dev/null @@ -1,20 +0,0 @@ -// -// Copyright (c) 2026-present, The Dash Core developers -// SPDX-License-Identifier: MIT -// See the accompanying file LICENSE or https://opensource.org/license/MIT -// - -//! Legacy BLS signatures (non-standard hash-to-G2, min-pubkey-size). - -pub mod threshold; - -pub use crate::bls::BlsError; - -/// A legacy BLS public key (48-byte G1 point in legacy serialization). -pub type PublicKey = crate::bls::BlsPublicKey; - -/// A legacy BLS secret key (32-byte scalar). -pub type SecretKey = crate::bls::BlsSecretKey; - -/// A legacy BLS signature (96-byte G2 point in legacy serialization). -pub type Signature = crate::bls::BlsSignature; diff --git a/pkgs/pkc/src/bls_chia/threshold.rs b/pkgs/pkc/src/bls_chia/threshold.rs deleted file mode 100644 index d9b865e6..00000000 --- a/pkgs/pkc/src/bls_chia/threshold.rs +++ /dev/null @@ -1,141 +0,0 @@ -// -// Copyright (c) 2026-present, The Dash Core developers -// SPDX-License-Identifier: MIT -// See the accompanying file LICENSE or https://opensource.org/license/MIT -// - -//! Thresholds for legacy scheme (m-of-n secret sharing and signature recovery). - -use super::{PublicKey, SecretKey, Signature}; -use crate::bls::scheme_ops::BlsScheme; -use crate::bls::{BlsError, BlsScChia}; -use crate::prelude::*; - -use dash_num::Hash256; -use dash_types::Unencodable; - -use core::fmt; -use core::hash::{Hash, Hasher}; - -/// Secret key share for threshold signing. -#[derive(Clone)] -pub struct SecretKeyShare { - id: Hash256, - sk: SecretKey, -} - -impl SecretKeyShare { - /// Construct a secret key share from an ID and a secret key. - pub fn new(id: Hash256, sk: SecretKey) -> Self { - Self { id, sk } - } - - /// Participant identifier (32-byte hash). - pub fn id(&self) -> &Hash256 { - &self.id - } - - /// Sign a 32-byte message, producing a signature share. - pub fn sign(&self, msg: &[u8; 32]) -> SignatureShare { - SignatureShare { - id: self.id, - sig: self.sk.sign(msg), - } - } - - /// The underlying secret key. - pub fn secret_key(&self) -> &SecretKey { - &self.sk - } -} - -impl fmt::Debug for SecretKeyShare { - fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { - write!(f, "SecretKeyShare(id={:?})", self.id) - } -} - -/// Signature share from one threshold participant. -#[derive(Clone, Eq, PartialEq, Unencodable)] -#[cfg_attr(feature = "serde", derive(::serde::Serialize, ::serde::Deserialize))] -pub struct SignatureShare { - id: Hash256, - sig: Signature, -} - -impl Hash for SignatureShare { - fn hash(&self, state: &mut H) { - self.id.hash(state); - state.write(&self.sig.to_bytes()); - } -} - -impl SignatureShare { - /// Construct a signature share from an ID and a signature. - pub fn new(id: Hash256, sig: Signature) -> Self { - Self { id, sig } - } - - /// Participant identifier (32-byte hash). - pub fn id(&self) -> &Hash256 { - &self.id - } - - /// The underlying signature. - pub fn signature(&self) -> &Signature { - &self.sig - } -} - -impl fmt::Debug for SignatureShare { - fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { - write!(f, "SignatureShare(id={:?})", self.id) - } -} - -/// Split a secret key into shares for the given participant IDs, requiring -/// `threshold` shares to recover. -/// -/// # Errors -/// -/// Returns `ThresholdTooLarge` if `threshold < 2` (a 1-of-n split hands -/// the master key to every participant), `ids` is empty, or `threshold > -/// ids.len()`; `InvalidShareId` if any id reduces to zero in the scalar -/// field; `DuplicateShareId` if any ids collide after reduction; -/// `InvalidSecretKey` if share generation or parsing fails. -pub fn split_sk( - sk: &SecretKey, - threshold: usize, - ids: &[Hash256], - rng: &mut impl rand_core::CryptoRngCore, -) -> Result, BlsError> { - BlsScChia::split_sk(&sk.0, threshold, ids, rng, |id, inner| { - SecretKeyShare::new(id, SecretKey::from_inner(inner)) - }) -} - -/// Recover a full signature from threshold signature shares via Lagrange -/// interpolation in G2. -/// -/// # Errors -/// -/// Returns `InsufficientShares` if fewer than 2 shares are provided, -/// `InvalidShareId` if any id reduces to zero in the scalar field, or -/// `DuplicateShareId` if any ids collide after reduction. -pub fn recover_sig(shares: &[&SignatureShare]) -> Result { - let ids: Vec<_> = shares.iter().map(|s| &s.id).collect(); - let sigs: Vec<_> = shares.iter().map(|s| &s.sig.0).collect(); - BlsScChia::recover_sig_shares(&ids, &sigs).map(Signature::from_inner) -} - -/// Derive a public key share by evaluating the master public -/// key polynomial at the given participant id. -/// -/// # Errors -/// -/// Returns `InvalidVerificationVector` if fewer than 2 master keys are -/// given, or `InvalidShareId` if `id` reduces to zero in the scalar field. -pub fn derive_pk_share(master_pks: &[&PublicKey], id: &Hash256) -> Result { - let pks: Vec<_> = master_pks.iter().map(|pk| &pk.0).collect(); - BlsScChia::derive_pk_share(&pks, id).map(PublicKey::from_inner) -} diff --git a/pkgs/pkc/src/bls_ietf/mod.rs b/pkgs/pkc/src/bls_ietf/mod.rs deleted file mode 100644 index 6659c271..00000000 --- a/pkgs/pkc/src/bls_ietf/mod.rs +++ /dev/null @@ -1,22 +0,0 @@ -// -// Copyright (c) 2026-present, The Dash Core developers -// SPDX-License-Identifier: MIT -// See the accompanying file LICENSE or https://opensource.org/license/MIT -// - -//! IETF BLS12-381 signatures (basic scheme, min-pubkey-size). - -pub mod threshold; - -pub use crate::bls::BlsError; -/// BLS signature scheme (determines the DST). -pub use crate::bls::BlsSigId as Scheme; - -/// An IETF BLS public key (48-byte compressed G1 point). -pub type PublicKey = crate::bls::BlsPublicKey; - -/// An IETF BLS secret key (32-byte scalar). -pub type SecretKey = crate::bls::BlsSecretKey; - -/// An IETF BLS signature (96-byte compressed G2 point). -pub type Signature = crate::bls::BlsSignature; diff --git a/pkgs/pkc/src/bls_ietf/threshold.rs b/pkgs/pkc/src/bls_ietf/threshold.rs deleted file mode 100644 index b6b7ec1e..00000000 --- a/pkgs/pkc/src/bls_ietf/threshold.rs +++ /dev/null @@ -1,143 +0,0 @@ -// -// Copyright (c) 2026-present, The Dash Core developers -// SPDX-License-Identifier: MIT -// See the accompanying file LICENSE or https://opensource.org/license/MIT -// - -//! Thresholds for IETF scheme (m-of-n secret sharing and signature recovery). - -use super::{PublicKey, SecretKey, Signature}; -use crate::bls::scheme_ops::BlsScheme; -use crate::bls::{BlsError, BlsScIetf}; -use crate::prelude::*; - -use dash_num::Hash256; -use dash_types::Unencodable; - -use core::fmt; -use core::hash::{Hash, Hasher}; - -/// Secret key share for threshold signing. -#[derive(Clone)] -pub struct SecretKeyShare { - id: Hash256, - sk: SecretKey, -} - -impl SecretKeyShare { - /// Construct a secret key share from an ID and a secret key. - pub fn new(id: Hash256, sk: SecretKey) -> Self { - Self { id, sk } - } - - /// Participant identifier (32-byte hash). - pub fn id(&self) -> &Hash256 { - &self.id - } - - /// Sign a message, producing a signature share. - pub fn sign(&self, msg: &[u8]) -> SignatureShare { - SignatureShare { - id: self.id, - sig: self.sk.sign(msg), - } - } - - /// The underlying secret key. - pub fn secret_key(&self) -> &SecretKey { - &self.sk - } -} - -impl fmt::Debug for SecretKeyShare { - fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { - write!(f, "SecretKeyShare(id={:?})", self.id) - } -} - -/// Signature share from one threshold participant. -#[derive(Clone, Eq, PartialEq, Unencodable)] -#[cfg_attr(feature = "serde", derive(::serde::Serialize, ::serde::Deserialize))] -pub struct SignatureShare { - id: Hash256, - sig: Signature, -} - -impl Hash for SignatureShare { - fn hash(&self, state: &mut H) { - self.id.hash(state); - state.write(&self.sig.to_bytes()); - } -} - -impl SignatureShare { - /// Construct a signature share from an ID and a signature. - pub fn new(id: Hash256, sig: Signature) -> Self { - Self { id, sig } - } - - /// Participant identifier (32-byte hash). - pub fn id(&self) -> &Hash256 { - &self.id - } - - /// The underlying signature. - pub fn signature(&self) -> &Signature { - &self.sig - } -} - -impl fmt::Debug for SignatureShare { - fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { - write!(f, "SignatureShare(id={:?})", self.id) - } -} - -/// Split a secret key into shares for the given participant IDs, requiring -/// `threshold` shares to recover. -/// -/// # Errors -/// -/// Returns `ThresholdTooLarge` if `threshold < 2` (a 1-of-n split hands -/// the master key to every participant), `ids` is empty, or `threshold > -/// ids.len()`; `InvalidShareId` if any id reduces to zero in the scalar -/// field; `DuplicateShareId` if any ids collide after reduction; -/// `InvalidSecretKey` if share generation or parsing fails. -pub fn split_sk( - sk: &SecretKey, - threshold: usize, - ids: &[Hash256], - rng: &mut impl rand_core::CryptoRngCore, -) -> Result, BlsError> { - BlsScIetf::split_sk(&sk.0, threshold, ids, rng, |id, inner| { - SecretKeyShare::new(id, SecretKey::from_inner(inner)) - }) -} - -/// Recover a full signature from threshold signature shares via Lagrange -/// interpolation in G2. -/// -/// # Errors -/// -/// Returns `InsufficientShares` if fewer than 2 shares are provided, -/// `InvalidShareId` if any id reduces to zero, `DuplicateShareId` if any -/// ids collide after reduction, or `InvalidSignature` if a share or the -/// recovered point fails to decode. -pub fn recover_sig(shares: &[&SignatureShare]) -> Result { - let ids: Vec<_> = shares.iter().map(|s| &s.id).collect(); - let sigs: Vec<_> = shares.iter().map(|s| &s.sig.0).collect(); - BlsScIetf::recover_sig_shares(&ids, &sigs).map(Signature::from_inner) -} - -/// Derive a public key share by evaluating the master public -/// key polynomial at the given participant id. -/// -/// # Errors -/// -/// Returns `InvalidVerificationVector` if fewer than 2 master keys are -/// given, `InvalidShareId` if `id` reduces to zero in the scalar field, -/// or `InvalidPublicKey` if a coefficient or the result fails to decode. -pub fn derive_pk_share(master_pks: &[&PublicKey], id: &Hash256) -> Result { - let pks: Vec<_> = master_pks.iter().map(|pk| &pk.0).collect(); - BlsScIetf::derive_pk_share(&pks, id).map(PublicKey::from_inner) -} diff --git a/pkgs/pkc/src/lib.rs b/pkgs/pkc/src/lib.rs index 2f9325df..21824285 100644 --- a/pkgs/pkc/src/lib.rs +++ b/pkgs/pkc/src/lib.rs @@ -24,10 +24,3 @@ pub mod worker; pub mod __private { pub use crate::ecdsa::PubKeyHash as __PubKeyHash; } - -cfg_if::cfg_if! { - if #[cfg(feature = "bls")] { - pub mod bls_chia; - pub mod bls_ietf; - } -} diff --git a/pkgs/pkc/src/prelude.rs b/pkgs/pkc/src/prelude.rs index b0509665..f00ae53d 100644 --- a/pkgs/pkc/src/prelude.rs +++ b/pkgs/pkc/src/prelude.rs @@ -7,6 +7,6 @@ //! Re-exports for no_std compatibility. pub(crate) use alloc::format; -pub(crate) use alloc::string::String; +pub(crate) use alloc::string::{String, ToString}; pub(crate) use alloc::vec; pub(crate) use alloc::vec::Vec; diff --git a/pkgs/pkc/tests/bls_chia_llmq.rs b/pkgs/pkc/tests/bls_chia_llmq.rs deleted file mode 100644 index fdb476c9..00000000 --- a/pkgs/pkc/tests/bls_chia_llmq.rs +++ /dev/null @@ -1,282 +0,0 @@ -// -// Copyright (c) 2026-present, The Dash Core developers -// SPDX-License-Identifier: MIT -// See the accompanying file LICENSE or https://opensource.org/license/MIT -// - -//! End-to-end quorum DKG validation for legacy BLS. - -#![expect(clippy::unwrap_used, reason = "test code")] - -use dash_dev::{arr_from_hex, Corpus}; -use dash_num::Hash256; -use dash_pkc::bls::tests as common; -use dash_pkc::bls_chia::{threshold, PublicKey, SecretKey, Signature}; -use hex_conservative::DisplayHex; - -#[test] -fn llmq_contribute_vvec() { - let f: dash_dev::Value = Corpus::open(env!("CARGO_MANIFEST_DIR"), "bls_chia_llmq_100").into_value(); - - for c in f["contribute"].as_array().unwrap() { - let vvec: Vec<&str> = c["vvec"] - .as_array() - .unwrap() - .iter() - .map(|v| v.as_str().unwrap()) - .collect(); - - for pk_hex in &vvec { - assert!(PublicKey::from_bytes(&arr_from_hex(pk_hex)).is_ok()); - } - } -} - -#[test] -fn llmq_contribute_sk_shares() { - let f: dash_dev::Value = Corpus::open(env!("CARGO_MANIFEST_DIR"), "bls_chia_llmq_100").into_value(); - let n = f["inputs"]["n"].as_u64().unwrap() as usize; - - for c in f["contribute"].as_array().unwrap() { - let shares = c["sk_shares"].as_array().unwrap(); - assert_eq!(shares.len(), n); - for s in shares { - assert!(SecretKey::from_bytes(&arr_from_hex(s.as_str().unwrap())).is_ok()); - } - } -} - -#[test] -fn llmq_verify_contributions() { - let f: dash_dev::Value = Corpus::open(env!("CARGO_MANIFEST_DIR"), "bls_chia_llmq_100").into_value(); - let member_ids: Vec = f["inputs"]["member_ids"] - .as_array() - .unwrap() - .iter() - .map(|v| v.as_str().unwrap().to_string()) - .collect(); - - for v in f["verify"].as_array().unwrap() { - let member_idx = v["member_idx"].as_u64().unwrap() as usize; - let received_vvecs = v["received_vvecs"].as_array().unwrap(); - let received_sks = v["received_sk_contributions"].as_array().unwrap(); - let results = v["verification_results"].as_array().unwrap(); - - for (contrib_idx, ((vvec_arr, sk_hex), expected)) in received_vvecs - .iter() - .zip(received_sks.iter()) - .zip(results.iter()) - .enumerate() - { - let vvec: Vec = vvec_arr - .as_array() - .unwrap() - .iter() - .map(|v| PublicKey::from_bytes(&arr_from_hex(v.as_str().unwrap())).unwrap()) - .collect(); - let vvec_refs: Vec<&PublicKey> = vvec.iter().collect(); - - let sk_share = SecretKey::from_bytes(&arr_from_hex(sk_hex.as_str().unwrap())).unwrap(); - let pk_from_share = sk_share.public_key(); - - let member_id = common::hash_from_hex(&member_ids[member_idx]); - let pk_from_vvec = threshold::derive_pk_share(&vvec_refs, &member_id).unwrap(); - - let matches = pk_from_share.to_bytes() == pk_from_vvec.to_bytes(); - assert_eq!( - matches, - expected.as_bool().unwrap(), - "verification mismatch for member {} from contributor {}", - member_idx, - contrib_idx, - ); - } - } -} - -#[test] -fn llmq_commit_quorum_key() { - let f: dash_dev::Value = Corpus::open(env!("CARGO_MANIFEST_DIR"), "bls_chia_llmq_100").into_value(); - - let commits = f["commit"].as_array().unwrap(); - let expected_qpk = commits[0]["quorum_public_key"].as_str().unwrap(); - - for c in commits { - assert_eq!(c["quorum_public_key"].as_str().unwrap(), expected_qpk); - let qvvec = c["quorum_vvec"].as_array().unwrap(); - assert_eq!(qvvec[0].as_str().unwrap(), expected_qpk); - } - - // Reconstruct by aggregating each member's vvec[0]. - let contributions = f["contribute"].as_array().unwrap(); - let member_pks: Vec = contributions - .iter() - .map(|c| PublicKey::from_bytes(&arr_from_hex(c["vvec"][0].as_str().unwrap())).unwrap()) - .collect(); - let pk_refs: Vec<&PublicKey> = member_pks.iter().collect(); - let agg_pk = PublicKey::aggregate(&pk_refs).unwrap(); - assert_eq!(agg_pk.to_bytes().to_lower_hex_string(), expected_qpk); -} - -#[test] -fn llmq_commit_sk_share() { - let f: dash_dev::Value = Corpus::open(env!("CARGO_MANIFEST_DIR"), "bls_chia_llmq_100").into_value(); - - for (member_idx, c) in f["commit"].as_array().unwrap().iter().enumerate() { - let expected_share = c["sk_share"].as_str().unwrap(); - - let mut received: Vec = Vec::new(); - for contrib in f["contribute"].as_array().unwrap() { - let sk_hex = contrib["sk_shares"][member_idx].as_str().unwrap(); - received.push(SecretKey::from_bytes(&arr_from_hex(sk_hex)).unwrap()); - } - - let refs: Vec<&SecretKey> = received.iter().collect(); - let agg = SecretKey::aggregate(&refs).unwrap(); - assert_eq!(agg.to_bytes().to_lower_hex_string(), expected_share); - } -} - -#[test] -fn llmq_commit_member_sig() { - let f: dash_dev::Value = Corpus::open(env!("CARGO_MANIFEST_DIR"), "bls_chia_llmq_100").into_value(); - - for c in f["commit"].as_array().unwrap() { - let sk_share = SecretKey::from_bytes(&arr_from_hex(c["sk_share"].as_str().unwrap())).unwrap(); - let commitment_hash = arr_from_hex(c["commitment_hash"].as_str().unwrap()); - - // Sign and self-verify. - let sig = sk_share.sign(&commitment_hash); - let pk = sk_share.public_key(); - assert!( - sig.verify(&commitment_hash, &pk).is_ok(), - "member_sig failed self-verification at member {}", - c["member_idx"], - ); - } -} - -#[test] -fn llmq_commit_quorum_sig_share() { - let f: dash_dev::Value = Corpus::open(env!("CARGO_MANIFEST_DIR"), "bls_chia_llmq_100").into_value(); - - for c in f["commit"].as_array().unwrap() { - let sk_share = SecretKey::from_bytes(&arr_from_hex(c["sk_share"].as_str().unwrap())).unwrap(); - let quorum_hash = arr_from_hex(c["quorum_hash"].as_str().unwrap()); - - // Sign and self-verify. - let sig = sk_share.sign(&quorum_hash); - let pk = sk_share.public_key(); - assert!( - sig.verify(&quorum_hash, &pk).is_ok(), - "quorum_sig_share failed self-verification at member {}", - c["member_idx"], - ); - } -} - -#[test] -fn llmq_finalize_recover_quorum_sig() { - let f: dash_dev::Value = Corpus::open(env!("CARGO_MANIFEST_DIR"), "bls_chia_llmq_100").into_value(); - let fin = &f["finalize"]; - let commits = f["commit"].as_array().unwrap(); - - let member_ids: Vec = f["inputs"]["member_ids"] - .as_array() - .unwrap() - .iter() - .map(|v| v.as_str().unwrap().to_string()) - .collect(); - - let signer_ids: Vec = fin["signer_ids"] - .as_array() - .unwrap() - .iter() - .map(|v| v.as_str().unwrap().to_string()) - .collect(); - - let quorum_hash = arr_from_hex(fin["quorum_hash"].as_str().unwrap()); - - // signer_ids are internal byte order; byte-reverse to - // match the display-order member_ids. - let sig_shares: Vec = signer_ids - .iter() - .map(|sid| { - let sid_bytes = arr_from_hex::<32>(sid); - let sid_display = sid_bytes - .iter() - .copied() - .rev() - .collect::>() - .to_lower_hex_string(); - let idx = member_ids.iter().position(|m| *m == sid_display).unwrap(); - let sk = SecretKey::from_bytes(&arr_from_hex(commits[idx]["sk_share"].as_str().unwrap())).unwrap(); - let member_id = common::hash_from_hex(&sid_display); - let sk_share = threshold::SecretKeyShare::new(member_id, sk); - sk_share.sign(&quorum_hash) - }) - .collect(); - - let share_refs: Vec<&threshold::SignatureShare> = sig_shares.iter().collect(); - let recovered = threshold::recover_sig(&share_refs).unwrap(); - - let quorum_pk = PublicKey::from_bytes(&arr_from_hex(commits[0]["quorum_public_key"].as_str().unwrap())).unwrap(); - assert!( - recovered.verify(&quorum_hash, &quorum_pk).is_ok(), - "recovered quorum sig failed verification" - ); - - // Cross-check: recovery from all members should match. - let all_ids: Vec = member_ids.iter().map(|mid| common::hash_from_hex(mid)).collect(); - let all_shares: Vec = commits - .iter() - .zip(all_ids.iter()) - .map(|(c, id)| { - let sk = SecretKey::from_bytes(&arr_from_hex(c["sk_share"].as_str().unwrap())).unwrap(); - let sk_share = threshold::SecretKeyShare::new(*id, sk); - sk_share.sign(&quorum_hash) - }) - .collect(); - let all_refs: Vec<&threshold::SignatureShare> = all_shares.iter().collect(); - let recovered_all = threshold::recover_sig(&all_refs).unwrap(); - assert_eq!( - recovered.to_bytes(), - recovered_all.to_bytes(), - "recovery from subset and full set differ" - ); -} - -#[test] -fn llmq_finalize_aggregated_member_sigs() { - let f: dash_dev::Value = Corpus::open(env!("CARGO_MANIFEST_DIR"), "bls_chia_llmq_100").into_value(); - let commits = f["commit"].as_array().unwrap(); - - // Re-sign the commitment hash with each member's - // sk_share using our library, then aggregate. - let commitment_hash = arr_from_hex(commits[0]["commitment_hash"].as_str().unwrap()); - - let member_sigs: Vec = commits - .iter() - .map(|c| { - let sk = SecretKey::from_bytes(&arr_from_hex(c["sk_share"].as_str().unwrap())).unwrap(); - sk.sign(&commitment_hash) - }) - .collect(); - let sig_refs: Vec<&Signature> = member_sigs.iter().collect(); - - let agg_sig = Signature::aggregate(&sig_refs).unwrap(); - - let member_pks: Vec = commits - .iter() - .map(|c| { - let sk = SecretKey::from_bytes(&arr_from_hex(c["sk_share"].as_str().unwrap())).unwrap(); - sk.public_key() - }) - .collect(); - let pk_refs: Vec<&PublicKey> = member_pks.iter().collect(); - - assert!( - agg_sig.fast_verify_aggregates(&commitment_hash, &pk_refs).is_ok(), - "aggregated member sigs failed fast_verify" - ); -} diff --git a/pkgs/pkc/tests/bls_chia_threshold.rs b/pkgs/pkc/tests/bls_chia_threshold.rs deleted file mode 100644 index 10797d43..00000000 --- a/pkgs/pkc/tests/bls_chia_threshold.rs +++ /dev/null @@ -1,93 +0,0 @@ -// -// Copyright (c) 2026-present, The Dash Core developers -// SPDX-License-Identifier: MIT -// See the accompanying file LICENSE or https://opensource.org/license/MIT -// - -//! Threshold split and recovery tests for bls_chia. - -#![expect(clippy::unwrap_used, reason = "test code")] - -use common::*; -use dash_num::Hash256; -use dash_pkc::bls::tests as common; -use dash_pkc::bls_chia::{threshold, BlsError, SecretKey}; -use rstest::*; - -/// Threshold split/recover with legacy signing. -#[rstest] -fn threshold_split_recover(chia_sk0: SecretKey, msg32: [u8; 32]) { - let ids = common::sequential_ids(5); - let mut rng = rand_core::OsRng; - let shares = threshold::split_sk(&chia_sk0, 3, &ids, &mut rng).unwrap(); - let full_sig = chia_sk0.sign(&msg32); - - let sig_shares: Vec<_> = shares.iter().map(|s| s.sign(&msg32)).collect(); - let subset: Vec<&threshold::SignatureShare> = vec![&sig_shares[0], &sig_shares[2], &sig_shares[4]]; - let recovered = threshold::recover_sig(&subset).unwrap(); - assert_eq!(recovered.to_bytes(), full_sig.to_bytes()); -} - -/// An id that reduces to zero mod r (the null id, or the group order -/// itself) would make its share equal the master key; reject it. -#[rstest] -#[case::null_id([0u8; 32])] -#[case::group_order(GROUP_ORDER)] -fn split_rejects_zero_reducing_ids(chia_sk0: SecretKey, #[case] zero_id: [u8; 32]) { - let mut ids = common::sequential_ids(3); - let mut rng = rand_core::OsRng; - - ids[1] = Hash256::from(zero_id); - assert_eq!( - threshold::split_sk(&chia_sk0, 2, &ids, &mut rng).unwrap_err(), - BlsError::InvalidShareId - ); -} - -/// 1 and r+1 are distinct hashes but the same scalar mod r; a raw-byte -/// duplicate check misses them and interpolation would divide by zero. -#[rstest] -fn split_rejects_ids_congruent_mod_order(chia_sk0: SecretKey) { - let mut one = [0u8; 32]; - one[31] = 1; - let mut order_plus_one = GROUP_ORDER; - order_plus_one[31] = 2; - let ids = [Hash256::from(one), Hash256::from(order_plus_one)]; - - let mut rng = rand_core::OsRng; - assert_eq!( - threshold::split_sk(&chia_sk0, 2, &ids, &mut rng).unwrap_err(), - BlsError::DuplicateShareId - ); -} - -/// A verification vector shorter than 2 elements is malformed -/// (polynomial evaluation requires at least 2 coefficients). -#[rstest] -fn derive_pk_share_rejects_short_verification_vector(chia_sk0: SecretKey) { - let pk = chia_sk0.public_key(); - let id = common::make_id(1); - assert_eq!( - threshold::derive_pk_share(&[&pk], &id).unwrap_err(), - BlsError::InvalidVerificationVector - ); -} - -/// Recovery below threshold succeeds but yields a point unrelated to the -/// master signature; callers must verify recovered signatures. -#[rstest] -fn sub_threshold_recovery_does_not_verify(chia_sk0: SecretKey, msg32: [u8; 32]) { - let ids = common::sequential_ids(5); - let mut rng = rand_core::OsRng; - let shares = threshold::split_sk(&chia_sk0, 3, &ids, &mut rng).unwrap(); - let sig_shares: Vec<_> = shares.iter().map(|s| s.sign(&msg32)).collect(); - let pk = chia_sk0.public_key(); - - let below: Vec<&threshold::SignatureShare> = vec![&sig_shares[0], &sig_shares[1]]; - let recovered = threshold::recover_sig(&below).unwrap(); - assert!(recovered.verify(&msg32, &pk).is_err()); - - let at: Vec<&threshold::SignatureShare> = vec![&sig_shares[0], &sig_shares[2], &sig_shares[4]]; - let recovered = threshold::recover_sig(&at).unwrap(); - assert!(recovered.verify(&msg32, &pk).is_ok()); -} diff --git a/pkgs/pkc/tests/bls_ietf_llmq.rs b/pkgs/pkc/tests/bls_ietf_llmq.rs deleted file mode 100644 index c63ec36f..00000000 --- a/pkgs/pkc/tests/bls_ietf_llmq.rs +++ /dev/null @@ -1,327 +0,0 @@ -// -// Copyright (c) 2026-present, The Dash Core developers -// SPDX-License-Identifier: MIT -// See the accompanying file LICENSE or https://opensource.org/license/MIT -// - -//! End-to-end quorum DKG validation for IETF BLS. -//! -//! Exercises the full distributed key generation flow: -//! contribute -> verify -> commit -> finalize, validating -//! each step against reference vectors. - -#![expect(clippy::unwrap_used, reason = "test code")] - -use dash_dev::{arr_from_hex, Corpus}; -use dash_num::Hash256; -use dash_pkc::bls::tests as common; -use dash_pkc::bls_ietf::{threshold, PublicKey, SecretKey, Signature}; -use hex_conservative::DisplayHex; - -#[test] -fn llmq_contribute_vvec() { - let f: dash_dev::Value = Corpus::open(env!("CARGO_MANIFEST_DIR"), "bls_ietf_llmq_100").into_value(); - - let t = f["inputs"]["t"].as_u64().unwrap() as usize; - - let contributions = f["contribute"].as_array().unwrap(); - for c in contributions { - let vvec: Vec<&str> = c["vvec"] - .as_array() - .unwrap() - .iter() - .map(|v| v.as_str().unwrap()) - .collect(); - - // A commitment to a degree t-1 polynomial, so exactly t coefficients, - // the first being the member's contribution public key. - assert_eq!(vvec.len(), t, "vvec must hold one coefficient per threshold"); - - // Each vvec entry is a valid G1 point. - for pk_hex in &vvec { - assert!(PublicKey::from_bytes(&arr_from_hex(pk_hex)).is_ok()); - } - } -} - -#[test] -fn llmq_contribute_sk_shares() { - let f: dash_dev::Value = Corpus::open(env!("CARGO_MANIFEST_DIR"), "bls_ietf_llmq_100").into_value(); - let n = f["inputs"]["n"].as_u64().unwrap() as usize; - - for c in f["contribute"].as_array().unwrap() { - let shares = c["sk_shares"].as_array().unwrap(); - assert_eq!(shares.len(), n); - - // Each share is a valid 32-byte scalar. - for s in shares { - let sk = SecretKey::from_bytes(&arr_from_hex(s.as_str().unwrap())); - assert!(sk.is_ok()); - } - } -} - -#[test] -fn llmq_verify_contributions() { - let f: dash_dev::Value = Corpus::open(env!("CARGO_MANIFEST_DIR"), "bls_ietf_llmq_100").into_value(); - let member_ids: Vec = f["inputs"]["member_ids"] - .as_array() - .unwrap() - .iter() - .map(|v| v.as_str().unwrap().to_string()) - .collect(); - - for v in f["verify"].as_array().unwrap() { - let member_idx = v["member_idx"].as_u64().unwrap() as usize; - let received_vvecs = v["received_vvecs"].as_array().unwrap(); - let received_sks = v["received_sk_contributions"].as_array().unwrap(); - let results = v["verification_results"].as_array().unwrap(); - - // For each contributor, verify the sk_contribution - // against the vvec using polynomial evaluation. - for (contrib_idx, ((vvec_arr, sk_hex), expected)) in received_vvecs - .iter() - .zip(received_sks.iter()) - .zip(results.iter()) - .enumerate() - { - let vvec: Vec = vvec_arr - .as_array() - .unwrap() - .iter() - .map(|v| PublicKey::from_bytes(&arr_from_hex(v.as_str().unwrap())).unwrap()) - .collect(); - let vvec_refs: Vec<&PublicKey> = vvec.iter().collect(); - - let sk_share = SecretKey::from_bytes(&arr_from_hex(sk_hex.as_str().unwrap())).unwrap(); - let pk_from_share = sk_share.public_key(); - - // Evaluate the vvec polynomial at the receiver's - // participant ID. - let member_id = common::hash_from_hex(&member_ids[member_idx]); - let pk_from_vvec = threshold::derive_pk_share(&vvec_refs, &member_id).unwrap(); - - let matches = pk_from_share.to_bytes() == pk_from_vvec.to_bytes(); - assert_eq!( - matches, - expected.as_bool().unwrap(), - "verification mismatch for member {} from contributor {}", - member_idx, - contrib_idx, - ); - } - } -} - -#[test] -fn llmq_commit_quorum_key() { - let f: dash_dev::Value = Corpus::open(env!("CARGO_MANIFEST_DIR"), "bls_ietf_llmq_100").into_value(); - - // All members should agree on the quorum public key. - let commits = f["commit"].as_array().unwrap(); - let expected_qpk = commits[0]["quorum_public_key"].as_str().unwrap(); - - for c in commits { - assert_eq!( - c["quorum_public_key"].as_str().unwrap(), - expected_qpk, - "quorum pk disagreement at member {}", - c["member_idx"], - ); - - // The quorum vvec is the sum of all member vvecs. - // quorum_vvec[0] == quorum_public_key. - let qvvec = c["quorum_vvec"].as_array().unwrap(); - assert_eq!(qvvec[0].as_str().unwrap(), expected_qpk,); - } - - // The quorum pk can be reconstructed by aggregating - // each member's vvec[0] (their contribution pk). - let contributions = f["contribute"].as_array().unwrap(); - let member_pks: Vec = contributions - .iter() - .map(|c| PublicKey::from_bytes(&arr_from_hex(c["vvec"][0].as_str().unwrap())).unwrap()) - .collect(); - let pk_refs: Vec<&PublicKey> = member_pks.iter().collect(); - let agg_pk = PublicKey::aggregate(&pk_refs).unwrap(); - assert_eq!(agg_pk.to_bytes().to_lower_hex_string(), expected_qpk,); -} - -#[test] -fn llmq_commit_sk_share() { - let f: dash_dev::Value = Corpus::open(env!("CARGO_MANIFEST_DIR"), "bls_ietf_llmq_100").into_value(); - - // Each member's sk_share in the commit phase is the - // sum of all received sk_contributions for that member. - for (member_idx, c) in f["commit"].as_array().unwrap().iter().enumerate() { - let expected_share = c["sk_share"].as_str().unwrap(); - - // Collect the sk_contributions this member received - // from all contributors. - let mut received: Vec = Vec::new(); - for contrib in f["contribute"].as_array().unwrap() { - let sk_hex = contrib["sk_shares"][member_idx].as_str().unwrap(); - received.push(SecretKey::from_bytes(&arr_from_hex(sk_hex)).unwrap()); - } - - let refs: Vec<&SecretKey> = received.iter().collect(); - let agg = SecretKey::aggregate(&refs).unwrap(); - assert_eq!( - agg.to_bytes().to_lower_hex_string(), - expected_share, - "sk_share mismatch for member {}", - member_idx, - ); - } -} - -#[test] -fn llmq_commit_member_sig() { - let f: dash_dev::Value = Corpus::open(env!("CARGO_MANIFEST_DIR"), "bls_ietf_llmq_100").into_value(); - - for c in f["commit"].as_array().unwrap() { - let sk_share = SecretKey::from_bytes(&arr_from_hex(c["sk_share"].as_str().unwrap())).unwrap(); - let commitment_hash = arr_from_hex::<32>(c["commitment_hash"].as_str().unwrap()); - - // Sign the commitment hash and verify against pk. - let sig = sk_share.sign(&commitment_hash); - let pk = sk_share.public_key(); - assert!( - sig.verify(&commitment_hash, &pk).is_ok(), - "member_sig failed self-verification at member {}", - c["member_idx"], - ); - } -} - -#[test] -fn llmq_commit_quorum_sig_share() { - let f: dash_dev::Value = Corpus::open(env!("CARGO_MANIFEST_DIR"), "bls_ietf_llmq_100").into_value(); - - for c in f["commit"].as_array().unwrap() { - let sk_share = SecretKey::from_bytes(&arr_from_hex(c["sk_share"].as_str().unwrap())).unwrap(); - let quorum_hash = arr_from_hex::<32>(c["quorum_hash"].as_str().unwrap()); - - // Sign the quorum hash and verify. - let sig = sk_share.sign(&quorum_hash); - let pk = sk_share.public_key(); - assert!( - sig.verify(&quorum_hash, &pk).is_ok(), - "quorum_sig_share failed self-verification at member {}", - c["member_idx"], - ); - } -} - -#[test] -fn llmq_finalize_recover_quorum_sig() { - let f: dash_dev::Value = Corpus::open(env!("CARGO_MANIFEST_DIR"), "bls_ietf_llmq_100").into_value(); - let fin = &f["finalize"]; - let commits = f["commit"].as_array().unwrap(); - - let member_ids: Vec = f["inputs"]["member_ids"] - .as_array() - .unwrap() - .iter() - .map(|v| v.as_str().unwrap().to_string()) - .collect(); - - let signer_ids: Vec = fin["signer_ids"] - .as_array() - .unwrap() - .iter() - .map(|v| v.as_str().unwrap().to_string()) - .collect(); - - let quorum_hash = arr_from_hex::<32>(fin["quorum_hash"].as_str().unwrap()); - - // signer_ids are in internal byte order; member_ids - // are in display (reversed) order. We need to map - // signer_ids back to member indices and use - // Hash256::from_hex (display order) for the ID. - let sig_shares: Vec = signer_ids - .iter() - .map(|sid| { - // sid is internal byte order; byte-reverse to get - // the display hex that matches member_ids. - let sid_bytes = arr_from_hex::<32>(sid); - let sid_display = sid_bytes - .iter() - .copied() - .rev() - .collect::>() - .to_lower_hex_string(); - let idx = member_ids.iter().position(|m| *m == sid_display).unwrap(); - let sk = SecretKey::from_bytes(&arr_from_hex(commits[idx]["sk_share"].as_str().unwrap())).unwrap(); - let member_id = common::hash_from_hex(&sid_display); - let sk_share = threshold::SecretKeyShare::new(member_id, sk); - sk_share.sign(&quorum_hash) - }) - .collect(); - - let share_refs: Vec<&threshold::SignatureShare> = sig_shares.iter().collect(); - let recovered = threshold::recover_sig(&share_refs).unwrap(); - - // Verify the recovered signature against the quorum pk. - let quorum_pk = PublicKey::from_bytes(&arr_from_hex(commits[0]["quorum_public_key"].as_str().unwrap())).unwrap(); - assert!( - recovered.verify(&quorum_hash, &quorum_pk).is_ok(), - "recovered quorum sig failed verification" - ); - - // Cross-check: recovery from all members should match. - let all_ids: Vec = member_ids.iter().map(|mid| common::hash_from_hex(mid)).collect(); - let all_shares: Vec = commits - .iter() - .zip(all_ids.iter()) - .map(|(c, id)| { - let sk = SecretKey::from_bytes(&arr_from_hex(c["sk_share"].as_str().unwrap())).unwrap(); - let sk_share = threshold::SecretKeyShare::new(*id, sk); - sk_share.sign(&quorum_hash) - }) - .collect(); - let all_refs: Vec<&threshold::SignatureShare> = all_shares.iter().collect(); - let recovered_all = threshold::recover_sig(&all_refs).unwrap(); - assert_eq!( - recovered.to_bytes(), - recovered_all.to_bytes(), - "recovery from subset and full set differ" - ); -} - -#[test] -fn llmq_finalize_aggregated_member_sigs() { - let f: dash_dev::Value = Corpus::open(env!("CARGO_MANIFEST_DIR"), "bls_ietf_llmq_100").into_value(); - let commits = f["commit"].as_array().unwrap(); - - // Re-sign the commitment hash with each member's - // sk_share using our library, then aggregate. - let commitment_hash = arr_from_hex::<32>(commits[0]["commitment_hash"].as_str().unwrap()); - - let member_sigs: Vec = commits - .iter() - .map(|c| { - let sk = SecretKey::from_bytes(&arr_from_hex(c["sk_share"].as_str().unwrap())).unwrap(); - sk.sign(&commitment_hash) - }) - .collect(); - let sig_refs: Vec<&Signature> = member_sigs.iter().collect(); - - let agg_sig = Signature::aggregate(&sig_refs).unwrap(); - - // Verify the aggregated member sig against the - // commitment hash using each member's public key. - let member_pks: Vec = commits - .iter() - .map(|c| { - let sk = SecretKey::from_bytes(&arr_from_hex(c["sk_share"].as_str().unwrap())).unwrap(); - sk.public_key() - }) - .collect(); - let pk_refs: Vec<&PublicKey> = member_pks.iter().collect(); - - assert!( - agg_sig.fast_verify_aggregates(&commitment_hash, &pk_refs).is_ok(), - "aggregated member sigs failed fast_verify" - ); -} diff --git a/pkgs/pkc/tests/bls_ietf_threshold.rs b/pkgs/pkc/tests/bls_ietf_threshold.rs deleted file mode 100644 index dc8bfc13..00000000 --- a/pkgs/pkc/tests/bls_ietf_threshold.rs +++ /dev/null @@ -1,93 +0,0 @@ -// -// Copyright (c) 2026-present, The Dash Core developers -// SPDX-License-Identifier: MIT -// See the accompanying file LICENSE or https://opensource.org/license/MIT -// - -//! Threshold split and recovery tests for bls_ietf. - -#![expect(clippy::unwrap_used, reason = "test code")] - -use common::*; -use dash_num::Hash256; -use dash_pkc::bls::tests as common; -use dash_pkc::bls_ietf::{threshold, BlsError, SecretKey}; -use rstest::*; - -/// Split into 5 shares, recover from 3, verify the recovered -/// signature matches the original. -#[rstest] -fn threshold_split_recover(ietf_sk0: SecretKey) { - let ids = common::sequential_ids(5); - let mut rng = rand_core::OsRng; - let shares = threshold::split_sk(&ietf_sk0, 3, &ids, &mut rng).unwrap(); - assert_eq!(shares.len(), 5); - - let msg = b"threshold test message"; - let full_sig = ietf_sk0.sign(msg); - - let sig_shares: Vec<_> = shares.iter().map(|s| s.sign(msg)).collect(); - - // Recover from shares 0, 2, 4 (any 3 of 5). - let subset: Vec<&threshold::SignatureShare> = vec![&sig_shares[0], &sig_shares[2], &sig_shares[4]]; - let recovered = threshold::recover_sig(&subset).unwrap(); - assert_eq!(recovered.to_bytes(), full_sig.to_bytes()); -} - -/// Threshold recovery with insufficient shares fails. -#[rstest] -fn threshold_insufficient_shares() { - assert!(threshold::recover_sig(&[]).is_err()); -} - -/// Invalid threshold parameters are rejected. -#[rstest] -fn threshold_invalid_params(ietf_sk0: SecretKey) { - let mut rng = rand_core::OsRng; - let ids = common::sequential_ids(5); - assert!(threshold::split_sk(&ietf_sk0, 0, &ids, &mut rng).is_err()); - let ids6 = common::sequential_ids(5); - assert!(threshold::split_sk(&ietf_sk0, 6, &ids6, &mut rng).is_err()); -} - -/// An id reducing to zero mod r would leak the master key via its share. -#[rstest] -#[case::null_id([0u8; 32])] -#[case::group_order(GROUP_ORDER)] -fn split_rejects_zero_reducing_ids(ietf_sk0: SecretKey, #[case] zero_id: [u8; 32]) { - let mut ids = common::sequential_ids(3); - let mut rng = rand_core::OsRng; - - ids[1] = Hash256::from(zero_id); - assert_eq!( - threshold::split_sk(&ietf_sk0, 2, &ids, &mut rng).unwrap_err(), - BlsError::InvalidShareId - ); -} - -/// 1 and r+1 collide as scalars mod r; a raw-byte check misses them. -#[rstest] -fn split_rejects_ids_congruent_mod_order(ietf_sk0: SecretKey) { - let mut one = [0u8; 32]; - one[31] = 1; - let mut order_plus_one = GROUP_ORDER; - order_plus_one[31] = 2; - let ids = [Hash256::from(one), Hash256::from(order_plus_one)]; - - let mut rng = rand_core::OsRng; - assert_eq!( - threshold::split_sk(&ietf_sk0, 2, &ids, &mut rng).unwrap_err(), - BlsError::DuplicateShareId - ); -} - -/// A verification vector shorter than 2 elements is malformed. -#[rstest] -fn derive_pk_share_rejects_short_verification_vector(ietf_sk0: SecretKey) { - let pk = ietf_sk0.public_key(); - let id = common::make_id(1); - assert_eq!( - threshold::derive_pk_share(&[&pk], &id).unwrap_err(), - BlsError::InvalidVerificationVector - ); -} diff --git a/pkgs/pkc/tests/common/mod.rs b/pkgs/pkc/tests/common/mod.rs deleted file mode 100644 index b6c9e9a0..00000000 --- a/pkgs/pkc/tests/common/mod.rs +++ /dev/null @@ -1,12 +0,0 @@ -// -// Copyright (c) 2026-present, The Dash Core developers -// SPDX-License-Identifier: MIT -// See the accompanying file LICENSE or https://opensource.org/license/MIT -// - -//! Shared test fixtures and constants. - -#![allow(dead_code, reason = "usage dependent on build flags")] - -/// Shared test constants. -pub const MSG_DEADBEEF: [u8; 32] = hex_conservative::hex!("deadbeefdeadbeefdeadbeefdeadbeefcafebabecafebabecafebabecafebabe"); From 87e7c1469c0a882834a87240bf8d7bc09686599a Mon Sep 17 00:00:00 2001 From: Kittywhiskers Van Gogh <63189531+kwvg@users.noreply.github.com> Date: Wed, 5 Aug 2026 09:14:02 +0530 Subject: [PATCH 13/14] pkc%refac(bench): unify BLS benchmarks over the scheme trait --- pkgs/pkc/bench/bls.rs | 243 +++++++++++++++++++++++++++++++++ pkgs/pkc/bench/bls_chia.rs | 189 ------------------------- pkgs/pkc/bench/bls_ietf.rs | 223 ------------------------------ pkgs/pkc/bench/main.rs | 4 +- pkgs/pkc/src/bls/scheme_ops.rs | 6 +- 5 files changed, 247 insertions(+), 418 deletions(-) create mode 100644 pkgs/pkc/bench/bls.rs delete mode 100644 pkgs/pkc/bench/bls_chia.rs delete mode 100644 pkgs/pkc/bench/bls_ietf.rs diff --git a/pkgs/pkc/bench/bls.rs b/pkgs/pkc/bench/bls.rs new file mode 100644 index 00000000..bf1f780a --- /dev/null +++ b/pkgs/pkc/bench/bls.rs @@ -0,0 +1,243 @@ +// +// Copyright (c) 2026-present, The Dash Core developers +// SPDX-License-Identifier: MIT +// See the accompanying file LICENSE or https://opensource.org/license/MIT +// + +//! Benchmarks for BLS schemes. + +use dash_pkc::bls::tests::{sequential_ids, test_ikm, test_msg}; +use dash_pkc::bls::{BlsPublicKey, BlsScChia, BlsScIetf, BlsScheme, BlsSecretKey, BlsSigShare, BlsSignature}; +use divan::{counter::ItemsCount, Bencher}; +use rand_core::OsRng; + +/// Single signature creation. +#[divan::bench(types = [BlsScChia, BlsScIetf])] +fn sign(bencher: Bencher) { + let sk = BlsSecretKey::::generate(&test_ikm(1)).unwrap(); + bencher + .counter(ItemsCount::new(1u32)) + .bench(|| sk.sign(S::msg_ref(&test_msg(42)))); +} + +/// Single signature verification. +#[divan::bench(types = [BlsScChia, BlsScIetf])] +fn verify(bencher: Bencher) { + let sk = BlsSecretKey::::generate(&test_ikm(2)).unwrap(); + let msg = test_msg(99); + let sig = sk.sign(S::msg_ref(&msg)); + let pk = sk.public_key(); + bencher + .counter(ItemsCount::new(1u32)) + .bench(|| sig.verify(S::msg_ref(&msg), &pk)); +} + +/// Public key aggregation at various quorum sizes. +#[divan::bench(types = [BlsScChia, BlsScIetf], args = [2, 5, 25, 50, 100])] +fn aggregate_pk_n(bencher: Bencher, n: usize) { + let pks: Vec<_> = (0..n) + .map(|i| BlsSecretKey::::generate(&test_ikm(i as u8)).unwrap().public_key()) + .collect(); + let pk_refs: Vec<_> = pks.iter().collect(); + bencher + .counter(ItemsCount::new(n)) + .bench(|| BlsPublicKey::::aggregate(&pk_refs)); +} + +/// Signature aggregation at various batch sizes. +#[divan::bench(types = [BlsScChia, BlsScIetf], args = [2, 10, 100])] +fn aggregate_sig_n(bencher: Bencher, n: usize) { + let keys: Vec<_> = (0..n) + .map(|i| BlsSecretKey::::generate(&test_ikm(i as u8)).unwrap()) + .collect(); + let sigs: Vec<_> = keys + .iter() + .enumerate() + .map(|(i, key)| key.sign(S::msg_ref(&test_msg(i as u8)))) + .collect(); + let sig_refs: Vec<&BlsSignature> = sigs.iter().collect(); + bencher + .counter(ItemsCount::new(n)) + .bench(|| BlsSignature::::aggregate(&sig_refs)); +} + +/// N individual verifications in a loop. +#[divan::bench(types = [BlsScChia, BlsScIetf], args = [100, 1000])] +fn verify_n_individual(bencher: Bencher, n: usize) { + let keys: Vec<_> = (0..n) + .map(|i| BlsSecretKey::::generate(&test_ikm(i as u8)).unwrap()) + .collect(); + let msgs: Vec<[u8; 32]> = (0..n).map(|i| test_msg(i as u8)).collect(); + let pks: Vec<_> = keys.iter().map(BlsSecretKey::public_key).collect(); + let sigs: Vec<_> = keys + .iter() + .zip(&msgs) + .map(|(key, msg)| key.sign(S::msg_ref(msg))) + .collect(); + + bencher.counter(ItemsCount::new(n)).bench(|| { + for i in 0..n { + let _ = sigs[i].verify(S::msg_ref(&msgs[i]), &pks[i]); + } + }); +} + +/// Fast aggregate verification over a shared message. +#[divan::bench(types = [BlsScChia, BlsScIetf], args = [10, 100, 1000])] +fn fast_verify_n(bencher: Bencher, n: usize) { + let keys: Vec<_> = (0..n) + .map(|i| BlsSecretKey::::generate(&test_ikm(i as u8)).unwrap()) + .collect(); + let msg = test_msg(42); + let pks: Vec<_> = keys.iter().map(BlsSecretKey::public_key).collect(); + let sigs: Vec<_> = keys.iter().map(|key| key.sign(S::msg_ref(&msg))).collect(); + let sig_refs: Vec<&BlsSignature> = sigs.iter().collect(); + let aggregate = BlsSignature::::aggregate(&sig_refs).unwrap(); + let pk_refs: Vec<_> = pks.iter().collect(); + + bencher + .counter(ItemsCount::new(n)) + .bench(|| aggregate.fast_verify_aggregates(S::msg_ref(&msg), &pk_refs)); +} + +/// Public key serialization. +#[divan::bench(types = [BlsScChia, BlsScIetf])] +fn ser_pk(bencher: Bencher) { + let pk = BlsSecretKey::::generate(&test_ikm(1)).unwrap().public_key(); + bencher.bench(|| pk.to_bytes()); +} + +/// Public key deserialization. +#[divan::bench(types = [BlsScChia, BlsScIetf])] +fn deser_pk(bencher: Bencher) { + let bytes = BlsSecretKey::::generate(&test_ikm(1)) + .unwrap() + .public_key() + .to_bytes(); + bencher.bench(|| BlsPublicKey::::from_bytes(&bytes)); +} + +/// Signature serialization. +#[divan::bench(types = [BlsScChia, BlsScIetf])] +fn ser_sig(bencher: Bencher) { + let sig = BlsSecretKey::::generate(&test_ikm(1)) + .unwrap() + .sign(S::msg_ref(&test_msg(0))); + bencher.bench(|| sig.to_bytes()); +} + +/// Signature deserialization. +#[divan::bench(types = [BlsScChia, BlsScIetf])] +fn deser_sig(bencher: Bencher) { + let bytes = BlsSecretKey::::generate(&test_ikm(1)) + .unwrap() + .sign(S::msg_ref(&test_msg(0))) + .to_bytes(); + bencher.bench(|| BlsSignature::::from_bytes(&bytes)); +} + +/// Threshold secret key splitting at various quorum sizes. +#[divan::bench(types = [BlsScChia, BlsScIetf], args = [5, 10, 50])] +fn split_threshold(bencher: Bencher, n: usize) { + let sk = BlsSecretKey::::generate(&test_ikm(1)).unwrap(); + let threshold = n.div_ceil(2); + let ids = sequential_ids(n); + bencher + .counter(ItemsCount::new(n)) + .bench(|| sk.split(threshold, &ids, &mut OsRng)); +} + +/// Threshold signature recovery via Lagrange interpolation. +#[divan::bench(types = [BlsScChia, BlsScIetf], args = [3, 5, 10])] +fn recover_threshold(bencher: Bencher, threshold: usize) { + let sk = BlsSecretKey::::generate(&test_ikm(1)).unwrap(); + let ids = sequential_ids(threshold * 2); + let shares = sk.split(threshold, &ids, &mut OsRng).unwrap(); + let msg = test_msg(42); + let sig_shares: Vec<_> = shares.iter().map(|share| share.sign(S::msg_ref(&msg))).collect(); + let subset: Vec<&BlsSigShare> = sig_shares.iter().take(threshold).collect(); + bencher + .counter(ItemsCount::new(threshold)) + .bench(|| BlsSignature::::recover(&subset)); +} + +/// IETF-only BLS operations. +mod ietf { + use super::*; + + /// Aggregate signatures over distinct messages, then verify. + #[divan::bench(args = [10, 100, 1000])] + fn verify_aggregated_block(bencher: Bencher, n: usize) { + let keys: Vec<_> = (0..n) + .map(|i| BlsSecretKey::::generate(&test_ikm(i as u8)).unwrap()) + .collect(); + let msgs: Vec<[u8; 32]> = (0..n).map(|i| test_msg(i as u8)).collect(); + let pks: Vec<_> = keys.iter().map(BlsSecretKey::public_key).collect(); + let sigs: Vec<_> = keys + .iter() + .zip(&msgs) + .map(|(key, msg)| key.sign(msg.as_slice())) + .collect(); + let sig_refs: Vec<&BlsSignature> = sigs.iter().collect(); + let aggregate = BlsSignature::::aggregate(&sig_refs).unwrap(); + let pk_refs: Vec<_> = pks.iter().collect(); + let msg_refs: Vec<&[u8]> = msgs.iter().map(|msg| msg.as_slice()).collect(); + + bencher + .counter(ItemsCount::new(n)) + .bench(|| aggregate.verify_aggregates(&msg_refs, &pk_refs)); + } + + /// Proof of possession creation. + #[divan::bench] + fn prove_pop(bencher: Bencher) { + let sk = BlsSecretKey::::generate(&test_ikm(1)).unwrap(); + bencher.bench(|| sk.prove_possession()); + } + + /// Proof of possession verification. + #[divan::bench] + fn verify_pop(bencher: Bencher) { + let sk = BlsSecretKey::::generate(&test_ikm(1)).unwrap(); + let pop = sk.prove_possession(); + let pk = sk.public_key(); + bencher.bench(|| pk.verify_possession(&pop)); + } +} + +#[cfg(feature = "std")] +mod worker { + use super::*; + + use dash_pkc::worker; + + fn setup_sigs(n: usize) -> Vec<(BlsSignature, BlsPublicKey, [u8; 32])> { + (0..n) + .map(|i| { + let sk = BlsSecretKey::::generate(&test_ikm(i as u8)).unwrap(); + let msg = test_msg(i as u8); + let pk = sk.public_key(); + let sig = sk.sign(S::msg_ref(&msg)); + (sig, pk, msg) + }) + .collect() + } + + #[divan::bench(types = [BlsScChia, BlsScIetf], args = [100, 1000])] + fn verify_n(bencher: Bencher, n: usize) { + let tuples = setup_sigs::(n); + bencher + .counter(ItemsCount::new(n)) + .bench(|| worker::par_verify(&tuples, |(sig, pk, msg)| sig.verify(S::msg_ref(msg), pk).is_ok())); + } + + #[divan::bench(types = [BlsScChia, BlsScIetf], args = [100, 1000])] + fn aggregate_pk_n(bencher: Bencher, n: usize) { + let pks: Vec> = (0..n) + .map(|i| BlsSecretKey::::generate(&test_ikm(i as u8)).unwrap().public_key()) + .collect(); + bencher + .counter(ItemsCount::new(n)) + .bench(|| worker::par_reduce(pks.clone(), |a, b| BlsPublicKey::::aggregate(&[&a, &b]).unwrap())); + } +} diff --git a/pkgs/pkc/bench/bls_chia.rs b/pkgs/pkc/bench/bls_chia.rs deleted file mode 100644 index bb2cfa70..00000000 --- a/pkgs/pkc/bench/bls_chia.rs +++ /dev/null @@ -1,189 +0,0 @@ -// -// Copyright (c) 2026-present, The Dash Core developers -// SPDX-License-Identifier: MIT -// See the accompanying file LICENSE or https://opensource.org/license/MIT -// - -//! Benchmarks for the legacy BLS module. - -use common::*; -use dash_pkc::bls::tests as common; -use dash_pkc::bls::{BlsPublicKey, BlsScChia, BlsSecretKey, BlsSignature}; - -type PublicKey = BlsPublicKey; -type SecretKey = BlsSecretKey; -type Signature = BlsSignature; - -/// Single signature creation (legacy hash-to-G2). -#[divan::bench] -fn sign(bencher: divan::Bencher) { - let sk = SecretKey::generate(&test_ikm(1)).unwrap(); - bencher - .counter(divan::counter::ItemsCount::new(1u32)) - .bench(|| sk.sign(&test_msg(42))); -} - -/// Single signature verification (pairing check). -#[divan::bench] -fn verify(bencher: divan::Bencher) { - let sk = SecretKey::generate(&test_ikm(2)).unwrap(); - let msg = test_msg(99); - let sig = sk.sign(&msg); - let pk = sk.public_key(); - bencher - .counter(divan::counter::ItemsCount::new(1u32)) - .bench(|| sig.verify(&msg, &pk)); -} - -/// Public key aggregation at various quorum sizes. -#[divan::bench(args = [2, 5, 25, 50, 100])] -fn aggregate_pk_n(bencher: divan::Bencher, n: usize) { - let pks: Vec<_> = (0..n) - .map(|i| SecretKey::generate(&test_ikm(i as u8)).unwrap().public_key()) - .collect(); - let pk_refs: Vec<_> = pks.iter().collect(); - bencher - .counter(divan::counter::ItemsCount::new(n)) - .bench(|| PublicKey::aggregate(&pk_refs)); -} - -/// Signature aggregation at various batch sizes. -#[divan::bench(args = [2, 10, 100])] -fn aggregate_sig_n(bencher: divan::Bencher, n: usize) { - let keys: Vec<_> = (0..n) - .map(|i| SecretKey::generate(&test_ikm(i as u8)).unwrap()) - .collect(); - let sigs: Vec<_> = keys - .iter() - .enumerate() - .map(|(i, k)| k.sign(&test_msg(i as u8))) - .collect(); - let sig_refs: Vec<&Signature> = sigs.iter().collect(); - bencher - .counter(divan::counter::ItemsCount::new(n)) - .bench(|| Signature::aggregate(&sig_refs)); -} - -/// N individual verifications in a loop. -#[divan::bench(args = [100, 1000])] -fn verify_n_individual(bencher: divan::Bencher, n: usize) { - let keys: Vec<_> = (0..n) - .map(|i| SecretKey::generate(&test_ikm(i as u8)).unwrap()) - .collect(); - let msgs: Vec<[u8; 32]> = (0..n).map(|i| test_msg(i as u8)).collect(); - let pks: Vec<_> = keys.iter().map(|k| k.public_key()).collect(); - let sigs: Vec<_> = keys.iter().zip(msgs.iter()).map(|(k, m)| k.sign(m)).collect(); - bencher.counter(divan::counter::ItemsCount::new(n)).bench(|| { - for i in 0..n { - let _ = sigs[i].verify(&msgs[i], &pks[i]); - } - }); -} - -/// Aggregate verify over a shared message with N signers. -#[divan::bench(args = [10, 100, 1000])] -fn verify_aggregated_block(bencher: divan::Bencher, n: usize) { - let keys: Vec<_> = (0..n) - .map(|i| SecretKey::generate(&test_ikm(i as u8)).unwrap()) - .collect(); - let msg = test_msg(42); - let pks: Vec<_> = keys.iter().map(|k| k.public_key()).collect(); - let sigs: Vec<_> = keys.iter().map(|k| k.sign(&msg)).collect(); - let sig_refs: Vec<&Signature> = sigs.iter().collect(); - let agg_sig = Signature::aggregate(&sig_refs).unwrap(); - let pk_refs: Vec<_> = pks.iter().collect(); - bencher - .counter(divan::counter::ItemsCount::new(n)) - .bench(|| agg_sig.fast_verify_aggregates(&msg, &pk_refs)); -} - -/// Public key serialization (legacy format). -#[divan::bench] -fn ser_pk(bencher: divan::Bencher) { - let pk = SecretKey::generate(&test_ikm(1)).unwrap().public_key(); - bencher.bench(|| pk.to_bytes()); -} - -/// Public key deserialization (legacy format). -#[divan::bench] -fn deser_pk(bencher: divan::Bencher) { - let bytes = SecretKey::generate(&test_ikm(1)).unwrap().public_key().to_bytes(); - bencher.bench(|| PublicKey::from_bytes(&bytes)); -} - -/// Signature serialization (legacy format). -#[divan::bench] -fn ser_sig(bencher: divan::Bencher) { - let sig = SecretKey::generate(&test_ikm(1)).unwrap().sign(&test_msg(0)); - bencher.bench(|| sig.to_bytes()); -} - -/// Signature deserialization (legacy format). -#[divan::bench] -fn deser_sig(bencher: divan::Bencher) { - let bytes = SecretKey::generate(&test_ikm(1)).unwrap().sign(&test_msg(0)).to_bytes(); - bencher.bench(|| Signature::from_bytes(&bytes)); -} - -/// Threshold secret key splitting at various quorum sizes. -#[divan::bench(args = [5, 10, 50])] -fn split_threshold(bencher: divan::Bencher, n: usize) { - let sk = SecretKey::generate(&test_ikm(1)).unwrap(); - let t = n.div_ceil(2); - let ids = sequential_ids(n); - bencher - .counter(divan::counter::ItemsCount::new(n)) - .bench(|| sk.split(t, &ids, &mut rand_core::OsRng)); -} - -/// Threshold signature recovery via Lagrange interpolation. -#[divan::bench(args = [3, 5, 10])] -fn recover_threshold(bencher: divan::Bencher, t: usize) { - let sk = SecretKey::generate(&test_ikm(1)).unwrap(); - let n = t * 2; - let ids = sequential_ids(n); - let shares = sk.split(t, &ids, &mut rand_core::OsRng).unwrap(); - let msg = test_msg(42); - let sig_shares: Vec<_> = shares.iter().map(|s| s.sign(&msg)).collect(); - let subset: Vec<&dash_pkc::bls::BlsSigShare> = sig_shares.iter().take(t).collect(); - bencher - .counter(divan::counter::ItemsCount::new(t)) - .bench(|| Signature::recover(&subset)); -} - -#[cfg(feature = "std")] -mod worker_benches { - use super::{PublicKey, SecretKey, Signature}; - - use dash_pkc::worker; - - fn setup_sigs(n: usize) -> Vec<(Signature, PublicKey, [u8; 32])> { - (0..n) - .map(|i| { - let sk = SecretKey::generate(&super::test_ikm(i as u8)).unwrap(); - let msg = super::test_msg(i as u8); - let pk = sk.public_key(); - let sig = sk.sign(&msg); - (sig, pk, msg) - }) - .collect() - } - - #[divan::bench(args = [100, 1000])] - fn worker_verify_n(bencher: divan::Bencher, n: usize) { - let tuples = setup_sigs(n); - bencher - .counter(divan::counter::ItemsCount::new(n)) - .bench(|| worker::par_verify(&tuples, |(sig, pk, msg)| sig.verify(msg, pk).is_ok())); - } - - #[divan::bench(args = [100, 1000])] - fn worker_aggregate_pk_n(bencher: divan::Bencher, n: usize) { - let pks: Vec = (0..n) - .map(|i| SecretKey::generate(&super::test_ikm(i as u8)).unwrap().public_key()) - .collect(); - bencher - .counter(divan::counter::ItemsCount::new(n)) - .bench(|| worker::par_reduce(pks.clone(), |a, b| PublicKey::aggregate(&[&a, &b]).unwrap())); - } -} diff --git a/pkgs/pkc/bench/bls_ietf.rs b/pkgs/pkc/bench/bls_ietf.rs deleted file mode 100644 index c17a5caf..00000000 --- a/pkgs/pkc/bench/bls_ietf.rs +++ /dev/null @@ -1,223 +0,0 @@ -// -// Copyright (c) 2026-present, The Dash Core developers -// SPDX-License-Identifier: MIT -// See the accompanying file LICENSE or https://opensource.org/license/MIT -// - -//! Benchmarks for the IETF BLS module. - -use common::*; -use dash_pkc::bls::tests as common; -use dash_pkc::bls::{BlsPublicKey, BlsScIetf, BlsSecretKey, BlsSignature}; - -type PublicKey = BlsPublicKey; -type SecretKey = BlsSecretKey; -type Signature = BlsSignature; - -/// Single signature creation. -#[divan::bench] -fn sign(bencher: divan::Bencher) { - let sk = SecretKey::generate(&test_ikm(1)).unwrap(); - bencher - .counter(divan::counter::ItemsCount::new(1u32)) - .bench(|| sk.sign(&test_msg(42))); -} - -/// Single signature verification. -#[divan::bench] -fn verify(bencher: divan::Bencher) { - let sk = SecretKey::generate(&test_ikm(2)).unwrap(); - let msg = test_msg(99); - let sig = sk.sign(&msg); - let pk = sk.public_key(); - bencher - .counter(divan::counter::ItemsCount::new(1u32)) - .bench(|| sig.verify(&msg, &pk)); -} - -/// Public key aggregation at various quorum sizes. -#[divan::bench(args = [2, 5, 25, 50, 100])] -fn aggregate_pk_n(bencher: divan::Bencher, n: usize) { - let pks: Vec<_> = (0..n) - .map(|i| SecretKey::generate(&test_ikm(i as u8)).unwrap().public_key()) - .collect(); - let pk_refs: Vec<_> = pks.iter().collect(); - bencher - .counter(divan::counter::ItemsCount::new(n)) - .bench(|| PublicKey::aggregate(&pk_refs)); -} - -/// Signature aggregation at various batch sizes. -#[divan::bench(args = [2, 10, 100])] -fn aggregate_sig_n(bencher: divan::Bencher, n: usize) { - let keys: Vec<_> = (0..n) - .map(|i| SecretKey::generate(&test_ikm(i as u8)).unwrap()) - .collect(); - let sigs: Vec<_> = keys - .iter() - .enumerate() - .map(|(i, k)| k.sign(&test_msg(i as u8))) - .collect(); - let sig_refs: Vec<&Signature> = sigs.iter().collect(); - bencher - .counter(divan::counter::ItemsCount::new(n)) - .bench(|| Signature::aggregate(&sig_refs)); -} - -/// N individual verifications in a loop. -#[divan::bench(args = [100, 1000])] -fn verify_n_individual(bencher: divan::Bencher, n: usize) { - let keys: Vec<_> = (0..n) - .map(|i| SecretKey::generate(&test_ikm(i as u8)).unwrap()) - .collect(); - let msgs: Vec<[u8; 32]> = (0..n).map(|i| test_msg(i as u8)).collect(); - let pks: Vec<_> = keys.iter().map(|k| k.public_key()).collect(); - let sigs: Vec<_> = keys.iter().zip(msgs.iter()).map(|(k, m)| k.sign(m)).collect(); - bencher.counter(divan::counter::ItemsCount::new(n)).bench(|| { - for i in 0..n { - let _ = sigs[i].verify(&msgs[i], &pks[i]); - } - }); -} - -/// Aggregate N signatures over distinct messages, then verify. -#[divan::bench(args = [10, 100, 1000])] -fn verify_aggregated_block(bencher: divan::Bencher, n: usize) { - let keys: Vec<_> = (0..n) - .map(|i| SecretKey::generate(&test_ikm(i as u8)).unwrap()) - .collect(); - let msgs: Vec<[u8; 32]> = (0..n).map(|i| test_msg(i as u8)).collect(); - let pks: Vec<_> = keys.iter().map(|k| k.public_key()).collect(); - let sigs: Vec<_> = keys.iter().zip(msgs.iter()).map(|(k, m)| k.sign(m)).collect(); - let sig_refs: Vec<&Signature> = sigs.iter().collect(); - let agg_sig = Signature::aggregate(&sig_refs).unwrap(); - let pk_refs: Vec<_> = pks.iter().collect(); - let msg_slices: Vec<&[u8]> = msgs.iter().map(|m| m.as_slice()).collect(); - bencher - .counter(divan::counter::ItemsCount::new(n)) - .bench(|| agg_sig.verify_aggregates(&msg_slices, &pk_refs)); -} - -/// Fast aggregate verify (same message, N signers). -#[divan::bench(args = [10, 100, 1000])] -fn fast_verify_n(bencher: divan::Bencher, n: usize) { - let keys: Vec<_> = (0..n) - .map(|i| SecretKey::generate(&test_ikm(i as u8)).unwrap()) - .collect(); - let msg = test_msg(42); - let pks: Vec<_> = keys.iter().map(|k| k.public_key()).collect(); - let sigs: Vec<_> = keys.iter().map(|k| k.sign(&msg)).collect(); - let sig_refs: Vec<&Signature> = sigs.iter().collect(); - let agg_sig = Signature::aggregate(&sig_refs).unwrap(); - let pk_refs: Vec<_> = pks.iter().collect(); - bencher - .counter(divan::counter::ItemsCount::new(n)) - .bench(|| agg_sig.fast_verify_aggregates(&msg, &pk_refs)); -} - -/// Public key serialization (compress). -#[divan::bench] -fn ser_pk(bencher: divan::Bencher) { - let pk = SecretKey::generate(&test_ikm(1)).unwrap().public_key(); - bencher.bench(|| pk.to_bytes()); -} - -/// Public key deserialization (decompress + validate). -#[divan::bench] -fn deser_pk(bencher: divan::Bencher) { - let bytes = SecretKey::generate(&test_ikm(1)).unwrap().public_key().to_bytes(); - bencher.bench(|| PublicKey::from_bytes(&bytes)); -} - -/// Signature serialization (compress). -#[divan::bench] -fn ser_sig(bencher: divan::Bencher) { - let sig = SecretKey::generate(&test_ikm(1)).unwrap().sign(&test_msg(0)); - bencher.bench(|| sig.to_bytes()); -} - -/// Signature deserialization (decompress + validate). -#[divan::bench] -fn deser_sig(bencher: divan::Bencher) { - let bytes = SecretKey::generate(&test_ikm(1)).unwrap().sign(&test_msg(0)).to_bytes(); - bencher.bench(|| Signature::from_bytes(&bytes)); -} - -/// Threshold secret key splitting at various quorum sizes. -#[divan::bench(args = [5, 10, 50])] -fn split_threshold(bencher: divan::Bencher, n: usize) { - let sk = SecretKey::generate(&test_ikm(1)).unwrap(); - let t = n.div_ceil(2); - let ids = sequential_ids(n); - bencher - .counter(divan::counter::ItemsCount::new(n)) - .bench(|| sk.split(t, &ids, &mut rand_core::OsRng)); -} - -/// Threshold signature recovery via Lagrange interpolation. -#[divan::bench(args = [3, 5, 10])] -fn recover_threshold(bencher: divan::Bencher, t: usize) { - let sk = SecretKey::generate(&test_ikm(1)).unwrap(); - let n = t * 2; - let ids = sequential_ids(n); - let shares = sk.split(t, &ids, &mut rand_core::OsRng).unwrap(); - let msg = test_msg(42); - let sig_shares: Vec<_> = shares.iter().map(|s| s.sign(&msg)).collect(); - let subset: Vec<&dash_pkc::bls::BlsSigShare> = sig_shares.iter().take(t).collect(); - bencher - .counter(divan::counter::ItemsCount::new(t)) - .bench(|| Signature::recover(&subset)); -} - -/// Proof of possession creation. -#[divan::bench] -fn prove_pop(bencher: divan::Bencher) { - let sk = SecretKey::generate(&test_ikm(1)).unwrap(); - bencher.bench(|| sk.prove_possession()); -} - -/// Proof of possession verification. -#[divan::bench] -fn verify_pop(bencher: divan::Bencher) { - let sk = SecretKey::generate(&test_ikm(1)).unwrap(); - let pop = sk.prove_possession(); - let pk = sk.public_key(); - bencher.bench(|| pk.verify_possession(&pop)); -} - -#[cfg(feature = "std")] -mod worker_benches { - use super::{PublicKey, SecretKey, Signature}; - - use dash_pkc::worker; - - fn setup_sigs(n: usize) -> Vec<(Signature, PublicKey, Vec)> { - (0..n) - .map(|i| { - let sk = SecretKey::generate(&super::test_ikm(i as u8)).unwrap(); - let msg = super::test_msg(i as u8); - let pk = sk.public_key(); - let sig = sk.sign(&msg); - (sig, pk, msg.to_vec()) - }) - .collect() - } - - #[divan::bench(args = [100, 1000])] - fn worker_verify_n(bencher: divan::Bencher, n: usize) { - let tuples = setup_sigs(n); - bencher - .counter(divan::counter::ItemsCount::new(n)) - .bench(|| worker::par_verify(&tuples, |(sig, pk, msg)| sig.verify(msg, pk).is_ok())); - } - - #[divan::bench(args = [100, 1000])] - fn worker_aggregate_pk_n(bencher: divan::Bencher, n: usize) { - let pks: Vec = (0..n) - .map(|i| SecretKey::generate(&super::test_ikm(i as u8)).unwrap().public_key()) - .collect(); - bencher - .counter(divan::counter::ItemsCount::new(n)) - .bench(|| worker::par_reduce(pks.clone(), |a, b| PublicKey::aggregate(&[&a, &b]).unwrap())); - } -} diff --git a/pkgs/pkc/bench/main.rs b/pkgs/pkc/bench/main.rs index f77636c0..da06c8b9 100644 --- a/pkgs/pkc/bench/main.rs +++ b/pkgs/pkc/bench/main.rs @@ -10,9 +10,7 @@ )] #[cfg(feature = "bls")] -mod bls_chia; -#[cfg(feature = "bls")] -mod bls_ietf; +mod bls; #[cfg(feature = "ecdsa")] mod ecdsa; diff --git a/pkgs/pkc/src/bls/scheme_ops.rs b/pkgs/pkc/src/bls/scheme_ops.rs index b75d324d..10bcfbe2 100644 --- a/pkgs/pkc/src/bls/scheme_ops.rs +++ b/pkgs/pkc/src/bls/scheme_ops.rs @@ -36,11 +36,11 @@ pub(crate) fn verify_ok(result: BLST_ERROR) -> Result<(), BlsError> { /// BLS operations tied to a specific scheme. pub trait BlsScheme: BlsSchemeId { /// Inner secret key representation. - type InnerSk: Clone; + type InnerSk: Clone + Send + Sync; /// Inner public key representation. - type InnerPk: Clone + Debug + PartialEq + Eq; + type InnerPk: Clone + Debug + PartialEq + Eq + Send + Sync; /// Inner signature representation. - type InnerSig: Clone + Debug + PartialEq + Eq; + type InnerSig: Clone + Debug + PartialEq + Eq + Send + Sync; /// Message type accepted by signing and verification. type Msg: ?Sized; From c5357d157a495d77a34df283b760fe61abc64993 Mon Sep 17 00:00:00 2001 From: Kittywhiskers Van Gogh <63189531+kwvg@users.noreply.github.com> Date: Wed, 5 Aug 2026 00:16:11 +0530 Subject: [PATCH 14/14] pkc%refac(tests): gate unit tests under `cfg(test)` --- pkgs/pkc/src/bls/chia_h2c.rs | 2 +- pkgs/pkc/src/bls/mod.rs | 2 +- pkgs/pkc/src/bls/public_ops.rs | 2 +- pkgs/pkc/src/bls/scheme_chia.rs | 2 +- pkgs/pkc/src/bls/scheme_ietf.rs | 2 +- pkgs/pkc/src/bls/secret_ops.rs | 2 +- pkgs/pkc/src/bls/share_ops.rs | 2 +- pkgs/pkc/src/bls/sig_aggregate.rs | 2 +- pkgs/pkc/src/bls/sig_basic.rs | 2 +- pkgs/pkc/src/bls/sig_pop.rs | 2 +- pkgs/pkc/src/bls/sig_threshold.rs | 2 +- pkgs/pkc/src/bls/tests.rs | 7 ------- 12 files changed, 11 insertions(+), 18 deletions(-) diff --git a/pkgs/pkc/src/bls/chia_h2c.rs b/pkgs/pkc/src/bls/chia_h2c.rs index 6bf9b5e5..2ecbcd80 100644 --- a/pkgs/pkc/src/bls/chia_h2c.rs +++ b/pkgs/pkc/src/bls/chia_h2c.rs @@ -251,7 +251,7 @@ fn curve_rhs(x: &Fp2) -> Fp2 { x3 + b } -#[cfg(all(test, feature = "tests"))] +#[cfg(test)] mod tests { use super::*; use crate::prelude::*; diff --git a/pkgs/pkc/src/bls/mod.rs b/pkgs/pkc/src/bls/mod.rs index 0bf532c4..06b40de2 100644 --- a/pkgs/pkc/src/bls/mod.rs +++ b/pkgs/pkc/src/bls/mod.rs @@ -36,7 +36,7 @@ cfg_if::cfg_if! { pub(crate) mod chia_h2c; pub(crate) mod scheme_ops; - #[cfg(feature = "tests")] + #[cfg(any(test, feature = "tests"))] #[doc(hidden)] #[expect(clippy::unwrap_used, reason = "test support code")] pub mod tests; diff --git a/pkgs/pkc/src/bls/public_ops.rs b/pkgs/pkc/src/bls/public_ops.rs index eb3ec052..31ace6af 100644 --- a/pkgs/pkc/src/bls/public_ops.rs +++ b/pkgs/pkc/src/bls/public_ops.rs @@ -98,7 +98,7 @@ type_cvrt!(for[S: BlsScheme] TryFrom> for BlsPublicKey, BlsErro Self::from_bytes(bytes.as_bytes()) }); -#[cfg(all(test, feature = "tests"))] +#[cfg(test)] #[expect(clippy::unwrap_used, reason = "test code")] mod tests { use super::*; diff --git a/pkgs/pkc/src/bls/scheme_chia.rs b/pkgs/pkc/src/bls/scheme_chia.rs index fce56512..ff1f2b50 100644 --- a/pkgs/pkc/src/bls/scheme_chia.rs +++ b/pkgs/pkc/src/bls/scheme_chia.rs @@ -262,7 +262,7 @@ impl BlsScheme for BlsScChia { } } -#[cfg(all(test, feature = "tests"))] +#[cfg(test)] #[expect(clippy::unwrap_used, reason = "test code")] mod tests { use super::*; diff --git a/pkgs/pkc/src/bls/scheme_ietf.rs b/pkgs/pkc/src/bls/scheme_ietf.rs index ab6223ae..b42764e8 100644 --- a/pkgs/pkc/src/bls/scheme_ietf.rs +++ b/pkgs/pkc/src/bls/scheme_ietf.rs @@ -211,7 +211,7 @@ impl BlsScIetf { } } -#[cfg(all(test, feature = "tests"))] +#[cfg(test)] #[expect(clippy::unwrap_used, reason = "test code")] mod tests { use super::*; diff --git a/pkgs/pkc/src/bls/secret_ops.rs b/pkgs/pkc/src/bls/secret_ops.rs index efc77e20..af0aa0e0 100644 --- a/pkgs/pkc/src/bls/secret_ops.rs +++ b/pkgs/pkc/src/bls/secret_ops.rs @@ -145,7 +145,7 @@ type_cvrt!(for[S: BlsScheme] TryFrom> for BlsSecretKey, BlsErro Self::from_bytes(bytes.as_bytes()) }); -#[cfg(all(test, feature = "tests"))] +#[cfg(test)] #[expect(clippy::unwrap_used, reason = "test code")] mod tests { use super::*; diff --git a/pkgs/pkc/src/bls/share_ops.rs b/pkgs/pkc/src/bls/share_ops.rs index 81c4d977..61dec5af 100644 --- a/pkgs/pkc/src/bls/share_ops.rs +++ b/pkgs/pkc/src/bls/share_ops.rs @@ -162,7 +162,7 @@ impl BlsPublicKey { } } -#[cfg(all(test, feature = "tests"))] +#[cfg(test)] #[expect(clippy::unwrap_used, reason = "test code")] mod tests { use super::*; diff --git a/pkgs/pkc/src/bls/sig_aggregate.rs b/pkgs/pkc/src/bls/sig_aggregate.rs index a3e60c12..fa4e8545 100644 --- a/pkgs/pkc/src/bls/sig_aggregate.rs +++ b/pkgs/pkc/src/bls/sig_aggregate.rs @@ -72,7 +72,7 @@ impl BlsSignature { } } -#[cfg(all(test, feature = "tests"))] +#[cfg(test)] #[expect(clippy::unwrap_used, reason = "test code")] mod tests { use super::*; diff --git a/pkgs/pkc/src/bls/sig_basic.rs b/pkgs/pkc/src/bls/sig_basic.rs index f4cc8262..0f73983b 100644 --- a/pkgs/pkc/src/bls/sig_basic.rs +++ b/pkgs/pkc/src/bls/sig_basic.rs @@ -106,7 +106,7 @@ type_cvrt!(for[S: BlsScheme] TryFrom> for BlsSignature, BlsErr Self::from_bytes(bytes.as_bytes()) }); -#[cfg(all(test, feature = "tests"))] +#[cfg(test)] #[expect(clippy::unwrap_used, reason = "test code")] mod tests { use super::*; diff --git a/pkgs/pkc/src/bls/sig_pop.rs b/pkgs/pkc/src/bls/sig_pop.rs index 8e075b45..c6a2194b 100644 --- a/pkgs/pkc/src/bls/sig_pop.rs +++ b/pkgs/pkc/src/bls/sig_pop.rs @@ -36,7 +36,7 @@ impl BlsPublicKey { } } -#[cfg(all(test, feature = "tests"))] +#[cfg(test)] #[expect(clippy::unwrap_used, reason = "test code")] mod tests { use super::*; diff --git a/pkgs/pkc/src/bls/sig_threshold.rs b/pkgs/pkc/src/bls/sig_threshold.rs index 3777dc2b..85c2b20b 100644 --- a/pkgs/pkc/src/bls/sig_threshold.rs +++ b/pkgs/pkc/src/bls/sig_threshold.rs @@ -31,7 +31,7 @@ impl BlsSignature { } } -#[cfg(all(test, feature = "tests"))] +#[cfg(test)] #[expect(clippy::unwrap_used, reason = "test code")] mod tests { use crate::bls::scheme_ops::BlsScheme; diff --git a/pkgs/pkc/src/bls/tests.rs b/pkgs/pkc/src/bls/tests.rs index 793e2ba7..dd6acbe1 100644 --- a/pkgs/pkc/src/bls/tests.rs +++ b/pkgs/pkc/src/bls/tests.rs @@ -9,7 +9,6 @@ use crate::prelude::*; use hex_conservative::hex; -use rstest::*; /// IKM producing the first deterministic test key. pub const SEED_0: [u8; 32] = [0u8; 32]; @@ -43,12 +42,6 @@ pub fn sequential_ids(n: usize) -> Vec { (1..=n).map(|i| make_id(i as u32)).collect() } -/// Shared 32-byte test message fixture. -#[fixture] -pub fn msg32() -> [u8; 32] { - MSG_DEADBEEF -} - /// Build a distinct 32-byte IKM from an index, for multi-signer tests. pub fn test_ikm(i: u8) -> [u8; 32] { let mut ikm = [0u8; 32];