From 05af5104bca1b2f0879fded9a4194ed1dc6b08c3 Mon Sep 17 00:00:00 2001 From: Mr-Leshiy Date: Tue, 24 Sep 2024 13:24:33 +0300 Subject: [PATCH 01/36] initialize a new crate --- .github/workflows/semantic_pull_request.yml | 1 + rust/Cargo.toml | 1 + rust/catalyst-voting/Cargo.toml | 13 +++++++++++++ rust/catalyst-voting/src/lib.rs | 1 + 4 files changed, 16 insertions(+) create mode 100644 rust/catalyst-voting/Cargo.toml create mode 100644 rust/catalyst-voting/src/lib.rs diff --git a/.github/workflows/semantic_pull_request.yml b/.github/workflows/semantic_pull_request.yml index 85cd0539ea9..b788d8a508f 100644 --- a/.github/workflows/semantic_pull_request.yml +++ b/.github/workflows/semantic_pull_request.yml @@ -18,6 +18,7 @@ jobs: rust rust/c509-certificate rust/cardano-chain-follower + rust/catalyst-voting rust/cbork rust/hermes-ipfs dart diff --git a/rust/Cargo.toml b/rust/Cargo.toml index 45da0f472dc..930e2fc19cd 100644 --- a/rust/Cargo.toml +++ b/rust/Cargo.toml @@ -7,6 +7,7 @@ members = [ "cbork", "cbork-abnf-parser", "cbork-cddl-parser", + "catalyst-voting", ] [workspace.package] diff --git a/rust/catalyst-voting/Cargo.toml b/rust/catalyst-voting/Cargo.toml new file mode 100644 index 00000000000..c064da2c513 --- /dev/null +++ b/rust/catalyst-voting/Cargo.toml @@ -0,0 +1,13 @@ +[package] +name = "catalyst-voting" +version = "0.1.0" +edition.workspace = true +authors.workspace = true +homepage.workspace = true +repository.workspace = true +license.workspace = true + +[lints] +workspace = true + +[dependencies] diff --git a/rust/catalyst-voting/src/lib.rs b/rust/catalyst-voting/src/lib.rs new file mode 100644 index 00000000000..d207001f1b6 --- /dev/null +++ b/rust/catalyst-voting/src/lib.rs @@ -0,0 +1 @@ +//! Voting primitives which are used among Catalyst ecosystem. From 6935a1b573260f6d35a65f3ea802600eb9347dd5 Mon Sep 17 00:00:00 2001 From: Mr-Leshiy Date: Tue, 24 Sep 2024 14:22:21 +0300 Subject: [PATCH 02/36] add intentionally failed test --- rust/catalyst-voting/src/lib.rs | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/rust/catalyst-voting/src/lib.rs b/rust/catalyst-voting/src/lib.rs index d207001f1b6..62a2608fd67 100644 --- a/rust/catalyst-voting/src/lib.rs +++ b/rust/catalyst-voting/src/lib.rs @@ -1 +1,6 @@ //! Voting primitives which are used among Catalyst ecosystem. + +#[test] +fn test() { + assert!(false); +} \ No newline at end of file From 78f601fb843d1043ded855895aab1849caac6c18 Mon Sep 17 00:00:00 2001 From: Mr-Leshiy Date: Tue, 24 Sep 2024 14:36:56 +0300 Subject: [PATCH 03/36] fix CI --- rust/Earthfile | 6 +----- rust/catalyst-voting/src/lib.rs | 2 +- 2 files changed, 2 insertions(+), 6 deletions(-) diff --git a/rust/Earthfile b/rust/Earthfile index 2a74f564b91..ec7f9946e58 100644 --- a/rust/Earthfile +++ b/rust/Earthfile @@ -29,11 +29,7 @@ build: DO rust-ci+EXECUTE \ --cmd="/scripts/std_build.py" \ --output="release/[^\./]+" \ - --args1="--libs=c509-certificate" \ - --args2="--libs=cardano-chain-follower" \ - --args3="--libs=cbork-cddl-parser" \ - --args4="--libs=cbork-abnf-parser" \ - --args5="--libs=hermes-ipfs" \ + --args1="--libs=c509-certificate --libs=cardano-chain-follower --libs=cbork-cddl-parser --libs=cbork-abnf-parser --libs=hermes-ipfs --libs=catalyst-voting" \ --args6="--bins=cbork/cbork" \ --docs="true" diff --git a/rust/catalyst-voting/src/lib.rs b/rust/catalyst-voting/src/lib.rs index 62a2608fd67..25ce6c07c7d 100644 --- a/rust/catalyst-voting/src/lib.rs +++ b/rust/catalyst-voting/src/lib.rs @@ -3,4 +3,4 @@ #[test] fn test() { assert!(false); -} \ No newline at end of file +} From 9423567039113e5406ee9dc62bdd087a219d37bd Mon Sep 17 00:00:00 2001 From: Mr-Leshiy Date: Tue, 24 Sep 2024 14:38:40 +0300 Subject: [PATCH 04/36] fix --- rust/Earthfile | 11 ++++++++--- 1 file changed, 8 insertions(+), 3 deletions(-) diff --git a/rust/Earthfile b/rust/Earthfile index ec7f9946e58..1dd50692fab 100644 --- a/rust/Earthfile +++ b/rust/Earthfile @@ -12,9 +12,14 @@ builder: DO rust-ci+SETUP COPY Cargo.toml clippy.toml deny.toml rustfmt.toml . - COPY --dir .cargo .config c509-certificate cardano-chain-follower \ - cbork cbork-abnf-parser cbork-cddl-parser \ - hermes-ipfs . + COPY --dir .cargo .config \ + c509-certificate \ + cardano-chain-follower \ + catalyst-voting \ + cbork \ + cbork-abnf-parser \ + cbork-cddl-parser \ + hermes-ipfs . # check : Run basic check. check: From 5d6b95bfedabab661cd3b2346af585626f9311b9 Mon Sep 17 00:00:00 2001 From: Mr-Leshiy Date: Tue, 24 Sep 2024 14:44:27 +0300 Subject: [PATCH 05/36] fix --- rust/catalyst-voting/src/lib.rs | 5 ----- 1 file changed, 5 deletions(-) diff --git a/rust/catalyst-voting/src/lib.rs b/rust/catalyst-voting/src/lib.rs index 25ce6c07c7d..d207001f1b6 100644 --- a/rust/catalyst-voting/src/lib.rs +++ b/rust/catalyst-voting/src/lib.rs @@ -1,6 +1 @@ //! Voting primitives which are used among Catalyst ecosystem. - -#[test] -fn test() { - assert!(false); -} From b8d7ece11a2e7556a2838036dee20be6d7e27ffc Mon Sep 17 00:00:00 2001 From: Mr-Leshiy Date: Tue, 24 Sep 2024 14:52:58 +0300 Subject: [PATCH 06/36] update vscode setting.recommended.json --- .vscode/settings.recommended.json | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/.vscode/settings.recommended.json b/.vscode/settings.recommended.json index 03f0ee4a800..c152c545fde 100644 --- a/.vscode/settings.recommended.json +++ b/.vscode/settings.recommended.json @@ -37,6 +37,7 @@ "rust", "rust/c509-certificate", "rust/cardano-chain-follower", + "rust/catalyst-voting", "rust/cbork", "rust/hermes-ipfs", "dart", @@ -55,6 +56,9 @@ "-c", "cargo lint-vscode" ], + "rust-analyzer.linkedProjects": [ + "./rust/Cargo.toml" + ], "yaml.schemas": { "https://squidfunk.github.io/mkdocs-material/schema.json": "mkdocs.yml" }, From 0803b5093c9d46271ef9280c9de0258fada362ac Mon Sep 17 00:00:00 2001 From: Mr-Leshiy Date: Wed, 25 Sep 2024 14:33:21 +0300 Subject: [PATCH 07/36] add a basic interfaces for the vote part --- rust/catalyst-voting/Cargo.toml | 1 + rust/catalyst-voting/src/lib.rs | 55 +++++++++++++++++++++++++++++++++ 2 files changed, 56 insertions(+) diff --git a/rust/catalyst-voting/Cargo.toml b/rust/catalyst-voting/Cargo.toml index c064da2c513..9dee7f44f40 100644 --- a/rust/catalyst-voting/Cargo.toml +++ b/rust/catalyst-voting/Cargo.toml @@ -11,3 +11,4 @@ license.workspace = true workspace = true [dependencies] +anyhow = "1.0.71" diff --git a/rust/catalyst-voting/src/lib.rs b/rust/catalyst-voting/src/lib.rs index d207001f1b6..5da0b88a5d3 100644 --- a/rust/catalyst-voting/src/lib.rs +++ b/rust/catalyst-voting/src/lib.rs @@ -1 +1,56 @@ //! Voting primitives which are used among Catalyst ecosystem. + +#![allow(dead_code, unused_variables, clippy::todo)] + +/// A representation of the voting choice. +pub struct Vote; + +/// Generate a vote. +/// More detailed described [here](https://input-output-hk.github.io/catalyst-voices/architecture/08_concepts/voting_transaction/crypto/#voting-choice) +/// +/// # Errors +/// - TODO +pub fn vote(vote: usize, voting_options: usize) -> anyhow::Result { + todo!() +} + +/// A respresentation of the encrypted vote. +pub struct EncryptedVote; + +/// Election public key. +pub struct ElectionPublicKey; + +/// Encrypt vote function. +/// More detailed described [here](https://input-output-hk.github.io/catalyst-voices/architecture/08_concepts/voting_transaction/crypto/#vote-encryption) +/// +/// # Errors +/// - TODO +pub fn encrypt_vote(vote: &Vote, election_pk: &ElectionPublicKey) -> anyhow::Result { + todo!() +} + +/// Vote proof struct. +pub struct VoteProof; + +/// Generates a vote proof, which proofs that the given encrypted vote was correctly +/// generated. +/// More detailed described [here](https://input-output-hk.github.io/catalyst-voices/architecture/08_concepts/voting_transaction/crypto/#voters-proof) +/// +/// # Errors +/// - TODO +pub fn generate_vote_proof( + vote: &Vote, encrypted_vote: &EncryptedVote, election_pk: &ElectionPublicKey, +) -> anyhow::Result { + todo!() +} + +/// Verifies a vote proof, is it valid or not for the given encrypted vote. +/// More detailed described [here](https://input-output-hk.github.io/catalyst-voices/architecture/08_concepts/voting_transaction/crypto/#voters-proof) +/// +/// # Errors +/// - TODO +pub fn verify_vote( + vote: &Vote, proof: &VoteProof, election_pk: &ElectionPublicKey, +) -> anyhow::Result<()> { + todo!() +} From 0ea84b3c1ee97d94d614edd686b4f80a5b39ca26 Mon Sep 17 00:00:00 2001 From: Mr-Leshiy Date: Wed, 25 Sep 2024 21:27:40 +0300 Subject: [PATCH 08/36] add basic elgamal encryption based on the ristretto255 group --- rust/catalyst-voting/Cargo.toml | 3 + rust/catalyst-voting/src/crypto/elgamal.rs | 35 +++++++++++ rust/catalyst-voting/src/crypto/group/mod.rs | 4 ++ .../src/crypto/group/ristretto255.rs | 60 +++++++++++++++++++ rust/catalyst-voting/src/crypto/mod.rs | 4 ++ rust/catalyst-voting/src/lib.rs | 2 + 6 files changed, 108 insertions(+) create mode 100644 rust/catalyst-voting/src/crypto/elgamal.rs create mode 100644 rust/catalyst-voting/src/crypto/group/mod.rs create mode 100644 rust/catalyst-voting/src/crypto/group/ristretto255.rs create mode 100644 rust/catalyst-voting/src/crypto/mod.rs diff --git a/rust/catalyst-voting/Cargo.toml b/rust/catalyst-voting/Cargo.toml index 9dee7f44f40..87abc931f91 100644 --- a/rust/catalyst-voting/Cargo.toml +++ b/rust/catalyst-voting/Cargo.toml @@ -12,3 +12,6 @@ workspace = true [dependencies] anyhow = "1.0.71" +thiserror = "1.0.64" +rand_core = "0.6.4" +curve25519-dalek = { version = "4.0" } diff --git a/rust/catalyst-voting/src/crypto/elgamal.rs b/rust/catalyst-voting/src/crypto/elgamal.rs new file mode 100644 index 00000000000..823e0dd866e --- /dev/null +++ b/rust/catalyst-voting/src/crypto/elgamal.rs @@ -0,0 +1,35 @@ +//! Implementation of the lifted ``ElGamal`` cryptosystem, and combine with `ChaCha` +//! stream cipher to produce a hybrid encryption scheme. + +use std::ops::{Add, Mul}; + +use rand_core::CryptoRngCore; + +use super::group::ristretto255::{GroupElement, Scalar}; + +/// ``ElGamal`` secret key. +#[derive(Debug, Clone, PartialEq, Eq)] +pub struct SecretKey(Scalar); + +/// ``ElGamal`` public key. +#[derive(Debug, Clone, Eq, PartialEq)] +pub struct PublicKey(GroupElement); + +/// ``ElGamal`` ciphertext, encrypted message with the public key. +#[derive(Debug, Clone, PartialEq, Eq)] +pub struct Ciphertext(GroupElement, GroupElement); + +/// Given a `message` represented as a `Scalar`, return a ciphertext using the +/// lifted ``ElGamal`` mechanism. +/// Returns a ciphertext of type `Ciphertext` and a used randomness. +pub fn encrypt( + message: &Scalar, public_key: &PublicKey, rng: &mut R, +) -> (Ciphertext, Scalar) { + let r = Scalar::random(rng); + let e1 = GroupElement::GENERATOR.mul(&r); + + let a = GroupElement::GENERATOR.mul(message); + let b = public_key.0.mul(&r); + let e2 = a.add(&b); + (Ciphertext(e1, e2), r) +} diff --git a/rust/catalyst-voting/src/crypto/group/mod.rs b/rust/catalyst-voting/src/crypto/group/mod.rs new file mode 100644 index 00000000000..25e4aac87a3 --- /dev/null +++ b/rust/catalyst-voting/src/crypto/group/mod.rs @@ -0,0 +1,4 @@ +//! Group definitions used in voting protocol. +//! For more information, see: + +pub mod ristretto255; diff --git a/rust/catalyst-voting/src/crypto/group/ristretto255.rs b/rust/catalyst-voting/src/crypto/group/ristretto255.rs new file mode 100644 index 00000000000..a16fab59d14 --- /dev/null +++ b/rust/catalyst-voting/src/crypto/group/ristretto255.rs @@ -0,0 +1,60 @@ +//! ristretto255 group implementation. + +use std::ops::{Add, Mul}; + +use curve25519_dalek::{ + constants::{RISTRETTO_BASEPOINT_POINT, RISTRETTO_BASEPOINT_TABLE}, + ristretto::RistrettoPoint as Point, + scalar::Scalar as IScalar, +}; +use rand_core::CryptoRngCore; + +/// Ristretto group scalar. +#[derive(Debug, Clone, PartialEq, Eq)] +pub struct Scalar(IScalar); + +/// Ristretto group element. +#[derive(Debug, Clone, PartialEq, Eq)] +pub struct GroupElement(Point); + +impl GroupElement { + /// ristretto255 group generator. + pub const GENERATOR: GroupElement = GroupElement(RISTRETTO_BASEPOINT_POINT); +} + +impl Scalar { + /// Generate a random scalar value from the random number generator. + pub fn random(rng: &mut R) -> Self { + let mut scalar_bytes = [0u8; 64]; + rng.fill_bytes(&mut scalar_bytes); + Scalar(IScalar::from_bytes_mod_order_wide(&scalar_bytes)) + } +} + +impl Mul<&GroupElement> for &Scalar { + type Output = GroupElement; + + fn mul(self, other: &GroupElement) -> GroupElement { + other * self + } +} + +impl Mul<&Scalar> for &GroupElement { + type Output = GroupElement; + + fn mul(self, other: &Scalar) -> GroupElement { + if self.0 == RISTRETTO_BASEPOINT_POINT { + GroupElement(RISTRETTO_BASEPOINT_TABLE * &other.0) + } else { + GroupElement(other.0 * self.0) + } + } +} + +impl Add<&GroupElement> for &GroupElement { + type Output = GroupElement; + + fn add(self, other: &GroupElement) -> GroupElement { + GroupElement(self.0 + other.0) + } +} diff --git a/rust/catalyst-voting/src/crypto/mod.rs b/rust/catalyst-voting/src/crypto/mod.rs new file mode 100644 index 00000000000..d168772ac0a --- /dev/null +++ b/rust/catalyst-voting/src/crypto/mod.rs @@ -0,0 +1,4 @@ +//! Crypto primitives which are used by voting protocol. + +mod elgamal; +mod group; diff --git a/rust/catalyst-voting/src/lib.rs b/rust/catalyst-voting/src/lib.rs index 5da0b88a5d3..83cbaf37e88 100644 --- a/rust/catalyst-voting/src/lib.rs +++ b/rust/catalyst-voting/src/lib.rs @@ -2,6 +2,8 @@ #![allow(dead_code, unused_variables, clippy::todo)] +mod crypto; + /// A representation of the voting choice. pub struct Vote; From 91106ffba7631170b2c8616f30847f2b86cc4602 Mon Sep 17 00:00:00 2001 From: Mr-Leshiy Date: Thu, 26 Sep 2024 11:40:07 +0300 Subject: [PATCH 09/36] add arithmetic tests for ristretto255 --- rust/catalyst-voting/Cargo.toml | 3 + .../src/crypto/group/ristretto255.rs | 99 ++++++++++++++++++- 2 files changed, 99 insertions(+), 3 deletions(-) diff --git a/rust/catalyst-voting/Cargo.toml b/rust/catalyst-voting/Cargo.toml index 87abc931f91..6cde59a6627 100644 --- a/rust/catalyst-voting/Cargo.toml +++ b/rust/catalyst-voting/Cargo.toml @@ -15,3 +15,6 @@ anyhow = "1.0.71" thiserror = "1.0.64" rand_core = "0.6.4" curve25519-dalek = { version = "4.0" } + +[dev-dependencies] +proptest = {version = "1.5.0", features = ["attr-macro"] } diff --git a/rust/catalyst-voting/src/crypto/group/ristretto255.rs b/rust/catalyst-voting/src/crypto/group/ristretto255.rs index a16fab59d14..3fae0081cbd 100644 --- a/rust/catalyst-voting/src/crypto/group/ristretto255.rs +++ b/rust/catalyst-voting/src/crypto/group/ristretto255.rs @@ -6,6 +6,7 @@ use curve25519_dalek::{ constants::{RISTRETTO_BASEPOINT_POINT, RISTRETTO_BASEPOINT_TABLE}, ristretto::RistrettoPoint as Point, scalar::Scalar as IScalar, + traits::Identity, }; use rand_core::CryptoRngCore; @@ -17,9 +18,10 @@ pub struct Scalar(IScalar); #[derive(Debug, Clone, PartialEq, Eq)] pub struct GroupElement(Point); -impl GroupElement { - /// ristretto255 group generator. - pub const GENERATOR: GroupElement = GroupElement(RISTRETTO_BASEPOINT_POINT); +impl From for Scalar { + fn from(value: u64) -> Self { + Scalar(IScalar::from(value)) + } } impl Scalar { @@ -29,8 +31,35 @@ impl Scalar { rng.fill_bytes(&mut scalar_bytes); Scalar(IScalar::from_bytes_mod_order_wide(&scalar_bytes)) } + + /// additive identity + pub fn zero() -> Self { + Scalar(IScalar::ZERO) + } + + /// multiplicative identity + pub fn one() -> Self { + Scalar(IScalar::ONE) + } + + /// multiplicative inverse value, like `1 / Scalar`. + pub fn inverse(&self) -> Scalar { + Scalar(self.0.invert()) + } +} + +impl GroupElement { + /// ristretto255 group generator. + pub const GENERATOR: GroupElement = GroupElement(RISTRETTO_BASEPOINT_POINT); + + /// Generate a zero group element. + pub fn zero() -> Self { + GroupElement(Point::identity()) + } } +// `std::ops` traits implementations + impl Mul<&GroupElement> for &Scalar { type Output = GroupElement; @@ -51,6 +80,14 @@ impl Mul<&Scalar> for &GroupElement { } } +impl Mul<&Scalar> for &Scalar { + type Output = Scalar; + + fn mul(self, other: &Scalar) -> Scalar { + Scalar(self.0 * other.0) + } +} + impl Add<&GroupElement> for &GroupElement { type Output = GroupElement; @@ -58,3 +95,59 @@ impl Add<&GroupElement> for &GroupElement { GroupElement(self.0 + other.0) } } + +impl Add<&Scalar> for &Scalar { + type Output = Scalar; + + fn add(self, other: &Scalar) -> Scalar { + Scalar(self.0 + other.0) + } +} + +#[cfg(test)] +mod tests { + use proptest::{ + arbitrary::any, + prelude::{Arbitrary, BoxedStrategy, Strategy}, + property_test, + }; + + use super::*; + + impl Arbitrary for Scalar { + type Parameters = (); + type Strategy = BoxedStrategy; + + fn arbitrary_with((): Self::Parameters) -> Self::Strategy { + any::().prop_map(Scalar::from).boxed() + } + } + + #[property_test] + fn add_zero(fe: Scalar) { + let ge = GroupElement::GENERATOR.mul(&fe); + assert_eq!(GroupElement::zero().add(&ge), ge); + } + + #[property_test] + fn associative(fe1: Scalar, fe2: Scalar) { + let fe3 = fe1.add(&fe2); + + let ge1 = GroupElement::GENERATOR.mul(&fe1); + let ge2 = GroupElement::GENERATOR.mul(&fe2); + let ge3 = GroupElement::GENERATOR.mul(&fe3); + + let ge3_got = ge1.add(&ge2); + + assert_eq!(fe3, fe1.add(&fe2)); + assert_eq!(ge3_got, ge3); + } + + #[property_test] + fn inverse(fe1: Scalar) { + let g = GroupElement::GENERATOR.mul(&fe1).mul(&fe1.inverse()); + + assert_eq!(fe1.mul(&fe1.inverse()), Scalar::one()); + assert_eq!(g, GroupElement::GENERATOR); + } +} From e1b82516bb7beaf6c2fbc0379693dcf0fccc99f0 Mon Sep 17 00:00:00 2001 From: Mr-Leshiy Date: Thu, 26 Sep 2024 12:04:16 +0300 Subject: [PATCH 10/36] fix tests --- .../src/crypto/group/ristretto255.rs | 46 ++++++++++++++++++- 1 file changed, 44 insertions(+), 2 deletions(-) diff --git a/rust/catalyst-voting/src/crypto/group/ristretto255.rs b/rust/catalyst-voting/src/crypto/group/ristretto255.rs index 3fae0081cbd..1129b65a18b 100644 --- a/rust/catalyst-voting/src/crypto/group/ristretto255.rs +++ b/rust/catalyst-voting/src/crypto/group/ristretto255.rs @@ -1,6 +1,6 @@ //! ristretto255 group implementation. -use std::ops::{Add, Mul}; +use std::ops::{Add, Mul, Sub}; use curve25519_dalek::{ constants::{RISTRETTO_BASEPOINT_POINT, RISTRETTO_BASEPOINT_TABLE}, @@ -42,6 +42,11 @@ impl Scalar { Scalar(IScalar::ONE) } + /// negative value + pub fn negate(&self) -> Self { + Scalar(-self.0) + } + /// multiplicative inverse value, like `1 / Scalar`. pub fn inverse(&self) -> Scalar { Scalar(self.0.invert()) @@ -104,6 +109,14 @@ impl Add<&Scalar> for &Scalar { } } +impl Sub<&Scalar> for &Scalar { + type Output = Scalar; + + fn sub(self, other: &Scalar) -> Scalar { + Scalar(self.0 - other.0) + } +} + #[cfg(test)] mod tests { use proptest::{ @@ -126,6 +139,8 @@ mod tests { #[property_test] fn add_zero(fe: Scalar) { let ge = GroupElement::GENERATOR.mul(&fe); + + assert_eq!(fe.add(&Scalar::zero()), fe); assert_eq!(GroupElement::zero().add(&ge), ge); } @@ -139,7 +154,7 @@ mod tests { let ge3_got = ge1.add(&ge2); - assert_eq!(fe3, fe1.add(&fe2)); + assert_eq!(fe3, fe2.add(&fe1)); assert_eq!(ge3_got, ge3); } @@ -150,4 +165,31 @@ mod tests { assert_eq!(fe1.mul(&fe1.inverse()), Scalar::one()); assert_eq!(g, GroupElement::GENERATOR); } + + #[property_test] + fn scalar_arithmetic_tests(e1: Scalar, e2: Scalar, e3: Scalar) { + assert_eq!(&(&e1 + &e2) + &e3, &e1 + &(&e2 + &e3)); + assert_eq!(&e1 + &e2, &e2 + &e1); + assert_eq!(&e1 + &Scalar::zero(), e1.clone()); + assert_eq!(&e1 * &Scalar::one(), e1.clone()); + assert_eq!(&e1 * &e1.inverse(), Scalar::one()); + assert_eq!(&e1 + &e1.negate(), Scalar::zero()); + assert_eq!(&(&e1 - &e2) + &e2, e1.clone()); + assert_eq!(&(&e1 + &e2) * &e3, &(&e1 * &e3) + &(&e2 * &e3)); + } + + #[property_test] + fn group_element_arithmetic_tests(e1: Scalar, e2: Scalar) { + let ge = GroupElement::GENERATOR.mul(&e1); + assert_eq!(&GroupElement::zero() + &ge, ge); + + let ge1 = GroupElement::GENERATOR.mul(&e1); + let ge2 = GroupElement::GENERATOR.mul(&e2); + let ge3 = GroupElement::GENERATOR.mul(&(&e1 + &e2)); + + assert_eq!(&ge1 + &ge2, ge3); + + let ge = GroupElement::GENERATOR.mul(&e1).mul(&e1.inverse()); + assert_eq!(ge, GroupElement::GENERATOR); + } } From 6c2d9610dbd8213ce5fe25709d2d15e033efda69 Mon Sep 17 00:00:00 2001 From: Mr-Leshiy Date: Thu, 26 Sep 2024 12:50:43 +0300 Subject: [PATCH 11/36] wip --- rust/catalyst-voting/src/crypto/group/mod.rs | 2 + .../src/crypto/group/ristretto255.rs | 30 ------------ .../catalyst-voting/src/crypto/group/utils.rs | 49 +++++++++++++++++++ 3 files changed, 51 insertions(+), 30 deletions(-) create mode 100644 rust/catalyst-voting/src/crypto/group/utils.rs diff --git a/rust/catalyst-voting/src/crypto/group/mod.rs b/rust/catalyst-voting/src/crypto/group/mod.rs index 25e4aac87a3..b484b966ede 100644 --- a/rust/catalyst-voting/src/crypto/group/mod.rs +++ b/rust/catalyst-voting/src/crypto/group/mod.rs @@ -2,3 +2,5 @@ //! For more information, see: pub mod ristretto255; +// Probably it will be used at someday, but for now it is excluded +// mod utils; diff --git a/rust/catalyst-voting/src/crypto/group/ristretto255.rs b/rust/catalyst-voting/src/crypto/group/ristretto255.rs index 1129b65a18b..e248c7314e6 100644 --- a/rust/catalyst-voting/src/crypto/group/ristretto255.rs +++ b/rust/catalyst-voting/src/crypto/group/ristretto255.rs @@ -136,36 +136,6 @@ mod tests { } } - #[property_test] - fn add_zero(fe: Scalar) { - let ge = GroupElement::GENERATOR.mul(&fe); - - assert_eq!(fe.add(&Scalar::zero()), fe); - assert_eq!(GroupElement::zero().add(&ge), ge); - } - - #[property_test] - fn associative(fe1: Scalar, fe2: Scalar) { - let fe3 = fe1.add(&fe2); - - let ge1 = GroupElement::GENERATOR.mul(&fe1); - let ge2 = GroupElement::GENERATOR.mul(&fe2); - let ge3 = GroupElement::GENERATOR.mul(&fe3); - - let ge3_got = ge1.add(&ge2); - - assert_eq!(fe3, fe2.add(&fe1)); - assert_eq!(ge3_got, ge3); - } - - #[property_test] - fn inverse(fe1: Scalar) { - let g = GroupElement::GENERATOR.mul(&fe1).mul(&fe1.inverse()); - - assert_eq!(fe1.mul(&fe1.inverse()), Scalar::one()); - assert_eq!(g, GroupElement::GENERATOR); - } - #[property_test] fn scalar_arithmetic_tests(e1: Scalar, e2: Scalar, e3: Scalar) { assert_eq!(&(&e1 + &e2) + &e3, &e1 + &(&e2 + &e3)); diff --git a/rust/catalyst-voting/src/crypto/group/utils.rs b/rust/catalyst-voting/src/crypto/group/utils.rs new file mode 100644 index 00000000000..cad75596fa8 --- /dev/null +++ b/rust/catalyst-voting/src/crypto/group/utils.rs @@ -0,0 +1,49 @@ +//! Utulity functions and entities + +/// A convinient macro to autogenerate a `std::ops` traits implmentation as `Add`, `Sub` +/// etc. +/// +/// It is required to provide a basic implementation of the `std::ops` for reference +/// types, and then you can call `std_ops_gen` to provide a the remaining implementation +/// E.g. +/// ``` +/// struct YourType; +/// impl Mul<&YourType> for &YourType { +/// type Output = YourType; + +/// fn mul(self, other: &YourType) -> YourType { +/// unimplemented!() +/// } +/// } +/// +/// std_ops_gen!(Add, add, YourType, YourType, YourType); +/// ``` +macro_rules! std_ops_gen { + ($class: ident, $f: ident, $rty: ident, $lty: ident, $out: ident) => { + impl $class<$rty> for &$lty { + type Output = $out; + + fn $f(self, other: $rty) -> Self::Output { + self.$f(&other) + } + } + + impl $class<&$rty> for $lty { + type Output = $out; + + fn $f(self, other: &$rty) -> Self::Output { + (&self).$f(other) + } + } + + impl $class<$rty> for $lty { + type Output = $out; + + fn $f(self, other: $rty) -> Self::Output { + (&self).$f(&other) + } + } + }; +} + +pub(crate) use std_ops_gen; From 594d11483b48c2e4df4c86cef591e06743ce17c3 Mon Sep 17 00:00:00 2001 From: Mr-Leshiy Date: Thu, 26 Sep 2024 13:20:00 +0300 Subject: [PATCH 12/36] add decryption algorithm, add tests --- rust/catalyst-voting/src/crypto/elgamal.rs | 70 ++++++++++++++++---- rust/catalyst-voting/src/crypto/group/mod.rs | 5 +- 2 files changed, 61 insertions(+), 14 deletions(-) diff --git a/rust/catalyst-voting/src/crypto/elgamal.rs b/rust/catalyst-voting/src/crypto/elgamal.rs index 823e0dd866e..1a437f3304a 100644 --- a/rust/catalyst-voting/src/crypto/elgamal.rs +++ b/rust/catalyst-voting/src/crypto/elgamal.rs @@ -1,11 +1,11 @@ //! Implementation of the lifted ``ElGamal`` cryptosystem, and combine with `ChaCha` //! stream cipher to produce a hybrid encryption scheme. -use std::ops::{Add, Mul}; +use std::ops::Mul; use rand_core::CryptoRngCore; -use super::group::ristretto255::{GroupElement, Scalar}; +use super::group::{GroupElement, Scalar}; /// ``ElGamal`` secret key. #[derive(Debug, Clone, PartialEq, Eq)] @@ -19,17 +19,61 @@ pub struct PublicKey(GroupElement); #[derive(Debug, Clone, PartialEq, Eq)] pub struct Ciphertext(GroupElement, GroupElement); +impl SecretKey { + /// Generate a random `SecretKey` value from the random number generator. + pub fn random(rng: &mut R) -> Self { + Self(Scalar::random(rng)) + } + + /// Generate a corresponding `PublicKey`. + pub fn public_key(&self) -> PublicKey { + PublicKey(GroupElement::GENERATOR.mul(&self.0)) + } +} + /// Given a `message` represented as a `Scalar`, return a ciphertext using the /// lifted ``ElGamal`` mechanism. -/// Returns a ciphertext of type `Ciphertext` and a used randomness. -pub fn encrypt( - message: &Scalar, public_key: &PublicKey, rng: &mut R, -) -> (Ciphertext, Scalar) { - let r = Scalar::random(rng); - let e1 = GroupElement::GENERATOR.mul(&r); - - let a = GroupElement::GENERATOR.mul(message); - let b = public_key.0.mul(&r); - let e2 = a.add(&b); - (Ciphertext(e1, e2), r) +/// Returns a ciphertext of type `Ciphertext`. +pub fn encrypt(message: &Scalar, public_key: &PublicKey, randomness: &Scalar) -> Ciphertext { + let e1 = GroupElement::GENERATOR.mul(randomness); + let e2 = &GroupElement::GENERATOR.mul(message) + &public_key.0.mul(randomness); + Ciphertext(e1, e2) +} + +/// Decrypt ``ElGamal`` `Ciphertext`, returns the original message respresented as a +/// `GroupElement`. +pub fn decrypt(cipher: &Ciphertext, secret_key: &SecretKey) -> GroupElement { + &(&cipher.0 * &secret_key.0.negate()) + &cipher.1 +} + +#[cfg(test)] +mod tests { + use proptest::{ + arbitrary::any, + prelude::{Arbitrary, BoxedStrategy, Strategy}, + property_test, + }; + + use super::*; + + impl Arbitrary for SecretKey { + type Parameters = (); + type Strategy = BoxedStrategy; + + fn arbitrary_with((): Self::Parameters) -> Self::Strategy { + any::().prop_map(SecretKey).boxed() + } + } + + #[property_test] + fn elgalam_encryption_decryption_test( + secret_key: SecretKey, message: Scalar, randomness: Scalar, + ) { + let public_key = secret_key.public_key(); + + let cipher = encrypt(&message, &public_key, &randomness); + let decrypted = decrypt(&cipher, &secret_key); + + assert_eq!(decrypted, GroupElement::GENERATOR.mul(&message)); + } } diff --git a/rust/catalyst-voting/src/crypto/group/mod.rs b/rust/catalyst-voting/src/crypto/group/mod.rs index b484b966ede..c27928ba83f 100644 --- a/rust/catalyst-voting/src/crypto/group/mod.rs +++ b/rust/catalyst-voting/src/crypto/group/mod.rs @@ -1,6 +1,9 @@ //! Group definitions used in voting protocol. //! For more information, see: -pub mod ristretto255; +mod ristretto255; // Probably it will be used at someday, but for now it is excluded // mod utils; + +#[allow(clippy::module_name_repetitions)] +pub use ristretto255::{GroupElement, Scalar}; From c1b749d3d7454d28fd5b7a546036f89c22ae5343 Mon Sep 17 00:00:00 2001 From: Mr-Leshiy Date: Thu, 26 Sep 2024 15:34:34 +0300 Subject: [PATCH 13/36] fix CI --- .config/dictionaries/project.dic | 3 +++ rust/catalyst-voting/Cargo.toml | 1 - rust/catalyst-voting/src/crypto/elgamal.rs | 6 +++--- rust/catalyst-voting/src/crypto/group/ristretto255.rs | 2 ++ rust/catalyst-voting/src/crypto/group/utils.rs | 4 ++-- rust/catalyst-voting/src/lib.rs | 2 +- 6 files changed, 11 insertions(+), 7 deletions(-) diff --git a/.config/dictionaries/project.dic b/.config/dictionaries/project.dic index 36d32de2f4f..303a0a748b9 100644 --- a/.config/dictionaries/project.dic +++ b/.config/dictionaries/project.dic @@ -57,6 +57,7 @@ dotglob drep dreps Earthfile +elgamal encryptor Errno Eternl @@ -171,6 +172,7 @@ preopened preopens preprod Prokhorenko +proptest psql pubk pubkey @@ -188,6 +190,7 @@ Replayability Repr reqwest retriggering +ristretto rlib rulelist RULENAME diff --git a/rust/catalyst-voting/Cargo.toml b/rust/catalyst-voting/Cargo.toml index 6cde59a6627..ecb47fecc26 100644 --- a/rust/catalyst-voting/Cargo.toml +++ b/rust/catalyst-voting/Cargo.toml @@ -12,7 +12,6 @@ workspace = true [dependencies] anyhow = "1.0.71" -thiserror = "1.0.64" rand_core = "0.6.4" curve25519-dalek = { version = "4.0" } diff --git a/rust/catalyst-voting/src/crypto/elgamal.rs b/rust/catalyst-voting/src/crypto/elgamal.rs index 1a437f3304a..ea7669823c2 100644 --- a/rust/catalyst-voting/src/crypto/elgamal.rs +++ b/rust/catalyst-voting/src/crypto/elgamal.rs @@ -1,4 +1,4 @@ -//! Implementation of the lifted ``ElGamal`` cryptosystem, and combine with `ChaCha` +//! Implementation of the lifted ``ElGamal`` crypto system, and combine with `ChaCha` //! stream cipher to produce a hybrid encryption scheme. use std::ops::Mul; @@ -40,7 +40,7 @@ pub fn encrypt(message: &Scalar, public_key: &PublicKey, randomness: &Scalar) -> Ciphertext(e1, e2) } -/// Decrypt ``ElGamal`` `Ciphertext`, returns the original message respresented as a +/// Decrypt ``ElGamal`` `Ciphertext`, returns the original message represented as a /// `GroupElement`. pub fn decrypt(cipher: &Ciphertext, secret_key: &SecretKey) -> GroupElement { &(&cipher.0 * &secret_key.0.negate()) + &cipher.1 @@ -66,7 +66,7 @@ mod tests { } #[property_test] - fn elgalam_encryption_decryption_test( + fn elgamal_encryption_decryption_test( secret_key: SecretKey, message: Scalar, randomness: Scalar, ) { let public_key = secret_key.public_key(); diff --git a/rust/catalyst-voting/src/crypto/group/ristretto255.rs b/rust/catalyst-voting/src/crypto/group/ristretto255.rs index e248c7314e6..aa8b9df54ba 100644 --- a/rust/catalyst-voting/src/crypto/group/ristretto255.rs +++ b/rust/catalyst-voting/src/crypto/group/ristretto255.rs @@ -1,5 +1,7 @@ //! ristretto255 group implementation. +// cspell: words BASEPOINT + use std::ops::{Add, Mul, Sub}; use curve25519_dalek::{ diff --git a/rust/catalyst-voting/src/crypto/group/utils.rs b/rust/catalyst-voting/src/crypto/group/utils.rs index cad75596fa8..f13197925fa 100644 --- a/rust/catalyst-voting/src/crypto/group/utils.rs +++ b/rust/catalyst-voting/src/crypto/group/utils.rs @@ -1,6 +1,6 @@ -//! Utulity functions and entities +//! Utility functions and entities -/// A convinient macro to autogenerate a `std::ops` traits implmentation as `Add`, `Sub` +/// A convenient macro to autogenerate a `std::ops` traits implementation as `Add`, `Sub` /// etc. /// /// It is required to provide a basic implementation of the `std::ops` for reference diff --git a/rust/catalyst-voting/src/lib.rs b/rust/catalyst-voting/src/lib.rs index 83cbaf37e88..63f24bcaadf 100644 --- a/rust/catalyst-voting/src/lib.rs +++ b/rust/catalyst-voting/src/lib.rs @@ -16,7 +16,7 @@ pub fn vote(vote: usize, voting_options: usize) -> anyhow::Result { todo!() } -/// A respresentation of the encrypted vote. +/// A representation of the encrypted vote. pub struct EncryptedVote; /// Election public key. From 0a5bb99dcdd5462a803f9110da82b83403152880 Mon Sep 17 00:00:00 2001 From: Mr-Leshiy Date: Thu, 26 Sep 2024 16:03:28 +0300 Subject: [PATCH 14/36] remove unused std_ops_gen --- rust/catalyst-voting/src/crypto/group/mod.rs | 2 - .../catalyst-voting/src/crypto/group/utils.rs | 49 ------------------- 2 files changed, 51 deletions(-) delete mode 100644 rust/catalyst-voting/src/crypto/group/utils.rs diff --git a/rust/catalyst-voting/src/crypto/group/mod.rs b/rust/catalyst-voting/src/crypto/group/mod.rs index c27928ba83f..41c8c61592f 100644 --- a/rust/catalyst-voting/src/crypto/group/mod.rs +++ b/rust/catalyst-voting/src/crypto/group/mod.rs @@ -2,8 +2,6 @@ //! For more information, see: mod ristretto255; -// Probably it will be used at someday, but for now it is excluded -// mod utils; #[allow(clippy::module_name_repetitions)] pub use ristretto255::{GroupElement, Scalar}; diff --git a/rust/catalyst-voting/src/crypto/group/utils.rs b/rust/catalyst-voting/src/crypto/group/utils.rs deleted file mode 100644 index f13197925fa..00000000000 --- a/rust/catalyst-voting/src/crypto/group/utils.rs +++ /dev/null @@ -1,49 +0,0 @@ -//! Utility functions and entities - -/// A convenient macro to autogenerate a `std::ops` traits implementation as `Add`, `Sub` -/// etc. -/// -/// It is required to provide a basic implementation of the `std::ops` for reference -/// types, and then you can call `std_ops_gen` to provide a the remaining implementation -/// E.g. -/// ``` -/// struct YourType; -/// impl Mul<&YourType> for &YourType { -/// type Output = YourType; - -/// fn mul(self, other: &YourType) -> YourType { -/// unimplemented!() -/// } -/// } -/// -/// std_ops_gen!(Add, add, YourType, YourType, YourType); -/// ``` -macro_rules! std_ops_gen { - ($class: ident, $f: ident, $rty: ident, $lty: ident, $out: ident) => { - impl $class<$rty> for &$lty { - type Output = $out; - - fn $f(self, other: $rty) -> Self::Output { - self.$f(&other) - } - } - - impl $class<&$rty> for $lty { - type Output = $out; - - fn $f(self, other: &$rty) -> Self::Output { - (&self).$f(other) - } - } - - impl $class<$rty> for $lty { - type Output = $out; - - fn $f(self, other: $rty) -> Self::Output { - (&self).$f(&other) - } - } - }; -} - -pub(crate) use std_ops_gen; From 0348b6c0209e9d590ba3a5cdcf46f7f8a585466f Mon Sep 17 00:00:00 2001 From: Mr-Leshiy Date: Fri, 27 Sep 2024 12:43:16 +0300 Subject: [PATCH 15/36] add new voter module --- rust/catalyst-voting/Cargo.toml | 7 +- rust/catalyst-voting/src/crypto/elgamal.rs | 4 +- .../src/crypto/group/ristretto255.rs | 6 +- rust/catalyst-voting/src/crypto/mod.rs | 4 +- rust/catalyst-voting/src/lib.rs | 29 ++-- rust/catalyst-voting/src/voter.rs | 144 ++++++++++++++++++ 6 files changed, 172 insertions(+), 22 deletions(-) create mode 100644 rust/catalyst-voting/src/voter.rs diff --git a/rust/catalyst-voting/Cargo.toml b/rust/catalyst-voting/Cargo.toml index ecb47fecc26..c2ec52bcb10 100644 --- a/rust/catalyst-voting/Cargo.toml +++ b/rust/catalyst-voting/Cargo.toml @@ -12,8 +12,13 @@ workspace = true [dependencies] anyhow = "1.0.71" +thiserror = "1.0.56" rand_core = "0.6.4" curve25519-dalek = { version = "4.0" } +rayon = "1.10.0" [dev-dependencies] -proptest = {version = "1.5.0", features = ["attr-macro"] } +proptest = {version = "1.5.0" } +# Potentially it could be replaced with using `proptest::property_test` atribute macro, +# after this PR will be merged https://github.com/proptest-rs/proptest/pull/523 +test-strategy = "0.4.0" diff --git a/rust/catalyst-voting/src/crypto/elgamal.rs b/rust/catalyst-voting/src/crypto/elgamal.rs index ea7669823c2..a8eb85f014f 100644 --- a/rust/catalyst-voting/src/crypto/elgamal.rs +++ b/rust/catalyst-voting/src/crypto/elgamal.rs @@ -51,8 +51,8 @@ mod tests { use proptest::{ arbitrary::any, prelude::{Arbitrary, BoxedStrategy, Strategy}, - property_test, }; + use test_strategy::proptest; use super::*; @@ -65,7 +65,7 @@ mod tests { } } - #[property_test] + #[proptest] fn elgamal_encryption_decryption_test( secret_key: SecretKey, message: Scalar, randomness: Scalar, ) { diff --git a/rust/catalyst-voting/src/crypto/group/ristretto255.rs b/rust/catalyst-voting/src/crypto/group/ristretto255.rs index aa8b9df54ba..641c3e1d5ea 100644 --- a/rust/catalyst-voting/src/crypto/group/ristretto255.rs +++ b/rust/catalyst-voting/src/crypto/group/ristretto255.rs @@ -124,8 +124,8 @@ mod tests { use proptest::{ arbitrary::any, prelude::{Arbitrary, BoxedStrategy, Strategy}, - property_test, }; + use test_strategy::proptest; use super::*; @@ -138,7 +138,7 @@ mod tests { } } - #[property_test] + #[proptest] fn scalar_arithmetic_tests(e1: Scalar, e2: Scalar, e3: Scalar) { assert_eq!(&(&e1 + &e2) + &e3, &e1 + &(&e2 + &e3)); assert_eq!(&e1 + &e2, &e2 + &e1); @@ -150,7 +150,7 @@ mod tests { assert_eq!(&(&e1 + &e2) * &e3, &(&e1 * &e3) + &(&e2 * &e3)); } - #[property_test] + #[proptest] fn group_element_arithmetic_tests(e1: Scalar, e2: Scalar) { let ge = GroupElement::GENERATOR.mul(&e1); assert_eq!(&GroupElement::zero() + &ge, ge); diff --git a/rust/catalyst-voting/src/crypto/mod.rs b/rust/catalyst-voting/src/crypto/mod.rs index d168772ac0a..16822f6efed 100644 --- a/rust/catalyst-voting/src/crypto/mod.rs +++ b/rust/catalyst-voting/src/crypto/mod.rs @@ -1,4 +1,4 @@ //! Crypto primitives which are used by voting protocol. -mod elgamal; -mod group; +pub(crate) mod elgamal; +pub(crate) mod group; diff --git a/rust/catalyst-voting/src/lib.rs b/rust/catalyst-voting/src/lib.rs index 63f24bcaadf..c81efedce83 100644 --- a/rust/catalyst-voting/src/lib.rs +++ b/rust/catalyst-voting/src/lib.rs @@ -2,33 +2,34 @@ #![allow(dead_code, unused_variables, clippy::todo)] +use voter::{ + EncryptedVote, EncryptionRandomness, IncorrectChoiceError, IncorrectRandomnessLengthError, Vote, +}; + mod crypto; +pub mod voter; -/// A representation of the voting choice. -pub struct Vote; +/// Election public key. +pub type ElectionPublicKey = crypto::elgamal::PublicKey; /// Generate a vote. /// More detailed described [here](https://input-output-hk.github.io/catalyst-voices/architecture/08_concepts/voting_transaction/crypto/#voting-choice) /// /// # Errors -/// - TODO -pub fn vote(vote: usize, voting_options: usize) -> anyhow::Result { - todo!() +/// - `voter::IncorrectChoiceError` +pub fn vote(choice: usize, voting_options: usize) -> Result { + Vote::new(choice, voting_options) } -/// A representation of the encrypted vote. -pub struct EncryptedVote; - -/// Election public key. -pub struct ElectionPublicKey; - /// Encrypt vote function. /// More detailed described [here](https://input-output-hk.github.io/catalyst-voices/architecture/08_concepts/voting_transaction/crypto/#vote-encryption) /// /// # Errors -/// - TODO -pub fn encrypt_vote(vote: &Vote, election_pk: &ElectionPublicKey) -> anyhow::Result { - todo!() +/// - `voter::IncorrectRandomnessLengthError` +pub fn encrypt_vote( + vote: &Vote, election_pk: &ElectionPublicKey, randomness: &EncryptionRandomness, +) -> Result { + vote.encrypt(election_pk, randomness) } /// Vote proof struct. diff --git a/rust/catalyst-voting/src/voter.rs b/rust/catalyst-voting/src/voter.rs new file mode 100644 index 00000000000..ebde2bed8a0 --- /dev/null +++ b/rust/catalyst-voting/src/voter.rs @@ -0,0 +1,144 @@ +//! Module containing all primitives related to the voter. + +use rayon::iter::{IndexedParallelIterator, IntoParallelRefIterator, ParallelIterator}; + +use crate::crypto::{ + elgamal::{encrypt, Ciphertext, PublicKey}, + group::Scalar, +}; + +/// A representation of the voter's voting choice. +/// Represented as a Unit vector which size is `voting_options` +/// and the `choice` value is the index of the unit vector component equals to `1`, +/// and other components equal to `0`. +pub struct Vote { + /// Voter's voting choice. + choice: usize, + /// Number of voting options. + voting_options: usize, +} + +/// A representation of the encrypted vote. +pub struct EncryptedVote(Vec); + +/// A representation of the encryption randomness, used to encrypt the vote. +pub struct EncryptionRandomness(Vec); + +/// Incorrect voting choice error +#[derive(thiserror::Error, Debug)] +#[error( + "Invalid voting choice, the value of choice: {0}, should be less than the number of voting options: {1}." +)] +pub struct IncorrectChoiceError(usize, usize); + +/// Incorrect randomness length error +#[derive(thiserror::Error, Debug)] +#[error( + "Invalid randomness length, the length of randomness: {0}, should be equal to the number of voting options: {1}." +)] +pub struct IncorrectRandomnessLengthError(usize, usize); + +impl Vote { + /// Generate a vote. + /// More detailed described [here](https://input-output-hk.github.io/catalyst-voices/architecture/08_concepts/voting_transaction/crypto/#voting-choice) + /// + /// # Errors + /// - `IncorrectChoiceError` + pub(crate) fn new(choice: usize, voting_options: usize) -> Result { + if choice >= voting_options { + return Err(IncorrectChoiceError(choice, voting_options)); + } + + Ok(Vote { + choice, + voting_options, + }) + } + + /// Transform the vote into the unit vector. + fn to_unit_vector(&self) -> Vec { + (0..self.voting_options) + .map(|i| { + if i == self.choice { + Scalar::one() + } else { + Scalar::zero() + } + }) + .collect() + } + + /// Create a new encrypted vote from the given vote and public key. + /// More detailed described [here](https://input-output-hk.github.io/catalyst-voices/architecture/08_concepts/voting_transaction/crypto/#vote-encryption) + pub(crate) fn encrypt( + &self, public_key: &PublicKey, randomness: &EncryptionRandomness, + ) -> Result { + if self.voting_options != randomness.0.len() { + return Err(IncorrectRandomnessLengthError( + randomness.0.len(), + self.voting_options, + )); + } + + let unit_vector = self.to_unit_vector(); + let ciphers = unit_vector + .par_iter() + .zip(randomness.0.par_iter()) + .map(|(m, r)| encrypt(m, public_key, r)) + .collect(); + + Ok(EncryptedVote(ciphers)) + } +} + +#[cfg(test)] +mod tests { + use proptest::sample::size_range; + use test_strategy::proptest; + + use super::*; + use crate::crypto::elgamal::SecretKey; + + #[test] + fn vote_test() { + let voting_options = 3; + + let vote = Vote::new(0, voting_options).unwrap(); + assert_eq!(vote.to_unit_vector(), vec![ + Scalar::one(), + Scalar::zero(), + Scalar::zero() + ]); + + let vote = Vote::new(1, voting_options).unwrap(); + assert_eq!(vote.to_unit_vector(), vec![ + Scalar::zero(), + Scalar::one(), + Scalar::zero() + ]); + + let vote = Vote::new(2, voting_options).unwrap(); + assert_eq!(vote.to_unit_vector(), vec![ + Scalar::zero(), + Scalar::zero(), + Scalar::one() + ]); + + assert!(Vote::new(3, voting_options).is_err()); + assert!(Vote::new(4, voting_options).is_err()); + } + + #[proptest] + fn encrypt_test( + secret_key: SecretKey, #[strategy(1..10usize)] voting_options: usize, + #[any(size_range(#voting_options).lift())] randomness: Vec, + ) { + let public_key = secret_key.public_key(); + let vote = Vote::new(0, voting_options).unwrap(); + + let encrypted = vote + .encrypt(&public_key, &EncryptionRandomness(randomness)) + .unwrap(); + assert_eq!(encrypted.0.len(), vote.voting_options); + } +} From 9d9ddd62edd8f48e3b2e8b5025ba93cf3af99e3b Mon Sep 17 00:00:00 2001 From: Mr-Leshiy Date: Fri, 27 Sep 2024 13:42:15 +0300 Subject: [PATCH 16/36] add EncryptionRandomness random generation --- rust/catalyst-voting/src/voter.rs | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/rust/catalyst-voting/src/voter.rs b/rust/catalyst-voting/src/voter.rs index ebde2bed8a0..a086b14dd2a 100644 --- a/rust/catalyst-voting/src/voter.rs +++ b/rust/catalyst-voting/src/voter.rs @@ -1,5 +1,6 @@ //! Module containing all primitives related to the voter. +use rand_core::CryptoRngCore; use rayon::iter::{IndexedParallelIterator, IntoParallelRefIterator, ParallelIterator}; use crate::crypto::{ @@ -38,6 +39,13 @@ pub struct IncorrectChoiceError(usize, usize); )] pub struct IncorrectRandomnessLengthError(usize, usize); +impl EncryptionRandomness { + /// Randomly generate the `EncryptionRandomness`. + pub fn generate(rng: &mut R, voting_options: usize) -> Self { + Self((0..voting_options).map(|_| Scalar::random(rng)).collect()) + } +} + impl Vote { /// Generate a vote. /// More detailed described [here](https://input-output-hk.github.io/catalyst-voices/architecture/08_concepts/voting_transaction/crypto/#voting-choice) From c342a1e996611b0b22a9c454e694a37fce3048dc Mon Sep 17 00:00:00 2001 From: Mr-Leshiy Date: Fri, 27 Sep 2024 18:58:35 +0300 Subject: [PATCH 17/36] add a tally function --- rust/catalyst-voting/src/crypto/elgamal.rs | 46 +++++++++++++-- rust/catalyst-voting/src/lib.rs | 13 ++--- rust/catalyst-voting/src/tally.rs | 67 ++++++++++++++++++++++ rust/catalyst-voting/src/voter.rs | 43 +++++++++----- 4 files changed, 143 insertions(+), 26 deletions(-) create mode 100644 rust/catalyst-voting/src/tally.rs diff --git a/rust/catalyst-voting/src/crypto/elgamal.rs b/rust/catalyst-voting/src/crypto/elgamal.rs index a8eb85f014f..d845c95f6c8 100644 --- a/rust/catalyst-voting/src/crypto/elgamal.rs +++ b/rust/catalyst-voting/src/crypto/elgamal.rs @@ -1,7 +1,7 @@ //! Implementation of the lifted ``ElGamal`` crypto system, and combine with `ChaCha` //! stream cipher to produce a hybrid encryption scheme. -use std::ops::Mul; +use std::ops::{Add, Mul}; use rand_core::CryptoRngCore; @@ -17,7 +17,7 @@ pub struct PublicKey(GroupElement); /// ``ElGamal`` ciphertext, encrypted message with the public key. #[derive(Debug, Clone, PartialEq, Eq)] -pub struct Ciphertext(GroupElement, GroupElement); +pub struct Ciphertext(pub(crate) GroupElement, pub(crate) GroupElement); impl SecretKey { /// Generate a random `SecretKey` value from the random number generator. @@ -34,7 +34,7 @@ impl SecretKey { /// Given a `message` represented as a `Scalar`, return a ciphertext using the /// lifted ``ElGamal`` mechanism. /// Returns a ciphertext of type `Ciphertext`. -pub fn encrypt(message: &Scalar, public_key: &PublicKey, randomness: &Scalar) -> Ciphertext { +pub(crate) fn encrypt(message: &Scalar, public_key: &PublicKey, randomness: &Scalar) -> Ciphertext { let e1 = GroupElement::GENERATOR.mul(randomness); let e2 = &GroupElement::GENERATOR.mul(message) + &public_key.0.mul(randomness); Ciphertext(e1, e2) @@ -42,10 +42,26 @@ pub fn encrypt(message: &Scalar, public_key: &PublicKey, randomness: &Scalar) -> /// Decrypt ``ElGamal`` `Ciphertext`, returns the original message represented as a /// `GroupElement`. -pub fn decrypt(cipher: &Ciphertext, secret_key: &SecretKey) -> GroupElement { +pub(crate) fn decrypt(cipher: &Ciphertext, secret_key: &SecretKey) -> GroupElement { &(&cipher.0 * &secret_key.0.negate()) + &cipher.1 } +impl Mul<&Scalar> for &Ciphertext { + type Output = Ciphertext; + + fn mul(self, rhs: &Scalar) -> Self::Output { + Ciphertext(&self.0 * rhs, &self.1 * rhs) + } +} + +impl Add<&Ciphertext> for &Ciphertext { + type Output = Ciphertext; + + fn add(self, rhs: &Ciphertext) -> Self::Output { + Ciphertext(&self.0 + &rhs.0, &self.1 + &rhs.1) + } +} + #[cfg(test)] mod tests { use proptest::{ @@ -65,6 +81,28 @@ mod tests { } } + #[proptest] + fn ciphertext_add_test(e1: Scalar, e2: Scalar, e3: Scalar, e4: Scalar) { + let g1 = GroupElement::GENERATOR.mul(&e1); + let g2 = GroupElement::GENERATOR.mul(&e2); + let c1 = Ciphertext(g1.clone(), g2.clone()); + + let g3 = GroupElement::GENERATOR.mul(&e3); + let g4 = GroupElement::GENERATOR.mul(&e4); + let c2 = Ciphertext(g3.clone(), g4.clone()); + + assert_eq!(&c1 + &c2, Ciphertext(&g1 + &g3, &g2 + &g4)); + } + + #[proptest] + fn ciphertext_mul_test(e1: Scalar, e2: Scalar, e3: Scalar) { + let g1 = GroupElement::GENERATOR.mul(&e1); + let g2 = GroupElement::GENERATOR.mul(&e2); + let c1 = Ciphertext(g1.clone(), g2.clone()); + + assert_eq!(&c1 * &e3, Ciphertext(&g1 * &e3, &g2 * &e3)); + } + #[proptest] fn elgamal_encryption_decryption_test( secret_key: SecretKey, message: Scalar, randomness: Scalar, diff --git a/rust/catalyst-voting/src/lib.rs b/rust/catalyst-voting/src/lib.rs index c81efedce83..5527ae70b4e 100644 --- a/rust/catalyst-voting/src/lib.rs +++ b/rust/catalyst-voting/src/lib.rs @@ -2,11 +2,10 @@ #![allow(dead_code, unused_variables, clippy::todo)] -use voter::{ - EncryptedVote, EncryptionRandomness, IncorrectChoiceError, IncorrectRandomnessLengthError, Vote, -}; +use voter::{EncryptedVote, EncryptedVoteError, EncryptionRandomness, Vote, VoteError}; mod crypto; +pub mod tally; pub mod voter; /// Election public key. @@ -16,8 +15,8 @@ pub type ElectionPublicKey = crypto::elgamal::PublicKey; /// More detailed described [here](https://input-output-hk.github.io/catalyst-voices/architecture/08_concepts/voting_transaction/crypto/#voting-choice) /// /// # Errors -/// - `voter::IncorrectChoiceError` -pub fn vote(choice: usize, voting_options: usize) -> Result { +/// - `voter::VoteError` +pub fn vote(choice: usize, voting_options: usize) -> Result { Vote::new(choice, voting_options) } @@ -25,10 +24,10 @@ pub fn vote(choice: usize, voting_options: usize) -> Result Result { +) -> Result { vote.encrypt(election_pk, randomness) } diff --git a/rust/catalyst-voting/src/tally.rs b/rust/catalyst-voting/src/tally.rs new file mode 100644 index 00000000000..98f3eedc874 --- /dev/null +++ b/rust/catalyst-voting/src/tally.rs @@ -0,0 +1,67 @@ +//! Module containing all primitives related to the tally process. + +use std::ops::{Add, Mul}; + +use rayon::iter::{IndexedParallelIterator, IntoParallelRefIterator, ParallelIterator}; + +use crate::{ + crypto::{ + elgamal::Ciphertext, + group::{GroupElement, Scalar}, + }, + voter::EncryptedVote, +}; + +/// A representation of the encrypted tally result. +pub struct EncryptedTallyResult(Ciphertext); + +/// Tally error +#[derive(thiserror::Error, Debug)] +pub enum TallyError { + /// Votes and voting power mismatch + #[error("Votes and voting power mismatch. Votes amount: {0}. Voting powers amount: {1}.")] + VotingPowerAndVotesMismatch(usize, usize), + /// Invalid encrypted vote + #[error("Invalid encrypted vote at index {0}. Does not have a ciphertext for the voting option {1}.")] + InvalidEncryptedVote(usize, usize), +} + +/// Tally function. +/// More detailed described [here](https://input-output-hk.github.io/catalyst-voices/architecture/08_concepts/voting_transaction/crypto/#homomorphic-tally) +/// +/// # Errors +/// - `TallyError` +pub fn tally( + voting_option: usize, votes: &[EncryptedVote], voting_powers: &[u64], +) -> Result { + if votes.len() != voting_powers.len() { + return Err(TallyError::VotingPowerAndVotesMismatch( + votes.len(), + voting_powers.len(), + )); + } + + let mut ciphertexts_per_voting_option = Vec::new(); + for (i, vote) in votes.iter().enumerate() { + let ciphertext = vote + .get_ciphertext_for_choice(voting_option) + .ok_or(TallyError::InvalidEncryptedVote(i, voting_option))?; + ciphertexts_per_voting_option.push(ciphertext); + } + + let zero_ciphertext = Ciphertext(GroupElement::zero(), GroupElement::zero()); + + let res = ciphertexts_per_voting_option + .par_iter() + .zip(voting_powers.par_iter()) + .map(|(ciphertext, voting_power)| { + let voting_power_scalar = Scalar::from(*voting_power); + ciphertext.mul(&voting_power_scalar) + }) + .reduce( + || zero_ciphertext.clone(), + |res, ciphertext| res.add(&ciphertext), + ); + + Ok(EncryptedTallyResult(res)) +} diff --git a/rust/catalyst-voting/src/voter.rs b/rust/catalyst-voting/src/voter.rs index a086b14dd2a..5b98f20b607 100644 --- a/rust/catalyst-voting/src/voter.rs +++ b/rust/catalyst-voting/src/voter.rs @@ -25,19 +25,25 @@ pub struct EncryptedVote(Vec); /// A representation of the encryption randomness, used to encrypt the vote. pub struct EncryptionRandomness(Vec); -/// Incorrect voting choice error +/// Encrypted vote error #[derive(thiserror::Error, Debug)] -#[error( - "Invalid voting choice, the value of choice: {0}, should be less than the number of voting options: {1}." -)] -pub struct IncorrectChoiceError(usize, usize); +pub enum VoteError { + /// Incorrect voting choice + #[error( + "Invalid voting choice, the value of choice: {0}, should be less than the number of voting options: {1}." + )] + IncorrectChoiceError(usize, usize), +} -/// Incorrect randomness length error +/// Encrypted vote error #[derive(thiserror::Error, Debug)] -#[error( - "Invalid randomness length, the length of randomness: {0}, should be equal to the number of voting options: {1}." -)] -pub struct IncorrectRandomnessLengthError(usize, usize); +pub enum EncryptedVoteError { + /// Incorrect randomness length + #[error( + "Invalid randomness length, the length of randomness: {0}, should be equal to the number of voting options: {1}." + )] + IncorrectRandomnessLength(usize, usize), +} impl EncryptionRandomness { /// Randomly generate the `EncryptionRandomness`. @@ -46,15 +52,22 @@ impl EncryptionRandomness { } } +impl EncryptedVote { + /// Get the ciphertext to the corresponding `voting_option`. + pub(crate) fn get_ciphertext_for_choice(&self, voting_option: usize) -> Option<&Ciphertext> { + self.0.get(voting_option) + } +} + impl Vote { /// Generate a vote. /// More detailed described [here](https://input-output-hk.github.io/catalyst-voices/architecture/08_concepts/voting_transaction/crypto/#voting-choice) /// /// # Errors - /// - `IncorrectChoiceError` - pub(crate) fn new(choice: usize, voting_options: usize) -> Result { + /// - `VoteError` + pub(crate) fn new(choice: usize, voting_options: usize) -> Result { if choice >= voting_options { - return Err(IncorrectChoiceError(choice, voting_options)); + return Err(VoteError::IncorrectChoiceError(choice, voting_options)); } Ok(Vote { @@ -80,9 +93,9 @@ impl Vote { /// More detailed described [here](https://input-output-hk.github.io/catalyst-voices/architecture/08_concepts/voting_transaction/crypto/#vote-encryption) pub(crate) fn encrypt( &self, public_key: &PublicKey, randomness: &EncryptionRandomness, - ) -> Result { + ) -> Result { if self.voting_options != randomness.0.len() { - return Err(IncorrectRandomnessLengthError( + return Err(EncryptedVoteError::IncorrectRandomnessLength( randomness.0.len(), self.voting_options, )); From 7eacd5bd3d5961ed5eea4cb1d7e4a9c7a384713b Mon Sep 17 00:00:00 2001 From: Mr-Leshiy Date: Fri, 27 Sep 2024 19:30:23 +0300 Subject: [PATCH 18/36] fix --- rust/catalyst-voting/src/crypto/elgamal.rs | 10 +++++++++- rust/catalyst-voting/src/tally.rs | 7 ++----- 2 files changed, 11 insertions(+), 6 deletions(-) diff --git a/rust/catalyst-voting/src/crypto/elgamal.rs b/rust/catalyst-voting/src/crypto/elgamal.rs index d845c95f6c8..e2bec883ab1 100644 --- a/rust/catalyst-voting/src/crypto/elgamal.rs +++ b/rust/catalyst-voting/src/crypto/elgamal.rs @@ -17,7 +17,7 @@ pub struct PublicKey(GroupElement); /// ``ElGamal`` ciphertext, encrypted message with the public key. #[derive(Debug, Clone, PartialEq, Eq)] -pub struct Ciphertext(pub(crate) GroupElement, pub(crate) GroupElement); +pub struct Ciphertext(GroupElement, GroupElement); impl SecretKey { /// Generate a random `SecretKey` value from the random number generator. @@ -31,6 +31,14 @@ impl SecretKey { } } +impl Ciphertext { + /// Generate a zero `Ciphertext`. + /// The same as encrypt a `Scalar::zero()` message and `Scalar::zero()` randomness. + pub(crate) fn zero() -> Self { + Ciphertext(GroupElement::zero(), GroupElement::zero()) + } +} + /// Given a `message` represented as a `Scalar`, return a ciphertext using the /// lifted ``ElGamal`` mechanism. /// Returns a ciphertext of type `Ciphertext`. diff --git a/rust/catalyst-voting/src/tally.rs b/rust/catalyst-voting/src/tally.rs index 98f3eedc874..5f741529abe 100644 --- a/rust/catalyst-voting/src/tally.rs +++ b/rust/catalyst-voting/src/tally.rs @@ -5,10 +5,7 @@ use std::ops::{Add, Mul}; use rayon::iter::{IndexedParallelIterator, IntoParallelRefIterator, ParallelIterator}; use crate::{ - crypto::{ - elgamal::Ciphertext, - group::{GroupElement, Scalar}, - }, + crypto::{elgamal::Ciphertext, group::Scalar}, voter::EncryptedVote, }; @@ -49,7 +46,7 @@ pub fn tally( ciphertexts_per_voting_option.push(ciphertext); } - let zero_ciphertext = Ciphertext(GroupElement::zero(), GroupElement::zero()); + let zero_ciphertext = Ciphertext::zero(); let res = ciphertexts_per_voting_option .par_iter() From 15d97c6c8af24ca4c81dbb907692fdf58098190d Mon Sep 17 00:00:00 2001 From: Mr-Leshiy Date: Fri, 27 Sep 2024 19:43:16 +0300 Subject: [PATCH 19/36] wip --- .../src/crypto/group/{ristretto255.rs => ristretto255/mod.rs} | 0 1 file changed, 0 insertions(+), 0 deletions(-) rename rust/catalyst-voting/src/crypto/group/{ristretto255.rs => ristretto255/mod.rs} (100%) diff --git a/rust/catalyst-voting/src/crypto/group/ristretto255.rs b/rust/catalyst-voting/src/crypto/group/ristretto255/mod.rs similarity index 100% rename from rust/catalyst-voting/src/crypto/group/ristretto255.rs rename to rust/catalyst-voting/src/crypto/group/ristretto255/mod.rs From dd1b1a3fd07dbd6795e04a343cee72e7284f2d75 Mon Sep 17 00:00:00 2001 From: Mr-Leshiy Date: Sat, 28 Sep 2024 11:04:35 +0300 Subject: [PATCH 20/36] add a babystep implementation --- .../src/crypto/group/babystep.rs | 97 +++++++++++++++++++ rust/catalyst-voting/src/crypto/group/mod.rs | 1 + .../src/crypto/group/ristretto255/mod.rs | 20 +++- 3 files changed, 117 insertions(+), 1 deletion(-) create mode 100644 rust/catalyst-voting/src/crypto/group/babystep.rs diff --git a/rust/catalyst-voting/src/crypto/group/babystep.rs b/rust/catalyst-voting/src/crypto/group/babystep.rs new file mode 100644 index 00000000000..78b31d7e2da --- /dev/null +++ b/rust/catalyst-voting/src/crypto/group/babystep.rs @@ -0,0 +1,97 @@ +//! Implementation of baby steps giant step algorithm to solve the discrete logarithm over +//! for the Ristretto255 group. + +use std::collections::HashMap; + +use super::{GroupElement, Scalar}; + +/// Default balance value. +/// Make steps asymmetric, in order to better use caching of baby steps. +/// Balance of 2 means that baby steps are 2 time more than `sqrt(max_votes)` +const DEFAULT_BALANCE: u64 = 2; + +/// Holds precomputed baby steps for the baby-stap giant-step algorithm +/// for solving discrete log +#[derive(Debug, Clone)] +pub struct BabyStepGiantStep { + /// Table of baby step precomputed values + table: HashMap, + /// baby step size value + baby_step_size: u64, + /// giant step value + giant_step: GroupElement, +} + +#[derive(thiserror::Error, Debug)] +pub enum BabyStepError { + /// Invalid max value or balance + #[error("Maximum value and balance must be greater than zero, provided max value: {0} and balance: {1}.")] + InvalidMaxValueOrBalance(u64, u64), + /// Max value exceeded + #[error("Max log value exceeded. Means that the actual discret log for the provided group element is higher than the provided `max_log_value`.")] + MaxLogExceeded, +} + +impl BabyStepGiantStep { + /// Creates a new setup for the baby-stap giant-step algorithm. + /// + /// Balance is used to make steps asymmetrical. If the table is reused multiple times + /// with the same `max_value` it is recommended to set a balance > 1, since this + /// will allow to cache more results, at the expense of a higher memory footprint. + /// + /// If not provided it will default to 2, means that the table will precompute 2 times + /// more baby steps than the standard O(sqrt(n)), 1 means symmetrical steps. + /// + /// + /// **NOTE** It is a heavy operation, so pls reuse the same instance for performing + /// `baby_step_giant_step` function for the same `max_value`. + pub fn generate_with_balance( + max_log_value: u64, balance: Option, + ) -> Result { + let balance = balance.unwrap_or(DEFAULT_BALANCE); + + if balance == 0 || max_log_value == 0 { + return Err(BabyStepError::InvalidMaxValueOrBalance( + max_log_value, + balance, + )); + } + + #[allow( + clippy::cast_possible_truncation, + clippy::cast_sign_loss, + clippy::cast_precision_loss + )] + let sqrt_step_size = (max_log_value as f64).sqrt().ceil() as u64; + let baby_step_size = sqrt_step_size * balance; + let mut table = HashMap::new(); + + let mut e = GroupElement::zero(); + for baby_step in 0..=baby_step_size { + let new_e = &e + &GroupElement::GENERATOR; + table.insert(e, baby_step); + e = new_e; + } + + let giant_step = &GroupElement::GENERATOR * &Scalar::from(baby_step_size).negate(); + Ok(Self { + table, + baby_step_size, + giant_step, + }) + } + + /// Solve the discrete log using baby step giant step algorithm. + pub fn discrete_log(&self, mut point: GroupElement) -> Result { + for baby_step in 0..=self.baby_step_size { + if let Some(x) = self.table.get(&point) { + let r = baby_step * self.baby_step_size + x; + return Ok(r); + } + point = &point + &self.giant_step; + } + // If we get here, the point is not in the table + // So we exceeded the maximum value of the discrete log + Err(BabyStepError::MaxLogExceeded) + } +} diff --git a/rust/catalyst-voting/src/crypto/group/mod.rs b/rust/catalyst-voting/src/crypto/group/mod.rs index 41c8c61592f..dc94e53b3aa 100644 --- a/rust/catalyst-voting/src/crypto/group/mod.rs +++ b/rust/catalyst-voting/src/crypto/group/mod.rs @@ -1,6 +1,7 @@ //! Group definitions used in voting protocol. //! For more information, see: +mod babystep; mod ristretto255; #[allow(clippy::module_name_repetitions)] diff --git a/rust/catalyst-voting/src/crypto/group/ristretto255/mod.rs b/rust/catalyst-voting/src/crypto/group/ristretto255/mod.rs index 641c3e1d5ea..5dea4cd95c6 100644 --- a/rust/catalyst-voting/src/crypto/group/ristretto255/mod.rs +++ b/rust/catalyst-voting/src/crypto/group/ristretto255/mod.rs @@ -2,7 +2,10 @@ // cspell: words BASEPOINT -use std::ops::{Add, Mul, Sub}; +use std::{ + hash::Hash, + ops::{Add, Mul, Sub}, +}; use curve25519_dalek::{ constants::{RISTRETTO_BASEPOINT_POINT, RISTRETTO_BASEPOINT_TABLE}, @@ -12,6 +15,10 @@ use curve25519_dalek::{ }; use rand_core::CryptoRngCore; +/// Size of the byte representation of `GroupElement`. We always encode the compressed +/// with `CompressedRistretto`. +pub const GROUP_ELEMENT_SIZE: usize = 32; + /// Ristretto group scalar. #[derive(Debug, Clone, PartialEq, Eq)] pub struct Scalar(IScalar); @@ -26,6 +33,12 @@ impl From for Scalar { } } +impl Hash for GroupElement { + fn hash(&self, state: &mut H) { + self.0.compress().as_bytes().hash(state); + } +} + impl Scalar { /// Generate a random scalar value from the random number generator. pub fn random(rng: &mut R) -> Self { @@ -44,6 +57,11 @@ impl Scalar { Scalar(IScalar::ONE) } + /// Increment on `1`. + pub fn increment(&mut self) { + self.0 += IScalar::ONE; + } + /// negative value pub fn negate(&self) -> Self { Scalar(-self.0) From a3c4d618d27fb8d9c93079226c6fdb6ff9fbd143 Mon Sep 17 00:00:00 2001 From: Mr-Leshiy Date: Sat, 28 Sep 2024 11:19:35 +0300 Subject: [PATCH 21/36] wip --- .../src/crypto/group/babystep.rs | 30 +++++++++++++++---- .../{ristretto255/mod.rs => ristretto255.rs} | 0 2 files changed, 24 insertions(+), 6 deletions(-) rename rust/catalyst-voting/src/crypto/group/{ristretto255/mod.rs => ristretto255.rs} (100%) diff --git a/rust/catalyst-voting/src/crypto/group/babystep.rs b/rust/catalyst-voting/src/crypto/group/babystep.rs index 78b31d7e2da..1aab3570938 100644 --- a/rust/catalyst-voting/src/crypto/group/babystep.rs +++ b/rust/catalyst-voting/src/crypto/group/babystep.rs @@ -1,4 +1,4 @@ -//! Implementation of baby steps giant step algorithm to solve the discrete logarithm over +//! Implementation of baby-step giant-step algorithm to solve the discrete logarithm over //! for the Ristretto255 group. use std::collections::HashMap; @@ -10,8 +10,8 @@ use super::{GroupElement, Scalar}; /// Balance of 2 means that baby steps are 2 time more than `sqrt(max_votes)` const DEFAULT_BALANCE: u64 = 2; -/// Holds precomputed baby steps for the baby-stap giant-step algorithm -/// for solving discrete log +/// Holds precomputed baby steps `table` for the baby-step giant-step algorithm +/// for solving discrete log. #[derive(Debug, Clone)] pub struct BabyStepGiantStep { /// Table of baby step precomputed values @@ -45,9 +45,7 @@ impl BabyStepGiantStep { /// /// **NOTE** It is a heavy operation, so pls reuse the same instance for performing /// `baby_step_giant_step` function for the same `max_value`. - pub fn generate_with_balance( - max_log_value: u64, balance: Option, - ) -> Result { + pub fn new(max_log_value: u64, balance: Option) -> Result { let balance = balance.unwrap_or(DEFAULT_BALANCE); if balance == 0 || max_log_value == 0 { @@ -95,3 +93,23 @@ impl BabyStepGiantStep { Err(BabyStepError::MaxLogExceeded) } } + +#[cfg(test)] +mod tests { + use std::ops::Mul; + + use test_strategy::proptest; + + use super::*; + + #[proptest] + fn baby_step_giant_step_test( + #[strategy(1..10000u64)] max_log_value: u64, #[strategy(1..#max_log_value)] log: u64, + ) { + let ge = GroupElement::GENERATOR.mul(&Scalar::from(log)); + + let baby_step_giant_step = BabyStepGiantStep::new(max_log_value, None).unwrap(); + let result = baby_step_giant_step.discrete_log(ge).unwrap(); + assert_eq!(result, log); + } +} diff --git a/rust/catalyst-voting/src/crypto/group/ristretto255/mod.rs b/rust/catalyst-voting/src/crypto/group/ristretto255.rs similarity index 100% rename from rust/catalyst-voting/src/crypto/group/ristretto255/mod.rs rename to rust/catalyst-voting/src/crypto/group/ristretto255.rs From dcd14842b0812ed57b9f43b3812540e9eadf579f Mon Sep 17 00:00:00 2001 From: Mr-Leshiy Date: Sat, 28 Sep 2024 19:45:35 +0300 Subject: [PATCH 22/36] refactor, add decrypt_tally_result --- .../{babystep.rs => babystep_giantstep.rs} | 6 ++ rust/catalyst-voting/src/crypto/group/mod.rs | 6 +- rust/catalyst-voting/src/tally.rs | 61 ++++++++++++++++++- 3 files changed, 69 insertions(+), 4 deletions(-) rename rust/catalyst-voting/src/crypto/group/{babystep.rs => babystep_giantstep.rs} (97%) diff --git a/rust/catalyst-voting/src/crypto/group/babystep.rs b/rust/catalyst-voting/src/crypto/group/babystep_giantstep.rs similarity index 97% rename from rust/catalyst-voting/src/crypto/group/babystep.rs rename to rust/catalyst-voting/src/crypto/group/babystep_giantstep.rs index 1aab3570938..115ee8f9442 100644 --- a/rust/catalyst-voting/src/crypto/group/babystep.rs +++ b/rust/catalyst-voting/src/crypto/group/babystep_giantstep.rs @@ -45,6 +45,9 @@ impl BabyStepGiantStep { /// /// **NOTE** It is a heavy operation, so pls reuse the same instance for performing /// `baby_step_giant_step` function for the same `max_value`. + /// + /// # Errors + /// - `BabyStepError` pub fn new(max_log_value: u64, balance: Option) -> Result { let balance = balance.unwrap_or(DEFAULT_BALANCE); @@ -80,6 +83,9 @@ impl BabyStepGiantStep { } /// Solve the discrete log using baby step giant step algorithm. + /// + /// # Errors + /// - `BabyStepError` pub fn discrete_log(&self, mut point: GroupElement) -> Result { for baby_step in 0..=self.baby_step_size { if let Some(x) = self.table.get(&point) { diff --git a/rust/catalyst-voting/src/crypto/group/mod.rs b/rust/catalyst-voting/src/crypto/group/mod.rs index dc94e53b3aa..afea38ca878 100644 --- a/rust/catalyst-voting/src/crypto/group/mod.rs +++ b/rust/catalyst-voting/src/crypto/group/mod.rs @@ -1,8 +1,8 @@ //! Group definitions used in voting protocol. //! For more information, see: -mod babystep; +mod babystep_giantstep; mod ristretto255; -#[allow(clippy::module_name_repetitions)] -pub use ristretto255::{GroupElement, Scalar}; +pub(crate) use babystep_giantstep::BabyStepGiantStep; +pub(crate) use ristretto255::{GroupElement, Scalar}; diff --git a/rust/catalyst-voting/src/tally.rs b/rust/catalyst-voting/src/tally.rs index 5f741529abe..7081d27735b 100644 --- a/rust/catalyst-voting/src/tally.rs +++ b/rust/catalyst-voting/src/tally.rs @@ -5,15 +5,47 @@ use std::ops::{Add, Mul}; use rayon::iter::{IndexedParallelIterator, IntoParallelRefIterator, ParallelIterator}; use crate::{ - crypto::{elgamal::Ciphertext, group::Scalar}, + crypto::{ + elgamal::{decrypt, Ciphertext, SecretKey}, + group::{BabyStepGiantStep, Scalar}, + }, voter::EncryptedVote, }; +/// An important decription tally setup, which holds an important precomputed data needed +/// for decryption. +pub struct DecriptionTallySetup { + /// `BabyStepGiantStep` setup + discrete_log_setup: BabyStepGiantStep, +} + /// A representation of the encrypted tally result. pub struct EncryptedTallyResult(Ciphertext); /// Tally error #[derive(thiserror::Error, Debug)] +pub enum DecryptionTallySetupError { + /// Votes and voting power mismatch + #[error("Total voting power must more than 0.")] + InvalidTotalVotingPowerAmount, +} + +impl DecriptionTallySetup { + /// Generate a decryption tally setup. + /// + /// # Errors + /// - `DecryptionTallySetupError` + pub fn new(voting_powers: &[u64]) -> Result { + let total_voting_power = voting_powers.par_iter().sum(); + let discrete_log_setup = BabyStepGiantStep::new(total_voting_power, None) + .map_err(|_| DecryptionTallySetupError::InvalidTotalVotingPowerAmount)?; + Ok(Self { discrete_log_setup }) + } +} + +/// Tally error +#[derive(thiserror::Error, Debug)] +#[allow(clippy::module_name_repetitions)] pub enum TallyError { /// Votes and voting power mismatch #[error("Votes and voting power mismatch. Votes amount: {0}. Voting powers amount: {1}.")] @@ -62,3 +94,30 @@ pub fn tally( Ok(EncryptedTallyResult(res)) } + +/// Tally error +#[derive(thiserror::Error, Debug)] +pub enum DecryptTallyError { + /// Cannot decrypt tally result + #[error( + "Cannot decrypt tally result. Provided an invalid secret key or invalid encrypted tally result." + )] + CannotDecryptTallyResult, +} + +/// Decrypts the encrypted tally result. +/// More detailed described [here](https://input-output-hk.github.io/catalyst-voices/architecture/08_concepts/voting_transaction/crypto/#tally-decryption) +/// +/// # Errors +/// - `DecryptTallyError` +pub fn decrypt_tally_result( + tally_result: &EncryptedTallyResult, secret_key: &SecretKey, setup: &DecriptionTallySetup, +) -> Result { + let ge = decrypt(&tally_result.0, secret_key); + + let res = setup + .discrete_log_setup + .discrete_log(ge) + .map_err(|_| DecryptTallyError::CannotDecryptTallyResult)?; + Ok(res) +} From 49366874001d5c2931aac71b89c08706223b95b5 Mon Sep 17 00:00:00 2001 From: Mr-Leshiy Date: Sat, 28 Sep 2024 19:47:18 +0300 Subject: [PATCH 23/36] wip --- rust/catalyst-voting/src/tally.rs | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/rust/catalyst-voting/src/tally.rs b/rust/catalyst-voting/src/tally.rs index 7081d27735b..483b786e284 100644 --- a/rust/catalyst-voting/src/tally.rs +++ b/rust/catalyst-voting/src/tally.rs @@ -33,6 +33,9 @@ pub enum DecryptionTallySetupError { impl DecriptionTallySetup { /// Generate a decryption tally setup. /// + /// **NOTE** It is a heavy operation, so please reuse the same instance for performing + /// `decrypt_tally` function for the same `voting_powers`. + /// /// # Errors /// - `DecryptionTallySetupError` pub fn new(voting_powers: &[u64]) -> Result { @@ -110,7 +113,8 @@ pub enum DecryptTallyError { /// /// # Errors /// - `DecryptTallyError` -pub fn decrypt_tally_result( +#[allow(clippy::module_name_repetitions)] +pub fn decrypt_tally( tally_result: &EncryptedTallyResult, secret_key: &SecretKey, setup: &DecriptionTallySetup, ) -> Result { let ge = decrypt(&tally_result.0, secret_key); From f572ad3ed9f1d9d4e49d69dc7b035c6132855d0e Mon Sep 17 00:00:00 2001 From: Mr-Leshiy Date: Sat, 28 Sep 2024 19:53:17 +0300 Subject: [PATCH 24/36] wip --- rust/catalyst-voting/src/lib.rs | 53 ------------------- rust/catalyst-voting/src/voter.rs | 88 ++++++++++++++++--------------- 2 files changed, 45 insertions(+), 96 deletions(-) diff --git a/rust/catalyst-voting/src/lib.rs b/rust/catalyst-voting/src/lib.rs index 5527ae70b4e..b73f8902cd8 100644 --- a/rust/catalyst-voting/src/lib.rs +++ b/rust/catalyst-voting/src/lib.rs @@ -1,58 +1,5 @@ //! Voting primitives which are used among Catalyst ecosystem. -#![allow(dead_code, unused_variables, clippy::todo)] - -use voter::{EncryptedVote, EncryptedVoteError, EncryptionRandomness, Vote, VoteError}; - mod crypto; pub mod tally; pub mod voter; - -/// Election public key. -pub type ElectionPublicKey = crypto::elgamal::PublicKey; - -/// Generate a vote. -/// More detailed described [here](https://input-output-hk.github.io/catalyst-voices/architecture/08_concepts/voting_transaction/crypto/#voting-choice) -/// -/// # Errors -/// - `voter::VoteError` -pub fn vote(choice: usize, voting_options: usize) -> Result { - Vote::new(choice, voting_options) -} - -/// Encrypt vote function. -/// More detailed described [here](https://input-output-hk.github.io/catalyst-voices/architecture/08_concepts/voting_transaction/crypto/#vote-encryption) -/// -/// # Errors -/// - `voter::EncryptedVoteError` -pub fn encrypt_vote( - vote: &Vote, election_pk: &ElectionPublicKey, randomness: &EncryptionRandomness, -) -> Result { - vote.encrypt(election_pk, randomness) -} - -/// Vote proof struct. -pub struct VoteProof; - -/// Generates a vote proof, which proofs that the given encrypted vote was correctly -/// generated. -/// More detailed described [here](https://input-output-hk.github.io/catalyst-voices/architecture/08_concepts/voting_transaction/crypto/#voters-proof) -/// -/// # Errors -/// - TODO -pub fn generate_vote_proof( - vote: &Vote, encrypted_vote: &EncryptedVote, election_pk: &ElectionPublicKey, -) -> anyhow::Result { - todo!() -} - -/// Verifies a vote proof, is it valid or not for the given encrypted vote. -/// More detailed described [here](https://input-output-hk.github.io/catalyst-voices/architecture/08_concepts/voting_transaction/crypto/#voters-proof) -/// -/// # Errors -/// - TODO -pub fn verify_vote( - vote: &Vote, proof: &VoteProof, election_pk: &ElectionPublicKey, -) -> anyhow::Result<()> { - todo!() -} diff --git a/rust/catalyst-voting/src/voter.rs b/rust/catalyst-voting/src/voter.rs index 5b98f20b607..58ccc6d153d 100644 --- a/rust/catalyst-voting/src/voter.rs +++ b/rust/catalyst-voting/src/voter.rs @@ -25,26 +25,6 @@ pub struct EncryptedVote(Vec); /// A representation of the encryption randomness, used to encrypt the vote. pub struct EncryptionRandomness(Vec); -/// Encrypted vote error -#[derive(thiserror::Error, Debug)] -pub enum VoteError { - /// Incorrect voting choice - #[error( - "Invalid voting choice, the value of choice: {0}, should be less than the number of voting options: {1}." - )] - IncorrectChoiceError(usize, usize), -} - -/// Encrypted vote error -#[derive(thiserror::Error, Debug)] -pub enum EncryptedVoteError { - /// Incorrect randomness length - #[error( - "Invalid randomness length, the length of randomness: {0}, should be equal to the number of voting options: {1}." - )] - IncorrectRandomnessLength(usize, usize), -} - impl EncryptionRandomness { /// Randomly generate the `EncryptionRandomness`. pub fn generate(rng: &mut R, voting_options: usize) -> Self { @@ -59,13 +39,23 @@ impl EncryptedVote { } } +/// Encrypted vote error +#[derive(thiserror::Error, Debug)] +pub enum VoteError { + /// Incorrect voting choice + #[error( + "Invalid voting choice, the value of choice: {0}, should be less than the number of voting options: {1}." + )] + IncorrectChoiceError(usize, usize), +} + impl Vote { /// Generate a vote. /// More detailed described [here](https://input-output-hk.github.io/catalyst-voices/architecture/08_concepts/voting_transaction/crypto/#voting-choice) /// /// # Errors /// - `VoteError` - pub(crate) fn new(choice: usize, voting_options: usize) -> Result { + pub fn new(choice: usize, voting_options: usize) -> Result { if choice >= voting_options { return Err(VoteError::IncorrectChoiceError(choice, voting_options)); } @@ -88,28 +78,41 @@ impl Vote { }) .collect() } +} - /// Create a new encrypted vote from the given vote and public key. - /// More detailed described [here](https://input-output-hk.github.io/catalyst-voices/architecture/08_concepts/voting_transaction/crypto/#vote-encryption) - pub(crate) fn encrypt( - &self, public_key: &PublicKey, randomness: &EncryptionRandomness, - ) -> Result { - if self.voting_options != randomness.0.len() { - return Err(EncryptedVoteError::IncorrectRandomnessLength( - randomness.0.len(), - self.voting_options, - )); - } - - let unit_vector = self.to_unit_vector(); - let ciphers = unit_vector - .par_iter() - .zip(randomness.0.par_iter()) - .map(|(m, r)| encrypt(m, public_key, r)) - .collect(); +/// Encrypted vote error +#[derive(thiserror::Error, Debug)] +pub enum EncryptedVoteError { + /// Incorrect randomness length + #[error( + "Invalid randomness length, the length of randomness: {0}, should be equal to the number of voting options: {1}." + )] + IncorrectRandomnessLength(usize, usize), +} - Ok(EncryptedVote(ciphers)) +/// Create a new encrypted vote from the given vote and public key. +/// More detailed described [here](https://input-output-hk.github.io/catalyst-voices/architecture/08_concepts/voting_transaction/crypto/#vote-encryption) +/// +/// # Errors +/// - `EncryptedVoteError` +pub fn encrypt_vote( + vote: &Vote, public_key: &PublicKey, randomness: &EncryptionRandomness, +) -> Result { + if vote.voting_options != randomness.0.len() { + return Err(EncryptedVoteError::IncorrectRandomnessLength( + randomness.0.len(), + vote.voting_options, + )); } + + let unit_vector = vote.to_unit_vector(); + let ciphers = unit_vector + .par_iter() + .zip(randomness.0.par_iter()) + .map(|(m, r)| encrypt(m, public_key, r)) + .collect(); + + Ok(EncryptedVote(ciphers)) } #[cfg(test)] @@ -157,9 +160,8 @@ mod tests { let public_key = secret_key.public_key(); let vote = Vote::new(0, voting_options).unwrap(); - let encrypted = vote - .encrypt(&public_key, &EncryptionRandomness(randomness)) - .unwrap(); + let encrypted = + encrypt_vote(&vote, &public_key, &EncryptionRandomness(randomness)).unwrap(); assert_eq!(encrypted.0.len(), vote.voting_options); } } From 81a632398f996e9a2912134e161cf10ac8c760ef Mon Sep 17 00:00:00 2001 From: Mr-Leshiy Date: Sat, 28 Sep 2024 20:56:13 +0300 Subject: [PATCH 25/36] add voting test --- rust/catalyst-voting/src/crypto/elgamal.rs | 2 +- .../src/crypto/group/ristretto255.rs | 4 - rust/catalyst-voting/src/lib.rs | 73 +++++++++++++++++++ rust/catalyst-voting/src/tally.rs | 11 +-- 4 files changed, 80 insertions(+), 10 deletions(-) diff --git a/rust/catalyst-voting/src/crypto/elgamal.rs b/rust/catalyst-voting/src/crypto/elgamal.rs index e2bec883ab1..91e85f6cd11 100644 --- a/rust/catalyst-voting/src/crypto/elgamal.rs +++ b/rust/catalyst-voting/src/crypto/elgamal.rs @@ -21,7 +21,7 @@ pub struct Ciphertext(GroupElement, GroupElement); impl SecretKey { /// Generate a random `SecretKey` value from the random number generator. - pub fn random(rng: &mut R) -> Self { + pub fn generate(rng: &mut R) -> Self { Self(Scalar::random(rng)) } diff --git a/rust/catalyst-voting/src/crypto/group/ristretto255.rs b/rust/catalyst-voting/src/crypto/group/ristretto255.rs index 5dea4cd95c6..3f2a718dba6 100644 --- a/rust/catalyst-voting/src/crypto/group/ristretto255.rs +++ b/rust/catalyst-voting/src/crypto/group/ristretto255.rs @@ -15,10 +15,6 @@ use curve25519_dalek::{ }; use rand_core::CryptoRngCore; -/// Size of the byte representation of `GroupElement`. We always encode the compressed -/// with `CompressedRistretto`. -pub const GROUP_ELEMENT_SIZE: usize = 32; - /// Ristretto group scalar. #[derive(Debug, Clone, PartialEq, Eq)] pub struct Scalar(IScalar); diff --git a/rust/catalyst-voting/src/lib.rs b/rust/catalyst-voting/src/lib.rs index b73f8902cd8..e3cbae17080 100644 --- a/rust/catalyst-voting/src/lib.rs +++ b/rust/catalyst-voting/src/lib.rs @@ -3,3 +3,76 @@ mod crypto; pub mod tally; pub mod voter; + +#[cfg(test)] +mod tests { + use proptest::prelude::ProptestConfig; + use test_strategy::{proptest, Arbitrary}; + + use crate::{ + crypto::elgamal::SecretKey, + tally::{decrypt_tally, tally, DecriptionTallySetup}, + voter::{encrypt_vote, EncryptionRandomness, Vote}, + }; + + const VOTING_OPTIONS: usize = 3; + + #[derive(Arbitrary, Debug)] + struct Voter { + voting_power: u32, + // range from 0 to `VOTING_OPTIONS` + #[strategy(0..3_usize)] + choice: usize, + } + + #[proptest(ProptestConfig::with_cases(1))] + fn voting_test(voters: [Voter; 100]) { + let mut rng = rand_core::OsRng; + + let election_secret_key = SecretKey::generate(&mut rng); + let election_public_key = election_secret_key.public_key(); + + let votes: Vec<_> = voters + .iter() + .map(|voter| Vote::new(voter.choice, VOTING_OPTIONS).unwrap()) + .collect(); + + let voters_randomness: Vec<_> = (0..voters.len()) + .map(|_| EncryptionRandomness::generate(&mut rng, VOTING_OPTIONS)) + .collect(); + + let encrypted_votes: Vec<_> = votes + .iter() + .zip(voters_randomness.iter()) + .map(|(vote, r)| encrypt_vote(vote, &election_public_key, r).unwrap()) + .collect(); + + let voting_powers: Vec<_> = voters + .iter() + .map(|voter| u64::from(voter.voting_power)) + .collect(); + + let encrypted_tallies: Vec<_> = (0..VOTING_OPTIONS) + .map(|voting_option| tally(voting_option, &encrypted_votes, &voting_powers).unwrap()) + .collect(); + + let decription_tally_setup = DecriptionTallySetup::new(&voting_powers).unwrap(); + + let decrypted_tallies: Vec<_> = encrypted_tallies + .iter() + .map(|t| decrypt_tally(t, &election_secret_key, &decription_tally_setup).unwrap()) + .collect(); + + let expected_tallies: Vec<_> = (0..VOTING_OPTIONS) + .map(|i| { + voters + .iter() + .filter(|v| v.choice == i) + .map(|v| u64::from(v.voting_power)) + .sum::() + }) + .collect(); + + assert_eq!(decrypted_tallies, expected_tallies); + } +} diff --git a/rust/catalyst-voting/src/tally.rs b/rust/catalyst-voting/src/tally.rs index 483b786e284..43e823165df 100644 --- a/rust/catalyst-voting/src/tally.rs +++ b/rust/catalyst-voting/src/tally.rs @@ -19,8 +19,9 @@ pub struct DecriptionTallySetup { discrete_log_setup: BabyStepGiantStep, } -/// A representation of the encrypted tally result. -pub struct EncryptedTallyResult(Ciphertext); +/// A representation of the encrypted tally. +#[allow(clippy::module_name_repetitions)] +pub struct EncryptedTally(Ciphertext); /// Tally error #[derive(thiserror::Error, Debug)] @@ -65,7 +66,7 @@ pub enum TallyError { /// - `TallyError` pub fn tally( voting_option: usize, votes: &[EncryptedVote], voting_powers: &[u64], -) -> Result { +) -> Result { if votes.len() != voting_powers.len() { return Err(TallyError::VotingPowerAndVotesMismatch( votes.len(), @@ -95,7 +96,7 @@ pub fn tally( |res, ciphertext| res.add(&ciphertext), ); - Ok(EncryptedTallyResult(res)) + Ok(EncryptedTally(res)) } /// Tally error @@ -115,7 +116,7 @@ pub enum DecryptTallyError { /// - `DecryptTallyError` #[allow(clippy::module_name_repetitions)] pub fn decrypt_tally( - tally_result: &EncryptedTallyResult, secret_key: &SecretKey, setup: &DecriptionTallySetup, + tally_result: &EncryptedTally, secret_key: &SecretKey, setup: &DecriptionTallySetup, ) -> Result { let ge = decrypt(&tally_result.0, secret_key); From f988417e065cc60df32bdca54e5402714f1ac7d1 Mon Sep 17 00:00:00 2001 From: Mr-Leshiy Date: Sun, 29 Sep 2024 11:55:08 +0300 Subject: [PATCH 26/36] remove rayon dependency for now --- rust/catalyst-voting/Cargo.toml | 1 - rust/catalyst-voting/src/tally.rs | 13 ++++--------- rust/catalyst-voting/src/voter.rs | 5 ++--- 3 files changed, 6 insertions(+), 13 deletions(-) diff --git a/rust/catalyst-voting/Cargo.toml b/rust/catalyst-voting/Cargo.toml index c2ec52bcb10..d81ad26deb5 100644 --- a/rust/catalyst-voting/Cargo.toml +++ b/rust/catalyst-voting/Cargo.toml @@ -15,7 +15,6 @@ anyhow = "1.0.71" thiserror = "1.0.56" rand_core = "0.6.4" curve25519-dalek = { version = "4.0" } -rayon = "1.10.0" [dev-dependencies] proptest = {version = "1.5.0" } diff --git a/rust/catalyst-voting/src/tally.rs b/rust/catalyst-voting/src/tally.rs index 43e823165df..e3b3c8daecb 100644 --- a/rust/catalyst-voting/src/tally.rs +++ b/rust/catalyst-voting/src/tally.rs @@ -2,8 +2,6 @@ use std::ops::{Add, Mul}; -use rayon::iter::{IndexedParallelIterator, IntoParallelRefIterator, ParallelIterator}; - use crate::{ crypto::{ elgamal::{decrypt, Ciphertext, SecretKey}, @@ -40,7 +38,7 @@ impl DecriptionTallySetup { /// # Errors /// - `DecryptionTallySetupError` pub fn new(voting_powers: &[u64]) -> Result { - let total_voting_power = voting_powers.par_iter().sum(); + let total_voting_power = voting_powers.iter().sum(); let discrete_log_setup = BabyStepGiantStep::new(total_voting_power, None) .map_err(|_| DecryptionTallySetupError::InvalidTotalVotingPowerAmount)?; Ok(Self { discrete_log_setup }) @@ -85,16 +83,13 @@ pub fn tally( let zero_ciphertext = Ciphertext::zero(); let res = ciphertexts_per_voting_option - .par_iter() - .zip(voting_powers.par_iter()) + .iter() + .zip(voting_powers.iter()) .map(|(ciphertext, voting_power)| { let voting_power_scalar = Scalar::from(*voting_power); ciphertext.mul(&voting_power_scalar) }) - .reduce( - || zero_ciphertext.clone(), - |res, ciphertext| res.add(&ciphertext), - ); + .fold(zero_ciphertext, |acc, c| acc.add(&c)); Ok(EncryptedTally(res)) } diff --git a/rust/catalyst-voting/src/voter.rs b/rust/catalyst-voting/src/voter.rs index 58ccc6d153d..c67041aa906 100644 --- a/rust/catalyst-voting/src/voter.rs +++ b/rust/catalyst-voting/src/voter.rs @@ -1,7 +1,6 @@ //! Module containing all primitives related to the voter. use rand_core::CryptoRngCore; -use rayon::iter::{IndexedParallelIterator, IntoParallelRefIterator, ParallelIterator}; use crate::crypto::{ elgamal::{encrypt, Ciphertext, PublicKey}, @@ -107,8 +106,8 @@ pub fn encrypt_vote( let unit_vector = vote.to_unit_vector(); let ciphers = unit_vector - .par_iter() - .zip(randomness.0.par_iter()) + .iter() + .zip(randomness.0.iter()) .map(|(m, r)| encrypt(m, public_key, r)) .collect(); From 37cf886ca8bc2b04914e0148932c8ff74edce899 Mon Sep 17 00:00:00 2001 From: Mr-Leshiy Date: Mon, 30 Sep 2024 11:58:14 +0300 Subject: [PATCH 27/36] fix spelling, remove rayon --- .config/dictionaries/project.dic | 3 +++ rust/catalyst-voting/Cargo.toml | 2 +- .../src/crypto/group/babystep_giantstep.rs | 4 ++-- rust/catalyst-voting/src/lib.rs | 4 ++-- rust/catalyst-voting/src/tally.rs | 8 ++++---- 5 files changed, 12 insertions(+), 9 deletions(-) diff --git a/.config/dictionaries/project.dic b/.config/dictionaries/project.dic index 303a0a748b9..2f3da07262a 100644 --- a/.config/dictionaries/project.dic +++ b/.config/dictionaries/project.dic @@ -10,6 +10,7 @@ Arissara asyncio Attributes auditability +babystep backpressure bech bimap @@ -34,6 +35,7 @@ Chotivichit chrono cids ciphertext +ciphertexts codegen codepoints coti @@ -83,6 +85,7 @@ futimens genhtml GETFL getres +giantstep gmtime gossipsub happ diff --git a/rust/catalyst-voting/Cargo.toml b/rust/catalyst-voting/Cargo.toml index d81ad26deb5..1f3b4586df4 100644 --- a/rust/catalyst-voting/Cargo.toml +++ b/rust/catalyst-voting/Cargo.toml @@ -18,6 +18,6 @@ curve25519-dalek = { version = "4.0" } [dev-dependencies] proptest = {version = "1.5.0" } -# Potentially it could be replaced with using `proptest::property_test` atribute macro, +# Potentially it could be replaced with using `proptest::property_test` attribute macro, # after this PR will be merged https://github.com/proptest-rs/proptest/pull/523 test-strategy = "0.4.0" diff --git a/rust/catalyst-voting/src/crypto/group/babystep_giantstep.rs b/rust/catalyst-voting/src/crypto/group/babystep_giantstep.rs index 115ee8f9442..6f6d356feae 100644 --- a/rust/catalyst-voting/src/crypto/group/babystep_giantstep.rs +++ b/rust/catalyst-voting/src/crypto/group/babystep_giantstep.rs @@ -28,12 +28,12 @@ pub enum BabyStepError { #[error("Maximum value and balance must be greater than zero, provided max value: {0} and balance: {1}.")] InvalidMaxValueOrBalance(u64, u64), /// Max value exceeded - #[error("Max log value exceeded. Means that the actual discret log for the provided group element is higher than the provided `max_log_value`.")] + #[error("Max log value exceeded. Means that the actual discrete log for the provided group element is higher than the provided `max_log_value`.")] MaxLogExceeded, } impl BabyStepGiantStep { - /// Creates a new setup for the baby-stap giant-step algorithm. + /// Creates a new setup for the baby-step giant-step algorithm. /// /// Balance is used to make steps asymmetrical. If the table is reused multiple times /// with the same `max_value` it is recommended to set a balance > 1, since this diff --git a/rust/catalyst-voting/src/lib.rs b/rust/catalyst-voting/src/lib.rs index e3cbae17080..7f0edb1869d 100644 --- a/rust/catalyst-voting/src/lib.rs +++ b/rust/catalyst-voting/src/lib.rs @@ -11,7 +11,7 @@ mod tests { use crate::{ crypto::elgamal::SecretKey, - tally::{decrypt_tally, tally, DecriptionTallySetup}, + tally::{decrypt_tally, tally, DecryptionTallySetup}, voter::{encrypt_vote, EncryptionRandomness, Vote}, }; @@ -56,7 +56,7 @@ mod tests { .map(|voting_option| tally(voting_option, &encrypted_votes, &voting_powers).unwrap()) .collect(); - let decription_tally_setup = DecriptionTallySetup::new(&voting_powers).unwrap(); + let decription_tally_setup = DecryptionTallySetup::new(&voting_powers).unwrap(); let decrypted_tallies: Vec<_> = encrypted_tallies .iter() diff --git a/rust/catalyst-voting/src/tally.rs b/rust/catalyst-voting/src/tally.rs index e3b3c8daecb..b78a17e4f9f 100644 --- a/rust/catalyst-voting/src/tally.rs +++ b/rust/catalyst-voting/src/tally.rs @@ -10,9 +10,9 @@ use crate::{ voter::EncryptedVote, }; -/// An important decription tally setup, which holds an important precomputed data needed +/// An important decryption tally setup, which holds an important precomputed data needed /// for decryption. -pub struct DecriptionTallySetup { +pub struct DecryptionTallySetup { /// `BabyStepGiantStep` setup discrete_log_setup: BabyStepGiantStep, } @@ -29,7 +29,7 @@ pub enum DecryptionTallySetupError { InvalidTotalVotingPowerAmount, } -impl DecriptionTallySetup { +impl DecryptionTallySetup { /// Generate a decryption tally setup. /// /// **NOTE** It is a heavy operation, so please reuse the same instance for performing @@ -111,7 +111,7 @@ pub enum DecryptTallyError { /// - `DecryptTallyError` #[allow(clippy::module_name_repetitions)] pub fn decrypt_tally( - tally_result: &EncryptedTally, secret_key: &SecretKey, setup: &DecriptionTallySetup, + tally_result: &EncryptedTally, secret_key: &SecretKey, setup: &DecryptionTallySetup, ) -> Result { let ge = decrypt(&tally_result.0, secret_key); From 24da2bc146d007e4e982c02711c3753a643ffa65 Mon Sep 17 00:00:00 2001 From: Mr-Leshiy Date: Mon, 30 Sep 2024 12:00:58 +0300 Subject: [PATCH 28/36] fix --- rust/catalyst-voting/src/lib.rs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/rust/catalyst-voting/src/lib.rs b/rust/catalyst-voting/src/lib.rs index 7f0edb1869d..ada735e5761 100644 --- a/rust/catalyst-voting/src/lib.rs +++ b/rust/catalyst-voting/src/lib.rs @@ -56,11 +56,11 @@ mod tests { .map(|voting_option| tally(voting_option, &encrypted_votes, &voting_powers).unwrap()) .collect(); - let decription_tally_setup = DecryptionTallySetup::new(&voting_powers).unwrap(); + let decryption_tally_setup = DecryptionTallySetup::new(&voting_powers).unwrap(); let decrypted_tallies: Vec<_> = encrypted_tallies .iter() - .map(|t| decrypt_tally(t, &election_secret_key, &decription_tally_setup).unwrap()) + .map(|t| decrypt_tally(t, &election_secret_key, &decryption_tally_setup).unwrap()) .collect(); let expected_tallies: Vec<_> = (0..VOTING_OPTIONS) From 804c7238b60bd52f43055df722c8cd674650c0e0 Mon Sep 17 00:00:00 2001 From: Mr-Leshiy Date: Mon, 30 Sep 2024 12:01:33 +0300 Subject: [PATCH 29/36] remove unused anyhow dep --- rust/catalyst-voting/Cargo.toml | 1 - 1 file changed, 1 deletion(-) diff --git a/rust/catalyst-voting/Cargo.toml b/rust/catalyst-voting/Cargo.toml index 1f3b4586df4..9044c3ce5e5 100644 --- a/rust/catalyst-voting/Cargo.toml +++ b/rust/catalyst-voting/Cargo.toml @@ -11,7 +11,6 @@ license.workspace = true workspace = true [dependencies] -anyhow = "1.0.71" thiserror = "1.0.56" rand_core = "0.6.4" curve25519-dalek = { version = "4.0" } From 921ad466be5da8b47cf442cc179a20f00f0ef5c3 Mon Sep 17 00:00:00 2001 From: Mr-Leshiy Date: Mon, 30 Sep 2024 12:10:54 +0300 Subject: [PATCH 30/36] intentionally break the test --- rust/catalyst-voting/src/lib.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/rust/catalyst-voting/src/lib.rs b/rust/catalyst-voting/src/lib.rs index ada735e5761..83ee1165155 100644 --- a/rust/catalyst-voting/src/lib.rs +++ b/rust/catalyst-voting/src/lib.rs @@ -73,6 +73,6 @@ mod tests { }) .collect(); - assert_eq!(decrypted_tallies, expected_tallies); + assert_ne!(decrypted_tallies, expected_tallies); } } From fa63fe1a01778b35b11b186188e10a6c1cadb62b Mon Sep 17 00:00:00 2001 From: Mr-Leshiy Date: Mon, 30 Sep 2024 12:17:25 +0300 Subject: [PATCH 31/36] try --- rust/Earthfile | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/rust/Earthfile b/rust/Earthfile index cd1d38dfe02..ad579aaa9df 100644 --- a/rust/Earthfile +++ b/rust/Earthfile @@ -34,9 +34,7 @@ build: DO rust-ci+EXECUTE \ --cmd="/scripts/std_build.py" \ --output="release/[^\./]+" \ - --args1="--libs=c509-certificate --libs=cardano-chain-follower --libs=hermes-ipfs" \ - --args2="--libs=cbork-cddl-parser --libs=cbork-abnf-parser" \ - --args3="--libs=catalyst-voting" \ + --args1="--libs=c509-certificate --libs=cardano-chain-follower --libs=hermes-ipfs --libs=cbork-cddl-parser --libs=cbork-abnf-parser --libs=catalyst-voting" \ --args4="--bins=cbork/cbork" \ --docs="true" From c54f806b0bb04894e95885eaf7bf38a842060707 Mon Sep 17 00:00:00 2001 From: Mr-Leshiy Date: Mon, 30 Sep 2024 12:36:27 +0300 Subject: [PATCH 32/36] wip --- rust/Earthfile | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/rust/Earthfile b/rust/Earthfile index ad579aaa9df..cd1d38dfe02 100644 --- a/rust/Earthfile +++ b/rust/Earthfile @@ -34,7 +34,9 @@ build: DO rust-ci+EXECUTE \ --cmd="/scripts/std_build.py" \ --output="release/[^\./]+" \ - --args1="--libs=c509-certificate --libs=cardano-chain-follower --libs=hermes-ipfs --libs=cbork-cddl-parser --libs=cbork-abnf-parser --libs=catalyst-voting" \ + --args1="--libs=c509-certificate --libs=cardano-chain-follower --libs=hermes-ipfs" \ + --args2="--libs=cbork-cddl-parser --libs=cbork-abnf-parser" \ + --args3="--libs=catalyst-voting" \ --args4="--bins=cbork/cbork" \ --docs="true" From c444a0272dc9d9018f58952fc4e8aab03c73cad1 Mon Sep 17 00:00:00 2001 From: Mr-Leshiy Date: Mon, 30 Sep 2024 18:04:06 +0300 Subject: [PATCH 33/36] update DecryptionTallySetup interface --- rust/catalyst-voting/src/lib.rs | 5 +++-- rust/catalyst-voting/src/tally.rs | 5 +++-- 2 files changed, 6 insertions(+), 4 deletions(-) diff --git a/rust/catalyst-voting/src/lib.rs b/rust/catalyst-voting/src/lib.rs index 83ee1165155..4e204b5ab2a 100644 --- a/rust/catalyst-voting/src/lib.rs +++ b/rust/catalyst-voting/src/lib.rs @@ -56,7 +56,8 @@ mod tests { .map(|voting_option| tally(voting_option, &encrypted_votes, &voting_powers).unwrap()) .collect(); - let decryption_tally_setup = DecryptionTallySetup::new(&voting_powers).unwrap(); + let total_voting_power = voting_powers.iter().sum(); + let decryption_tally_setup = DecryptionTallySetup::new(total_voting_power).unwrap(); let decrypted_tallies: Vec<_> = encrypted_tallies .iter() @@ -73,6 +74,6 @@ mod tests { }) .collect(); - assert_ne!(decrypted_tallies, expected_tallies); + assert_eq!(decrypted_tallies, expected_tallies); } } diff --git a/rust/catalyst-voting/src/tally.rs b/rust/catalyst-voting/src/tally.rs index b78a17e4f9f..a6ef55a02b7 100644 --- a/rust/catalyst-voting/src/tally.rs +++ b/rust/catalyst-voting/src/tally.rs @@ -31,14 +31,15 @@ pub enum DecryptionTallySetupError { impl DecryptionTallySetup { /// Generate a decryption tally setup. + /// `total_voting_power` must be a total sum of all voting powers used in the `tally` + /// procedure. /// /// **NOTE** It is a heavy operation, so please reuse the same instance for performing /// `decrypt_tally` function for the same `voting_powers`. /// /// # Errors /// - `DecryptionTallySetupError` - pub fn new(voting_powers: &[u64]) -> Result { - let total_voting_power = voting_powers.iter().sum(); + pub fn new(total_voting_power: u64) -> Result { let discrete_log_setup = BabyStepGiantStep::new(total_voting_power, None) .map_err(|_| DecryptionTallySetupError::InvalidTotalVotingPowerAmount)?; Ok(Self { discrete_log_setup }) From a63d11a363650a350eacd5743d331d6f896c76e5 Mon Sep 17 00:00:00 2001 From: Mr-Leshiy Date: Mon, 30 Sep 2024 18:32:09 +0300 Subject: [PATCH 34/36] add doctest example --- rust/catalyst-voting/src/crypto/elgamal.rs | 1 + rust/catalyst-voting/src/crypto/mod.rs | 2 + rust/catalyst-voting/src/lib.rs | 81 +++++++++++++++++++++- 3 files changed, 81 insertions(+), 3 deletions(-) diff --git a/rust/catalyst-voting/src/crypto/elgamal.rs b/rust/catalyst-voting/src/crypto/elgamal.rs index 91e85f6cd11..c33ccfc2db7 100644 --- a/rust/catalyst-voting/src/crypto/elgamal.rs +++ b/rust/catalyst-voting/src/crypto/elgamal.rs @@ -26,6 +26,7 @@ impl SecretKey { } /// Generate a corresponding `PublicKey`. + #[must_use] pub fn public_key(&self) -> PublicKey { PublicKey(GroupElement::GENERATOR.mul(&self.0)) } diff --git a/rust/catalyst-voting/src/crypto/mod.rs b/rust/catalyst-voting/src/crypto/mod.rs index 16822f6efed..f5f94bfd9b9 100644 --- a/rust/catalyst-voting/src/crypto/mod.rs +++ b/rust/catalyst-voting/src/crypto/mod.rs @@ -2,3 +2,5 @@ pub(crate) mod elgamal; pub(crate) mod group; + +pub use elgamal::{PublicKey, SecretKey}; diff --git a/rust/catalyst-voting/src/lib.rs b/rust/catalyst-voting/src/lib.rs index 4e204b5ab2a..9b4c5e70a21 100644 --- a/rust/catalyst-voting/src/lib.rs +++ b/rust/catalyst-voting/src/lib.rs @@ -1,6 +1,81 @@ //! Voting primitives which are used among Catalyst ecosystem. - -mod crypto; +//! +//! ```rust +//! use catalyst_voting::{ +//! crypto::SecretKey, +//! tally::{decrypt_tally, tally, DecryptionTallySetup}, +//! voter::{encrypt_vote, EncryptionRandomness, Vote}, +//! }; +//! +//! struct Voter { +//! voting_power: u64, +//! choice: usize, +//! } +//! +//! let mut rng = rand_core::OsRng; +//! let voting_options = 3; +//! let election_secret_key = SecretKey::generate(&mut rng); +//! let election_public_key = election_secret_key.public_key(); +//! +//! let voter_1 = Voter { +//! voting_power: 10, +//! choice: 0, +//! }; +//! +//! let voter_2 = Voter { +//! voting_power: 20, +//! choice: 1, +//! }; +//! +//! let voter_3 = Voter { +//! voting_power: 30, +//! choice: 2, +//! }; +//! +//! let vote_1 = Vote::new(voter_1.choice, voting_options).unwrap(); +//! let vote_2 = Vote::new(voter_2.choice, voting_options).unwrap(); +//! let vote_3 = Vote::new(voter_3.choice, voting_options).unwrap(); +//! +//! let voter_1_randomness = EncryptionRandomness::generate(&mut rng, voting_options); +//! let voter_2_randomness = EncryptionRandomness::generate(&mut rng, voting_options); +//! let voter_3_randomness = EncryptionRandomness::generate(&mut rng, voting_options); +//! +//! let encrypted_vote_1 = +//! encrypt_vote(&vote_1, &election_public_key, &voter_1_randomness).unwrap(); +//! let encrypted_vote_2 = +//! encrypt_vote(&vote_2, &election_public_key, &voter_2_randomness).unwrap(); +//! let encrypted_vote_3 = +//! encrypt_vote(&vote_3, &election_public_key, &voter_3_randomness).unwrap(); +//! let encrypted_votes = vec![encrypted_vote_1, encrypted_vote_2, encrypted_vote_3]; +//! +//! let encrypted_tallies: Vec<_> = (0..voting_options) +//! .map(|voting_option| { +//! tally(voting_option, &encrypted_votes, &[ +//! voter_1.voting_power, +//! voter_2.voting_power, +//! voter_3.voting_power, +//! ]) +//! .unwrap() +//! }) +//! .collect(); +//! +//! let decryption_tally_setup = DecryptionTallySetup::new( +//! voter_1.voting_power + voter_2.voting_power + voter_3.voting_power, +//! ) +//! .unwrap(); +//! let decrypted_tallies: Vec<_> = encrypted_tallies +//! .iter() +//! .map(|t| decrypt_tally(t, &election_secret_key, &decryption_tally_setup).unwrap()) +//! .collect(); +//! +//! assert_eq!(decrypted_tallies, vec![ +//! voter_1.voting_power, +//! voter_2.voting_power, +//! voter_3.voting_power +//! ]); +//! ``` + +pub mod crypto; pub mod tally; pub mod voter; @@ -10,7 +85,7 @@ mod tests { use test_strategy::{proptest, Arbitrary}; use crate::{ - crypto::elgamal::SecretKey, + crypto::SecretKey, tally::{decrypt_tally, tally, DecryptionTallySetup}, voter::{encrypt_vote, EncryptionRandomness, Vote}, }; From 688f5b7c8c4f817928f0a17d0e5bbb04baf0125d Mon Sep 17 00:00:00 2001 From: Mr-Leshiy Date: Mon, 30 Sep 2024 18:59:06 +0300 Subject: [PATCH 35/36] refactor, make voting_test as integration test --- rust/catalyst-voting/src/crypto/mod.rs | 2 - rust/catalyst-voting/src/lib.rs | 83 ++--------------------- rust/catalyst-voting/tests/voting_test.rs | 69 +++++++++++++++++++ 3 files changed, 75 insertions(+), 79 deletions(-) create mode 100644 rust/catalyst-voting/tests/voting_test.rs diff --git a/rust/catalyst-voting/src/crypto/mod.rs b/rust/catalyst-voting/src/crypto/mod.rs index f5f94bfd9b9..16822f6efed 100644 --- a/rust/catalyst-voting/src/crypto/mod.rs +++ b/rust/catalyst-voting/src/crypto/mod.rs @@ -2,5 +2,3 @@ pub(crate) mod elgamal; pub(crate) mod group; - -pub use elgamal::{PublicKey, SecretKey}; diff --git a/rust/catalyst-voting/src/lib.rs b/rust/catalyst-voting/src/lib.rs index 9b4c5e70a21..ef3e7456cde 100644 --- a/rust/catalyst-voting/src/lib.rs +++ b/rust/catalyst-voting/src/lib.rs @@ -2,9 +2,8 @@ //! //! ```rust //! use catalyst_voting::{ -//! crypto::SecretKey, -//! tally::{decrypt_tally, tally, DecryptionTallySetup}, -//! voter::{encrypt_vote, EncryptionRandomness, Vote}, +//! decrypt_tally, encrypt_vote, tally, DecryptionTallySetup, EncryptionRandomness, SecretKey, +//! Vote, //! }; //! //! struct Voter { @@ -75,80 +74,10 @@ //! ]); //! ``` -pub mod crypto; +mod crypto; pub mod tally; pub mod voter; -#[cfg(test)] -mod tests { - use proptest::prelude::ProptestConfig; - use test_strategy::{proptest, Arbitrary}; - - use crate::{ - crypto::SecretKey, - tally::{decrypt_tally, tally, DecryptionTallySetup}, - voter::{encrypt_vote, EncryptionRandomness, Vote}, - }; - - const VOTING_OPTIONS: usize = 3; - - #[derive(Arbitrary, Debug)] - struct Voter { - voting_power: u32, - // range from 0 to `VOTING_OPTIONS` - #[strategy(0..3_usize)] - choice: usize, - } - - #[proptest(ProptestConfig::with_cases(1))] - fn voting_test(voters: [Voter; 100]) { - let mut rng = rand_core::OsRng; - - let election_secret_key = SecretKey::generate(&mut rng); - let election_public_key = election_secret_key.public_key(); - - let votes: Vec<_> = voters - .iter() - .map(|voter| Vote::new(voter.choice, VOTING_OPTIONS).unwrap()) - .collect(); - - let voters_randomness: Vec<_> = (0..voters.len()) - .map(|_| EncryptionRandomness::generate(&mut rng, VOTING_OPTIONS)) - .collect(); - - let encrypted_votes: Vec<_> = votes - .iter() - .zip(voters_randomness.iter()) - .map(|(vote, r)| encrypt_vote(vote, &election_public_key, r).unwrap()) - .collect(); - - let voting_powers: Vec<_> = voters - .iter() - .map(|voter| u64::from(voter.voting_power)) - .collect(); - - let encrypted_tallies: Vec<_> = (0..VOTING_OPTIONS) - .map(|voting_option| tally(voting_option, &encrypted_votes, &voting_powers).unwrap()) - .collect(); - - let total_voting_power = voting_powers.iter().sum(); - let decryption_tally_setup = DecryptionTallySetup::new(total_voting_power).unwrap(); - - let decrypted_tallies: Vec<_> = encrypted_tallies - .iter() - .map(|t| decrypt_tally(t, &election_secret_key, &decryption_tally_setup).unwrap()) - .collect(); - - let expected_tallies: Vec<_> = (0..VOTING_OPTIONS) - .map(|i| { - voters - .iter() - .filter(|v| v.choice == i) - .map(|v| u64::from(v.voting_power)) - .sum::() - }) - .collect(); - - assert_eq!(decrypted_tallies, expected_tallies); - } -} +pub use crypto::elgamal::{PublicKey, SecretKey}; +pub use tally::{decrypt_tally, tally, DecryptionTallySetup}; +pub use voter::{encrypt_vote, EncryptionRandomness, Vote}; diff --git a/rust/catalyst-voting/tests/voting_test.rs b/rust/catalyst-voting/tests/voting_test.rs new file mode 100644 index 00000000000..d68ab42e43b --- /dev/null +++ b/rust/catalyst-voting/tests/voting_test.rs @@ -0,0 +1,69 @@ +//! A general voting integration test, which performs a full voting procedure. + +use catalyst_voting::{ + decrypt_tally, encrypt_vote, tally, DecryptionTallySetup, EncryptionRandomness, SecretKey, Vote, +}; +use proptest::prelude::ProptestConfig; +use test_strategy::{proptest, Arbitrary}; + +const VOTING_OPTIONS: usize = 3; + +#[derive(Arbitrary, Debug)] +struct Voter { + voting_power: u32, + // range from 0 to `VOTING_OPTIONS` + #[strategy(0..3_usize)] + choice: usize, +} + +#[proptest(ProptestConfig::with_cases(1))] +fn voting_test(voters: [Voter; 100]) { + let mut rng = rand_core::OsRng; + + let election_secret_key = SecretKey::generate(&mut rng); + let election_public_key = election_secret_key.public_key(); + + let votes: Vec<_> = voters + .iter() + .map(|voter| Vote::new(voter.choice, VOTING_OPTIONS).unwrap()) + .collect(); + + let voters_randomness: Vec<_> = (0..voters.len()) + .map(|_| EncryptionRandomness::generate(&mut rng, VOTING_OPTIONS)) + .collect(); + + let encrypted_votes: Vec<_> = votes + .iter() + .zip(voters_randomness.iter()) + .map(|(vote, r)| encrypt_vote(vote, &election_public_key, r).unwrap()) + .collect(); + + let voting_powers: Vec<_> = voters + .iter() + .map(|voter| u64::from(voter.voting_power)) + .collect(); + + let encrypted_tallies: Vec<_> = (0..VOTING_OPTIONS) + .map(|voting_option| tally(voting_option, &encrypted_votes, &voting_powers).unwrap()) + .collect(); + + let total_voting_power = voting_powers.iter().sum(); + let decryption_tally_setup = DecryptionTallySetup::new(total_voting_power).unwrap(); + + let decrypted_tallies: Vec<_> = encrypted_tallies + .iter() + .map(|t| decrypt_tally(t, &election_secret_key, &decryption_tally_setup).unwrap()) + .collect(); + + let expected_tallies: Vec<_> = (0..VOTING_OPTIONS) + .map(|i| { + voters + .iter() + .filter(|v| v.choice == i) + .map(|v| u64::from(v.voting_power)) + .sum::() + }) + .collect(); + + assert_eq!(decrypted_tallies, expected_tallies); +} From 8466187a4a3232b6ecd0a6f3e5f90522edc6da91 Mon Sep 17 00:00:00 2001 From: Mr-Leshiy Date: Tue, 1 Oct 2024 11:49:05 +0300 Subject: [PATCH 36/36] fix baby_step_giant_step_test --- rust/catalyst-voting/src/crypto/group/babystep_giantstep.rs | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/rust/catalyst-voting/src/crypto/group/babystep_giantstep.rs b/rust/catalyst-voting/src/crypto/group/babystep_giantstep.rs index 6f6d356feae..35bc38ada09 100644 --- a/rust/catalyst-voting/src/crypto/group/babystep_giantstep.rs +++ b/rust/catalyst-voting/src/crypto/group/babystep_giantstep.rs @@ -108,9 +108,11 @@ mod tests { use super::*; + // Starting `max_log_value` from 2 allows to eliminate possible `Invalid use of empty + // range 1..1` for `log` strategy #[proptest] fn baby_step_giant_step_test( - #[strategy(1..10000u64)] max_log_value: u64, #[strategy(1..#max_log_value)] log: u64, + #[strategy(2..10000u64)] max_log_value: u64, #[strategy(1..#max_log_value)] log: u64, ) { let ge = GroupElement::GENERATOR.mul(&Scalar::from(log));