diff --git a/ferveo-python/examples/server_api_precomputed.py b/ferveo-python/examples/server_api_precomputed.py index 72263405..a9b98001 100644 --- a/ferveo-python/examples/server_api_precomputed.py +++ b/ferveo-python/examples/server_api_precomputed.py @@ -16,18 +16,16 @@ 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) ] -# Validators must be sorted by their public key -validators.sort(key=lambda v: v.address) - # Each validator holds their own DKG instance and generates a transcript every # validator, including themselves messages = [] @@ -52,11 +50,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 +74,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/ferveo/__init__.pyi b/ferveo-python/ferveo/__init__.pyi index a7c5abb6..3d44f51b 100644 --- a/ferveo-python/ferveo/__init__.pyi +++ b/ferveo-python/ferveo/__init__.pyi @@ -25,12 +25,14 @@ class FerveoPublicKey: @final class Validator: - def __init__(self, address: str, public_key: FerveoPublicKey): ... + def __init__(self, address: str, public_key: FerveoPublicKey, share_index: int): ... address: str public_key: FerveoPublicKey + share_index: int + @final class Transcript: @staticmethod @@ -189,11 +191,29 @@ class InsufficientValidators(Exception): class InvalidTranscriptAggregate(Exception): pass -class ValidatorsNotSorted(Exception): - pass - class ValidatorPublicKeyMismatch(Exception): pass class SerializationError(Exception): pass + +class InvalidVariant(Exception): + pass + +class InvalidDkgParameters(Exception): + pass + +class InvalidDkgParametersForPrecomputedVariant(Exception): + pass + +class InvalidShareIndex(Exception): + pass + +class DuplicatedShareIndex(Exception): + pass + +class NoTranscriptsToAggregate(Exception): + pass + +class InvalidAggregateVerificationParameters(Exception): + pass diff --git a/ferveo-python/test/test_ferveo.py b/ferveo-python/test/test_ferveo.py index b3496d3e..51af3867 100644 --- a/ferveo-python/test/test_ferveo.py +++ b/ferveo-python/test/test_ferveo.py @@ -5,6 +5,7 @@ combine_decryption_shares_simple, combine_decryption_shares_precomputed, decrypt_with_shared_secret, + AggregatedTranscript, Keypair, Validator, ValidatorMessage, @@ -37,18 +38,29 @@ 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 variant == FerveoVariant.Precomputed and shares_to_use != validators_num: + raise ValueError( + "In precomputed variant, shares_to_use must be equal to validators_num" + ) + 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) ] - validators.sort(key=lambda v: v.address) + # Each validator holds their own DKG instance and generates a transcript every + # validator, including themselves messages = [] for sender in validators: dkg = Dkg( @@ -60,6 +72,7 @@ def scenario_for_variant(variant: FerveoVariant, shares_num, threshold, shares_t ) messages.append(ValidatorMessage(sender, dkg.generate_transcript())) + # Both client and server should be able to verify the aggregated transcript dkg = Dkg( tau=tau, shares_num=shares_num, @@ -67,18 +80,23 @@ def scenario_for_variant(variant: FerveoVariant, shares_num, threshold, shares_t validators=validators, me=validators[0], ) - pvss_aggregated = dkg.aggregate_transcripts(messages) - assert pvss_aggregated.verify(shares_num, messages) + server_aggregate = dkg.aggregate_transcripts(messages) + assert server_aggregate.verify(validators_num, messages) - dkg_pk_bytes = bytes(dkg.public_key) - dkg_pk = DkgPublicKey.from_bytes(dkg_pk_bytes) + client_aggregate = AggregatedTranscript(messages) + assert client_aggregate.verify(validators_num, messages) + # Client creates a ciphertext and requests decryption shares from validators msg = "abc".encode() aad = "my-aad".encode() - ciphertext = encrypt(msg, aad, dkg_pk) + ciphertext = encrypt(msg, aad, dkg.public_key) + # Having aggregated the transcripts, the validators can now create decryption shares decryption_shares = [] for validator, validator_keypair in zip(validators, validator_keypairs): + assert validator.public_key == validator_keypair.public_key() + print("validator: ", validator.share_index) + dkg = Dkg( tau=tau, shares_num=shares_num, @@ -87,15 +105,17 @@ 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 ) decryption_shares.append(decryption_share) - decryption_shares = decryption_shares[:shares_to_use] + # We are limiting the number of decryption shares to use for testing purposes + # decryption_shares = decryption_shares[:shares_to_use] + # Client combines the decryption shares and decrypts the ciphertext shared_secret = combine_shares_for_variant(variant, decryption_shares) if variant == FerveoVariant.Simple and len(decryption_shares) < threshold: @@ -103,7 +123,7 @@ def scenario_for_variant(variant: FerveoVariant, shares_num, threshold, shares_t decrypt_with_shared_secret(ciphertext, aad, shared_secret) return - if variant == FerveoVariant.Precomputed and len(decryption_shares) < shares_num: + if variant == FerveoVariant.Precomputed and len(decryption_shares) < threshold: with pytest.raises(ThresholdEncryptionError): decrypt_with_shared_secret(ciphertext, aad, shared_secret) return @@ -113,27 +133,55 @@ 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 = shares_num - 1 + for validators_num in [shares_num, shares_num + 2]: + 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 = shares_num - 1 + for validators_num in [shares_num, shares_num + 2]: + scenario_for_variant( + FerveoVariant.Simple, + shares_num=shares_num, + validators_num=validators_num, + threshold=threshold, + shares_to_use=validators_num - 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 = shares_num # in precomputed variant, we need all shares + for validators_num in [shares_num, shares_num + 2]: + scenario_for_variant( + FerveoVariant.Precomputed, + shares_num=shares_num, + validators_num=validators_num, + threshold=threshold, + shares_to_use=validators_num, + ) 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 = shares_num # in precomputed variant, we need all shares + for validators_num in [shares_num, shares_num + 2]: + scenario_for_variant( + FerveoVariant.Simple, + shares_num=shares_num, + validators_num=validators_num, + threshold=threshold, + shares_to_use=threshold - 1, + ) PARAMS = [ diff --git a/ferveo-wasm/examples/node/src/main.test.ts b/ferveo-wasm/examples/node/src/main.test.ts index 5814a53d..00da665b 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,16 +50,16 @@ 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 + // Client creates a ciphertext and requests decryption shares from validators const msg = Buffer.from("my-msg"); const aad = Buffer.from("my-aad"); const ciphertext = ferveoEncrypt(msg, aad, dkg.publicKey()); @@ -73,94 +78,78 @@ 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 = sharesNum - 1; + [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]) => { + 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.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..44f43e55 100644 --- a/ferveo/src/bindings_python.rs +++ b/ferveo/src/bindings_python.rs @@ -104,7 +104,7 @@ impl From for PyErr { )) } Error::InvalidDkgParametersForPrecomputedVariant(shares_num, security_threshold) => { - InvalidDkgParameters::new_err(format!( + InvalidDkgParametersForPrecomputedVariant::new_err(format!( "shares_num: {shares_num}, security_threshold: {security_threshold}" )) } @@ -116,6 +116,11 @@ impl From for PyErr { Error::NoTranscriptsToAggregate => { NoTranscriptsToAggregate::new_err("") } + Error::InvalidAggregateVerificationParameters(validators_num, messages_num) => { + InvalidAggregateVerificationParameters::new_err(format!( + "validators_num: {validators_num}, messages_num: {messages_num}" + )) + } }, _ => default(), } @@ -151,9 +156,19 @@ create_exception!(exceptions, SerializationError, PyValueError); create_exception!(exceptions, InvalidByteLength, PyValueError); create_exception!(exceptions, InvalidVariant, PyValueError); create_exception!(exceptions, InvalidDkgParameters, PyValueError); +create_exception!( + exceptions, + InvalidDkgParametersForPrecomputedVariant, + PyValueError +); create_exception!(exceptions, InvalidShareIndex, PyValueError); create_exception!(exceptions, DuplicatedShareIndex, PyValueError); create_exception!(exceptions, NoTranscriptsToAggregate, PyValueError); +create_exception!( + exceptions, + InvalidAggregateVerificationParameters, + PyValueError +); fn from_py_bytes(bytes: &[u8]) -> PyResult { T::from_bytes(bytes) @@ -421,6 +436,11 @@ impl Validator { pub fn public_key(&self) -> FerveoPublicKey { FerveoPublicKey(self.0.public_key) } + + #[getter] + pub fn share_index(&self) -> u32 { + self.0.share_index + } } #[pyclass(module = "ferveo")] @@ -595,14 +615,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) } @@ -736,13 +756,33 @@ pub fn make_ferveo_py_module(py: Python<'_>, m: &PyModule) -> PyResult<()> { "InvalidTranscriptAggregate", py.get_type::(), )?; - m.add("ValidatorsNotSorted", py.get_type::())?; m.add( "ValidatorPublicKeyMismatch", py.get_type::(), )?; m.add("SerializationError", py.get_type::())?; m.add("InvalidVariant", py.get_type::())?; + m.add( + "InvalidDkgParameters", + py.get_type::(), + )?; + m.add( + "InvalidDkgParametersForPrecomputedVariant", + py.get_type::(), + )?; + m.add("InvalidShareIndex", py.get_type::())?; + m.add( + "DuplicatedShareIndex", + py.get_type::(), + )?; + m.add( + "NoTranscriptsToAggregate", + py.get_type::(), + )?; + m.add( + "InvalidAggregateVerificationParameters", + py.get_type::(), + )?; Ok(()) } @@ -832,7 +872,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 +898,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 +950,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 +977,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],