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-tdec/benches/tpke.rs b/ferveo-tdec/benches/tpke.rs index e47d6b51..420bf869 100644 --- a/ferveo-tdec/benches/tpke.rs +++ b/ferveo-tdec/benches/tpke.rs @@ -189,7 +189,7 @@ pub fn bench_create_decryption_share(c: &mut Criterion) { .map(|ctx| { // Using create_unchecked here to avoid the cost of verifying the ciphertext DecryptionShareSimple::create_unchecked( - &ctx.validator_private_key, + &ctx.setup_params.b, &ctx.private_key_share, &setup.shared.ciphertext.header().unwrap(), ) diff --git a/ferveo-tdec/src/context.rs b/ferveo-tdec/src/context.rs index 32fa91fb..238db71c 100644 --- a/ferveo-tdec/src/context.rs +++ b/ferveo-tdec/src/context.rs @@ -29,7 +29,7 @@ pub struct PublicDecryptionContextSimple { #[derive(Clone, Debug)] pub struct SetupParams { - pub b: E::ScalarField, + pub b: E::ScalarField, // Validator private key pub b_inv: E::ScalarField, pub g: E::G1Affine, pub g_inv: E::G1Prepared, @@ -71,8 +71,6 @@ pub struct PrivateDecryptionContextSimple { pub setup_params: SetupParams, pub private_key_share: PrivateKeyShare, pub public_decryption_contexts: Vec>, - // TODO: Remove/replace with `setup_params.b` after refactoring - pub validator_private_key: E::ScalarField, } impl PrivateDecryptionContextSimple { @@ -82,7 +80,7 @@ impl PrivateDecryptionContextSimple { aad: &[u8], ) -> Result> { DecryptionShareSimple::create( - &self.validator_private_key, + &self.setup_params.b, &self.private_key_share, ciphertext_header, aad, @@ -104,7 +102,7 @@ impl PrivateDecryptionContextSimple { DecryptionSharePrecomputed::new( self.index, - &self.validator_private_key, + &self.setup_params.b, &self.private_key_share, ciphertext_header, aad, diff --git a/ferveo-tdec/src/lib.rs b/ferveo-tdec/src/lib.rs index a5c1b302..297b066c 100644 --- a/ferveo-tdec/src/lib.rs +++ b/ferveo-tdec/src/lib.rs @@ -243,7 +243,6 @@ pub mod test_common { h, }, private_key_share, - validator_private_key: b, public_decryption_contexts: vec![], }); public_contexts.push(PublicDecryptionContextSimple:: { diff --git a/ferveo-wasm/examples/node/src/main.test.ts b/ferveo-wasm/examples/node/src/main.test.ts index 28144861..047beeb5 100644 --- a/ferveo-wasm/examples/node/src/main.test.ts +++ b/ferveo-wasm/examples/node/src/main.test.ts @@ -22,11 +22,8 @@ const genEthAddr = (i: number) => { return EthereumAddress.fromString(ethAddr); }; -function setupTest() { - const tau = 1; - const sharesNum = 4; - const threshold = Math.floor((sharesNum * 2) / 3); - +const tau = 1; +function setupTest(sharesNum :number, threshold: number) { const validatorKeypairs: Keypair[] = []; const validators: Validator[] = []; for (let i = 0; i < sharesNum; i++) { @@ -63,9 +60,6 @@ function setupTest() { const ciphertext = ferveoEncrypt(msg, aad, dkg.publicKey()); return { - tau, - sharesNum, - threshold, validatorKeypairs, validators, dkg, @@ -79,17 +73,16 @@ function setupTest() { // This test suite replicates tests from ferveo-wasm/tests/node.rs describe("ferveo-wasm", () => { it("simple tdec variant", () => { + const sharesNum = 4; + const threshold = 3; const { - tau, - sharesNum, - threshold, validatorKeypairs, validators, messages, msg, aad, ciphertext, - } = setupTest(); + } = setupTest(sharesNum, threshold); // Having aggregated the transcripts, the validators can now create decryption shares const decryptionShares: DecryptionShareSimple[] = []; @@ -128,17 +121,16 @@ describe("ferveo-wasm", () => { }); it("precomputed tdec variant", () => { + const sharesNum = 4; + const threshold = sharesNum; // threshold is equal to sharesNum in precomputed variant const { - tau, - sharesNum, - threshold, validatorKeypairs, validators, messages, msg, aad, ciphertext, - } = setupTest(); + } = setupTest(sharesNum, threshold); // Having aggregated the transcripts, the validators can now create decryption shares const decryptionShares: DecryptionSharePrecomputed[] = []; diff --git a/ferveo-wasm/tests/node.rs b/ferveo-wasm/tests/node.rs index b4234d07..68e5f641 100644 --- a/ferveo-wasm/tests/node.rs +++ b/ferveo-wasm/tests/node.rs @@ -7,9 +7,6 @@ use itertools::zip_eq; use wasm_bindgen_test::*; type TestSetup = ( - u32, - usize, - usize, Vec, Vec, ValidatorArray, @@ -19,13 +16,12 @@ type TestSetup = ( Ciphertext, ); -fn setup_dkg() -> TestSetup { - let tau = 1; - let shares_num = 16; - let security_threshold = shares_num * 2 / 3; +const TAU: u32 = 0; - let validator_keypairs = - (0..shares_num).map(gen_keypair).collect::>(); +fn setup_dkg(shares_num: u32, security_threshold: u32) -> TestSetup { + let validator_keypairs = (0..shares_num as usize) + .map(gen_keypair) + .collect::>(); let validators = validator_keypairs .iter() .enumerate() @@ -37,9 +33,9 @@ fn setup_dkg() -> TestSetup { // validator, including themselves let messages = validators.iter().map(|sender| { let dkg = Dkg::new( - tau, - shares_num as u32, - security_threshold as u32, + TAU, + shares_num, + security_threshold, &validators_js, sender, ) @@ -53,9 +49,9 @@ fn setup_dkg() -> TestSetup { // every validator can aggregate the transcripts let mut dkg = Dkg::new( - tau, - shares_num as u32, - security_threshold as u32, + TAU, + shares_num, + security_threshold, &validators_js, &validators[0], ) @@ -79,9 +75,6 @@ fn setup_dkg() -> TestSetup { let ciphertext = ferveo_encrypt(&msg, &aad, &dkg.public_key()).unwrap(); ( - tau, - shares_num, - security_threshold, validator_keypairs, validators, validators_js, @@ -94,10 +87,9 @@ fn setup_dkg() -> TestSetup { #[wasm_bindgen_test] fn tdec_simple() { + let shares_num = 16; + let security_threshold = 10; let ( - tau, - shares_num, - security_threshold, validator_keypairs, validators, validators_js, @@ -105,15 +97,15 @@ fn tdec_simple() { msg, aad, ciphertext, - ) = setup_dkg(); + ) = setup_dkg(shares_num, security_threshold); // Having aggregated the transcripts, the validators can now create decryption shares let decryption_shares = zip_eq(validators, validator_keypairs) .map(|(validator, keypair)| { let mut dkg = Dkg::new( - tau, - shares_num as u32, - security_threshold as u32, + TAU, + shares_num, + security_threshold, &validators_js, &validator, ) @@ -148,10 +140,9 @@ fn tdec_simple() { #[wasm_bindgen_test] fn tdec_precomputed() { + let shares_num = 16; + let security_threshold = shares_num; // Must be equal to shares_num in precomputed variant let ( - tau, - shares_num, - security_threshold, validator_keypairs, validators, validators_js, @@ -159,15 +150,15 @@ fn tdec_precomputed() { msg, aad, ciphertext, - ) = setup_dkg(); + ) = setup_dkg(shares_num, security_threshold); // Having aggregated the transcripts, the validators can now create decryption shares let decryption_shares = zip_eq(validators, validator_keypairs) .map(|(validator, keypair)| { let mut dkg = Dkg::new( - tau, - shares_num as u32, - security_threshold as u32, + TAU, + 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/block_proposer.rs b/ferveo/benches/benchmarks/block_proposer.rs index 1f18a4ee..5043450f 100644 --- a/ferveo/benches/benchmarks/block_proposer.rs +++ b/ferveo/benches/benchmarks/block_proposer.rs @@ -1,5 +1,7 @@ #![allow(non_snake_case)] +// TODO: Currently not maintained - see mod.rs + use criterion::{black_box, criterion_group, criterion_main, Criterion}; use ark_bls12_381::*; diff --git a/ferveo/benches/benchmarks/mod.rs b/ferveo/benches/benchmarks/mod.rs index 7e19ab37..46d8d434 100644 --- a/ferveo/benches/benchmarks/mod.rs +++ b/ferveo/benches/benchmarks/mod.rs @@ -1,4 +1,5 @@ -//pub mod block_proposer; +// We disabled the following benchmarks because their outcomes were not relevant to us at the time. +// pub mod block_proposer; // pub mod pairing; pub mod eval_domain; pub mod validity_checks; 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..3c2295d1 100644 --- a/ferveo/src/api.rs +++ b/ferveo/src/api.rs @@ -9,7 +9,10 @@ pub use ferveo_tdec::api::{ prepare_combine_simple, share_combine_precomputed, share_combine_simple, Fr, G1Affine, G1Prepared, G2Affine, SecretBox, E, }; -use generic_array::{typenum::U48, GenericArray}; +use generic_array::{ + typenum::{Unsigned, U48}, + GenericArray, +}; use rand::RngCore; use serde::{Deserialize, Serialize}; use serde_with::serde_as; @@ -18,6 +21,7 @@ pub type PublicKey = ferveo_common::PublicKey; pub type Keypair = ferveo_common::Keypair; pub type Validator = crate::Validator; pub type Transcript = PubliclyVerifiableSS; + pub type ValidatorMessage = (Validator, Transcript); #[cfg(feature = "bindings-python")] @@ -164,7 +168,7 @@ impl DkgPublicKey { } pub fn serialized_size() -> usize { - 48 + U48::to_usize() } /// Generate a random DKG public key. @@ -203,11 +207,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, @@ -308,11 +309,19 @@ impl AggregatedTranscript { aad: &[u8], validator_keypair: &Keypair, ) -> Result { + if dkg.0.dkg_params.shares_num() + != dkg.0.dkg_params.security_threshold() + { + return Err(Error::InvalidDkgParametersForPrecomputedVariant( + dkg.0.dkg_params.shares_num(), + dkg.0.dkg_params.security_threshold(), + )); + } let domain_points: Vec<_> = dkg .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, @@ -397,8 +406,9 @@ mod test_ferveo_api { use ferveo_tdec::SecretBox; use itertools::izip; use rand::{prelude::StdRng, SeedableRng}; + use test_case::test_case; - use crate::{api::*, dkg::test_common::*}; + use crate::{api::*, test_common::*}; type TestInputs = (Vec, Vec, Vec); @@ -434,6 +444,7 @@ mod test_ferveo_api { (sender.clone(), dkg.generate_transcript(rng).unwrap()) }) .collect(); + (messages, validators, validator_keypairs) } @@ -442,212 +453,189 @@ mod test_ferveo_api { let dkg_pk = DkgPublicKey::random(); let serialized = dkg_pk.to_bytes().unwrap(); let deserialized = DkgPublicKey::from_bytes(&serialized).unwrap(); + assert_eq!(serialized.len(), 48_usize); assert_eq!(dkg_pk, deserialized); } - #[test] - fn test_server_api_tdec_precomputed() { + #[test_case(4; "number of shares (validators) is a power of 2")] + #[test_case(7; "number of shares (validators) is not a power of 2")] + fn test_server_api_tdec_precomputed(shares_num: u32) { 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; - // In precomputed variant, the security threshold is equal to the number of shares - // TODO: Refactor DKG constructor to not require security threshold or this case. - // Or figure out a different way to simplify the precomputed variant API. - let security_threshold = shares_num; - - 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 me = validators[0].clone(); - let mut dkg = - Dkg::new(tau, shares_num, security_threshold, &validators, &me) - .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 dkg_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, &dkg_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!(pvss_aggregated - .verify(shares_num, &messages) - .unwrap()); - - // And then each validator creates their own decryption share - aggregate - .create_decryption_share_precomputed( - &dkg, - &ciphertext.header().unwrap(), - aad, - validator_keypair, - ) - .unwrap() - }) - .collect(); - - // Now, the decryption share can be used to decrypt the ciphertext - // This part is part of the client API - - let shared_secret = share_combine_precomputed(&decryption_shares); - let plaintext = decrypt_with_shared_secret( - &ciphertext, - aad, - &SharedSecret(shared_secret), - ) - .unwrap(); - assert_eq!(plaintext, msg); - - // Since we're using a precomputed variant, we need all the shares to be able to decrypt - // So if we remove one share, we should not be able to decrypt - let decryption_shares = - decryption_shares[..shares_num as usize - 1].to_vec(); - - let shared_secret = share_combine_precomputed(&decryption_shares); - let result = decrypt_with_shared_secret( - &ciphertext, - aad, - &SharedSecret(shared_secret), - ); - assert!(result.is_err()); - } + // In precomputed variant, the security threshold is equal to the number of shares + let security_threshold = shares_num; + + 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 me = validators[0].clone(); + let mut dkg = + Dkg::new(TAU, shares_num, security_threshold, &validators, &me) + .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 dkg_public_key = dkg.public_key(); + + // In the meantime, the client creates a ciphertext and decryption request + let ciphertext = + encrypt(SecretBox::new(MSG.to_vec()), AAD, &dkg_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!(pvss_aggregated.verify(shares_num, &messages).unwrap()); + + // And then each validator creates their own decryption share + aggregate + .create_decryption_share_precomputed( + &dkg, + &ciphertext.header().unwrap(), + AAD, + validator_keypair, + ) + .unwrap() + }) + .collect(); + + // Now, the decryption share can be used to decrypt the ciphertext + // This part is part of the client API + + let shared_secret = share_combine_precomputed(&decryption_shares); + let plaintext = decrypt_with_shared_secret( + &ciphertext, + AAD, + &SharedSecret(shared_secret), + ) + .unwrap(); + assert_eq!(plaintext, MSG); + + // Since we're using a precomputed variant, we need all the shares to be able to decrypt + // So if we remove one share, we should not be able to decrypt + let decryption_shares = + decryption_shares[..shares_num as usize - 1].to_vec(); + + let shared_secret = share_combine_precomputed(&decryption_shares); + let result = decrypt_with_shared_secret( + &ciphertext, + AAD, + &SharedSecret(shared_secret), + ); + assert!(result.is_err()); } - #[test] - fn test_server_api_tdec_simple() { + #[test_case(4; "number of shares (validators) is a power of 2")] + #[test_case(7; "number of shares (validators) is not a power of 2")] + fn test_server_api_tdec_simple(shares_num: u32) { 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 / 2 + 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()); - } + let security_threshold = shares_num / 2 + 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 ciphertext = + encrypt(SecretBox::new(MSG.to_vec()), AAD, &public_key).unwrap(); + + // Having aggregated the transcripts, the validators can now create decryption shares + let decryption_shares: Vec<_> = izip!(&validators, &validator_keypairs) + .map(|(validator, validator_keypair)| { + // Each validator holds their own instance of DKG and creates their own aggregate + let mut dkg = Dkg::new( + TAU, + shares_num, + security_threshold, + &validators, + validator, + ) + .unwrap(); + let aggregate = dkg.aggregate_transcripts(&messages).unwrap(); + assert!(aggregate.verify(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); - let tau = 1; - let security_threshold = 3; - let shares_num = 4; - let (messages, validators, _) = - make_test_inputs(rng, tau, security_threshold, shares_num); + 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 me = validators[0].clone(); let mut dkg = - Dkg::new(tau, shares_num, security_threshold, &validators, &me) + Dkg::new(TAU, SHARES_NUM, SECURITY_THRESHOLD, &validators, &me) .unwrap(); 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()); } @@ -655,15 +643,11 @@ mod test_ferveo_api { fn client_side_local_verification() { let rng = &mut StdRng::seed_from_u64(0); - let tau = 1; - let security_threshold = 3; - let shares_num = 4; - let (messages, _, _) = - make_test_inputs(rng, tau, security_threshold, shares_num); + make_test_inputs(rng, TAU, SECURITY_THRESHOLD, SHARES_NUM); // We only need `security_threshold` transcripts to aggregate - let messages = &messages[..security_threshold as usize]; + let messages = &messages[..SECURITY_THRESHOLD as usize]; // Create an aggregated transcript on the client side let aggregated_transcript = AggregatedTranscript::new(messages); @@ -672,27 +656,27 @@ mod test_ferveo_api { // the aggregate from a side-channel or decide to persist it and verify it later // Now, the client can verify the aggregated transcript - let result = aggregated_transcript.verify(shares_num, messages); + let result = aggregated_transcript.verify(SHARES_NUM, messages); assert!(result.is_ok()); assert!(result.unwrap()); // Test negative cases // Not enough transcripts - let not_enough_messages = &messages[..2]; - assert!(not_enough_messages.len() < security_threshold as usize); + let not_enough_messages = &messages[..SECURITY_THRESHOLD as usize - 1]; + assert!(not_enough_messages.len() < SECURITY_THRESHOLD as usize); let insufficient_aggregate = AggregatedTranscript::new(not_enough_messages); - let result = insufficient_aggregate.verify(shares_num, messages); + let result = insufficient_aggregate.verify(SHARES_NUM, messages); assert!(result.is_err()); // Unexpected transcripts in the aggregate or transcripts from a different ritual // Using same DKG parameters, but different DKG instances and validators let (bad_messages, _, _) = - make_test_inputs(rng, tau, security_threshold, shares_num); + make_test_inputs(rng, TAU, SECURITY_THRESHOLD, SHARES_NUM); let mixed_messages = [&messages[..2], &bad_messages[..1]].concat(); let bad_aggregate = AggregatedTranscript::new(&mixed_messages); - let result = bad_aggregate.verify(shares_num, messages); + let result = bad_aggregate.verify(SHARES_NUM, messages); assert!(result.is_err()); } } diff --git a/ferveo/src/bindings_python.rs b/ferveo/src/bindings_python.rs index ed965f3e..f897c8f6 100644 --- a/ferveo/src/bindings_python.rs +++ b/ferveo/src/bindings_python.rs @@ -93,7 +93,22 @@ 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}" + )) + }, + Error::InvalidDkgParametersForPrecomputedVariant(num_shares, security_threshold) => { + InvalidDkgParameters::new_err(format!( + "num_shares: {num_shares}, security_threshold: {security_threshold}" + )) + }, }, _ => default(), } @@ -128,6 +143,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) @@ -219,17 +236,13 @@ macro_rules! generate_boxed_bytes_serialization { #[pyfunction] pub fn encrypt( - message: &[u8], + message: Vec, aad: &[u8], dkg_public_key: &DkgPublicKey, ) -> PyResult { - let ciphertext = api::encrypt( - // TODO: Avoid double-allocation here. `SecretBox` already allocates for its contents. - api::SecretBox::new(message.to_vec()), - aad, - &dkg_public_key.0, - ) - .map_err(FerveoPythonError::FerveoError)?; + let ciphertext = + api::encrypt(api::SecretBox::new(message), aad, &dkg_public_key.0) + .map_err(FerveoPythonError::FerveoError)?; Ok(Ciphertext(ciphertext)) } @@ -727,7 +740,7 @@ pub fn make_ferveo_py_module(py: Python<'_>, m: &PyModule) -> PyResult<()> { mod test_ferveo_python { use itertools::izip; - use crate::bindings_python::*; + use crate::{bindings_python::*, test_common::*}; type TestInputs = (Vec, Vec, Vec); @@ -773,21 +786,19 @@ mod test_ferveo_python { #[test] fn test_server_api_tdec_precomputed() { - let tau = 1; - let shares_num = 4; // In precomputed variant, the security threshold is equal to the number of shares - let security_threshold = shares_num; + let security_threshold = SHARES_NUM; let (messages, validators, validator_keypairs) = - make_test_inputs(tau, security_threshold, shares_num); + make_test_inputs(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 me = validators[0].clone(); let mut dkg = Dkg::new( - tau, - shares_num, + TAU, + SHARES_NUM, security_threshold, validators.clone(), &me, @@ -799,24 +810,22 @@ mod test_ferveo_python { let pvss_aggregated = dkg.aggregate_transcripts(messages.clone()).unwrap(); assert!(pvss_aggregated - .verify(shares_num, messages.clone()) + .verify(SHARES_NUM, messages.clone()) .unwrap()); // At this point, any given validator should be able to provide a DKG public key let dkg_public_key = dkg.public_key(); // In the meantime, the client creates a ciphertext and decryption request - let msg: &[u8] = "my-msg".as_bytes(); - let aad: &[u8] = "my-aad".as_bytes(); - let ciphertext = encrypt(msg, aad, &dkg_public_key).unwrap(); + let ciphertext = encrypt(MSG.to_vec(), AAD, &dkg_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, + TAU, + SHARES_NUM, security_threshold, validators.clone(), validator, @@ -825,13 +834,13 @@ mod test_ferveo_python { let aggregate = dkg.aggregate_transcripts(messages.clone()).unwrap(); assert!(pvss_aggregated - .verify(shares_num, messages.clone()) + .verify(SHARES_NUM, messages.clone()) .is_ok()); aggregate .create_decryption_share_precomputed( &dkg, &ciphertext.header().unwrap(), - aad, + AAD, validator_keypair, ) .unwrap() @@ -845,56 +854,50 @@ mod test_ferveo_python { combine_decryption_shares_precomputed(decryption_shares); let plaintext = - decrypt_with_shared_secret(&ciphertext, aad, &shared_secret) + decrypt_with_shared_secret(&ciphertext, AAD, &shared_secret) .unwrap(); - assert_eq!(plaintext, msg); + assert_eq!(plaintext, MSG); } #[test] fn test_server_api_tdec_simple() { - let tau = 1; - let shares_num = 4; - let security_threshold = 3; - let (messages, validators, validator_keypairs) = - make_test_inputs(tau, security_threshold, shares_num); + make_test_inputs(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 me = validators[0].clone(); let mut dkg = Dkg::new( - tau, - shares_num, - security_threshold, + TAU, + SHARES_NUM, + SECURITY_THRESHOLD, validators.clone(), &me, ) .unwrap(); // Lets say that we've only receives `security_threshold` transcripts - let messages = messages[..security_threshold as usize].to_vec(); + let messages = messages[..SECURITY_THRESHOLD as usize].to_vec(); let pvss_aggregated = dkg.aggregate_transcripts(messages.clone()).unwrap(); assert!(pvss_aggregated - .verify(shares_num, messages.clone()) + .verify(SHARES_NUM, messages.clone()) .unwrap()); // At this point, any given validator should be able to provide a DKG public key let dkg_public_key = dkg.public_key(); // In the meantime, the client creates a ciphertext and decryption request - let msg: &[u8] = "my-msg".as_bytes(); - let aad: &[u8] = "my-aad".as_bytes(); - let ciphertext = encrypt(msg, aad, &dkg_public_key).unwrap(); + let ciphertext = encrypt(MSG.to_vec(), AAD, &dkg_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, + TAU, + SHARES_NUM, + SECURITY_THRESHOLD, validators.clone(), validator, ) @@ -902,13 +905,13 @@ mod test_ferveo_python { let aggregate = dkg.aggregate_transcripts(messages.clone()).unwrap(); assert!(aggregate - .verify(shares_num, messages.clone()) + .verify(SHARES_NUM, messages.clone()) .unwrap()); aggregate .create_decryption_share_simple( &dkg, &ciphertext.header().unwrap(), - aad, + AAD, validator_keypair, ) .unwrap() @@ -921,8 +924,8 @@ mod test_ferveo_python { let shared_secret = combine_decryption_shares_simple(decryption_shares); let plaintext = - decrypt_with_shared_secret(&ciphertext, aad, &shared_secret) + decrypt_with_shared_secret(&ciphertext, AAD, &shared_secret) .unwrap(); - assert_eq!(plaintext, msg); + assert_eq!(plaintext, MSG); } } diff --git a/ferveo/src/bindings_wasm.rs b/ferveo/src/bindings_wasm.rs index e412b6e0..a1310277 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) } @@ -597,7 +595,6 @@ impl Keypair { } } -/// Factory functions for testing pub mod test_common { use crate::bindings_wasm::*; diff --git a/ferveo/src/dkg.rs b/ferveo/src/dkg.rs index 3f7fc09d..e13c4894 100644 --- a/ferveo/src/dkg.rs +++ b/ferveo/src/dkg.rs @@ -15,9 +15,50 @@ 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 + || shares_num == 0 + || security_threshold == 0 + { + 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)] @@ -123,6 +164,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 +193,7 @@ impl PubliclyVerifiableDkg { } } + // TODO: Make private, use `share` instead. Currently used only in bindings pub fn create_share( &self, rng: &mut R, @@ -248,6 +291,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 @@ -314,110 +361,14 @@ pub enum Message { Aggregate(Aggregation), } -/// Factory functions for testing -#[cfg(test)] -pub(crate) mod test_common { - use std::str::FromStr; - - pub use ark_bls12_381::Bls12_381 as E; - use ferveo_common::Keypair; - - pub use super::*; - - pub type G1 = ::G1Affine; - - pub fn gen_keypairs(n: u32) -> Vec> { - let rng = &mut ark_std::test_rng(); - (0..n).map(|_| Keypair::::new(rng)).collect() - } - - pub fn gen_address(i: usize) -> EthereumAddress { - EthereumAddress::from_str(&format!("0x{i:040}")).unwrap() - } - - pub fn gen_validators(keypairs: &[Keypair]) -> Vec> { - keypairs - .iter() - .enumerate() - .map(|(i, keypair)| Validator { - address: gen_address(i), - public_key: keypair.public_key(), - }) - .collect() - } - - pub type TestSetup = (PubliclyVerifiableDkg, Vec>); - - pub fn setup_dkg_for_n_validators( - security_threshold: u32, - shares_num: u32, - my_index: usize, - ) -> TestSetup { - let keypairs = gen_keypairs(shares_num); - 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, - }, - &me, - ) - .expect("Setup failed"); - (dkg, keypairs) - } - - /// 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) - } - - /// 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) - } - - pub fn setup_dealt_dkg_with_n_validators( - security_threshold: u32, - shares_num: u32, - ) -> TestSetup { - let rng = &mut ark_std::test_rng(); - - // Gather everyone's transcripts - let messages: Vec<_> = (0..shares_num) - .map(|my_index| { - let (mut dkg, _) = setup_dkg_for_n_validators( - security_threshold, - shares_num, - my_index as usize, - ); - let me = dkg.me.validator.clone(); - let message = dkg.share(rng).unwrap(); - (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) - } -} - /// Test initializing DKG #[cfg(test)] mod test_dkg_init { - use super::test_common::*; + use crate::{ + dkg::{PubliclyVerifiableDkg, Validator}, + test_common::*, + DkgParams, + }; /// Test that dkg fails to start if the `me` input /// is not in the validator set @@ -433,11 +384,7 @@ mod test_dkg_init { }; let err = PubliclyVerifiableDkg::::new( &gen_validators(&known_keypairs), - &DkgParams { - tau: 0, - security_threshold: shares_num / 2, - shares_num, - }, + &DkgParams::new(TAU, SECURITY_THRESHOLD, SHARES_NUM).unwrap(), &unknown_validator, ) .unwrap_err(); @@ -451,8 +398,7 @@ mod test_dkg_init { mod test_dealing { use ark_ec::AffineRepr; - use super::test_common::*; - use crate::DkgState::Dealt; + use crate::{test_common::*, DkgState, DkgState::Dealt, Validator}; /// Test that dealing correct PVSS transcripts /// pass verification an application and that @@ -461,18 +407,18 @@ mod test_dealing { fn test_pvss_dealing() { let rng = &mut ark_std::test_rng(); + // Create a test DKG instance + let (mut dkg, _) = setup_dkg(0); + // Gather everyone's transcripts let mut messages = vec![]; - for i in 0..4 { - let (mut dkg, _) = setup_dkg(i); + for i in 0..dkg.dkg_params.shares_num() { + let (mut dkg, _) = setup_dkg(i as usize); 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 @@ -669,7 +615,7 @@ mod test_dealing { mod test_aggregation { use ark_ec::AffineRepr; - use super::test_common::*; + use crate::{dkg::*, test_common::*, DkgState, Message}; /// Test that if the security threshold is /// met, we can create a final key @@ -751,3 +697,22 @@ mod test_aggregation { assert!(dkg.verify_message(&sender, &aggregate).is_err()); } } + +/// Test DKG parameters +#[cfg(test)] +mod test_dkg_params { + use crate::test_common::*; + + #[test] + fn test_shares_num_less_than_security_threshold() { + let dkg_params = super::DkgParams::new(TAU, SHARES_NUM + 1, SHARES_NUM); + assert!(dkg_params.is_err()); + } + + #[test] + fn test_valid_dkg_params() { + let dkg_params = + super::DkgParams::new(TAU, SECURITY_THRESHOLD, SHARES_NUM); + assert!(dkg_params.is_ok()); + } +} diff --git a/ferveo/src/lib.rs b/ferveo/src/lib.rs index 59a44024..c316c815 100644 --- a/ferveo/src/lib.rs +++ b/ferveo/src/lib.rs @@ -21,6 +21,9 @@ pub mod validator; mod utils; +#[cfg(test)] +mod test_common; + pub use dkg::*; pub use primitives::*; pub use pvss::*; @@ -96,11 +99,25 @@ pub enum Error { #[error(transparent)] ArkSerializeError(#[from] ark_serialize::SerializationError), + /// Invalid byte length #[error("Invalid byte length. Expected {0}, got {1}")] InvalidByteLength(usize, usize), + /// Invalid variant #[error("Invalid variant: {0}")] InvalidVariant(String), + + /// DKG parameters validaiton failed + #[error("Invalid DKG parameters: number of shares {0}, threshold {1}")] + InvalidDkgParameters(u32, u32), + + /// Failed to access a share for a given share index + #[error("Invalid share index: {0}")] + InvalidShareIndex(u32), + + /// Failed to produce a precomputed variant decryption share + #[error("Invalid DKG parameters for precomputed variant: number of shares {0}, threshold {1}")] + InvalidDkgParametersForPrecomputedVariant(u32, u32), } pub type Result = std::result::Result; @@ -121,7 +138,7 @@ mod test_dkg_full { use std::collections::HashMap; use ark_bls12_381::{Bls12_381 as E, Fr, G1Affine}; - use ark_ec::{pairing::Pairing, AffineRepr, CurveGroup}; + use ark_ec::{AffineRepr, CurveGroup}; use ark_ff::{UniformRand, Zero}; use ark_poly::EvaluationDomain; use ark_std::test_rng; @@ -131,11 +148,11 @@ mod test_dkg_full { SharedSecret, }; use itertools::izip; + use rand::seq::SliceRandom; + use test_case::test_case; use super::*; - use crate::dkg::test_common::*; - - type TargetField = ::TargetField; + use crate::test_common::*; fn make_shared_secret_simple_tdec( dkg: &PubliclyVerifiableDkg, @@ -189,120 +206,113 @@ mod test_dkg_full { (pvss_aggregated, decryption_shares, shared_secret) } - #[test] - fn test_dkg_simple_tdec() { + #[test_case(4; "number of shares (validators) is a power of 2")] + #[test_case(7; "number of shares (validators) is not a power of 2")] + fn test_dkg_simple_tdec(shares_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(threshold, shares_num); - let (_, _, shared_secret) = make_shared_secret_simple_tdec( - &dkg, - aad, - &ciphertext.header().unwrap(), - validator_keypairs.as_slice(), - ); + let public_key = dkg.public_key(); + let ciphertext = ferveo_tdec::encrypt::( + SecretBox::new(MSG.to_vec()), + AAD, + &public_key, + rng, + ) + .unwrap(); - let plaintext = ferveo_tdec::decrypt_with_shared_secret( - &ciphertext, - aad, - &shared_secret, - &dkg.pvss_params.g_inv(), - ) - .unwrap(); - assert_eq!(plaintext, msg); - } + 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); } - #[test] - fn test_dkg_simple_tdec_precomputed() { + #[test_case(4; "number of shares (validators) is a power of 2")] + #[test_case(7; "number of shares (validators) is not a power of 2")] + fn test_dkg_simple_tdec_precomputed(shares_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(threshold, shares_num); + let public_key = dkg.public_key(); + let ciphertext = ferveo_tdec::encrypt::( + SecretBox::new(MSG.to_vec()), + 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 shared_secret = - ferveo_tdec::share_combine_precomputed::(&decryption_shares); + let mut 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(); + decryption_shares.shuffle(rng); + assert_eq!(domain_points.len(), decryption_shares.len()); - // 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); - } + 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); } #[test] fn test_dkg_simple_tdec_share_verification() { let rng = &mut test_rng(); - let (dkg, validator_keypairs) = setup_dealt_dkg_with_n_validators(3, 4); - let msg = "my-msg".as_bytes().to_vec(); - let aad: &[u8] = "my-aad".as_bytes(); + let (dkg, validator_keypairs) = + setup_dealt_dkg_with(SECURITY_THRESHOLD, SHARES_NUM); let public_key = dkg.public_key(); let ciphertext = ferveo_tdec::encrypt::( - SecretBox::new(msg), - aad, + SecretBox::new(MSG.to_vec()), + AAD, &public_key, rng, ) @@ -311,7 +321,7 @@ mod test_dkg_full { let (pvss_aggregated, decryption_shares, _) = make_shared_secret_simple_tdec( &dkg, - aad, + AAD, &ciphertext.header().unwrap(), validator_keypairs.as_slice(), ); @@ -360,16 +370,12 @@ mod test_dkg_full { fn test_dkg_simple_tdec_share_recovery() { 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 msg = "my-msg".as_bytes().to_vec(); - let aad: &[u8] = "my-aad".as_bytes(); + setup_dealt_dkg_with(SECURITY_THRESHOLD, SHARES_NUM); let public_key = &dkg.public_key(); let ciphertext = ferveo_tdec::encrypt::( - SecretBox::new(msg), - aad, + SecretBox::new(MSG.to_vec()), + AAD, public_key, rng, ) @@ -378,7 +384,7 @@ mod test_dkg_full { // Create an initial shared secret let (_, _, old_shared_secret) = make_shared_secret_simple_tdec( &dkg, - aad, + AAD, &ciphertext.header().unwrap(), validator_keypairs.as_slice(), ); @@ -410,7 +416,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 +445,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(); @@ -471,7 +479,7 @@ mod test_dkg_full { pvss_aggregated .make_decryption_share_simple( &ciphertext.header().unwrap(), - aad, + AAD, &validator_keypair.decryption_key, share_index, &dkg.pvss_params.g_inv(), @@ -487,21 +495,21 @@ mod test_dkg_full { &new_validator_decryption_key, &new_private_key_share, &ciphertext.header().unwrap(), - aad, + AAD, &dkg.pvss_params.g_inv(), ) .unwrap(), ); domain_points.push(x_r); - assert_eq!(domain_points.len(), shares_num as usize); - assert_eq!(decryption_shares.len(), shares_num as usize); + assert_eq!(domain_points.len(), SHARES_NUM as usize); + assert_eq!(decryption_shares.len(), SHARES_NUM as usize); // Maybe parametrize this test with [1..] and [..threshold] let domain_points = &domain_points[1..]; let decryption_shares = &decryption_shares[1..]; - assert_eq!(domain_points.len(), security_threshold as usize); - assert_eq!(decryption_shares.len(), security_threshold as usize); + assert_eq!(domain_points.len(), SECURITY_THRESHOLD as usize); + assert_eq!(decryption_shares.len(), SECURITY_THRESHOLD as usize); let lagrange = ferveo_tdec::prepare_combine_simple::(domain_points); let new_shared_secret = ferveo_tdec::share_combine_simple::( @@ -519,16 +527,12 @@ mod test_dkg_full { fn test_dkg_simple_tdec_share_refreshing() { 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 msg = "my-msg".as_bytes().to_vec(); - let aad: &[u8] = "my-aad".as_bytes(); + setup_dealt_dkg_with(SECURITY_THRESHOLD, SHARES_NUM); let public_key = &dkg.public_key(); let ciphertext = ferveo_tdec::encrypt::( - SecretBox::new(msg), - aad, + SecretBox::new(MSG.to_vec()), + AAD, public_key, rng, ) @@ -537,7 +541,7 @@ mod test_dkg_full { // Create an initial shared secret let (_, _, old_shared_secret) = make_shared_secret_simple_tdec( &dkg, - aad, + AAD, &ciphertext.header().unwrap(), validator_keypairs.as_slice(), ); @@ -552,7 +556,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 +586,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(); @@ -600,7 +606,7 @@ mod test_dkg_full { &validator_keypair.decryption_key, updated_shares.get(share_index).unwrap(), &ciphertext.header().unwrap(), - aad, + AAD, &dkg.pvss_params.g_inv(), ) .unwrap() @@ -608,10 +614,10 @@ mod test_dkg_full { .collect(); let lagrange = ferveo_tdec::prepare_combine_simple::( - &domain_points[..security_threshold as usize], + &domain_points[..SECURITY_THRESHOLD as usize], ); let new_shared_secret = ferveo_tdec::share_combine_simple::( - &decryption_shares[..security_threshold as usize], + &decryption_shares[..SECURITY_THRESHOLD as usize], &lagrange, ); diff --git a/ferveo/src/pvss.rs b/ferveo/src/pvss.rs index 4f63da82..c8498bb7 100644 --- a/ferveo/src/pvss.rs +++ b/ferveo/src/pvss.rs @@ -40,9 +40,6 @@ pub trait Aggregate {} /// Apply trait gate to Aggregated marker struct impl Aggregate for Aggregated {} -// /// Type alias for non aggregated PVSS transcripts -// pub type Pvss = PubliclyVerifiableSS; - /// Type alias for aggregated PVSS transcripts pub type AggregatedPvss = PubliclyVerifiableSS; @@ -138,7 +135,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, ); @@ -311,19 +308,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() + .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 +332,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 +353,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 +376,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, + )) } } @@ -462,11 +462,7 @@ mod test_pvss { use rand::seq::SliceRandom; use super::*; - use crate::{dkg::test_common::*, utils::is_sorted}; - - type ScalarField = ::ScalarField; - type G1 = ::G1Affine; - type G2 = ::G2Affine; + use crate::{test_common::*, utils::is_sorted, DkgParams}; /// Test the happy flow that a pvss with the correct form is created /// and that appropriate validations pass @@ -482,7 +478,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()); @@ -555,11 +551,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, security_threshold, shares_num).unwrap(), &me, ); assert!(result.is_err()); @@ -578,7 +570,7 @@ mod test_pvss { // 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()); diff --git a/ferveo/src/refresh.rs b/ferveo/src/refresh.rs index c9e692d4..524e6569 100644 --- a/ferveo/src/refresh.rs +++ b/ferveo/src/refresh.rs @@ -122,22 +122,18 @@ mod tests_refresh { use std::collections::HashMap; use ark_bls12_381::Fr; - use ark_ec::pairing::Pairing; use ark_std::{test_rng, UniformRand, Zero}; - use rand_core::RngCore; - - type E = ark_bls12_381::Bls12_381; - type ScalarField = ::ScalarField; - use ferveo_tdec::{ test_common::setup_simple, PrivateDecryptionContextSimple, PrivateKeyShare, }; + use rand_core::RngCore; + use test_case::test_matrix; use crate::{ apply_updates_to_private_share, prepare_share_updates_for_recovery, prepare_share_updates_for_refresh, - recover_share_from_updated_private_shares, + recover_share_from_updated_private_shares, test_common::*, }; fn make_new_share_fragments_for_recovery( @@ -191,14 +187,13 @@ mod tests_refresh { /// Ñ parties (where t <= Ñ <= N) jointly execute a "share recovery" algorithm, and the output is 1 new share. /// The new share is intended to restore a previously existing share, e.g., due to loss or corruption. - #[test] - fn tdec_simple_variant_share_recovery_at_selected_point() { + #[test_matrix([4, 7, 11, 16])] + fn tdec_simple_variant_share_recovery_at_selected_point(shares_num: usize) { let rng = &mut test_rng(); - let shares_num = 16; - let threshold = shares_num * 2 / 3; + let security_threshold = shares_num * 2 / 3; let (_, _, mut contexts) = - setup_simple::(threshold, shares_num, rng); + setup_simple::(security_threshold, shares_num, rng); // Prepare participants @@ -220,7 +215,7 @@ mod tests_refresh { // Each participant prepares an update for each other participant, and uses it to create a new share fragment let new_share_fragments = make_new_share_fragments_for_recovery( rng, - threshold, + security_threshold, &x_r, &remaining_participants, ); @@ -233,8 +228,8 @@ mod tests_refresh { .collect::>(); let new_private_key_share = recover_share_from_updated_private_shares( &x_r, - &domain_points[..threshold], - &new_share_fragments[..threshold], + &domain_points[..security_threshold], + &new_share_fragments[..security_threshold], ); assert_eq!(new_private_key_share, original_private_key_share); @@ -244,8 +239,8 @@ mod tests_refresh { let incorrect_private_key_share = recover_share_from_updated_private_shares( &x_r, - &domain_points[..(threshold - 1)], - &new_share_fragments[..(threshold - 1)], + &domain_points[..(security_threshold - 1)], + &new_share_fragments[..(security_threshold - 1)], ); assert_ne!(incorrect_private_key_share, original_private_key_share); @@ -253,10 +248,9 @@ mod tests_refresh { /// Ñ parties (where t <= Ñ <= N) jointly execute a "share recovery" algorithm, and the output is 1 new share. /// The new share is independent from the previously existing shares. We can use this to on-board a new participant into an existing cohort. - #[test] - fn tdec_simple_variant_share_recovery_at_random_point() { + #[test_matrix([4, 7, 11, 16])] + fn tdec_simple_variant_share_recovery_at_random_point(shares_num: usize) { let rng = &mut test_rng(); - let shares_num = 16; let threshold = shares_num * 2 / 3; let (_, shared_private_key, mut contexts) = @@ -321,10 +315,10 @@ mod tests_refresh { /// Ñ parties (where t <= Ñ <= N) jointly execute a "share refresh" algorithm. /// The output is M new shares (with M <= Ñ), with each of the M new shares substituting the /// original share (i.e., the original share is deleted). - #[test] - fn tdec_simple_variant_share_refreshing() { + #[test_matrix([4, 7, 11, 16])] + + fn tdec_simple_variant_share_refreshing(shares_num: usize) { let rng = &mut test_rng(); - let shares_num = 16; let threshold = shares_num * 2 / 3; let (_, shared_private_key, contexts) = diff --git a/ferveo/src/test_common.rs b/ferveo/src/test_common.rs new file mode 100644 index 00000000..22d072a2 --- /dev/null +++ b/ferveo/src/test_common.rs @@ -0,0 +1,110 @@ +/// Factory functions and variables for testing +use std::str::FromStr; + +pub use ark_bls12_381::Bls12_381 as E; +use ark_ec::pairing::Pairing; +use ferveo_common::Keypair; +use rand::seq::SliceRandom; + +use crate::{DkgParams, EthereumAddress, PubliclyVerifiableDkg, Validator}; + +pub type ScalarField = ::ScalarField; +pub type G1 = ::G1Affine; +pub type G2 = ::G2Affine; +pub type TargetField = ::TargetField; + +pub const TAU: u32 = 0; +pub const MSG: &[u8] = b"my-msg"; +pub const AAD: &[u8] = b"my-aad"; +pub const SECURITY_THRESHOLD: u32 = 3; +pub const SHARES_NUM: u32 = 4; + +pub fn gen_keypairs(n: u32) -> Vec> { + let rng = &mut ark_std::test_rng(); + (0..n).map(|_| Keypair::::new(rng)).collect() +} + +pub fn gen_address(i: usize) -> EthereumAddress { + EthereumAddress::from_str(&format!("0x{i:040}")).unwrap() +} + +pub fn gen_validators(keypairs: &[Keypair]) -> Vec> { + keypairs + .iter() + .enumerate() + .map(|(i, keypair)| Validator { + address: gen_address(i), + public_key: keypair.public_key(), + }) + .collect() +} + +pub type TestSetup = (PubliclyVerifiableDkg, Vec>); + +pub fn setup_dkg_for_n_validators( + security_threshold: u32, + shares_num: u32, + my_validator_index: usize, +) -> TestSetup { + let keypairs = gen_keypairs(shares_num); + let mut validators = gen_validators(keypairs.as_slice()); + validators.sort(); + let me = validators[my_validator_index].clone(); + let dkg = PubliclyVerifiableDkg::new( + &validators, + &DkgParams::new(TAU, security_threshold, shares_num).unwrap(), + &me, + ) + .expect("Setup failed"); + (dkg, keypairs) +} + +/// Create a test dkg +/// +/// The [`crate::dkg::test_dkg_init`] module checks correctness of this setup +pub fn setup_dkg(my_validator_index: usize) -> TestSetup { + setup_dkg_for_n_validators( + SECURITY_THRESHOLD, + SHARES_NUM, + my_validator_index, + ) +} + +/// Set up a dkg with enough pvss transcripts to meet the threshold +/// +/// The correctness of this function is tested in the module [`crate::dkg::test_dealing`] +pub fn setup_dealt_dkg() -> TestSetup { + setup_dealt_dkg_with(SECURITY_THRESHOLD, SHARES_NUM) +} + +pub fn setup_dealt_dkg_with( + security_threshold: u32, + shares_num: u32, +) -> TestSetup { + let rng = &mut ark_std::test_rng(); + + // Gather everyone's transcripts + let mut messages: Vec<_> = (0..shares_num) + .map(|my_index| { + let (mut dkg, _) = setup_dkg_for_n_validators( + security_threshold, + shares_num, + my_index as usize, + ); + let me = dkg.me.validator.clone(); + let message = dkg.share(rng).unwrap(); + (me, message) + }) + .collect(); + + // Create a test DKG instance + let (mut dkg, keypairs) = + setup_dkg_for_n_validators(security_threshold, shares_num, 0); + + // The ordering of messages should not matter + messages.shuffle(rng); + messages.iter().for_each(|(sender, message)| { + dkg.apply_message(sender, message).expect("Setup failed"); + }); + (dkg, keypairs) +} diff --git a/ferveo/src/validator.rs b/ferveo/src/validator.rs index 7b014266..d931ca06 100644 --- a/ferveo/src/validator.rs +++ b/ferveo/src/validator.rs @@ -54,6 +54,7 @@ impl PartialOrd for Validator { } impl Ord for Validator { + // Validators are ordered by their address only fn cmp(&self, other: &Self) -> Ordering { self.address.cmp(&other.address) }