From 18e4f0610eb655a0c653a46d1fde8b5fddd74f68 Mon Sep 17 00:00:00 2001 From: Kisslove Dewangan Date: Mon, 5 Oct 2026 09:32:07 +0530 Subject: [PATCH 1/2] feat(wasm-mps): add RedPallas rerand protocol, bump mps to pre.13 Ticket: WCI-1725 --- packages/wasm-mps/Cargo.lock | 4 +- packages/wasm-mps/Cargo.toml | 2 +- packages/wasm-mps/src/lib.rs | 470 ++++++++++++++++++++++++++++++----- 3 files changed, 412 insertions(+), 64 deletions(-) diff --git a/packages/wasm-mps/Cargo.lock b/packages/wasm-mps/Cargo.lock index e496014c19b..d280dfafa5c 100644 --- a/packages/wasm-mps/Cargo.lock +++ b/packages/wasm-mps/Cargo.lock @@ -690,9 +690,9 @@ checksum = "3d97bbf43eb4f088f8ca469930cde17fa036207c9a5e02ccc5107c4e8b17c964" [[package]] name = "multi-party-schnorr" -version = "1.3.0-pre.11" +version = "1.3.0-pre.13" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5856d2fea7c520b5d88ffa949a2d3153ae6c03ddf6e095c5272647cf5bc90507" +checksum = "3d5f327e3a5278e33de35d5cde7f371c2d569ef2d4429df8c58ffee59b7b192d" dependencies = [ "blake2b_simd", "bytemuck", diff --git a/packages/wasm-mps/Cargo.toml b/packages/wasm-mps/Cargo.toml index aca01144a33..f4e72813253 100644 --- a/packages/wasm-mps/Cargo.toml +++ b/packages/wasm-mps/Cargo.toml @@ -17,7 +17,7 @@ bincode = { package = "bincode-next", version = "3.1.1", features = ["serde"] } crypto_box = "0.9" getrandom = { version = "0.2", features = ["js"] } js-sys = "0.3" -multi-party-schnorr = { version = "=1.3.0-pre.11", features = ["serde", "eddsa", "redpallas", "vrf"] } +multi-party-schnorr = { version = "=1.3.0-pre.13", features = ["serde", "eddsa", "redpallas", "vrf"] } sl-mpc-derive = "=0.1.1" sl-mpc-vrf = "=0.1.0-pre.5" orchard = { version = "0.13", default-features = false } diff --git a/packages/wasm-mps/src/lib.rs b/packages/wasm-mps/src/lib.rs index 4cf33c55ecd..2a1da9c372f 100644 --- a/packages/wasm-mps/src/lib.rs +++ b/packages/wasm-mps/src/lib.rs @@ -14,8 +14,8 @@ mod mps { derive::{HardDeriveOutputEd25519, HardDerivePartyEd25519, MpcDeriveInitEd25519}, group::{Group, GroupEncoding}, keygen::{ - KeyRefreshData, KeygenMsg1, KeygenMsg2, KeygenParty, Keyshare, R0 as DkgR0, - R1 as DkgR1, R2 as DkgR2, + KeyRefreshData, KeygenMsg1, KeygenMsg2, KeygenParty, Keyshare, RerandMsg1, RerandMsg2, + RerandParty, RerandR1, RerandR2, R0 as DkgR0, R1 as DkgR1, R2 as DkgR2, }, sign::{ messages::{SignMsg1, SignMsg2, SignMsg3}, @@ -264,7 +264,20 @@ mod mps { { pub msg: SignMsg3, pub party: PartialSign, - pub alpha: [u8; 32], + } + + /// Internal RedPallas rerand state used for round 1. + #[derive(Serialize, Deserialize)] + struct RedPallasRerandStateR1 { + pub msg: RerandMsg1, + pub party: RerandParty, + } + + /// Internal RedPallas rerand state used for round 2. + #[derive(Serialize, Deserialize)] + struct RedPallasRerandStateR2 { + pub msg: RerandMsg2, + pub party: RerandParty, } /// Result from processing that includes a public messages for other @@ -287,6 +300,15 @@ mod mps { pub pk: [u8; 32], } + /// Rerandomized keyshare returned from round 2 of RedPallas rerand. + /// The share plugs into `redpallas_dsg_round0_process`; the signature then + /// verifies against `pk`, and `alpha` is exposed for Orchard proof binding. + pub struct RedPallasRerandShare { + pub share: Vec, + pub pk: [u8; 32], + pub alpha: [u8; 32], + } + /// Result from processing that includes a per-recipient message pool and a private state to be stored in memory. pub struct MsgStateMap { pub msg: HashMap>, @@ -302,28 +324,6 @@ mod mps { pub struct RedPallasSignature { pub signature: Vec, pub rk: [u8; 32], - pub alpha: [u8; 32], - } - - trait IntoSignReady { - fn into_sign_ready(self) -> Result<(SignReady, [u8; 32]), MpsError>; - } - - impl IntoSignReady for SignReady { - fn into_sign_ready(self) -> Result<(SignReady, [u8; 32]), MpsError> { - Ok((self, [0u8; 32])) - } - } - - impl IntoSignReady for (SignReady, T) { - fn into_sign_ready(self) -> Result<(SignReady, [u8; 32]), MpsError> { - let alpha: [u8; 32] = - bincode::serde::encode_to_vec(&self.1, bincode::config::standard()) - .map_err(|_| MpsError::SerializationError)? - .try_into() - .map_err(|_| MpsError::SerializationError)?; - Ok((self.0, alpha)) - } } fn rem_prefix(prefix: &str, data: &[u8]) -> Result, MpsError> { @@ -580,9 +580,9 @@ mod mps { ) -> Result where G: GroupElem, - G::Scalar: ScalarReduce<[u8; 32]> + Serializable, - SignerParty, G>: Round>, Error = SignError>, - , G> as Round>::Output: IntoSignReady, + G::Scalar: Serializable, + SignerParty, G>: + Round>, Output = SignReady, Error = SignError>, SignReady: Round, SignMsg3), Error = SignError>, { let i0_msg2: SignMsg2 = @@ -594,18 +594,16 @@ mod mps { .map(|(v, _)| v) .map_err(|_| MpsError::DeserializationError)?; let msgs = vec![i0_msg2, state.msg]; - let (ready_signer, alpha) = state + let ready_signer = state .party .process(msgs) - .map_err(|_| MpsError::ProtocolError)? - .into_sign_ready()?; + .map_err(|_| MpsError::ProtocolError)?; let (p3, msg3) = ready_signer .process(()) .map_err(|_| MpsError::ProtocolError)?; let new_state = DsgStateR3 { msg: msg3.clone(), party: p3, - alpha, }; Ok(MsgState { msg: bincode::serde::encode_to_vec(&msg3, bincode::config::standard()) @@ -618,10 +616,10 @@ mod mps { fn internal_dsg_round3_process( round3_message: &[u8], state: &[u8], - ) -> Result<(Vec, G, [u8; 32]), MpsError> + ) -> Result<(Vec, G), MpsError> where G: GroupElem, - G::Scalar: ScalarReduce<[u8; 32]> + Serializable, + G::Scalar: Serializable, PartialSign: Round>, Output = (S, SC), Error = SignError>, S: Into<[u8; 64]>, { @@ -634,13 +632,109 @@ mod mps { .map(|(v, _)| v) .map_err(|_| MpsError::DeserializationError)?; let public_key = state.party.public_key; - let alpha = state.alpha; let (sig, _) = state .party .process(vec![i0_msg3, state.msg]) .map_err(|_| MpsError::ProtocolError)?; let sig_bytes: [u8; 64] = sig.into(); - Ok((sig_bytes.to_vec(), public_key, alpha)) + Ok((sig_bytes.to_vec(), public_key)) + } + + fn internal_rerand_round0_process(share: &[u8]) -> Result { + // Parse share + let keyshare: Keyshare = + bincode::serde::decode_from_slice(share, bincode::config::standard()) + .map(|(v, _)| v) + .map_err(|_| MpsError::DeserializationError)?; + + // Sample entropy and broadcast the commitment + let (p1, msg1) = RerandParty::new(keyshare, &mut rand::thread_rng()) + .process(()) + .map_err(|_| MpsError::ProtocolError)?; + + // Create the state for storage between rounds + let state = RedPallasRerandStateR1 { + msg: msg1.clone(), + party: p1, + }; + + Ok(MsgState { + msg: bincode::serde::encode_to_vec(msg1, bincode::config::standard()) + .map_err(|_| MpsError::SerializationError)?, + state: bincode::serde::encode_to_vec(&state, bincode::config::standard()) + .map_err(|_| MpsError::SerializationError)?, + }) + } + + fn internal_rerand_round1_process( + round1_message: &[u8], + state: &[u8], + ) -> Result { + // Parse state + let state: RedPallasRerandStateR1 = + bincode::serde::decode_from_slice(state, bincode::config::standard()) + .map(|(v, _)| v) + .map_err(|_| MpsError::DeserializationError)?; + + // Parse message + let i0_msg1: RerandMsg1 = + bincode::serde::decode_from_slice(round1_message, bincode::config::standard()) + .map(|(v, _)| v) + .map_err(|_| MpsError::DeserializationError)?; + + // The crate expects the full participant set, own commitment included + let msgs = vec![i0_msg1, state.msg]; + + // Validate the participant set, derive final_session_id, and open + let (p2, msg2) = state + .party + .process(msgs) + .map_err(|_| MpsError::ProtocolError)?; + + // Create the state for storage between rounds + let state = RedPallasRerandStateR2 { + msg: msg2.clone(), + party: p2, + }; + + Ok(MsgState { + msg: bincode::serde::encode_to_vec(&msg2, bincode::config::standard()) + .map_err(|_| MpsError::SerializationError)?, + state: bincode::serde::encode_to_vec(&state, bincode::config::standard()) + .map_err(|_| MpsError::SerializationError)?, + }) + } + + fn internal_rerand_round2_process( + round2_message: &[u8], + state: &[u8], + ) -> Result<(Keyshare, [u8; 32]), MpsError> { + // Parse state + let state: RedPallasRerandStateR2 = + bincode::serde::decode_from_slice(state, bincode::config::standard()) + .map(|(v, _)| v) + .map_err(|_| MpsError::DeserializationError)?; + + // Parse message + let i0_msg2: RerandMsg2 = + bincode::serde::decode_from_slice(round2_message, bincode::config::standard()) + .map(|(v, _)| v) + .map_err(|_| MpsError::DeserializationError)?; + + // Verify openings, aggregate the randomizers, and tweak the share + let msgs = vec![i0_msg2, state.msg]; + let (share, alpha) = state + .party + .process(msgs) + .map_err(|_| MpsError::ProtocolError)?; + + let alpha_bytes: [u8; 32] = + bincode::serde::encode_to_vec(alpha, bincode::config::standard()) + .map_err(|_| MpsError::SerializationError)? + .try_into() + .map_err(|_| MpsError::SerializationError)?; + + Ok((share, alpha_bytes)) } /// Process round 0 of DKG protocol for Ed25519. @@ -1213,8 +1307,7 @@ mod mps { let round3_message = rem_prefix("mps-ed25519-dsg-round3-message$", round3_message)?; let state = rem_prefix("mps-ed25519-dsg-round3-state$", state)?; - let (sig, _, _) = - internal_dsg_round3_process::(&round3_message, &state)?; + let (sig, _) = internal_dsg_round3_process::(&round3_message, &state)?; Ok(sig) } @@ -1325,19 +1418,64 @@ mod mps { } /// Process round 3 of RedPallas DSG; returns the 64-byte signature and the - /// per-session randomized verification key (rk) against which it verifies. + /// verification key (rk) against which it verifies. With rerandomization + /// moved into the separate rerand protocol, rk is the keyshare's own + /// public key — fresh per session when rerand ran before DSG. pub fn redpallas_dsg_round3_process( round3_message: &[u8], state: &[u8], ) -> Result { let round3_message = rem_prefix("mps-redpallas-dsg-round3-message$", round3_message)?; let state = rem_prefix("mps-redpallas-dsg-round3-state$", state)?; - let (signature, pk, alpha) = + let (signature, pk) = internal_dsg_round3_process::(&round3_message, &state)?; let rk = RedPallasPointBytes::from(pk).0; - Ok(RedPallasSignature { - signature, - rk, + Ok(RedPallasSignature { signature, rk }) + } + + /// Process round 0 of RedPallas rerand; broadcasts the randomizer + /// commitment. share: Signing share from RedPallas DKG (unrerandomized). + /// Run this before every redpallas_dsg_round0_process. + pub fn redpallas_rerand_round0_process(share: &[u8]) -> Result { + let result = internal_rerand_round0_process(share)?; + Ok(MsgState { + msg: add_prefix("mps-redpallas-rerand-round1-message$", &result.msg), + state: add_prefix("mps-redpallas-rerand-round1-state$", &result.state), + }) + } + + /// Process round 1 of RedPallas rerand; validates the participant set, + /// binds the session via the aggregate final_session_id, and opens the + /// round-1 commitment. + pub fn redpallas_rerand_round1_process( + round1_message: &[u8], + state: &[u8], + ) -> Result { + let round1_message = rem_prefix("mps-redpallas-rerand-round1-message$", round1_message)?; + let state = rem_prefix("mps-redpallas-rerand-round1-state$", state)?; + let result = internal_rerand_round1_process(&round1_message, &state)?; + Ok(MsgState { + msg: add_prefix("mps-redpallas-rerand-round2-message$", &result.msg), + state: add_prefix("mps-redpallas-rerand-round2-state$", &result.state), + }) + } + + /// Process round 2 of RedPallas rerand; verifies every opening against its + /// commitment and returns the rerandomized keyshare with the aggregate + /// alpha. The share feeds redpallas_dsg_round0_process. + pub fn redpallas_rerand_round2_process( + round2_message: &[u8], + state: &[u8], + ) -> Result { + let round2_message = rem_prefix("mps-redpallas-rerand-round2-message$", round2_message)?; + let state = rem_prefix("mps-redpallas-rerand-round2-state$", state)?; + let (share, alpha) = internal_rerand_round2_process(&round2_message, &state)?; + let pk = RedPallasPointBytes::from(share.public_key).0; + let share_bytes = bincode::serde::encode_to_vec(&share, bincode::config::standard()) + .map_err(|_| MpsError::SerializationError)?; + Ok(RedPallasRerandShare { + share: share_bytes, + pk, alpha, }) } @@ -1930,8 +2068,9 @@ mod tests { .unwrap(); } - /// Test full RedPallas DSG protocol; verifies alpha is non-zero, consistent - /// between parties, and that the signature verifies against rk. + /// Test full RedPallas rerand + DSG protocol: rerand tweaks the keyshare + /// before signing, DSG signs with the rerandomized share, and the + /// signature verifies against the rerandomized key (rk), not the DKG key. #[test] fn test_redpallas_dsg() { use orchard::primitives::redpallas::{Signature, SpendAuth, VerificationKey}; @@ -2020,11 +2159,33 @@ mod tests { let msg = b"Test message for RedPallas signing"; - // DSG round 0 - let dsg_p0_0 = - mps::redpallas_dsg_round0_process(dkg_p0_init.share.as_slice(), msg).unwrap(); - let dsg_p2_0 = - mps::redpallas_dsg_round0_process(dkg_p2_init.share.as_slice(), msg).unwrap(); + // Rerand rounds (commit, open, tweak) between the signing parties + let rr_p0_0 = mps::redpallas_rerand_round0_process(dkg_p0_init.share.as_slice()).unwrap(); + let rr_p2_0 = mps::redpallas_rerand_round0_process(dkg_p2_init.share.as_slice()).unwrap(); + + let rr_p0_1 = + mps::redpallas_rerand_round1_process(rr_p2_0.msg.as_slice(), rr_p0_0.state.as_slice()) + .unwrap(); + let rr_p2_1 = + mps::redpallas_rerand_round1_process(rr_p0_0.msg.as_slice(), rr_p2_0.state.as_slice()) + .unwrap(); + + let rr_p0 = + mps::redpallas_rerand_round2_process(rr_p2_1.msg.as_slice(), rr_p0_1.state.as_slice()) + .unwrap(); + let rr_p2 = + mps::redpallas_rerand_round2_process(rr_p0_1.msg.as_slice(), rr_p2_1.state.as_slice()) + .unwrap(); + + // Both parties derive the same rerandomized key and alpha + assert_eq!(rr_p0.pk, rr_p2.pk, "Rerandomized keys differ"); + assert_ne!(rr_p0.pk, dkg_p0_init.pk, "Rerand must tweak the public key"); + assert_eq!(rr_p0.alpha, rr_p2.alpha, "Alpha values differ"); + assert_ne!(rr_p0.alpha, [0u8; 32], "Alpha is zero"); + + // DSG round 0 with the rerandomized shares + let dsg_p0_0 = mps::redpallas_dsg_round0_process(rr_p0.share.as_slice(), msg).unwrap(); + let dsg_p2_0 = mps::redpallas_dsg_round0_process(rr_p2.share.as_slice(), msg).unwrap(); // DSG round 1 let dsg_p0_1 = @@ -2050,13 +2211,10 @@ mod tests { mps::redpallas_dsg_round3_process(dsg_p0_2.msg.as_slice(), dsg_p2_2.state.as_slice()) .unwrap(); - // Both parties produce identical outputs + // Both parties produce identical outputs; rk is the rerandomized key assert_eq!(dsg_p0.signature, dsg_p2.signature, "Signatures differ"); assert_eq!(dsg_p0.rk, dsg_p2.rk, "Randomized keys differ"); - assert_eq!(dsg_p0.alpha, dsg_p2.alpha, "Alpha values differ"); - - // Alpha is a random field element and must not be zero - assert_ne!(dsg_p0.alpha, [0u8; 32], "Alpha is zero"); + assert_eq!(dsg_p0.rk, rr_p0.pk, "DSG rk must be the rerandomized key"); // Signature verifies against rk, not the original public key let rk = VerificationKey::::try_from(dsg_p0.rk) @@ -2066,6 +2224,139 @@ mod tests { ); rk.verify(msg, &sig) .expect("signature must verify against rk"); + assert!( + !redpallas_verify(&dkg_p0_init.pk, &dsg_p0.signature, msg).unwrap(), + "signature must not verify under the original DKG public key" + ); + } + + /// Test RedPallas rerand tweak math: both parties derive the same alpha, + /// each share shifts by d_i' = d_i + alpha and the group public key by + /// pk' = pk + G*alpha. + #[test] + fn test_redpallas_rerand() { + use multi_party_schnorr::{ + common::redpallas::{RedPallasPoint, RedPallasPointBytes}, + common::traits::ScalarReduce, + group::Group, + }; + + use crate::mps::KeyshareCompat; + + let mut prv_keys = Vec::new(); + let mut pub_keys = Vec::new(); + let mut seeds = Vec::new(); + for i in 0..3 { + let secret_key = crypto_box::SecretKey::generate(&mut rand::thread_rng()); + let public_key = secret_key.public_key(); + prv_keys.push(secret_key); + pub_keys.push((i, public_key)); + let seed: [u8; 32] = rand::thread_rng().gen(); + seeds.push(seed); + } + + // DKG rounds 0-2 for all three parties; quorum is {0, 2} + let dkg_p0_0 = mps::redpallas_dkg_round0_process( + 0, + &prv_keys[0].to_bytes(), + &[ + pub_keys[1].1.to_bytes().to_vec(), + pub_keys[2].1.to_bytes().to_vec(), + ], + &seeds[0], + ) + .unwrap(); + let dkg_p1_0 = mps::redpallas_dkg_round0_process( + 1, + &prv_keys[1].to_bytes(), + &[ + pub_keys[0].1.to_bytes().to_vec(), + pub_keys[2].1.to_bytes().to_vec(), + ], + &seeds[1], + ) + .unwrap(); + let dkg_p2_0 = mps::redpallas_dkg_round0_process( + 2, + &prv_keys[2].to_bytes(), + &[ + pub_keys[0].1.to_bytes().to_vec(), + pub_keys[1].1.to_bytes().to_vec(), + ], + &seeds[2], + ) + .unwrap(); + + let dkg_p0_1 = mps::redpallas_dkg_round1_process( + &[dkg_p1_0.msg.clone(), dkg_p2_0.msg.clone()], + dkg_p0_0.state.as_slice(), + ) + .unwrap(); + let dkg_p1_1 = mps::redpallas_dkg_round1_process( + &[dkg_p0_0.msg.clone(), dkg_p2_0.msg.clone()], + dkg_p1_0.state.as_slice(), + ) + .unwrap(); + let dkg_p2_1 = mps::redpallas_dkg_round1_process( + &[dkg_p0_0.msg.clone(), dkg_p1_0.msg.clone()], + dkg_p2_0.state.as_slice(), + ) + .unwrap(); + + let dkg_p0_init = mps::redpallas_dkg_round2_process( + &[dkg_p1_1.msg.clone(), dkg_p2_1.msg.clone()], + dkg_p0_1.state.as_slice(), + ) + .unwrap(); + let dkg_p2_init = mps::redpallas_dkg_round2_process( + &[dkg_p0_1.msg.clone(), dkg_p1_1.msg.clone()], + dkg_p2_1.state.as_slice(), + ) + .unwrap(); + + // Rerand rounds between parties 0 and 2 + let rr_p0_0 = mps::redpallas_rerand_round0_process(dkg_p0_init.share.as_slice()).unwrap(); + let rr_p2_0 = mps::redpallas_rerand_round0_process(dkg_p2_init.share.as_slice()).unwrap(); + + let rr_p0_1 = + mps::redpallas_rerand_round1_process(rr_p2_0.msg.as_slice(), rr_p0_0.state.as_slice()) + .unwrap(); + let rr_p2_1 = + mps::redpallas_rerand_round1_process(rr_p0_0.msg.as_slice(), rr_p2_0.state.as_slice()) + .unwrap(); + + let rr_p0 = + mps::redpallas_rerand_round2_process(rr_p2_1.msg.as_slice(), rr_p0_1.state.as_slice()) + .unwrap(); + let rr_p2 = + mps::redpallas_rerand_round2_process(rr_p0_1.msg.as_slice(), rr_p2_1.state.as_slice()) + .unwrap(); + + // Same alpha and key from both parties, different from the DKG key + assert_eq!(rr_p0.alpha, rr_p2.alpha, "Alpha values differ"); + assert_ne!(rr_p0.alpha, [0u8; 32], "Alpha is zero"); + assert_eq!(rr_p0.pk, rr_p2.pk, "Rerandomized keys differ"); + assert_ne!(rr_p0.pk, dkg_p0_init.pk, "Rerand must tweak the public key"); + + // Share delta math: d_i' = d_i + alpha and pk' = pk + G*alpha + let old_p0: KeyshareCompat = bincode::serde::decode_from_slice( + dkg_p0_init.share.as_slice(), + bincode::config::standard(), + ) + .map(|(v, _)| v) + .unwrap(); + let new_p0: KeyshareCompat = + bincode::serde::decode_from_slice(rr_p0.share.as_slice(), bincode::config::standard()) + .map(|(v, _)| v) + .unwrap(); + let alpha = >::reduce_from_bytes(&rr_p0.alpha); + + assert_eq!(new_p0.d_i - old_p0.d_i, alpha, "Share delta must be alpha"); + assert_eq!( + RedPallasPointBytes::from(new_p0.public_key - old_p0.public_key).0, + RedPallasPointBytes::from(RedPallasPoint::generator() * alpha).0, + "Public key delta must be G*alpha" + ); } /// Orchard requires the spend-validating key `ak` to have a canonical @@ -2586,7 +2877,6 @@ pub fn redpallas_dsg_round2_process( pub struct RedPallasSignature { signature: Vec, rk: Vec, - alpha: Vec, } #[wasm_bindgen] @@ -2600,11 +2890,6 @@ impl RedPallasSignature { pub fn rk(&self) -> Vec { self.rk.clone() } - - #[wasm_bindgen(getter)] - pub fn alpha(&self) -> Vec { - self.alpha.clone() - } } #[wasm_bindgen] @@ -2617,6 +2902,69 @@ pub fn redpallas_dsg_round3_process( Ok(RedPallasSignature { signature: result.signature, rk: result.rk.to_vec(), + }) +} + +#[wasm_bindgen] +pub struct RedPallasRerandShare { + share: Vec, + pk: Vec, + alpha: Vec, +} + +#[wasm_bindgen] +impl RedPallasRerandShare { + #[wasm_bindgen(getter)] + pub fn share(&self) -> Vec { + self.share.clone() + } + + #[wasm_bindgen(getter)] + pub fn pk(&self) -> Vec { + self.pk.clone() + } + + #[wasm_bindgen(getter)] + pub fn alpha(&self) -> Vec { + self.alpha.clone() + } +} + +#[wasm_bindgen] +pub fn redpallas_rerand_round0_process(share: &[u8]) -> Result { + let result = mps::redpallas_rerand_round0_process(share).map_err(|e| e.to_string())?; + + Ok(MsgState { + msg: result.msg, + state: result.state, + }) +} + +#[wasm_bindgen] +pub fn redpallas_rerand_round1_process( + round1_message: &[u8], + state: &[u8], +) -> Result { + let result = + mps::redpallas_rerand_round1_process(round1_message, state).map_err(|e| e.to_string())?; + + Ok(MsgState { + msg: result.msg, + state: result.state, + }) +} + +#[wasm_bindgen] +pub fn redpallas_rerand_round2_process( + round2_message: &[u8], + state: &[u8], +) -> Result { + let result = + mps::redpallas_rerand_round2_process(round2_message, state).map_err(|e| e.to_string())?; + + Ok(RedPallasRerandShare { + share: result.share, + pk: result.pk.to_vec(), alpha: result.alpha.to_vec(), }) } From ce2c6340495c74d689128c6247995e7ac4024184 Mon Sep 17 00:00:00 2001 From: Kisslove Dewangan Date: Mon, 5 Oct 2026 09:34:08 +0530 Subject: [PATCH 2/2] test(wasm-mps): add RedPallas rerand protocol tests Ticket: WCI-1725 --- packages/wasm-mps/test/mps.ts | 205 ++++++++++++++++++++++++++++++++-- 1 file changed, 198 insertions(+), 7 deletions(-) diff --git a/packages/wasm-mps/test/mps.ts b/packages/wasm-mps/test/mps.ts index f6da4cd67e2..b6accaad2c3 100644 --- a/packages/wasm-mps/test/mps.ts +++ b/packages/wasm-mps/test/mps.ts @@ -1258,6 +1258,183 @@ describe("mps", function () { }); }); + describe("rerand", function () { + // Signing quorum {0, 2}; result arrays are indexed by position. + const quorum = [0, 2]; + const otherIndex = [1, 0]; + let shares: Array; + + before("performs dkg", function () { + const results1 = [0, 1, 2].map((i) => + mps.redpallas_dkg_round0_process( + i, + keypairs[i].privateKey, + otherIndices[i].map((j) => keypairs[j].publicKey), + crypto.randomBytes(32), + ), + ); + const results2 = [0, 1, 2].map((i) => + mps.redpallas_dkg_round1_process( + otherIndices[i].map((j) => results1[j].msg), + results1[i].state, + ), + ); + shares = [0, 1, 2].map((i) => + mps.redpallas_dkg_round2_process( + otherIndices[i].map((j) => results2[j].msg), + results2[i].state, + ), + ); + }); + + it("performs round 0", function () { + const messagePrefix = Buffer.from("mps-redpallas-rerand-round1-message$"); + const statePrefix = Buffer.from("mps-redpallas-rerand-round1-state$"); + for (const i of quorum) { + const result = mps.redpallas_rerand_round0_process(shares[i].share); + assert(Buffer.from(result.msg).slice(0, messagePrefix.length).equals(messagePrefix)); + assert(Buffer.from(result.state).slice(0, statePrefix.length).equals(statePrefix)); + } + }); + + let results1: Array; + + before("performs round 0", function () { + results1 = quorum.map((i) => mps.redpallas_rerand_round0_process(shares[i].share)); + }); + + it("performs round 1", function () { + const messagePrefix = Buffer.from("mps-redpallas-rerand-round2-message$"); + const statePrefix = Buffer.from("mps-redpallas-rerand-round2-state$"); + for (let i = 0; i < results1.length; i++) { + const result = mps.redpallas_rerand_round1_process( + results1[otherIndex[i]].msg, + results1[i].state, + ); + assert(Buffer.from(result.msg).slice(0, messagePrefix.length).equals(messagePrefix)); + assert(Buffer.from(result.state).slice(0, statePrefix.length).equals(statePrefix)); + } + }); + + it("fails to perform round 1 with invalid message prefix", function () { + const messagePrefix = Buffer.from("mps-redpallas-rerand-round1-message$"); + for (let i = 0; i < results1.length; i++) { + shouldThrow(() => + mps.redpallas_rerand_round1_process( + Buffer.from(results1[otherIndex[i]].msg).slice(messagePrefix.length), + results1[i].state, + ), + ); + shouldThrow(() => + mps.redpallas_rerand_round1_process( + Buffer.concat([ + Buffer.from("mps-redpallas-rerand-round2-message$"), + Buffer.from(results1[otherIndex[i]].msg).slice(messagePrefix.length), + ]), + results1[i].state, + ), + ); + } + }); + + it("fails to perform round 1 with invalid state prefix", function () { + const statePrefix = Buffer.from("mps-redpallas-rerand-round1-state$"); + for (let i = 0; i < results1.length; i++) { + shouldThrow(() => + mps.redpallas_rerand_round1_process( + results1[otherIndex[i]].msg, + Buffer.from(results1[i].state).slice(statePrefix.length), + ), + ); + shouldThrow(() => + mps.redpallas_rerand_round1_process( + results1[otherIndex[i]].msg, + Buffer.concat([ + Buffer.from("mps-redpallas-rerand-round2-state$"), + Buffer.from(results1[i].state).slice(statePrefix.length), + ]), + ), + ); + } + }); + + let results2: Array; + + before("performs round 1", function () { + results2 = [0, 1].map((i) => + mps.redpallas_rerand_round1_process(results1[otherIndex[i]].msg, results1[i].state), + ); + }); + + it("performs round 2", function () { + const rerandShares = [0, 1].map((i) => + mps.redpallas_rerand_round2_process(results2[otherIndex[i]].msg, results2[i].state), + ); + // Both parties agree on the rerandomized public key and alpha + assert.deepStrictEqual(rerandShares[0].pk, rerandShares[1].pk, "pk values differ"); + assert.deepStrictEqual(rerandShares[0].alpha, rerandShares[1].alpha, "alpha values differ"); + // The rerandomized key differs from the DKG key + assert.notDeepStrictEqual(rerandShares[0].pk, shares[0].pk, "pk was not rerandomized"); + // Alpha is a random field element — must not be zero + assert(!rerandShares[0].alpha.every((b) => b === 0), "alpha is zero"); + // The rerandomized share differs from the original share + assert.notDeepStrictEqual(rerandShares[0].share, shares[0].share, "share unchanged"); + }); + + it("produces a fresh alpha per session", function () { + const run = () => { + const r1 = quorum.map((i) => mps.redpallas_rerand_round0_process(shares[i].share)); + const r2 = [0, 1].map((i) => + mps.redpallas_rerand_round1_process(r1[otherIndex[i]].msg, r1[i].state), + ); + return mps.redpallas_rerand_round2_process(r2[1].msg, r2[0].state); + }; + assert.notDeepStrictEqual(run().alpha, run().alpha, "alpha reused across sessions"); + }); + + it("fails to perform round 2 with invalid message prefix", function () { + const messagePrefix = Buffer.from("mps-redpallas-rerand-round2-message$"); + for (let i = 0; i < results2.length; i++) { + shouldThrow(() => + mps.redpallas_rerand_round2_process( + Buffer.from(results2[otherIndex[i]].msg).slice(messagePrefix.length), + results2[i].state, + ), + ); + shouldThrow(() => + mps.redpallas_rerand_round2_process( + Buffer.concat([ + Buffer.from("mps-redpallas-rerand-round3-message$"), + Buffer.from(results2[otherIndex[i]].msg).slice(messagePrefix.length), + ]), + results2[i].state, + ), + ); + } + }); + + it("fails to perform round 2 with invalid state prefix", function () { + const statePrefix = Buffer.from("mps-redpallas-rerand-round2-state$"); + for (let i = 0; i < results2.length; i++) { + shouldThrow(() => + mps.redpallas_rerand_round2_process( + results2[otherIndex[i]].msg, + Buffer.from(results2[i].state).slice(statePrefix.length), + ), + ); + shouldThrow(() => + mps.redpallas_rerand_round2_process( + results2[otherIndex[i]].msg, + Buffer.concat([ + Buffer.from("mps-redpallas-rerand-round3-state$"), + Buffer.from(results2[i].state).slice(statePrefix.length), + ]), + ), + ); + } + }); + }); + describe("dsg", function () { const otherIndex = [1, 0]; let shares: Array; @@ -1285,6 +1462,18 @@ describe("mps", function () { ); }); + let rerandShares: Array; + + before("performs rerand", function () { + const r1 = [0, 2].map((i) => mps.redpallas_rerand_round0_process(shares[i].share)); + const r2 = [0, 1].map((i) => + mps.redpallas_rerand_round1_process(r1[otherIndex[i]].msg, r1[i].state), + ); + rerandShares = [0, 1].map((i) => + mps.redpallas_rerand_round2_process(r2[otherIndex[i]].msg, r2[i].state), + ); + }); + const message = Buffer.from( "The Times 03/Jan/2009 Chancellor on brink of second bailout for banks", ); @@ -1292,8 +1481,8 @@ describe("mps", function () { it("performs round 0", function () { const messagePrefix = Buffer.from("mps-redpallas-dsg-round1-message$"); const statePrefix = Buffer.from("mps-redpallas-dsg-round1-state$"); - for (const i of [0, 2]) { - const result = mps.redpallas_dsg_round0_process(shares[i].share, message); + for (let i = 0; i < rerandShares.length; i++) { + const result = mps.redpallas_dsg_round0_process(rerandShares[i].share, message); assert(Buffer.from(result.msg).slice(0, messagePrefix.length).equals(messagePrefix)); assert(Buffer.from(result.state).slice(0, statePrefix.length).equals(statePrefix)); } @@ -1302,7 +1491,9 @@ describe("mps", function () { let results1: Array; before("performs round 0", function () { - results1 = [0, 2].map((i) => mps.redpallas_dsg_round0_process(shares[i].share, message)); + results1 = [0, 1].map((i) => + mps.redpallas_dsg_round0_process(rerandShares[i].share, message), + ); }); it("performs round 1", function () { @@ -1432,11 +1623,11 @@ describe("mps", function () { for (let i = 0; i < 2; i++) { assert(mps.redpallas_verify(results4[i].rk, results4[i].signature, message)); } - // Both parties produce the same alpha and rk - assert.deepStrictEqual(results4[0].alpha, results4[1].alpha, "alpha values differ"); + // Both parties produce the same rk, equal to the rerandomized public key assert.deepStrictEqual(results4[0].rk, results4[1].rk, "rk values differ"); - // Alpha is a random field element — must not be zero - assert(!results4[0].alpha.every((b) => b === 0), "alpha is zero"); + assert.deepStrictEqual(results4[0].rk, rerandShares[0].pk, "rk is not the rerandomized pk"); + // The signature must not verify under the un-rerandomized DKG key + assert(!mps.redpallas_verify(shares[0].pk, results4[0].signature, message)); }); it("fails to perform round 3 with invalid message prefix", function () {