From 7e57d2a71e45927e5688a7ef08da825fc41336a2 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
---
ferveo-wasm/tests/node.rs | 217 ++++++++++++++++++----------------
ferveo/src/api.rs | 156 ++++++++++++++++--------
ferveo/src/bindings_python.rs | 5 +
ferveo/src/lib.rs | 4 +
4 files changed, 233 insertions(+), 149 deletions(-)
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..137bc718 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(),
}
diff --git a/ferveo/src/lib.rs b/ferveo/src/lib.rs
index d5f1fb11..47926b3f 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;