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 420bf869..db8a7424 100644 --- a/ferveo-tdec/benches/tpke.rs +++ b/ferveo-tdec/benches/tpke.rs @@ -1,6 +1,6 @@ #![allow(clippy::redundant_closure)] -use ark_bls12_381::{Bls12_381, Fr, G1Affine as G1, G2Affine as G2}; +use ark_bls12_381::{Bls12_381, Fr}; use ark_ec::pairing::Pairing; use criterion::{ black_box, criterion_group, criterion_main, BenchmarkId, Criterion, @@ -25,8 +25,8 @@ struct SetupShared { shares_num: usize, msg: Vec, aad: Vec, - pubkey: G1, - privkey: G2, + pubkey: PublicKeyShare, + privkey: PrivateKeyShare, ciphertext: Ciphertext, shared_secret: SharedSecret, } @@ -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/ciphertext.rs b/ferveo-tdec/src/ciphertext.rs index 81f79389..cdaf956c 100644 --- a/ferveo-tdec/src/ciphertext.rs +++ b/ferveo-tdec/src/ciphertext.rs @@ -13,7 +13,10 @@ use serde_with::serde_as; use sha2::{digest::Digest, Sha256}; use zeroize::ZeroizeOnDrop; -use crate::{htp_bls12381_g2, Error, Result, SecretBox, SharedSecret}; +use crate::{ + htp_bls12381_g2, Error, PrivateKeyShare, PublicKeyShare, Result, SecretBox, + SharedSecret, +}; #[serde_as] #[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)] @@ -95,7 +98,7 @@ impl CiphertextHeader { pub fn encrypt( message: SecretBox>, aad: &[u8], - pubkey: &E::G1Affine, + pubkey: &PublicKeyShare, rng: &mut impl rand::Rng, ) -> Result> { // r @@ -105,7 +108,8 @@ pub fn encrypt( // h let h_gen = E::G2Affine::generator(); - let ry_prep = E::G1Prepared::from(pubkey.mul(rand_element).into()); + let ry_prep = + E::G1Prepared::from(pubkey.public_key_share.mul(rand_element).into()); // s let product = E::pairing(ry_prep, h_gen).0; // u @@ -140,13 +144,13 @@ pub fn encrypt( pub fn decrypt_symmetric( ciphertext: &Ciphertext, aad: &[u8], - private_key: &E::G2Affine, + private_key: &PrivateKeyShare, g_inv: &E::G1Prepared, ) -> Result> { ciphertext.check(aad, g_inv)?; let shared_secret = E::pairing( E::G1Prepared::from(ciphertext.commitment), - E::G2Prepared::from(*private_key), + E::G2Prepared::from(private_key.private_key_share), ) .0; let shared_secret = SharedSecret(shared_secret); 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 0622e6a8..dc93fee4 100644 --- a/ferveo-tdec/src/decryption.rs +++ b/ferveo-tdec/src/decryption.rs @@ -2,6 +2,7 @@ use std::ops::Mul; use ark_ec::{pairing::Pairing, CurveGroup}; use ark_ff::{Field, One, Zero}; +use ark_std::UniformRand; use ferveo_common::serialization; use itertools::{izip, zip_eq}; use rand_core::RngCore; @@ -9,8 +10,8 @@ use serde::{de::DeserializeOwned, Deserialize, Serialize}; use serde_with::serde_as; use crate::{ - generate_random, Ciphertext, CiphertextHeader, PrivateKeyShare, - PublicDecryptionContextFast, PublicDecryptionContextSimple, Result, + Ciphertext, CiphertextHeader, PrivateKeyShare, PublicDecryptionContextFast, + PublicDecryptionContextSimple, Result, }; #[serde_as] @@ -36,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() @@ -226,6 +224,15 @@ impl DecryptionSharePrecomputed { } } +pub fn generate_random_scalars( + n: usize, + rng: &mut R, +) -> Vec { + (0..n) + .map(|_| E::ScalarField::rand(rng)) + .collect::>() +} + // TODO: Remove this code? Currently only used in benchmarks. Move to benchmark suite? pub fn batch_verify_decryption_shares( pub_contexts: &[PublicDecryptionContextFast], @@ -240,16 +247,17 @@ pub fn batch_verify_decryption_shares( let blinding_keys = decryption_shares[0] .iter() .map(|d| { - pub_contexts[d.decrypter_index] - .blinded_key_share - .blinding_key_prepared - .clone() + E::G2Prepared::from( + pub_contexts[d.decrypter_index] + .blinded_key_share + .blinding_key, + ) }) .collect::>(); // For each ciphertext, generate num_shares random scalars let alpha_ij = (0..num_ciphertexts) - .map(|_| generate_random::<_, E>(num_shares, rng)) + .map(|_| generate_random_scalars::<_, E>(num_shares, rng)) .collect::>(); let mut pairings_a = Vec::with_capacity(num_shares + 1); @@ -302,10 +310,11 @@ pub fn verify_decryption_shares_fast( let blinding_keys = decryption_shares .iter() .map(|d| { - pub_contexts[d.decrypter_index] - .blinded_key_share - .blinding_key_prepared - .clone() + E::G2Prepared::from( + pub_contexts[d.decrypter_index] + .blinded_key_share + .blinding_key, + ) }) .collect::>(); diff --git a/ferveo-tdec/src/key_share.rs b/ferveo-tdec/src/key_share.rs index 2daaae56..4c164f6e 100644 --- a/ferveo-tdec/src/key_share.rs +++ b/ferveo-tdec/src/key_share.rs @@ -3,10 +3,14 @@ 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? pub struct PublicKeyShare { pub public_key_share: E::G1Affine, // A_{i, \omega_i} } @@ -15,16 +19,6 @@ pub struct PublicKeyShare { pub struct BlindedKeyShare { pub blinding_key: E::G2Affine, // [b] H pub blinded_key_share: E::G2Affine, // [b] Z_{i, \omega_i} - pub blinding_key_prepared: E::G2Prepared, -} - -pub fn generate_random( - n: usize, - rng: &mut R, -) -> Vec { - (0..n) - .map(|_| E::ScalarField::rand(rng)) - .collect::>() } impl BlindedKeyShare { @@ -58,8 +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, } @@ -68,7 +67,6 @@ impl PrivateKeyShare { let blinding_key = E::G2Affine::generator().mul(b).into_affine(); BlindedKeyShare:: { blinding_key, - blinding_key_prepared: E::G2Prepared::from(blinding_key), blinded_key_share: self.private_key_share.mul(b).into_affine(), } } diff --git a/ferveo-tdec/src/lib.rs b/ferveo-tdec/src/lib.rs index 297b066c..322d1bf9 100644 --- a/ferveo-tdec/src/lib.rs +++ b/ferveo-tdec/src/lib.rs @@ -77,8 +77,8 @@ pub mod test_common { shares_num: usize, rng: &mut impl RngCore, ) -> ( - E::G1Affine, - E::G2Affine, + PublicKeyShare, + PrivateKeyShare, Vec>, ) { assert!(shares_num >= threshold); @@ -138,7 +138,7 @@ pub mod test_common { ) .enumerate() { - let private_key_share = PrivateKeyShare:: { + let private_key_share = PrivateKeyShare { private_key_share: *private, }; let b = E::ScalarField::rand(rng); @@ -171,7 +171,15 @@ pub mod test_common { private.public_decryption_contexts = public_contexts.clone(); } - (pubkey.into(), privkey.into(), private_contexts) + ( + PublicKeyShare { + public_key_share: pubkey.into(), + }, + PrivateKeyShare { + private_key_share: privkey.into(), + }, + private_contexts, + ) } pub fn setup_simple( @@ -179,8 +187,8 @@ pub mod test_common { shares_num: usize, rng: &mut impl rand::Rng, ) -> ( - E::G1Affine, - E::G2Affine, + PublicKeyShare, + PrivateKeyShare, Vec>, ) { assert!(shares_num >= threshold); @@ -259,22 +267,30 @@ pub mod test_common { private.public_decryption_contexts = public_contexts.clone(); } - (pubkey.into(), privkey.into(), private_contexts) + ( + PublicKeyShare { + public_key_share: pubkey.into(), + }, + PrivateKeyShare { + private_key_share: privkey.into(), + }, + private_contexts, + ) } pub fn setup_precomputed( shares_num: usize, rng: &mut impl rand::Rng, ) -> ( - E::G1Affine, - E::G2Affine, + PublicKeyShare, + PrivateKeyShare, Vec>, ) { // In precomputed variant, the security threshold is equal to the number of shares setup_simple::(shares_num, shares_num, rng) } - pub fn make_shared_secret( + pub fn create_shared_secret( pub_contexts: &[PublicDecryptionContextSimple], decryption_shares: &[DecryptionShareSimple], ) -> SharedSecret { @@ -292,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; @@ -465,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, @@ -479,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 7328515e..dd8e40bd 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; @@ -7,35 +8,33 @@ use bincode; use ferveo_common::serialization; pub use ferveo_tdec::api::{ prepare_combine_simple, share_combine_precomputed, share_combine_simple, - Fr, G1Affine, G1Prepared, G2Affine, SecretBox, E, + DecryptionSharePrecomputed, Fr, G1Affine, G1Prepared, G2Affine, SecretBox, + E, }; +use ferveo_tdec::PublicKeyShare; use generic_array::{ typenum::{Unsigned, U48}, GenericArray, }; -use rand::RngCore; +use rand::{thread_rng, RngCore}; use serde::{Deserialize, Serialize}; use serde_with::serde_as; -pub type PublicKey = ferveo_common::PublicKey; -pub type Keypair = ferveo_common::Keypair; -pub type Validator = crate::Validator; -pub type Transcript = PubliclyVerifiableSS; - -pub type ValidatorMessage = (Validator, Transcript); - #[cfg(feature = "bindings-python")] use crate::bindings_python; #[cfg(feature = "bindings-wasm")] 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 DecryptionSharePrecomputed = - ferveo_tdec::api::DecryptionSharePrecomputed; +pub type PublicKey = ferveo_common::PublicKey; +pub type Keypair = ferveo_common::Keypair; +pub type Validator = crate::Validator; +pub type Transcript = PubliclyVerifiableSS; +pub type ValidatorMessage = (Validator, Transcript); // Normally, we would use a custom trait for this, but we can't because // the arkworks will not let us create a blanket implementation for G1Affine @@ -58,8 +57,14 @@ pub fn encrypt( pubkey: &DkgPublicKey, ) -> Result { let mut rng = rand::thread_rng(); - let ciphertext = - ferveo_tdec::api::encrypt(message, aad, &pubkey.0, &mut rng)?; + let ciphertext = ferveo_tdec::api::encrypt( + message, + aad, + &PublicKeyShare { + public_key_share: pubkey.0, + }, + &mut rng, + )?; Ok(Ciphertext(ciphertext)) } @@ -91,7 +96,6 @@ impl Ciphertext { } } -#[serde_as] #[derive(Clone, Debug, Serialize, Deserialize, PartialEq, Eq)] pub struct CiphertextHeader(ferveo_tdec::api::CiphertextHeader); @@ -146,6 +150,7 @@ impl From for FerveoVariant { #[serde_as] #[derive(Copy, Clone, Debug, Eq, PartialEq, Serialize, Deserialize)] pub struct DkgPublicKey( + // TODO: Consider not using G1Affine directly here #[serde_as(as = "serialization::SerdeAs")] pub(crate) G1Affine, ); @@ -218,13 +223,12 @@ impl Dkg { } pub fn public_key(&self) -> DkgPublicKey { - DkgPublicKey(self.0.public_key()) + DkgPublicKey(self.0.public_key().public_key_share) } 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), @@ -236,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 @@ -261,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 { @@ -327,6 +338,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, @@ -343,16 +355,17 @@ 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.decryption_key, - dkg.0.me.share_index as usize, + validator_keypair, + dkg.0.me.share_index, &dkg.0.domain_points(), &dkg.0.pvss_params.g_inv(), ) } + // TODO: Consider deprecating in favor of PrivateKeyShare::create_decryption_share_simple pub fn create_decryption_share_simple( &self, dkg: &Dkg, @@ -360,11 +373,11 @@ 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.decryption_key, - dkg.0.me.share_index as usize, + validator_keypair, + dkg.0.me.share_index, &dkg.0.pvss_params.g_inv(), )?; let domain_point = dkg.0.get_domain_point(dkg.0.me.share_index)?; @@ -373,6 +386,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] @@ -383,6 +409,7 @@ pub struct DecryptionShareSimple { domain_point: Fr, } +// TODO: Deprecate? #[serde_as] #[derive(Clone, Debug, PartialEq, Serialize, Deserialize)] pub struct DkgPublicParameters { @@ -422,14 +449,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); @@ -452,7 +674,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| { @@ -516,33 +738,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 @@ -558,9 +783,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, @@ -575,7 +800,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( @@ -608,29 +832,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 @@ -808,4 +1037,362 @@ mod test_ferveo_api { Err(Error::InvalidTranscriptAggregate) )); } + + 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 e8afbe30..1ee6ec6e 100644 --- a/ferveo/src/dkg.rs +++ b/ferveo/src/dkg.rs @@ -15,6 +15,8 @@ use crate::{ Validator, }; +pub type DomainPoint = ::ScalarField; + #[derive(Copy, Clone, Debug, Serialize, Deserialize)] pub struct DkgParams { tau: u32, @@ -69,9 +71,14 @@ pub type PVSSMap = BTreeMap>; #[derive(Debug, Clone)] pub enum DkgState { // TODO: Do we need to keep track of the block number? - Sharing { accumulated_shares: u32, block: u32 }, + Sharing { + accumulated_shares: u32, + block: u32, + }, Dealt, - Success { public_key: E::G1Affine }, + Success { + public_key: ferveo_tdec::PublicKeyShare, + }, Invalid, } @@ -84,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 @@ -111,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())) @@ -160,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, )?; @@ -178,7 +184,7 @@ impl PubliclyVerifiableDkg { let pvss_list = self.vss.values().cloned().collect::>(); Ok(Message::Aggregate(Aggregation { vss: aggregate(&pvss_list)?, - public_key, + public_key: public_key.public_key_share, })) } _ => Err(Error::InvalidDkgStateToAggregate), @@ -186,35 +192,48 @@ impl PubliclyVerifiableDkg { } /// Returns the public key generated by the DKG - pub fn public_key(&self) -> E::G1Affine { - self.vss - .values() - .map(|vss| vss.coeffs[0].into_group()) - .sum::() - .into_affine() + pub fn public_key(&self) -> ferveo_tdec::PublicKeyShare { + ferveo_tdec::PublicKeyShare { + public_key_share: self + .vss + .values() + .map(|vss| vss.coeffs[0].into_group()) + .sum::() + .into_affine(), + } } /// 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() } - /// `payload` is the content of the message + 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( &self, sender: &Validator, - payload: &Message, + message: &Message, ) -> Result<()> { - match payload { + match message { Message::Deal(pvss) if matches!( self.state, @@ -245,7 +264,7 @@ impl PubliclyVerifiableDkg { )) } else if vss.verify_aggregation(self).is_err() { Err(Error::InvalidTranscriptAggregate) - } else if &self.public_key() == public_key { + } else if &self.public_key().public_key_share == public_key { Ok(()) } else { Err(Error::InvalidDkgPublicKey) @@ -255,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, @@ -382,6 +400,7 @@ mod test_dkg_init { #[cfg(test)] mod test_dealing { use ark_ec::AffineRepr; + use ferveo_tdec::PublicKeyShare; use crate::{ test_common::*, DkgParams, DkgState, DkgState::Dealt, Error, @@ -587,7 +606,9 @@ mod test_dealing { )); dkg.state = DkgState::Success { - public_key: G1::zero(), + public_key: PublicKeyShare { + public_key_share: G1::zero(), + }, }; assert!(dkg.share(rng).is_err()); @@ -614,7 +635,9 @@ mod test_dealing { let sender = dkg.me.clone(); dkg.state = DkgState::Success { - public_key: G1::zero(), + public_key: PublicKeyShare { + public_key_share: G1::zero(), + }, }; assert!(dkg.verify_message(&sender, &pvss).is_err()); assert!(dkg.apply_message(&sender, &pvss).is_err()); @@ -631,13 +654,14 @@ mod test_dealing { #[cfg(test)] mod test_aggregation { use ark_ec::AffineRepr; + use ferveo_tdec::PublicKeyShare; use test_case::test_case; use crate::{dkg::*, test_common::*, DkgState, Message}; /// Test that if the security threshold is met, we can create a final key - #[test_case(4,4; "number of validators equal to the number of shares")] - #[test_case(4,6; "number of validators greater than the number of shares")] + #[test_case(4, 4; "number of validators equal to the number of shares")] + #[test_case(4, 6; "number of validators greater than the number of shares")] fn test_aggregate(shares_num: u32, validators_num: u32) { let security_threshold = shares_num - 1; let (mut dkg, _) = setup_dealt_dkg_with_n_validators( @@ -649,7 +673,7 @@ mod test_aggregation { if let Message::Aggregate(Aggregation { public_key, .. }) = &aggregate_msg { - assert_eq!(public_key, &dkg.public_key()); + assert_eq!(public_key, &dkg.public_key().public_key_share); } else { panic!("Expected aggregate message") } @@ -669,7 +693,9 @@ mod test_aggregation { }; assert!(dkg.aggregate().is_err()); dkg.state = DkgState::Success { - public_key: G1::zero(), + public_key: PublicKeyShare { + public_key_share: G1::zero(), + }, }; assert!(dkg.aggregate().is_err()); } @@ -690,7 +716,9 @@ mod test_aggregation { assert!(dkg.apply_message(&sender, &aggregate).is_err()); dkg.state = DkgState::Success { - public_key: G1::zero(), + public_key: PublicKeyShare { + public_key_share: G1::zero(), + }, }; assert!(dkg.verify_message(&sender, &aggregate).is_err()); assert!(dkg.apply_message(&sender, &aggregate).is_err()) @@ -713,7 +741,7 @@ mod test_aggregation { fn test_aggregate_wont_verify_if_wrong_key() { let (dkg, _) = setup_dealt_dkg(); let mut aggregate = dkg.aggregate().unwrap(); - while dkg.public_key() == G1::zero() { + while dkg.public_key().public_key_share == G1::zero() { let (_dkg, _) = setup_dealt_dkg(); } if let Message::Aggregate(Aggregation { public_key, .. }) = diff --git a/ferveo/src/lib.rs b/ferveo/src/lib.rs index f9d6c1a5..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,11 +172,11 @@ 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.decryption_key, - validator.share_index as usize, + validator_keypair, + validator.share_index, &dkg.pvss_params.g_inv(), ) .unwrap() @@ -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,11 +278,11 @@ 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.decryption_key, - validator.share_index as usize, + validator_keypair, + validator.share_index, &domain_points, &dkg.pvss_params.g_inv(), ) @@ -305,7 +306,8 @@ mod test_dkg_full { assert_eq!(plaintext, MSG); } - #[test_case(4, 4; "number of validators equal to the number of shares")] + #[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_verification( shares_num: u32, @@ -329,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(), @@ -376,12 +378,21 @@ mod test_dkg_full { )); } - #[test] - fn test_dkg_simple_tdec_share_recovery() { + #[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_recovery( + shares_num: u32, + validators_num: u32, + ) { let rng = &mut test_rng(); + let security_threshold = shares_num / 2 + 1; - let (dkg, validator_keypairs) = - setup_dealt_dkg_with(SECURITY_THRESHOLD, SHARES_NUM); + let (dkg, validator_keypairs) = setup_dealt_dkg_with_n_validators( + security_threshold, + shares_num, + validators_num, + ); let public_key = &dkg.public_key(); let ciphertext = ferveo_tdec::encrypt::( SecretBox::new(MSG.to_vec()), @@ -392,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(), @@ -406,7 +417,11 @@ mod test_dkg_full { 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(); @@ -422,11 +437,11 @@ mod test_dkg_full { let share_updates = remaining_validators .keys() .map(|v_addr| { - let deltas_i = prepare_share_updates_for_recovery::( + let deltas_i = ShareRecoveryUpdate::create_share_updates( &domain_points, &dkg.pvss_params.h.into_affine(), &x_r, - dkg.dkg_params.security_threshold() as usize, + dkg.dkg_params.security_threshold(), rng, ); (v_addr.clone(), deltas_i) @@ -436,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| { @@ -444,59 +458,54 @@ mod test_dkg_full { let updates_for_participant: Vec<_> = share_updates .values() .map(|updates| { - *updates.get(validator.share_index as usize).unwrap() + updates.get(validator.share_index as usize).unwrap() }) + .cloned() .collect(); // Each validator uses their decryption key to update their share - let decryption_key = validator_keypairs + let validator_keypair = validator_keypairs .get(validator.share_index as usize) - .unwrap() - .decryption_key; + .unwrap(); // Creates updated private key shares - // TODO: Why not using dkg.aggregate()? + // TODO: Use self.aggregate upon simplifying Message handling let pvss_list = dkg.vss.values().cloned().collect::>(); let pvss_aggregated = aggregate(&pvss_list).unwrap(); pvss_aggregated - .update_private_key_share_for_recovery( - &decryption_key, - validator.share_index as usize, + .create_updated_private_key_share( + validator_keypair, + validator.share_index, updates_for_participant.as_slice(), ) .unwrap() }) .collect(); - // TODO: Rename updated_private_shares to something that doesn't imply mutation (see #162, #163) - // Now, we have to combine new share fragments into a new share - let new_private_key_share = recover_share_from_updated_private_shares( - &x_r, - &domain_points, - &updated_shares, - ); + let recovered_key_share = + PrivateKeyShare::recover_share_from_updated_private_shares( + &x_r, + &domain_points, + &updated_shares, + ); // 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() .enumerate() .map(|(share_index, validator_keypair)| { - // TODO: Why not using dkg.aggregate()? + // TODO: Use self.aggregate upon simplifying Message handling let pvss_list = 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.decryption_key, - share_index, + validator_keypair, + share_index as u32, &dkg.pvss_params.g_inv(), ) .unwrap() @@ -508,7 +517,7 @@ mod test_dkg_full { decryption_shares.push( DecryptionShareSimple::create( &new_validator_decryption_key, - &new_private_key_share, + &recovered_key_share.0, &ciphertext.header().unwrap(), AAD, &dkg.pvss_params.g_inv(), @@ -517,14 +526,15 @@ mod test_dkg_full { ); domain_points.push(x_r); - assert_eq!(domain_points.len(), SHARES_NUM as usize); - assert_eq!(decryption_shares.len(), SHARES_NUM as usize); + assert_eq!(domain_points.len(), validators_num as usize); + assert_eq!(decryption_shares.len(), validators_num as usize); - // Maybe parametrize this test with [1..] and [..threshold] - let domain_points = &domain_points[1..]; - let decryption_shares = &decryption_shares[1..]; - assert_eq!(domain_points.len(), SECURITY_THRESHOLD as usize); - assert_eq!(decryption_shares.len(), SECURITY_THRESHOLD as usize); + // TODO: Maybe parametrize this test with [1..] and [..threshold] + 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 lagrange = ferveo_tdec::prepare_combine_simple::(domain_points); let new_shared_secret = ferveo_tdec::share_combine_simple::( @@ -538,12 +548,21 @@ mod test_dkg_full { ); } - #[test] - fn test_dkg_simple_tdec_share_refreshing() { + #[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_refreshing( + shares_num: u32, + validators_num: u32, + ) { let rng = &mut test_rng(); + let security_threshold = shares_num / 2 + 1; - let (dkg, validator_keypairs) = - setup_dealt_dkg_with(SECURITY_THRESHOLD, SHARES_NUM); + let (dkg, validator_keypairs) = setup_dealt_dkg_with_n_validators( + security_threshold, + shares_num, + validators_num, + ); let public_key = &dkg.public_key(); let ciphertext = ferveo_tdec::encrypt::( SecretBox::new(MSG.to_vec()), @@ -554,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(), @@ -566,10 +585,10 @@ mod test_dkg_full { .validators .keys() .map(|v_addr| { - let deltas_i = prepare_share_updates_for_refresh::( + let deltas_i = ShareRefreshUpdate::create_share_updates( &dkg.domain_points(), &dkg.pvss_params.h.into_affine(), - dkg.dkg_params.security_threshold() as usize, + dkg.dkg_params.security_threshold(), rng, ); (v_addr.clone(), deltas_i) @@ -579,8 +598,7 @@ 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<_> = dkg + let updated_private_key_shares: Vec<_> = dkg .validators .values() .map(|validator| { @@ -588,24 +606,26 @@ mod test_dkg_full { let updates_for_participant: Vec<_> = share_updates .values() .map(|updates| { - *updates.get(validator.share_index as usize).unwrap() + updates + .get(validator.share_index as usize) + .cloned() + .unwrap() }) .collect(); // Each validator uses their decryption key to update their share - let decryption_key = validator_keypairs + let validator_keypair = validator_keypairs .get(validator.share_index as usize) - .unwrap() - .decryption_key; + .unwrap(); // Creates updated private key shares - // TODO: Why not using dkg.aggregate()? + // TODO: Use self.aggregate upon simplifying Message handling let pvss_list = dkg.vss.values().cloned().collect::>(); let pvss_aggregated = aggregate(&pvss_list).unwrap(); pvss_aggregated - .update_private_key_share_for_recovery( - &decryption_key, - validator.share_index as usize, + .create_updated_private_key_share( + validator_keypair, + validator.share_index, updates_for_participant.as_slice(), ) .unwrap() @@ -618,9 +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, - updated_shares.get(share_index).unwrap(), + private_key_share, &ciphertext.header().unwrap(), AAD, &dkg.pvss_params.g_inv(), @@ -630,10 +656,10 @@ mod test_dkg_full { .collect(); let lagrange = ferveo_tdec::prepare_combine_simple::( - &dkg.domain_points()[..SECURITY_THRESHOLD as usize], + &dkg.domain_points()[..security_threshold as usize], ); let new_shared_secret = ferveo_tdec::share_combine_simple::( - &decryption_shares[..SECURITY_THRESHOLD as usize], + &decryption_shares[..security_threshold as usize], &lagrange, ); diff --git a/ferveo/src/pvss.rs b/ferveo/src/pvss.rs index 5108f6b9..44ef5dc6 100644 --- a/ferveo/src/pvss.rs +++ b/ferveo/src/pvss.rs @@ -6,9 +6,9 @@ use ark_poly::{ polynomial::univariate::DensePolynomial, DenseUVPolynomial, EvaluationDomain, Polynomial, }; +use ferveo_common::{serialization, Keypair}; use ferveo_tdec::{ - prepare_combine_simple, CiphertextHeader, DecryptionSharePrecomputed, - DecryptionShareSimple, PrivateKeyShare, + CiphertextHeader, DecryptionSharePrecomputed, DecryptionShareSimple, }; use itertools::Itertools; use rand::RngCore; @@ -18,12 +18,13 @@ use subproductdomain::fast_multiexp; use zeroize::{self, Zeroize, ZeroizeOnDrop}; use crate::{ - apply_updates_to_private_share, assert_no_share_duplicates, - batch_to_projective_g1, batch_to_projective_g2, Error, PVSSMap, - PubliclyVerifiableDkg, Result, Validator, + assert_no_share_duplicates, batch_to_projective_g1, batch_to_projective_g2, + DomainPoint, Error, PVSSMap, PrivateKeyShare, PrivateKeyShareUpdate, + PubliclyVerifiableDkg, Result, UpdatedPrivateKeyShare, Validator, }; /// These are the blinded evaluations of shares of a single random polynomial +// TODO: Are these really blinded like in tdec or encrypted? pub type ShareEncryptions = ::G2Affine; /// Marker struct for unaggregated PVSS transcripts @@ -67,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) } @@ -106,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 +173,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, @@ -302,86 +304,79 @@ impl PubliclyVerifiableSS { pub fn decrypt_private_key_share( &self, - validator_decryption_key: &E::ScalarField, - share_index: usize, + validator_keypair: &Keypair, + share_index: u32, ) -> Result> { - // Decrypt private key shares https://nikkolasg.github.io/ferveo/pvss.html#validator-decryption-of-private-key-shares - let private_key_share = self - .shares - .get(share_index) - .ok_or(Error::InvalidShareIndex(share_index as u32))? - .mul( - validator_decryption_key - .inverse() - .expect("Validator decryption key must have an inverse"), - ) - .into_affine(); - Ok(PrivateKeyShare { private_key_share }) + // Decrypt private key share https://nikkolasg.github.io/ferveo/pvss.html#validator-decryption-of-private-key-shares + let private_key_share = + self.shares + .get(share_index as usize) + .ok_or(Error::InvalidShareIndex(share_index))? + .mul( + validator_keypair.decryption_key.inverse().expect( + "Validator decryption key must have an inverse", + ), + ) + .into_affine(); + Ok(PrivateKeyShare(ferveo_tdec::PrivateKeyShare { + private_key_share, + })) } - pub fn make_decryption_share_simple( + /// 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 + // TODO: Consider deprecating to use PrivateKeyShare method directly + pub fn create_decryption_share_simple( &self, - ciphertext: &CiphertextHeader, + ciphertext_header: &CiphertextHeader, aad: &[u8], - validator_decryption_key: &E::ScalarField, - share_index: usize, + validator_keypair: &Keypair, + share_index: u32, g_inv: &E::G1Prepared, ) -> Result> { - let private_key_share = self - .decrypt_private_key_share(validator_decryption_key, share_index)?; - DecryptionShareSimple::create( - validator_decryption_key, - &private_key_share, - ciphertext, - aad, - g_inv, - ) - .map_err(|e| e.into()) + self.decrypt_private_key_share(validator_keypair, share_index)? + .create_decryption_share_simple( + ciphertext_header, + aad, + validator_keypair, + g_inv, + ) } - pub fn make_decryption_share_simple_precomputed( + /// 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 + // TODO: Consider deprecating to use PrivateKeyShare method directly + pub fn create_decryption_share_simple_precomputed( &self, ciphertext_header: &CiphertextHeader, aad: &[u8], - validator_decryption_key: &E::ScalarField, - share_index: usize, - domain_points: &[E::ScalarField], + validator_keypair: &Keypair, + share_index: u32, + domain_points: &[DomainPoint], g_inv: &E::G1Prepared, ) -> Result> { - let private_key_share = self - .decrypt_private_key_share(validator_decryption_key, share_index)?; - - // We use the `prepare_combine_simple` function to precompute the lagrange coefficients - let lagrange_coeffs = prepare_combine_simple::(domain_points); - - DecryptionSharePrecomputed::new( - share_index, - validator_decryption_key, - &private_key_share, - ciphertext_header, - aad, - &lagrange_coeffs[share_index], - g_inv, - ) - .map_err(|e| e.into()) + self.decrypt_private_key_share(validator_keypair, share_index)? + .create_decryption_share_simple_precomputed( + ciphertext_header, + aad, + validator_keypair, + share_index, + domain_points, + g_inv, + ) } - // TODO: Consider relocate to different place, maybe PrivateKeyShare? (see #162, #163) - pub fn update_private_key_share_for_recovery( + // TODO: Consider deprecating to use PrivateKeyShare method directly + pub fn create_updated_private_key_share( &self, - validator_decryption_key: &E::ScalarField, - share_index: usize, - share_updates: &[E::G2], - ) -> Result> { - // Retrieves their private key share - let private_key_share = self - .decrypt_private_key_share(validator_decryption_key, share_index)?; - - // And updates their share - Ok(apply_updates_to_private_share::( - &private_key_share, - share_updates, - )) + validator_keypair: &Keypair, + share_index: u32, + share_updates: &[impl PrivateKeyShareUpdate], + ) -> Result> { + // Retrieve the private key share and apply the updates + Ok(self + .decrypt_private_key_share(validator_keypair, share_index)? + .create_updated_key_share(share_updates)) } } @@ -436,8 +431,8 @@ mod test_pvss { /// Test the happy flow such that the PVSS with the correct form is created /// and that appropriate validations pass - #[test_case(4,4; "number of validators is equal to the number of shares")] - #[test_case(4,6; "number of validators is greater than the number of shares")] + #[test_case(4, 4; "number of validators is equal to the number of shares")] + #[test_case(4, 6; "number of validators is greater than the number of shares")] fn test_new_pvss(shares_num: u32, validators_num: u32) { let rng = &mut ark_std::test_rng(); let security_threshold = shares_num - 1; @@ -511,8 +506,8 @@ mod test_pvss { /// Check that happy flow of aggregating PVSS transcripts /// has the correct form and it's validations passes - #[test_case(4,4; "number of validators is equal to the number of shares")] - #[test_case(4,6; "number of validators is greater than the number of shares")] + #[test_case(4, 4; "number of validators is equal to the number of shares")] + #[test_case(4, 6; "number of validators is greater than the number of shares")] fn test_aggregate_pvss(shares_num: u32, validators_num: u32) { let security_threshold = shares_num - 1; let (dkg, _) = setup_dealt_dkg_with_n_validators( diff --git a/ferveo/src/refresh.rs b/ferveo/src/refresh.rs index b02eba3b..87797a9b 100644 --- a/ferveo/src/refresh.rs +++ b/ferveo/src/refresh.rs @@ -1,86 +1,221 @@ use std::{ops::Mul, usize}; -use ark_ec::{pairing::Pairing, AffineRepr, CurveGroup}; +use ark_ec::{pairing::Pairing, CurveGroup}; use ark_ff::Zero; use ark_poly::{univariate::DensePolynomial, DenseUVPolynomial, Polynomial}; -use ferveo_tdec::{lagrange_basis_at, PrivateKeyShare}; +use ferveo_common::Keypair; +use ferveo_tdec::{ + lagrange_basis_at, prepare_combine_simple, CiphertextHeader, + DecryptionSharePrecomputed, DecryptionShareSimple, +}; use itertools::zip_eq; use rand_core::RngCore; +use serde::{Deserialize, Serialize}; +use zeroize::ZeroizeOnDrop; -// SHARE UPDATE FUNCTIONS: +use crate::{DomainPoint, Error, Result}; -/// From PSS paper, section 4.2.1, (https://link.springer.com/content/pdf/10.1007/3-540-44750-4_27.pdf) -pub fn prepare_share_updates_for_recovery( - domain_points: &[E::ScalarField], - h: &E::G2Affine, - x_r: &E::ScalarField, - threshold: usize, - rng: &mut impl RngCore, -) -> Vec { - // Update polynomial has root at x_r - prepare_share_updates_with_root::(domain_points, h, x_r, threshold, rng) +// TODO: Rename refresh.rs to key_share.rs? + +type InnerPrivateKeyShare = ferveo_tdec::PrivateKeyShare; + +/// Private key share held by a participant in the DKG protocol. +#[derive(Debug, Clone, PartialEq, Eq, ZeroizeOnDrop)] +pub struct PrivateKeyShare(pub InnerPrivateKeyShare); + +impl PrivateKeyShare { + pub fn new(private_key_share: InnerPrivateKeyShare) -> Self { + Self(private_key_share) + } } -// TODO: Consider relocating to PrivateKeyShare (see #162, #163) -/// From PSS paper, section 4.2.3, (https://link.springer.com/content/pdf/10.1007/3-540-44750-4_27.pdf) -pub fn apply_updates_to_private_share( - private_key_share: &PrivateKeyShare, - share_updates: &[E::G2], -) -> PrivateKeyShare { - let private_key_share = share_updates - .iter() - .fold( - private_key_share.private_key_share.into_group(), - |acc, delta| acc + delta, +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 create_updated_key_share( + &self, + share_updates: &[impl PrivateKeyShareUpdate], + ) -> UpdatedPrivateKeyShare { + let updated_key_share = share_updates + .iter() + .fold(self.0.private_key_share, |acc, delta| { + (acc + delta.inner().private_key_share).into() + }); + let updated_key_share = InnerPrivateKeyShare { + private_key_share: updated_key_share, + }; + UpdatedPrivateKeyShare(updated_key_share) + } + + /// 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: &DomainPoint, + domain_points: &[DomainPoint], + updated_private_shares: &[UpdatedPrivateKeyShare], + ) -> PrivateKeyShare { + // Interpolate new shares to recover y_r + let lagrange = lagrange_basis_at::(domain_points, x_r); + let prods = zip_eq(updated_private_shares, lagrange) + .map(|(y_j, l)| y_j.0.private_key_share.mul(l)); + let y_r = prods.fold(E::G2::zero(), |acc, y_j| acc + y_j); + PrivateKeyShare(ferveo_tdec::PrivateKeyShare { + private_key_share: y_r.into_affine(), + }) + } + + pub fn create_decryption_share_simple( + &self, + ciphertext_header: &CiphertextHeader, + aad: &[u8], + validator_keypair: &Keypair, + g_inv: &E::G1Prepared, + ) -> Result> { + DecryptionShareSimple::create( + &validator_keypair.decryption_key, + &self.0, + ciphertext_header, + aad, + g_inv, ) - .into_affine(); - PrivateKeyShare { private_key_share } + .map_err(|e| e.into()) + } + + pub fn create_decryption_share_simple_precomputed( + &self, + ciphertext_header: &CiphertextHeader, + aad: &[u8], + validator_keypair: &Keypair, + share_index: u32, + domain_points: &[DomainPoint], + g_inv: &E::G1Prepared, + ) -> Result> { + // In precomputed variant, we offload the some of the decryption related computation to the server-side: + // We use the `prepare_combine_simple` function to precompute the lagrange coefficients + let lagrange_coeffs = prepare_combine_simple::(domain_points); + let lagrange_coeff = &lagrange_coeffs + .get(share_index as usize) + .ok_or(Error::InvalidShareIndex(share_index))?; + DecryptionSharePrecomputed::new( + share_index as usize, + &validator_keypair.decryption_key, + &self.0, + ciphertext_header, + aad, + lagrange_coeff, + g_inv, + ) + .map_err(|e| e.into()) + } } -/// From the PSS paper, section 4.2.4, (https://link.springer.com/content/pdf/10.1007/3-540-44750-4_27.pdf) -pub fn recover_share_from_updated_private_shares( - x_r: &E::ScalarField, - domain_points: &[E::ScalarField], - updated_private_shares: &[PrivateKeyShare], -) -> PrivateKeyShare { - // Interpolate new shares to recover y_r - let lagrange = lagrange_basis_at::(domain_points, x_r); - let prods = zip_eq(updated_private_shares, lagrange) - .map(|(y_j, l)| y_j.private_key_share.mul(l)); - let y_r = prods.fold(E::G2::zero(), |acc, y_j| acc + y_j); - - PrivateKeyShare { - private_key_share: y_r.into_affine(), +/// An updated private key share, resulting from an intermediate step in a share recovery or refresh operation. +#[derive(Debug, Clone, PartialEq, Eq, ZeroizeOnDrop)] +pub struct UpdatedPrivateKeyShare( + pub(crate) InnerPrivateKeyShare, +); + +impl UpdatedPrivateKeyShare { + /// One-way conversion from `UpdatedPrivateKeyShare` to `PrivateKeyShare`. + /// Use this method to eject from the `UpdatedPrivateKeyShare` type and use the resulting `PrivateKeyShare` in further operations. + pub fn inner(&self) -> PrivateKeyShare { + PrivateKeyShare(self.0.clone()) } } -// SHARE REFRESH FUNCTIONS: +impl UpdatedPrivateKeyShare { + pub fn new(private_key_share: InnerPrivateKeyShare) -> Self { + Self(private_key_share) + } +} -pub fn prepare_share_updates_for_refresh( - domain_points: &[E::ScalarField], - h: &E::G2Affine, - threshold: usize, - rng: &mut impl RngCore, -) -> Vec { - // Update polynomial has root at 0 - prepare_share_updates_with_root::( - domain_points, - h, - &E::ScalarField::zero(), - threshold, - rng, - ) +// 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; +} + +/// 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(pub(crate) InnerPrivateKeyShare); + +impl PrivateKeyShareUpdate for ShareRecoveryUpdate { + fn inner(&self) -> &InnerPrivateKeyShare { + &self.0 + } +} + +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 create_share_updates( + domain_points: &[DomainPoint], + h: &E::G2Affine, + x_r: &DomainPoint, + threshold: u32, + rng: &mut impl RngCore, + ) -> Vec> { + // Update polynomial has root at x_r + prepare_share_updates_with_root::( + domain_points, + h, + x_r, + threshold, + rng, + ) + .iter() + .map(|p| Self(p.clone())) + .collect() + } } -// UTILS: +/// An update to a private key share generated by a participant in a share refresh operation. +#[derive( + Serialize, Deserialize, Debug, Clone, PartialEq, Eq, ZeroizeOnDrop, +)] +pub struct ShareRefreshUpdate( + pub(crate) ferveo_tdec::PrivateKeyShare, +); + +impl PrivateKeyShareUpdate for ShareRefreshUpdate { + fn inner(&self) -> &InnerPrivateKeyShare { + &self.0 + } +} +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 create_share_updates( + domain_points: &[DomainPoint], + h: &E::G2Affine, + threshold: u32, + rng: &mut impl RngCore, + ) -> Vec> { + // Update polynomial has root at 0 + prepare_share_updates_with_root::( + domain_points, + h, + &DomainPoint::::zero(), + threshold, + rng, + ) + .iter() + .cloned() + .map(|p| ShareRefreshUpdate(p)) + .collect() + } +} + +/// Prepare share updates with a given root +/// 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, - threshold: usize, + root: &DomainPoint, + threshold: u32, rng: &mut impl RngCore, -) -> Vec { +) -> Vec> { // Generate a new random polynomial with defined root let d_i = make_random_polynomial_with_root::(threshold - 1, root, rng); @@ -89,29 +224,34 @@ fn prepare_share_updates_with_root( .iter() .map(|x_i| { let eval = d_i.evaluate(x_i); - h.mul(eval) + h.mul(eval).into_affine() + }) + .map(|p| InnerPrivateKeyShare { + private_key_share: p, }) .collect() } -pub fn make_random_polynomial_with_root( - degree: usize, - root: &E::ScalarField, +/// Generate a random polynomial with a given root +fn make_random_polynomial_with_root( + degree: u32, + root: &DomainPoint, rng: &mut impl RngCore, -) -> DensePolynomial { +) -> DensePolynomial> { // [c_0, c_1, ..., c_{degree}] (Random polynomial) - let mut poly = DensePolynomial::::rand(degree, rng); + let mut poly = + 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.coeffs.len() == degree + 1); + debug_assert!(poly.evaluate(root) == DomainPoint::::zero()); + debug_assert!(poly.coeffs.len() == (degree + 1) as usize); poly } @@ -124,25 +264,23 @@ mod tests_refresh { use ark_std::{test_rng, UniformRand, Zero}; use ferveo_tdec::{ test_common::setup_simple, PrivateDecryptionContextSimple, - PrivateKeyShare, }; use rand_core::RngCore; - use test_case::test_matrix; + use test_case::{test_case, test_matrix}; use crate::{ - apply_updates_to_private_share, prepare_share_updates_for_recovery, - prepare_share_updates_for_refresh, - recover_share_from_updated_private_shares, test_common::*, + test_common::*, PrivateKeyShare, ShareRecoveryUpdate, + ShareRefreshUpdate, UpdatedPrivateKeyShare, }; - fn make_new_share_fragments_for_recovery( + /// Using tdec test utilities here instead of PVSS to test the internals of the shared key recovery + fn create_updated_private_key_shares( rng: &mut R, - threshold: usize, + threshold: u32, x_r: &Fr, remaining_participants: &[PrivateDecryptionContextSimple], - ) -> Vec> { + ) -> Vec> { // Each participant prepares an update for each other participant - // TODO: Extract as parameter let domain_points = remaining_participants[0] .public_decryption_contexts .iter() @@ -152,47 +290,52 @@ mod tests_refresh { let share_updates = remaining_participants .iter() .map(|p| { - let deltas_i = prepare_share_updates_for_recovery::( + let share_updates = ShareRecoveryUpdate::create_share_updates( &domain_points, &h, x_r, threshold, rng, ); - (p.index, deltas_i) + (p.index, share_updates) }) .collect::>(); // Participants share updates and update their shares - let new_share_fragments: Vec<_> = remaining_participants + let updated_private_key_shares: Vec<_> = remaining_participants .iter() .map(|p| { // Current participant receives updates from other participants let updates_for_participant: Vec<_> = share_updates .values() - .map(|updates| *updates.get(p.index).unwrap()) + .map(|updates| updates.get(p.index).cloned().unwrap()) .collect(); // And updates their share - apply_updates_to_private_share::( - &p.private_key_share, - &updates_for_participant, - ) + PrivateKeyShare(p.private_key_share.clone()) + .create_updated_key_share(&updates_for_participant) }) .collect(); - new_share_fragments + updated_private_key_shares } /// Ñ parties (where t <= Ñ <= N) jointly execute a "share recovery" algorithm, and the output is 1 new share. /// The new share is intended to restore a previously existing share, e.g., due to loss or corruption. - #[test_matrix([4, 7, 11, 16])] - fn tdec_simple_variant_share_recovery_at_selected_point(shares_num: usize) { + #[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_selected_point( + shares_num: u32, + _validators_num: u32, + ) { let rng = &mut test_rng(); let security_threshold = shares_num * 2 / 3; - let (_, _, mut contexts) = - setup_simple::(security_threshold, shares_num, rng); + let (_, _, mut contexts) = setup_simple::( + security_threshold as usize, + shares_num as usize, + rng, + ); // Prepare participants @@ -203,16 +346,17 @@ mod tests_refresh { .last() .unwrap() .domain; - let original_private_key_share = selected_participant.private_key_share; + let original_private_key_share = + PrivateKeyShare(selected_participant.private_key_share); - // Remove one participant from the contexts and all nested structures + // Remove the selected participant from the contexts and all nested structures let mut remaining_participants = contexts; for p in &mut remaining_participants { p.public_decryption_contexts.pop().unwrap(); } // Each participant prepares an update for each other participant, and uses it to create a new share fragment - let new_share_fragments = make_new_share_fragments_for_recovery( + let updated_private_key_shares = create_updated_private_key_shares( rng, security_threshold, &x_r, @@ -225,35 +369,41 @@ mod tests_refresh { .iter() .map(|ctxt| ctxt.domain) .collect::>(); - let new_private_key_share = recover_share_from_updated_private_shares( - &x_r, - &domain_points[..security_threshold], - &new_share_fragments[..security_threshold], - ); + let new_private_key_share = + PrivateKeyShare::recover_share_from_updated_private_shares( + &x_r, + &domain_points[..security_threshold as usize], + &updated_private_key_shares[..security_threshold as usize], + ); assert_eq!(new_private_key_share, original_private_key_share); // If we don't have enough private share updates, the resulting private share will be incorrect - assert_eq!(domain_points.len(), new_share_fragments.len()); + assert_eq!(domain_points.len(), updated_private_key_shares.len()); let incorrect_private_key_share = - recover_share_from_updated_private_shares( + PrivateKeyShare::recover_share_from_updated_private_shares( &x_r, - &domain_points[..(security_threshold - 1)], - &new_share_fragments[..(security_threshold - 1)], + &domain_points[..(security_threshold - 1) as usize], + &updated_private_key_shares + [..(security_threshold - 1) as usize], ); assert_ne!(incorrect_private_key_share, original_private_key_share); } /// Ñ 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. - #[test_matrix([4, 7, 11, 16])] - fn tdec_simple_variant_share_recovery_at_random_point(shares_num: usize) { + /// 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( + shares_num: u32, + _validators_num: u32, + ) { let rng = &mut test_rng(); let threshold = shares_num * 2 / 3; let (_, shared_private_key, mut contexts) = - setup_simple::(threshold, shares_num, rng); + setup_simple::(threshold as usize, shares_num as usize, rng); // Prepare participants @@ -270,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 new_share_fragments = make_new_share_fragments_for_recovery( + let share_recovery_fragmetns = create_updated_private_key_shares( rng, threshold, &x_r, @@ -283,11 +433,12 @@ mod tests_refresh { .iter() .map(|ctxt| ctxt.domain) .collect::>(); - let new_private_key_share = recover_share_from_updated_private_shares( - &x_r, - &domain_points[..threshold], - &new_share_fragments[..threshold], - ); + let recovered_private_key_share = + PrivateKeyShare::recover_share_from_updated_private_shares( + &x_r, + &domain_points[..threshold as usize], + &share_recovery_fragmetns[..threshold as usize], + ); let mut private_shares = contexts .iter() @@ -297,18 +448,24 @@ mod tests_refresh { // Finally, let's recreate the shared private key from some original shares and the recovered one domain_points.push(x_r); - private_shares.push(new_private_key_share); + private_shares.push(recovered_private_key_share.0.clone()); + + // This is a workaround for a type mismatch - We need to convert the private shares to updated private shares + // This is just to test that we are able to recover the shared private key from the updated private shares + let updated_private_key_shares = private_shares + .iter() + .cloned() + .map(UpdatedPrivateKeyShare::new) + .collect::>(); let start_from = shares_num - threshold; - let new_shared_private_key = recover_share_from_updated_private_shares( - &ScalarField::zero(), - &domain_points[start_from..], - &private_shares[start_from..], - ); + let new_shared_private_key = + PrivateKeyShare::recover_share_from_updated_private_shares( + &ScalarField::zero(), + &domain_points[start_from as usize..], + &updated_private_key_shares[start_from as usize..], + ); - assert_eq!( - shared_private_key, - new_shared_private_key.private_key_share - ); + assert_eq!(shared_private_key, new_shared_private_key.0); } /// Ñ parties (where t <= Ñ <= N) jointly execute a "share refresh" algorithm. @@ -319,7 +476,7 @@ mod tests_refresh { let rng = &mut test_rng(); let threshold = shares_num * 2 / 3; - let (_, shared_private_key, contexts) = + let (_, private_key_share, contexts) = setup_simple::(threshold, shares_num, rng); let domain_points = &contexts[0] @@ -333,13 +490,14 @@ mod tests_refresh { let share_updates = contexts .iter() .map(|p| { - let deltas_i = prepare_share_updates_for_refresh::( - domain_points, - &h, - threshold, - rng, - ); - (p.index, deltas_i) + let share_updates = + ShareRefreshUpdate::::create_share_updates( + domain_points, + &h, + threshold as u32, + rng, + ); + (p.index, share_updates) }) .collect::>(); @@ -350,27 +508,23 @@ mod tests_refresh { // Current participant receives updates from other participants let updates_for_participant: Vec<_> = share_updates .values() - .map(|updates| *updates.get(p.index).unwrap()) + .map(|updates| updates.get(p.index).cloned().unwrap()) .collect(); - // And updates their share - apply_updates_to_private_share::( - &p.private_key_share, - &updates_for_participant, - ) + // And creates a new, refreshed share + PrivateKeyShare(p.private_key_share.clone()) + .create_updated_key_share(&updates_for_participant) }) .collect(); // Finally, let's recreate the shared private key from the refreshed shares - let new_shared_private_key = recover_share_from_updated_private_shares( - &ScalarField::zero(), - &domain_points[..threshold], - &refreshed_shares[..threshold], - ); + let new_shared_private_key = + PrivateKeyShare::recover_share_from_updated_private_shares( + &ScalarField::zero(), + &domain_points[..threshold], + &refreshed_shares[..threshold], + ); - assert_eq!( - shared_private_key, - new_shared_private_key.private_key_share - ); + assert_eq!(private_key_share, new_shared_private_key.0); } } 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(()) }