diff --git a/Cargo.toml b/Cargo.toml index ba7c81cb..8210acd5 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -41,6 +41,7 @@ sha2 = "0.10.8" thiserror = "1" tracing = "0.1.37" zeroize = "1.5" +itertools = "0.13.0" sodiumoxide = "0.2.7" [dependencies.gmp-mpfr-sys] @@ -76,4 +77,4 @@ harness = false # This isn't strictly necessary but helps certain IDEs (Clion) find the code. [[example]] name = "threaded_example" -path = "examples/threaded_example/threaded.rs" \ No newline at end of file +path = "examples/threaded_example/threaded.rs" diff --git a/examples/threaded_example/threaded.rs b/examples/threaded_example/threaded.rs index 134db359..62eec745 100644 --- a/examples/threaded_example/threaded.rs +++ b/examples/threaded_example/threaded.rs @@ -427,7 +427,8 @@ impl Worker { let key_shares = self.key_gen_material.retrieve(&key_id).public_key_shares(); let record = self.presign_records.take(&key_id); - let inputs = sign::Input::new(b"hello world", record, key_shares.to_vec(), None); + let threshold = key_shares.len(); + let inputs = sign::Input::new(b"hello world", record, key_shares.to_vec(), threshold, None); self.new_sub_protocol::(sid, inputs, key_id) } } diff --git a/src/broadcast/participant.rs b/src/broadcast/participant.rs index b62235ca..ea0db7c9 100644 --- a/src/broadcast/participant.rs +++ b/src/broadcast/participant.rs @@ -52,6 +52,7 @@ pub(crate) struct BroadcastParticipant { pub(crate) enum BroadcastTag { AuxinfoR1CommitHash, KeyGenR1CommitHash, + TshareR1CommitHash, KeyRefreshR1CommitHash, PresignR1Ciphertexts, } diff --git a/src/lib.rs b/src/lib.rs index 674ee908..674bcf3d 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -222,6 +222,7 @@ mod protocol; mod ring_pedersen; pub mod sign; pub mod slip0010; +pub mod tshare; mod utils; mod zkp; mod zkstar; diff --git a/src/messages.rs b/src/messages.rs index dce3acb3..c732f5d1 100644 --- a/src/messages.rs +++ b/src/messages.rs @@ -30,6 +30,8 @@ pub enum MessageType { Auxinfo(AuxinfoMessageType), /// Keygen messages Keygen(KeygenMessageType), + /// Tshare messages + Tshare(TshareMessageType), /// Keyrefresh messages Keyrefresh(KeyrefreshMessageType), /// Presign messages @@ -68,6 +70,22 @@ pub enum KeygenMessageType { R3Proof, } +/// An enum consisting of all tshare message types +#[derive(Debug, Copy, Clone, Hash, PartialEq, Eq, Serialize, Deserialize)] +pub enum TshareMessageType { + /// Signal to self that we're ready to run the protocol + Ready, + /// A hash commitment to the public keyshare and associated proofs + R1CommitHash, + /// The information committed to in Round 1 + R2Decommit, + /// The encrypted private share from a participant to another. + R2PrivateShare, + /// A proof of knowledge of the discrete log of the value decommitted in + /// Round 2 + R3Proof, +} + /// An enum consisting of all keyrefresh message types #[derive(Debug, Copy, Clone, Hash, PartialEq, Eq, Serialize, Deserialize)] pub enum KeyrefreshMessageType { @@ -211,4 +229,17 @@ impl Message { } Ok(()) } + + /// Check if the message type is one of the valid options. + pub(crate) fn check_one_of_type(&self, expected_types: &[MessageType]) -> Result<()> { + if !expected_types.contains(&self.message_type()) { + error!( + "A message was misrouted. Expected one of {:?}, Got {:?}", + expected_types, + self.message_type() + ); + return Err(InternalError::InternalInvariantFailed); + } + Ok(()) + } } diff --git a/src/participant.rs b/src/participant.rs index e0d7c841..f0ce6d67 100644 --- a/src/participant.rs +++ b/src/participant.rs @@ -460,7 +460,7 @@ pub enum Status { /// This variant is used by /// [`InteractiveSignParticipant`](crate::sign::InteractiveSignParticipant) RunningPresign, - /// Participant completed presign and is running sign. + /// Participant received a ready message and is running tshare. /// /// This variant is used by /// [`InteractiveSignParticipant`](crate::sign::InteractiveSignParticipant) diff --git a/src/presign/record.rs b/src/presign/record.rs index 0054d1fa..b8d72a81 100644 --- a/src/presign/record.rs +++ b/src/presign/record.rs @@ -266,7 +266,7 @@ mod tests { /// Simulate creation of a random presign record. Do not use outside of /// testing. - fn simulate(rng: &mut StdRng) -> PresignRecord { + pub(crate) fn simulate(rng: &mut StdRng) -> PresignRecord { let mask_point = CurvePoint::random(StdRng::from_seed(rng.gen())); let mask_share = Scalar::random(StdRng::from_seed(rng.gen())); let masked_key_share = Scalar::random(rng); diff --git a/src/protocol.rs b/src/protocol.rs index 77bfb411..62f8f052 100644 --- a/src/protocol.rs +++ b/src/protocol.rs @@ -35,6 +35,7 @@ use tracing::{error, info, instrument, trace}; #[derive(Debug)] pub enum ProtocolType { Keygen, + Tshare, Keyrefresh, AuxInfo, Presign, @@ -168,6 +169,7 @@ impl Participant

