use frost_core::Ciphersuite; use frost_core::serialization::SerializableScalar; use itertools::multizip; use rand_core::CryptoRngCore; use serde::{Deserialize, Serialize}; use serde_with::serde_as; use crate::participants::{Participant, ParticipantList, ParticipantMap}; use crate::thresholds::ReconstructionThreshold; use crate::{ crypto::{ commitment::{Commitment, commit}, hash::{HashOutput, hash}, proofs::{dlog, dlogeq, strobe_transcript::Transcript}, random::Randomness, }, ecdsa::{ CoefficientCommitment, Polynomial, PolynomialCommitment, ProjectivePoint, Scalar, Secp256K1Sha256, }, errors::{InitializationError, ProtocolError}, protocol::{ Protocol, helpers::recv_from_others, internal::{Comms, make_protocol}, }, }; use super::{TriplePub, TripleShare, multiplication::multiplication_many}; /// Creates a transcript and internally encodes the following data: /// LABEL, NAME, Participants, threshold fn create_transcript( participants: &ParticipantList, threshold: ReconstructionThreshold, ) -> Result { let mut transcript = Transcript::new(NEAR_TRIPLE_GENERATION_LABEL); transcript.message(b"group", NAME); let enc = rmp_serde::encode::to_vec(participants).map_err(|_| ProtocolError::ErrorEncoding)?; transcript.message(b"participants", &enc); // To allow interop between platforms where usize is different transcript.message( b"threshold", &u64::try_from(threshold.value()) .expect("threshold should always fit in u64") .to_be_bytes(), ); Ok(transcript) } /// The output of running the triple generation protocol. pub type TripleGenerationOutput = (TripleShare, TriplePub); pub type TripleGenerationOutputMany = Vec<(TripleShare, TriplePub)>; type C = Secp256K1Sha256; #[allow(dead_code)] // superseded by `PolynomialCommitmentsMessageMany`; kept as the single-instance wire-format reference. #[derive(Serialize, Deserialize)] struct PolynomialCommitmentsMessage { big_e: PolynomialCommitment, big_f: PolynomialCommitment, big_l: PolynomialCommitment, randomizer: Randomness, phi_proof0: dlog::Proof, phi_proof1: dlog::Proof, } use crate::crypto::constants::NEAR_TRIPLE_GENERATION_LABEL; const NAME: &[u8] = b"Secp256K1Sha256"; #[allow(clippy::too_many_lines)] async fn do_generation( comms: Comms, participants: ParticipantList, me: Participant, threshold: ReconstructionThreshold, rng: impl CryptoRngCore, ) -> Result { let mut triple = do_generation_many::<1>(comms, participants, me, threshold, rng).await?; if triple.len() != 1 { return Err(ProtocolError::Other( "Triple generation did not output one element".to_string(), )); } let triple = triple.pop().expect("The triple exist"); Ok(triple) } #[allow(clippy::struct_field_names)] struct ParallelToMultiplicationTaskOutputMany { big_e_v: Vec, big_f_v: Vec, big_l_v: Vec, big_c_v: Vec, a_i_v: Vec, b_i_v: Vec, } #[serde_as] #[derive(Serialize, Deserialize)] #[serde(transparent)] #[serde(bound(serialize = "T: Serialize", deserialize = "T: Deserialize<'de>"))] struct FixedArray(#[serde_as(as = "[_; N]")] [T; N]); #[serde_as] #[derive(Serialize, Deserialize)] #[allow(clippy::struct_field_names)] struct PolynomialCommitmentsMessageMany { #[serde_as(as = "[_; N]")] big_e_v: [PolynomialCommitment; N], #[serde_as(as = "[_; N]")] big_f_v: [PolynomialCommitment; N], #[serde_as(as = "[_; N]")] big_l_v: [PolynomialCommitment; N], #[serde_as(as = "[_; N]")] randomizer_v: [Randomness; N], #[serde_as(as = "[_; N]")] phi_proof0_v: [dlog::Proof; N], #[serde_as(as = "[_; N]")] phi_proof1_v: [dlog::Proof; N], } #[allow(clippy::too_many_lines)] async fn do_generation_many( comms: Comms, participants: ParticipantList, me: Participant, threshold: ReconstructionThreshold, mut rng: impl CryptoRngCore, ) -> Result { if N == 0 { return Err(ProtocolError::InvalidInput( "N must be greater than 0".to_string(), )); } let mut chan = comms.shared_channel(); let mut transcript = create_transcript(&participants, threshold)?; let mut my_commitments = vec![]; let mut my_randomizers = vec![]; let mut e_v = vec![]; let mut f_v = vec![]; let mut l_v = vec![]; let mut big_e_i_v = vec![]; let mut big_f_i_v = vec![]; let mut big_l_i_v = vec![]; let degree1 = threshold .value() .checked_sub(1) .ok_or(ProtocolError::IntegerOverflow)?; let degree2 = threshold .value() .checked_sub(2) .ok_or(ProtocolError::IntegerOverflow)?; for _ in 0..N { // Spec 1.2 let e = Polynomial::generate_polynomial(None, degree1, &mut rng)?; let f = Polynomial::generate_polynomial(None, degree1, &mut rng)?; let l = Polynomial::generate_polynomial(None, degree2, &mut rng)?; // Spec 1.4 let big_e_i = e.commit_polynomial()?; let big_f_i = f.commit_polynomial()?; let big_l_i = l.commit_polynomial()?; // Spec 1.5 let (my_commitment, my_randomizer) = commit(&mut rng, &(&big_e_i, &big_f_i, &big_l_i)) .map_err(|_| ProtocolError::PointSerialization)?; my_commitments.push(my_commitment); my_randomizers.push(my_randomizer); e_v.push(e); f_v.push(f); l_v.push(l); big_e_i_v.push(big_e_i); big_f_i_v.push(big_f_i); big_l_i_v.push(big_l_i); chan.yield_point().await; } // Spec 1.6 let wait0 = chan.next_waitpoint(); chan.send_many(wait0, &my_commitments)?; // Spec 2.1 let mut all_commitments_vec: Vec> = vec![]; for comi in my_commitments.iter().take(N) { let mut m = ParticipantMap::new(&participants); m.put(me, *comi); all_commitments_vec.push(m); } while all_commitments_vec .iter() .any(|all_commitments| !all_commitments.full()) { let (from, FixedArray(commitments)) = chan.recv::>(wait0).await?; for (acv, commit) in all_commitments_vec.iter_mut().zip(commitments.iter()) { acv.put(from, *commit); } } // Spec 2.2 let mut my_confirmations = vec![]; for c in all_commitments_vec.iter().take(N) { let my_confirmation = hash(c)?; my_confirmations.push(my_confirmation); } // Spec 2.3 let enc_confirmations = rmp_serde::encode::to_vec(&my_confirmations).map_err(|_| ProtocolError::ErrorEncoding)?; transcript.message(b"confirmation", &enc_confirmations); let my_phi_proof0_nonces: Vec<_> = (0..N).map(|_| ::generate_nonce(&mut rng)).collect(); let my_phi_proof1_nonces: Vec<_> = (0..N).map(|_| ::generate_nonce(&mut rng)).collect(); let my_phi_proof_nonces: Vec<_> = (0..N) .map(|_| frost_core::random_nonzero::(&mut rng)) .collect(); let my_l0_phi_proof_nonces: Vec<_> = (0..N).map(|_| ::generate_nonce(&mut rng)).collect(); // Spec 2.4 let multiplication_task = { let e0_v = e_v .iter() .map(|e| e.eval_at_zero().map(|x| x.0)) .collect::, _>>()?; let f0_v = f_v .iter() .map(|f| f.eval_at_zero().map(|x| x.0)) .collect::, _>>()?; multiplication_many::( comms.clone(), my_confirmations.clone(), participants.clone(), me, e0_v, f0_v, &mut rng, ) }; let parallel_to_multiplication_task = async { // Spec 2.5 let wait1 = chan.next_waitpoint(); chan.send_many(wait1, &my_confirmations)?; let mut my_phi_proof0v = vec![]; let mut my_phi_proof1v = vec![]; for (big_e_i, big_f_i, e, f, nonce0, nonce1) in multizip(( big_e_i_v.iter(), big_f_i_v.iter(), e_v.iter(), f_v.iter(), my_phi_proof0_nonces.iter(), my_phi_proof1_nonces.iter(), )) { // Spec 2.6 let statement0 = dlog::Statement:: { public: &big_e_i.eval_at_zero()?.value(), }; let witness0 = dlog::Witness:: { x: e.eval_at_zero()?, }; let my_phi_proof0 = dlog::prove_with_nonce( &mut transcript.fork(b"dlog0", &me.bytes()), statement0, &witness0, *nonce0, )?; let statement1 = dlog::Statement:: { public: &big_f_i.eval_at_zero()?.value(), }; let witness1 = dlog::Witness:: { x: f.eval_at_zero()?, }; let my_phi_proof1 = dlog::prove_with_nonce( &mut transcript.fork(b"dlog1", &me.bytes()), statement1, &witness1, *nonce1, )?; my_phi_proof0v.push(my_phi_proof0); my_phi_proof1v.push(my_phi_proof1); chan.yield_point().await; } // Spec 2.7 let wait2 = chan.next_waitpoint(); let message: PolynomialCommitmentsMessageMany = PolynomialCommitmentsMessageMany { big_e_v: big_e_i_v.to_array()?, big_f_v: big_f_i_v.to_array()?, big_l_v: big_l_i_v.to_array()?, randomizer_v: my_randomizers.to_array()?, phi_proof0_v: my_phi_proof0v.to_array()?, phi_proof1_v: my_phi_proof1v.to_array()?, }; chan.send_many(wait2, &message)?; let (big_e_i_v, big_f_i_v, big_l_i_v) = (message.big_e_v, message.big_f_v, message.big_l_v); // Spec 2.8 let wait3 = chan.next_waitpoint(); for p in participants.others(me) { let mut a_i_j_v = vec![]; let mut b_i_j_v = vec![]; for (e, f) in e_v.iter().zip(f_v.iter()) { let a_i_j = e.eval_at_participant(p)?.0; let b_i_j = f.eval_at_participant(p)?.0; a_i_j_v.push(a_i_j); b_i_j_v.push(b_i_j); } chan.send_private(wait3, p, &(a_i_j_v, b_i_j_v))?; } let mut a_i_v = vec![]; let mut b_i_v = vec![]; for (e, f) in e_v.iter().zip(f_v.iter()) { let a_i = e.eval_at_participant(me)?; let b_i = f.eval_at_participant(me)?; a_i_v.push(a_i.0); b_i_v.push(b_i.0); } // Spec 3.1 + 3.2 for (from, confirmation) in recv_from_others::>(&chan, wait1, &participants, me).await? { if confirmation != my_confirmations { return Err(ProtocolError::AssertionFailed(format!( "confirmation from {from:?} did not match expectation" ))); } } // Spec 3.3 + 3.4, and part of 3.6, 5.3, for summing up the Es, Fs, and Ls. let mut big_e_v: Vec<_> = big_e_i_v.iter().cloned().collect(); let mut big_f_v: Vec<_> = big_f_i_v.iter().cloned().collect(); let mut big_l_v: Vec<_> = big_l_i_v.iter().cloned().collect(); let mut big_e_j_zero_v: Vec<_> = (0..N).map(|_| ParticipantMap::new(&participants)).collect(); for (from, their) in recv_from_others::>(&chan, wait2, &participants, me) .await? { for ( all_commitments, their_big_e, their_big_f, their_big_l, their_randomizer, their_phi_proof0, their_phi_proof1, big_e_j_zero, big_e, big_f, big_l, ) in multizip(( all_commitments_vec.iter(), their.big_e_v.iter(), their.big_f_v.iter(), their.big_l_v.iter(), their.randomizer_v.iter(), their.phi_proof0_v.iter(), their.phi_proof1_v.iter(), big_e_j_zero_v.iter_mut(), big_e_v.iter_mut(), big_f_v.iter_mut(), big_l_v.iter_mut(), )) { if their_big_e.degree() != threshold.value() - 1 || their_big_f.degree() != threshold.value() - 1 // degree is threshold - 2 because the constant element identity is not serializable || their_big_l.degree() != threshold.value() - 2 { return Err(ProtocolError::AssertionFailed(format!( "polynomial from {from:?} has the wrong length" ))); } if !all_commitments .index(from)? .check( &(&their_big_e, &their_big_f, &their_big_l), their_randomizer, ) .map_err(|_| ProtocolError::PointSerialization)? { return Err(ProtocolError::AssertionFailed(format!( "commitment from {from:?} did not match revealed F" ))); } let statement0 = dlog::Statement:: { public: &their_big_e.eval_at_zero()?.value(), }; if !dlog::verify( &mut transcript.fork(b"dlog0", &from.bytes()), statement0, their_phi_proof0, )? { return Err(ProtocolError::AssertionFailed(format!( "dlog proof from {from:?} failed to verify" ))); } let statement1 = dlog::Statement:: { public: &their_big_f.eval_at_zero()?.value(), }; if !dlog::verify( &mut transcript.fork(b"dlog1", &from.bytes()), statement1, their_phi_proof1, )? { return Err(ProtocolError::AssertionFailed(format!( "dlog proof from {from:?} failed to verify" ))); } big_e_j_zero.put(from, their_big_e.eval_at_zero()?); *big_e = big_e.add(their_big_e)?; *big_f = big_f.add(their_big_f)?; *big_l = big_l.add(their_big_l)?; chan.yield_point().await; } } // Spec 3.5 + 3.6 for (_, (FixedArray(a_j_i_v), FixedArray(b_j_i_v))) in recv_from_others::<( FixedArray, N>, FixedArray, N>, )>(&chan, wait3, &participants, me) .await? { for ((a_i, b_i), (a_j_i, b_j_i)) in a_i_v .iter_mut() .zip(b_i_v.iter_mut()) .zip(a_j_i_v.iter().zip(b_j_i_v.iter())) { *a_i += &a_j_i.0; *b_i += &b_j_i.0; } } let mut big_c_i_points = vec![]; let mut big_c_i_v = vec![]; let mut my_phi_proofs = vec![]; for ((((big_e, big_f), (a_i, b_i)), (e, big_e_i)), nonce) in big_e_v .iter() .zip(big_f_v.iter()) .zip(a_i_v.iter().zip(b_i_v.iter())) .zip(e_v.iter().zip(big_e_i_v.iter())) .zip(my_phi_proof_nonces.iter()) { // Spec 3.7 let check1 = big_e.eval_at_participant(me)?.value() != ProjectivePoint::GENERATOR * a_i; let check2 = big_f.eval_at_participant(me)?.value() != ProjectivePoint::GENERATOR * b_i; if check1 || check2 { return Err(ProtocolError::AssertionFailed( "received bad private share".to_string(), )); } // Spec 3.8 let big_c_i = big_f.eval_at_zero()?.value() * e.eval_at_zero()?.0; // Spec 3.9 let statement = dlogeq::Statement:: { public0: &big_e_i.eval_at_zero()?.value(), generator1: &big_f.eval_at_zero()?.value(), public1: &big_c_i, }; let witness = dlogeq::Witness { x: e.eval_at_zero()?, }; let my_phi_proof = dlogeq::prove_with_nonce( &mut transcript.fork(b"dlogeq0", &me.bytes()), statement, &witness, *nonce, )?; big_c_i_points.push(CoefficientCommitment::new(big_c_i)); big_c_i_v.push(big_c_i); my_phi_proofs.push(my_phi_proof); chan.yield_point().await; } // Spec 3.10 let wait4 = chan.next_waitpoint(); chan.send_many(wait4, &(&big_c_i_points, &my_phi_proofs))?; // Spec 4.1 + 4.2 + 4.3 let mut big_c_v: Vec<_> = big_c_i_v.clone(); for (from, (FixedArray(big_c_j_v), FixedArray(their_phi_proofs))) in recv_from_others::<( FixedArray, FixedArray, N>, )>(&chan, wait4, &participants, me) .await? { for ((((big_e_j_zero, big_f), big_c_j), their_phi_proof), big_c) in big_e_j_zero_v .iter() .zip(big_f_v.iter()) .zip(big_c_j_v.iter()) .zip(their_phi_proofs.iter()) .zip(big_c_v.iter_mut()) { let big_c_j_val = big_c_j.value(); let statement = dlogeq::Statement:: { public0: &big_e_j_zero.index(from)?.value(), generator1: &big_f.eval_at_zero()?.value(), public1: &big_c_j_val, }; if !dlogeq::verify( &mut transcript.fork(b"dlogeq0", &from.bytes()), statement, their_phi_proof, )? { return Err(ProtocolError::AssertionFailed(format!( "dlogeq proof from {from:?} failed to verify" ))); } *big_c += big_c_j_val; chan.yield_point().await; } } let big_l_v = big_l_v .iter() .map(crate::crypto::polynomials::PolynomialCommitment::extend_with_identity) .collect::, _>>()?; Ok(ParallelToMultiplicationTaskOutputMany { big_e_v, big_f_v, big_l_v, big_c_v, a_i_v, b_i_v, }) }; // Spec 4.4 let ( l0_v, ParallelToMultiplicationTaskOutputMany { big_e_v, big_f_v, mut big_l_v, big_c_v, a_i_v, b_i_v, }, ) = futures::future::try_join(multiplication_task, parallel_to_multiplication_task).await?; let mut hat_big_c_i_points = vec![]; let mut hat_big_c_i_v = vec![]; let mut my_phi_proofs = vec![]; for (l0, nonce) in l0_v.iter().zip(my_l0_phi_proof_nonces.iter()) { // Spec 4.5 let hat_big_c_i = ProjectivePoint::GENERATOR * l0; // Spec 4.6 let statement = dlog::Statement:: { public: &hat_big_c_i, }; let witness = dlog::Witness:: { x: SerializableScalar::(*l0), }; let my_l0_phi_proof = dlog::prove_with_nonce( &mut transcript.fork(b"dlog2", &me.bytes()), statement, &witness, *nonce, )?; hat_big_c_i_points.push(CoefficientCommitment::new(hat_big_c_i)); hat_big_c_i_v.push(hat_big_c_i); my_phi_proofs.push(my_l0_phi_proof); chan.yield_point().await; } // Spec 4.8 let wait5 = chan.next_waitpoint(); chan.send_many(wait5, &(&hat_big_c_i_points, &my_phi_proofs))?; // Spec 4.9 for (l, l0) in l_v.iter_mut().zip(l0_v.iter()) { // extend to make the degree threshold - 1 *l = l.extend_with_zero()?; l.set_nonzero_constant(*l0)?; } let wait6 = chan.next_waitpoint(); let mut c_i_v = vec![]; for p in participants.others(me) { let mut c_i_j_v = Vec::new(); for l in &mut l_v { let c_i_j = l.eval_at_participant(p)?.0; c_i_j_v.push(c_i_j); } chan.send_private(wait6, p, &c_i_j_v)?; } for l in &mut l_v { let c_i = l.eval_at_participant(me)?; c_i_v.push(c_i.0); } // Spec 5.1 + 5.2 + 5.3 let mut hat_big_c_v: Vec<_> = hat_big_c_i_v.clone(); for (from, (FixedArray(their_hat_big_c_i_points), FixedArray(their_phi_proofs))) in recv_from_others::<( FixedArray, FixedArray, N>, )>(&chan, wait5, &participants, me) .await? { for ((their_point, their_phi_proof), hat_big_c) in their_hat_big_c_i_points .iter() .zip(their_phi_proofs.iter()) .zip(hat_big_c_v.iter_mut()) { let their_hat_big_c = their_point.value(); let statement = dlog::Statement:: { public: &their_hat_big_c, }; if !dlog::verify( &mut transcript.fork(b"dlog2", &from.bytes()), statement, their_phi_proof, )? { return Err(ProtocolError::AssertionFailed(format!( "dlog proof from {from:?} failed to verify" ))); } *hat_big_c += &their_hat_big_c; chan.yield_point().await; } } for ((big_l, hat_big_c), big_c) in big_l_v .iter_mut() .zip(hat_big_c_v.iter()) .zip(big_c_v.iter()) { // Spec 5.3 big_l.set_non_identity_constant(CoefficientCommitment::new(*hat_big_c))?; // Spec 5.4 if big_l.eval_at_zero()?.value() != *big_c { return Err(ProtocolError::AssertionFailed( "final polynomial doesn't match C value".to_owned(), )); } chan.yield_point().await; } // Spec 5.5 + 5.6 for (_, FixedArray(c_j_i_v)) in recv_from_others::, N>>(&chan, wait6, &participants, me) .await? { for (c_i, c_j_i) in c_i_v.iter_mut().zip(c_j_i_v.iter()) { *c_i += c_j_i.0; } } let mut ret = vec![]; // Spec 5.7 for (((big_l, (c_i, (a_i, b_i))), big_e), (big_f, big_c)) in big_l_v .iter() .zip(c_i_v.iter().zip(a_i_v.iter().zip(b_i_v.iter()))) .zip(big_e_v.iter()) .zip(big_f_v.iter().zip(big_c_v.iter())) { if big_l.eval_at_participant(me)?.value() != ProjectivePoint::GENERATOR * c_i { return Err(ProtocolError::AssertionFailed( "received bad private share of c".to_string(), )); } let big_a = big_e.eval_at_zero()?.value().to_affine(); let big_b = big_f.eval_at_zero()?.value().to_affine(); let big_c = (*big_c).into(); ret.push(( TripleShare { a: *a_i, b: *b_i, c: *c_i, }, TriplePub { big_a, big_b, big_c, participants: participants.clone().into(), threshold, }, )); chan.yield_point().await; } Ok(ret) } /// Generate a triple through a multi-party protocol. /// /// This requires a setup phase to have been conducted with these parties /// previously. /// /// The resulting triple will be threshold shared, according to the threshold /// provided to this function. fn validate_triple_inputs( participants: &[Participant], threshold: impl Into, ) -> Result<(ParticipantList, ReconstructionThreshold), InitializationError> { let threshold = threshold.into(); let threshold_value = threshold.value(); if participants.len() < 2 { return Err(InitializationError::NotEnoughParticipants { participants: participants.len(), }); } // Spec 1.1 if threshold_value > participants.len() { return Err(InitializationError::ThresholdTooLarge { threshold: threshold_value, max: participants.len(), }); } if threshold_value < 2 { return Err(InitializationError::ThresholdTooSmall { threshold: threshold_value, min: 2, }); } let participants = ParticipantList::new(participants).ok_or(InitializationError::DuplicateParticipants)?; Ok((participants, threshold)) } /// Maximum incoming buffer entries for OT-based triple generation. /// /// Returns `131 * N * (P - 1) + 7` where P = `num_participants` and /// N = `num_triples`. This is a worst-case upper bound; actual usage is /// typically ~50% of this value. /// /// The formula comes from two components: /// - The main protocol contributes 7 waitpoints, constant regardless of N /// (all N triples are batched within the same messages). /// - The multiplication sub-protocol runs once per (peer, triple) pair. Each /// pair's sender/receiver role is determined by a hash comparison /// (`hash(&(i, me))` vs `hash(&(i, p))`). The sender side creates 5 buffer /// entries; the receiver side creates 131 (128 from batch random OT + 3 from /// OT extension and MTA). The bound assumes the worst case: all-receiver. /// /// For N=1 the bound is exact (the max-buffered participant happens to be /// receiver for all peers). For N>1 it is conservative because hash-based role /// assignment makes it statistically unlikely for one participant to be receiver /// for all N*(P-1) pairs. /// /// Threshold does not affect the count. /// /// Returns an error if `num_participants` is 0 or if the result overflows. pub fn triple_generation_max_incoming_buffer_entries( num_participants: usize, num_triples: usize, ) -> Result { let peers = num_participants .checked_sub(1) .ok_or_else(|| InitializationError::BadParameters("num_participants must be > 0".into()))?; 131usize .checked_mul(num_triples) .and_then(|v| v.checked_mul(peers)) .and_then(|v| v.checked_add(7)) .ok_or_else(|| InitializationError::BadParameters("buffer size overflow".into())) } /// Generate a triple through a multi-party protocol. /// /// This requires a setup phase to have been conducted with these parties /// previously. /// /// The resulting triple will be threshold shared, according to the threshold /// provided to this function. pub fn generate_triple( participants: &[Participant], me: Participant, threshold: T, rng: R, ) -> Result + use, InitializationError> where T: Into, R: CryptoRngCore + Send + 'static, { let (participants, threshold) = validate_triple_inputs(participants, threshold)?; let ctx = Comms::with_buffer_capacity(triple_generation_max_incoming_buffer_entries( participants.len(), 1, )?); let fut = do_generation(ctx.clone(), participants, me, threshold, rng); Ok(make_protocol(ctx, fut)) } /// As [`generate_triple`] but for many triples at once pub fn generate_triple_many( participants: &[Participant], me: Participant, threshold: T, rng: R, ) -> Result + use, InitializationError> where T: Into, R: CryptoRngCore + Send + 'static, { let (participants, threshold) = validate_triple_inputs(participants, threshold)?; let ctx = Comms::with_buffer_capacity(triple_generation_max_incoming_buffer_entries( participants.len(), N, )?); let fut = do_generation_many::(ctx.clone(), participants, me, threshold, rng); Ok(make_protocol(ctx, fut)) } /// Extension trait for converting a `Vec` into a fixed-size array `[T; N]`. trait ToArray { fn to_array(self) -> Result<[T; N], ProtocolError>; } impl ToArray for Vec { fn to_array(self) -> Result<[T; N], ProtocolError> { let len = self.len(); self.try_into() .map_err(|_: Self| ProtocolError::UnexpectedLength { expected: N, actual: len, }) } } #[cfg(test)] mod test { use rand::{RngCore, SeedableRng}; use rstest::rstest; use crate::{ ecdsa::{ProjectivePoint, ot_based_ecdsa::triples::generate_triple}, participants::{Participant, ParticipantList}, protocol::Protocol, test_utils::{ MockCryptoRng, generate_participants, run_protocol, run_protocol_counting_yields, }, }; use super::{ C, TripleGenerationOutput, TripleGenerationOutputMany, do_generation_many, generate_triple_many, triple_generation_max_incoming_buffer_entries, }; #[test] fn test_triple_generation() { let mut rng = MockCryptoRng::seed_from_u64(42); let participants = generate_participants(3); let threshold = 3; let mut protocols: Vec<( Participant, Box>, )> = Vec::with_capacity(participants.len()); for &p in &participants { let rng_p = MockCryptoRng::seed_from_u64(rng.next_u64()); let protocol = generate_triple(&participants, p, threshold, rng_p).unwrap(); protocols.push((p, Box::new(protocol))); } let result = run_protocol(protocols).unwrap(); assert!(result.len() == participants.len()); assert_eq!(result[0].1.1, result[1].1.1); assert_eq!(result[1].1.1, result[2].1.1); let triple_pub = result[2].1.1.clone(); let participants = vec![result[0].0, result[1].0, result[2].0]; let triple_shares = [ result[0].1.0.clone(), result[1].1.0.clone(), result[2].1.0.clone(), ]; let p_list = ParticipantList::new(&participants).unwrap(); let a = p_list.lagrange::(participants[0]).unwrap() * triple_shares[0].a + p_list.lagrange::(participants[1]).unwrap() * triple_shares[1].a + p_list.lagrange::(participants[2]).unwrap() * triple_shares[2].a; assert_eq!(ProjectivePoint::GENERATOR * a, triple_pub.big_a); let b = p_list.lagrange::(participants[0]).unwrap() * triple_shares[0].b + p_list.lagrange::(participants[1]).unwrap() * triple_shares[1].b + p_list.lagrange::(participants[2]).unwrap() * triple_shares[2].b; assert_eq!(ProjectivePoint::GENERATOR * b, triple_pub.big_b); let c = p_list.lagrange::(participants[0]).unwrap() * triple_shares[0].c + p_list.lagrange::(participants[1]).unwrap() * triple_shares[1].c + p_list.lagrange::(participants[2]).unwrap() * triple_shares[2].c; assert_eq!(ProjectivePoint::GENERATOR * c, triple_pub.big_c); assert_eq!(a * b, c); insta::assert_json_snapshot!(result); } /// Deterministic guard that the CPU-bound triple-generation code surfaces /// cooperative yields; fails if the `yield_point` calls in the crypto loops /// are removed. #[test] #[expect(non_snake_case)] fn generate_triple_many__should_emit_yield_actions() { // Given - a real triple-generation batch across three participants let mut rng = MockCryptoRng::seed_from_u64(42); let participants = generate_participants(3); let threshold = 3; let mut protocols: Vec<( Participant, Box>, )> = Vec::with_capacity(participants.len()); for &p in &participants { let rng_p = MockCryptoRng::seed_from_u64(rng.next_u64()); let protocol = generate_triple_many::<2, _, _>(&participants, p, threshold, rng_p).unwrap(); protocols.push((p, Box::new(protocol))); } // When let yields = run_protocol_counting_yields(protocols).unwrap(); // Then assert!( yields > 0, "expected triple generation to yield cooperatively, but it never did" ); } #[test] fn test_triple_generation_many() { let mut rng = MockCryptoRng::seed_from_u64(42); let participants = generate_participants(3); let threshold = 3; let mut protocols: Vec<( Participant, Box>, )> = Vec::with_capacity(participants.len()); for &p in &participants { let rng_p = MockCryptoRng::seed_from_u64(rng.next_u64()); let protocol = generate_triple_many::<1, _, _>(&participants, p, threshold, rng_p).unwrap(); protocols.push((p, Box::new(protocol))); } let result = run_protocol(protocols).unwrap(); assert!(result.len() == participants.len()); assert_eq!(result[0].1[0].1, result[1].1[0].1); assert_eq!(result[1].1[0].1, result[2].1[0].1); let triple_pub = result[2].1[0].1.clone(); let participants = vec![result[0].0, result[1].0, result[2].0]; let triple_shares = [ result[0].1[0].0.clone(), result[1].1[0].0.clone(), result[2].1[0].0.clone(), ]; let p_list = ParticipantList::new(&participants).unwrap(); let a = p_list.lagrange::(participants[0]).unwrap() * triple_shares[0].a + p_list.lagrange::(participants[1]).unwrap() * triple_shares[1].a + p_list.lagrange::(participants[2]).unwrap() * triple_shares[2].a; assert_eq!(ProjectivePoint::GENERATOR * a, triple_pub.big_a); let b = p_list.lagrange::(participants[0]).unwrap() * triple_shares[0].b + p_list.lagrange::(participants[1]).unwrap() * triple_shares[1].b + p_list.lagrange::(participants[2]).unwrap() * triple_shares[2].b; assert_eq!(ProjectivePoint::GENERATOR * b, triple_pub.big_b); let c = p_list.lagrange::(participants[0]).unwrap() * triple_shares[0].c + p_list.lagrange::(participants[1]).unwrap() * triple_shares[1].c + p_list.lagrange::(participants[2]).unwrap() * triple_shares[2].c; assert_eq!(ProjectivePoint::GENERATOR * c, triple_pub.big_c); assert_eq!(a * b, c); insta::assert_json_snapshot!(result); } // The formula `131*N*(P-1) + 7` is exact for N=1 (the max-buffered // participant is receiver for all peers). For N>1 it is a conservative // upper bound because hash-based role assignment makes it unlikely for one // participant to be receiver for all N*(P-1) pairs. fn run_and_check_buffer(num_participants: usize, threshold: usize) { // Given let mut rng = MockCryptoRng::seed_from_u64(42); let participants = generate_participants(num_participants); let (protocols, comms_refs) = crate::test_utils::build_buffer_test( &participants, &mut rng, |comms, p_list, p, rng_p| { do_generation_many::(comms.clone(), p_list, p, threshold.into(), rng_p) }, ); // When let _ = run_protocol(protocols).unwrap(); // Then let capacity = triple_generation_max_incoming_buffer_entries(num_participants, N).unwrap(); let max_entries = comms_refs .iter() .map(|(_, comms)| comms.buffer_len()) .max() .unwrap(); assert!( max_entries <= capacity, "Buffer entries ({max_entries}) exceed capacity ({capacity}) \ for P={num_participants} N={N}" ); } /// Dispatches a runtime `num_triples` to a const-generic `run_and_check_buffer::`. macro_rules! dispatch_n { ($n:expr, $p:expr, $t:expr, [$($val:literal),+]) => { match $n { $($val => run_and_check_buffer::<$val>($p, $t),)+ _ => panic!("unsupported N={}", $n), } }; } #[rstest] // N=1: formula is exact #[case(2, 2, 1)] #[case(3, 2, 1)] #[case(4, 3, 1)] #[case(5, 3, 1)] #[case(7, 4, 1)] // N>1: formula is a conservative upper bound #[case(2, 2, 2)] #[case(4, 3, 4)] #[case(5, 3, 8)] #[case(6, 4, 16)] // Current node parameters: participants=9, threshold=6, batch triples=64 #[case(9, 6, 64)] fn test_triple_generation_max_incoming_buffer_entries( #[case] num_participants: usize, #[case] threshold: usize, #[case] num_triples: usize, ) { dispatch_n!( num_triples, num_participants, threshold, [1, 2, 4, 8, 16, 64] ); } }