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();