From cf281c4e755da596074a936132daeeb74e8fd6c6 Mon Sep 17 00:00:00 2001 From: Piotr Roslaniec Date: Mon, 12 Feb 2024 15:17:32 +0100 Subject: [PATCH] refactor: introduce refreshing api in ferveo --- ferveo-common/src/keypair.rs | 9 +- ferveo-python/ferveo/__init__.py | 1 + ferveo-python/ferveo/__init__.pyi | 3 + ferveo-tdec/benches/tpke.rs | 2 +- ferveo-tdec/src/combine.rs | 2 - ferveo-tdec/src/decryption.rs | 3 - ferveo-tdec/src/key_share.rs | 11 +- ferveo-tdec/src/lib.rs | 8 +- ferveo/src/api.rs | 706 +++++++++++++++++++++++++++--- ferveo/src/bindings_python.rs | 9 +- ferveo/src/bindings_wasm.rs | 1 - ferveo/src/dkg.rs | 47 +- ferveo/src/lib.rs | 81 ++-- ferveo/src/pvss.rs | 42 +- ferveo/src/refresh.rs | 107 ++--- ferveo/src/validator.rs | 9 +- 16 files changed, 819 insertions(+), 222 deletions(-) diff --git a/ferveo-common/src/keypair.rs b/ferveo-common/src/keypair.rs index 9b251125..582a0cb8 100644 --- a/ferveo-common/src/keypair.rs +++ b/ferveo-common/src/keypair.rs @@ -6,7 +6,10 @@ use ark_std::{ rand::{prelude::StdRng, RngCore, SeedableRng}, UniformRand, }; -use generic_array::{typenum::U96, GenericArray}; +use generic_array::{ + typenum::{Unsigned, U96}, + GenericArray, +}; use serde::*; use serde_with::serde_as; @@ -55,7 +58,7 @@ impl PublicKey { } pub fn serialized_size() -> usize { - 96 + U96::to_usize() } } @@ -106,7 +109,6 @@ impl Ord for Keypair { impl Keypair { /// Returns the public session key for the publicly verifiable DKG participant - pub fn public_key(&self) -> PublicKey { PublicKey:: { encryption_key: E::G2Affine::generator() @@ -116,7 +118,6 @@ impl Keypair { } /// Creates a new ephemeral session key for participating in the DKG - pub fn new(rng: &mut R) -> Self { Self { decryption_key: E::ScalarField::rand(rng), diff --git a/ferveo-python/ferveo/__init__.py b/ferveo-python/ferveo/__init__.py index fd906e54..58a3a140 100644 --- a/ferveo-python/ferveo/__init__.py +++ b/ferveo-python/ferveo/__init__.py @@ -39,4 +39,5 @@ DuplicatedShareIndex, NoTranscriptsToAggregate, InvalidAggregateVerificationParameters, + UnknownValidator, ) diff --git a/ferveo-python/ferveo/__init__.pyi b/ferveo-python/ferveo/__init__.pyi index ba7e7403..894f71ed 100644 --- a/ferveo-python/ferveo/__init__.pyi +++ b/ferveo-python/ferveo/__init__.pyi @@ -219,3 +219,6 @@ class NoTranscriptsToAggregate(Exception): class InvalidAggregateVerificationParameters(Exception): pass + +class UnknownValidator(Exception): + pass diff --git a/ferveo-tdec/benches/tpke.rs b/ferveo-tdec/benches/tpke.rs index 0bbba434..db8a7424 100644 --- a/ferveo-tdec/benches/tpke.rs +++ b/ferveo-tdec/benches/tpke.rs @@ -550,7 +550,7 @@ pub fn bench_decryption_share_validity_checks(c: &mut Criterion) { // for &shares_num in NUM_SHARES_CASES.iter() { // let setup = SetupSimple::new(shares_num, msg_size, rng); // let threshold = setup.shared.threshold; -// let polynomial = make_random_polynomial_with_root::( +// let polynomial = create_random_polynomial_with_root::( // threshold - 1, // &Fr::zero(), // rng, diff --git a/ferveo-tdec/src/combine.rs b/ferveo-tdec/src/combine.rs index f9d8ddbb..a46477fb 100644 --- a/ferveo-tdec/src/combine.rs +++ b/ferveo-tdec/src/combine.rs @@ -56,8 +56,6 @@ pub fn prepare_combine_fast( .collect::>() } -// TODO: Combine `tpke::prepare_combine_simple` and `tpke::share_combine_simple` into -// one function and expose it in the tpke::api? pub fn prepare_combine_simple( domain: &[E::ScalarField], ) -> Vec { diff --git a/ferveo-tdec/src/decryption.rs b/ferveo-tdec/src/decryption.rs index eb9068bd..dc93fee4 100644 --- a/ferveo-tdec/src/decryption.rs +++ b/ferveo-tdec/src/decryption.rs @@ -37,9 +37,6 @@ impl ValidatorShareChecksum { // C_i = dk_i^{-1} * U let checksum = ciphertext_header .commitment - // TODO: Should we panic here? I think we should since that would mean that the decryption key is invalid. - // And so, the validator should not be able to create a decryption share. - // And so, the validator should remake their keypair. .mul( validator_decryption_key .inverse() diff --git a/ferveo-tdec/src/key_share.rs b/ferveo-tdec/src/key_share.rs index 64a9e3e4..4c164f6e 100644 --- a/ferveo-tdec/src/key_share.rs +++ b/ferveo-tdec/src/key_share.rs @@ -3,8 +3,11 @@ use std::ops::Mul; use ark_ec::{pairing::Pairing, AffineRepr, CurveGroup}; use ark_ff::One; use ark_std::UniformRand; +use ferveo_common::serialization; use rand_core::RngCore; -use zeroize::ZeroizeOnDrop; +use serde::{Deserialize, Serialize}; +use serde_with::serde_as; +use zeroize::{Zeroize, ZeroizeOnDrop}; #[derive(Debug, Clone)] // TODO: Should we rename it to PublicKey or SharedPublicKey? @@ -49,9 +52,13 @@ impl BlindedKeyShare { } } -#[derive(Debug, Clone, PartialEq, Eq, ZeroizeOnDrop)] +#[serde_as] +#[derive( + Clone, Debug, PartialEq, Eq, Serialize, Deserialize, Zeroize, ZeroizeOnDrop, +)] pub struct PrivateKeyShare { // TODO: Replace with a tuple? + #[serde_as(as = "serialization::SerdeAs")] pub private_key_share: E::G2Affine, } diff --git a/ferveo-tdec/src/lib.rs b/ferveo-tdec/src/lib.rs index b5ffed22..322d1bf9 100644 --- a/ferveo-tdec/src/lib.rs +++ b/ferveo-tdec/src/lib.rs @@ -290,7 +290,7 @@ pub mod test_common { setup_simple::(shares_num, shares_num, rng) } - pub fn make_shared_secret( + pub fn create_shared_secret( pub_contexts: &[PublicDecryptionContextSimple], decryption_shares: &[DecryptionShareSimple], ) -> SharedSecret { @@ -308,7 +308,7 @@ mod tests { use ark_std::{test_rng, UniformRand}; use ferveo_common::{FromBytes, ToBytes}; - use crate::test_common::{make_shared_secret, setup_simple, *}; + use crate::test_common::{create_shared_secret, setup_simple, *}; type E = ark_bls12_381::Bls12_381; type TargetField = ::TargetField; @@ -481,7 +481,7 @@ mod tests { let pub_contexts = contexts[0].public_decryption_contexts[..threshold].to_vec(); let shared_secret = - make_shared_secret(&pub_contexts, &decryption_shares); + create_shared_secret(&pub_contexts, &decryption_shares); test_ciphertext_validation_fails( &msg, @@ -495,7 +495,7 @@ mod tests { let decryption_shares = decryption_shares[..threshold - 1].to_vec(); let pub_contexts = pub_contexts[..threshold - 1].to_vec(); let shared_secret = - make_shared_secret(&pub_contexts, &decryption_shares); + create_shared_secret(&pub_contexts, &decryption_shares); let result = decrypt_with_shared_secret(&ciphertext, aad, &shared_secret, g_inv); diff --git a/ferveo/src/api.rs b/ferveo/src/api.rs index 36cdc7c5..fe25a931 100644 --- a/ferveo/src/api.rs +++ b/ferveo/src/api.rs @@ -1,5 +1,6 @@ use std::{fmt, io}; +use ark_ec::CurveGroup; use ark_poly::{EvaluationDomain, GeneralEvaluationDomain}; use ark_serialize::{CanonicalDeserialize, CanonicalSerialize}; use ark_std::UniformRand; @@ -15,7 +16,7 @@ use generic_array::{ typenum::{Unsigned, U48}, GenericArray, }; -use rand::RngCore; +use rand::{thread_rng, RngCore}; use serde::{Deserialize, Serialize}; use serde_with::serde_as; @@ -25,8 +26,8 @@ use crate::bindings_python; use crate::bindings_wasm; pub use crate::EthereumAddress; use crate::{ - do_verify_aggregation, Error, Message, PVSSMap, PubliclyVerifiableParams, - PubliclyVerifiableSS, Result, + do_verify_aggregation, DomainPoint, Error, Message, PVSSMap, + PubliclyVerifiableParams, PubliclyVerifiableSS, Result, }; pub type PublicKey = ferveo_common::PublicKey; @@ -95,7 +96,6 @@ impl Ciphertext { } } -#[serde_as] // TODO: Redundant serde_as? #[derive(Clone, Debug, Serialize, Deserialize, PartialEq, Eq)] pub struct CiphertextHeader(ferveo_tdec::api::CiphertextHeader); @@ -229,7 +229,6 @@ impl Dkg { pub fn generate_transcript( &mut self, rng: &mut R, - // TODO: Replace with Message::Deal? ) -> Result { match self.0.share(rng) { Ok(Message::Deal(transcript)) => Ok(transcript), @@ -241,7 +240,6 @@ impl Dkg { pub fn aggregate_transcripts( &mut self, messages: &[ValidatorMessage], - // TODO: Replace with Message::Aggregate? ) -> Result { // We must use `deal` here instead of to produce AggregatedTranscript instead of simply // creating an AggregatedTranscript from the messages, because `deal` also updates the @@ -266,6 +264,14 @@ impl Dkg { g1_inv: self.0.pvss_params.g_inv(), } } + + pub fn me(&self) -> &Validator { + &self.0.me + } + + pub fn domain_points(&self) -> Vec> { + self.0.domain_points() + } } fn make_pvss_map(messages: &[ValidatorMessage]) -> PVSSMap { @@ -330,6 +336,7 @@ impl AggregatedTranscript { Ok(is_valid) } + // TODO: Consider deprecating in favor of PrivateKeyShare::create_decryption_share_simple pub fn create_decryption_share_precomputed( &self, dkg: &Dkg, @@ -346,7 +353,7 @@ impl AggregatedTranscript { dkg.0.dkg_params.security_threshold(), )); } - self.0.make_decryption_share_simple_precomputed( + self.0.create_decryption_share_simple_precomputed( &ciphertext_header.0, aad, validator_keypair, @@ -356,6 +363,7 @@ impl AggregatedTranscript { ) } + // TODO: Consider deprecating in favor of PrivateKeyShare::create_decryption_share_simple pub fn create_decryption_share_simple( &self, dkg: &Dkg, @@ -363,7 +371,7 @@ impl AggregatedTranscript { aad: &[u8], validator_keypair: &Keypair, ) -> Result { - let share = self.0.make_decryption_share_simple( + let share = self.0.create_decryption_share_simple( &ciphertext_header.0, aad, validator_keypair, @@ -376,6 +384,19 @@ impl AggregatedTranscript { domain_point, }) } + + pub fn get_private_key_share( + &self, + validator_keypair: &Keypair, + share_index: u32, + ) -> Result { + Ok(PrivateKeyShare( + self.0 + .decrypt_private_key_share(validator_keypair, share_index)? + .0 + .clone(), + )) + } } #[serde_as] @@ -386,6 +407,7 @@ pub struct DecryptionShareSimple { domain_point: Fr, } +// TODO: Deprecate? #[serde_as] #[derive(Clone, Debug, PartialEq, Serialize, Deserialize)] pub struct DkgPublicParameters { @@ -425,14 +447,209 @@ pub fn combine_shares_simple(shares: &[DecryptionShareSimple]) -> SharedSecret { #[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)] pub struct SharedSecret(pub ferveo_tdec::api::SharedSecret); +#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)] +// TODO: serde is failing to serialize E = ark_bls12_381::Bls12_381 +// pub struct ShareRecoveryUpdate(pub crate::refresh::ShareRecoveryUpdate); +pub struct ShareRecoveryUpdate(pub ferveo_tdec::PrivateKeyShare); + +impl ShareRecoveryUpdate { + // TODO: There are two recovery scenarios: at random and at a specific point. Do we ever want + // to recover at a specific point? What scenario would that be? Validator rotation? + pub fn create_share_updates( + // TODO: Decouple from Dkg? We don't need any specific Dkg instance here, just some params etc + dkg: &Dkg, + x_r: &DomainPoint, + ) -> Result> { + let rng = &mut thread_rng(); + let updates = + crate::refresh::ShareRecoveryUpdate::create_share_updates( + &dkg.0.domain_points(), + &dkg.0.pvss_params.h.into_affine(), + x_r, + dkg.0.dkg_params.security_threshold(), + rng, + ) + .iter() + .map(|update| ShareRecoveryUpdate(update.0.clone())) + .collect(); + Ok(updates) + } + + pub fn to_bytes(&self) -> Result> { + bincode::serialize(self).map_err(|e| e.into()) + } + + pub fn from_bytes(bytes: &[u8]) -> Result { + bincode::deserialize(bytes).map_err(|e| e.into()) + } +} + +#[serde_as] +#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)] +pub struct ShareRefreshUpdate(pub ferveo_tdec::PrivateKeyShare); + +impl ShareRefreshUpdate { + pub fn create_share_updates(dkg: &Dkg) -> Result> { + let rng = &mut thread_rng(); + let updates = crate::refresh::ShareRefreshUpdate::create_share_updates( + &dkg.0.domain_points(), + &dkg.0.pvss_params.h.into_affine(), + dkg.0.dkg_params.security_threshold(), + rng, + ) + .iter() + .map(|update| ShareRefreshUpdate(update.0.clone())) + .collect(); + Ok(updates) + } + + pub fn to_bytes(&self) -> Result> { + bincode::serialize(self).map_err(|e| e.into()) + } + + pub fn from_bytes(bytes: &[u8]) -> Result { + bincode::deserialize(bytes).map_err(|e| e.into()) + } +} + +#[serde_as] +#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)] +pub struct UpdatedPrivateKeyShare(pub ferveo_tdec::PrivateKeyShare); + +impl UpdatedPrivateKeyShare { + pub fn into_private_key_share(self) -> PrivateKeyShare { + PrivateKeyShare(self.0) + } + pub fn to_bytes(&self) -> Result> { + bincode::serialize(self).map_err(|e| e.into()) + } + + pub fn from_bytes(bytes: &[u8]) -> Result { + bincode::deserialize(bytes).map_err(|e| e.into()) + } +} + +#[serde_as] +#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)] +pub struct PrivateKeyShare(pub ferveo_tdec::PrivateKeyShare); + +impl PrivateKeyShare { + pub fn create_updated_private_key_share_for_recovery( + &self, + share_updates: &[ShareRecoveryUpdate], + ) -> Result { + let share_updates: Vec<_> = share_updates + .iter() + .map(|update| crate::refresh::ShareRecoveryUpdate(update.0.clone())) + .collect(); + // TODO: Remove this wrapping after figuring out serde_as + let updated_key_share = crate::PrivateKeyShare(self.0.clone()) + .create_updated_key_share(&share_updates); + Ok(UpdatedPrivateKeyShare(updated_key_share.0.clone())) + } + + pub fn create_updated_private_key_share_for_refresh( + &self, + share_updates: &[ShareRefreshUpdate], + ) -> Result { + let share_updates: Vec<_> = share_updates + .iter() + .map(|update| crate::refresh::ShareRefreshUpdate(update.0.clone())) + .collect(); + let updated_key_share = crate::PrivateKeyShare(self.0.clone()) + .create_updated_key_share(&share_updates); + Ok(UpdatedPrivateKeyShare(updated_key_share.0.clone())) + } + + /// Recover a private key share from updated private key shares + pub fn recover_share_from_updated_private_shares( + x_r: &DomainPoint, + domain_points: &[DomainPoint], + updated_shares: &[UpdatedPrivateKeyShare], + ) -> Result { + let updated_shares: Vec<_> = updated_shares + .iter() + // TODO: Remove this wrapping after figuring out serde_as + .map(|s| crate::refresh::UpdatedPrivateKeyShare(s.0.clone())) + .collect(); + let share = + crate::PrivateKeyShare::recover_share_from_updated_private_shares( + x_r, + domain_points, + &updated_shares[..], + ); + Ok(PrivateKeyShare(share.0.clone())) + } + + /// Make a decryption share (simple variant) for a given ciphertext + pub fn create_decryption_share_simple( + &self, + dkg: &Dkg, + ciphertext_header: &CiphertextHeader, + validator_keypair: &Keypair, + aad: &[u8], + ) -> Result { + let share = crate::PrivateKeyShare(self.0.clone()) + .create_decryption_share_simple( + &ciphertext_header.0, + aad, + validator_keypair, + &dkg.public_params().g1_inv, + )?; + let domain_point = dkg.0.get_domain_point(dkg.0.me.share_index)?; + Ok(DecryptionShareSimple { + share, + domain_point, + }) + } + + /// Make a decryption share (precomputed variant) for a given ciphertext + pub fn create_decryption_share_simple_precomputed( + &self, + ciphertext_header: &CiphertextHeader, + aad: &[u8], + validator_keypair: &Keypair, + share_index: u32, + domain_points: &[DomainPoint], + ) -> Result { + let dkg_public_params = DkgPublicParameters::default(); + let share = crate::PrivateKeyShare(self.0.clone()) + .create_decryption_share_simple_precomputed( + &ciphertext_header.0, + aad, + validator_keypair, + share_index, + domain_points, + &dkg_public_params.g1_inv, + )?; + Ok(share) + } + + pub fn to_bytes(&self) -> Result> { + bincode::serialize(self).map_err(|e| e.into()) + } + + pub fn from_bytes(bytes: &[u8]) -> Result { + bincode::deserialize(bytes).map_err(|e| e.into()) + } +} + #[cfg(test)] mod test_ferveo_api { + use std::collections::HashMap; + use ferveo_tdec::SecretBox; - use itertools::izip; - use rand::{prelude::StdRng, SeedableRng}; + use itertools::{izip, Itertools}; + use rand::{ + prelude::{SliceRandom, StdRng}, + SeedableRng, + }; use test_case::test_case; - use crate::{api::*, test_common::*}; + use crate::{ + api::*, + test_common::{gen_address, gen_keypairs, AAD, MSG, TAU}, + }; type TestInputs = (Vec, Vec, Vec); @@ -455,7 +672,7 @@ mod test_ferveo_api { .collect::>(); // Each validator holds their own DKG instance and generates a transcript every - // every validator, including themselves + // validator, including themselves let messages: Vec<_> = validators .iter() .map(|sender| { @@ -519,33 +736,36 @@ mod test_ferveo_api { .unwrap(); // Having aggregated the transcripts, the validators can now create decryption shares - let decryption_shares: Vec<_> = izip!(&validators, &validator_keypairs) - .map(|(validator, validator_keypair)| { - // Each validator holds their own instance of DKG and creates their own aggregate - let mut dkg = Dkg::new( - TAU, - shares_num, - security_threshold, - &validators, - validator, - ) - .unwrap(); - let aggregate = dkg.aggregate_transcripts(&messages).unwrap(); - assert!(pvss_aggregated - .verify(validators_num, &messages) - .unwrap()); - - // And then each validator creates their own decryption share - aggregate - .create_decryption_share_precomputed( - &dkg, - &ciphertext.header().unwrap(), - AAD, - validator_keypair, + let mut decryption_shares: Vec<_> = + izip!(&validators, &validator_keypairs) + .map(|(validator, validator_keypair)| { + // Each validator holds their own instance of DKG and creates their own aggregate + let mut dkg = Dkg::new( + TAU, + shares_num, + security_threshold, + &validators, + validator, ) - .unwrap() - }) - .collect(); + .unwrap(); + let aggregate = + dkg.aggregate_transcripts(&messages).unwrap(); + assert!(pvss_aggregated + .verify(validators_num, &messages) + .unwrap()); + + // And then each validator creates their own decryption share + aggregate + .create_decryption_share_precomputed( + &dkg, + &ciphertext.header().unwrap(), + AAD, + validator_keypair, + ) + .unwrap() + }) + .collect(); + decryption_shares.shuffle(rng); // Now, the decryption share can be used to decrypt the ciphertext // This part is part of the client API @@ -561,9 +781,9 @@ mod test_ferveo_api { // Since we're using a precomputed variant, we need all the shares to be able to decrypt // So if we remove one share, we should not be able to decrypt + let decryption_shares = decryption_shares[..shares_num as usize - 1].to_vec(); - let shared_secret = share_combine_precomputed(&decryption_shares); let result = decrypt_with_shared_secret( &ciphertext, @@ -578,7 +798,6 @@ mod test_ferveo_api { #[test_case(4, 6; "number of validators greater than the number of shares")] fn test_server_api_tdec_simple(shares_num: u32, validators_num: u32) { let rng = &mut StdRng::seed_from_u64(0); - let security_threshold = shares_num / 2 + 1; let (messages, validators, validator_keypairs) = make_test_inputs( @@ -611,29 +830,34 @@ mod test_ferveo_api { encrypt(SecretBox::new(MSG.to_vec()), AAD, &public_key).unwrap(); // Having aggregated the transcripts, the validators can now create decryption shares - let decryption_shares: Vec<_> = izip!(&validators, &validator_keypairs) - .map(|(validator, validator_keypair)| { - // Each validator holds their own instance of DKG and creates their own aggregate - let mut dkg = Dkg::new( - TAU, - shares_num, - security_threshold, - &validators, - validator, - ) - .unwrap(); - let aggregate = dkg.aggregate_transcripts(&messages).unwrap(); - assert!(aggregate.verify(validators_num, &messages).unwrap()); - aggregate - .create_decryption_share_simple( - &dkg, - &ciphertext.header().unwrap(), - AAD, - validator_keypair, + let mut decryption_shares: Vec<_> = + izip!(&validators, &validator_keypairs) + .map(|(validator, validator_keypair)| { + // Each validator holds their own instance of DKG and creates their own aggregate + let mut dkg = Dkg::new( + TAU, + shares_num, + security_threshold, + &validators, + validator, ) - .unwrap() - }) - .collect(); + .unwrap(); + let aggregate = + dkg.aggregate_transcripts(&messages).unwrap(); + assert!(aggregate + .verify(validators_num, &messages) + .unwrap()); + aggregate + .create_decryption_share_simple( + &dkg, + &ciphertext.header().unwrap(), + AAD, + validator_keypair, + ) + .unwrap() + }) + .collect(); + decryption_shares.shuffle(rng); // Now, the decryption share can be used to decrypt the ciphertext // This part is part of the client API @@ -796,4 +1020,362 @@ mod test_ferveo_api { let result = bad_aggregate.verify(validators_num, messages); assert!(result.is_err()); } + + fn make_share_update_test_inputs( + shares_num: u32, + validators_num: u32, + rng: &mut StdRng, + security_threshold: u32, + ) -> ( + Vec, + Vec, + Vec, + Vec, + CiphertextHeader, + SharedSecret, + ) { + let (messages, validators, validator_keypairs) = make_test_inputs( + rng, + TAU, + security_threshold, + shares_num, + validators_num, + ); + let mut dkgs = validators + .iter() + .map(|validator| { + Dkg::new( + TAU, + shares_num, + security_threshold, + &validators, + validator, + ) + .unwrap() + }) + .collect::>(); + let pvss_aggregated = dkgs[0].aggregate_transcripts(&messages).unwrap(); + assert!(pvss_aggregated.verify(validators_num, &messages).unwrap()); + + // Create an initial shared secret for testing purposes + let public_key = &dkgs[0].public_key(); + let ciphertext = + encrypt(SecretBox::new(MSG.to_vec()), AAD, public_key).unwrap(); + let ciphertext_header = ciphertext.header().unwrap(); + let (_, _, old_shared_secret) = + crate::test_dkg_full::create_shared_secret_simple_tdec( + &dkgs[0].0, + AAD, + &ciphertext_header.0, + validator_keypairs.as_slice(), + ); + + ( + messages, + validators, + validator_keypairs, + dkgs, + ciphertext_header, + SharedSecret(old_shared_secret), + ) + } + + #[test_case(4, 4, true; "number of shares (validators) is a power of 2")] + #[test_case(7, 7, true; "number of shares (validators) is not a power of 2")] + #[test_case(4, 6, true; "number of validators greater than the number of shares")] + #[test_case(4, 6, false; "recovery at a specific point")] + fn test_dkg_simple_tdec_share_recovery( + shares_num: u32, + validators_num: u32, + recover_at_random_point: bool, + ) { + let rng = &mut StdRng::seed_from_u64(0); + let security_threshold = shares_num / 2 + 1; + + let ( + mut messages, + mut validators, + mut validator_keypairs, + mut dkgs, + ciphertext_header, + old_shared_secret, + ) = make_share_update_test_inputs( + shares_num, + validators_num, + rng, + security_threshold, + ); + + // We assume that all participants have the same aggregate, and that participants created + // their own aggregates before the off-boarding of the validator + // If we didn't create this aggregate here, we risk having a "dangling validator message" + // later when we off-board the validator + let aggregated_transcript = + dkgs[0].clone().aggregate_transcripts(&messages).unwrap(); + assert!(aggregated_transcript + .verify(validators_num, &messages) + .unwrap()); + + // We need to save this domain point to be user in the recovery testing scenario + let mut domain_points = dkgs[0].domain_points(); + let removed_domain_point = domain_points.pop().unwrap(); + + // Remove one participant from the contexts and all nested structure + // to simulate off-boarding a validator + messages.pop().unwrap(); + dkgs.pop(); + validator_keypairs.pop().unwrap(); + + let removed_validator = validators.pop().unwrap(); + for dkg in dkgs.iter_mut() { + dkg.0 + .offboard_validator(&removed_validator.address) + .expect("Unable to off-board a validator from the DKG context"); + } + + // Now, we're going to recover a new share at a random point or at a specific point + // and check that the shared secret is still the same. + let x_r = if recover_at_random_point { + // Onboarding a validator with a completely new private key share + DomainPoint::::rand(rng) + } else { + // Onboarding a validator with a private key share recovered from the removed validator + removed_domain_point + }; + + // Each participant prepares an update for each other participant + let share_updates = dkgs + .iter() + .map(|validator_dkg| { + let share_update = ShareRecoveryUpdate::create_share_updates( + validator_dkg, + &x_r, + ) + .unwrap(); + (validator_dkg.me().address.clone(), share_update) + }) + .collect::>(); + + // Participants share updates and update their shares + + // Now, every participant separately: + let updated_shares: Vec<_> = dkgs + .iter() + .map(|validator_dkg| { + // Current participant receives updates from other participants + let updates_for_participant: Vec<_> = share_updates + .values() + .map(|updates| { + updates + .get(validator_dkg.me().share_index as usize) + .unwrap() + }) + .cloned() + .collect(); + + // Each validator uses their decryption key to update their share + let validator_keypair = validator_keypairs + .get(validator_dkg.me().share_index as usize) + .unwrap(); + + // And creates updated private key shares + aggregated_transcript + .get_private_key_share( + validator_keypair, + validator_dkg.me().share_index, + ) + .unwrap() + .create_updated_private_key_share_for_recovery( + &updates_for_participant, + ) + .unwrap() + }) + .collect(); + + // Now, we have to combine new share fragments into a new share + let recovered_key_share = + PrivateKeyShare::recover_share_from_updated_private_shares( + &x_r, + &domain_points, + &updated_shares, + ) + .unwrap(); + + // Get decryption shares from remaining participants + let mut decryption_shares: Vec = + validator_keypairs + .iter() + .zip_eq(dkgs.iter()) + .map(|(validator_keypair, validator_dkg)| { + aggregated_transcript + .create_decryption_share_simple( + validator_dkg, + &ciphertext_header, + AAD, + validator_keypair, + ) + .unwrap() + }) + .collect(); + decryption_shares.shuffle(rng); + + // In order to test the recovery, we need to create a new decryption share from the recovered + // private key share. To do that, we need a new validator + + // Let's create and onboard a new validator + // TODO: Add test scenarios for onboarding and offboarding validators + let new_validator_keypair = Keypair::random(); + // Normally, we would get these from the Coordinator: + let new_validator_share_index = removed_validator.share_index; + let new_validator = Validator { + address: gen_address(new_validator_share_index as usize), + public_key: new_validator_keypair.public_key(), + share_index: new_validator_share_index, + }; + validators.push(new_validator.clone()); + let new_validator_dkg = Dkg::new( + TAU, + shares_num, + security_threshold, + &validators, + &new_validator, + ) + .unwrap(); + + let new_decryption_share = recovered_key_share + .create_decryption_share_simple( + &new_validator_dkg, + &ciphertext_header, + &new_validator_keypair, + AAD, + ) + .unwrap(); + decryption_shares.push(new_decryption_share); + domain_points.push(x_r); + assert_eq!(domain_points.len(), validators_num as usize); + assert_eq!(decryption_shares.len(), validators_num as usize); + + let domain_points = &domain_points[..security_threshold as usize]; + let decryption_shares = + &decryption_shares[..security_threshold as usize]; + assert_eq!(domain_points.len(), security_threshold as usize); + assert_eq!(decryption_shares.len(), security_threshold as usize); + + let new_shared_secret = combine_shares_simple(decryption_shares); + assert_eq!( + old_shared_secret, new_shared_secret, + "Shared secret reconstruction failed" + ); + } + + #[test_case(4, 4; "number of shares (validators) is a power of 2")] + #[test_case(7, 7; "number of shares (validators) is not a power of 2")] + #[test_case(4, 6; "number of validators greater than the number of shares")] + fn test_dkg_simple_tdec_share_refresh( + shares_num: u32, + validators_num: u32, + ) { + let rng = &mut StdRng::seed_from_u64(0); + let security_threshold = shares_num / 2 + 1; + + let ( + messages, + _validators, + validator_keypairs, + dkgs, + ciphertext_header, + old_shared_secret, + ) = make_share_update_test_inputs( + shares_num, + validators_num, + rng, + security_threshold, + ); + + // Each participant prepares an update for each other participant + let share_updates = dkgs + .iter() + .map(|validator_dkg| { + let share_update = + ShareRefreshUpdate::create_share_updates(validator_dkg) + .unwrap(); + (validator_dkg.me().address.clone(), share_update) + }) + .collect::>(); + + // Participants share updates and update their shares + + // Now, every participant separately: + let updated_shares: Vec<_> = dkgs + .iter() + .map(|validator_dkg| { + // Current participant receives updates from other participants + let updates_for_participant: Vec<_> = share_updates + .values() + .map(|updates| { + updates + .get(validator_dkg.me().share_index as usize) + .unwrap() + }) + .cloned() + .collect(); + + // Each validator uses their decryption key to update their share + let validator_keypair = validator_keypairs + .get(validator_dkg.me().share_index as usize) + .unwrap(); + + // And creates updated private key shares + // We need an aggregate for that + let aggregate = validator_dkg + .clone() + .aggregate_transcripts(&messages) + .unwrap(); + assert!(aggregate.verify(validators_num, &messages).unwrap()); + + aggregate + .get_private_key_share( + validator_keypair, + validator_dkg.me().share_index, + ) + .unwrap() + .create_updated_private_key_share_for_refresh( + &updates_for_participant, + ) + .unwrap() + }) + .collect(); + + // Participants create decryption shares + let mut decryption_shares: Vec = + validator_keypairs + .iter() + .zip_eq(dkgs.iter()) + .map(|(validator_keypair, validator_dkg)| { + let pks = updated_shares + .get(validator_dkg.me().share_index as usize) + .unwrap() + .clone() + .into_private_key_share(); + pks.create_decryption_share_simple( + validator_dkg, + &ciphertext_header, + validator_keypair, + AAD, + ) + .unwrap() + }) + .collect(); + decryption_shares.shuffle(rng); + + let decryption_shares = + &decryption_shares[..security_threshold as usize]; + assert_eq!(decryption_shares.len(), security_threshold as usize); + + let new_shared_secret = combine_shares_simple(decryption_shares); + assert_eq!( + old_shared_secret, new_shared_secret, + "Shared secret reconstruction failed" + ); + } } diff --git a/ferveo/src/bindings_python.rs b/ferveo/src/bindings_python.rs index fe534af8..f6fce72c 100644 --- a/ferveo/src/bindings_python.rs +++ b/ferveo/src/bindings_python.rs @@ -120,7 +120,12 @@ impl From for PyErr { InvalidAggregateVerificationParameters::new_err(format!( "validators_num: {validators_num}, messages_num: {messages_num}" )) - } + }, + Error::UnknownValidator(validator) => { + UnknownValidator::new_err(validator.to_string()) + }, + // Remember to create Python exceptions using `create_exception!` macro, and to register them in the + // `make_ferveo_py_module` function. You will have to update the `ferveo/__init__.{py, pyi}` files too. }, _ => default(), } @@ -168,6 +173,7 @@ create_exception!( InvalidAggregateVerificationParameters, PyValueError ); +create_exception!(exceptions, UnknownValidator, PyValueError); fn from_py_bytes(bytes: &[u8]) -> PyResult { T::from_bytes(bytes) @@ -782,6 +788,7 @@ pub fn make_ferveo_py_module(py: Python<'_>, m: &PyModule) -> PyResult<()> { "InvalidAggregateVerificationParameters", py.get_type::(), )?; + m.add("UnknownValidator", py.get_type::())?; Ok(()) } diff --git a/ferveo/src/bindings_wasm.rs b/ferveo/src/bindings_wasm.rs index 07e22e3f..3c885269 100644 --- a/ferveo/src/bindings_wasm.rs +++ b/ferveo/src/bindings_wasm.rs @@ -460,7 +460,6 @@ impl Validator { } } -// TODO: Consider removing and replacing with tuple #[derive(TryFromJsValue)] #[wasm_bindgen] #[derive(Clone, Debug, derive_more::AsRef, derive_more::From)] diff --git a/ferveo/src/dkg.rs b/ferveo/src/dkg.rs index bc4acdfd..1ee6ec6e 100644 --- a/ferveo/src/dkg.rs +++ b/ferveo/src/dkg.rs @@ -11,10 +11,12 @@ use serde_with::serde_as; use crate::{ aggregate, assert_no_share_duplicates, AggregatedPvss, Error, - EthereumAddress, PrivateKeyShare, PubliclyVerifiableParams, - PubliclyVerifiableSS, Result, Validator, + EthereumAddress, PubliclyVerifiableParams, PubliclyVerifiableSS, Result, + Validator, }; +pub type DomainPoint = ::ScalarField; + #[derive(Copy, Clone, Debug, Serialize, Deserialize)] pub struct DkgParams { tau: u32, @@ -89,7 +91,7 @@ impl DkgState { } } -/// The DKG context that holds all of the local state for participating in the DKG +/// The DKG context that holds all the local state for participating in the DKG // TODO: Consider removing Clone to avoid accidentally NOT-mutating state. // Currently, we're assuming that the DKG is only mutated by the owner of the instance. // Consider removing Clone after finalizing ferveo::api @@ -116,13 +118,12 @@ impl PubliclyVerifiableDkg { dkg_params: &DkgParams, me: &Validator, ) -> Result { + assert_no_share_duplicates(validators)?; + let domain = ark_poly::GeneralEvaluationDomain::::new( validators.len(), ) .expect("unable to construct domain"); - - assert_no_share_duplicates(validators)?; - let validators: ValidatorsMap = validators .iter() .map(|validator| (validator.address.clone(), validator.clone())) @@ -165,7 +166,7 @@ impl PubliclyVerifiableDkg { match self.state { DkgState::Sharing { .. } | DkgState::Dealt => { let vss = PubliclyVerifiableSS::::new( - &E::ScalarField::rand(rng), + &DomainPoint::::rand(rng), self, rng, )?; @@ -203,29 +204,28 @@ impl PubliclyVerifiableDkg { } /// Return a domain point for the share_index - pub fn get_domain_point(&self, share_index: u32) -> Result { - let domain_points = self.domain_points(); - domain_points + pub fn get_domain_point(&self, share_index: u32) -> Result> { + self.domain_points() .get(share_index as usize) .ok_or_else(|| Error::InvalidShareIndex(share_index)) .copied() } /// Return an appropriate amount of domain points for the DKG - pub fn domain_points(&self) -> Vec { + pub fn domain_points(&self) -> Vec> { self.domain.elements().take(self.validators.len()).collect() } - /// Return a private key for the share_index - pub fn get_private_key_share( - &self, - keypair: &ferveo_common::Keypair, - ) -> Result> { - // TODO: Use self.aggregate upon simplifying Message handling - let pvss_list = self.vss.values().cloned().collect::>(); - aggregate(&pvss_list) - .unwrap() - .decrypt_private_key_share(keypair, self.me.share_index) + pub fn offboard_validator( + &mut self, + address: &EthereumAddress, + ) -> Result> { + if let Some(validator) = self.validators.remove(address) { + self.vss.remove(address); + Ok(validator) + } else { + Err(Error::UnknownValidator(address.clone())) + } } pub fn verify_message( @@ -274,9 +274,8 @@ impl PubliclyVerifiableDkg { } } - /// After consensus has agreed to include a verified - /// message on the blockchain, we apply the chains - /// to the state machine + /// After consensus has agreed to include a verified message on the blockchain, + /// we apply the chains to the state machine pub fn apply_message( &mut self, sender: &Validator, diff --git a/ferveo/src/lib.rs b/ferveo/src/lib.rs index 8ef4c8e7..ed39fc4b 100644 --- a/ferveo/src/lib.rs +++ b/ferveo/src/lib.rs @@ -121,6 +121,10 @@ pub enum Error { /// The number of messages may not be greater than the number of validators #[error("Invalid aggregate verification parameters: number of validators {0}, number of messages: {1}")] InvalidAggregateVerificationParameters(u32, u32), + + /// Validator not found in the DKG set of validators + #[error("Validator not found: {0}")] + UnknownValidator(EthereumAddress), } pub type Result = std::result::Result; @@ -146,7 +150,7 @@ mod test_dkg_full { use super::*; use crate::test_common::*; - fn make_shared_secret_simple_tdec( + pub fn create_shared_secret_simple_tdec( dkg: &PubliclyVerifiableDkg, aad: &[u8], ciphertext_header: &ferveo_tdec::CiphertextHeader, @@ -168,7 +172,7 @@ mod test_dkg_full { .get_validator(&validator_keypair.public_key()) .unwrap(); pvss_aggregated - .make_decryption_share_simple( + .create_decryption_share_simple( ciphertext_header, aad, validator_keypair, @@ -186,9 +190,6 @@ mod test_dkg_full { .collect::>(); assert_eq!(domain_points.len(), decryption_shares.len()); - // TODO: Consider refactor this part into ferveo_tdec::combine_simple and expose it - // as a public API in ferveo_tdec::api - let lagrange_coeffs = ferveo_tdec::prepare_combine_simple::(domain_points); let shared_secret = ferveo_tdec::share_combine_simple::( @@ -221,7 +222,7 @@ mod test_dkg_full { ) .unwrap(); - let (_, _, shared_secret) = make_shared_secret_simple_tdec( + let (_, _, shared_secret) = create_shared_secret_simple_tdec( &dkg, AAD, &ciphertext.header().unwrap(), @@ -277,7 +278,7 @@ mod test_dkg_full { .get_validator(&validator_keypair.public_key()) .unwrap(); pvss_aggregated - .make_decryption_share_simple_precomputed( + .create_decryption_share_simple_precomputed( &ciphertext.header().unwrap(), AAD, validator_keypair, @@ -330,7 +331,7 @@ mod test_dkg_full { .unwrap(); let (pvss_aggregated, decryption_shares, _) = - make_shared_secret_simple_tdec( + create_shared_secret_simple_tdec( &dkg, AAD, &ciphertext.header().unwrap(), @@ -402,7 +403,7 @@ mod test_dkg_full { .unwrap(); // Create an initial shared secret - let (_, _, old_shared_secret) = make_shared_secret_simple_tdec( + let (_, _, old_shared_secret) = create_shared_secret_simple_tdec( &dkg, AAD, &ciphertext.header().unwrap(), @@ -410,14 +411,17 @@ mod test_dkg_full { ); // Remove one participant from the contexts and all nested structure - // TODO: Improve by removing a random participant and/or adding test cases let removed_validator_addr = dkg.validators.keys().last().unwrap().clone(); let mut remaining_validators = dkg.validators.clone(); remaining_validators .remove(&removed_validator_addr) .unwrap(); - // dkg.vss.remove(&removed_validator_addr); // TODO: Test whether it makes any difference + + let mut remaining_validator_keypairs = validator_keypairs.clone(); + remaining_validator_keypairs + .pop() + .expect("Should have a keypair"); // Remember to remove one domain point too let mut domain_points = dkg.domain_points(); @@ -433,14 +437,13 @@ mod test_dkg_full { let share_updates = remaining_validators .keys() .map(|v_addr| { - let deltas_i = - ShareRecoveryUpdate::make_share_updates_for_recovery( - &domain_points, - &dkg.pvss_params.h.into_affine(), - &x_r, - dkg.dkg_params.security_threshold(), - rng, - ); + let deltas_i = ShareRecoveryUpdate::create_share_updates( + &domain_points, + &dkg.pvss_params.h.into_affine(), + &x_r, + dkg.dkg_params.security_threshold(), + rng, + ); (v_addr.clone(), deltas_i) }) .collect::>(); @@ -448,7 +451,6 @@ mod test_dkg_full { // Participants share updates and update their shares // Now, every participant separately: - // TODO: Move this logic outside tests (see #162, #163) let updated_shares: Vec<_> = remaining_validators .values() .map(|validator| { @@ -471,7 +473,7 @@ mod test_dkg_full { let pvss_list = dkg.vss.values().cloned().collect::>(); let pvss_aggregated = aggregate(&pvss_list).unwrap(); pvss_aggregated - .make_updated_private_key_share( + .create_updated_private_key_share( validator_keypair, validator.share_index, updates_for_participant.as_slice(), @@ -489,10 +491,6 @@ mod test_dkg_full { ); // Get decryption shares from remaining participants - let mut remaining_validator_keypairs = validator_keypairs; - remaining_validator_keypairs - .pop() - .expect("Should have a keypair"); let mut decryption_shares: Vec> = remaining_validator_keypairs .iter() @@ -503,7 +501,7 @@ mod test_dkg_full { dkg.vss.values().cloned().collect::>(); let pvss_aggregated = aggregate(&pvss_list).unwrap(); pvss_aggregated - .make_decryption_share_simple( + .create_decryption_share_simple( &ciphertext.header().unwrap(), AAD, validator_keypair, @@ -575,7 +573,7 @@ mod test_dkg_full { .unwrap(); // Create an initial shared secret - let (_, _, old_shared_secret) = make_shared_secret_simple_tdec( + let (_, _, old_shared_secret) = create_shared_secret_simple_tdec( &dkg, AAD, &ciphertext.header().unwrap(), @@ -587,13 +585,12 @@ mod test_dkg_full { .validators .keys() .map(|v_addr| { - let deltas_i = - ShareRefreshUpdate::make_share_updates_for_refresh( - &dkg.domain_points(), - &dkg.pvss_params.h.into_affine(), - dkg.dkg_params.security_threshold(), - rng, - ); + let deltas_i = ShareRefreshUpdate::create_share_updates( + &dkg.domain_points(), + &dkg.pvss_params.h.into_affine(), + dkg.dkg_params.security_threshold(), + rng, + ); (v_addr.clone(), deltas_i) }) .collect::>(); @@ -601,7 +598,6 @@ mod test_dkg_full { // Participants share updates and update their shares // Now, every participant separately: - // TODO: Move this logic outside tests (see #162, #163) let updated_private_key_shares: Vec<_> = dkg .validators .values() @@ -627,7 +623,7 @@ mod test_dkg_full { let pvss_list = dkg.vss.values().cloned().collect::>(); let pvss_aggregated = aggregate(&pvss_list).unwrap(); pvss_aggregated - .make_updated_private_key_share( + .create_updated_private_key_share( validator_keypair, validator.share_index, updates_for_participant.as_slice(), @@ -642,14 +638,15 @@ mod test_dkg_full { .iter() .enumerate() .map(|(share_index, validator_keypair)| { + // In order to proceed with the decryption, we need to convert the updated private key shares + let private_key_share = &updated_private_key_shares + .get(share_index) + .unwrap() + .inner() + .0; DecryptionShareSimple::create( &validator_keypair.decryption_key, - // In order to proceed with the decryption, we need to convert the updated private key shares - &updated_private_key_shares - .get(share_index) - .unwrap() - .inner() - .0, + private_key_share, &ciphertext.header().unwrap(), AAD, &dkg.pvss_params.g_inv(), diff --git a/ferveo/src/pvss.rs b/ferveo/src/pvss.rs index 52f4279c..e98dffe4 100644 --- a/ferveo/src/pvss.rs +++ b/ferveo/src/pvss.rs @@ -6,7 +6,7 @@ use ark_poly::{ polynomial::univariate::DensePolynomial, DenseUVPolynomial, EvaluationDomain, Polynomial, }; -use ferveo_common::Keypair; +use ferveo_common::{serialization, Keypair}; use ferveo_tdec::{ CiphertextHeader, DecryptionSharePrecomputed, DecryptionShareSimple, }; @@ -19,7 +19,7 @@ use zeroize::{self, Zeroize, ZeroizeOnDrop}; use crate::{ assert_no_share_duplicates, batch_to_projective_g1, batch_to_projective_g2, - Error, PVSSMap, PrivateKeyShare, PrivateKeyShareUpdate, + DomainPoint, Error, PVSSMap, PrivateKeyShare, PrivateKeyShareUpdate, PubliclyVerifiableDkg, Result, UpdatedPrivateKeyShare, Validator, }; @@ -68,16 +68,16 @@ impl Default for PubliclyVerifiableParams { /// Secret polynomial used in the PVSS protocol /// We wrap this in a struct so that we can zeroize it after use -pub struct SecretPolynomial(pub DensePolynomial); +pub struct SecretPolynomial(pub DensePolynomial>); impl SecretPolynomial { pub fn new( - s: &E::ScalarField, + s: &DomainPoint, degree: usize, rng: &mut impl RngCore, ) -> Self { // Our random polynomial, \phi(x) = s + \sum_{i=1}^{t-1} a_i x^i - let mut phi = DensePolynomial::::rand(degree, rng); + let mut phi = DensePolynomial::>::rand(degree, rng); phi.coeffs[0] = *s; // setting the first coefficient to secret value Self(phi) } @@ -107,16 +107,17 @@ impl ZeroizeOnDrop for SecretPolynomial {} #[derive(Serialize, Deserialize, Clone, Debug, PartialEq, Eq)] pub struct PubliclyVerifiableSS { /// Used in Feldman commitment to the VSS polynomial, F = g^{\phi} - #[serde_as(as = "ferveo_common::serialization::SerdeAs")] + #[serde_as(as = "serialization::SerdeAs")] pub coeffs: Vec, /// The shares to be dealt to each validator - #[serde_as(as = "ferveo_common::serialization::SerdeAs")] - // pub shares: Vec>, // TODO: Using a custom type instead of referring to E:G2Affine breaks the serialization + #[serde_as(as = "serialization::SerdeAs")] + // TODO: Using a custom type instead of referring to E:G2Affine breaks the serialization + // pub shares: Vec>, pub shares: Vec, /// Proof of Knowledge - #[serde_as(as = "ferveo_common::serialization::SerdeAs")] + #[serde_as(as = "serialization::SerdeAs")] pub sigma: E::G2Affine, /// Marker struct to distinguish between aggregated and @@ -171,7 +172,7 @@ impl PubliclyVerifiableSS { // TODO: Cross check proof of knowledge check with the whitepaper; this check proves that there is a relationship between the secret and the pvss transcript // Sigma is a proof of knowledge of the secret, sigma = h^s - let sigma = E::G2Affine::generator().mul(*s).into(); //todo hash to curve + let sigma = E::G2Affine::generator().mul(*s).into(); // TODO: Use hash-to-curve here let vss = Self { coeffs, shares, @@ -323,17 +324,18 @@ impl PubliclyVerifiableSS { /// Make a decryption share (simple variant) for a given ciphertext /// With this method, we wrap the PrivateKeyShare method to avoid exposing the private key share - pub fn make_decryption_share_simple( + // TODO: Consider deprecating to use PrivateKeyShare method directly + pub fn create_decryption_share_simple( &self, - ciphertext: &CiphertextHeader, + ciphertext_header: &CiphertextHeader, aad: &[u8], validator_keypair: &Keypair, share_index: u32, g_inv: &E::G1Prepared, ) -> Result> { self.decrypt_private_key_share(validator_keypair, share_index)? - .make_decryption_share_simple( - ciphertext, + .create_decryption_share_simple( + ciphertext_header, aad, validator_keypair, g_inv, @@ -342,17 +344,18 @@ impl PubliclyVerifiableSS { /// Make a decryption share (precomputed variant) for a given ciphertext /// With this method, we wrap the PrivateKeyShare method to avoid exposing the private key share - pub fn make_decryption_share_simple_precomputed( + // TODO: Consider deprecating to use PrivateKeyShare method directly + pub fn create_decryption_share_simple_precomputed( &self, ciphertext_header: &CiphertextHeader, aad: &[u8], validator_keypair: &Keypair, share_index: u32, - domain_points: &[E::ScalarField], + domain_points: &[DomainPoint], g_inv: &E::G1Prepared, ) -> Result> { self.decrypt_private_key_share(validator_keypair, share_index)? - .make_decryption_share_simple_precomputed( + .create_decryption_share_simple_precomputed( ciphertext_header, aad, validator_keypair, @@ -362,7 +365,8 @@ impl PubliclyVerifiableSS { ) } - pub fn make_updated_private_key_share( + // TODO: Consider deprecating to use PrivateKeyShare method directly + pub fn create_updated_private_key_share( &self, validator_keypair: &Keypair, share_index: u32, @@ -371,7 +375,7 @@ impl PubliclyVerifiableSS { // Retrieve the private key share and apply the updates Ok(self .decrypt_private_key_share(validator_keypair, share_index)? - .make_updated_key_share(share_updates)) + .create_updated_key_share(share_updates)) } } diff --git a/ferveo/src/refresh.rs b/ferveo/src/refresh.rs index 126e281a..87797a9b 100644 --- a/ferveo/src/refresh.rs +++ b/ferveo/src/refresh.rs @@ -10,9 +10,10 @@ use ferveo_tdec::{ }; use itertools::zip_eq; use rand_core::RngCore; +use serde::{Deserialize, Serialize}; use zeroize::ZeroizeOnDrop; -use crate::{Error, Result}; +use crate::{DomainPoint, Error, Result}; // TODO: Rename refresh.rs to key_share.rs? @@ -30,7 +31,7 @@ impl PrivateKeyShare { impl PrivateKeyShare { /// From PSS paper, section 4.2.3, (https://link.springer.com/content/pdf/10.1007/3-540-44750-4_27.pdf) - pub fn make_updated_key_share( + pub fn create_updated_key_share( &self, share_updates: &[impl PrivateKeyShareUpdate], ) -> UpdatedPrivateKeyShare { @@ -46,9 +47,10 @@ impl PrivateKeyShare { } /// From the PSS paper, section 4.2.4, (https://link.springer.com/content/pdf/10.1007/3-540-44750-4_27.pdf) + /// `x_r` is the point at which the share is to be recovered pub fn recover_share_from_updated_private_shares( - x_r: &E::ScalarField, - domain_points: &[E::ScalarField], + x_r: &DomainPoint, + domain_points: &[DomainPoint], updated_private_shares: &[UpdatedPrivateKeyShare], ) -> PrivateKeyShare { // Interpolate new shares to recover y_r @@ -61,9 +63,9 @@ impl PrivateKeyShare { }) } - pub fn make_decryption_share_simple( + pub fn create_decryption_share_simple( &self, - ciphertext: &CiphertextHeader, + ciphertext_header: &CiphertextHeader, aad: &[u8], validator_keypair: &Keypair, g_inv: &E::G1Prepared, @@ -71,20 +73,20 @@ impl PrivateKeyShare { DecryptionShareSimple::create( &validator_keypair.decryption_key, &self.0, - ciphertext, + ciphertext_header, aad, g_inv, ) .map_err(|e| e.into()) } - pub fn make_decryption_share_simple_precomputed( + pub fn create_decryption_share_simple_precomputed( &self, ciphertext_header: &CiphertextHeader, aad: &[u8], validator_keypair: &Keypair, share_index: u32, - domain_points: &[E::ScalarField], + domain_points: &[DomainPoint], g_inv: &E::G1Prepared, ) -> Result> { // In precomputed variant, we offload the some of the decryption related computation to the server-side: @@ -107,9 +109,10 @@ impl PrivateKeyShare { } /// An updated private key share, resulting from an intermediate step in a share recovery or refresh operation. -// TODO: After recovery, should we replace existing private key shares with updated ones? #[derive(Debug, Clone, PartialEq, Eq, ZeroizeOnDrop)] -pub struct UpdatedPrivateKeyShare(InnerPrivateKeyShare); +pub struct UpdatedPrivateKeyShare( + pub(crate) InnerPrivateKeyShare, +); impl UpdatedPrivateKeyShare { /// One-way conversion from `UpdatedPrivateKeyShare` to `PrivateKeyShare`. @@ -128,12 +131,12 @@ impl UpdatedPrivateKeyShare { // TODO: Replace with an into trait? /// Trait for types that can be used to update a private key share. pub trait PrivateKeyShareUpdate { - fn inner(&self) -> &InnerPrivateKeyShare; // TODO: Should we return g2affine instead? + fn inner(&self) -> &InnerPrivateKeyShare; } /// An update to a private key share generated by a participant in a share recovery operation. #[derive(Debug, Clone, PartialEq, Eq, ZeroizeOnDrop)] -pub struct ShareRecoveryUpdate(InnerPrivateKeyShare); +pub struct ShareRecoveryUpdate(pub(crate) InnerPrivateKeyShare); impl PrivateKeyShareUpdate for ShareRecoveryUpdate { fn inner(&self) -> &InnerPrivateKeyShare { @@ -143,10 +146,10 @@ impl PrivateKeyShareUpdate for ShareRecoveryUpdate { impl ShareRecoveryUpdate { /// From PSS paper, section 4.2.1, (https://link.springer.com/content/pdf/10.1007/3-540-44750-4_27.pdf) - pub fn make_share_updates_for_recovery( - domain_points: &[E::ScalarField], + pub fn create_share_updates( + domain_points: &[DomainPoint], h: &E::G2Affine, - x_r: &E::ScalarField, + x_r: &DomainPoint, threshold: u32, rng: &mut impl RngCore, ) -> Vec> { @@ -165,8 +168,12 @@ impl ShareRecoveryUpdate { } /// An update to a private key share generated by a participant in a share refresh operation. -#[derive(Debug, Clone, PartialEq, Eq, ZeroizeOnDrop)] -pub struct ShareRefreshUpdate(InnerPrivateKeyShare); +#[derive( + Serialize, Deserialize, Debug, Clone, PartialEq, Eq, ZeroizeOnDrop, +)] +pub struct ShareRefreshUpdate( + pub(crate) ferveo_tdec::PrivateKeyShare, +); impl PrivateKeyShareUpdate for ShareRefreshUpdate { fn inner(&self) -> &InnerPrivateKeyShare { @@ -176,8 +183,8 @@ impl PrivateKeyShareUpdate for ShareRefreshUpdate { impl ShareRefreshUpdate { /// From PSS paper, section 4.2.1, (https://link.springer.com/content/pdf/10.1007/3-540-44750-4_27.pdf) - pub fn make_share_updates_for_refresh( - domain_points: &[E::ScalarField], + pub fn create_share_updates( + domain_points: &[DomainPoint], h: &E::G2Affine, threshold: u32, rng: &mut impl RngCore, @@ -186,7 +193,7 @@ impl ShareRefreshUpdate { prepare_share_updates_with_root::( domain_points, h, - &E::ScalarField::zero(), + &DomainPoint::::zero(), threshold, rng, ) @@ -198,14 +205,14 @@ impl ShareRefreshUpdate { } /// Prepare share updates with a given root -/// This is a helper function for `ShareRecoveryUpdate::make_share_updates_for_recovery` and `ShareRefreshUpdate::make_share_updates_for_refresh` +/// This is a helper function for `ShareRecoveryUpdate::create_share_updates_for_recovery` and `ShareRefreshUpdate::create_share_updates_for_refresh` /// It generates a new random polynomial with a defined root and evaluates it at each of the participants' indices. /// The result is a list of share updates. /// We represent the share updates as `InnerPrivateKeyShare` to avoid dependency on the concrete implementation of `PrivateKeyShareUpdate`. fn prepare_share_updates_with_root( - domain_points: &[E::ScalarField], + domain_points: &[DomainPoint], h: &E::G2Affine, - root: &E::ScalarField, + root: &DomainPoint, threshold: u32, rng: &mut impl RngCore, ) -> Vec> { @@ -228,22 +235,22 @@ fn prepare_share_updates_with_root( /// Generate a random polynomial with a given root fn make_random_polynomial_with_root( degree: u32, - root: &E::ScalarField, + root: &DomainPoint, rng: &mut impl RngCore, -) -> DensePolynomial { +) -> DensePolynomial> { // [c_0, c_1, ..., c_{degree}] (Random polynomial) let mut poly = - DensePolynomial::::rand(degree as usize, rng); + DensePolynomial::>::rand(degree as usize, rng); // [0, c_1, ... , c_{degree}] (We zeroize the free term) - poly[0] = E::ScalarField::zero(); + poly[0] = DomainPoint::::zero(); // Now, we calculate a new free term so that `poly(root) = 0` - let new_c_0 = E::ScalarField::zero() - poly.evaluate(root); + let new_c_0 = DomainPoint::::zero() - poly.evaluate(root); poly[0] = new_c_0; // Evaluating the polynomial at the root should result in 0 - debug_assert!(poly.evaluate(root) == E::ScalarField::zero()); + debug_assert!(poly.evaluate(root) == DomainPoint::::zero()); debug_assert!(poly.coeffs.len() == (degree + 1) as usize); poly @@ -267,16 +274,13 @@ mod tests_refresh { }; /// Using tdec test utilities here instead of PVSS to test the internals of the shared key recovery - // TODO: Can I fix that using combine_shared_secret? - - fn make_updated_private_key_shares( + fn create_updated_private_key_shares( rng: &mut R, threshold: u32, x_r: &Fr, remaining_participants: &[PrivateDecryptionContextSimple], ) -> Vec> { // Each participant prepares an update for each other participant - // TODO: Extract as parameter let domain_points = remaining_participants[0] .public_decryption_contexts .iter() @@ -286,15 +290,14 @@ mod tests_refresh { let share_updates = remaining_participants .iter() .map(|p| { - let deltas_i = - ShareRecoveryUpdate::make_share_updates_for_recovery( - &domain_points, - &h, - x_r, - threshold, - rng, - ); - (p.index, deltas_i) + let share_updates = ShareRecoveryUpdate::create_share_updates( + &domain_points, + &h, + x_r, + threshold, + rng, + ); + (p.index, share_updates) }) .collect::>(); @@ -309,9 +312,8 @@ mod tests_refresh { .collect(); // And updates their share - // TODO: Remove wrapping after migrating to dkg based tests PrivateKeyShare(p.private_key_share.clone()) - .make_updated_key_share(&updates_for_participant) + .create_updated_key_share(&updates_for_participant) }) .collect(); @@ -354,7 +356,7 @@ mod tests_refresh { } // Each participant prepares an update for each other participant, and uses it to create a new share fragment - let updated_private_key_shares = make_updated_private_key_shares( + let updated_private_key_shares = create_updated_private_key_shares( rng, security_threshold, &x_r, @@ -390,7 +392,7 @@ mod tests_refresh { } /// Ñ parties (where t <= Ñ <= N) jointly execute a "share recovery" algorithm, and the output is 1 new share. - /// The new share is independent from the previously existing shares. We can use this to on-board a new participant into an existing cohort. + /// The new share is independent of the previously existing shares. We can use this to on-board a new participant into an existing cohort. #[test_case(4, 4; "number of shares (validators) is a power of 2")] #[test_case(7, 7; "number of shares (validators) is not a power of 2")] fn tdec_simple_variant_share_recovery_at_random_point( @@ -418,7 +420,7 @@ mod tests_refresh { let x_r = ScalarField::rand(rng); // Each participant prepares an update for each other participant, and uses it to create a new share fragment - let share_recovery_fragmetns = make_updated_private_key_shares( + let share_recovery_fragmetns = create_updated_private_key_shares( rng, threshold, &x_r, @@ -488,14 +490,14 @@ mod tests_refresh { let share_updates = contexts .iter() .map(|p| { - let deltas_i = - ShareRefreshUpdate::::make_share_updates_for_refresh( + let share_updates = + ShareRefreshUpdate::::create_share_updates( domain_points, &h, threshold as u32, rng, ); - (p.index, deltas_i) + (p.index, share_updates) }) .collect::>(); @@ -510,9 +512,8 @@ mod tests_refresh { .collect(); // And creates a new, refreshed share - // TODO: Remove wrapping after migrating to dkg based tests PrivateKeyShare(p.private_key_share.clone()) - .make_updated_key_share(&updates_for_participant) + .create_updated_key_share(&updates_for_participant) }) .collect(); diff --git a/ferveo/src/validator.rs b/ferveo/src/validator.rs index a1c73e11..f0d88e49 100644 --- a/ferveo/src/validator.rs +++ b/ferveo/src/validator.rs @@ -7,6 +7,8 @@ use thiserror::Error; use crate::Error; +const ETHEREUM_ADDRESS_LEN: usize = 42; + #[derive( Clone, Debug, PartialEq, Eq, Ord, PartialOrd, Serialize, Deserialize, Hash, )] @@ -25,10 +27,11 @@ impl FromStr for EthereumAddress { type Err = EthereumAddressParseError; fn from_str(s: &str) -> Result { - if s.len() != 42 { + if s.len() != ETHEREUM_ADDRESS_LEN { return Err(EthereumAddressParseError::InvalidLength); } - hex::decode(&s[2..]) + let prefix_len = "0x".len(); + hex::decode(&s[prefix_len..]) .map_err(|_| EthereumAddressParseError::InvalidHex)?; Ok(EthereumAddress(s.to_string())) } @@ -69,7 +72,6 @@ pub fn assert_no_share_duplicates( validators: &[Validator], ) -> Result<(), Error> { let mut set = HashSet::new(); - for validator in validators { if set.contains(&validator.share_index) { return Err(Error::DuplicatedShareIndex(validator.share_index)); @@ -77,6 +79,5 @@ pub fn assert_no_share_duplicates( set.insert(validator.share_index); } } - Ok(()) }