From 2b475e71d04638f6e3666949ef2ae4da4d1572d9 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Aur=C3=A9lien=20Nicolas?= Date: Sat, 20 Jul 2024 13:03:34 +0200 Subject: [PATCH] inv-tshare: Threshold (re-)sharing protocol --- src/broadcast/participant.rs | 1 + src/lib.rs | 1 + src/messages.rs | 31 + src/protocol.rs | 7 + src/tshare/commit.rs | 139 +++++ src/tshare/input.rs | 138 +++++ src/tshare/mod.rs | 16 + src/tshare/output.rs | 198 +++++++ src/tshare/participant.rs | 1076 ++++++++++++++++++++++++++++++++++ src/tshare/share.rs | 202 +++++++ src/zkp/pisch.rs | 7 +- 11 files changed, 1814 insertions(+), 2 deletions(-) create mode 100644 src/tshare/commit.rs create mode 100644 src/tshare/input.rs create mode 100644 src/tshare/mod.rs create mode 100644 src/tshare/output.rs create mode 100644 src/tshare/participant.rs create mode 100644 src/tshare/share.rs 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 8e48edac..a8fde1ce 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -220,6 +220,7 @@ pub mod presign; mod protocol; mod ring_pedersen; pub mod sign; +pub mod tshare; mod utils; mod zkp; mod zkstar; diff --git a/src/messages.rs b/src/messages.rs index dce3acb3..97603210 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, + /// A proof of knowledge of the discrete log of the value decommitted in + /// Round 2 + R3Proofs, + /// The encrypted private share from a participant to another. + R3PrivateShare, +} + /// 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/protocol.rs b/src/protocol.rs index fb0276a6..5f5159f5 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) @@ -420,6 +422,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 diff --git a/src/tshare/commit.rs b/src/tshare/commit.rs new file mode 100644 index 00000000..8f44a2d3 --- /dev/null +++ b/src/tshare/commit.rs @@ -0,0 +1,139 @@ +// 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 As: Vec, +} + +impl TshareDecommit { + ///`sid` corresponds to a unique session identifier. + pub(crate) fn new( + rng: &mut R, + sid: &Identifier, + sender: &ParticipantIdentifier, + coeff_publics: Vec, + sch_precoms: Vec, + ) -> 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, + As: sch_precoms, + } + } + + /// Deserialize a TshareDecommit from a message and verify it. + pub(crate) fn from_message( + message: &Message, + com: &TshareCommit, + threshold: usize, + ) -> Result { + message.check_type(MessageType::Tshare(TshareMessageType::R2Decommit))?; + let tshare_decommit: TshareDecommit = deserialize!(&message.unverified_bytes)?; + tshare_decommit.verify(message.id(), message.from(), com, threshold)?; + 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, + threshold: usize, + ) -> 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))); + } + + // Check the number of commitments As. + if self.As.len() != threshold { + error!("Incorrect number of As"); + return Err(InternalError::ProtocolError(Some(sender))); + } + + // Check the set of coefficients. + if self.coeff_publics.len() != threshold { + error!("Incorrect number of public shares"); + 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("...", &"[redacted]") + .finish() + } +} diff --git a/src/tshare/input.rs b/src/tshare/input.rs new file mode 100644 index 00000000..d3040501 --- /dev/null +++ b/src/tshare/input.rs @@ -0,0 +1,138 @@ +// 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, 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: None, + threshold, + }) + } + + pub fn share(&self) -> Option<&CoeffPrivate> { + self.share.as_ref() + } + + pub fn threshold(&self) -> usize { + self.threshold + } + + 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}, + keygen, + utils::testing::init_testing, + Identifier, ParticipantConfig, 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(5, rng); + let auxinfo_output = auxinfo::Output::simulate(&config.all_participants(), rng); + let input = Input::new(auxinfo_output, 2)?; + + // Create valid config with PIDs independent of those used to make the input set + let config = ParticipantConfig::random(SIZE, rng); + + let result = TshareParticipant::new( + Identifier::random(rng), + config.id(), + config.other_ids().to_vec(), + input, + ); + assert!(result.is_err()); + assert_eq!( + result.unwrap_err(), + InternalError::CallingApplicationMistake(CallerError::BadInput) + ); + + Ok(()) + } + + // TODO: tests for other invalid inputs. +} diff --git a/src/tshare/mod.rs b/src/tshare/mod.rs new file mode 100644 index 00000000..f5fd528b --- /dev/null +++ b/src/tshare/mod.rs @@ -0,0 +1,16 @@ +//! 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 participant::TshareParticipant; diff --git a/src/tshare/output.rs b/src/tshare/output.rs new file mode 100644 index 00000000..fbdb8920 --- /dev/null +++ b/src/tshare/output.rs @@ -0,0 +1,198 @@ +// 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 crate::{ + errors::{CallerError, InternalError, Result}, + keygen::{KeySharePrivate, KeySharePublic}, + utils::CurvePoint, + ParticipantIdentifier, +}; + +use k256::ecdsa::VerifyingKey; +use tracing::error; + +/// Output type from key generation, including all parties' public key shares, +/// this party's private key share, and a bit of global randomness. +#[derive(Debug, Clone)] +pub struct Output { + public_key_shares: Vec, + private_key_share: KeySharePrivate, +} + +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 + } + + pub(crate) fn private_key_share(&self) -> &KeySharePrivate { + &self.private_key_share + } + + /// Get the [`ParticipantIdentifier`] corresponding to the + /// [`KeySharePrivate`]. + pub(crate) fn private_pid(&self) -> Result { + let expected_public_share = self.private_key_share.public_share()?; + match self + .public_key_shares + .iter() + .find(|share| share.as_ref() == &expected_public_share) + { + Some(public_key_share) => Ok(public_key_share.participant()), + None => { + error!("Didn't find a public key share corresponding to the private key share, but there should be one by construction"); + Err(InternalError::InternalInvariantFailed) + } + } + } + + /// 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: + /// - There is a valid key pair -- that is, the public key corresponding to + /// the private key share must be contained in the list of public shares. + /// - The public key shares must be from a unique set of participants + pub fn from_parts( + public_coeffs: Vec, + private_key_share: KeySharePrivate, + ) -> Result { + let pids = public_coeffs + .iter() + .map(KeySharePublic::participant) + .collect::>(); + if pids.len() != public_coeffs.len() { + error!("Tried to create a keygen output using a set of public material from non-unique participants"); + Err(CallerError::BadInput)? + } + + Ok(Self { + public_key_shares: public_coeffs, + private_key_share, + }) + } + + /// Decompose the `Output` into its constituent parts. + /// + /// # 🔒 Storage requirements + /// The [`KeySharePrivate`] 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, KeySharePrivate) { + (self.public_key_shares, self.private_key_share) + } +} + +#[cfg(test)] +mod tests { + use super::*; + use crate::{utils::testing::init_testing, ParticipantIdentifier}; + use rand::{CryptoRng, RngCore}; + + 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], + rng: &mut (impl CryptoRng + RngCore), + ) -> Self { + let (mut private_key_shares, public_key_shares): (Vec<_>, Vec<_>) = pids + .iter() + .map(|&pid| { + // TODO #340: Replace with KeyShare methods once they exist. + let secret = KeySharePrivate::random(rng); + let public = secret.public_share().unwrap(); + (secret, KeySharePublic::new(pid, public)) + }) + .unzip(); + + Self::from_parts(public_key_shares, private_key_shares.pop().unwrap()).unwrap() + } + } + + #[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, rng); + + let (public, private) = output.into_parts(); + assert!(Output::from_parts(public, private).is_ok()); + } + + #[test] + fn private_field_must_correspond_to_a_public() { + let rng = &mut init_testing(); + let pids = std::iter::repeat_with(|| ParticipantIdentifier::random(rng)) + .take(5) + .collect::>(); + + // Use the simulate function to get a set of valid public components + let output = Output::simulate(&pids, rng); + + // Create a random private share. It's legally possible for this to match one of + // the public keys but it's so unlikely that we won't check it. + let bad_private_key_share = KeySharePrivate::random(rng); + + // TODO: move the check from TshareParticipant::maybe_finish here. + //assert!( + // Output::from_parts(output.public_key_shares, + // bad_private_key_share, output.rid) .is_err() + //) + } + + #[test] + fn public_shares_must_not_have_duplicate_pids() { + let 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): (Vec<_>, Vec<_>) = pids + .iter() + .map(|&pid| { + // TODO #340: Replace with KeyShare methods once they exist. + let secret = KeySharePrivate::random(rng); + let public = secret.public_share().unwrap(); + (secret, KeySharePublic::new(pid, public)) + }) + .unzip(); + + assert!(Output::from_parts(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..6e82300c --- /dev/null +++ b/src/tshare/participant.rs @@ -0,0 +1,1076 @@ +//! 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 super::{ + commit::{TshareCommit, TshareDecommit}, + share::{CoeffPrivate, CoeffPublic, EvalEncrypted}, +}; +use crate::{ + broadcast::participant::{BroadcastOutput, BroadcastParticipant, BroadcastTag}, + errors::{CallerError, InternalError, Result}, + local_storage::LocalStorage, + messages::{Message, MessageType, TshareMessageType}, + participant::{ + Broadcast, InnerProtocolParticipant, ProcessOutcome, ProtocolParticipant, Status, + }, + protocol::{ParticipantIdentifier, ProtocolType, SharedContext}, + run_only_once, + utils::{k256_order, CurvePoint}, + zkp::pisch::{CommonInput, PiSchPrecommit, PiSchProof, ProverSecret}, + Identifier, ParticipantConfig, +}; + +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; + + 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 VecSchnorrPrecom; + impl TypeTag for VecSchnorrPrecom { + type Value = Vec; + } + 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 ValidPublicCoeffs; + impl TypeTag for ValidPublicCoeffs { + type Value = Vec; + } + pub(super) struct ValidPrivateEval; + impl TypeTag for ValidPrivateEval { + type Value = super::CoeffPrivate; + } +} + +/** +A [`ProtocolParticipant`] that runs the threshold sharing protocol. + +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 and a Schnorr proof. + +Rounds 2: +- Each participant decommits to their polynomial and Schnorr proof. + +Rounds 3: +- Each participant verifies the Schnorr proofs of all other participants. +- Each participant shares their private evaluation of the polynomial with all other participants. + +Output: +- The public commitments to the shared polynomial. 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(rng, message) + } + MessageType::Tshare(TshareMessageType::R3Proofs) => { + self.handle_round_three_msg(message) + } + MessageType::Tshare(TshareMessageType::R3PrivateShare) => { + self.handle_round_three_msg_private(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 { + fn coeff_ids(&self) -> Vec { + // TODO: Introduce dedicated types. + (0..self.input.threshold()) + .map(|i| ParticipantIdentifier::from_u128(i as u128)) + .collect() + } + + /// 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 pid in self.coeff_ids() { + let (private, public) = CoeffPublic::new_pair(pid, rng)?; + privates.push(private); + publics.push(public); + } + + if let Some(private) = self.input.share() { + privates[0] = private.clone(); + publics[0] = private.to_public(self.coeff_ids()[0])?; + } + + (privates, publics) + }; + + // Generate proof precommitments. + let sch_precoms = (0..coeff_publics.len()) + .map(|_| PiSchProof::precommit(rng)) + .collect::>>()?; + + let decom = TshareDecommit::new( + rng, + &sid, + &self.id(), + coeff_publics, + sch_precoms + .iter() + .map(|sch_precom| *sch_precom.precommitment()) + .collect(), + ); + + // Store the beginning of our proofs so we can continue the proofs later. + self.local_storage + .store::(self.id(), sch_precoms); + + // Mark our own public shares as verified. + self.local_storage + .store::(self.id(), decom.coeff_publics.clone()); + + // Store the private share from ourselves to ourselves. + let my_private_share = Self::eval_private_share(&coeff_privates, self.id()); + self.local_storage + .store::(self.id(), my_private_share); + + // Store the private coeffs from us to others so we can share them later. + self.local_storage + .store::(self.id(), coeff_privates); + + 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()))?; + + // 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(rng, msg)) + .collect::>>()?; + + ProcessOutcome::collect_with_messages(round_two_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); + Ok(messages) + } + + /// 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, + rng: &mut R, + 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.input.threshold())?; + self.local_storage + .store_once::(message.from(), decom)?; + + // Check if we've received all the decommits + let r2_done = self + .local_storage + .contains_for_all_ids::(&self.all_participants()); + + if r2_done { + // Generate messages for round 3... + let round_three_messages = run_only_once!(self.gen_round_three_msgs(rng))?; + + // ...and handle any messages that other participants have sent for round 3. + let mut round_three_outcomes = self + .fetch_messages(MessageType::Tshare(TshareMessageType::R3Proofs))? + .iter() + .map(|msg| self.handle_round_three_msg(msg)) + .collect::>>()?; + + let outcomes_private = self + .fetch_messages(MessageType::Tshare(TshareMessageType::R3PrivateShare))? + .iter() + .map(|msg| self.handle_round_three_msg_private(msg)) + .collect::>>()?; + round_three_outcomes.extend(outcomes_private); + + 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, + rng: &mut R, + ) -> 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 decom = self + .local_storage + .retrieve::(self.id())?; + + let transcript = schnorr_proof_transcript(self.sid(), &global_rid, self.id())?; + + // Generate proofs for each share. + let precoms = self + .local_storage + .retrieve::(self.id())?; + + let private_coeffs = self + .local_storage + .retrieve::(self.id())?; + + let mut proofs: Vec = vec![]; + for i in 0..precoms.len() { + let pk = &decom.coeff_publics[i]; + let input = CommonInput::new(pk); + let precom = &precoms[i]; + let sk = &private_coeffs[i]; + + let proof = PiSchProof::prove_from_precommit( + &self.retrieve_context(), + precom, + &input, + &ProverSecret::new(sk.as_ref()), + &transcript, + )?; + + proofs.push(proof); + } + + // 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 all proofs to everybody. + let mut messages = self.message_for_other_participants( + MessageType::Tshare(TshareMessageType::R3Proofs), + proofs, + )?; + + // 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::R3PrivateShare), + self.sid(), + self.id(), + *other_participant_id, + encrypted_share, + ) + }) + .collect::>>()?, + ); + + Ok(messages) + } + + /// Assign a non-null x coordinate to each participant. + fn participant_coordinate(pid: ParticipantIdentifier) -> BigNumber { + BigNumber::from(pid.as_u128()) + BigNumber::one() + } + + fn eval_private_share( + coeff_privates: &[CoeffPrivate], + recipient_id: ParticipantIdentifier, + ) -> CoeffPrivate { + // TODO: Enforce that no participant ID equals the shared evaluation point + // (0, constant term). + // TODO: Use a field type. + + let x = Self::participant_coordinate(recipient_id); + let mut sum = BigNumber::zero(); + for coeff in coeff_privates.iter().rev() { + sum *= &x; + sum = sum.modadd(&coeff.x, &k256_order()); + } + // TODO: introduce a different type for evaluations. + CoeffPrivate { x: sum } + } + + 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_bignum(&x)?; + sum = sum + *coeff.as_ref(); + } + Ok(sum) + } + + /// 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 proofs = PiSchProof::from_message_multi(message)?; + let decom = self + .local_storage + .retrieve::(message.from())?; + + // Check that there is one proof per coeff. + // TODO: check (or compute) the exact value of the eval points. + if proofs.len() != decom.coeff_publics.len() { + error!("Received incorrect number of proofs",); + return Err(InternalError::ProtocolError(Some(message.from()))); + } + + for ((proof, precommit), public_share) in proofs + .into_iter() + .zip(decom.As.iter()) + .zip(decom.coeff_publics.iter()) + { + let mut transcript = schnorr_proof_transcript(self.sid(), &global_rid, message.from())?; + proof.verify_with_precommit( + CommonInput::new(public_share), + &self.retrieve_context(), + &mut transcript, + precommit, + )?; + } + + // Only if the proof verifies do we store the participant's shares. + self.local_storage + .store_once::( + message.from(), + decom.coeff_publics.clone(), + )?; + + self.maybe_finish() + } + + /// Handle the protocol's round three 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_three_msg_private( + &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 private message."); + self.stash_message(message)?; + return Ok(ProcessOutcome::Incomplete); + } + info!("Handling round three tshare private message."); + + message.check_type(MessageType::Tshare(TshareMessageType::R3PrivateShare))?; + 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)?; + + // Check that this private share matches our public share in TshareDecommit + // from this participant. + let decom = self + .local_storage + .retrieve::(message.from())?; + let expected_public = Self::eval_public_share(&decom.coeff_publics, self.id())?; + 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(message.from()))); + } + + self.local_storage + .store::(message.from(), private_share); + + self.maybe_finish() + } + + fn maybe_finish(&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()); + + // Have we got the private shares from everybody to us? + let got_all_private_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 && got_all_private_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); + + // Compute the one's own private evaluation. + let from_all_to_me_private = self + .all_participants() + .iter() + .map(|pid| self.local_storage.remove::(*pid)) + .collect::>>()?; + let my_private_share = Self::aggregate_private_shares(&from_all_to_me_private); + + // Double-check that the aggregated private share matches the aggregated public + // coeffs. + let expected_public = Self::eval_public_share(&all_public_coeffs, self.id())?; + let implied_public = my_private_share.public_point()?; + if implied_public != expected_public { + error!("the aggregated private share does not match the public coeffs"); + return Err(InternalError::InternalInvariantFailed); + } + + // Return the output and stop. + let all_public_coeffs = all_public_coeffs + .iter() + .enumerate() + .map(|(i, coeff)| coeff.to_keyshare(i)) + .collect(); + let output = Output::from_parts(all_public_coeffs, my_private_share.to_keyshare())?; + + // Check that the new shared value is consistent with the old one (if given). + /*if let Some(share) = self.input.share() { + let old_public = // TODO: get previous public shared value. + if old_public != all_public_coeffs[0] { + error!("The new public key share is inconsistent with the old one."); + return Err(InternalError::ProtocolError(None)); + } + }*/ + + 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: &[CoeffPrivate]) -> CoeffPrivate { + CoeffPrivate::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, keygen, + utils::testing::{init_testing, init_testing_with_seed}, + Identifier, ParticipantConfig, + }; + use rand::{CryptoRng, Rng, RngCore}; + use std::{ + collections::{HashMap, HashSet}, + iter::zip, + }; + use tracing::debug; + + impl TshareParticipant { + pub fn new_quorum( + sid: Identifier, + quorum_size: usize, + 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, 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 mut quorum = TshareParticipant::new_quorum(sid, quorum_size, &mut rng)?; + let mut inboxes = HashMap::new(); + for participant in &quorum { + let _ = inboxes.insert(participant.id(), vec![]); + } + + let inputs = quorum.iter().map(|p| p.input.clone()).collect::>(); + + 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_key_shares() == o[1].public_key_shares())); + + // 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 party in quorum.iter_mut() { + let pid = party.id(); + + // Check that each participant fully completed its broadcast portion. + if let Status::ParticipantCompletedBroadcast(participants) = + party.broadcast_participant().status() + { + assert_eq!(participants.len(), party.other_ids().len()); + } else { + panic!("Broadcast not completed!"); + } + } + + // TODO. + // Check that each participant's own `CoeffPublic` corresponds to their + // `CoeffPrivate` + for (output, pid) in outputs + .iter() + .zip(quorum.iter().map(ProtocolParticipant::id)) + { + /* + let coeff_publics = output + .public_coeffs() // TODO + .iter() + .map(|coeff| coeff.as_ref()) + .collect::>(); + let public_share = TshareParticipant::eval_public_share(&coeff_publics, id)?; + + let expected_public_share = + CurvePoint::GENERATOR.multiply_by_bignum(output.private_key_share().as_ref())?; + assert_eq!(public_share.unwrap().as_ref(), &expected_public_share); + */ + } + + for (input, output) in inputs.iter().zip(outputs.iter()) { + // TODO. Check the shared value has not changed. + + // All shares have changed. + /* + let public_shares_before = input + .public_key_shares() + .iter() + .map(|key_share| serialize!(key_share.as_ref()).unwrap()) + .collect::>(); + + let public_shares_after = output + .public_key_shares() + .iter() + .map(|key_share| serialize!(key_share.as_ref()).unwrap()) + .collect::>(); + + public_shares_before + .intersection(&public_shares_after) + .for_each(|_| { + panic!("All public key shares must change."); + }); + */ + } + + Ok(()) + } +} diff --git a/src/tshare/share.rs b/src/tshare/share.rs new file mode 100644 index 00000000..ed42183a --- /dev/null +++ b/src/tshare/share.rs @@ -0,0 +1,202 @@ +// 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}, + keygen::{KeySharePrivate, KeySharePublic}, + paillier::{Ciphertext, DecryptionKey, EncryptionKey}, + utils::{k256_order, CurvePoint}, + ParticipantIdentifier, +}; +use libpaillier::unknown_order::BigNumber; +use rand::{CryptoRng, RngCore}; +use serde::{Deserialize, Serialize}; +use std::fmt::Debug; +use tracing::error; +use zeroize::ZeroizeOnDrop; + +/// Encrypted [`CoeffPrivate`]. +#[derive(Clone, Serialize, Deserialize)] +pub struct EvalEncrypted { + ciphertext: Ciphertext, +} + +impl EvalEncrypted { + pub fn encrypt( + share_private: &CoeffPrivate, + 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, &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(CoeffPrivate { x }) + } +} + +/// Private coefficient share. +#[derive(Clone, ZeroizeOnDrop, PartialEq, Eq, Serialize, Deserialize)] +pub struct CoeffPrivate { + pub x: BigNumber, // in the range [1, q) +} + +impl Debug for CoeffPrivate { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + f.write_str("CoeffPrivate([redacted])") + } +} + +// TODO: remove unused methods. +impl CoeffPrivate { + /// Sample a private key share uniformly at random. + pub(crate) fn random(rng: &mut (impl CryptoRng + RngCore)) -> Self { + let random_bn = BigNumber::from_rng(&k256_order(), rng); + CoeffPrivate { x: random_bn } + } + + pub(crate) fn sum(shares: &[Self]) -> Self { + let sum = shares + .iter() + .fold(BigNumber::zero(), |sum, o| sum + o.x.clone()) + .nmod(&k256_order()); + CoeffPrivate { x: sum } + } + + // TODO: Introduce a dedicated Output for tshare, + // implement tshare::Output::public_key(), + // and remove this conversion. + pub fn to_keyshare(&self) -> KeySharePrivate { + KeySharePrivate::from_bigint(&self.x) + } + + /// Computes the "raw" curve point corresponding to this private key. + pub(crate) fn public_point(&self) -> Result { + CurvePoint::GENERATOR.multiply_by_bignum(&self.x) + } + + pub(crate) fn to_public(&self, participant: ParticipantIdentifier) -> Result { + Ok(CoeffPublic::new(self.public_point()?)) + } +} + +impl AsRef for CoeffPrivate { + /// Get the coeff as a number. + fn as_ref(&self) -> &BigNumber { + &self.x + } +} + +// TODO: remove unused methods. +/// 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( + participant: ParticipantIdentifier, + rng: &mut R, + ) -> Result<(CoeffPrivate, CoeffPublic)> { + let private_share = CoeffPrivate::random(rng); + let public_share = private_share.to_public(participant)?; + Ok((private_share, public_share)) + } + + // TODO: Introduce a dedicated Output for tshare, and remove this conversion. + pub fn to_keyshare(&self, i: usize) -> KeySharePublic { + KeySharePublic::new(ParticipantIdentifier::from_u128(i as u128), self.X) + } +} + +impl AsRef for CoeffPublic { + /// Get the coeff as a curve point. + fn as_ref(&self) -> &CurvePoint { + &self.X + } +} + +#[cfg(test)] +mod tests { + use super::*; + use crate::{ + auxinfo, + utils::{k256_order, testing::init_testing}, + }; + 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 = CoeffPrivate::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_out_of_range() { + let (mut rng, pk, dk) = setup(); + let rng = &mut rng; + + // Encrypt invalid shares. + for x in [BigNumber::zero(), -BigNumber::one(), k256_order()].iter() { + let share = CoeffPrivate { x: x.clone() }; + 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/zkp/pisch.rs b/src/zkp/pisch.rs index 6eb5530b..2ced72d3 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}, }; @@ -219,7 +219,10 @@ impl PiSchProof { // Deserialize multiple proofs from a single message. pub(crate) fn from_message_multi(message: &Message) -> Result> { - message.check_type(MessageType::Keyrefresh(KeyrefreshMessageType::R3Proofs))?; + message.check_one_of_type(&[ + MessageType::Keyrefresh(KeyrefreshMessageType::R3Proofs), + MessageType::Tshare(TshareMessageType::R3Proofs), + ])?; let pisch_proofs: Vec = deserialize!(&message.unverified_bytes)?; for pisch_proof in &pisch_proofs {