diff --git a/Cargo.lock b/Cargo.lock index 7aa7e272..685f931f 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -786,6 +786,7 @@ dependencies = [ "serde", "serde_with", "subproductdomain-pre-release", + "test-case", "thiserror", "wasm-bindgen", "wasm-bindgen-derive", @@ -1881,6 +1882,39 @@ dependencies = [ "winapi-util", ] +[[package]] +name = "test-case" +version = "3.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "eb2550dd13afcd286853192af8601920d959b14c401fcece38071d53bf0768a8" +dependencies = [ + "test-case-macros", +] + +[[package]] +name = "test-case-core" +version = "3.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "adcb7fd841cd518e279be3d5a3eb0636409487998a4aff22f3de87b81e88384f" +dependencies = [ + "cfg-if", + "proc-macro2", + "quote", + "syn 2.0.15", +] + +[[package]] +name = "test-case-macros" +version = "3.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5c89e72a01ed4c579669add59014b9a524d609c0c88c6a585ce37485879f6ffb" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.15", + "test-case-core", +] + [[package]] name = "textwrap" version = "0.11.0" diff --git a/ferveo-python/test/test_ferveo.py b/ferveo-python/test/test_ferveo.py index 6f00b6df..c3874d40 100644 --- a/ferveo-python/test/test_ferveo.py +++ b/ferveo-python/test/test_ferveo.py @@ -135,6 +135,79 @@ def test_precomputed_tdec_doesnt_have_enough_messages(): FerveoVariant.Precomputed, shares_num=4, threshold=4, shares_to_use=3 ) +def test_dkg_has_min_shares(): + total_shares_num = 5 + min_shares_num = 3 + threshold = 3 + + tau = 1 + validator_keypairs = [Keypair.random() for _ in range(0, total_shares_num)] + validators = [ + Validator(gen_eth_addr(i), keypair.public_key()) + for i, keypair in enumerate(validator_keypairs) + ] + validators.sort(key=lambda v: v.address) + + messages = [] + for sender in validators: + dkg = Dkg( + tau=tau, + shares_num=min_shares_num, + security_threshold=threshold, + validators=validators, + me=sender, + ) + messages.append(ValidatorMessage(sender, dkg.generate_transcript())) + + dkg = Dkg( + tau=tau, + shares_num=min_shares_num, + security_threshold=threshold, + validators=validators, + me=validators[0], + ) + pvss_aggregated = dkg.aggregate_transcripts(messages) + assert pvss_aggregated.verify(min_shares_num, messages) + + dkg_pk_bytes = bytes(dkg.public_key) + dkg_pk = DkgPublicKey.from_bytes(dkg_pk_bytes) + + msg = "abc".encode() + aad = "my-aad".encode() + ciphertext = encrypt(msg, aad, dkg_pk) + + decryption_shares = [] + for validator, validator_keypair in zip(validators, validator_keypairs): + dkg = Dkg( + tau=tau, + shares_num=total_shares_num, + security_threshold=threshold, + validators=validators, + me=validator, + ) + pvss_aggregated = dkg.aggregate_transcripts(messages) + assert pvss_aggregated.verify(total_shares_num, messages) + + decryption_share = decryption_share_for_variant(variant, pvss_aggregated)( + dkg, ciphertext.header, aad, validator_keypair + ) + decryption_shares.append(decryption_share) + + shared_secret = combine_shares_for_variant(variant, decryption_shares) + + if variant == FerveoVariant.Simple and len(decryption_shares) < threshold: + with pytest.raises(ThresholdEncryptionError): + decrypt_with_shared_secret(ciphertext, aad, shared_secret) + return + + if variant == FerveoVariant.Precomputed and len(decryption_shares) < total_shares_num: + with pytest.raises(ThresholdEncryptionError): + decrypt_with_shared_secret(ciphertext, aad, shared_secret) + return + + plaintext = decrypt_with_shared_secret(ciphertext, aad, shared_secret) + assert bytes(plaintext) == msg + PARAMS = [ (1, FerveoVariant.Simple), diff --git a/ferveo-wasm/tests/node.rs b/ferveo-wasm/tests/node.rs index b4234d07..4ac71429 100644 --- a/ferveo-wasm/tests/node.rs +++ b/ferveo-wasm/tests/node.rs @@ -8,8 +8,8 @@ use wasm_bindgen_test::*; type TestSetup = ( u32, - usize, - usize, + u32, + u32, Vec, Vec, ValidatorArray, @@ -21,11 +21,12 @@ type TestSetup = ( fn setup_dkg() -> TestSetup { let tau = 1; - let shares_num = 16; + let shares_num: u32 = 16; let security_threshold = shares_num * 2 / 3; - let validator_keypairs = - (0..shares_num).map(gen_keypair).collect::>(); + let validator_keypairs = (0..shares_num as usize) + .map(gen_keypair) + .collect::>(); let validators = validator_keypairs .iter() .enumerate() @@ -38,8 +39,8 @@ fn setup_dkg() -> TestSetup { let messages = validators.iter().map(|sender| { let dkg = Dkg::new( tau, - shares_num as u32, - security_threshold as u32, + shares_num, + security_threshold, &validators_js, sender, ) @@ -54,8 +55,8 @@ fn setup_dkg() -> TestSetup { let mut dkg = Dkg::new( tau, - shares_num as u32, - security_threshold as u32, + shares_num, + security_threshold, &validators_js, &validators[0], ) @@ -112,8 +113,8 @@ fn tdec_simple() { .map(|(validator, keypair)| { let mut dkg = Dkg::new( tau, - shares_num as u32, - security_threshold as u32, + shares_num, + security_threshold, &validators_js, &validator, ) @@ -166,8 +167,8 @@ fn tdec_precomputed() { .map(|(validator, keypair)| { let mut dkg = Dkg::new( tau, - shares_num as u32, - security_threshold as u32, + shares_num, + security_threshold, &validators_js, &validator, ) diff --git a/ferveo/Cargo.toml b/ferveo/Cargo.toml index c063c502..d2a0d0eb 100644 --- a/ferveo/Cargo.toml +++ b/ferveo/Cargo.toml @@ -51,6 +51,7 @@ wasm-bindgen-derive = { version = "0.2.1", optional = true } criterion = "0.3" # supports pprof, # TODO: Figure out if/how we can update to 0.4 digest = { version = "0.10.0", features = ["alloc"] } pprof = { version = "0.6", features = ["flamegraph", "criterion"] } +test-case = "3.3.1" # WASM bindings console_error_panic_hook = "0.1.7" diff --git a/ferveo/benches/benchmarks/validity_checks.rs b/ferveo/benches/benchmarks/validity_checks.rs index a6dd9f48..cc7266f7 100644 --- a/ferveo/benches/benchmarks/validity_checks.rs +++ b/ferveo/benches/benchmarks/validity_checks.rs @@ -45,11 +45,7 @@ fn setup_dkg( let me = validators[validator].clone(); PubliclyVerifiableDkg::new( &validators, - &DkgParams { - tau: 0, - security_threshold: shares_num / 3, - shares_num, - }, + &DkgParams::new(0, shares_num / 3, shares_num).unwrap(), &me, ) .expect("Setup failed") diff --git a/ferveo/examples/bench_primitives_size.rs b/ferveo/examples/bench_primitives_size.rs index 18adf673..79afb8a4 100644 --- a/ferveo/examples/bench_primitives_size.rs +++ b/ferveo/examples/bench_primitives_size.rs @@ -80,11 +80,7 @@ fn setup_dkg( let me = validators[validator].clone(); PubliclyVerifiableDkg::new( &validators, - &DkgParams { - tau: 0, - security_threshold, - shares_num, - }, + &DkgParams::new(0, security_threshold, shares_num).unwrap(), &me, ) .expect("Setup failed") diff --git a/ferveo/src/api.rs b/ferveo/src/api.rs index af3edcd4..c625eb53 100644 --- a/ferveo/src/api.rs +++ b/ferveo/src/api.rs @@ -26,8 +26,8 @@ use crate::bindings_python; use crate::bindings_wasm; pub use crate::EthereumAddress; use crate::{ - do_verify_aggregation, Error, PVSSMap, PubliclyVerifiableParams, - PubliclyVerifiableSS, Result, + do_verify_aggregation, DkgValidator, Error, PVSSMap, + PubliclyVerifiableParams, PubliclyVerifiableSS, Result, ValidatorsMap, }; pub type DecryptionSharePrecomputed = @@ -203,11 +203,8 @@ impl Dkg { validators: &[Validator], me: &Validator, ) -> Result { - let dkg_params = crate::DkgParams { - tau, - security_threshold, - shares_num, - }; + let dkg_params = + crate::DkgParams::new(tau, security_threshold, shares_num)?; let dkg = crate::PubliclyVerifiableDkg::::new( validators, &dkg_params, @@ -259,6 +256,24 @@ fn make_pvss_map(messages: &[ValidatorMessage]) -> PVSSMap { pvss_map } +fn make_validator_map(messages: &[ValidatorMessage]) -> ValidatorsMap { + let mut validator_map = ValidatorsMap::new(); + // TODO: Don't use enumerate here. Instead, add `share_index` to `ValidatorMessage` + messages.iter().enumerate().for_each( + |(share_index, (validator, _transcript))| { + let dkg_validator = DkgValidator { + share_index, + validator: Validator { + address: validator.address.clone(), + public_key: validator.public_key, + }, + }; + validator_map.insert(validator.address.clone(), dkg_validator); + }, + ); + validator_map +} + #[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)] pub struct AggregatedTranscript(PubliclyVerifiableSS); @@ -282,12 +297,10 @@ impl AggregatedTranscript { return Err(Error::InvalidTranscriptAggregate); } + // TODO: Can we simplify this? Do we need another map? + // TODO: See if do_verify_aggregation can be simplified in terms of parameters let pvss_map = make_pvss_map(messages); - let validators: Vec<_> = messages - .iter() - .map(|(validator, _)| validator) - .cloned() - .collect(); + let validators = make_validator_map(messages); // This check also includes `verify_full`. See impl. for details. let is_valid = do_verify_aggregation( @@ -312,7 +325,7 @@ impl AggregatedTranscript { .0 .domain .elements() - .take(dkg.0.dkg_params.shares_num as usize) + .take(dkg.0.dkg_params.shares_num() as usize) .collect(); self.0.make_decryption_share_simple_precomputed( &ciphertext_header.0, @@ -539,6 +552,7 @@ mod test_ferveo_api { } #[test] + fn test_server_api_tdec_simple() { let rng = &mut StdRng::seed_from_u64(0); @@ -627,6 +641,95 @@ mod test_ferveo_api { } } + #[test] + fn test_server_api_tdec_simple_with_min_shares() { + let rng = &mut StdRng::seed_from_u64(0); + + // Works for both power of 2 and non-power of 2 + for shares_num in [4, 7] { + let tau = 1; + let security_threshold = shares_num - 1; + + let (messages, validators, validator_keypairs) = + make_test_inputs(rng, tau, security_threshold, shares_num); + + // Now that every validator holds a dkg instance and a transcript for every other validator, + // every validator can aggregate the transcripts + let mut dkg = Dkg::new( + tau, + shares_num, + security_threshold, + &validators, + &validators[0], + ) + .unwrap(); + + let pvss_aggregated = dkg.aggregate_transcripts(&messages).unwrap(); + assert!(pvss_aggregated.verify(shares_num, &messages).unwrap()); + + // At this point, any given validator should be able to provide a DKG public key + let public_key = dkg.public_key(); + + // In the meantime, the client creates a ciphertext and decryption request + let msg = "my-msg".as_bytes().to_vec(); + let aad: &[u8] = "my-aad".as_bytes(); + let ciphertext = + encrypt(SecretBox::new(msg.clone()), 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(shares_num, &messages) + .unwrap()); + aggregate + .create_decryption_share_simple( + &dkg, + &ciphertext.header().unwrap(), + aad, + validator_keypair, + ) + .unwrap() + }) + .collect(); + + // Now, the decryption share can be used to decrypt the ciphertext + // This part is part of the client API + + // In simple variant, we only need `security_threshold` shares to be able to decrypt + let decryption_shares = + decryption_shares[..security_threshold as usize].to_vec(); + + let shared_secret = combine_shares_simple(&decryption_shares); + let plaintext = + decrypt_with_shared_secret(&ciphertext, aad, &shared_secret) + .unwrap(); + assert_eq!(plaintext, msg); + + // Let's say that we've only received `security_threshold - 1` shares + // In this case, we should not be able to decrypt + let decryption_shares = + decryption_shares[..security_threshold as usize - 1].to_vec(); + + let shared_secret = combine_shares_simple(&decryption_shares); + let result = + decrypt_with_shared_secret(&ciphertext, aad, &shared_secret); + assert!(result.is_err()); + } + } + #[test] fn server_side_local_verification() { let rng = &mut StdRng::seed_from_u64(0); @@ -647,7 +750,7 @@ mod test_ferveo_api { let local_aggregate = dkg.aggregate_transcripts(&messages).unwrap(); assert!(local_aggregate - .verify(dkg.0.dkg_params.shares_num, &messages) + .verify(dkg.0.dkg_params.shares_num(), &messages) .is_ok()); } diff --git a/ferveo/src/bindings_python.rs b/ferveo/src/bindings_python.rs index ed965f3e..00c455a3 100644 --- a/ferveo/src/bindings_python.rs +++ b/ferveo/src/bindings_python.rs @@ -93,7 +93,17 @@ impl From for PyErr { } Error::InvalidVariant(variant) => { InvalidVariant::new_err(variant.to_string()) - } + }, + Error::InvalidDkgParameters(num_shares, security_threshold) => { + InvalidDkgParameters::new_err(format!( + "num_shares: {num_shares}, security_threshold: {security_threshold}" + )) + }, + Error::InvalidShareIndex(index) => { + InvalidShareIndex::new_err(format!( + "{index}" + )) + }, }, _ => default(), } @@ -128,6 +138,8 @@ create_exception!(exceptions, ValidatorPublicKeyMismatch, PyValueError); create_exception!(exceptions, SerializationError, PyValueError); create_exception!(exceptions, InvalidByteLength, PyValueError); create_exception!(exceptions, InvalidVariant, PyValueError); +create_exception!(exceptions, InvalidDkgParameters, PyValueError); +create_exception!(exceptions, InvalidShareIndex, PyValueError); fn from_py_bytes(bytes: &[u8]) -> PyResult { T::from_bytes(bytes) diff --git a/ferveo/src/bindings_wasm.rs b/ferveo/src/bindings_wasm.rs index e412b6e0..5b03f188 100644 --- a/ferveo/src/bindings_wasm.rs +++ b/ferveo/src/bindings_wasm.rs @@ -510,15 +510,13 @@ impl AggregatedTranscript { #[wasm_bindgen] pub fn verify( &self, - shares_num: usize, + shares_num: u32, messages: &ValidatorMessageArray, ) -> JsResult { set_panic_hook(); let messages = unwrap_messages_js(messages)?; - let is_valid = self - .0 - .verify(shares_num as u32, &messages) - .map_err(map_js_err)?; + let is_valid = + self.0.verify(shares_num, &messages).map_err(map_js_err)?; Ok(is_valid) } diff --git a/ferveo/src/dkg.rs b/ferveo/src/dkg.rs index 3f7fc09d..760a6c57 100644 --- a/ferveo/src/dkg.rs +++ b/ferveo/src/dkg.rs @@ -15,9 +15,47 @@ use crate::{ #[derive(Copy, Clone, Debug, Serialize, Deserialize)] pub struct DkgParams { - pub tau: u32, - pub security_threshold: u32, - pub shares_num: u32, + tau: u32, + security_threshold: u32, + shares_num: u32, +} + +impl DkgParams { + /// Create new DKG parameters + /// `tau` is a unique identifier for the DKG (ritual id) + /// `security_threshold` is the minimum number of shares required to reconstruct the key + /// `shares_num` is the total number of shares to be generated + /// Returns an error if the parameters are invalid + /// Parameters must hold: `shares_num` >= `security_threshold` + pub fn new( + tau: u32, + security_threshold: u32, + shares_num: u32, + ) -> Result { + if shares_num < security_threshold { + return Err(Error::InvalidDkgParameters( + shares_num, + security_threshold, + )); + } + Ok(Self { + tau, + security_threshold, + shares_num, + }) + } + + pub fn tau(&self) -> u32 { + self.tau + } + + pub fn security_threshold(&self) -> u32 { + self.security_threshold + } + + pub fn shares_num(&self) -> u32 { + self.shares_num + } } #[derive(Clone, Debug, Serialize, Deserialize, Eq, PartialEq)] @@ -77,14 +115,15 @@ impl PubliclyVerifiableDkg { me: &Validator, ) -> Result { let domain = ark_poly::GeneralEvaluationDomain::::new( - dkg_params.shares_num as usize, + validators.len(), ) .expect("unable to construct domain"); - // Sort the validators to verify a global ordering + // Verify the global ordering of validators if !is_sorted(validators) { return Err(Error::ValidatorsNotSorted); } + // TODO: Should we take it from the coordinator instead of using validators: &[Validator] as input? let validators: ValidatorsMap = validators .iter() .enumerate() @@ -123,6 +162,7 @@ impl PubliclyVerifiableDkg { validators, state: DkgState::Sharing { accumulated_shares: 0, + // TODO: Do we need to keep track of the block number? block: 0, }, }) @@ -151,6 +191,7 @@ impl PubliclyVerifiableDkg { } } + // TODO: Make private, use `share` instead. Currently used only in bindings pub fn create_share( &self, rng: &mut R, @@ -248,6 +289,10 @@ impl PubliclyVerifiableDkg { return Err(Error::UnknownDealer(sender.clone().address)); } + // TODO: Throw error instead of silently accepting excess shares? + // if self.vss.len() < self.dkg_params.shares_num as usize { + // self.vss.insert(sender.address.clone(), pvss.clone()); + // } self.vss.insert(sender.address.clone(), pvss.clone()); // we keep track of the amount of shares seen until the security @@ -321,6 +366,7 @@ pub(crate) mod test_common { pub use ark_bls12_381::Bls12_381 as E; use ferveo_common::Keypair; + use rand::seq::SliceRandom; pub use super::*; @@ -348,22 +394,32 @@ pub(crate) mod test_common { pub type TestSetup = (PubliclyVerifiableDkg, Vec>); - pub fn setup_dkg_for_n_validators( + pub fn setup_dkg_for_me( + security_threshold: u32, + shares_num: u32, + my_index: usize, + ) -> TestSetup { + setup_dkg_for_me_with_n_validators( + security_threshold, + shares_num, + my_index, + shares_num, + ) + } + + pub fn setup_dkg_for_me_with_n_validators( security_threshold: u32, shares_num: u32, my_index: usize, + n_validators: u32, ) -> TestSetup { - let keypairs = gen_keypairs(shares_num); + let keypairs = gen_keypairs(n_validators); let mut validators = gen_validators(keypairs.as_slice()); validators.sort(); let me = validators[my_index].clone(); let dkg = PubliclyVerifiableDkg::new( &validators, - &DkgParams { - tau: 0, - security_threshold, - shares_num, - }, + &DkgParams::new(0, security_threshold, shares_num).unwrap(), &me, ) .expect("Setup failed"); @@ -373,44 +429,56 @@ pub(crate) mod test_common { /// Create a test dkg /// /// The [`test_dkg_init`] module checks correctness of this setup - pub fn setup_dkg(validator: usize) -> TestSetup { - setup_dkg_for_n_validators(2, 4, validator) + pub fn setup_dkg() -> TestSetup { + setup_dkg_for_me(2, 4, 0) } /// Set up a dkg with enough pvss transcripts to meet the threshold /// /// The correctness of this function is tested in the module [`test_dealing`] pub fn setup_dealt_dkg() -> TestSetup { - setup_dealt_dkg_with_n_validators(2, 4) + setup_dealt_dkg_with_n_validators(2, 4, 4) } pub fn setup_dealt_dkg_with_n_validators( security_threshold: u32, shares_num: u32, + validators_num: u32, ) -> TestSetup { let rng = &mut ark_std::test_rng(); // Gather everyone's transcripts - let messages: Vec<_> = (0..shares_num) + let mut dkgs = Vec::new(); + let mut messages: Vec<_> = (0..validators_num) .map(|my_index| { - let (mut dkg, _) = setup_dkg_for_n_validators( + let (mut dkg, _) = setup_dkg_for_me_with_n_validators( security_threshold, shares_num, my_index as usize, + validators_num, ); + dkgs.push(dkg.clone()); + let me = dkg.me.validator.clone(); let message = dkg.share(rng).unwrap(); + println!("{my_index}"); (me, message) }) .collect(); // Create a test DKG instance - let (mut dkg, keypairs) = - setup_dkg_for_n_validators(security_threshold, shares_num, 0); - messages.iter().for_each(|(sender, message)| { - dkg.apply_message(sender, message).expect("Setup failed"); - }); - (dkg, keypairs) + let mut dkg = dkgs.pop().unwrap(); + + // `shares_num` is either equal or lower than `validators_num`, so we always take `shares_num` messages + assert!(shares_num <= validators_num); + // Make sure messages are out of order - The ordering should not matter + messages.shuffle(rng); + messages.iter().take(shares_num as usize).for_each( + |(sender, message)| { + dkg.apply_message(sender, message).expect("Setup failed"); + }, + ); + (dkg, gen_keypairs(validators_num)) } } @@ -450,29 +518,41 @@ mod test_dkg_init { #[cfg(test)] mod test_dealing { use ark_ec::AffineRepr; + use test_case::test_case; use super::test_common::*; use crate::DkgState::Dealt; - /// Test that dealing correct PVSS transcripts - /// pass verification an application and that + /// Test that dealing correct PVSS transcripts pass verification an application and that /// state is updated correctly - #[test] - fn test_pvss_dealing() { - let rng = &mut ark_std::test_rng(); + #[test_case(4, 4 ; "number of shares is equal to number of validators")] + #[test_case(4, 6 ; "number of shares is smaller than the number of validators")] + fn test_pvss_dealing(shares_num: u32, validator_num: u32) { + let threshold = 3; + // Create a test DKG instance + let (mut dkg, _) = setup_dkg_for_me_with_n_validators( + threshold, + shares_num, + 0, + validator_num, + ); // Gather everyone's transcripts let mut messages = vec![]; - for i in 0..4 { - let (mut dkg, _) = setup_dkg(i); + let rng = &mut ark_std::test_rng(); + // Notice how we use `shares_num` instead of `validator_num` - Not everyone will send a message + for i in 0..shares_num { + let (mut dkg, _) = setup_dkg_for_me_with_n_validators( + threshold, + shares_num, + i as usize, + validator_num, + ); let message = dkg.share(rng).unwrap(); let sender = dkg.me.validator.clone(); messages.push((sender, message)); } - // Create a test DKG instance - let (mut dkg, _) = setup_dkg(0); - let mut expected = 0u32; for (sender, pvss) in messages.iter() { // Check the verification passes @@ -483,7 +563,7 @@ mod test_dealing { expected += 1; if expected < dkg.dkg_params.security_threshold { - // check that shares accumulates correctly + // Check that shares accumulates correctly match dkg.state { DkgState::Sharing { accumulated_shares, .. @@ -505,7 +585,7 @@ mod test_dealing { #[test] fn test_pvss_from_unknown_dealer_rejected() { let rng = &mut ark_std::test_rng(); - let (mut dkg, _) = setup_dkg(0); + let (mut dkg, _) = setup_dkg(); assert!(matches!( dkg.state, DkgState::Sharing { @@ -538,7 +618,7 @@ mod test_dealing { #[test] fn test_pvss_sent_twice_rejected() { let rng = &mut ark_std::test_rng(); - let (mut dkg, _) = setup_dkg(0); + let (mut dkg, _) = setup_dkg(); // We start with an empty state assert!(matches!( dkg.state, @@ -573,7 +653,7 @@ mod test_dealing { #[test] fn test_own_pvss() { let rng = &mut ark_std::test_rng(); - let (mut dkg, _) = setup_dkg(0); + let (mut dkg, _) = setup_dkg(); // We start with an empty state assert!(matches!( dkg.state, @@ -613,7 +693,7 @@ mod test_dealing { #[test] fn test_pvss_cannot_share_from_wrong_state() { let rng = &mut ark_std::test_rng(); - let (mut dkg, _) = setup_dkg(0); + let (mut dkg, _) = setup_dkg(); assert!(matches!( dkg.state, DkgState::Sharing { @@ -638,7 +718,7 @@ mod test_dealing { #[test] fn test_share_message_state_guards() { let rng = &mut ark_std::test_rng(); - let (mut dkg, _) = setup_dkg(0); + let (mut dkg, _) = setup_dkg(); let pvss = dkg.share(rng).unwrap(); assert!(matches!( dkg.state, @@ -751,3 +831,21 @@ mod test_aggregation { assert!(dkg.verify_message(&sender, &aggregate).is_err()); } } + +/// Test DKG parameters +#[cfg(test)] +mod test_dkg_params { + const TAU: u32 = 0; + + #[test] + fn test_shares_num_less_than_security_threshold() { + let dkg_params = super::DkgParams::new(TAU, 4, 3); + assert!(dkg_params.is_err()); + } + + #[test] + fn test_valid_dkg_params() { + let dkg_params = super::DkgParams::new(TAU, 2, 3); + assert!(dkg_params.is_ok()); + } +} diff --git a/ferveo/src/lib.rs b/ferveo/src/lib.rs index 59a44024..72a9a183 100644 --- a/ferveo/src/lib.rs +++ b/ferveo/src/lib.rs @@ -101,6 +101,12 @@ pub enum Error { #[error("Invalid variant: {0}")] InvalidVariant(String), + + #[error("Invalid DKG parameters: number of shares {0}, threshold {1}")] + InvalidDkgParameters(u32, u32), + + #[error("Invalid share index: {0}")] + InvalidShareIndex(u32), } pub type Result = std::result::Result; @@ -131,6 +137,7 @@ mod test_dkg_full { SharedSecret, }; use itertools::izip; + use test_case::test_case; use super::*; use crate::dkg::test_common::*; @@ -189,114 +196,125 @@ mod test_dkg_full { (pvss_aggregated, decryption_shares, shared_secret) } - #[test] - fn test_dkg_simple_tdec() { + #[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, 4; "number of shares is equal to number of validators")] + #[test_case(4, 6; "number of shares is smaller than the number of validators")] + fn test_dkg_simple_tdec(shares_num: u32, validator_num: u32) { let rng = &mut test_rng(); - // Works for both power of 2 and non-power of 2 - for shares_num in [4, 7] { - let threshold = shares_num / 2 + 1; - let (dkg, validator_keypairs) = - setup_dealt_dkg_with_n_validators(threshold, shares_num); - let msg = "my-msg".as_bytes().to_vec(); - let aad: &[u8] = "my-aad".as_bytes(); - let public_key = dkg.public_key(); - let ciphertext = ferveo_tdec::encrypt::( - SecretBox::new(msg.clone()), - aad, - &public_key, - rng, - ) - .unwrap(); + let threshold = shares_num / 2 + 1; + let (dkg, validator_keypairs) = setup_dealt_dkg_with_n_validators( + threshold, + shares_num, + validator_num, + ); + let msg = "my-msg".as_bytes().to_vec(); + let aad: &[u8] = "my-aad".as_bytes(); + let public_key = dkg.public_key(); + let ciphertext = ferveo_tdec::encrypt::( + SecretBox::new(msg.clone()), + aad, + &public_key, + rng, + ) + .unwrap(); - let (_, _, shared_secret) = make_shared_secret_simple_tdec( - &dkg, - aad, - &ciphertext.header().unwrap(), - validator_keypairs.as_slice(), - ); + let (_, _, shared_secret) = make_shared_secret_simple_tdec( + &dkg, + aad, + &ciphertext.header().unwrap(), + validator_keypairs.as_slice(), + ); - let plaintext = ferveo_tdec::decrypt_with_shared_secret( - &ciphertext, - aad, - &shared_secret, - &dkg.pvss_params.g_inv(), - ) - .unwrap(); - assert_eq!(plaintext, msg); - } + let plaintext = ferveo_tdec::decrypt_with_shared_secret( + &ciphertext, + aad, + &shared_secret, + &dkg.pvss_params.g_inv(), + ) + .unwrap(); + assert_eq!(plaintext, msg); } - #[test] - fn test_dkg_simple_tdec_precomputed() { + #[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, 4; "number of shares is equal to number of validators")] + #[test_case(4, 6; "number of shares is smaller than the number of validators")] + fn test_dkg_simple_tdec_precomputed(shares_num: u32, validators_num: u32) { let rng = &mut test_rng(); - // Works for both power of 2 and non-power of 2 - for shares_num in [4, 7] { - // In precomputed variant, threshold must be equal to shares_num - let threshold = shares_num; - let (dkg, validator_keypairs) = - setup_dealt_dkg_with_n_validators(threshold, shares_num); - let msg = "my-msg".as_bytes().to_vec(); - let aad: &[u8] = "my-aad".as_bytes(); - let public_key = dkg.public_key(); - let ciphertext = ferveo_tdec::encrypt::( - SecretBox::new(msg.clone()), - aad, - &public_key, - rng, - ) - .unwrap(); + // In precomputed variant, threshold must be equal to shares_num + let threshold = shares_num; + let (dkg, validator_keypairs) = setup_dealt_dkg_with_n_validators( + threshold, + shares_num, + validators_num, + ); + let msg = "my-msg".as_bytes().to_vec(); + let aad: &[u8] = "my-aad".as_bytes(); + let public_key = dkg.public_key(); + let ciphertext = ferveo_tdec::encrypt::( + SecretBox::new(msg.clone()), + aad, + &public_key, + rng, + ) + .unwrap(); - let pvss_aggregated = aggregate(&dkg.vss); - pvss_aggregated.verify_aggregation(&dkg).unwrap(); - let domain_points = dkg - .domain - .elements() - .take(validator_keypairs.len()) - .collect::>(); - - let decryption_shares: Vec> = - validator_keypairs - .iter() - .map(|validator_keypair| { - let validator = dkg - .get_validator(&validator_keypair.public_key()) - .unwrap(); - pvss_aggregated - .make_decryption_share_simple_precomputed( - &ciphertext.header().unwrap(), - aad, - &validator_keypair.decryption_key, - validator.share_index, - &domain_points, - &dkg.pvss_params.g_inv(), - ) - .unwrap() - }) - .collect(); - assert_eq!(domain_points.len(), decryption_shares.len()); + let pvss_aggregated = aggregate(&dkg.vss); + pvss_aggregated.verify_aggregation(&dkg).unwrap(); + let domain_points = dkg + .domain + .elements() + .take(validator_keypairs.len()) + .collect::>(); + + let decryption_shares: Vec> = + validator_keypairs + .iter() + .map(|validator_keypair| { + let validator = dkg + .get_validator(&validator_keypair.public_key()) + .unwrap(); + pvss_aggregated + .make_decryption_share_simple_precomputed( + &ciphertext.header().unwrap(), + aad, + &validator_keypair.decryption_key, + validator.share_index, + &domain_points, + &dkg.pvss_params.g_inv(), + ) + .unwrap() + }) + .collect(); + assert_eq!(domain_points.len(), decryption_shares.len()); - let shared_secret = - ferveo_tdec::share_combine_precomputed::(&decryption_shares); + let shared_secret = + ferveo_tdec::share_combine_precomputed::(&decryption_shares); - // Combination works, let's decrypt - let plaintext = ferveo_tdec::decrypt_with_shared_secret( - &ciphertext, - aad, - &shared_secret, - &dkg.pvss_params.g_inv(), - ) - .unwrap(); - assert_eq!(plaintext, msg); - } + // Combination works, let's decrypt + let plaintext = ferveo_tdec::decrypt_with_shared_secret( + &ciphertext, + aad, + &shared_secret, + &dkg.pvss_params.g_inv(), + ) + .unwrap(); + assert_eq!(plaintext, msg); } - #[test] - fn test_dkg_simple_tdec_share_verification() { + #[test_case(4, 4; "number of shares is equal to number of validators")] + #[test_case(4, 6; "number of shares is smaller than the number of validators")] + fn test_dkg_simple_tdec_share_verification( + shares_num: u32, + validators_num: u32, + ) { let rng = &mut test_rng(); - let (dkg, validator_keypairs) = setup_dealt_dkg_with_n_validators(3, 4); + let (dkg, validator_keypairs) = + setup_dealt_dkg_with_n_validators(2, shares_num, validators_num); let msg = "my-msg".as_bytes().to_vec(); let aad: &[u8] = "my-aad".as_bytes(); let public_key = dkg.public_key(); @@ -322,7 +340,7 @@ mod test_dkg_full { &decryption_shares, ) .for_each( - |(aggregated_share, validator_keypair, decryption_share)| { + |((_, aggregated_share), validator_keypair, decryption_share)| { assert!(decryption_share.verify( aggregated_share, &validator_keypair.public_key().encryption_key, @@ -339,7 +357,7 @@ mod test_dkg_full { let mut with_bad_decryption_share = decryption_share.clone(); with_bad_decryption_share.decryption_share = TargetField::zero(); assert!(!with_bad_decryption_share.verify( - &pvss_aggregated.shares[0], + &pvss_aggregated.shares[&0], &validator_keypairs[0].public_key().encryption_key, &dkg.pvss_params.h, &ciphertext, @@ -349,21 +367,28 @@ mod test_dkg_full { let mut with_bad_checksum = decryption_share; with_bad_checksum.validator_checksum.checksum = G1Affine::zero(); assert!(!with_bad_checksum.verify( - &pvss_aggregated.shares[0], + &pvss_aggregated.shares[&0], &validator_keypairs[0].public_key().encryption_key, &dkg.pvss_params.h, &ciphertext, )); } - #[test] - fn test_dkg_simple_tdec_share_recovery() { + #[test_case(4, 4; "number of shares is equal to number of validators")] + // TODO: Doesn't work - Should it work? Or is a case that we don't care about? + // #[test_case(4, 6; "number of shares is smaller than the number of validators")] + fn test_dkg_simple_tdec_share_recovery( + shares_num: u32, + validators_num: u32, + ) { let rng = &mut test_rng(); let security_threshold = 3; - let shares_num = 4; - let (dkg, validator_keypairs) = - setup_dealt_dkg_with_n_validators(security_threshold, shares_num); + let (dkg, validator_keypairs) = setup_dealt_dkg_with_n_validators( + security_threshold, + shares_num, + validators_num, + ); let msg = "my-msg".as_bytes().to_vec(); let aad: &[u8] = "my-aad".as_bytes(); let public_key = &dkg.public_key(); @@ -410,7 +435,7 @@ mod test_dkg_full { &domain_points, &dkg.pvss_params.h.into_affine(), &x_r, - dkg.dkg_params.security_threshold as usize, + dkg.dkg_params.security_threshold() as usize, rng, ); (v_addr.clone(), deltas_i) @@ -439,11 +464,13 @@ mod test_dkg_full { // Creates updated private key shares // TODO: Why not using dkg.aggregate()? let pvss_aggregated = aggregate(&dkg.vss); - pvss_aggregated.update_private_key_share_for_recovery( - &decryption_key, - validator.share_index, - updates_for_participant.as_slice(), - ) + pvss_aggregated + .update_private_key_share_for_recovery( + &decryption_key, + validator.share_index, + updates_for_participant.as_slice(), + ) + .unwrap() }) .collect(); @@ -515,14 +542,16 @@ mod test_dkg_full { ); } - #[test] - fn test_dkg_simple_tdec_share_refreshing() { + #[test_case(4, 4; "number of shares is equal to number of validators")] + #[test_case(4, 6; "number of shares is smaller than the number of validators")] + fn test_dkg_simple_tdec_share_refreshing( + shares_num: u32, + validators_num: u32, + ) { let rng = &mut test_rng(); - let security_threshold = 3; - let shares_num = 4; let (dkg, validator_keypairs) = - setup_dealt_dkg_with_n_validators(security_threshold, shares_num); + setup_dealt_dkg_with_n_validators(2, shares_num, validators_num); let msg = "my-msg".as_bytes().to_vec(); let aad: &[u8] = "my-aad".as_bytes(); let public_key = &dkg.public_key(); @@ -552,7 +581,7 @@ mod test_dkg_full { let deltas_i = prepare_share_updates_for_refresh::( &domain_points, &dkg.pvss_params.h.into_affine(), - dkg.dkg_params.security_threshold as usize, + dkg.dkg_params.security_threshold() as usize, rng, ); (v_addr.clone(), deltas_i) @@ -582,11 +611,13 @@ mod test_dkg_full { // Creates updated private key shares // TODO: Why not using dkg.aggregate()? let pvss_aggregated = aggregate(&dkg.vss); - pvss_aggregated.update_private_key_share_for_recovery( - &decryption_key, - validator.share_index, - updates_for_participant.as_slice(), - ) + pvss_aggregated + .update_private_key_share_for_recovery( + &decryption_key, + validator.share_index, + updates_for_participant.as_slice(), + ) + .unwrap() }) .collect(); @@ -608,10 +639,10 @@ mod test_dkg_full { .collect(); let lagrange = ferveo_tdec::prepare_combine_simple::( - &domain_points[..security_threshold as usize], + &domain_points[..dkg.dkg_params.security_threshold() as usize], ); let new_shared_secret = ferveo_tdec::share_combine_simple::( - &decryption_shares[..security_threshold as usize], + &decryption_shares[..dkg.dkg_params.security_threshold() as usize], &lagrange, ); diff --git a/ferveo/src/pvss.rs b/ferveo/src/pvss.rs index 4f63da82..c5b6f2de 100644 --- a/ferveo/src/pvss.rs +++ b/ferveo/src/pvss.rs @@ -19,12 +19,15 @@ use zeroize::{self, Zeroize, ZeroizeOnDrop}; use crate::{ apply_updates_to_private_share, batch_to_projective_g1, - batch_to_projective_g2, utils::is_sorted, Error, PVSSMap, - PubliclyVerifiableDkg, Result, Validator, + batch_to_projective_g2, Error, PVSSMap, PubliclyVerifiableDkg, Result, + ValidatorsMap, }; /// These are the blinded evaluations of shares of a single random polynomial -pub type ShareEncryptions = ::G2Affine; +pub type ShareEncryption = ::G2Affine; + +pub type ShareEncryptionMap = + std::collections::BTreeMap>; /// Marker struct for unaggregated PVSS transcripts #[derive(Serialize, Deserialize, Clone, Debug, PartialEq, Eq)] @@ -115,7 +118,7 @@ pub struct PubliclyVerifiableSS { /// 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 - pub shares: Vec, + pub shares: ShareEncryptionMap, /// Proof of Knowledge #[serde_as(as = "ferveo_common::serialization::SerdeAs")] @@ -138,7 +141,7 @@ impl PubliclyVerifiableSS { ) -> Result { let phi = SecretPolynomial::::new( s, - (dkg.dkg_params.security_threshold - 1) as usize, + (dkg.dkg_params.security_threshold() - 1) as usize, rng, ); @@ -151,17 +154,23 @@ impl PubliclyVerifiableSS { .values() .map(|validator| { // ek_{i}^{eval_i}, i = validator index - fast_multiexp( + let share_encryption = fast_multiexp( // &evals.evals[i..i] = &evals.evals[i] &[evals.evals[validator.share_index]], // one share per validator validator.validator.public_key.encryption_key.into_group(), - )[0] + )[0]; + println!( + "share_index: {}, share_encryption: {}", + validator.share_index, share_encryption + ); + (validator.share_index, share_encryption) }) - .collect::>>(); - if shares.len() != dkg.validators.len() { + .collect::>(); + + if shares.len() < dkg.dkg_params.shares_num() as usize { return Err(Error::InsufficientValidators( shares.len() as u32, - dkg.validators.len() as u32, + dkg.dkg_params.shares_num(), )); } @@ -201,17 +210,11 @@ impl PubliclyVerifiableSS { /// transcript was at fault so that the can issue a new one. This /// function may also be used for that purpose. pub fn verify_full(&self, dkg: &PubliclyVerifiableDkg) -> bool { - let validators = dkg - .validators - .values() - .map(|v| v.validator.clone()) - .collect::>(); - let validators = validators.as_slice(); do_verify_full( &self.coeffs, &self.shares, &dkg.pvss_params, - validators, + &dkg.validators, &dkg.domain, ) } @@ -220,58 +223,67 @@ impl PubliclyVerifiableSS { // TODO: Return validator that failed the check pub fn do_verify_full( pvss_coefficients: &[E::G1Affine], - pvss_encrypted_shares: &[E::G2Affine], + pvss_share_map: &ShareEncryptionMap, pvss_params: &PubliclyVerifiableParams, - validators: &[Validator], + validator_map: &ValidatorsMap, domain: &ark_poly::GeneralEvaluationDomain, ) -> bool { let mut commitment = batch_to_projective_g1::(pvss_coefficients); domain.fft_in_place(&mut commitment); - // At this point, validators must be sorted - assert!(is_sorted(validators)); + for validator in validator_map.values() { + println!("validator.share_index: {}", validator.share_index); + } + + println!("pvss_encrypted_shares.len(): {}", pvss_share_map.len()); + println!("validator_map.len(): {}", validator_map.len()); // Each validator checks that their share is correct - validators - .iter() - .zip(pvss_encrypted_shares.iter()) - .enumerate() - .all(|(share_index, (validator, y_i))| { - // TODO: Check #3 is missing - // See #3 in 4.2.3 section of https://eprint.iacr.org/2022/898.pdf - - // Validator checks aggregated shares against commitment - let ek_i = validator.public_key.encryption_key.into_group(); - let a_i = &commitment[share_index]; - // We verify that e(G, Y_i) = e(A_i, ek_i) for validator i - // See #4 in 4.2.3 section of https://eprint.iacr.org/2022/898.pdf - // e(G,Y) = e(A, ek) - E::pairing(pvss_params.g, *y_i) == E::pairing(a_i, ek_i) - }) + validator_map.values().all(|validator| { + // TODO: Check #3 is missing + // See #3 in 4.2.3 section of https://eprint.iacr.org/2022/898.pdf + + // Validator checks aggregated shares against commitment + let ek_i = validator.validator.public_key.encryption_key.into_group(); + let a_i = &commitment[validator.share_index]; + let y_i = &pvss_share_map[&validator.share_index]; + println!("share index: {}", validator.share_index); + println!("ek_i: {ek_i}"); + println!("a_i: {a_i}"); + println!("y_i: {y_i}"); + + // We verify that e(G, Y_i) = e(A_i, ek_i) for validator i + // See #4 in 4.2.3 section of https://eprint.iacr.org/2022/898.pdf + // e(G,Y) = e(A, ek) + let result = E::pairing(pvss_params.g, *y_i) == E::pairing(a_i, ek_i); + println!("result: {result}"); + result + }) } pub fn do_verify_aggregation( pvss_agg_coefficients: &[E::G1Affine], - pvss_agg_encrypted_shares: &[E::G2Affine], + share_encryption_map: &ShareEncryptionMap, pvss_params: &PubliclyVerifiableParams, - validators: &[Validator], + validator_map: &ValidatorsMap, domain: &ark_poly::GeneralEvaluationDomain, - vss: &PVSSMap, + pvss_map: &PVSSMap, ) -> Result { let is_valid = do_verify_full( pvss_agg_coefficients, - pvss_agg_encrypted_shares, + share_encryption_map, pvss_params, - validators, + validator_map, domain, ); if !is_valid { + // TODO: Throws here return Err(Error::InvalidTranscriptAggregate); } // Now, we verify that the aggregated PVSS transcript is a valid aggregation let mut y = E::G1::zero(); - for (_, pvss) in vss.iter() { + for (_, pvss) in pvss_map.iter() { y += pvss.coeffs[0].into_group(); } if y.into_affine() == pvss_agg_coefficients[0] { @@ -291,17 +303,11 @@ impl PubliclyVerifiableSS { &self, dkg: &PubliclyVerifiableDkg, ) -> Result { - let validators = dkg - .validators - .values() - .map(|v| v.validator.clone()) - .collect::>(); - let validators = validators.as_slice(); do_verify_aggregation( &self.coeffs, &self.shares, &dkg.pvss_params, - validators, + &dkg.validators, &dkg.domain, &dkg.vss, ) @@ -311,19 +317,19 @@ impl PubliclyVerifiableSS { &self, validator_decryption_key: &E::ScalarField, share_index: usize, - ) -> PrivateKeyShare { + ) -> 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) - .unwrap() + .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(); - PrivateKeyShare { private_key_share } + Ok(PrivateKeyShare { private_key_share }) } pub fn make_decryption_share_simple( @@ -335,7 +341,7 @@ impl PubliclyVerifiableSS { g_inv: &E::G1Prepared, ) -> Result> { let private_key_share = self - .decrypt_private_key_share(validator_decryption_key, share_index); + .decrypt_private_key_share(validator_decryption_key, share_index)?; DecryptionShareSimple::create( validator_decryption_key, &private_key_share, @@ -356,7 +362,7 @@ impl PubliclyVerifiableSS { g_inv: &E::G1Prepared, ) -> Result> { let private_key_share = self - .decrypt_private_key_share(validator_decryption_key, share_index); + .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); @@ -379,13 +385,16 @@ impl PubliclyVerifiableSS { validator_decryption_key: &E::ScalarField, share_index: usize, share_updates: &[E::G2], - ) -> PrivateKeyShare { + ) -> Result> { // Retrieves their private key share let private_key_share = self - .decrypt_private_key_share(validator_decryption_key, share_index); + .decrypt_private_key_share(validator_decryption_key, share_index)?; // And updates their share - apply_updates_to_private_share::(&private_key_share, share_updates) + Ok(apply_updates_to_private_share::( + &private_key_share, + share_updates, + )) } } @@ -402,7 +411,13 @@ pub fn aggregate( let mut coeffs = batch_to_projective_g1::(&first_pvss.coeffs); let mut sigma = first_pvss.sigma; - let mut shares = batch_to_projective_g2::(&first_pvss.shares); + // Ordering of shares doesn't matter here, so we can just collect them from the map + let shares = first_pvss + .shares + .values() + .cloned() + .collect::>>(); + let mut shares = batch_to_projective_g2::(&shares); // So now we're iterating over the PVSS instances, and adding their coefficients and shares, and their sigma // sigma is the sum of all the sigma_i, which is the proof of knowledge of the secret polynomial @@ -416,9 +431,16 @@ pub fn aggregate( shares .iter_mut() .zip_eq(next.shares.iter()) - .for_each(|(a, b)| *a += b); + .for_each(|(a, (_, b))| *a += b); } let shares = E::G2::normalize_batch(&shares); + // TODO: There is just one share, but we need to convert it back to a map + // TODO: Find a better way to do this + let shares = shares + .into_iter() + .enumerate() + .map(|(i, share)| (i, share)) + .collect::>(); PubliclyVerifiableSS { coeffs: E::G1::normalize_batch(&coeffs), @@ -430,18 +452,30 @@ pub fn aggregate( pub fn aggregate_for_decryption( dkg: &PubliclyVerifiableDkg, -) -> Vec> { +) -> Vec> { // From docs: https://nikkolasg.github.io/ferveo/pvss.html?highlight=aggregate#aggregation // "Two PVSS instances may be aggregated into a single PVSS instance by adding elementwise each of the corresponding group elements." - let shares = dkg + let share_maps = dkg .vss .values() .map(|pvss| pvss.shares.clone()) - .collect::>(); - let first_share = shares - .first() - .expect("Need one or more decryption shares to aggregate") - .to_vec(); + .collect::>>(); + + // Now, get shares from each map. Sort them by index, and then collect them into a vector + let shares = share_maps + .into_iter() + .map(|share_map| { + share_map + .into_iter() + .sorted_by_key(|(index, _)| *index) + .map(|(_, share)| share) + .collect::>>() + }) + .collect::>>>(); + + // Now, we're going to add the shares together + let first_share = shares[0].clone(); + shares .into_iter() .skip(1) @@ -456,10 +490,11 @@ pub fn aggregate_for_decryption( #[cfg(test)] mod test_pvss { - use ark_bls12_381::Bls12_381 as EllipticCurve; + use ark_bls12_381::{Bls12_381 as EllipticCurve, Bls12_381}; use ark_ec::AffineRepr; use ark_ff::UniformRand; use rand::seq::SliceRandom; + use test_case::test_case; use super::*; use crate::{dkg::test_common::*, utils::is_sorted}; @@ -468,12 +503,35 @@ mod test_pvss { type G1 = ::G1Affine; type G2 = ::G2Affine; + fn _setup_dkg( + shares_num: u32, + validators_num: u32, + ) -> PubliclyVerifiableDkg { + let (dkg, _) = setup_dkg_for_me_with_n_validators( + 2, + shares_num, + 0, + validators_num, + ); + dkg + } + + fn _setup_dealt_dkg( + shares_num: u32, + validators_num: u32, + ) -> PubliclyVerifiableDkg { + let (dkg, _) = + setup_dealt_dkg_with_n_validators(2, shares_num, validators_num); + dkg + } + /// Test the happy flow that a pvss with the correct form is created /// and that appropriate validations pass - #[test] - fn test_new_pvss() { + #[test_case(4, 4; "number of shares is equal to number of validators")] + #[test_case(4, 6; "number of shares is smaller than the number of validators")] + fn test_new_pvss(shares_num: u32, validators_num: u32) { let rng = &mut ark_std::test_rng(); - let (dkg, _) = setup_dkg(0); + let dkg = _setup_dkg(shares_num, validators_num); let s = ScalarField::rand(rng); let pvss = PubliclyVerifiableSS::::new(&s, &dkg, rng) .expect("Test failed"); @@ -482,7 +540,7 @@ mod test_pvss { // Check that a polynomial of the correct degree was created assert_eq!( pvss.coeffs.len(), - dkg.dkg_params.security_threshold as usize + dkg.dkg_params.security_threshold() as usize ); // Check that the correct number of shares were created assert_eq!(pvss.shares.len(), dkg.validators.len()); @@ -496,10 +554,15 @@ mod test_pvss { /// Check that if the proof of knowledge is wrong, /// the optimistic verification of PVSS fails - #[test] - fn test_verify_pvss_wrong_proof_of_knowledge() { + #[test_case(4, 4; "number of shares is equal to number of validators")] + #[test_case(4, 6; "number of shares is smaller than the number of validators")] + fn test_verify_pvss_wrong_proof_of_knowledge( + shares_num: u32, + validators_num: u32, + ) { let rng = &mut ark_std::test_rng(); - let (dkg, _) = setup_dkg(0); + // Make sure it works for relaxed DKG ceremony constraints + let dkg = _setup_dkg(shares_num, validators_num); let mut s = ScalarField::rand(rng); // Ensure that the proof of knowledge is not zero while s == ScalarField::zero() { @@ -514,13 +577,20 @@ mod test_pvss { } /// Check that if PVSS shares are tampered with, the full verification fails - #[test] - fn test_verify_pvss_bad_shares() { + #[test_case(4, 4; "number of shares is equal to number of validators")] + #[test_case(4, 6; "number of shares is smaller than the number of validators")] + fn test_verify_pvss_bad_shares(shares_num: u32, validators_num: u32) { let rng = &mut ark_std::test_rng(); - let (dkg, _) = setup_dkg(0); - let s = ScalarField::rand(rng); + let keypairs = gen_keypairs(validators_num); + let mut validators = gen_validators(&keypairs); + validators.sort(); + let _me = validators[0].clone(); + + let dkg = _setup_dkg(shares_num, validators_num); + let secret = ScalarField::rand(rng); let pvss = - PubliclyVerifiableSS::::new(&s, &dkg, rng).unwrap(); + PubliclyVerifiableSS::::new(&secret, &dkg, rng) + .unwrap(); // So far, everything works assert!(pvss.verify_optimistic()); @@ -528,7 +598,7 @@ mod test_pvss { // Now, we're going to tamper with the PVSS shares let mut bad_pvss = pvss; - bad_pvss.shares[0] = G2::zero(); + bad_pvss.shares.insert(0_usize, G2::zero()); // Optimistic verification should not catch this issue assert!(bad_pvss.verify_optimistic()); @@ -543,7 +613,6 @@ mod test_pvss { let rng = &mut ark_std::test_rng(); let shares_num = 4; - let security_threshold = shares_num - 1; let keypairs = gen_keypairs(shares_num); let mut validators = gen_validators(&keypairs); let me = validators[0].clone(); @@ -555,11 +624,7 @@ mod test_pvss { // And because of that the DKG should fail let result = PubliclyVerifiableDkg::new( &validators, - &DkgParams { - tau: 0, - security_threshold, - shares_num, - }, + &DkgParams::new(0, 2, shares_num).unwrap(), &me, ); assert!(result.is_err()); @@ -571,14 +636,15 @@ mod test_pvss { /// Check that happy flow of aggregating PVSS transcripts /// Should have the correct form and validations pass - #[test] - fn test_aggregate_pvss() { - let (dkg, _) = setup_dealt_dkg(); + #[test_case(4, 4; "number of shares is equal to number of validators")] + #[test_case(4, 6; "number of shares is smaller than the number of validators")] + fn test_aggregate_pvss(shares_num: u32, validators_num: u32) { + let dkg = _setup_dealt_dkg(shares_num, validators_num); let aggregate = aggregate(&dkg.vss); // Check that a polynomial of the correct degree was created assert_eq!( aggregate.coeffs.len(), - dkg.dkg_params.security_threshold as usize + dkg.dkg_params.security_threshold() as usize ); // Check that the correct number of shares were created assert_eq!(aggregate.shares.len(), dkg.validators.len()); @@ -592,12 +658,16 @@ mod test_pvss { /// Check that if the aggregated pvss transcript has an /// incorrect constant term, the verification fails - #[test] - fn test_verify_aggregation_fails_if_constant_term_wrong() { - let (dkg, _) = setup_dealt_dkg(); + #[test_case(4, 4; "number of shares is equal to number of validators")] + #[test_case(4, 6; "number of shares is smaller than the number of validators")] + fn test_verify_aggregation_fails_if_constant_term_wrong( + shares_num: u32, + validators_num: u32, + ) { + let dkg = _setup_dealt_dkg(shares_num, validators_num); let mut aggregated = aggregate(&dkg.vss); while aggregated.coeffs[0] == G1::zero() { - let (dkg, _) = setup_dkg(0); + let (dkg, _) = setup_dkg(); aggregated = aggregate(&dkg.vss); } aggregated.coeffs[0] = G1::zero();