diff --git a/.github/workflows/workspace.yml b/.github/workflows/workspace.yml index a70b85b1..ce39c5d6 100644 --- a/.github/workflows/workspace.yml +++ b/.github/workflows/workspace.yml @@ -172,7 +172,7 @@ jobs: working-directory: ferveo-python - name: Run ruff - run: ruff check ferveo + run: ruff check . working-directory: ferveo-python codecov: diff --git a/ferveo-common/Cargo.toml b/ferveo-common/Cargo.toml index 2084a1eb..7824eac3 100644 --- a/ferveo-common/Cargo.toml +++ b/ferveo-common/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "ferveo-nucypher-common" -version = "0.4.0" +version = "0.4.1" edition = "2021" license = "GPL-3.0" authors = ["David Nuñez ", "Piotr Roslaniec ", "Heliax AG "] diff --git a/ferveo-python/Cargo.toml b/ferveo-python/Cargo.toml index 4c45df1e..2350e110 100644 --- a/ferveo-python/Cargo.toml +++ b/ferveo-python/Cargo.toml @@ -1,7 +1,7 @@ [package] name = "ferveo-python" authors = ["David Nuñez ", "Piotr Roslaniec "] -version = "0.4.0" +version = "0.4.1" edition = "2021" repository = "https://github.com/nucypher/ferveo" publish = false diff --git a/ferveo-python/examples/exception.py b/ferveo-python/examples/exception.py index 7ee08bdd..1feb858d 100644 --- a/ferveo-python/examples/exception.py +++ b/ferveo-python/examples/exception.py @@ -1,7 +1,7 @@ from ferveo import ( + Dkg, Keypair, Validator, - Dkg, ) diff --git a/ferveo-python/examples/server_api_precomputed.py b/ferveo-python/examples/server_api_precomputed.py index dc12c108..5df05bb7 100644 --- a/ferveo-python/examples/server_api_precomputed.py +++ b/ferveo-python/examples/server_api_precomputed.py @@ -1,12 +1,12 @@ from ferveo import ( - encrypt, - combine_decryption_shares_precomputed, - decrypt_with_shared_secret, + AggregatedTranscript, + Dkg, Keypair, Validator, ValidatorMessage, - Dkg, - AggregatedTranscript, + combine_decryption_shares_precomputed, + decrypt_with_shared_secret, + encrypt, ) diff --git a/ferveo-python/examples/server_api_simple.py b/ferveo-python/examples/server_api_simple.py index 6bb75474..2c5dc158 100644 --- a/ferveo-python/examples/server_api_simple.py +++ b/ferveo-python/examples/server_api_simple.py @@ -1,12 +1,12 @@ from ferveo import ( - encrypt, - combine_decryption_shares_simple, - decrypt_with_shared_secret, + AggregatedTranscript, + Dkg, Keypair, Validator, ValidatorMessage, - Dkg, - AggregatedTranscript, + combine_decryption_shares_simple, + decrypt_with_shared_secret, + encrypt, ) @@ -24,9 +24,6 @@ def gen_eth_addr(i: int) -> str: 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 = [] diff --git a/ferveo-python/examples/server_api_simple_with_handover.py b/ferveo-python/examples/server_api_simple_with_handover.py index 7870a49f..723b8bea 100644 --- a/ferveo-python/examples/server_api_simple_with_handover.py +++ b/ferveo-python/examples/server_api_simple_with_handover.py @@ -1,12 +1,12 @@ from ferveo import ( - encrypt, - combine_decryption_shares_simple, - decrypt_with_shared_secret, + AggregatedTranscript, + Dkg, Keypair, Validator, ValidatorMessage, - Dkg, - AggregatedTranscript, + combine_decryption_shares_simple, + decrypt_with_shared_secret, + encrypt, ) @@ -24,9 +24,6 @@ def gen_eth_addr(i: int) -> str: 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 = [] diff --git a/ferveo-python/pyproject.toml b/ferveo-python/pyproject.toml index 2532c800..f927a43e 100644 --- a/ferveo-python/pyproject.toml +++ b/ferveo-python/pyproject.toml @@ -4,3 +4,6 @@ requires = ["setuptools", "wheel", "setuptools-rust"] [tool.ruff] exclude = ["ferveo/__init__.py"] # false-positive unused-import select = ["E", "F", "I"] + +[tool.ruff.lint] +ignore = ["E501"] \ No newline at end of file diff --git a/ferveo-python/setup.py b/ferveo-python/setup.py index 0de59e05..d7ff6f33 100644 --- a/ferveo-python/setup.py +++ b/ferveo-python/setup.py @@ -1,8 +1,8 @@ +from pathlib import Path + from setuptools import setup from setuptools_rust import Binding, RustExtension -from pathlib import Path - this_directory = Path(__file__).parent long_description = (this_directory / "README.md").read_text() diff --git a/ferveo-python/test/test_ferveo.py b/ferveo-python/test/test_ferveo.py index a4fa578e..f7516af5 100644 --- a/ferveo-python/test/test_ferveo.py +++ b/ferveo-python/test/test_ferveo.py @@ -1,18 +1,17 @@ import pytest from ferveo import ( - encrypt, - combine_decryption_shares_simple, - combine_decryption_shares_precomputed, - decrypt_with_shared_secret, AggregatedTranscript, + Dkg, + FerveoVariant, Keypair, + ThresholdEncryptionError, Validator, ValidatorMessage, - Dkg, - DkgPublicKey, - ThresholdEncryptionError, - FerveoVariant, + combine_decryption_shares_precomputed, + combine_decryption_shares_simple, + decrypt_with_shared_secret, + encrypt, ) diff --git a/ferveo-python/test/test_serialization.py b/ferveo-python/test/test_serialization.py index c20c2037..43b928fa 100644 --- a/ferveo-python/test/test_serialization.py +++ b/ferveo-python/test/test_serialization.py @@ -1,11 +1,14 @@ +import pytest + from ferveo import ( - Keypair, - Validator, + AggregatedTranscript, Dkg, DkgPublicKey, FerveoPublicKey, FerveoVariant, - ValidatorMessage + Keypair, + Validator, + ValidatorMessage, ) @@ -24,19 +27,24 @@ def gen_eth_addr(i: int) -> str: validators.sort(key=lambda v: v.address) -def make_dkg_public_key(): +@pytest.fixture(scope="module") +def dkg(): me = validators[0] - dkg = Dkg( + return Dkg( tau=tau, shares_num=shares_num, security_threshold=security_threshold, validators=validators, me=me, ) + + +@pytest.fixture(scope="module") +def aggregate(dkg): transcripts = [ValidatorMessage(v, dkg.generate_transcript()) for v in validators] aggregate = dkg.aggregate_transcripts(transcripts) assert aggregate.verify(shares_num, transcripts) - return aggregate.public_key + return aggregate def make_shared_secret(): @@ -67,8 +75,8 @@ def test_keypair_serialization(): assert serialized == bytes(deserialized) -def test_dkg_public_key_serialization(): - dkg_pk = make_dkg_public_key() +def test_dkg_public_key_serialization(aggregate): + dkg_pk = aggregate.public_key serialized = bytes(dkg_pk) deserialized = DkgPublicKey.from_bytes(serialized) # TODO: Implement __richcmp__ @@ -90,3 +98,36 @@ def test_ferveo_variant_serialization(): assert FerveoVariant.Precomputed == FerveoVariant.Precomputed assert FerveoVariant.Simple == FerveoVariant.Simple assert FerveoVariant.Precomputed != FerveoVariant.Simple + + +def test_aggregate_transcript_serialization(aggregate): + serialized = bytes(aggregate) + deserialized = AggregatedTranscript.from_bytes(serialized) + assert bytes(aggregate.public_key) == bytes(deserialized.public_key) + + +def test_aggregate_transcript_validity(dkg, aggregate): + assert aggregate.verify_for_dkg(dkg) + + +@pytest.mark.parametrize("handover_slot_index", range(shares_num)) +def test_handover_serialization(dkg, aggregate, handover_slot_index): + incoming_validator_keypair = Keypair.random() + departing_keypair = validator_keypairs[handover_slot_index] + + handover_transcript = dkg.generate_handover_transcript( + aggregate, + handover_slot_index, + incoming_validator_keypair, + ) + + assert handover_transcript.share_index == handover_slot_index + + assert aggregate.validate_handover_transcript(handover_transcript) + + new_aggregate = aggregate.finalize_handover( + handover_transcript, departing_keypair + ) + + assert bytes(new_aggregate.public_key) == bytes(aggregate.public_key) + assert bytes(new_aggregate) != bytes(aggregate) \ No newline at end of file diff --git a/ferveo-tdec/Cargo.toml b/ferveo-tdec/Cargo.toml index 9c94fe6e..5a11364c 100644 --- a/ferveo-tdec/Cargo.toml +++ b/ferveo-tdec/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "ferveo-nucypher-tdec" -version = "0.4.0" +version = "0.4.1" edition = "2021" authors = ["David Nuñez ", "Piotr Roslaniec ", "Heliax AG "] license = "GPL-3.0" @@ -24,7 +24,7 @@ ark-serialize = { workspace = true } ark-std = { workspace = true } bincode = { workspace = true } chacha20poly1305 = { workspace = true } -ferveo-common = { package = "ferveo-nucypher-common", path = "../ferveo-common", version = "^0.4.0" } +ferveo-common = { package = "ferveo-nucypher-common", path = "../ferveo-common", version = "^0.4.1" } itertools = { workspace = true } miracl_core = { workspace = true } rand = { workspace = true } @@ -33,7 +33,7 @@ serde = { workspace = true, features = ["derive"] } serde_bytes = { workspace = true } serde_with = { workspace = true } sha2 = { workspace = true } -subproductdomain = { package = "subproductdomain-nucypher", path = "../subproductdomain", version = "^0.4.0" } +subproductdomain = { package = "subproductdomain-nucypher", path = "../subproductdomain", version = "^0.4.1" } thiserror = { workspace = true } zeroize = { workspace = true } diff --git a/ferveo-wasm/Cargo.toml b/ferveo-wasm/Cargo.toml index 83872fd1..2e24b214 100644 --- a/ferveo-wasm/Cargo.toml +++ b/ferveo-wasm/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "ferveo-wasm" -version = "0.4.0" +version = "0.4.1" authors = ["David Nuñez ", "Piotr Roslaniec "] edition = "2021" license = "GPL-3.0-only" diff --git a/ferveo/Cargo.toml b/ferveo/Cargo.toml index ba1e0b9e..8fccb97f 100644 --- a/ferveo/Cargo.toml +++ b/ferveo/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "ferveo-nucypher" -version = "0.4.0" +version = "0.4.1" edition = "2021" license = "GPL-3.0" repository = "https://github.com/nucypher/ferveo" @@ -21,8 +21,8 @@ ark-poly = { workspace = true } ark-serialize = { workspace = true } ark-std = { workspace = true } bincode = { workspace = true } -ferveo-common = { package = "ferveo-nucypher-common", path = "../ferveo-common", version = "^0.4.0" } -ferveo-tdec = { package = "ferveo-nucypher-tdec", path = "../ferveo-tdec", features = ["api", "test-common"], version = "^0.4.0" } +ferveo-common = { package = "ferveo-nucypher-common", path = "../ferveo-common", version = "^0.4.1" } +ferveo-tdec = { package = "ferveo-nucypher-tdec", path = "../ferveo-tdec", features = ["api", "test-common"], version = "^0.4.1" } hex = { workspace = true } itertools = { workspace = true } measure_time = { workspace = true } @@ -31,7 +31,7 @@ rand_core = { workspace = true } rand_old = { workspace = true } serde = { workspace = true, features = ["derive"] } serde_with = { workspace = true } -subproductdomain = { package = "subproductdomain-nucypher", path = "../subproductdomain", version = "^0.4.0" } +subproductdomain = { package = "subproductdomain-nucypher", path = "../subproductdomain", version = "^0.4.1" } thiserror = { workspace = true } zeroize = { workspace = true, features = ["derive"] } generic-array = { workspace = true } diff --git a/ferveo/src/api.rs b/ferveo/src/api.rs index 04421c55..5ef99896 100644 --- a/ferveo/src/api.rs +++ b/ferveo/src/api.rs @@ -25,7 +25,7 @@ use crate::bindings_python; use crate::bindings_wasm; pub use crate::EthereumAddress; use crate::{ - do_verify_aggregation, Error, PubliclyVerifiableSS, Result, + do_verify_aggregation, Aggregated, Error, PubliclyVerifiableSS, Result, UpdateTranscript, }; @@ -315,6 +315,10 @@ impl AggregatedTranscript { ) } + pub fn verify_for_dkg(&self, dkg: &Dkg) -> Result { + self.0.aggregate.verify_full(&dkg.0) + } + pub fn create_decryption_share_precomputed( &self, dkg: &Dkg, @@ -382,6 +386,19 @@ impl AggregatedTranscript { Ok(AggregatedTranscript(eeww)) } + pub fn validate_handover_transcript( + &self, + handover_transcript: &HandoverTranscript, + ) -> Result { + let share_commitments = self.0.aggregate.get_share_commitments(); + let share_commitment = share_commitments + .get(handover_transcript.0.share_index as usize) + .ok_or( + Error::InvalidTranscriptAggregate, // FIXME: better error + )?; + handover_transcript.0.validate(share_commitment) + } + pub fn finalize_handover( &self, handover_transcript: &HandoverTranscript, @@ -398,6 +415,16 @@ impl AggregatedTranscript { .unwrap(); Ok(AggregatedTranscript(eeww)) } + + pub fn aggregate(&self) -> &PubliclyVerifiableSS { + &self.0.aggregate + } +} + +impl HandoverTranscript { + pub fn share_index(&self) -> u32 { + self.0.share_index + } } #[serde_as] diff --git a/ferveo/src/bindings_python.rs b/ferveo/src/bindings_python.rs index e9c64229..39a38235 100644 --- a/ferveo/src/bindings_python.rs +++ b/ferveo/src/bindings_python.rs @@ -646,6 +646,14 @@ impl AggregatedTranscript { Ok(is_valid) } + pub fn verify_for_dkg(&self, dkg: &Dkg) -> PyResult { + let is_valid = self + .0 + .verify_for_dkg(&dkg.0) + .map_err(FerveoPythonError::FerveoError)?; + Ok(is_valid) + } + pub fn create_decryption_share_precomputed( &self, dkg: &Dkg, @@ -688,6 +696,17 @@ impl AggregatedTranscript { Ok(DecryptionShareSimple(decryption_share)) } + pub fn validate_handover_transcript( + &self, + handover_transcript: &HandoverTranscript, + ) -> PyResult { + let is_valid = self + .0 + .validate_handover_transcript(&handover_transcript.0) + .map_err(FerveoPythonError::FerveoError)?; + Ok(is_valid) + } + pub fn finalize_handover( &self, handover_transcript: &HandoverTranscript, @@ -706,6 +725,14 @@ impl AggregatedTranscript { } } +#[pymethods] +impl HandoverTranscript { + #[getter] + pub fn share_index(&self) -> u32 { + self.0.share_index() + } +} + // Since adding functions in pyo3 requires a two-step process // (`#[pyfunction]` + `wrap_pyfunction!`), and `wrap_pyfunction` // needs `#[pyfunction]` in the same module, we need these trampolines diff --git a/ferveo/src/dkg.rs b/ferveo/src/dkg.rs index 08842eb1..d1444536 100644 --- a/ferveo/src/dkg.rs +++ b/ferveo/src/dkg.rs @@ -99,13 +99,13 @@ impl PubliclyVerifiableDkg { ) .expect("unable to construct domain"); - let validators: ValidatorsByIndex = validators + let validators_by_index: ValidatorsByIndex = validators .iter() .map(|validator| (validator.share_index, validator.clone())) .collect(); // Make sure that `me` is a known validator - if let Some(my_validator) = validators.get(&me.share_index) { + if let Some(my_validator) = validators_by_index.get(&me.share_index) { if my_validator.public_key != me.public_key { return Err(Error::ValidatorPublicKeyMismatch); } @@ -117,10 +117,14 @@ impl PubliclyVerifiableDkg { dkg_params: *dkg_params, domain, me: me.clone(), - validators, + validators: validators_by_index, }) } + pub fn all_validators(&self) -> Vec> { + self.validators.values().cloned().collect::>() + } + /// Get the validator with for the given public key pub fn get_validator( &self, diff --git a/ferveo/src/lib.rs b/ferveo/src/lib.rs index f6633969..bfc2c099 100644 --- a/ferveo/src/lib.rs +++ b/ferveo/src/lib.rs @@ -803,7 +803,7 @@ mod test_dkg_full { .unwrap() .into_affine(), ); - assert!(handover_transcript.validate(share_commitment).unwrap()); + assert!(handover_transcript.validate(&share_commitment).unwrap()); // The departing validator uses the handover transcript produced by the // incoming validator to create a new aggregate transcript. diff --git a/ferveo/src/pvss.rs b/ferveo/src/pvss.rs index 762ed1af..dfbcf387 100644 --- a/ferveo/src/pvss.rs +++ b/ferveo/src/pvss.rs @@ -198,9 +198,9 @@ pub fn get_share_commitments_from_poly_commitments( poly_comms: &[E::G1Affine], domain: &ark_poly::GeneralEvaluationDomain, ) -> Vec { - let mut commitment = batch_to_projective_g1::(poly_comms); - domain.fft_in_place(&mut commitment); - commitment + let mut commitments = batch_to_projective_g1::(poly_comms); + domain.fft_in_place(&mut commitments); + commitments } pub fn verify_validator_share( @@ -209,8 +209,6 @@ pub fn verify_validator_share( share_index: usize, validator_public_key: PublicKey, ) -> Result { - // TODO: Check #3 is missing - // See #3 in 4.2.3 section of https://eprint.iacr.org/2022/898.pdf let y_i = pvss_encrypted_shares .get(share_index) .ok_or(Error::InvalidShareIndex(share_index as u32))?; @@ -289,13 +287,12 @@ pub fn do_verify_aggregation( /// Extra methods available to aggregated PVSS transcripts impl PubliclyVerifiableSS { /// Verify that this PVSS instance is a valid aggregation of - /// the PVSS instances, produced by [`aggregate`], + /// the PVSS instances, produced by [`transcripts`], /// and received by the DKG context `dkg`. - /// Returns the total nr of shares in the aggregated PVSS pub fn verify_aggregation( &self, dkg: &PubliclyVerifiableDkg, - pvss: &[PubliclyVerifiableSS], + transcripts: &[PubliclyVerifiableSS], ) -> Result { let validators = dkg.validators.values().cloned().collect::>(); do_verify_aggregation( @@ -303,7 +300,7 @@ impl PubliclyVerifiableSS { &self.shares, &validators, &dkg.domain, - pvss, + transcripts, ) } @@ -472,7 +469,7 @@ impl PubliclyVerifiableSS { .into_affine(), ); let new_blind_share = handover_transcript - .finalize(validator_keypair, share_commitment) + .finalize(validator_keypair, &share_commitment) .unwrap(); let mut original_shares = self.shares.clone(); @@ -495,6 +492,21 @@ impl PubliclyVerifiableSS { }; Ok(aggregrate_post_handover) } + + pub fn get_share_commitments(&self) -> Vec> { + let size = self.shares.len(); + let domain = + ark_poly::GeneralEvaluationDomain::::new(size) + .expect("Invalid domain size"); + let commitment_points = get_share_commitments_from_poly_commitments::( + self.coeffs.as_slice(), + &domain, + ); + commitment_points + .iter() + .map(|c| ShareCommitment::(c.into_affine())) + .collect() + } } #[derive(Serialize, Deserialize, Clone, Debug, PartialEq, Eq)] diff --git a/ferveo/src/refresh.rs b/ferveo/src/refresh.rs index 2b9abaa7..43f9e86d 100644 --- a/ferveo/src/refresh.rs +++ b/ferveo/src/refresh.rs @@ -368,7 +368,7 @@ impl HandoverTranscript { // See similarity with transcript check #4 (do_verify_full in pvss) pub fn validate( &self, - share_commitment: ShareCommitment, + share_commitment: &ShareCommitment, ) -> Result { // e(comm_G1, double_blind_share) == e(A_i, comm_share) // or equivalently: @@ -400,7 +400,7 @@ impl HandoverTranscript { pub fn finalize( &self, departing_validator_keypair: &Keypair, - share_commitment: ShareCommitment, + share_commitment: &ShareCommitment, ) -> Result> { let is_valid = &self.validate(share_commitment).unwrap(); if !is_valid { @@ -916,7 +916,7 @@ mod tests_refresh { // Make sure handover transcript is valid. This is publicly verifiable. assert!(handover_transcript - .validate(departing_public_context.share_commitment) + .validate(&departing_public_context.share_commitment) .unwrap()); // This portion shows that handover can be finalized by the departing participant, @@ -930,7 +930,7 @@ mod tests_refresh { let new_blinded_share = handover_transcript .finalize( &departing_validator_keypair, - departing_public_context.share_commitment, + &departing_public_context.share_commitment, ) .unwrap(); diff --git a/subproductdomain/Cargo.toml b/subproductdomain/Cargo.toml index a1786e84..c090b759 100644 --- a/subproductdomain/Cargo.toml +++ b/subproductdomain/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "subproductdomain-nucypher" -version = "0.4.0" +version = "0.4.1" edition = "2021" license = "GPL-3.0" authors = ["David Nuñez ", "Heliax AG ", "Piotr Roslaniec "]