From b0d074b3bf6c05b14fef3335c5eec4c5cad23364 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 | 1075 ++++++++++++++++++++++++++++++++++ src/tshare/share.rs | 202 +++++++ src/zkp/pisch.rs | 7 +- 11 files changed, 1813 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..1d7ce394 --- /dev/null +++ b/src/tshare/participant.rs @@ -0,0 +1,1075 @@ +//! 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; + } +} + +/** +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 and a Schnorr proof. + +Rounds 2: +- Each participant decommits the public form of their polynomial and Schnorr proofs. + +Rounds 3: +- Each participant shares a private evaluation of the polynomial with each of the other participants. + +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(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 {