{ match (message.message_type(), P::protocol_type()) { (MessageType::Auxinfo(_), ProtocolType::AuxInfo) | (MessageType::Keygen(_), ProtocolType::Keygen) + | (MessageType::Tshare(_), ProtocolType::Tshare) | (MessageType::Keyrefresh(_), ProtocolType::Keyrefresh) | (MessageType::Presign(_), ProtocolType::Presign) | (MessageType::Sign(_), ProtocolType::Sign) @@ -331,6 +333,23 @@ pub(crate) mod participant_config { let id = ParticipantIdentifier::random(rng); Self { id, other_ids } } + + /// Remove id from ParticipantConfig + pub fn remove(&self) -> Result> { + assert!(self.other_ids.len() > 1); + let other_ids = self.other_ids(); + // for each element in other_ids, create a new ParticipantConfig + // with that element as the id + other_ids + .iter() + .enumerate() + .map(|(i, id)| { + let mut others = other_ids.to_vec(); + let _removed = others.swap_remove(i); + Self::new(*id, &others[..]) + }) + .collect::>>() + } } #[cfg(test)] @@ -432,6 +451,11 @@ impl ParticipantIdentifier { pub fn from_u128(id: u128) -> Self { Self(id) } + + /// Get the ID as a number. + pub fn as_u128(&self) -> u128 { + self.0 + } } /// The `SharedContext` contains fixed known parameters across the entire @@ -614,20 +638,21 @@ impl std::fmt::Display for Identifier { mod tests { use super::*; use crate::{ - auxinfo::AuxInfoParticipant, + auxinfo::{self, AuxInfoParticipant, AuxInfoPublic}, keygen::KeygenParticipant, participant::Status, presign, sign::{self, InteractiveSignParticipant, SignParticipant}, slip0010, - utils::testing::init_testing, + tshare::{self, CoeffPrivate, TshareParticipant}, + utils::{bn_to_scalar, testing::init_testing}, PresignParticipant, }; use core::panic; - use k256::ecdsa::signature::DigestVerifier; + use k256::{ecdsa::signature::DigestVerifier, Scalar}; use rand::seq::IteratorRandom; use sha3::{Digest, Keccak256}; - use std::collections::HashMap; + use std::{collections::HashMap, vec}; use tracing::debug; // Negative test checking whether the message has the correct session id @@ -820,20 +845,57 @@ mod tests { inboxes.iter().all(|(_pid, messages)| messages.is_empty()) } + #[ignore] + #[test] + fn test_full_protocol_execution_with_noninteractive_signing_works_larger_values() { + assert!(full_protocol_execution_with_noninteractive_signing_works(5, 5, 5, 42).is_ok()); + assert!(full_protocol_execution_with_noninteractive_signing_works(5, 4, 5, 42).is_ok()); + assert!(full_protocol_execution_with_noninteractive_signing_works(4, 4, 5, 42).is_ok()); + assert!(full_protocol_execution_with_noninteractive_signing_works(5, 3, 5, 42).is_ok()); + assert!(full_protocol_execution_with_noninteractive_signing_works(4, 3, 5, 42).is_ok()); + assert!(full_protocol_execution_with_noninteractive_signing_works(3, 3, 5, 42).is_ok()); + } + #[cfg_attr(feature = "flame_it", flame)] #[test] fn test_full_protocol_execution_with_noninteractive_signing_works() { - assert!(full_protocol_execution_with_noninteractive_signing_works(42).is_ok()); + assert!(full_protocol_execution_with_noninteractive_signing_works(3, 3, 3, 42).is_ok()); + assert!(full_protocol_execution_with_noninteractive_signing_works(3, 2, 3, 42).is_ok()); + assert!(full_protocol_execution_with_noninteractive_signing_works(2, 2, 3, 42).is_ok()); // 2**31 let invalid_index = 1 << 31; - assert!(full_protocol_execution_with_noninteractive_signing_works(invalid_index).is_err()); + assert!( + full_protocol_execution_with_noninteractive_signing_works(3, 3, 3, invalid_index) + .is_err() + ); } - fn full_protocol_execution_with_noninteractive_signing_works(child_index: u32) -> Result<()> { + #[ignore] + #[test] + fn test_full_protocol_execution_with_noninteractive_signing_works_err_larger_values() { + assert!(full_protocol_execution_with_noninteractive_signing_works(3, 4, 5, 42).is_err()); + assert!(full_protocol_execution_with_noninteractive_signing_works(2, 4, 5, 42).is_err()); + } + + #[test] + fn test_full_protocol_execution_with_noninteractive_signing_works_err() { + assert!(full_protocol_execution_with_noninteractive_signing_works(2, 3, 4, 42).is_err()); + } + + fn full_protocol_execution_with_noninteractive_signing_works( + r: usize, + t: usize, + n: usize, + child_index: u32, + ) -> Result<()> { let mut rng = init_testing(); - let QUORUM_SIZE = 3; + let QUORUM_REAL = r; // The real quorum size, which is the number of participants that will actually + // participate in the protocol + let QUORUM_THRESHOLD = t; // threshold t, which is the minimum quorum allowed to complete the protocol + let QUORUM_SIZE = n; // total number of participants in the protocol + // Set GLOBAL config for participants - let configs = ParticipantConfig::random_quorum(QUORUM_SIZE, &mut rng).unwrap(); + let mut configs = ParticipantConfig::random_quorum(QUORUM_SIZE, &mut rng).unwrap(); // Set up auxinfo participants let auxinfo_sid = Identifier::random(&mut rng); @@ -912,26 +974,156 @@ mod tests { } } - // Keygen is done! Makre sure there are no more messages. + // Keygen is done! Make sure there are no more messages. assert!(inboxes_are_empty(&inboxes)); // And make sure all participants have successfully terminated. assert!(keygen_quorum .iter() .all(|p| *p.status() == Status::TerminatedSuccessfully)); - // Save the public key and key shares for later - let public_key_shares = keygen_outputs + // Tshare protocol + // After the Tshare protocol, we will have a set of t-out-of-t shares + // Therefore we need to remove the some participants from the configs, and from + // keygen and auxinfo outputs + let mut keygen_outputs_tshare = keygen_outputs.clone(); + let auxinfo_outputs_tshare = auxinfo_outputs.clone(); + let tshare_sid = Identifier::random(&mut rng); + + let tshare_inputs = configs + .iter() + .map(|config| { + ( + auxinfo_outputs.remove(&config.id()).unwrap(), + keygen_outputs_tshare.remove(&config.id()).unwrap(), + ) + }) + .map(|(auxinfo_output, keygen_output)| { + // convert the private share to CoeffPrivate + let secret = keygen_output.private_key_share().as_ref(); + tshare::Input::new( + auxinfo_output, + Some(CoeffPrivate { + x: bn_to_scalar(secret).unwrap(), + }), + QUORUM_THRESHOLD, + ) + .unwrap() + }) + .collect::>(); + + let mut tshare_quorum = configs + .clone() + .into_iter() + .zip(tshare_inputs.clone()) + .map(|(config, input)| { + Participant::::from_config(config, tshare_sid, input).unwrap() + }) + .collect::>(); + let mut tshare_outputs: HashMap< + ParticipantIdentifier, + ::Output, + > = HashMap::new(); + + let mut inboxes: HashMap> = HashMap::from_iter( + tshare_quorum + .iter() + .map(|p| (p.id, vec![])) + .collect::>(), + ); + + for participant in &mut tshare_quorum { + let inbox = inboxes.get_mut(&participant.id).unwrap(); + inbox.push(participant.initialize_message()?); + } + + while tshare_outputs.len() < QUORUM_SIZE { + let output = process_random_message(&mut tshare_quorum, &mut inboxes, &mut rng)?; + + if let Some((pid, output)) = output { + // Save the output, and make sure this participant didn't already return an + // output. + assert!(tshare_outputs.insert(pid, output).is_none()); + } + } + + // Tshare is done! Make sure there are no more messages. + assert!(inboxes_are_empty(&inboxes)); + // And make sure all participants have successfully terminated. + assert!(tshare_quorum + .iter() + .all(|p| *p.status() == Status::TerminatedSuccessfully)); + + // remove QUORUM_SIZE - QUORUM_REAL elements from the configs (the last ones) + assert!(QUORUM_REAL > 1); + assert!(QUORUM_SIZE >= QUORUM_REAL); + for _ in 0..(QUORUM_SIZE - QUORUM_REAL) { + configs = configs.clone().last().unwrap().remove().unwrap(); + } + assert!(configs.len() == QUORUM_REAL); + + let all_participants = configs.first().unwrap().all_participants(); + + // t-out-of-t conversion + let chain_code = keygen_outputs[&configs[0].id()].chain_code(); + let rid = keygen_outputs[&configs[0].id()].rid(); + let (mut toft_keygen_outputs, _toft_public_keys) = + TshareParticipant::convert_to_t_out_of_t_shares( + tshare_outputs, + all_participants.clone(), + *chain_code, + *rid, + )?; + + if QUORUM_REAL >= QUORUM_THRESHOLD { + let mut sum_toft_private_shares = toft_keygen_outputs + .values() + .map(|output| output.private_key_share().as_ref().clone()) + .fold(BigNumber::zero(), |acc, x| acc + x); + + // Check the sum is indeed the sum of original private keys used as input of + // tshare + let sum_tshare_input = tshare_inputs + .iter() + .map(|input| input.share().unwrap().x) + .fold(Scalar::ZERO, |acc, x| acc + x); + // reduce mod the order + sum_toft_private_shares %= k256_order(); + assert_eq!( + bn_to_scalar(&sum_toft_private_shares).unwrap(), + sum_tshare_input + ); + } + + let public_key_shares = toft_keygen_outputs .get(&configs.first().unwrap().id()) .unwrap() .public_key_shares() .to_vec(); - let saved_public_key = keygen_outputs + let saved_public_key = toft_keygen_outputs .get(&configs.first().unwrap().id()) .unwrap() .public_key()?; let keygen_outputs_clone = keygen_outputs.clone(); // Set up presign participants + let mut auxinfo_outputs_presign = HashMap::new(); + + // remove elements not in `all-participants` from auxinfo_outputs_presign + for pid in auxinfo_outputs_tshare.keys() { + if all_participants.contains(pid) { + let output = auxinfo_outputs_tshare.get(pid).unwrap(); + let new_aux_pk: Vec = output + .public_auxinfo() + .iter() + .filter(|auxinfo| all_participants.contains(&auxinfo.participant())) + .cloned() + .collect(); + let new_output = + auxinfo::Output::from_parts(new_aux_pk, output.private_auxinfo().clone())?; + assert!(auxinfo_outputs_presign.insert(*pid, new_output).is_none()); + } + } + let presign_sid = Identifier::random(&mut rng); // Prepare presign inputs: a pair of outputs from keygen and auxinfo. @@ -939,8 +1131,8 @@ mod tests { .iter() .map(|config| { ( - auxinfo_outputs.remove(&config.id()).unwrap(), - keygen_outputs.remove(&config.id()).unwrap(), + auxinfo_outputs_presign.remove(&config.id()).unwrap(), + toft_keygen_outputs.remove(&config.id()).unwrap(), ) }) .map(|(auxinfo_output, keygen_output)| { @@ -966,7 +1158,7 @@ mod tests { inbox.push(participant.initialize_message()?); } - while presign_outputs.len() < QUORUM_SIZE { + while presign_outputs.len() < QUORUM_REAL { let output = process_random_message(&mut presign_quorum, &mut inboxes, &mut rng)?; if let Some((pid, output)) = output { @@ -1009,6 +1201,7 @@ mod tests { // Make signing participants let mut sign_quorum = configs + .clone() .into_iter() .map(|config| { let record = presign_outputs.remove(&config.id()).unwrap(); @@ -1016,6 +1209,7 @@ mod tests { message, record, public_key_shares.clone(), + QUORUM_THRESHOLD, Some(shift_scalar), ); Participant::::from_config(config, sign_sid, input) @@ -1030,7 +1224,7 @@ mod tests { } // Run signing protocol - while sign_outputs.len() < QUORUM_SIZE { + while sign_outputs.len() < QUORUM_REAL { let output = process_random_message(&mut sign_quorum, &mut inboxes, &mut rng)?; if let Some((_pid, output)) = output { diff --git a/src/sign/interactive_sign/participant.rs b/src/sign/interactive_sign/participant.rs index ce973716..4b65833d 100644 --- a/src/sign/interactive_sign/participant.rs +++ b/src/sign/interactive_sign/participant.rs @@ -111,8 +111,11 @@ impl SigningMaterial { digest, public_keys, } => { + // TODO: threhsold is not implemented yet for the interactive signing, must use + // the same as the public keys size + let threshold = public_keys.len(); let signing_input = - sign::Input::new_from_digest(*digest, record, public_keys, None); + sign::Input::new_from_digest(*digest, record, public_keys, threshold, None); // Note: this shouldn't throw an error because the only failure case should have // also been checked by the presign constructor, and computation // halted far before we reach this point. @@ -257,7 +260,7 @@ impl ProtocolParticipant for InteractiveSignParticipant { // and sign -- e.g. we will not pass a ready message to the `signer` until // the `presigner` is sucessfully completed. // Another option would be to maintain a status field and update it at - // the appropriate poitns. + // the appropriate points. if !self.presigner.status().is_ready() { return &Status::NotReady; } diff --git a/src/sign/non_interactive_sign/participant.rs b/src/sign/non_interactive_sign/participant.rs index af704bb9..a7298d18 100644 --- a/src/sign/non_interactive_sign/participant.rs +++ b/src/sign/non_interactive_sign/participant.rs @@ -79,6 +79,7 @@ pub struct Input { digest: Keccak256, presign_record: PresignRecord, public_key_shares: Vec, + threshold: usize, shift: Option, } @@ -91,12 +92,14 @@ impl Input { message: &[u8], record: PresignRecord, public_key_shares: Vec, + threshold: usize, shift: Option, ) -> Self { Self { digest: Keccak256::new_with_prefix(message), presign_record: record, public_key_shares, + threshold, shift, } } @@ -109,12 +112,14 @@ impl Input { digest: Keccak256, record: PresignRecord, public_key_shares: Vec, + threshold: usize, shift: Option, ) -> Self { Self { digest, presign_record: record, public_key_shares, + threshold, shift, } } @@ -232,6 +237,9 @@ impl ProtocolParticipant for SignParticipant { if public_key_pids != pids || config.count() != input.public_key_shares.len() { Err(CallerError::BadInput)? } + if config.count() < input.threshold { + Err(CallerError::BadInput)? + } Ok(Self { sid, @@ -433,16 +441,20 @@ impl SignParticipant { fn compute_output(&mut self) -> Result::Output>> { // Retrieve everyone's share and the x-projection we saved in round one // (This will fail if we're missing any shares) + + // Retrieve everyone's id and share let shares = self .all_participants() .into_iter() .map(|pid| self.storage.remove::(pid)) .collect::>>()?; + let x_projection = self.storage.remove::(self.id())?; // Sum up the signature shares and convert to BIP-0062 format (negating if the // sum is > group order /2) let mut sum = shares.into_iter().fold(Scalar::ZERO, |a, b| a + b); + sum.conditional_assign(&sum.negate(), sum.is_high()); let signature = Signature::try_from_scalars(x_projection, sum)?; @@ -465,6 +477,7 @@ impl SignParticipant { #[cfg(test)] mod test { + use crate::ParticipantIdentifier; use std::collections::HashMap; use k256::{ @@ -584,7 +597,13 @@ mod test { // Form signing inputs and participants let inputs = std::iter::zip(keygen_outputs, presign_records).map(|(keygen, record)| { - sign::Input::new(message, record, keygen.public_key_shares().to_vec(), None) + sign::Input::new( + message, + record, + keygen.public_key_shares().to_vec(), + quorum_size, + None, + ) }); let mut quorum = std::iter::zip(configs, inputs) .map(|(config, input)| { @@ -678,4 +697,39 @@ mod test { Ok(()) } + + // test threshold signature with less than t participants + #[test] + fn signing_fails_with_less_than_threshold() -> Result<()> { + // create SignParticipant + let threshold = 3; + let rng = &mut init_testing(); + let sid = Identifier::random(rng); + + // create participant_ids + let id = ParticipantIdentifier::random(rng); + // not enough other participants + let other_participant_ids = (0..threshold - 2) + .map(|_| ParticipantIdentifier::random(rng)) + .collect::>(); + let participant_ids = std::iter::once(id) + .chain(other_participant_ids.clone()) + .collect::>(); + + // create input + let message = b"the quick brown fox jumped over the lazy dog"; + let keygen_output = keygen::Output::simulate(&participant_ids, rng); + let presign_record = PresignRecord::simulate(rng); + let input = sign::Input::new( + message, + presign_record, + keygen_output.public_key_shares().to_vec(), + threshold, + None, + ); + + let participant = SignParticipant::new(sid, id, other_participant_ids, input); + assert!(participant.is_err()); + Ok(()) + } } diff --git a/src/sign/non_interactive_sign/share.rs b/src/sign/non_interactive_sign/share.rs index fd41fbaa..b8bc4784 100644 --- a/src/sign/non_interactive_sign/share.rs +++ b/src/sign/non_interactive_sign/share.rs @@ -16,7 +16,7 @@ use crate::{ /// A single participant's share of the signature. #[allow(unused)] #[derive(Debug, Clone, Serialize, Deserialize)] -pub struct SignatureShare(Scalar); +pub struct SignatureShare(pub Scalar); impl SignatureShare { pub(super) fn new(share: Scalar) -> Self { diff --git a/src/tshare/commit.rs b/src/tshare/commit.rs new file mode 100644 index 00000000..acc061f5 --- /dev/null +++ b/src/tshare/commit.rs @@ -0,0 +1,123 @@ +// Copyright (c) Facebook, Inc. and its affiliates. +// Modifications Copyright (c) 2022-2023 Bolt Labs Holdings, Inc +// +// This source code is licensed under both the MIT license found in the +// LICENSE-MIT file in the root directory of this source tree and the Apache +// License, Version 2.0 found in the LICENSE-APACHE file in the root directory +// of this source tree. + +use super::share::CoeffPublic; +use crate::{ + errors::{InternalError, Result}, + messages::{Message, MessageType, TshareMessageType}, + protocol::{Identifier, ParticipantIdentifier}, + utils::CurvePoint, +}; +use merlin::Transcript; +use rand::{CryptoRng, RngCore}; +use serde::{Deserialize, Serialize}; +use tracing::error; + +/// Public commitment to `TshareDecommit` in round 1. +#[derive(Debug, Serialize, Deserialize, Clone, PartialEq, Eq)] +pub(crate) struct TshareCommit { + hash: [u8; 32], +} +impl TshareCommit { + pub(crate) fn from_message(message: &Message) -> Result { + message.check_type(MessageType::Tshare(TshareMessageType::R1CommitHash))?; + let tshare_commit: TshareCommit = deserialize!(&message.unverified_bytes)?; + Ok(tshare_commit) + } +} + +/// Decommitment published in round 2. +#[derive(Serialize, Deserialize, Clone)] +pub(crate) struct TshareDecommit { + sid: Identifier, + sender: ParticipantIdentifier, + u_i: [u8; 32], // The blinding factor is never read but it is included in the commitment. + pub rid: [u8; 32], + pub coeff_publics: Vec, + pub precom: CurvePoint, +} + +impl TshareDecommit { + ///`sid` corresponds to a unique session identifier. + pub(crate) fn new( + rng: &mut R, + sid: &Identifier, + sender: &ParticipantIdentifier, + coeff_publics: &[CoeffPublic], + sch_precom: CurvePoint, + ) -> Self { + let mut rid = [0u8; 32]; + let mut u_i = [0u8; 32]; + rng.fill_bytes(rid.as_mut_slice()); + rng.fill_bytes(u_i.as_mut_slice()); + Self { + sid: *sid, + sender: *sender, + rid, + u_i, + coeff_publics: coeff_publics.to_vec(), + precom: sch_precom, + } + } + + /// Deserialize a TshareDecommit from a message and verify it. + pub(crate) fn from_message(message: &Message, com: &TshareCommit) -> Result { + message.check_type(MessageType::Tshare(TshareMessageType::R2Decommit))?; + let tshare_decommit: TshareDecommit = deserialize!(&message.unverified_bytes)?; + tshare_decommit.verify(message.id(), message.from(), com)?; + Ok(tshare_decommit) + } + + pub(crate) fn commit(&self) -> Result { + let mut transcript = Transcript::new(b"TshareR1"); + transcript.append_message(b"decom", &serialize!(&self)?); + let mut hash = [0u8; 32]; + transcript.challenge_bytes(b"hashing r1", &mut hash); + Ok(TshareCommit { hash }) + } + + /// Verify this TshareDecommit against a commitment and expected + /// content. + fn verify( + &self, + sid: Identifier, + sender: ParticipantIdentifier, + com: &TshareCommit, + ) -> Result<()> { + // Check the commitment. + let rebuilt_com = self.commit()?; + if &rebuilt_com != com { + error!("decommitment does not match original commitment"); + return Err(InternalError::ProtocolError(Some(sender))); + } + + // Check the session ID and sender ID. + if self.sid != sid { + error!("Incorrect session ID"); + return Err(InternalError::ProtocolError(Some(sender))); + } + if self.sender != sender { + error!("Incorrect sender ID"); + return Err(InternalError::ProtocolError(Some(sender))); + } + + Ok(()) + } +} + +// Implement custom Debug to avoid leaking secret information. +impl std::fmt::Debug for TshareDecommit { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + f.debug_struct("TshareDecommit") + .field("sid", &self.sid) + .field("sender", &self.sender) + .field("coeff_publics", &self.coeff_publics) + .field("...", &"[redacted]") + .finish() + } +} diff --git a/src/tshare/input.rs b/src/tshare/input.rs new file mode 100644 index 00000000..c20143bd --- /dev/null +++ b/src/tshare/input.rs @@ -0,0 +1,232 @@ +// Copyright (c) 2022-2023 Bolt Labs Holdings, Inc +// +// This source code is licensed under both the MIT license found in the +// LICENSE-MIT file in the root directory of this source tree and the Apache +// License, Version 2.0 found in the LICENSE-APACHE file in the root directory +// of this source tree. +use std::collections::HashSet; + +use tracing::error; + +use crate::{ + auxinfo::{self, AuxInfoPrivate, AuxInfoPublic}, + errors::{CallerError, InternalError, Result}, + ParticipantConfig, ParticipantIdentifier, +}; + +use super::share::CoeffPrivate; + +/// Input needed for a +/// [`TshareParticipant`](crate::tshare::TshareParticipant) to run. +#[derive(Debug, Clone)] +pub struct Input { + /// How many parties are needed to sign. + threshold: usize, + /// An additive share to turn into Shamir sharing. + /// Or None to generate a random share. + share: Option, + /// The auxiliary info to encrypt/decrypt messages with other participants. + auxinfo_output: auxinfo::Output, +} + +impl Input { + /// Creates a new [`Input`] from the outputs of the + /// [`auxinfo`](crate::auxinfo::AuxInfoParticipant) and + /// [`keygen`](crate::keygen::KeygenParticipant) protocols. + pub fn new( + auxinfo_output: auxinfo::Output, + share: Option, + threshold: usize, + ) -> Result { + // The constructor for auxinfo output already check other important + // properties, like that the private component maps to one of public + // components for each one. + Ok(Self { + auxinfo_output, + share, + threshold, + }) + } + + /// Returns the share to be used in the protocol. + pub fn share(&self) -> Option<&CoeffPrivate> { + self.share.as_ref() + } + + /// Returns the threshold for the protocol. + pub fn threshold(&self) -> usize { + self.threshold + } + + /// Returns the participant IDs associated with the auxinfo output. + fn auxinfo_pids(&self) -> HashSet { + self.auxinfo_output + .public_auxinfo() + .iter() + .map(AuxInfoPublic::participant) + .collect() + } + + // Check the consistency of participant IDs. + pub(crate) fn check_participant_config(&self, config: &ParticipantConfig) -> Result<()> { + let config_pids = config + .all_participants() + .iter() + .cloned() + .collect::>(); + if config_pids != self.auxinfo_pids() { + error!("Public auxinfo and participant inputs weren't from the same set of parties."); + Err(CallerError::BadInput)? + } + + if config.id() != self.auxinfo_output.private_pid()? { + error!("Expected private auxinfo output and tshare input to correspond to the same participant, but they didn't"); + Err(CallerError::BadInput)? + } + + Ok(()) + } + + pub(crate) fn private_auxinfo(&self) -> &AuxInfoPrivate { + self.auxinfo_output.private_auxinfo() + } + + /// Returns the [`AuxInfoPublic`] associated with the given + /// [`ParticipantIdentifier`]. + pub(crate) fn find_auxinfo_public(&self, pid: ParticipantIdentifier) -> Result<&AuxInfoPublic> { + self.auxinfo_output.find_public(pid) + .ok_or_else(|| { + error!("Presign input doesn't contain a public auxinfo for {}, even though we checked for it at construction.", pid); + InternalError::InternalInvariantFailed + }) + } +} + +#[cfg(test)] +mod test { + use super::{super::TshareParticipant, Input}; + use crate::{ + auxinfo, + errors::{CallerError, InternalError, Result}, + utils::testing::init_testing, + Identifier, ParticipantConfig, ParticipantIdentifier, ProtocolParticipant, + }; + + #[test] + fn protocol_participants_must_match_input_participants() -> Result<()> { + let rng = &mut init_testing(); + let SIZE = 5; + + // Create valid input set with random PIDs + let config = ParticipantConfig::random(SIZE, rng); + let auxinfo_output = auxinfo::Output::simulate(&config.all_participants(), rng); + let input = Input::new(auxinfo_output, None, 2)?; + + // Create valid config with PIDs independent of those used to make the input set + let independent_config = ParticipantConfig::random(SIZE, rng); + + let result = TshareParticipant::new( + Identifier::random(rng), + independent_config.id(), + independent_config.other_ids().to_vec(), + input, + ); + assert!(result.is_err()); + assert_eq!( + result.unwrap_err(), + InternalError::CallingApplicationMistake(CallerError::BadInput) + ); + + Ok(()) + } + + #[test] + fn auxinfo_must_match_input_participants() -> Result<()> { + let rng = &mut init_testing(); + let SIZE = 5; + + // Create valid input set with random PIDs + let config = ParticipantConfig::random(SIZE, rng); + + // Create valid config with PIDs independent of those used to make the input set + let independent_config = ParticipantConfig::random(SIZE, rng); + + // Replace auxinfo_output with a new one that doesn't match the config + let auxinfo_output = auxinfo::Output::simulate(&independent_config.all_participants(), rng); + let input_with_invalid_auxinfo = Input::new(auxinfo_output, None, 2)?; + let result = TshareParticipant::new( + Identifier::random(rng), + config.id(), + config.other_ids().to_vec(), + input_with_invalid_auxinfo, + ); + assert!(result.is_err()); + assert_eq!( + result.unwrap_err(), + InternalError::CallingApplicationMistake(CallerError::BadInput) + ); + + Ok(()) + } + + #[test] + fn auxinfo_id_must_match_input_participants() -> Result<()> { + let rng = &mut init_testing(); + let SIZE = 5; + + // create quorum + let quorum = ParticipantConfig::random_quorum(SIZE, rng).unwrap(); + + // Replace auxinfo_output with a new one that doesn't match the config + let auxinfo_output = auxinfo::Output::simulate(&quorum[0].all_participants(), rng); + let input_auxinfo = Input::new(auxinfo_output, None, 2)?; + let result = TshareParticipant::new( + Identifier::random(rng), + quorum[1].id(), + quorum[1].other_ids().to_vec(), + input_auxinfo, + ); + assert!(result.is_err()); + assert_eq!( + result.unwrap_err(), + InternalError::CallingApplicationMistake(CallerError::BadInput) + ); + + Ok(()) + } + + #[test] + fn find_existing_auxinfo_public() -> Result<()> { + let rng = &mut init_testing(); + let SIZE = 5; + + // Create valid input set with random PIDs + let config = ParticipantConfig::random(SIZE, rng); + let auxinfo_output = auxinfo::Output::simulate(&config.all_participants(), rng); + let input = Input::new(auxinfo_output, None, 2)?; + + let pid = config.all_participants()[0]; + let auxinfo_public = input.find_auxinfo_public(pid)?; + assert_eq!(auxinfo_public.participant(), pid); + + Ok(()) + } + + #[test] + fn find_non_existing_auxinfo_public_should_fail() -> Result<()> { + let rng = &mut init_testing(); + let SIZE = 5; + + // Create valid input set with random PIDs + let config = ParticipantConfig::random(SIZE, rng); + let auxinfo_output = auxinfo::Output::simulate(&config.all_participants(), rng); + let input = Input::new(auxinfo_output, None, 2)?; + + let pid = ParticipantIdentifier::random(rng); + let result = input.find_auxinfo_public(pid); + assert!(result.is_err()); + assert_eq!(result.unwrap_err(), InternalError::InternalInvariantFailed); + + Ok(()) + } +} diff --git a/src/tshare/mod.rs b/src/tshare/mod.rs new file mode 100644 index 00000000..19adc5d3 --- /dev/null +++ b/src/tshare/mod.rs @@ -0,0 +1,19 @@ +//! Types and functions related to key refresh sub-protocol. +// Copyright (c) Facebook, Inc. and its affiliates. +// Modifications Copyright (c) 2022-2023 Bolt Labs Holdings, Inc +// +// This source code is licensed under both the MIT license found in the +// LICENSE-MIT file in the root directory of this source tree and the Apache +// License, Version 2.0 found in the LICENSE-APACHE file in the root directory +// of this source tree. + +mod commit; +mod input; +mod output; +mod participant; +mod share; + +pub use input::Input; +pub use output::Output; +pub use participant::TshareParticipant; +pub use share::{CoeffPrivate, CoeffPublic}; diff --git a/src/tshare/output.rs b/src/tshare/output.rs new file mode 100644 index 00000000..2b2cd464 --- /dev/null +++ b/src/tshare/output.rs @@ -0,0 +1,221 @@ +// Copyright (c) 2022-2023 Bolt Labs Holdings, Inc +// +// This source code is licensed under both the MIT license found in the +// LICENSE-MIT file in the root directory of this source tree and the Apache +// License, Version 2.0 found in the LICENSE-APACHE file in the root directory +// of this source tree. + +use k256::Scalar; +use std::collections::HashSet; + +use crate::{ + errors::{CallerError, InternalError, Result}, + keygen::KeySharePublic, + utils::CurvePoint, +}; + +use k256::ecdsa::VerifyingKey; +use tracing::error; + +use super::CoeffPublic; + +/// Output type from key generation, including all parties' public key shares, +/// this party's private key share, and the public coefficients from the +/// subjacent Lagrange interpolation. +#[derive(Debug, Clone)] +pub struct Output { + // Public coefficients for the polynomial + public_coeffs: Vec, + // Public keys for each participant + public_key_shares: Vec, + //private_key_share: KeySharePrivate, + private_key_share: Scalar, +} + +impl Output { + /// Construct the generated public key. + pub fn public_key(&self) -> Result { + // Add up all the key shares + let public_key_point = self + .public_key_shares + .iter() + .fold(CurvePoint::IDENTITY, |sum, share| sum + *share.as_ref()); + + VerifyingKey::from_encoded_point(&public_key_point.into()).map_err(|_| { + error!("Keygen output does not produce a valid public key."); + InternalError::InternalInvariantFailed + }) + } + + /// Get the individual shares of the public key. + pub fn public_key_shares(&self) -> &[KeySharePublic] { + &self.public_key_shares + } + + /// Get the public coefficients (coefficients in the exponent). + pub fn public_coeffs(&self) -> &[CoeffPublic] { + &self.public_coeffs + } + + /// Get the private share + pub fn private_key_share(&self) -> &Scalar { + &self.private_key_share + } + + /// Create a new `Output` from its constitutent parts. + /// + /// This method should only be used with components that were previously + /// derived via the [`Output::into_parts()`] method; the calling application + /// should not try to form public and private key shares independently. + /// + /// The provided components must satisfy the following properties: + /// - Validity of private key share can be checked using Feldman's VSS. + /// - The public key shares must be from a unique set of participants. + pub fn from_parts( + public_coeffs: Vec, + public_keys: Vec, + private_key_share: Scalar, + ) -> Result { + let pids = public_keys + .iter() + .map(KeySharePublic::participant) + .collect::>(); + if pids.len() != public_keys.len() { + error!("Tried to create a keygen output using a set of public material from non-unique participants"); + Err(CallerError::BadInput)? + } + if pids.len() < public_coeffs.len() { + error!("Not enough participants to support the given polynomial"); + Err(CallerError::BadInput)? + } + + Ok(Self { + public_coeffs, + public_key_shares: public_keys, + private_key_share, + }) + } + + /// Decompose the `Output` into its constituent parts. + /// + /// # 🔒 Storage requirements + /// The private_key_share must be stored securely by the calling + /// application, and a best effort should be made to drop it from memory + /// after it's securely stored. + /// + /// The public components (including the byte array and the public key + /// shares) can be stored in the clear. + pub fn into_parts(self) -> (Vec, Vec, Scalar) { + ( + self.public_coeffs, + self.public_key_shares, + self.private_key_share, + ) + } +} + +#[cfg(test)] +mod tests { + use super::*; + use crate::{ + tshare::{CoeffPrivate, CoeffPublic, TshareParticipant}, + utils::{bn_to_scalar, k256_order, testing::init_testing}, + ParticipantIdentifier, + }; + use itertools::Itertools; + use k256::elliptic_curve::Field; + use libpaillier::unknown_order::BigNumber; + + impl Output { + /// Simulate the valid output of a keygen run with the given + /// participants. + /// + /// This should __never__ be called outside of tests! The given `pids` + /// must not contain duplicates. Self is the last participant in `pids`. + pub(crate) fn simulate(pids: &[ParticipantIdentifier]) -> Self { + let (private_key_shares, public_key_shares): (Vec<_>, Vec<_>) = pids + .iter() + .map(|&pid| { + // TODO #340: Replace with KeyShare methods once they exist. + let secret = BigNumber::random(&k256_order()); + let public = CurvePoint::GENERATOR + .multiply_by_bignum(&secret) + .expect("can't multiply by generator"); + (secret, KeySharePublic::new(pid, public)) + }) + .unzip(); + + // simulate a random evaluation + let converted_publics = public_key_shares + .iter() + .map(|x| CoeffPublic::new(*x.as_ref())) + .collect::>(); + let converted_privates = private_key_shares + .iter() + .map(|x| CoeffPrivate { + x: bn_to_scalar(x).unwrap(), + }) + .collect::>(); + let eval_public_at_first_pid = + TshareParticipant::eval_public_share(converted_publics.as_slice(), pids[0]) + .unwrap(); + let eval_private_at_first_pid = + TshareParticipant::eval_private_share(converted_privates.as_slice(), pids[0]); + let output = Self::from_parts( + converted_publics, + public_key_shares, + eval_private_at_first_pid.x, + ) + .unwrap(); + + let implied_public = eval_private_at_first_pid.public_point(); + assert!(implied_public == eval_public_at_first_pid); + output + } + } + + #[test] + fn from_into_parts_works() { + let rng = &mut init_testing(); + let pids = std::iter::repeat_with(|| ParticipantIdentifier::random(rng)) + .take(5) + .collect::>(); + let output = Output::simulate(&pids); + + let (public_coeffs, public_keys, private_key) = output.into_parts(); + assert!(Output::from_parts(public_coeffs, public_keys, private_key).is_ok()); + } + + #[test] + fn public_shares_must_not_have_duplicate_pids() { + let mut rng = &mut init_testing(); + let mut pids = std::iter::repeat_with(|| ParticipantIdentifier::random(rng)) + .take(5) + .collect::>(); + + // Duplicate one of the PIDs + pids.push(pids[4]); + + // Form output with the duplicated PID + let (mut private_key_shares, public_key_shares, public_coeffs): (Vec<_>, Vec<_>, Vec<_>) = + pids.iter() + .map(|&pid| { + // TODO #340: Replace with KeyShare methods once they exist. + let secret = Scalar::random(&mut rng); + let public = CurvePoint::GENERATOR.multiply_by_scalar(&secret); + ( + secret, + KeySharePublic::new(pid, public), + CoeffPublic::new(public), + ) + }) + .multiunzip(); + + assert!(Output::from_parts( + public_coeffs, + public_key_shares, + private_key_shares.pop().unwrap() + ) + .is_err()); + } +} diff --git a/src/tshare/participant.rs b/src/tshare/participant.rs new file mode 100644 index 00000000..5647ff3f --- /dev/null +++ b/src/tshare/participant.rs @@ -0,0 +1,1229 @@ +//! Types and functions related to the key refresh sub-protocol Participant. + +// Copyright (c) Facebook, Inc. and its affiliates. +// Modifications Copyright (c) 2022-2023 Bolt Labs Holdings, Inc +// +// This source code is licensed under both the MIT license found in the +// LICENSE-MIT file in the root directory of this source tree and the Apache +// License, Version 2.0 found in the LICENSE-APACHE file in the root directory +// of this source tree. + +use std::collections::HashMap; + +use super::{ + commit::{TshareCommit, TshareDecommit}, + share::{CoeffPrivate, CoeffPublic, EvalEncrypted, EvalPrivate}, +}; +use crate::{ + broadcast::participant::{BroadcastOutput, BroadcastParticipant, BroadcastTag}, + errors::{CallerError, InternalError, Result}, + keygen::{KeySharePrivate, KeySharePublic, KeygenParticipant}, + local_storage::LocalStorage, + messages::{Message, MessageType, TshareMessageType}, + participant::{ + Broadcast, InnerProtocolParticipant, ProcessOutcome, ProtocolParticipant, Status, + }, + protocol::{ParticipantIdentifier, ProtocolType, SharedContext}, + run_only_once, + tshare::share::EvalPublic, + utils::{bn_to_scalar, scalar_to_bn, CurvePoint}, + zkp::pisch::{CommonInput, PiSchPrecommit, PiSchProof, ProverSecret}, + Identifier, ParticipantConfig, +}; + +use k256::{elliptic_curve::PrimeField, Scalar}; +use libpaillier::unknown_order::BigNumber; +use merlin::Transcript; +use rand::{CryptoRng, RngCore}; +use tracing::{error, info, instrument, warn}; + +use super::{input::Input, output::Output}; + +mod storage { + use super::*; + use crate::{local_storage::TypeTag, tshare::share::EvalPublic}; + + pub(super) struct Commit; + impl TypeTag for Commit { + type Value = TshareCommit; + } + pub(super) struct Decommit; + impl TypeTag for Decommit { + type Value = TshareDecommit; + } + pub(super) struct SchnorrPrecom; + impl TypeTag for SchnorrPrecom { + type Value = PiSchPrecommit; + } + pub(super) struct GlobalRid; + impl TypeTag for GlobalRid { + type Value = [u8; 32]; + } + pub(super) struct PrivateCoeffs; + impl TypeTag for PrivateCoeffs { + type Value = Vec; + } + pub(super) struct PublicCoeffs; + impl TypeTag for PublicCoeffs { + type Value = Vec; + } + pub(super) struct ValidPublicShare; + impl TypeTag for ValidPublicShare { + type Value = EvalPublic; + } + pub(super) struct ValidPrivateEval; + impl TypeTag for ValidPrivateEval { + type Value = super::EvalPrivate; + } +} + +/** +This is a protocol that converts additive shares to Shamir shares. + +Input: +- The threshold `t` of parties needed to reconstruct the shared secret. +- The auxiliary information for encryption. +- Optionally, an existing n-out-of-n share to be converted to t-out-of-n. + +Rounds 1: +- Each participant generates a random polynomial of degree `threshold - 1`. + - Alternatively, set an existing additive share as the constant term. +- Each participant commits to their polynomial. + +Rounds 2: +- Each participant decommits the public form of their polynomial. + +Rounds 3: +- Each participant prove knowledge of their private shares. + +Output: +- The public commitment to the shared polynomial. It is represented in coefficients form in the exponent (EC points). +The constant term corresponds to the shared value. This can be used to evaluate the commitment to the share of any participant. +- The private evaluation of the shared polynomial for our participant. `t` of those can reconstruct the secret. + +*/ +#[derive(Debug)] +pub struct TshareParticipant { + /// The current session identifier + sid: Identifier, + /// The current protocol input. + input: Input, + /// A unique identifier for this participant. + id: ParticipantIdentifier, + /// A list of all other participant identifiers participating in the + /// protocol + other_participant_ids: Vec, + /// Local storage for this participant to store secrets + local_storage: LocalStorage, + /// Broadcast subprotocol handler + broadcast_participant: BroadcastParticipant, + /// Status of the protocol execution. + status: Status, +} + +impl ProtocolParticipant for TshareParticipant { + type Input = Input; + type Output = Output; + + fn new( + sid: Identifier, + id: ParticipantIdentifier, + other_participant_ids: Vec, + input: Self::Input, + ) -> Result { + input.check_participant_config(&ParticipantConfig::new(id, &other_participant_ids)?)?; + + Ok(Self { + sid, + input, + id, + other_participant_ids: other_participant_ids.clone(), + local_storage: Default::default(), + broadcast_participant: BroadcastParticipant::new(sid, id, other_participant_ids, ())?, + status: Status::NotReady, + }) + } + + fn ready_type() -> MessageType { + MessageType::Tshare(TshareMessageType::Ready) + } + + fn protocol_type() -> ProtocolType { + ProtocolType::Tshare + } + + fn id(&self) -> ParticipantIdentifier { + self.id + } + + fn other_ids(&self) -> &[ParticipantIdentifier] { + &self.other_participant_ids + } + + fn sid(&self) -> Identifier { + self.sid + } + + #[cfg_attr(feature = "flame_it", flame("tshare"))] + #[instrument(skip_all)] + fn process_message( + &mut self, + rng: &mut R, + message: &Message, + ) -> Result> { + info!( + "TSHARE: Player {}: received {:?} from {}", + self.id(), + message.message_type(), + message.from() + ); + + if *self.status() == Status::TerminatedSuccessfully { + Err(CallerError::ProtocolAlreadyTerminated)?; + } + + if !self.status().is_ready() && message.message_type() != Self::ready_type() { + self.stash_message(message)?; + return Ok(ProcessOutcome::Incomplete); + } + + match message.message_type() { + MessageType::Tshare(TshareMessageType::Ready) => self.handle_ready_msg(rng, message), + MessageType::Tshare(TshareMessageType::R1CommitHash) => { + let broadcast_outcome = self.handle_broadcast(rng, message)?; + + // Handle the broadcasted message if all parties have agreed on it + broadcast_outcome.convert(self, Self::handle_round_one_msg, rng) + } + MessageType::Tshare(TshareMessageType::R2Decommit) => { + self.handle_round_two_msg(message) + } + MessageType::Tshare(TshareMessageType::R2PrivateShare) => { + self.handle_round_two_msg_private(message) + } + MessageType::Tshare(TshareMessageType::R3Proof) => self.handle_round_three_msg(message), + message_type => { + error!( + "Incorrect MessageType given to TshareParticipant. Got: {:?}", + message_type + ); + Err(InternalError::InternalInvariantFailed) + } + } + } + + fn status(&self) -> &Status { + &self.status + } +} + +impl InnerProtocolParticipant for TshareParticipant { + type Context = SharedContext; + + fn retrieve_context(&self) -> ::Context { + SharedContext::collect(self) + } + + fn local_storage(&self) -> &LocalStorage { + &self.local_storage + } + + fn local_storage_mut(&mut self) -> &mut LocalStorage { + &mut self.local_storage + } + + fn status_mut(&mut self) -> &mut Status { + &mut self.status + } +} + +impl Broadcast for TshareParticipant { + fn broadcast_participant(&mut self) -> &mut BroadcastParticipant { + &mut self.broadcast_participant + } +} + +impl TshareParticipant { + /// Handle "Ready" messages from the protocol participants. + /// + /// Once "Ready" messages have been received from all participants, this + /// method will trigger this participant to generate its round one message. + #[cfg_attr(feature = "flame_it", flame("tshare"))] + #[instrument(skip_all, err(Debug))] + fn handle_ready_msg( + &mut self, + rng: &mut R, + message: &Message, + ) -> Result::Output>> { + info!("Handling ready tshare message."); + + let ready_outcome = self.process_ready_message(rng, message)?; + let round_one_messages = run_only_once!(self.gen_round_one_msgs(rng, message.id()))?; + // extend the output with r1 messages (if they hadn't already been generated) + Ok(ready_outcome.with_messages(round_one_messages)) + } + + /// Generate the protocol's round one message. + /// + /// The outcome is a broadcast message containing a commitment to: + /// - shares [`CoeffPublic`] X_ij for all other participants, + /// - a "pre-commitment" A to a Schnorr proof. + #[cfg_attr(feature = "flame_it", flame("tshare"))] + #[instrument(skip_all, err(Debug))] + fn gen_round_one_msgs( + &mut self, + rng: &mut R, + sid: Identifier, + ) -> Result> { + info!("Generating round one tshare messages."); + + // Generate shares for all participants. + let (coeff_privates, coeff_publics) = { + let mut privates = vec![]; + let mut publics = vec![]; + + for _ in 0..self.input.threshold() { + let (private, public) = CoeffPublic::new_pair(rng)?; + privates.push(private); + publics.push(public); + } + + if let Some(private) = self.input.share() { + privates[0] = private.clone(); + publics[0] = private.to_public(); + } + + (privates, publics) + }; + + // Generate proof precommitments. + let sch_precom = PiSchProof::precommit(rng)?; + + // Store the beginning of our proofs so we can continue the proofs later. + self.local_storage + .store::(self.id(), sch_precom.clone()); + + let decom = TshareDecommit::new( + rng, + &sid, + &self.id(), + &coeff_publics, + *sch_precom.precommitment(), + ); + + // Store the private coeffs from us to others so we can share them later. + self.local_storage + .store::(self.id(), coeff_privates); + + // Store the public coeffs from us to others so we can share them later. + self.local_storage + .store::(self.id(), coeff_publics); + + let com = decom.commit()?; + let com_bytes = serialize!(&com)?; + self.local_storage.store::(self.id(), com); + + // Store our committed values so we can open the commitment later. + self.local_storage + .store::(self.id(), decom); + + let messages = self.broadcast( + rng, + MessageType::Tshare(TshareMessageType::R1CommitHash), + com_bytes, + sid, + BroadcastTag::TshareR1CommitHash, + )?; + Ok(messages) + } + + /// Handle round one messages from the protocol participants. + /// + /// In round one, each participant broadcasts its commitment to its public + /// key share and a "precommitment" to a Schnorr proof. Once all such + /// commitments have been received, this participant will send an opening of + /// its own commitment to all other parties. + #[cfg_attr(feature = "flame_it", flame("tshare"))] + #[instrument(skip_all, err(Debug))] + fn handle_round_one_msg( + &mut self, + rng: &mut R, + broadcast_message: BroadcastOutput, + ) -> Result::Output>> { + let message = broadcast_message.into_message(BroadcastTag::TshareR1CommitHash)?; + + self.check_for_duplicate_msg::(message.from())?; + info!("Handling round one tshare message."); + + let tshare_commit = TshareCommit::from_message(&message)?; + self.local_storage + .store_once::(message.from(), tshare_commit)?; + + // Check if we've received all the commits, which signals an end to + // round one. + // + // Note: This does _not_ check `self.all_participants` on purpose. There + // could be a setting where we've received all the round one messages + // from all other participants, yet haven't ourselves generated our + // round one message. If we switched to `self.all_participants` here + // then the result would be `false`, causing the execution to hang. + // + // The "right" solution would be to only process the message once the + // "Ready" round is complete, and stashing messages if it is not yet + // complete (a la how we do it in `handle_round_two_msg`). + // Unfortunately, this does not work given the current API because we + // are dealing with a [`BroadcastOutput`] type instead of a [`Message`] + // type. + let r1_done = self + .local_storage + .contains_for_all_ids::(self.other_ids()); + + if r1_done { + // Finish round 1 by generating messages for round 2 + let round_one_messages = run_only_once!(self.gen_round_two_msgs(rng, message.id()))?; + + let mut outcomes = self + .fetch_messages(MessageType::Tshare(TshareMessageType::R2PrivateShare))? + .iter() + .map(|msg| self.handle_round_two_msg_private(msg)) + .collect::>>()?; + + // Process any round 2 messages we may have received early + let round_two_outcomes = self + .fetch_messages(MessageType::Tshare(TshareMessageType::R2Decommit))? + .iter() + .map(|msg| self.handle_round_two_msg(msg)) + .collect::>>()?; + + outcomes.extend(round_two_outcomes); + + ProcessOutcome::collect_with_messages(outcomes, round_one_messages) + } else { + // Otherwise, wait for more round 1 messages + Ok(ProcessOutcome::Incomplete) + } + } + + /// Generate the protocol's round two messages. + /// + /// The outcome is an opening to the commitment generated in round one. + #[cfg_attr(feature = "flame_it", flame("tshare"))] + #[instrument(skip_all, err(Debug))] + fn gen_round_two_msgs( + &mut self, + rng: &mut R, + sid: Identifier, + ) -> Result> { + info!("Generating round two tshare messages."); + + let mut messages = vec![]; + // Check that we've generated our share before trying to retrieve it. + // + // Because we are not checking `self.all_participants` in + // `handle_round_one_msg`, we may reach this point and not actually have + // generated round one messages for ourselves (in particular, + // `CoeffPublic` and `Decommit`). This check forces that behavior. + // Without it we'll get a `InternalInvariantFailed` error when trying to + // retrieve `Decommit` below. + if !self.local_storage.contains::(self.id()) { + let more_messages = run_only_once!(self.gen_round_one_msgs(rng, sid))?; + messages.extend_from_slice(&more_messages); + } + + let decom = self + .local_storage + .retrieve::(self.id())?; + let more_messages = self.message_for_other_participants( + MessageType::Tshare(TshareMessageType::R2Decommit), + decom, + )?; + //messages.extend_from_slice(&more_messages); + + let private_coeffs = self + .local_storage + .retrieve::(self.id())?; + + // Encrypt the private shares to each participant. + let encrypted_shares = self + .other_ids() + .iter() + .map(|other_participant_id| { + let private_share = Self::eval_private_share(private_coeffs, *other_participant_id); + + let auxinfo = self.input.find_auxinfo_public(*other_participant_id)?; + EvalEncrypted::encrypt(&private_share, auxinfo.pk(), rng) + }) + .collect::>>()?; + + // Send their private shares to each individual participant. + messages.extend( + self.other_ids() + .iter() + .zip(encrypted_shares.iter()) + .map(|(other_participant_id, encrypted_share)| { + Message::new( + MessageType::Tshare(TshareMessageType::R2PrivateShare), + self.sid(), + self.id(), + *other_participant_id, + encrypted_share, + ) + }) + .collect::>>()?, + ); + messages.extend_from_slice(&more_messages); + + Ok(messages) + } + + /// Handle the protocol's round two private messages. + /// + /// Here we validate and store a private share from someone to us. + #[cfg_attr(feature = "flame_it", flame("tshare"))] + #[instrument(skip_all, err(Debug))] + fn handle_round_two_msg_private( + &mut self, + message: &Message, + ) -> Result::Output>> { + self.check_for_duplicate_msg::(message.from())?; + + info!("Handling round two tshare private message."); + + message.check_type(MessageType::Tshare(TshareMessageType::R2PrivateShare))?; + let encrypted_share: EvalEncrypted = deserialize!(&message.unverified_bytes)?; + + // Get my private key from the AuxInfo protocol. + let my_dk = self.input.private_auxinfo().decryption_key(); + + // Decrypt the private share. + let private_share = encrypted_share.decrypt(my_dk)?; + // Write the private share to the storage + self.local_storage + .store_once::(message.from(), private_share)?; + + Ok(self + .maybe_finish_round2() + .expect("Could not finish round 2")) + } + + /// Handle the protocol's round two messages. + /// + /// Here we check that the decommitments from each participant are valid. + #[cfg_attr(feature = "flame_it", flame("tshare"))] + #[instrument(skip_all, err(Debug))] + fn handle_round_two_msg( + &mut self, + message: &Message, + ) -> Result::Output>> { + self.check_for_duplicate_msg::(message.from())?; + info!("Handling round two tshare message."); + + // We must receive all commitments in round 1 before we start processing + // decommits in round 2. + let r1_done = self + .local_storage + .contains_for_all_ids::(&self.all_participants()); + if !r1_done { + // Store any early round 2 messages + self.stash_message(message)?; + return Ok(ProcessOutcome::Incomplete); + } + // Check that the decommitment contained in the message is valid against + // the previously received commitment and protocol rules. + let com = self + .local_storage + .retrieve::(message.from())?; + let decom = TshareDecommit::from_message(message, com)?; + self.local_storage + .store_once::(message.from(), decom.clone())?; + self.local_storage + .store::(message.from(), decom.coeff_publics); + + Ok(self + .maybe_finish_round2() + .expect("Could not finish round 2")) + } + + fn maybe_finish_round2( + &mut self, + ) -> Result::Output>> { + let got_all_private_shares = self + .local_storage + .contains_for_all_ids::(self.other_ids()); + + // Check if we've received all the decommits + let mut r2_done = self + .local_storage + .contains_for_all_ids::(&self.all_participants()); + + r2_done &= got_all_private_shares; + + if r2_done { + // for each participant, read the private share and check if it matches the + // public share + for pid in self.other_ids() { + let decom = self.local_storage.retrieve::(*pid)?; + let coeff_publics = decom.coeff_publics.clone(); + let expected_public = Self::eval_public_share(coeff_publics.as_slice(), self.id())?; + let private_share = self + .local_storage + .retrieve::(*pid)?; + let implied_public = private_share.public_point(); + if implied_public != expected_public { + error!("the private share does not match the public share"); + return Err(InternalError::ProtocolError(Some(*pid))); + } + } + + // Generate messages for round 3... + let round_three_messages = run_only_once!(self.gen_round_three_msgs())?; + + // ...and handle any messages that other participants have sent for round 3. + let round_three_outcomes = self + .fetch_messages(MessageType::Tshare(TshareMessageType::R3Proof))? + .iter() + .map(|msg| self.handle_round_three_msg(msg)) + .collect::>>()?; + + ProcessOutcome::collect_with_messages(round_three_outcomes, round_three_messages) + } else { + // Otherwise, wait for more round 2 messages. + Ok(ProcessOutcome::Incomplete) + } + } + + /// Generate the protocol's round three messages. + /// + /// At this point, we have validated each participant's commitment, and can + /// now proceed to constructing a Schnorr proof that this participant knows + /// the private value corresponding to its public key share. + #[cfg_attr(feature = "flame_it", flame("tshare"))] + #[instrument(skip_all, err(Debug))] + fn gen_round_three_msgs(&mut self) -> Result> { + info!("Generating round three tshare messages."); + + // Construct `global rid` out of each participant's `rid`s. + let my_rid = self + .local_storage + .retrieve::(self.id())? + .rid; + let rids: Vec<[u8; 32]> = self + .other_ids() + .iter() + .map(|&other_participant_id| { + let decom = self + .local_storage + .retrieve::(other_participant_id)?; + Ok(decom.rid) + }) + .collect::>>()?; + let mut global_rid = my_rid; + // xor all the rids together. + for rid in rids.iter() { + for i in 0..32 { + global_rid[i] ^= rid[i]; + } + } + self.local_storage + .store::(self.id(), global_rid); + + let transcript = schnorr_proof_transcript(self.sid(), &global_rid, self.id())?; + + let private_coeffs = self + .local_storage + .retrieve::(self.id())?; + + let my_private_share = Self::eval_private_share(private_coeffs, self.id()); + let my_contant_term = Self::eval_private_share_at_zero(private_coeffs); + if let Some(private) = self.input.share() { + assert_eq!(my_contant_term, private.x); + } + + // Have we got the private shares from everybody to us? + let got_all_private_shares = self + .local_storage + .contains_for_all_ids::(self.other_ids()); + + if got_all_private_shares { + // Compute the one's own private evaluation. + + // Get a slice of EvalPrivate from other participants + let mut from_all_to_me_private = vec![]; + for pid in self.other_ids() { + let private_share = self + .local_storage + .retrieve::(*pid)?; + from_all_to_me_private.push(private_share.clone()); + } + + let other_private_shares = Self::aggregate_private_shares(&from_all_to_me_private); + let final_private_share = other_private_shares + &my_private_share; + let final_public_share = EvalPublic::new(final_private_share.public_point()); + + // Generate proofs for each share. + let precom = self + .local_storage + .retrieve::(self.id())?; + + let pk = &final_public_share; + let input = CommonInput::new(pk); + let sk = &final_private_share.x; + + let proof = PiSchProof::prove_from_precommit( + &self.retrieve_context(), + precom, + &input, + &ProverSecret::new(&scalar_to_bn(sk)), + &transcript, + )?; + + self.local_storage + .store::(self.id(), final_private_share.clone()); + self.local_storage + .store::(self.id(), final_public_share.clone()); + + // Send all proofs to everybody. + let messages = self.message_for_other_participants( + MessageType::Tshare(TshareMessageType::R3Proof), + proof, + )?; + + Ok(messages) + } else { + Err(InternalError::ProtocolError(None)) + } + } + + /// Assign a non-null x coordinate to each participant. + fn participant_coordinate(pid: ParticipantIdentifier) -> Scalar { + Scalar::from_u128(pid.as_u128()) + Scalar::ONE + } + + /// Evaluate the private share + pub fn eval_private_share( + coeff_privates: &[CoeffPrivate], + recipient_id: ParticipantIdentifier, + ) -> EvalPrivate { + let x = Self::participant_coordinate(recipient_id); + assert!(x > Scalar::ZERO); + let mut sum = Scalar::ZERO; + for coeff in coeff_privates.iter().rev() { + sum *= &x; + sum += &coeff.x; + } + EvalPrivate { x: sum } + } + + /// Evaluate the private share at the point 0. + fn eval_private_share_at_zero(coeff_privates: &[CoeffPrivate]) -> Scalar { + coeff_privates[0].x + } + + /// Feldman VSS evaluation of the public share. + /// This algorithm is slow. Consider using MSMs. + pub(crate) fn eval_public_share( + coeff_publics: &[CoeffPublic], + recipient_id: ParticipantIdentifier, + ) -> Result { + let x = Self::participant_coordinate(recipient_id); + let mut sum = CurvePoint::IDENTITY; + for coeff in coeff_publics.iter().rev() { + sum = sum.multiply_by_scalar(&x); + sum = sum + *coeff.as_ref(); + } + Ok(sum) + } + + /// Convert the tshares to t-out-of-t shares. + /// This is done by multiplying the shares by the Lagrange coefficients. + /// Since the constant term is the secret, we need to multiply by the + /// Lagrange coefficient at zero. This is done by the function + /// `lagrange_coefficient_at_zero`. + /// Also convert the public tshares in the same way as the private shares. + /// Finally a vector of all public keys is returned. + #[allow(clippy::type_complexity)] + pub fn convert_to_t_out_of_t_shares( + tshares: HashMap, + all_participants: Vec, + chain_code: [u8; 32], + rid: [u8; 32], + ) -> Result<( + HashMap::Output>, + Vec, + )> { + let mut new_private_shares = HashMap::new(); + let mut public_keys = vec![]; + + // Compute the new private shares and public keys. + for pid in tshares.keys() { + if all_participants.contains(pid) { + let output = tshares.get(pid).unwrap(); + let private_key = output.private_key_share(); + let private_share = KeySharePrivate::from_bigint(&scalar_to_bn(private_key)); + let public_share = CurvePoint::GENERATOR.multiply_by_scalar(private_key); + let lagrange = Self::lagrange_coefficient_at_zero(pid, &all_participants); + let new_private_share: BigNumber = + private_share.clone().as_ref() * BigNumber::from_slice(lagrange.to_bytes()); + let new_public_share = public_share.as_ref().multiply_by_scalar(&lagrange); + assert!(new_private_shares + .insert(*pid, KeySharePrivate::from_bigint(&new_private_share)) + .is_none()); + public_keys.push(KeySharePublic::new(*pid, new_public_share)); + } + } + + // Compute the new outputs + let mut keygen_outputs: HashMap< + ParticipantIdentifier, + ::Output, + > = HashMap::new(); + for (pid, private_key_share) in new_private_shares { + let output = crate::keygen::Output::from_parts( + public_keys.clone(), + private_key_share, + chain_code, + rid, + )?; + assert!(keygen_outputs.insert(pid, output).is_none()); + } + Ok((keygen_outputs, public_keys)) + } + + /// Reconstruct the secret from the shares. + /// Use lagrange_coefficients_at_zero to get the constant term. + pub fn reconstruct_secret( + shares: HashMap, + all: Vec, + ) -> Result { + let mut secret = Scalar::ZERO; + // compute the coordinates + for (id, share) in shares.iter() { + let lagrange = Self::lagrange_coefficient_at_zero(id, &all); + let t_out_of_t_share = lagrange * bn_to_scalar(share.clone().as_ref()).unwrap(); + secret += t_out_of_t_share; + } + Ok(secret) + } + + /// Compute the Lagrange coefficient evaluated at zero. + /// This is used to reconstruct the secret from the shares. + pub fn lagrange_coefficient_at_zero( + my_point: &ParticipantIdentifier, + other_points: &Vec, + ) -> Scalar { + let mut result = Scalar::ONE; + for point in other_points { + if point != my_point { + let point_coordinate = &Self::participant_coordinate(*point); + let my_point_coordinate = &Self::participant_coordinate(*my_point); + let numerator = Scalar::ZERO - point_coordinate; + let denominator = my_point_coordinate - point_coordinate; + let inv = denominator.invert().unwrap(); + result *= numerator * inv; + } + } + result + } + + /// Handle round three messages only after our own `gen_round_three_msgs`. + fn can_handle_round_three_msg(&self) -> bool { + self.local_storage.contains::(self.id()) + } + + /// Handle the protocol's round three public messages. + /// + /// Here we validate the Schnorr proofs from each participant. + #[cfg_attr(feature = "flame_it", flame("tshare"))] + #[instrument(skip_all, err(Debug))] + fn handle_round_three_msg( + &mut self, + message: &Message, + ) -> Result::Output>> { + self.check_for_duplicate_msg::(message.from())?; + + if !self.can_handle_round_three_msg() { + info!("Not yet ready to handle round three tshare broadcast message."); + self.stash_message(message)?; + return Ok(ProcessOutcome::Incomplete); + } + info!("Handling round three tshare broadcast message."); + + let global_rid = *self + .local_storage + .retrieve::(self.id())?; + + let proof = PiSchProof::from_message(message)?; + let decom = self + .local_storage + .retrieve::(message.from())?; + + let mut final_public_share = CurvePoint::IDENTITY; + for pid in self.all_participants().iter() { + let coeff_publics = self.local_storage.retrieve::(*pid)?; + let public_share = Self::eval_public_share(coeff_publics, message.from())?; + final_public_share = final_public_share + public_share; + } + + let mut transcript = schnorr_proof_transcript(self.sid(), &global_rid, message.from())?; + proof.verify_with_precommit( + CommonInput::new(&final_public_share), + &self.retrieve_context(), + &mut transcript, + &decom.precom, + )?; + + let final_public_share = EvalPublic::new(final_public_share); + // Only if the proof verifies do we store the participant's shares. + self.local_storage + .store_once::(message.from(), final_public_share.clone())?; + + self.maybe_finish_protocol() + } + + fn maybe_finish_protocol( + &mut self, + ) -> Result::Output>> { + // Have we validated and stored the public shares from everybody to everybody? + let got_all_public_shares = self + .local_storage + .contains_for_all_ids::(&self.all_participants()); + + // If so, we completed the protocol! Return the outputs. + if got_all_public_shares { + // Compute the public polynomial. + let coeffs_from_all = self + .all_participants() + .iter() + .map(|pid| self.local_storage.remove::(*pid)) + .collect::>>()?; + let all_public_coeffs = Self::aggregate_public_coeffs(&coeffs_from_all); + + // Read my_private_share from the storage, it was already aggregated in the end + // of round 2 + let my_private_share = self + .local_storage + .retrieve::(self.id())?; + + // Double-check that the aggregated private share matches the aggregated public + // coeffs. + let my_public_share = my_private_share.public_point(); + let mut all_public_keys = vec![]; + for pid in self.other_participant_ids.iter() { + let public_share = Self::eval_public_share(&all_public_coeffs, *pid)?; + let public_share = KeySharePublic::new(*pid, public_share); + all_public_keys.push(public_share); + } + all_public_keys.push(KeySharePublic::new(self.id(), my_public_share)); + + let output = Output::from_parts( + all_public_coeffs.clone(), + all_public_keys, + my_private_share.x, + )?; + + self.status = Status::TerminatedSuccessfully; + Ok(ProcessOutcome::Terminated(output)) + } else { + // Otherwise, we'll have to wait for more round three messages. + Ok(ProcessOutcome::Incomplete) + } + } + + fn aggregate_private_shares(private_shares: &[EvalPrivate]) -> EvalPrivate { + EvalPrivate::sum(private_shares) + } + + /// Return coeffs.sum(axis=0) + fn aggregate_public_coeffs(coeffs_from_all: &[Vec]) -> Vec { + let n_coeffs = coeffs_from_all[0].len(); + (0..n_coeffs) + .map(|i| { + let sum = coeffs_from_all + .iter() + .fold(CurvePoint::IDENTITY, |sum, coeffs| { + sum + *coeffs[i].as_ref() + }); + CoeffPublic::new(sum) + }) + .collect() + } +} + +/// Generate a [`Transcript`] for [`PiSchProof`]. +fn schnorr_proof_transcript( + sid: Identifier, + global_rid: &[u8; 32], + sender_id: ParticipantIdentifier, +) -> Result { + let mut transcript = Transcript::new(b"tshare schnorr"); + transcript.append_message(b"sid", &serialize!(&sid)?); + transcript.append_message(b"rid", &serialize!(global_rid)?); + transcript.append_message(b"sender_id", &serialize!(&sender_id)?); + Ok(transcript) +} + +#[cfg(test)] +mod tests { + use super::{super::input::Input, *}; + use crate::{auxinfo, utils::testing::init_testing_with_seed, Identifier, ParticipantConfig}; + use k256::elliptic_curve::{Field, PrimeField}; + use rand::{thread_rng, CryptoRng, Rng, RngCore}; + use std::{collections::HashMap, iter::zip}; + use tracing::debug; + + impl TshareParticipant { + pub fn new_quorum( + sid: Identifier, + quorum_size: usize, + share: Option, + rng: &mut R, + ) -> Result> { + // Prepare prereqs for making TshareParticipant's. Assume all the + // simulations are stable (e.g. keep config order) + let configs = ParticipantConfig::random_quorum(quorum_size, rng)?; + let auxinfo_outputs = auxinfo::Output::simulate_set(&configs, rng); + + // Make the participants + zip(configs, auxinfo_outputs) + .map(|(config, auxinfo_output)| { + let input = Input::new(auxinfo_output, share.clone(), 2)?; + Self::new(sid, config.id(), config.other_ids().to_vec(), input) + }) + .collect::>>() + } + + pub fn initialize_tshare_message(&self, tshare_identifier: Identifier) -> Result { + let empty: [u8; 0] = []; + Message::new( + MessageType::Tshare(TshareMessageType::Ready), + tshare_identifier, + self.id(), + self.id(), + &empty, + ) + } + } + + /// Delivers all messages into their respective participant's inboxes. + fn deliver_all( + messages: &[Message], + inboxes: &mut HashMap>, + ) { + for message in messages { + inboxes + .get_mut(&message.to()) + .unwrap() + .push(message.clone()); + } + } + + fn is_tshare_done(quorum: &[TshareParticipant]) -> bool { + for participant in quorum { + if *participant.status() != Status::TerminatedSuccessfully { + return false; + } + } + true + } + + #[allow(clippy::type_complexity)] + fn process_messages( + quorum: &mut [TshareParticipant], + inboxes: &mut HashMap>, + rng: &mut R, + ) -> Option<(usize, ProcessOutcome)> { + // Pick a random participant to process + let index = rng.gen_range(0..quorum.len()); + let participant = quorum.get_mut(index).unwrap(); + + let inbox = inboxes.get_mut(&participant.id()).unwrap(); + if inbox.is_empty() { + // No messages to process for this participant, so pick another participant + return None; + } + let message = inbox.remove(rng.gen_range(0..inbox.len())); + debug!( + "processing participant: {}, with message type: {:?} from {}", + &participant.id(), + &message.message_type(), + &message.from(), + ); + Some((index, participant.process_message(rng, &message).unwrap())) + } + + #[cfg_attr(feature = "flame_it", flame)] + #[test] + fn tshare_always_produces_valid_outputs() -> Result<()> { + for size in 2..4 { + tshare_produces_valid_outputs(size)?; + } + Ok(()) + } + + fn tshare_produces_valid_outputs(quorum_size: usize) -> Result<()> { + let mut rng = init_testing_with_seed(Default::default()); + let sid = Identifier::random(&mut rng); + let test_share = Some(CoeffPrivate { + x: Scalar::from_u128(42), + }); + let mut quorum = TshareParticipant::new_quorum(sid, quorum_size, test_share, &mut rng)?; + let mut inboxes = HashMap::new(); + for participant in &quorum { + let _ = inboxes.insert(participant.id(), vec![]); + } + + let mut outputs = std::iter::repeat_with(|| None) + .take(quorum_size) + .collect::>(); + + for participant in &quorum { + let inbox = inboxes.get_mut(&participant.id()).unwrap(); + inbox.push(participant.initialize_tshare_message(sid)?); + } + + while !is_tshare_done(&quorum) { + let (index, outcome) = match process_messages(&mut quorum, &mut inboxes, &mut rng) { + None => continue, + Some(x) => x, + }; + + // Deliver messages and save outputs + match outcome { + ProcessOutcome::Incomplete => {} + ProcessOutcome::Processed(messages) => deliver_all(&messages, &mut inboxes), + ProcessOutcome::Terminated(output) => outputs[index] = Some(output), + ProcessOutcome::TerminatedForThisParticipant(output, messages) => { + deliver_all(&messages, &mut inboxes); + outputs[index] = Some(output); + } + } + } + + // Make sure every player got an output + let outputs: Vec<_> = outputs.into_iter().flatten().collect(); + assert_eq!(outputs.len(), quorum_size); + + // Make sure everybody agrees on the public parts. + assert!(outputs + .windows(2) + .all(|o| o[0].public_coeffs() == o[1].public_coeffs())); + assert!(outputs.windows(2).all(|o| { + let first_pks = o[0].public_key_shares(); + let second_pks = o[1].public_key_shares(); + // for each element in first_pks, there must be a corresponding element in + // second_pks + first_pks.iter().all(|x| second_pks.iter().any(|y| x == y)) + })); + + // Check returned outputs + // + // Every participant should have a public output from every other participant + // and, for a given participant, they should be the same in every output + for participant in quorum.iter_mut() { + // Check that each participant fully completed its broadcast portion. + if let Status::ParticipantCompletedBroadcast(participants) = + participant.broadcast_participant().status() + { + assert_eq!(participants.len(), participant.other_ids().len()); + } else { + panic!("Broadcast not completed!"); + } + } + + // Check that each participant's own `CoeffPublic` corresponds to their + // `CoeffPrivate` + for (output, pid) in outputs + .iter() + .zip(quorum.iter().map(ProtocolParticipant::id)) + { + let publics_coeffs = output + .public_coeffs() + .iter() + .map(|coeff| CoeffPublic::new(*coeff.as_ref())) + .collect::>(); + let public_share = TshareParticipant::eval_public_share(&publics_coeffs, pid)?; + + let expected_public_share = + CurvePoint::GENERATOR.multiply_by_scalar(output.private_key_share()); + // if the output already contains the public key, then we don't need to + // recompute and check it here. + assert_eq!(public_share, expected_public_share); + + // get the public key from output and validate against the expected public share + let public_key = output + .public_key_shares() + .iter() + .find(|x| x.participant() == pid) + .unwrap(); + assert_eq!(public_key.as_ref(), &public_share); + } + + // validate the final public key, which is given by the sum of the public keys + // of all participants + let mut sum_public_keys_first = CurvePoint::IDENTITY; + for public_key in outputs.first().unwrap().public_key_shares() { + sum_public_keys_first = sum_public_keys_first + *public_key.as_ref(); + } + for output in outputs.iter().skip(1) { + let mut sum_public_keys = CurvePoint::IDENTITY; + for public_key in output.public_key_shares() { + sum_public_keys = sum_public_keys + *public_key.as_ref(); + } + assert_eq!(sum_public_keys, sum_public_keys_first); + } + + Ok(()) + } + + fn generate_polynomial(t: usize, rng: &mut R) -> Vec { + let mut coefficients = Vec::with_capacity(t); + for _ in 0..t { + coefficients.push(Scalar::random(&mut *rng)); + } + coefficients + } + + pub fn evaluate_polynomial(coefficients: &[Scalar], x: &Scalar) -> Scalar { + coefficients + .iter() + .rev() + .fold(Scalar::ZERO, |acc, coef| acc * x + coef) + } + + fn evaluate_at_points(coefficients: &[Scalar], points: &[Scalar]) -> Vec { + points + .iter() + .map(|x| evaluate_polynomial(coefficients, x)) + .collect() + } + + #[test] + fn test_evaluate_points_at_zero() { + let mut rng = thread_rng(); + let t: u128 = 3; + let n: u128 = 7; + let coefficients = generate_polynomial(t as usize, &mut rng); + + // test that reconstruction works as long as we have enough points + for n in t..n { + let points: Vec = (1..=n).map(|i: u128| Scalar::from_u128(i + 1)).collect(); + let values = evaluate_at_points(&coefficients, &points); + + let zero = Scalar::ZERO; + let zero_value = evaluate_polynomial(&coefficients, &zero); + + let points: Vec = (1..=n) + .map(|i: u128| ParticipantIdentifier::from_u128(i)) + .collect(); + let zero_value_reconstructed = values + .iter() + .zip(&points) + .map(|(value, point)| { + *value * TshareParticipant::lagrange_coefficient_at_zero(point, &points) + }) + .fold(Scalar::ZERO, |acc, x| acc + x); + + assert_eq!(zero_value, zero_value_reconstructed); + } + } +} diff --git a/src/tshare/share.rs b/src/tshare/share.rs new file mode 100644 index 00000000..8016cbaf --- /dev/null +++ b/src/tshare/share.rs @@ -0,0 +1,272 @@ +// Copyright (c) Facebook, Inc. and its affiliates. +// Modifications Copyright (c) 2022-2023 Bolt Labs Holdings, Inc +// +// This source code is licensed under both the MIT license found in the +// LICENSE-MIT file in the root directory of this source tree and the Apache +// License, Version 2.0 found in the LICENSE-APACHE file in the root directory +// of this source tree. + +use crate::{ + errors::{CallerError, InternalError, Result}, + paillier::{Ciphertext, DecryptionKey, EncryptionKey}, + utils::{bn_to_scalar, k256_order, scalar_to_bn, CurvePoint}, +}; +use k256::{elliptic_curve::Field, Scalar}; +use libpaillier::unknown_order::BigNumber; +use rand::{CryptoRng, RngCore}; +use serde::{Deserialize, Serialize}; +use std::{fmt::Debug, ops::Add}; +use tracing::error; +use zeroize::ZeroizeOnDrop; + +/// Encrypted [`CoeffPrivate`]. +#[derive(Clone, Serialize, Deserialize)] +pub struct EvalEncrypted { + ciphertext: Ciphertext, +} + +impl EvalEncrypted { + pub fn encrypt( + share_private: &EvalPrivate, + pk: &EncryptionKey, + rng: &mut R, + ) -> Result { + if &(k256_order() * 2) >= pk.modulus() { + error!("EvalEncrypted encryption failed, pk.modulus() is too small"); + Err(InternalError::InternalInvariantFailed)?; + } + + let (ciphertext, _nonce) = pk + .encrypt(rng, &scalar_to_bn(&share_private.x)) + .map_err(|_| InternalError::InternalInvariantFailed)?; + + Ok(EvalEncrypted { ciphertext }) + } + + pub fn decrypt(&self, dk: &DecryptionKey) -> Result { + let x = dk.decrypt(&self.ciphertext).map_err(|_| { + error!("EvalEncrypted decryption failed, ciphertext out of range",); + CallerError::DeserializationFailed + })?; + if x >= k256_order() || x < BigNumber::one() { + error!( + "EvalEncrypted decryption failed, plaintext out of range (x={})", + x + ); + Err(CallerError::DeserializationFailed)?; + } + Ok(EvalPrivate { + x: bn_to_scalar(&x).unwrap(), + }) + } +} + +/// Private coefficient share. +#[derive(Clone, ZeroizeOnDrop, PartialEq, Eq, Serialize, Deserialize)] +pub struct CoeffPrivate { + /// A BigNumber element in the range [1, q) representing a polynomial + /// coefficient + pub x: Scalar, +} + +#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)] +pub struct EvalPrivate { + /// A BigNumber element in the range [1, q) representing a polynomial + /// coefficient + pub x: Scalar, +} + +/// Implement addition operation for `EvalPrivate`. +impl Add<&EvalPrivate> for EvalPrivate { + type Output = Self; + + fn add(self, rhs: &EvalPrivate) -> Self::Output { + EvalPrivate { x: self.x + rhs.x } + } +} + +impl EvalPrivate { + pub fn new(x: Scalar) -> Self { + EvalPrivate { x } + } +} + +impl Debug for CoeffPrivate { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + f.write_str("CoeffPrivate([redacted])") + } +} + +/// Represents a coefficient of a polynomial. +/// Coefficients and Evaluations are represented as curve scalars. +/// The input shares are interpreted as coefficients, while the output shares +/// are interpreted as evaluations. +impl CoeffPrivate { + /// Sample a private key share uniformly at random. + pub(crate) fn random(rng: &mut (impl CryptoRng + RngCore)) -> Self { + let random_bn = Scalar::random(rng); + CoeffPrivate { x: random_bn } + } + + /// Computes the "raw" curve point corresponding to this private key. + pub(crate) fn public_point(&self) -> CurvePoint { + CurvePoint::GENERATOR.multiply_by_scalar(&self.x) + } + + pub(crate) fn to_public(&self) -> CoeffPublic { + CoeffPublic::new(self.public_point()) + } +} + +/// Represents an evaluation of a polynomial at a given point. +/// Coefficients and Evaluations are represented as curve scalars. +/// The input shares are interpreted as coefficients, while the output shares +/// are interpreted as evaluations. +impl EvalPrivate { + /// Sample a private key share uniformly at random. + pub fn random(rng: &mut (impl CryptoRng + RngCore)) -> Self { + let random_scalar = Scalar::random(rng); + EvalPrivate { x: random_scalar } + } + + pub(crate) fn sum(shares: &[Self]) -> Self { + let sum = shares.iter().fold(Scalar::ZERO, |sum, o| sum + o.x); + EvalPrivate { x: sum } + } + + pub(crate) fn public_point(&self) -> CurvePoint { + CurvePoint::GENERATOR.multiply_by_scalar(&self.x) + } +} + +impl AsRef for CoeffPrivate { + /// Get the coeff as a number. + fn as_ref(&self) -> &Scalar { + &self.x + } +} + +/// A curve point representing a given [`Participant`](crate::Participant)'s +/// public key. +#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)] +pub struct CoeffPublic { + X: CurvePoint, +} + +impl CoeffPublic { + /// Wrap a curve point as a public coeff. + pub(crate) fn new(X: CurvePoint) -> Self { + Self { X } + } + + /// Generate a new [`CoeffPrivate`] and [`CoeffPublic`]. + pub(crate) fn new_pair( + rng: &mut R, + ) -> Result<(CoeffPrivate, CoeffPublic)> { + let private_share = CoeffPrivate::random(rng); + let public_share = private_share.to_public(); + Ok((private_share, public_share)) + } +} + +impl AsRef for CoeffPublic { + /// Get the coeff as a curve point. + fn as_ref(&self) -> &CurvePoint { + &self.X + } +} + +impl Add<&CoeffPublic> for CoeffPublic { + type Output = Self; + + fn add(self, rhs: &CoeffPublic) -> Self::Output { + CoeffPublic { X: self.X + rhs.X } + } +} + +/// A curve point representing a given [`Participant`](crate::Participant)'s +/// public evaluation. +#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)] +pub struct EvalPublic { + X: CurvePoint, +} + +impl EvalPublic { + /// Wrap a curve point as a public evaluation. + pub(crate) fn new(X: CurvePoint) -> Self { + Self { X } + } +} + +impl AsRef for EvalPublic { + /// Get the coeff as a curve point. + fn as_ref(&self) -> &CurvePoint { + &self.X + } +} + +#[cfg(test)] +mod tests { + use super::*; + use crate::{ + auxinfo, + utils::{bn_to_scalar, k256_order, testing::init_testing}, + ParticipantIdentifier, + }; + use rand::rngs::StdRng; + + /// Generate an encryption key pair. + fn setup() -> (StdRng, EncryptionKey, DecryptionKey) { + let mut rng = init_testing(); + let pid = ParticipantIdentifier::random(&mut rng); + let auxinfo = auxinfo::Output::simulate(&[pid], &mut rng); + let dk = auxinfo.private_auxinfo().decryption_key(); + let pk = auxinfo.find_public(pid).unwrap().pk(); + assert!( + &(k256_order() * 2) < pk.modulus(), + "the Paillier modulus is supposed to be much larger than the k256 order" + ); + (rng, pk.clone(), dk.clone()) + } + + #[test] + fn coeff_encryption_works() { + let (mut rng, pk, dk) = setup(); + let rng = &mut rng; + + // Encryption round-trip. + let coeff = EvalPrivate::random(rng); + let encrypted = EvalEncrypted::encrypt(&coeff, &pk, rng).expect("encryption failed"); + let decrypted = encrypted.decrypt(&dk).expect("decryption failed"); + + assert_eq!(decrypted, coeff); + } + + #[test] + fn coeff_decrypt_unexpected() { + let (mut rng, pk, dk) = setup(); + let rng = &mut rng; + + // Encrypt unexpected shares. + { + let x = &(-BigNumber::one()); + let share = EvalPrivate { + x: bn_to_scalar(x).expect("Failed to convert to scalar"), + }; + let encrypted = EvalEncrypted::encrypt(&share, &pk, rng).expect("encryption failed"); + // Decryption reports an error. + let decrypt_result = encrypted.decrypt(&dk); + assert!(decrypt_result.is_ok()); + } + // Encrypt zero returns an error in decryption. + for x in [BigNumber::zero(), k256_order()].iter() { + let share = EvalPrivate { + x: bn_to_scalar(x).expect("Failed to convert to scalar"), + }; + let encrypted = EvalEncrypted::encrypt(&share, &pk, rng).expect("encryption failed"); + // Decryption reports an error. + let decrypt_result = encrypted.decrypt(&dk); + assert!(decrypt_result.is_err()); + } + } +} diff --git a/src/utils.rs b/src/utils.rs index ed5eb8e3..2ed9cede 100644 --- a/src/utils.rs +++ b/src/utils.rs @@ -373,6 +373,12 @@ pub(crate) fn bn_to_scalar(x: &BigNumber) -> Result { Ok(ret) } +// Convert from k256::Scalar to BigNumber +pub(crate) fn scalar_to_bn(x: &k256::Scalar) -> BigNumber { + let bytes = x.to_repr(); + BigNumber::from_slice(bytes) +} + pub(crate) fn k256_order() -> BigNumber { // Set order = q let order_bytes: [u8; 32] = k256::Secp256k1::ORDER.to_be_bytes(); diff --git a/src/zkp/pisch.rs b/src/zkp/pisch.rs index 6eb5530b..f2456845 100644 --- a/src/zkp/pisch.rs +++ b/src/zkp/pisch.rs @@ -28,7 +28,7 @@ //! [EPrint archive, 2021](https://eprint.iacr.org/2021/060.pdf). use crate::{ errors::*, - messages::{KeygenMessageType, KeyrefreshMessageType, Message, MessageType}, + messages::{KeygenMessageType, KeyrefreshMessageType, Message, MessageType, TshareMessageType}, utils::{self, k256_order, positive_challenge_from_transcript, random_positive_bn}, zkp::{Proof, ProofContext}, }; @@ -58,7 +58,7 @@ pub(crate) struct PiSchProof { /// Implementation note: this type includes the mask itself. This is for /// convenience; the mask must not be sent to the verifier at any point as this /// breaks the security of the proof. -#[derive(Debug, Serialize, Deserialize)] +#[derive(Debug, Serialize, Deserialize, Clone)] pub(crate) struct PiSchPrecommit { /// Precommitment value (`A` in the paper). precommitment: CurvePoint, @@ -236,7 +236,10 @@ impl PiSchProof { } pub(crate) fn from_message(message: &Message) -> Result { - message.check_type(MessageType::Keygen(KeygenMessageType::R3Proof))?; + message.check_one_of_type(&[ + MessageType::Keygen(KeygenMessageType::R3Proof), + MessageType::Tshare(TshareMessageType::R3Proof), + ])?; let pisch_proof: PiSchProof = deserialize!(&message.unverified_bytes)?; if pisch_proof.challenge >= k256_order() {