From f98a90de0f8a5c247cd8eb22e38db2653f6dfb03 Mon Sep 17 00:00:00 2001 From: Piotr Roslaniec Date: Wed, 31 Jan 2024 13:08:56 +0100 Subject: [PATCH] test: update tests for dkgs with relaxed constraints --- .../examples/server_api_precomputed.py | 9 +- ferveo-python/examples/server_api_simple.py | 9 +- ferveo-python/test/test_ferveo.py | 67 ++++-- ferveo-wasm/examples/node/src/main.test.ts | 169 +++++++------- ferveo-wasm/tests/node.rs | 217 ++++++++++-------- ferveo/src/api.rs | 156 +++++++++---- ferveo/src/bindings_python.rs | 13 +- ferveo/src/bindings_wasm.rs | 8 +- ferveo/src/lib.rs | 10 +- 9 files changed, 380 insertions(+), 278 deletions(-) diff --git a/ferveo-python/examples/server_api_precomputed.py b/ferveo-python/examples/server_api_precomputed.py index 72263405..983f600a 100644 --- a/ferveo-python/examples/server_api_precomputed.py +++ b/ferveo-python/examples/server_api_precomputed.py @@ -16,10 +16,11 @@ def gen_eth_addr(i: int) -> str: tau = 1 shares_num = 4 +validators_num = shares_num + 2 # In precomputed variant, security threshold must be equal to shares_num security_threshold = shares_num -validator_keypairs = [Keypair.random() for _ in range(0, shares_num)] +validator_keypairs = [Keypair.random() for _ in range(0, validators_num)] validators = [ Validator(gen_eth_addr(i), keypair.public_key(), i) for i, keypair in enumerate(validator_keypairs) @@ -52,11 +53,11 @@ def gen_eth_addr(i: int) -> str: # Server can aggregate the transcripts server_aggregate = dkg.aggregate_transcripts(messages) -assert server_aggregate.verify(shares_num, messages) +assert server_aggregate.verify(validators_num, messages) # And the client can also aggregate and verify the transcripts client_aggregate = AggregatedTranscript(messages) -assert client_aggregate.verify(shares_num, messages) +assert client_aggregate.verify(validators_num, messages) # In the meantime, the client creates a ciphertext and decryption request msg = "abc".encode() @@ -76,7 +77,7 @@ def gen_eth_addr(i: int) -> str: # We can also obtain the aggregated transcript from the side-channel (deserialize) aggregate = AggregatedTranscript(messages) - assert aggregate.verify(shares_num, messages) + assert aggregate.verify(validators_num, messages) # The ciphertext is obtained from the client diff --git a/ferveo-python/examples/server_api_simple.py b/ferveo-python/examples/server_api_simple.py index 5fd2c8e5..44fb69c4 100644 --- a/ferveo-python/examples/server_api_simple.py +++ b/ferveo-python/examples/server_api_simple.py @@ -17,7 +17,8 @@ def gen_eth_addr(i: int) -> str: tau = 1 security_threshold = 3 shares_num = 4 -validator_keypairs = [Keypair.random() for _ in range(0, shares_num)] +validators_num = shares_num + 2 +validator_keypairs = [Keypair.random() for _ in range(0, validators_num)] validators = [ Validator(gen_eth_addr(i), keypair.public_key(), i) for i, keypair in enumerate(validator_keypairs) @@ -52,11 +53,11 @@ def gen_eth_addr(i: int) -> str: # Server can aggregate the transcripts server_aggregate = dkg.aggregate_transcripts(messages) -assert server_aggregate.verify(shares_num, messages) +assert server_aggregate.verify(validators_num, messages) # And the client can also aggregate and verify the transcripts client_aggregate = AggregatedTranscript(messages) -assert client_aggregate.verify(shares_num, messages) +assert client_aggregate.verify(validators_num, messages) # In the meantime, the client creates a ciphertext and decryption request msg = "abc".encode() @@ -79,7 +80,7 @@ def gen_eth_addr(i: int) -> str: # We can also obtain the aggregated transcript from the side-channel (deserialize) aggregate = AggregatedTranscript(messages) - assert aggregate.verify(shares_num, messages) + assert aggregate.verify(validators_num, messages) # The ciphertext is obtained from the client diff --git a/ferveo-python/test/test_ferveo.py b/ferveo-python/test/test_ferveo.py index b3496d3e..9f6b1f9a 100644 --- a/ferveo-python/test/test_ferveo.py +++ b/ferveo-python/test/test_ferveo.py @@ -37,12 +37,18 @@ def combine_shares_for_variant(v: FerveoVariant, decryption_shares): raise ValueError("Unknown variant") -def scenario_for_variant(variant: FerveoVariant, shares_num, threshold, shares_to_use): +def scenario_for_variant(variant: FerveoVariant, shares_num, validators_num, threshold, shares_to_use): if variant not in [FerveoVariant.Simple, FerveoVariant.Precomputed]: raise ValueError("Unknown variant: " + variant) + if validators_num < shares_num: + raise ValueError("validators_num must be >= shares_num") + + if shares_to_use < threshold: + raise ValueError("shares_to_use must be >= threshold") + tau = 1 - validator_keypairs = [Keypair.random() for _ in range(0, shares_num)] + validator_keypairs = [Keypair.random() for _ in range(0, validators_num)] validators = [ Validator(gen_eth_addr(i), keypair.public_key(), i) for i, keypair in enumerate(validator_keypairs) @@ -68,7 +74,7 @@ def scenario_for_variant(variant: FerveoVariant, shares_num, threshold, shares_t me=validators[0], ) pvss_aggregated = dkg.aggregate_transcripts(messages) - assert pvss_aggregated.verify(shares_num, messages) + assert pvss_aggregated.verify(validators_num, messages) dkg_pk_bytes = bytes(dkg.public_key) dkg_pk = DkgPublicKey.from_bytes(dkg_pk_bytes) @@ -87,7 +93,7 @@ def scenario_for_variant(variant: FerveoVariant, shares_num, threshold, shares_t me=validator, ) pvss_aggregated = dkg.aggregate_transcripts(messages) - assert pvss_aggregated.verify(shares_num, messages) + assert pvss_aggregated.verify(validators_num, messages) decryption_share = decryption_share_for_variant(variant, pvss_aggregated)( dkg, ciphertext.header, aad, validator_keypair @@ -113,27 +119,54 @@ def scenario_for_variant(variant: FerveoVariant, shares_num, threshold, shares_t def test_simple_tdec_has_enough_messages(): - scenario_for_variant( - FerveoVariant.Simple, shares_num=4, threshold=3, shares_to_use=3 - ) + shares_num = 4 + threshold = 3 + for validators_num in [shares_num, 6]: + scenario_for_variant( + FerveoVariant.Simple, + shares_num=shares_num, + validators_num=validators_num, + threshold=threshold, + shares_to_use=threshold + ) def test_simple_tdec_doesnt_have_enough_messages(): - scenario_for_variant( - FerveoVariant.Simple, shares_num=4, threshold=3, shares_to_use=2 - ) + shares_num = 4 + threshold = 3 + for validators_num in [shares_num, 6]: + scenario_for_variant( + FerveoVariant.Simple, + shares_num=shares_num, + validators_num=validators_num, + threshold=threshold, + shares_to_use=threshold-1 + ) def test_precomputed_tdec_has_enough_messages(): - scenario_for_variant( - FerveoVariant.Precomputed, shares_num=4, threshold=4, shares_to_use=4 - ) - + shares_num = 4 + threshold = 3 + for validators_num in [shares_num, 6]: + scenario_for_variant( + FerveoVariant.Precomputed, + shares_num=shares_num, + validators_num=validators_num, + threshold=threshold, + shares_to_use=threshold + ) def test_precomputed_tdec_doesnt_have_enough_messages(): - scenario_for_variant( - FerveoVariant.Precomputed, shares_num=4, threshold=4, shares_to_use=3 - ) + shares_num = 4 + threshold = 3 + for validators_num in [shares_num, 6]: + scenario_for_variant( + FerveoVariant.Simple, + shares_num=shares_num, + validators_num=validators_num, + threshold=threshold, + shares_to_use=threshold + ) PARAMS = [ diff --git a/ferveo-wasm/examples/node/src/main.test.ts b/ferveo-wasm/examples/node/src/main.test.ts index 5814a53d..7f390613 100644 --- a/ferveo-wasm/examples/node/src/main.test.ts +++ b/ferveo-wasm/examples/node/src/main.test.ts @@ -22,11 +22,16 @@ const genEthAddr = (i: number) => { return EthereumAddress.fromString(ethAddr); }; -const tau = 1; -function setupTest(sharesNum :number, threshold: number) { +const TAU = 1; + +function setupTest( + sharesNum: number, + validatorsNum: number, + threshold: number +) { const validatorKeypairs: Keypair[] = []; const validators: Validator[] = []; - for (let i = 0; i < sharesNum; i++) { + for (let i = 0; i < validatorsNum; i++) { const keypair = Keypair.random(); validatorKeypairs.push(keypair); const validator = new Validator(genEthAddr(i), keypair.publicKey, i); @@ -37,7 +42,7 @@ function setupTest(sharesNum :number, threshold: number) { // validator, including themselves const messages: ValidatorMessage[] = []; validators.forEach((sender) => { - const dkg = new Dkg(tau, sharesNum, threshold, validators, sender); + const dkg = new Dkg(TAU, sharesNum, threshold, validators, sender); const transcript = dkg.generateTranscript(); const message = new ValidatorMessage(sender, transcript); messages.push(message); @@ -45,14 +50,14 @@ function setupTest(sharesNum :number, threshold: number) { // Now that every validator holds a dkg instance and a transcript for every other validator, // every validator can aggregate the transcripts - const dkg = new Dkg(tau, sharesNum, threshold, validators, validators[0]); + const dkg = new Dkg(TAU, sharesNum, threshold, validators, validators[0]); const serverAggregate = dkg.aggregateTranscript(messages); - expect(serverAggregate.verify(sharesNum, messages)).toBe(true); + expect(serverAggregate.verify(validatorsNum, messages)).toBe(true); // Client can also aggregate the transcripts and verify them const clientAggregate = new AggregatedTranscript(messages); - expect(clientAggregate.verify(sharesNum, messages)).toBe(true); + expect(clientAggregate.verify(validatorsNum, messages)).toBe(true); // In the meantime, the client creates a ciphertext and decryption request const msg = Buffer.from("my-msg"); @@ -73,94 +78,76 @@ function setupTest(sharesNum :number, threshold: number) { // 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 { - validatorKeypairs, - validators, - messages, - msg, - aad, - ciphertext, - } = setupTest(sharesNum, threshold); - - // Having aggregated the transcripts, the validators can now create decryption shares - const decryptionShares: DecryptionShareSimple[] = []; - zip(validators, validatorKeypairs).forEach(([validator, keypair]) => { - expect(validator.publicKey.equals(keypair.publicKey)).toBe(true); - - const dkg = new Dkg(tau, sharesNum, threshold, validators, validator); - const aggregate = dkg.aggregateTranscript(messages); - const isValid = aggregate.verify(sharesNum, messages); - expect(isValid).toBe(true); - - const decryptionShare = aggregate.createDecryptionShareSimple( - dkg, - ciphertext.header, - aad, - keypair - ); - decryptionShares.push(decryptionShare); + const sharesNum = 4; + const threshold = 3; + [sharesNum, sharesNum + 2].forEach((validatorsNum) => { + const { validatorKeypairs, validators, messages, msg, aad, ciphertext } = + setupTest(sharesNum, validatorsNum, threshold); + + // Having aggregated the transcripts, the validators can now create decryption shares + const decryptionShares: DecryptionShareSimple[] = []; + zip(validators, validatorKeypairs).forEach(([validator, keypair]) => { + expect(validator.publicKey.equals(keypair.publicKey)).toBe(true); + + const dkg = new Dkg(TAU, sharesNum, threshold, validators, validator); + const aggregate = dkg.aggregateTranscript(messages); + const isValid = aggregate.verify(validatorsNum, messages); + expect(isValid).toBe(true); + + const decryptionShare = aggregate.createDecryptionShareSimple( + dkg, + ciphertext.header, + aad, + keypair + ); + decryptionShares.push(decryptionShare); + }); + + // Now, the decryption share can be used to decrypt the ciphertext + // This part is in the client API + + const sharedSecret = combineDecryptionSharesSimple(decryptionShares); + + // The client should have access to the public parameters of the DKG + + const plaintext = decryptWithSharedSecret(ciphertext, aad, sharedSecret); + expect(Buffer.from(plaintext)).toEqual(msg); }); - - // Now, the decryption share can be used to decrypt the ciphertext - // This part is in the client API - - const sharedSecret = combineDecryptionSharesSimple( - decryptionShares, - ); - - // The client should have access to the public parameters of the DKG - - const plaintext = decryptWithSharedSecret( - ciphertext, - aad, - sharedSecret, - ); - expect(Buffer.from(plaintext)).toEqual(msg); }); it("precomputed tdec variant", () => { - const sharesNum = 4; - const threshold = sharesNum; // threshold is equal to sharesNum in precomputed variant - const { - validatorKeypairs, - validators, - messages, - msg, - aad, - ciphertext, - } = setupTest(sharesNum, threshold); - - // Having aggregated the transcripts, the validators can now create decryption shares - const decryptionShares: DecryptionSharePrecomputed[] = []; - zip(validators, validatorKeypairs).forEach(([validator, keypair]) => { - const dkg = new Dkg(tau, sharesNum, threshold, validators, validator); - const aggregate = dkg.aggregateTranscript(messages); - const isValid = aggregate.verify(sharesNum, messages); - expect(isValid).toBe(true); - - const decryptionShare = aggregate.createDecryptionSharePrecomputed( - dkg, - ciphertext.header, - aad, - keypair - ); - decryptionShares.push(decryptionShare); + const sharesNum = 4; + const threshold = sharesNum; // threshold is equal to sharesNum in precomputed variant + [sharesNum, sharesNum + 2].forEach((validatorsNum) => { + const { validatorKeypairs, validators, messages, msg, aad, ciphertext } = + setupTest(sharesNum, validatorsNum, threshold); + + // Having aggregated the transcripts, the validators can now create decryption shares + const decryptionShares: DecryptionSharePrecomputed[] = []; + zip(validators, validatorKeypairs).forEach(([validator, keypair]) => { + const dkg = new Dkg(TAU, sharesNum, threshold, validators, validator); + const aggregate = dkg.aggregateTranscript(messages); + const isValid = aggregate.verify(validatorsNum, messages); + expect(isValid).toBe(true); + + const decryptionShare = aggregate.createDecryptionSharePrecomputed( + dkg, + ciphertext.header, + aad, + keypair + ); + decryptionShares.push(decryptionShare); + }); + + // Now, the decryption share can be used to decrypt the ciphertext + // This part is in the client API + + const sharedSecret = combineDecryptionSharesPrecomputed(decryptionShares); + + // The client should have access to the public parameters of the DKG + + const plaintext = decryptWithSharedSecret(ciphertext, aad, sharedSecret); + expect(Buffer.from(plaintext)).toEqual(msg); }); - - // Now, the decryption share can be used to decrypt the ciphertext - // This part is in the client API - - const sharedSecret = combineDecryptionSharesPrecomputed(decryptionShares); - - // The client should have access to the public parameters of the DKG - - const plaintext = decryptWithSharedSecret( - ciphertext, - aad, - sharedSecret, - ); - expect(Buffer.from(plaintext)).toEqual(msg); }); }); diff --git a/ferveo-wasm/tests/node.rs b/ferveo-wasm/tests/node.rs index bae5750b..7b35efa5 100644 --- a/ferveo-wasm/tests/node.rs +++ b/ferveo-wasm/tests/node.rs @@ -18,8 +18,12 @@ type TestSetup = ( const TAU: u32 = 0; -fn setup_dkg(shares_num: u32, security_threshold: u32) -> TestSetup { - let validator_keypairs = (0..shares_num as usize) +fn setup_dkg( + shares_num: u32, + validators_num: u32, + security_threshold: u32, +) -> TestSetup { + let validator_keypairs = (0..validators_num as usize) .map(gen_keypair) .collect::>(); let validators = validator_keypairs @@ -32,7 +36,7 @@ fn setup_dkg(shares_num: u32, security_threshold: u32) -> TestSetup { // Each validator holds their own DKG instance and generates a transcript every // validator, including themselves let messages = validators.iter().map(|sender| { - let mut dkg = Dkg::new( + let mut validator_dkg = Dkg::new( TAU, shares_num, security_threshold, @@ -40,7 +44,7 @@ fn setup_dkg(shares_num: u32, security_threshold: u32) -> TestSetup { sender, ) .unwrap(); - let transcript = dkg.generate_transcript().unwrap(); + let transcript = validator_dkg.generate_transcript().unwrap(); ValidatorMessage::new(sender, &transcript).unwrap() }); @@ -61,12 +65,16 @@ fn setup_dkg(shares_num: u32, security_threshold: u32) -> TestSetup { // Server can aggregate the transcripts and verify them let server_aggregate = dkg.aggregate_transcripts(&messages_js).unwrap(); - let is_valid = server_aggregate.verify(shares_num, &messages_js).unwrap(); + let is_valid = server_aggregate + .verify(validators_num, &messages_js) + .unwrap(); assert!(is_valid); // Client can also aggregate the transcripts and verify them let client_aggregate = AggregatedTranscript::new(&messages_js).unwrap(); - let is_valid = client_aggregate.verify(shares_num, &messages_js).unwrap(); + let is_valid = client_aggregate + .verify(validators_num, &messages_js) + .unwrap(); assert!(is_valid); // In the meantime, the client creates a ciphertext and decryption request @@ -88,105 +96,116 @@ fn setup_dkg(shares_num: u32, security_threshold: u32) -> TestSetup { #[wasm_bindgen_test] fn tdec_simple() { let shares_num = 16; - let security_threshold = 10; - let ( - validator_keypairs, - validators, - validators_js, - messages_js, - msg, - aad, - ciphertext, - ) = 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, - security_threshold, - &validators_js, - &validator, - ) - .unwrap(); - let aggregate = dkg.aggregate_transcripts(&messages_js).unwrap(); - let is_valid = aggregate.verify(shares_num, &messages_js).unwrap(); - assert!(is_valid); - - aggregate - .create_decryption_share_simple( - &dkg, - &ciphertext.header().unwrap(), - &aad, - &keypair, + let security_threshold = shares_num / 2; + for validators_num in [shares_num, shares_num + 2] { + let ( + validator_keypairs, + validators, + validators_js, + messages_js, + msg, + aad, + ciphertext, + ) = setup_dkg(shares_num, validators_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, + security_threshold, + &validators_js, + &validator, ) - .unwrap() - }) - .collect::>(); - let decryption_shares_js = into_js_array(decryption_shares); - - // Now, the decryption share can be used to decrypt the ciphertext - // This part is in the client API - - let shared_secret = - combine_decryption_shares_simple(&decryption_shares_js).unwrap(); - - // The client should have access to the public parameters of the DKG - let plaintext = - decrypt_with_shared_secret(&ciphertext, &aad, &shared_secret).unwrap(); - assert_eq!(msg, plaintext); + .unwrap(); + let aggregate = + dkg.aggregate_transcripts(&messages_js).unwrap(); + let is_valid = + aggregate.verify(validators_num, &messages_js).unwrap(); + assert!(is_valid); + + aggregate + .create_decryption_share_simple( + &dkg, + &ciphertext.header().unwrap(), + &aad, + &keypair, + ) + .unwrap() + }) + .collect::>(); + let decryption_shares_js = into_js_array(decryption_shares); + + // Now, the decryption share can be used to decrypt the ciphertext + // This part is in the client API + + let shared_secret = + combine_decryption_shares_simple(&decryption_shares_js).unwrap(); + + // The client should have access to the public parameters of the DKG + let plaintext = + decrypt_with_shared_secret(&ciphertext, &aad, &shared_secret) + .unwrap(); + assert_eq!(msg, plaintext); + } } #[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 ( - validator_keypairs, - validators, - validators_js, - messages_js, - msg, - aad, - ciphertext, - ) = 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, - security_threshold, - &validators_js, - &validator, - ) - .unwrap(); - let aggregate = dkg.aggregate_transcripts(&messages_js).unwrap(); - let is_valid = aggregate.verify(shares_num, &messages_js).unwrap(); - assert!(is_valid); - - aggregate - .create_decryption_share_precomputed( - &dkg, - &ciphertext.header().unwrap(), - &aad, - &keypair, + for validators_num in [shares_num, shares_num + 2] { + let ( + validator_keypairs, + validators, + validators_js, + messages_js, + msg, + aad, + ciphertext, + ) = setup_dkg(shares_num, validators_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, + security_threshold, + &validators_js, + &validator, ) - .unwrap() - }) - .collect::>(); - let decryption_shares_js = into_js_array(decryption_shares); - - // Now, the decryption share can be used to decrypt the ciphertext - // This part is in the client API - - let shared_secret = - combine_decryption_shares_precomputed(&decryption_shares_js).unwrap(); - - // The client should have access to the public parameters of the DKG - let plaintext = - decrypt_with_shared_secret(&ciphertext, &aad, &shared_secret).unwrap(); - assert_eq!(msg, plaintext); + .unwrap(); + let aggregate = + dkg.aggregate_transcripts(&messages_js).unwrap(); + let is_valid = + aggregate.verify(validators_num, &messages_js).unwrap(); + assert!(is_valid); + + aggregate + .create_decryption_share_precomputed( + &dkg, + &ciphertext.header().unwrap(), + &aad, + &keypair, + ) + .unwrap() + }) + .collect::>(); + let decryption_shares_js = into_js_array(decryption_shares); + + // Now, the decryption share can be used to decrypt the ciphertext + // This part is in the client API + + let shared_secret = + combine_decryption_shares_precomputed(&decryption_shares_js) + .unwrap(); + + // The client should have access to the public parameters of the DKG + let plaintext = + decrypt_with_shared_secret(&ciphertext, &aad, &shared_secret) + .unwrap(); + assert_eq!(msg, plaintext); + } } diff --git a/ferveo/src/api.rs b/ferveo/src/api.rs index 80ab58d6..caa0d9b4 100644 --- a/ferveo/src/api.rs +++ b/ferveo/src/api.rs @@ -286,12 +286,20 @@ impl AggregatedTranscript { pub fn verify( &self, - shares_num: u32, + validators_num: u32, messages: &[ValidatorMessage], ) -> Result { + if validators_num < messages.len() as u32 { + return Err(Error::InvalidAggregateVerificationParameters( + validators_num, + messages.len() as u32, + )); + } + let pvss_params = PubliclyVerifiableParams::::default(); - let domain = GeneralEvaluationDomain::::new(shares_num as usize) - .expect("Unable to construct an evaluation domain"); + let domain = + GeneralEvaluationDomain::::new(validators_num as usize) + .expect("Unable to construct an evaluation domain"); let is_valid_optimistic = self.0.verify_optimistic(); if !is_valid_optimistic { @@ -428,8 +436,9 @@ mod test_ferveo_api { tau: u32, security_threshold: u32, shares_num: u32, + validators_num: u32, ) -> TestInputs { - let validator_keypairs = gen_keypairs(shares_num); + let validator_keypairs = gen_keypairs(validators_num); let validators = validator_keypairs .iter() .enumerate() @@ -469,16 +478,22 @@ mod test_ferveo_api { assert_eq!(dkg_pk, deserialized); } - #[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) { + #[test_case(4, 4; "number of shares (validators) is a power of 2")] + #[test_case(7, 7; "number of shares (validators) is not a power of 2")] + #[test_case(4, 6; "number of validators greater than the number of shares")] + fn test_server_api_tdec_precomputed(shares_num: u32, validators_num: u32) { let rng = &mut StdRng::seed_from_u64(0); // 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); + let (messages, validators, validator_keypairs) = make_test_inputs( + rng, + TAU, + security_threshold, + shares_num, + validators_num, + ); // Now that every validator holds a dkg instance and a transcript for every other validator, // every validator can aggregate the transcripts @@ -488,7 +503,7 @@ mod test_ferveo_api { .unwrap(); let pvss_aggregated = dkg.aggregate_transcripts(&messages).unwrap(); - assert!(pvss_aggregated.verify(shares_num, &messages).unwrap()); + assert!(pvss_aggregated.verify(validators_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(); @@ -511,7 +526,9 @@ mod test_ferveo_api { ) .unwrap(); let aggregate = dkg.aggregate_transcripts(&messages).unwrap(); - assert!(pvss_aggregated.verify(shares_num, &messages).unwrap()); + assert!(pvss_aggregated + .verify(validators_num, &messages) + .unwrap()); // And then each validator creates their own decryption share aggregate @@ -551,15 +568,21 @@ mod test_ferveo_api { assert!(result.is_err()); } - #[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) { + #[test_case(4, 4; "number of shares (validators) is a power of 2")] + #[test_case(7, 7; "number of shares (validators) is not a power of 2")] + #[test_case(4, 6; "number of validators greater than the number of shares")] + fn test_server_api_tdec_simple(shares_num: u32, validators_num: u32) { let rng = &mut StdRng::seed_from_u64(0); let security_threshold = shares_num / 2 + 1; - let (messages, validators, validator_keypairs) = - make_test_inputs(rng, TAU, security_threshold, shares_num); + let (messages, validators, validator_keypairs) = make_test_inputs( + rng, + TAU, + security_threshold, + shares_num, + validators_num, + ); // Now that every validator holds a dkg instance and a transcript for every other validator, // every validator can aggregate the transcripts @@ -573,7 +596,7 @@ mod test_ferveo_api { .unwrap(); let pvss_aggregated = dkg.aggregate_transcripts(&messages).unwrap(); - assert!(pvss_aggregated.verify(shares_num, &messages).unwrap()); + assert!(pvss_aggregated.verify(validators_num, &messages).unwrap()); // At this point, any given validator should be able to provide a DKG public key let public_key = dkg.public_key(); @@ -595,7 +618,7 @@ mod test_ferveo_api { ) .unwrap(); let aggregate = dkg.aggregate_transcripts(&messages).unwrap(); - assert!(aggregate.verify(shares_num, &messages).unwrap()); + assert!(aggregate.verify(validators_num, &messages).unwrap()); aggregate .create_decryption_share_simple( &dkg, @@ -635,104 +658,137 @@ mod test_ferveo_api { // implementation for aggregation and aggregate verification. // Here, we focus on testing user-facing APIs for server and client users. - #[test] - fn server_side_local_verification() { + #[test_case(4, 4; "number of shares (validators) is a power of 2")] + #[test_case(7, 7; "number of shares (validators) is not a power of 2")] + #[test_case(4, 6; "number of validators greater than the number of shares")] + fn server_side_local_verification(shares_num: u32, validators_num: u32) { let rng = &mut StdRng::seed_from_u64(0); + let security_threshold = shares_num / 2 + 1; - let (messages, validators, _) = - make_test_inputs(rng, TAU, SECURITY_THRESHOLD, SHARES_NUM); + let (messages, validators, _) = make_test_inputs( + rng, + TAU, + security_threshold, + shares_num, + validators_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) - .is_ok()); + let good_aggregate = dkg.aggregate_transcripts(&messages).unwrap(); + assert!(good_aggregate.verify(validators_num, &messages).is_ok()); // Test negative cases // Notice that the dkg instance is mutable, so we need to get a fresh one // for every test case + // Should fail if the number of validators is less than the number of messages + assert!(good_aggregate + .verify(messages.len() as u32 - 1, &messages) + .is_err()); + // Should fail if no transcripts are provided let mut dkg = - Dkg::new(TAU, SHARES_NUM, SECURITY_THRESHOLD, &validators, &me) + Dkg::new(TAU, shares_num, security_threshold, &validators, &me) .unwrap(); let result = dkg.aggregate_transcripts(&[]); assert!(result.is_err()); // Not enough transcripts let mut dkg = - Dkg::new(TAU, SHARES_NUM, SECURITY_THRESHOLD, &validators, &me) + Dkg::new(TAU, shares_num, security_threshold, &validators, &me) .unwrap(); - let not_enough_messages = &messages[..SECURITY_THRESHOLD as usize - 1]; - 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 = dkg.aggregate_transcripts(not_enough_messages).unwrap(); - let result = insufficient_aggregate.verify(SHARES_NUM, &messages); + let result = insufficient_aggregate.verify(validators_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 mut dkg = - Dkg::new(TAU, SHARES_NUM, SECURITY_THRESHOLD, &validators, &me) + Dkg::new(TAU, shares_num, security_threshold, &validators, &me) .unwrap(); - let (bad_messages, _, _) = - make_test_inputs(rng, TAU, SECURITY_THRESHOLD, SHARES_NUM); + let (bad_messages, _, _) = make_test_inputs( + rng, + TAU, + security_threshold, + shares_num, + validators_num, + ); let mixed_messages = [&messages[..2], &bad_messages[..1]].concat(); let bad_aggregate = dkg.aggregate_transcripts(&mixed_messages).unwrap(); - let result = bad_aggregate.verify(SHARES_NUM, &messages); + let result = bad_aggregate.verify(validators_num, &messages); assert!(result.is_err()); } - #[test] - fn client_side_local_verification() { + #[test_case(4, 4; "number of shares (validators) is a power of 2")] + #[test_case(7, 7; "number of shares (validators) is not a power of 2")] + #[test_case(4, 6; "number of validators greater than the number of shares")] + fn client_side_local_verification(shares_num: u32, validators_num: u32) { let rng = &mut StdRng::seed_from_u64(0); + let security_threshold = shares_num / 2 + 1; - let (messages, _, _) = - make_test_inputs(rng, TAU, SECURITY_THRESHOLD, SHARES_NUM); + let (messages, _, _) = make_test_inputs( + rng, + TAU, + security_threshold, + shares_num, + validators_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).unwrap(); + let good_aggregate = AggregatedTranscript::new(messages).unwrap(); // We are separating the verification from the aggregation since the client may fetch // 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 = good_aggregate.verify(validators_num, messages); assert!(result.is_ok()); assert!(result.unwrap()); // Test negative cases + // Should fail if the number of validators is less than the number of messages + assert!(good_aggregate + .verify(messages.len() as u32 - 1, messages) + .is_err()); + // Should fail if no transcripts are provided let result = AggregatedTranscript::new(&[]); assert!(result.is_err()); // Not enough transcripts - let not_enough_messages = &messages[..SECURITY_THRESHOLD as usize - 1]; - 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).unwrap(); - let result = insufficient_aggregate.verify(SHARES_NUM, messages); + let result = insufficient_aggregate.verify(validators_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); + let (bad_messages, _, _) = make_test_inputs( + rng, + TAU, + security_threshold, + shares_num, + validators_num, + ); let mixed_messages = [&messages[..2], &bad_messages[..1]].concat(); let bad_aggregate = AggregatedTranscript::new(&mixed_messages).unwrap(); - let result = bad_aggregate.verify(SHARES_NUM, messages); + let result = bad_aggregate.verify(validators_num, messages); assert!(result.is_err()); } } diff --git a/ferveo/src/bindings_python.rs b/ferveo/src/bindings_python.rs index bd29dc0d..138bf073 100644 --- a/ferveo/src/bindings_python.rs +++ b/ferveo/src/bindings_python.rs @@ -116,6 +116,11 @@ impl From for PyErr { Error::NoTranscriptsToAggregate => { NoTranscriptsToAggregate::new_err("") } + Error::InvalidAggregateVerificationParameters(validators_num, messages_num) => { + InvalidDkgParameters::new_err(format!( + "validators_num: {validators_num}, messages_num: {messages_num}" + )) + } }, _ => default(), } @@ -595,14 +600,14 @@ impl AggregatedTranscript { pub fn verify( &self, - shares_num: u32, + validators_num: u32, messages: Vec, ) -> PyResult { let messages: Vec<_> = messages.into_iter().map(|vm| vm.to_inner()).collect(); let is_valid = self .0 - .verify(shares_num, &messages) + .verify(validators_num, &messages) .map_err(FerveoPythonError::FerveoError)?; Ok(is_valid) } @@ -832,7 +837,6 @@ mod test_ferveo_python { let messages = messages[..security_threshold as usize].to_vec(); let pvss_aggregated = dkg.aggregate_transcripts(messages.clone()).unwrap(); - // TODO: Redo how verification API works; assert!(pvss_aggregated .verify(validators_num, messages.clone()) .unwrap()); @@ -859,7 +863,6 @@ mod test_ferveo_python { let aggregate = validator_dkg .aggregate_transcripts(messages.clone()) .unwrap(); - // TODO: Redo how verification API works; assert!(pvss_aggregated .verify(validators_num, messages.clone()) .is_ok()); @@ -912,7 +915,6 @@ mod test_ferveo_python { let messages = messages[..security_threshold as usize].to_vec(); let pvss_aggregated = dkg.aggregate_transcripts(messages.clone()).unwrap(); - // TODO: Redo how verification API works; assert!(pvss_aggregated .verify(validators_num, messages.clone()) .unwrap()); @@ -940,7 +942,6 @@ mod test_ferveo_python { .aggregate_transcripts(messages.clone()) .unwrap(); - // TODO: Redo how verification API works; assert!(aggregate .verify(validators_num, messages.clone()) .unwrap()); diff --git a/ferveo/src/bindings_wasm.rs b/ferveo/src/bindings_wasm.rs index 1396de13..07e22e3f 100644 --- a/ferveo/src/bindings_wasm.rs +++ b/ferveo/src/bindings_wasm.rs @@ -515,13 +515,15 @@ impl AggregatedTranscript { #[wasm_bindgen] pub fn verify( &self, - shares_num: u32, + validators_num: u32, messages: &ValidatorMessageArray, ) -> JsResult { set_panic_hook(); let messages = unwrap_messages_js(messages)?; - let is_valid = - self.0.verify(shares_num, &messages).map_err(map_js_err)?; + let is_valid = self + .0 + .verify(validators_num, &messages) + .map_err(map_js_err)?; Ok(is_valid) } diff --git a/ferveo/src/lib.rs b/ferveo/src/lib.rs index d5f1fb11..f9d6c1a5 100644 --- a/ferveo/src/lib.rs +++ b/ferveo/src/lib.rs @@ -117,6 +117,10 @@ pub enum Error { /// Creating a transcript aggregate requires at least one transcript #[error("No transcripts to aggregate")] NoTranscriptsToAggregate, + + /// The number of messages may not be greater than the number of validators + #[error("Invalid aggregate verification parameters: number of validators {0}, number of messages: {1}")] + InvalidAggregateVerificationParameters(u32, u32), } pub type Result = std::result::Result; @@ -557,15 +561,13 @@ mod test_dkg_full { validator_keypairs.as_slice(), ); - let domain_points = dkg.domain_points(); - // Each participant prepares an update for each other participant let share_updates = dkg .validators .keys() .map(|v_addr| { let deltas_i = prepare_share_updates_for_refresh::( - &domain_points, + &dkg.domain_points(), &dkg.pvss_params.h.into_affine(), dkg.dkg_params.security_threshold() as usize, rng, @@ -628,7 +630,7 @@ mod test_dkg_full { .collect(); let lagrange = ferveo_tdec::prepare_combine_simple::( - &domain_points[..SECURITY_THRESHOLD as usize], + &dkg.domain_points()[..SECURITY_THRESHOLD as usize], ); let new_shared_secret = ferveo_tdec::share_combine_simple::( &decryption_shares[..SECURITY_THRESHOLD as usize],