From 912fb74b9a259654a9e3cf1850ca5b5dcb82e6a8 Mon Sep 17 00:00:00 2001 From: Piotr Roslaniec Date: Mon, 12 Feb 2024 15:17:32 +0100 Subject: [PATCH] sketch refresh api --- ferveo-common/src/keypair.rs | 9 +- ferveo-tdec/src/combine.rs | 2 - ferveo-tdec/src/decryption.rs | 3 - ferveo-tdec/src/key_share.rs | 11 +- ferveo/src/api.rs | 587 +++++++++++++++++++++++++++++++++- ferveo/src/bindings_python.rs | 2 + ferveo/src/dkg.rs | 30 +- ferveo/src/lib.rs | 59 ++-- ferveo/src/pvss.rs | 30 +- ferveo/src/refresh.rs | 87 ++--- 10 files changed, 699 insertions(+), 121 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-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/src/api.rs b/ferveo/src/api.rs index 36cdc7c5..57f22a12 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; @@ -266,6 +267,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 { @@ -376,6 +385,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] @@ -425,14 +447,211 @@ pub fn combine_shares_simple(shares: &[DecryptionShareSimple]) -> SharedSecret { #[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)] pub struct SharedSecret(pub ferveo_tdec::api::SharedSecret); +pub type ShareRecoveryUpdateInner = crate::refresh::ShareRecoveryUpdate; + +#[serde_as] +#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)] +// TODO: serde is failing to serialize E = ark_bls12_381::Bls12_381 +// pub struct ShareRecoveryUpdate(pub ShareRecoveryUpdateInner); +// 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 make_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::make_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 crate::api::ShareRefreshUpdate { + pub fn make_share_updates( + dkg: &Dkg, + ) -> Result> { + let rng = &mut thread_rng(); + let updates = crate::refresh::ShareRefreshUpdate::make_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 make_decryption_share_simple( + &self, + dkg: &Dkg, + ciphertext_header: &CiphertextHeader, + validator_keypair: &Keypair, + aad: &[u8], + ) -> Result { + let share = crate::PrivateKeyShare(self.0.clone()) + .make_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 make_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()) + .make_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 itertools::{izip, Itertools}; use rand::{prelude::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 +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| { @@ -796,4 +1015,360 @@ mod test_ferveo_api { let result = bad_aggregate.verify(validators_num, messages); assert!(result.is_err()); } + + #[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 StdRng::seed_from_u64(0); + let security_threshold = shares_num / 2 + 1; + + // Create DKG instances for testing + let (messages, mut validators, mut 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::make_shared_secret_simple_tdec( + &dkgs[0].0, + AAD, + &ciphertext_header.0, + validator_keypairs.as_slice(), + ); + + // Recovery starts here + + // Remove one participant from the contexts and all nested structure + // to simulate offboarding a validator + // TODO: Implement dkg.offboard(validator)? + dkgs.pop(); + // TODO: Not removing the messages here, because we still need the message + // from the now-offboarded-validator to be present in other validators' PVSS + // If we don't do this, the PVSS is incomplete and validators don't have + // enough information to proceed with recovery + // messages.pop().unwrap(); + validator_keypairs.pop().unwrap(); + let removed_validator = validators.pop().unwrap(); + + // Remember to remove one domain point too + // TODO: How to keep track of this in the DKG? + let mut domain_points = dkgs[0].domain_points(); + // TODO: Removing this line will cause errors as long as we don't track domain points + // internally in the DKG (i.e. as long as we don't implement offboarding) + domain_points.pop().unwrap(); + + // Now, we're going to recover a new share at a random point, + // and check that the shared secret is still the same. + // TODO: Create a seperate test case for non-random domain point + let x_r = DomainPoint::::rand(rng); + + // Each participant prepares an update for each other participant + // TODO: Do I need to recreate the dkg after removing a participant? + let share_updates = dkgs + .iter() + .map(|validator_dkg| { + let share_update = ShareRecoveryUpdate::make_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 + // 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_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)| { + // TODO: How does this cloning impact DKG mutation? + let pvss_aggregated = validator_dkg + .clone() + .aggregate_transcripts(&messages) + .unwrap(); + pvss_aggregated.verify(validators_num, &messages).unwrap(); + pvss_aggregated + .create_decryption_share_simple( + validator_dkg, + &ciphertext_header, + AAD, + validator_keypair, + ) + .unwrap() + }) + .collect(); + + // 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 + .make_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); + + // 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 new_shared_secret = combine_shares_simple(decryption_shares); + + assert_eq!( + old_shared_secret, new_shared_secret.0, + "Shared secret reconstruction failed" + ); + } + + // TODO: Deduplicate this test with the one above if possible + #[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; + + // Create DKG instances for testing + 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::make_shared_secret_simple_tdec( + &dkgs[0].0, + AAD, + &ciphertext_header.0, + validator_keypairs.as_slice(), + ); + + // Refreshing starts here + + // Each participant prepares an update for each other participant + // TODO: Do I need to recreate the dkg after removing a participant? + let share_updates = dkgs + .iter() + .map(|validator_dkg| { + let share_update = + ShareRefreshUpdate::make_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 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(); + // TODO: Need to clone all over the place because PrivateKeyShare doesnt implement Copy + pks.make_decryption_share_simple( + validator_dkg, + &ciphertext_header, + validator_keypair, + AAD, + ) + .unwrap() + }) + .collect(); + + // TODO: Maybe parametrize this test with [1..] and [..threshold] + 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.0, + "Shared secret reconstruction failed" + ); + } } diff --git a/ferveo/src/bindings_python.rs b/ferveo/src/bindings_python.rs index fe534af8..e0c0bd3a 100644 --- a/ferveo/src/bindings_python.rs +++ b/ferveo/src/bindings_python.rs @@ -121,6 +121,8 @@ impl From for PyErr { "validators_num: {validators_num}, messages_num: {messages_num}" )) } + // 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(), } diff --git a/ferveo/src/dkg.rs b/ferveo/src/dkg.rs index bc4acdfd..a81e703d 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, @@ -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,8 +204,9 @@ impl PubliclyVerifiableDkg { } /// Return a domain point for the share_index - pub fn get_domain_point(&self, share_index: u32) -> Result { + pub fn get_domain_point(&self, share_index: u32) -> Result> { let domain_points = self.domain_points(); + eprintln!("Domain points: {:?}", domain_points.len()); domain_points .get(share_index as usize) .ok_or_else(|| Error::InvalidShareIndex(share_index)) @@ -212,22 +214,10 @@ impl PubliclyVerifiableDkg { } /// 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 verify_message( &self, sender: &Validator, diff --git a/ferveo/src/lib.rs b/ferveo/src/lib.rs index 8ef4c8e7..41eb2ca7 100644 --- a/ferveo/src/lib.rs +++ b/ferveo/src/lib.rs @@ -146,7 +146,7 @@ mod test_dkg_full { use super::*; use crate::test_common::*; - fn make_shared_secret_simple_tdec( + pub fn make_shared_secret_simple_tdec( dkg: &PubliclyVerifiableDkg, aad: &[u8], ciphertext_header: &ferveo_tdec::CiphertextHeader, @@ -419,6 +419,11 @@ mod test_dkg_full { .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(); domain_points.pop().unwrap(); @@ -433,14 +438,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::make_share_updates( + &domain_points, + &dkg.pvss_params.h.into_affine(), + &x_r, + dkg.dkg_params.security_threshold(), + rng, + ); (v_addr.clone(), deltas_i) }) .collect::>(); @@ -471,7 +475,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 +493,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() @@ -587,13 +587,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::make_share_updates( + &dkg.domain_points(), + &dkg.pvss_params.h.into_affine(), + dkg.dkg_params.security_threshold(), + rng, + ); (v_addr.clone(), deltas_i) }) .collect::>(); @@ -627,7 +626,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 +641,18 @@ 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 + + // TODO: Ok, I think we need to somehow put updated key shares back into pvss + // TODO: OR rather expose share creation methods from private key share + 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..b6500f74 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,16 @@ 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")] + #[serde_as(as = "serialization::SerdeAs")] // pub shares: Vec>, // TODO: Using a custom type instead of referring to E:G2Affine breaks the serialization 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 @@ -323,17 +323,19 @@ 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 + // TODO: Remove, use PrivateKeyShare method directly pub fn make_decryption_share_simple( &self, - ciphertext: &CiphertextHeader, + ciphertext_header: &CiphertextHeader, aad: &[u8], validator_keypair: &Keypair, share_index: u32, g_inv: &E::G1Prepared, ) -> Result> { + // TODO: Expose the private key share as a first class citizen in the API self.decrypt_private_key_share(validator_keypair, share_index)? .make_decryption_share_simple( - ciphertext, + ciphertext_header, aad, validator_keypair, g_inv, @@ -342,13 +344,14 @@ 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 + // TODO: Remove, use PrivateKeyShare method directly pub fn make_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)? @@ -362,7 +365,8 @@ impl PubliclyVerifiableSS { ) } - pub fn make_updated_private_key_share( + // TODO: Replace with PrivateKeyShare methods + 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..0407540a 100644 --- a/ferveo/src/refresh.rs +++ b/ferveo/src/refresh.rs @@ -12,7 +12,7 @@ use itertools::zip_eq; use rand_core::RngCore; use zeroize::ZeroizeOnDrop; -use crate::{Error, Result}; +use crate::{DomainPoint, Error, Result}; // TODO: Rename refresh.rs to key_share.rs? @@ -30,7 +30,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 +46,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 @@ -63,7 +64,7 @@ impl PrivateKeyShare { pub fn make_decryption_share_simple( &self, - ciphertext: &CiphertextHeader, + ciphertext_header: &CiphertextHeader, aad: &[u8], validator_keypair: &Keypair, g_inv: &E::G1Prepared, @@ -71,7 +72,7 @@ impl PrivateKeyShare { DecryptionShareSimple::create( &validator_keypair.decryption_key, &self.0, - ciphertext, + ciphertext_header, aad, g_inv, ) @@ -84,7 +85,7 @@ impl PrivateKeyShare { 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: @@ -109,7 +110,9 @@ 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`. @@ -133,7 +136,7 @@ pub trait PrivateKeyShareUpdate { /// 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 make_share_updates( + domain_points: &[DomainPoint], h: &E::G2Affine, - x_r: &E::ScalarField, + x_r: &DomainPoint, threshold: u32, rng: &mut impl RngCore, ) -> Vec> { @@ -166,7 +169,7 @@ 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); +pub struct ShareRefreshUpdate(pub(crate) InnerPrivateKeyShare); impl PrivateKeyShareUpdate for ShareRefreshUpdate { fn inner(&self) -> &InnerPrivateKeyShare { @@ -176,8 +179,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 make_share_updates( + domain_points: &[DomainPoint], h: &E::G2Affine, threshold: u32, rng: &mut impl RngCore, @@ -186,7 +189,7 @@ impl ShareRefreshUpdate { prepare_share_updates_with_root::( domain_points, h, - &E::ScalarField::zero(), + &DomainPoint::::zero(), threshold, rng, ) @@ -203,9 +206,9 @@ impl ShareRefreshUpdate { /// 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 +231,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 @@ -286,15 +289,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::make_share_updates( + &domain_points, + &h, + x_r, + threshold, + rng, + ); + (p.index, share_updates) }) .collect::>(); @@ -311,7 +313,7 @@ mod tests_refresh { // 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(); @@ -488,14 +490,13 @@ mod tests_refresh { let share_updates = contexts .iter() .map(|p| { - let deltas_i = - ShareRefreshUpdate::::make_share_updates_for_refresh( - domain_points, - &h, - threshold as u32, - rng, - ); - (p.index, deltas_i) + let share_updates = ShareRefreshUpdate::::make_share_updates( + domain_points, + &h, + threshold as u32, + rng, + ); + (p.index, share_updates) }) .collect::>(); @@ -512,7 +513,7 @@ mod tests_refresh { // 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();