From 3789ea6f098b3bc8551bebbaef4a9ee956912450 Mon Sep 17 00:00:00 2001 From: John Driscoll Date: Mon, 24 Aug 2026 14:32:24 -0500 Subject: [PATCH] feat(wasm-mps): add vrf and hard derive protocols Ticket: HSM-569 --- packages/wasm-mps/Cargo.lock | 69 +++- packages/wasm-mps/Cargo.toml | 2 +- packages/wasm-mps/src/lib.rs | 586 ++++++++++++++++++++++++++++++++++ packages/wasm-mps/test/mps.ts | 368 +++++++++++++++++++++ 4 files changed, 1022 insertions(+), 3 deletions(-) diff --git a/packages/wasm-mps/Cargo.lock b/packages/wasm-mps/Cargo.lock index d5bb526f2fb..a5486bf712b 100644 --- a/packages/wasm-mps/Cargo.lock +++ b/packages/wasm-mps/Cargo.lock @@ -889,6 +889,40 @@ dependencies = [ "zeroize", ] +[[package]] +name = "multi-party-schnorr" +version = "1.3.0-pre.7" +source = "git+https://github.com/silence-laboratories/multi-party-schnorr?rev=09185d1e4e78d096f2ab3e64d877fb0c88099688#09185d1e4e78d096f2ab3e64d877fb0c88099688" +dependencies = [ + "blake2b_simd", + "bytemuck", + "ciborium", + "crypto-bigint", + "crypto_box", + "curve25519-dalek", + "derivation-path", + "ed25519-dalek", + "elliptic-curve", + "ff", + "group", + "hmac", + "k256", + "pasta_curves", + "rand", + "rand_chacha", + "rand_core", + "reddsa", + "serde", + "serde_bytes", + "sha2", + "signature", + "sl-mpc-derive", + "sl-mpc-mate", + "sl-mpc-vrf", + "thiserror 1.0.69", + "zeroize", +] + [[package]] name = "nonempty" version = "0.11.0" @@ -1338,6 +1372,28 @@ dependencies = [ "zeroize", ] +[[package]] +name = "sl-mpc-vrf" +version = "0.1.0-pre.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "78aa6ea9cad657a879d3cc8d11e1df100c5c53b764902ed7139fbae32425b72e" +dependencies = [ + "crypto-bigint", + "curve25519-dalek", + "elliptic-curve", + "ff", + "rand", + "rand_chacha", + "serde", + "serde_bytes", + "sha2", + "sl-mpc-derive", + "sl-mpc-mate", + "sl-transcript", + "thiserror 1.0.69", + "zeroize", +] + [[package]] name = "sl-secret-sharing" version = "0.1.0-pre.1" @@ -1346,6 +1402,15 @@ dependencies = [ "ff", ] +[[package]] +name = "sl-transcript" +version = "0.1.0-pre.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e33f0e5dbfa7ca28f40e1a16bb2763e468d10e6a3716f24d2ecf498d262832e3" +dependencies = [ + "elliptic-curve", +] + [[package]] name = "slab" version = "0.4.12" @@ -1588,7 +1653,7 @@ dependencies = [ "ed25519-dalek", "getrandom", "js-sys", - "multi-party-schnorr", + "multi-party-schnorr 1.3.0-pre.7", "orchard", "pasta_curves", "rand", @@ -1642,7 +1707,7 @@ dependencies = [ "group", "hex", "hmac", - "multi-party-schnorr", + "multi-party-schnorr 1.3.0-pre.6", "orchard", "pasta_curves", "rand", diff --git a/packages/wasm-mps/Cargo.toml b/packages/wasm-mps/Cargo.toml index 75750826579..9e2d441bb97 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.6", features = ["serde", "eddsa", "redpallas"] } +multi-party-schnorr = { git = "https://github.com/silence-laboratories/multi-party-schnorr", rev = "09185d1e4e78d096f2ab3e64d877fb0c88099688", features = ["serde", "eddsa", "redpallas", "vrf"] } orchard = { version = "0.13", default-features = false } zcash = { git = "https://github.com/silence-laboratories/garbling", rev = "268a8499bca4f1a1eea532117430fd6fd55310c8", features = ["dkg"] } pasta_curves = { version = "0.5", default-features = false } diff --git a/packages/wasm-mps/src/lib.rs b/packages/wasm-mps/src/lib.rs index 783c91fcce0..d10115995b1 100644 --- a/packages/wasm-mps/src/lib.rs +++ b/packages/wasm-mps/src/lib.rs @@ -12,6 +12,7 @@ mod mps { Bip32Public, }, curve25519_dalek::EdwardsPoint, + derive::{HardDeriveOutputEd25519, HardDerivePartyEd25519, MpcDeriveInitEd25519}, keygen::{ KeyRefreshData, KeygenMsg1, KeygenMsg2, KeygenParty, Keyshare, R0 as DkgR0, R1 as DkgR1, R2 as DkgR2, @@ -20,6 +21,11 @@ mod mps { messages::{SignMsg1, SignMsg2, SignMsg3}, PartialSign, SignError, SignReady, SignerParty, R0 as DsgR0, R1 as DsgR1, R2 as DsgR2, }, + vrf::{ + dkg::Party as VrfDkgConfig, keyshare_after_hard_derive, HardDeriveMsg0, HardDeriveMsg1, + HardDeriveR0, HardDeriveR1, HardDeriveR2, VrfDkgParty, VrfDkgR0, VrfDkgR1, VrfDkgR2, + VrfKeygenMsg1, VrfKeygenMsg2, VrfPoint, + }, }; use rand::Rng; use serde::{Deserialize, Serialize}; @@ -151,6 +157,18 @@ mod mps { pub state: Vec, } + /// Result from processing that includes a per-recipient message pool + /// (see [`MsgDerivationInit::msg`]) and a private state to be stored in memory. + pub struct MsgStateMap { + pub msg: HashMap>, + pub state: Vec, + } + + /// VRF signing share returned from the final round of VRF DKG. + pub struct VrfShare { + pub share: Vec, + } + /// Result from round 3 of RedPallas DSG. pub struct RedPallasSignature { pub signature: Vec, @@ -678,6 +696,317 @@ mod mps { }) } + /// Internal DKG state for VRF DKG (Ristretto) round 1. + #[derive(Serialize, Deserialize)] + struct VrfDkgStateR1 { + pub party_id: u8, + pub msg: VrfKeygenMsg1, + pub party: VrfDkgParty, + } + + /// Internal DKG state for VRF DKG (Ristretto) round 2. + #[derive(Serialize, Deserialize)] + struct VrfDkgStateR2 { + pub party_id: u8, + pub msg: VrfKeygenMsg2, + pub party: VrfDkgParty, + } + + /// Duplicate a single broadcast payload into a per-recipient message pool + /// addressed to every other party in a 3-party quorum. + fn broadcast_to_hashmap(party_id: u8, msg: Vec) -> HashMap> { + (0..3) + .filter(|&pid| pid != party_id) + .map(|pid| (pid, msg.clone())) + .collect() + } + + /// Process round 0 of VRF DKG (Ristretto, used to back hard derivation). + /// party_id: Party identifier / index. + /// seed: PRNG seed for entropy. + pub fn ed25519_vrf_dkg_round0_process( + party_id: u8, + seed: &[u8; 32], + ) -> Result { + if party_id >= 3 { + return Err(MpsError::InvalidInput); + } + + let p0 = VrfDkgParty::::new( + VrfDkgConfig::new(3, 2, party_id), + *seed, + &mut rand::thread_rng(), + ) + .map_err(|_| MpsError::ProtocolError)?; + let (p1, msg1) = p0.process(()).map_err(|_| MpsError::ProtocolError)?; + + let state = VrfDkgStateR1 { + party_id, + msg: msg1, + party: p1, + }; + + Ok(MsgState { + msg: add_prefix( + "mps-ed25519-vrf-dkg-round1-message$", + &bincode::serde::encode_to_vec(msg1, bincode::config::standard()) + .map_err(|_| MpsError::SerializationError)?, + ), + state: add_prefix( + "mps-ed25519-vrf-dkg-round1-state$", + &bincode::serde::encode_to_vec(&state, bincode::config::standard()) + .map_err(|_| MpsError::SerializationError)?, + ), + }) + } + + /// Process round 1 of VRF DKG. + /// round1_messages: Public messages from other parties. + /// state: Private state result from round 0. + /// Returns a per-recipient message pool: round 2's message carries plaintext + /// polynomial shares for every party, so it must never share a pool slot + /// across recipients (see `redpallas_derivation_process` for the same pattern). + pub fn ed25519_vrf_dkg_round1_process( + round1_messages: &[Vec; 2], + state: &[u8], + ) -> Result { + let i0_msg1: VrfKeygenMsg1 = bincode::serde::decode_from_slice( + &rem_prefix("mps-ed25519-vrf-dkg-round1-message$", &round1_messages[0])?, + bincode::config::standard(), + ) + .map(|(v, _)| v) + .map_err(|_| MpsError::DeserializationError)?; + let i1_msg1: VrfKeygenMsg1 = bincode::serde::decode_from_slice( + &rem_prefix("mps-ed25519-vrf-dkg-round1-message$", &round1_messages[1])?, + bincode::config::standard(), + ) + .map(|(v, _)| v) + .map_err(|_| MpsError::DeserializationError)?; + let state: VrfDkgStateR1 = bincode::serde::decode_from_slice( + &rem_prefix("mps-ed25519-vrf-dkg-round1-state$", state)?, + bincode::config::standard(), + ) + .map(|(v, _)| v) + .map_err(|_| MpsError::DeserializationError)?; + + let party_id = state.party_id; + let (p2, msg2) = state + .party + .process(vec![i0_msg1, i1_msg1, state.msg]) + .map_err(|_| MpsError::ProtocolError)?; + + let new_state = VrfDkgStateR2 { + party_id, + msg: msg2.clone(), + party: p2, + }; + + let msg = add_prefix( + "mps-ed25519-vrf-dkg-round2-message$", + &bincode::serde::encode_to_vec(&msg2, bincode::config::standard()) + .map_err(|_| MpsError::SerializationError)?, + ); + + Ok(MsgStateMap { + msg: broadcast_to_hashmap(party_id, msg), + state: add_prefix( + "mps-ed25519-vrf-dkg-round2-state$", + &bincode::serde::encode_to_vec(&new_state, bincode::config::standard()) + .map_err(|_| MpsError::SerializationError)?, + ), + }) + } + + /// Process round 2 of VRF DKG; finalizes the VRF signing share. + /// round2_messages: Public messages from other parties. + /// state: Private state result from round 1. + pub fn ed25519_vrf_dkg_round2_process( + round2_messages: &[Vec; 2], + state: &[u8], + ) -> Result { + let i0_msg2: VrfKeygenMsg2 = bincode::serde::decode_from_slice( + &rem_prefix("mps-ed25519-vrf-dkg-round2-message$", &round2_messages[0])?, + bincode::config::standard(), + ) + .map(|(v, _)| v) + .map_err(|_| MpsError::DeserializationError)?; + let i1_msg2: VrfKeygenMsg2 = bincode::serde::decode_from_slice( + &rem_prefix("mps-ed25519-vrf-dkg-round2-message$", &round2_messages[1])?, + bincode::config::standard(), + ) + .map(|(v, _)| v) + .map_err(|_| MpsError::DeserializationError)?; + let state: VrfDkgStateR2 = bincode::serde::decode_from_slice( + &rem_prefix("mps-ed25519-vrf-dkg-round2-state$", state)?, + bincode::config::standard(), + ) + .map(|(v, _)| v) + .map_err(|_| MpsError::DeserializationError)?; + + let share = state + .party + .process(vec![i0_msg2, i1_msg2, state.msg]) + .map_err(|_| MpsError::ProtocolError)?; + + Ok(VrfShare { + share: bincode::serde::encode_to_vec(&share, bincode::config::standard()) + .map_err(|_| MpsError::SerializationError)?, + }) + } + + /// Internal hard-derivation state for round 1. + #[derive(Serialize, Deserialize)] + struct HardDeriveStateR1 { + pub init: MpcDeriveInitEd25519, + pub msg: HardDeriveMsg0, + pub party: HardDerivePartyEd25519, + } + + /// Internal hard-derivation state for round 2. + #[derive(Serialize, Deserialize)] + struct HardDeriveStateR2 { + pub init: MpcDeriveInitEd25519, + pub msg: HardDeriveMsg1, + pub party: HardDerivePartyEd25519, + } + + /// Process round 0 of MPC hard derivation. + /// vrf_share: VRF signing share from VRF DKG. + /// root_share: Signing share from DKG, whose hardened child is being derived. + /// path: Arbitrary derivation path bytes (e.g. `"m/44'/0'/0'"`), evaluated in one shot. + pub fn ed25519_hard_derive_round0_process( + vrf_share: &[u8], + root_share: &[u8], + path: String, + ) -> Result { + let vrf_keyshare: Keyshare = + bincode::serde::decode_from_slice(vrf_share, bincode::config::standard()) + .map(|(v, _)| v) + .map_err(|_| MpsError::DeserializationError)?; + let root_keyshare: Keyshare = + bincode::serde::decode_from_slice(root_share, bincode::config::standard()) + .map(|(v, _)| v) + .map_err(|_| MpsError::DeserializationError)?; + if vrf_keyshare.party_id() != root_keyshare.party_id() { + return Err(MpsError::InvalidInput); + } + + let init = MpcDeriveInitEd25519::new(vrf_keyshare, root_keyshare); + let p0 = HardDerivePartyEd25519::::new( + init.clone(), + path.into_bytes(), + &mut rand::thread_rng(), + ) + .map_err(|_| MpsError::ProtocolError)?; + let (p1, msg0) = p0.process(()).map_err(|_| MpsError::ProtocolError)?; + + let state = HardDeriveStateR1 { + init, + msg: msg0.clone(), + party: p1, + }; + + Ok(MsgState { + msg: add_prefix( + "mps-ed25519-hard-derive-round1-message$", + &bincode::serde::encode_to_vec(&msg0, bincode::config::standard()) + .map_err(|_| MpsError::SerializationError)?, + ), + state: add_prefix( + "mps-ed25519-hard-derive-round1-state$", + &bincode::serde::encode_to_vec(&state, bincode::config::standard()) + .map_err(|_| MpsError::SerializationError)?, + ), + }) + } + + /// Process round 1 of MPC hard derivation. + /// round1_message: Public message from the other active party. + /// state: Private state result from round 0. + pub fn ed25519_hard_derive_round1_process( + round1_message: &[u8], + state: &[u8], + ) -> Result { + let other_msg0: HardDeriveMsg0 = bincode::serde::decode_from_slice( + &rem_prefix("mps-ed25519-hard-derive-round1-message$", round1_message)?, + bincode::config::standard(), + ) + .map(|(v, _)| v) + .map_err(|_| MpsError::DeserializationError)?; + let state: HardDeriveStateR1 = bincode::serde::decode_from_slice( + &rem_prefix("mps-ed25519-hard-derive-round1-state$", state)?, + bincode::config::standard(), + ) + .map(|(v, _)| v) + .map_err(|_| MpsError::DeserializationError)?; + + let (p2, msg1) = state + .party + .process(vec![other_msg0, state.msg]) + .map_err(|_| MpsError::ProtocolError)?; + + let new_state = HardDeriveStateR2 { + init: state.init, + msg: msg1.clone(), + party: p2, + }; + + Ok(MsgState { + msg: add_prefix( + "mps-ed25519-hard-derive-round2-message$", + &bincode::serde::encode_to_vec(&msg1, bincode::config::standard()) + .map_err(|_| MpsError::SerializationError)?, + ), + state: add_prefix( + "mps-ed25519-hard-derive-round2-state$", + &bincode::serde::encode_to_vec(&new_state, bincode::config::standard()) + .map_err(|_| MpsError::SerializationError)?, + ), + }) + } + + /// Process round 2 of MPC hard derivation; returns the derived child share. + /// The returned share plugs directly into `ed25519_dsg_round0_process`, + /// optionally with a further soft `derivation_path` on top. + /// round2_message: Public message from the other active party. + /// state: Private state result from round 1. + pub fn ed25519_hard_derive_round2_process( + round2_message: &[u8], + state: &[u8], + ) -> Result { + let other_msg1: HardDeriveMsg1 = bincode::serde::decode_from_slice( + &rem_prefix("mps-ed25519-hard-derive-round2-message$", round2_message)?, + bincode::config::standard(), + ) + .map(|(v, _)| v) + .map_err(|_| MpsError::DeserializationError)?; + let state: HardDeriveStateR2 = bincode::serde::decode_from_slice( + &rem_prefix("mps-ed25519-hard-derive-round2-state$", state)?, + bincode::config::standard(), + ) + .map(|(v, _)| v) + .map_err(|_| MpsError::DeserializationError)?; + + let self_party_id = state.init.party_id(); + let other_party_id = other_msg1.party_id(); + + let output: HardDeriveOutputEd25519 = state + .party + .process(vec![other_msg1, state.msg]) + .map_err(|_| MpsError::ProtocolError)?; + + let mut participating_party_ids = [self_party_id, other_party_id]; + participating_party_ids.sort_unstable(); + let derived = keyshare_after_hard_derive(&state.init, &output, &participating_party_ids); + + Ok(Share { + share: bincode::serde::encode_to_vec(&derived, bincode::config::standard()) + .map_err(|_| MpsError::SerializationError)?, + pk: derived.public_key.compress().to_bytes(), + chaincode: derived.root_chain_code, + }) + } + /// Process round 0 of DSG protocol. /// share: Signing share from DKG. /// derivation_path: Key derivation path. @@ -1387,6 +1716,146 @@ mod tests { .unwrap(); } + /// Test full VRF DKG + MPC hard derivation, then DSG with the derived share. + #[test] + fn test_ed25519_hard_derive() { + // Generate root signing shares (parties 0 and 2 will be the active quorum). + let mut prv_keys = Vec::new(); + let mut pub_keys = Vec::new(); + let mut seeds = Vec::new(); + let mut vrf_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)); + seeds.push(rand::thread_rng().gen::<[u8; 32]>()); + vrf_seeds.push(rand::thread_rng().gen::<[u8; 32]>()); + } + + let dkg_r0: Vec<_> = (0u8..3) + .map(|i| { + let others: Vec> = (0u8..3) + .filter(|&j| j != i) + .map(|j| pub_keys[j as usize].1.to_bytes().to_vec()) + .collect(); + mps::ed25519_dkg_round0_process( + i, + &prv_keys[i as usize].to_bytes(), + &[others[0].clone(), others[1].clone()], + &seeds[i as usize], + ) + .unwrap() + }) + .collect(); + let dkg_r1: Vec<_> = (0u8..3) + .map(|i| { + let others: Vec> = (0u8..3) + .filter(|&j| j != i) + .map(|j| dkg_r0[j as usize].msg.clone()) + .collect(); + mps::ed25519_dkg_round1_process( + &[others[0].clone(), others[1].clone()], + &dkg_r0[i as usize].state, + ) + .unwrap() + }) + .collect(); + let dkg_p0_share = mps::ed25519_dkg_round2_process( + &[dkg_r1[1].msg.clone(), dkg_r1[2].msg.clone()], + &dkg_r1[0].state, + ) + .unwrap(); + let dkg_p2_share = mps::ed25519_dkg_round2_process( + &[dkg_r1[0].msg.clone(), dkg_r1[1].msg.clone()], + &dkg_r1[2].state, + ) + .unwrap(); + + // Generate the VRF signing shares backing hard derivation. + let vrf_r0: Vec<_> = (0u8..3) + .map(|i| mps::ed25519_vrf_dkg_round0_process(i, &vrf_seeds[i as usize]).unwrap()) + .collect(); + let vrf_r1: Vec<_> = (0u8..3) + .map(|i| { + let others: Vec> = (0u8..3) + .filter(|&j| j != i) + .map(|j| vrf_r0[j as usize].msg.clone()) + .collect(); + mps::ed25519_vrf_dkg_round1_process( + &[others[0].clone(), others[1].clone()], + &vrf_r0[i as usize].state, + ) + .unwrap() + }) + .collect(); + let vrf_p0_share = mps::ed25519_vrf_dkg_round2_process( + &[vrf_r1[1].msg[&0].clone(), vrf_r1[2].msg[&0].clone()], + &vrf_r1[0].state, + ) + .unwrap(); + let vrf_p2_share = mps::ed25519_vrf_dkg_round2_process( + &[vrf_r1[0].msg[&2].clone(), vrf_r1[1].msg[&2].clone()], + &vrf_r1[2].state, + ) + .unwrap(); + + // Hard-derive a single hardened path in one shot (parties 0 and 2 active). + let path = "m/44'/0'/0'".to_string(); + let hd_p0_0 = mps::ed25519_hard_derive_round0_process( + &vrf_p0_share.share, + &dkg_p0_share.share, + path.clone(), + ) + .unwrap(); + let hd_p2_0 = + mps::ed25519_hard_derive_round0_process(&vrf_p2_share.share, &dkg_p2_share.share, path) + .unwrap(); + + let hd_p0_1 = + mps::ed25519_hard_derive_round1_process(&hd_p2_0.msg, &hd_p0_0.state).unwrap(); + let hd_p2_1 = + mps::ed25519_hard_derive_round1_process(&hd_p0_0.msg, &hd_p2_0.state).unwrap(); + + let hd_p0_share = + mps::ed25519_hard_derive_round2_process(&hd_p2_1.msg, &hd_p0_1.state).unwrap(); + let hd_p2_share = + mps::ed25519_hard_derive_round2_process(&hd_p0_1.msg, &hd_p2_1.state).unwrap(); + + assert_eq!(hd_p0_share.pk, hd_p2_share.pk, "derived pubkeys differ"); + assert_eq!( + hd_p0_share.chaincode, hd_p2_share.chaincode, + "derived chain codes differ" + ); + assert_ne!( + hd_p0_share.pk, dkg_p0_share.pk, + "derived pubkey must differ from the root pubkey" + ); + + // Sign with the derived share and verify against the derived pubkey. + let msg = b"hard-derived signing test"; + let dsg_p0_0 = + mps::ed25519_dsg_round0_process(&hd_p0_share.share, "m".to_string(), msg).unwrap(); + let dsg_p2_0 = + mps::ed25519_dsg_round0_process(&hd_p2_share.share, "m".to_string(), msg).unwrap(); + let dsg_p0_1 = mps::ed25519_dsg_round1_process(&dsg_p2_0.msg, &dsg_p0_0.state).unwrap(); + let dsg_p2_1 = mps::ed25519_dsg_round1_process(&dsg_p0_0.msg, &dsg_p2_0.state).unwrap(); + let dsg_p0_2 = mps::ed25519_dsg_round2_process(&dsg_p2_1.msg, &dsg_p0_1.state).unwrap(); + let dsg_p2_2 = mps::ed25519_dsg_round2_process(&dsg_p0_1.msg, &dsg_p2_1.state).unwrap(); + let dsg_p0_sig = mps::ed25519_dsg_round3_process(&dsg_p2_2.msg, &dsg_p0_2.state).unwrap(); + let dsg_p2_sig = mps::ed25519_dsg_round3_process(&dsg_p0_2.msg, &dsg_p2_2.state).unwrap(); + + assert_eq!(dsg_p0_sig, dsg_p2_sig, "signatures differ"); + + VerifyingKey::from_bytes(&hd_p0_share.pk) + .unwrap() + .verify( + msg, + &Signature::from_bytes(dsg_p0_sig.as_slice().try_into().unwrap()), + ) + .unwrap(); + } + /// Test full RedPallas DSG protocol; verifies alpha is non-zero, consistent /// between parties, and that the signature verifies against rk. #[test] @@ -1570,6 +2039,19 @@ impl Share { } } +#[wasm_bindgen] +pub struct VrfShare { + share: Vec, +} + +#[wasm_bindgen] +impl VrfShare { + #[wasm_bindgen(getter)] + pub fn share(&self) -> Vec { + self.share.clone() + } +} + #[wasm_bindgen] pub struct MsgDerivationInit { share: Vec, @@ -1601,6 +2083,25 @@ impl MsgDerivationInit { } } +#[wasm_bindgen] +pub struct MsgStateMap { + msg: Result, + state: Vec, +} + +#[wasm_bindgen] +impl MsgStateMap { + #[wasm_bindgen(getter)] + pub fn msg(&self) -> Result { + self.msg.clone() + } + + #[wasm_bindgen(getter)] + pub fn state(&self) -> Vec { + self.state.clone() + } +} + #[wasm_bindgen] pub struct MsgDerivation { msg: Result, @@ -1755,6 +2256,47 @@ pub fn ed25519_dkg_round2_process(round2_messages: Array, state: &[u8]) -> Resul }) } +#[wasm_bindgen] +pub fn ed25519_vrf_dkg_round0_process(party_id: u8, seed: &[u8]) -> Result { + let seed_32: [u8; 32] = seed.try_into().map_err(|_| "Invalid input")?; + let result = + mps::ed25519_vrf_dkg_round0_process(party_id, &seed_32).map_err(|e| e.to_string())?; + + Ok(MsgState { + msg: result.msg, + state: result.state, + }) +} + +#[wasm_bindgen] +pub fn ed25519_vrf_dkg_round1_process( + round1_messages: Array, + state: &[u8], +) -> Result { + let [m0, m1] = js_array_to_2_bufs(&round1_messages)?; + let result = + mps::ed25519_vrf_dkg_round1_process(&[m0, m1], state).map_err(|e| e.to_string())?; + + Ok(MsgStateMap { + msg: hashmap_to_js(result.msg), + state: result.state, + }) +} + +#[wasm_bindgen] +pub fn ed25519_vrf_dkg_round2_process( + round2_messages: Array, + state: &[u8], +) -> Result { + let [m0, m1] = js_array_to_2_bufs(&round2_messages)?; + let result = + mps::ed25519_vrf_dkg_round2_process(&[m0, m1], state).map_err(|e| e.to_string())?; + + Ok(VrfShare { + share: result.share, + }) +} + #[wasm_bindgen] pub fn ed25519_dsg_round0_process( share: &[u8], @@ -1800,6 +2342,50 @@ pub fn ed25519_dsg_round3_process(round3_message: &[u8], state: &[u8]) -> Result Ok(result.to_vec()) } +#[wasm_bindgen] +pub fn ed25519_hard_derive_round0_process( + vrf_share: &[u8], + root_share: &[u8], + path: String, +) -> Result { + let result = mps::ed25519_hard_derive_round0_process(vrf_share, root_share, path) + .map_err(|e| e.to_string())?; + + Ok(MsgState { + msg: result.msg, + state: result.state, + }) +} + +#[wasm_bindgen] +pub fn ed25519_hard_derive_round1_process( + round1_message: &[u8], + state: &[u8], +) -> Result { + let result = mps::ed25519_hard_derive_round1_process(round1_message, state) + .map_err(|e| e.to_string())?; + + Ok(MsgState { + msg: result.msg, + state: result.state, + }) +} + +#[wasm_bindgen] +pub fn ed25519_hard_derive_round2_process( + round2_message: &[u8], + state: &[u8], +) -> Result { + let result = mps::ed25519_hard_derive_round2_process(round2_message, state) + .map_err(|e| e.to_string())?; + + Ok(Share { + share: result.share, + pk: result.pk.to_vec(), + chaincode: result.chaincode.to_vec(), + }) +} + #[wasm_bindgen] pub fn redpallas_dkg_round0_process( party_id: u8, diff --git a/packages/wasm-mps/test/mps.ts b/packages/wasm-mps/test/mps.ts index f4428a10e84..2835f943232 100644 --- a/packages/wasm-mps/test/mps.ts +++ b/packages/wasm-mps/test/mps.ts @@ -696,6 +696,374 @@ describe("mps", function () { } }); }); + + describe("vrf_dkg", function () { + it("performs round 0", function () { + const messagePrefix = Buffer.from("mps-ed25519-vrf-dkg-round1-message$"); + const statePrefix = Buffer.from("mps-ed25519-vrf-dkg-round1-state$"); + for (let i = 0; i < keypairs.length; i++) { + const result = mps.ed25519_vrf_dkg_round0_process(i, crypto.randomBytes(32)); + 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 = [0, 1, 2].map((i) => + mps.ed25519_vrf_dkg_round0_process(i, crypto.randomBytes(32)), + ); + }); + + it("performs round 1", function () { + const messagePrefix = Buffer.from("mps-ed25519-vrf-dkg-round2-message$"); + const statePrefix = Buffer.from("mps-ed25519-vrf-dkg-round2-state$"); + for (let i = 0; i < results1.length; i++) { + const result = mps.ed25519_vrf_dkg_round1_process( + otherIndices[i].map((i) => results1[i].msg), + results1[i].state, + ); + for (const value of Object.values(result.msg as Record)) { + assert(Buffer.from(value).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-ed25519-vrf-dkg-round1-message$"); + for (let i = 0; i < results1.length; i++) { + shouldThrow(() => + mps.ed25519_vrf_dkg_round1_process( + otherIndices[i].map((i) => Buffer.from(results1[i].msg).slice(messagePrefix.length)), + results1[i].state, + ), + ); + shouldThrow(() => + mps.ed25519_vrf_dkg_round1_process( + otherIndices[i].map((i) => + Buffer.concat([ + Buffer.from("mps-ed25519-vrf-dkg-round2-message$"), + Buffer.from(results1[i].msg).slice(messagePrefix.length), + ]), + ), + results1[i].state, + ), + ); + } + }); + + it("fails to perform round 1 with invalid state prefix", function () { + const statePrefix = Buffer.from("mps-ed25519-vrf-dkg-round1-state$"); + for (let i = 0; i < results1.length; i++) { + shouldThrow(() => + mps.ed25519_vrf_dkg_round1_process( + otherIndices[i].map((i) => results1[i].msg), + Buffer.from(results1[i].state).slice(statePrefix.length), + ), + ); + shouldThrow(() => + mps.ed25519_vrf_dkg_round1_process( + otherIndices[i].map((j) => results1[j].msg), + Buffer.concat([ + Buffer.from("mps-ed25519-vrf-dkg-round2-state$"), + Buffer.from(results1[i].state).slice(statePrefix.length), + ]), + ), + ); + } + }); + + let results2: Array; + + before("performs round 1", function () { + results2 = [0, 1, 2].map((i) => + mps.ed25519_vrf_dkg_round1_process( + otherIndices[i].map((i) => results1[i].msg), + results1[i].state, + ), + ); + }); + + it("performs round 2", function () { + const shares = [0, 1, 2].map((i) => + mps.ed25519_vrf_dkg_round2_process( + otherIndices[i].map((j) => (results2[j].msg as Record)[i]), + results2[i].state, + ), + ); + for (const share of shares) { + assert.ok(share.share.length > 0); + } + }); + + it("fails to perform round 2 with invalid message prefix", function () { + const messagePrefix = Buffer.from("mps-ed25519-vrf-dkg-round2-message$"); + for (let i = 0; i < results2.length; i++) { + shouldThrow(() => + mps.ed25519_vrf_dkg_round2_process( + otherIndices[i].map((j) => + Buffer.from((results2[j].msg as Record)[i]).slice( + messagePrefix.length, + ), + ), + results2[i].state, + ), + ); + shouldThrow(() => + mps.ed25519_vrf_dkg_round2_process( + otherIndices[i].map((j) => + Buffer.concat([ + Buffer.from("mps-ed25519-vrf-dkg-round3-message$"), + Buffer.from((results2[j].msg as Record)[i]).slice( + messagePrefix.length, + ), + ]), + ), + results2[i].state, + ), + ); + } + }); + + it("fails to perform round 2 with invalid state prefix", function () { + const statePrefix = Buffer.from("mps-ed25519-vrf-dkg-round2-state$"); + for (let i = 0; i < results2.length; i++) { + shouldThrow(() => + mps.ed25519_vrf_dkg_round2_process( + otherIndices[i].map((j) => (results2[j].msg as Record)[i]), + Buffer.from(results2[i].state).slice(statePrefix.length), + ), + ); + shouldThrow(() => + mps.ed25519_vrf_dkg_round2_process( + otherIndices[i].map((j) => (results2[j].msg as Record)[i]), + Buffer.concat([ + Buffer.from("mps-ed25519-vrf-dkg-round3-state$"), + Buffer.from(results2[i].state).slice(statePrefix.length), + ]), + ), + ); + } + }); + }); + + describe("hard_derive", function () { + const otherIndex = [1, 0]; + let rootShares: Array; + let vrfShares: Array; + + before("performs root dkg", function () { + const results1 = [0, 1, 2].map((i) => + mps.ed25519_dkg_round0_process( + i, + keypairs[i].privateKey, + otherIndices[i].map((i) => keypairs[i].publicKey), + crypto.randomBytes(32), + ), + ); + const results2 = [0, 1, 2].map((i) => + mps.ed25519_dkg_round1_process( + otherIndices[i].map((i) => results1[i].msg), + results1[i].state, + ), + ); + rootShares = [0, 1, 2].map((i) => + mps.ed25519_dkg_round2_process( + otherIndices[i].map((i) => results2[i].msg), + results2[i].state, + ), + ); + }); + + before("performs vrf dkg", function () { + const results1 = [0, 1, 2].map((i) => + mps.ed25519_vrf_dkg_round0_process(i, crypto.randomBytes(32)), + ); + const results2 = [0, 1, 2].map((i) => + mps.ed25519_vrf_dkg_round1_process( + otherIndices[i].map((i) => results1[i].msg), + results1[i].state, + ), + ); + vrfShares = [0, 1, 2].map((i) => + mps.ed25519_vrf_dkg_round2_process( + otherIndices[i].map((j) => (results2[j].msg as Record)[i]), + results2[i].state, + ), + ); + }); + + const path = "m/44'/0'/0'"; + + it("performs round 0", function () { + const messagePrefix = Buffer.from("mps-ed25519-hard-derive-round1-message$"); + const statePrefix = Buffer.from("mps-ed25519-hard-derive-round1-state$"); + for (const i of [0, 2]) { + const result = mps.ed25519_hard_derive_round0_process( + vrfShares[i].share, + rootShares[i].share, + path, + ); + assert(Buffer.from(result.msg).slice(0, messagePrefix.length).equals(messagePrefix)); + assert(Buffer.from(result.state).slice(0, statePrefix.length).equals(statePrefix)); + } + }); + + let results0: Array; + + before("performs round 0", function () { + results0 = [0, 2].map((i) => + mps.ed25519_hard_derive_round0_process(vrfShares[i].share, rootShares[i].share, path), + ); + }); + + it("performs round 1", function () { + const messagePrefix = Buffer.from("mps-ed25519-hard-derive-round2-message$"); + const statePrefix = Buffer.from("mps-ed25519-hard-derive-round2-state$"); + for (let i = 0; i < results0.length; i++) { + const result = mps.ed25519_hard_derive_round1_process( + results0[otherIndex[i]].msg, + results0[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-ed25519-hard-derive-round1-message$"); + for (let i = 0; i < results0.length; i++) { + shouldThrow(() => + mps.ed25519_hard_derive_round1_process( + Buffer.from(results0[otherIndex[i]].msg).slice(messagePrefix.length), + results0[i].state, + ), + ); + shouldThrow(() => + mps.ed25519_hard_derive_round1_process( + Buffer.concat([ + Buffer.from("mps-ed25519-hard-derive-round2-message$"), + Buffer.from(results0[otherIndex[i]].msg).slice(messagePrefix.length), + ]), + results0[i].state, + ), + ); + } + }); + + it("fails to perform round 1 with invalid state prefix", function () { + const statePrefix = Buffer.from("mps-ed25519-hard-derive-round1-state$"); + for (let i = 0; i < results0.length; i++) { + shouldThrow(() => + mps.ed25519_hard_derive_round1_process( + results0[otherIndex[i]].msg, + Buffer.from(results0[i].state).slice(statePrefix.length), + ), + ); + shouldThrow(() => + mps.ed25519_hard_derive_round1_process( + results0[otherIndex[i]].msg, + Buffer.concat([ + Buffer.from("mps-ed25519-hard-derive-round2-state$"), + results0[i].state, + ]), + ), + ); + } + }); + + let results1: Array; + + before("performs round 1", function () { + results1 = [0, 1].map((i) => + mps.ed25519_hard_derive_round1_process(results0[otherIndex[i]].msg, results0[i].state), + ); + }); + + it("performs round 2", function () { + const shares = [0, 1].map((i) => + mps.ed25519_hard_derive_round2_process(results1[otherIndex[i]].msg, results1[i].state), + ); + assert.deepStrictEqual(shares[0].pk, shares[1].pk, "derived pubkeys differ"); + assert.deepStrictEqual( + shares[0].chaincode, + shares[1].chaincode, + "derived chain codes differ", + ); + assert.notDeepStrictEqual( + shares[0].pk, + rootShares[0].pk, + "derived pubkey must differ from the root pubkey", + ); + }); + + it("fails to perform round 2 with invalid message prefix", function () { + const messagePrefix = Buffer.from("mps-ed25519-hard-derive-round2-message$"); + for (let i = 0; i < results1.length; i++) { + shouldThrow(() => + mps.ed25519_hard_derive_round2_process( + Buffer.from(results1[otherIndex[i]].msg).slice(messagePrefix.length), + results1[i].state, + ), + ); + shouldThrow(() => + mps.ed25519_hard_derive_round2_process( + Buffer.concat([ + Buffer.from("mps-ed25519-hard-derive-round3-message$"), + Buffer.from(results1[otherIndex[i]].msg).slice(messagePrefix.length), + ]), + results1[i].state, + ), + ); + } + }); + + it("fails to perform round 2 with invalid state prefix", function () { + const statePrefix = Buffer.from("mps-ed25519-hard-derive-round2-state$"); + for (let i = 0; i < results1.length; i++) { + shouldThrow(() => + mps.ed25519_hard_derive_round2_process( + results1[otherIndex[i]].msg, + Buffer.from(results1[i].state).slice(statePrefix.length), + ), + ); + shouldThrow(() => + mps.ed25519_hard_derive_round2_process( + results1[otherIndex[i]].msg, + Buffer.concat([ + Buffer.from("mps-ed25519-hard-derive-round3-state$"), + Buffer.from(results1[i].state).slice(statePrefix.length), + ]), + ), + ); + } + }); + + const derivedShares: Array = []; + + before("performs round 2", function () { + const shares = [0, 1].map((i) => + mps.ed25519_hard_derive_round2_process(results1[otherIndex[i]].msg, results1[i].state), + ); + derivedShares[0] = shares[0]; + derivedShares[2] = shares[1]; + }); + + it("signs with the derived share and verifies against the derived pubkey", function () { + const message = Buffer.from("hard-derived signing test"); + const [sig0, sig2] = runDsg(derivedShares, "m", message); + assert.deepStrictEqual(sig0, sig2); + assert.ok(sodium.crypto_sign_verify_detached(sig0, message, derivedShares[0].pk)); + }); + + it("composes soft derivation on top of a hard-derived share", function () { + const message = Buffer.from("soft-on-hard signing test"); + const [sig0, sig2] = runDsg(derivedShares, "m/0/1", message); + assert.deepStrictEqual(sig0, sig2); + }); + }); }); describe("redpallas", function () {