From 05af5104bca1b2f0879fded9a4194ed1dc6b08c3 Mon Sep 17 00:00:00 2001 From: Mr-Leshiy Date: Tue, 24 Sep 2024 13:24:33 +0300 Subject: [PATCH 01/53] 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 85cd0539ea..b788d8a508 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 45da0f472d..930e2fc19c 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 0000000000..c064da2c51 --- /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 0000000000..d207001f1b --- /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/53] 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 d207001f1b..62a2608fd6 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/53] 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 2a74f564b9..ec7f9946e5 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 62a2608fd6..25ce6c07c7 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/53] fix --- rust/Earthfile | 11 ++++++++--- 1 file changed, 8 insertions(+), 3 deletions(-) diff --git a/rust/Earthfile b/rust/Earthfile index ec7f9946e5..1dd50692fa 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/53] 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 25ce6c07c7..d207001f1b 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/53] 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 03f0ee4a80..c152c545fd 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/53] 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 c064da2c51..9dee7f44f4 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 d207001f1b..5da0b88a5d 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/53] 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 9dee7f44f4..87abc931f9 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 0000000000..823e0dd866 --- /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 0000000000..25e4aac87a --- /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 0000000000..a16fab59d1 --- /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 0000000000..d168772ac0 --- /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 5da0b88a5d..83cbaf37e8 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/53] 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 87abc931f9..6cde59a662 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 a16fab59d1..3fae0081cb 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/53] 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 3fae0081cb..1129b65a18 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/53] 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 25e4aac87a..b484b966ed 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 1129b65a18..e248c7314e 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 0000000000..cad75596fa --- /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/53] 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 823e0dd866..1a437f3304 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 b484b966ed..c27928ba83 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/53] 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 36d32de2f4..303a0a748b 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 6cde59a662..ecb47fecc2 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 1a437f3304..ea7669823c 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 e248c7314e..aa8b9df54b 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 cad75596fa..f13197925f 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 83cbaf37e8..63f24bcaad 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/53] 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 c27928ba83..41c8c61592 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 f13197925f..0000000000 --- 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/53] 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 ecb47fecc2..c2ec52bcb1 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 ea7669823c..a8eb85f014 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 aa8b9df54b..641c3e1d5e 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 d168772ac0..16822f6efe 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 63f24bcaad..c81efedce8 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 0000000000..ebde2bed8a --- /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/53] 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 ebde2bed8a..a086b14dd2 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/53] 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 a8eb85f014..d845c95f6c 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 c81efedce8..5527ae70b4 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 0000000000..98f3eedc87 --- /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 a086b14dd2..5b98f20b60 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/53] 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 d845c95f6c..e2bec883ab 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 98f3eedc87..5f741529ab 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/53] 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/53] 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 0000000000..78b31d7e2d --- /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 41c8c61592..dc94e53b3a 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 641c3e1d5e..5dea4cd95c 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/53] 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 78b31d7e2d..1aab357093 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/53] 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 1aab357093..115ee8f944 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 dc94e53b3a..afea38ca87 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 5f741529ab..7081d27735 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/53] 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 7081d27735..483b786e28 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/53] 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 5527ae70b4..b73f8902cd 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 5b98f20b60..58ccc6d153 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/53] 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 e2bec883ab..91e85f6cd1 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 5dea4cd95c..3f2a718dba 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 b73f8902cd..e3cbae1708 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 483b786e28..43e823165d 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/53] 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 c2ec52bcb1..d81ad26deb 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 43e823165d..e3b3c8daec 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 58ccc6d153..c67041aa90 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/53] 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 303a0a748b..2f3da07262 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 d81ad26deb..1f3b4586df 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 115ee8f944..6f6d356fea 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 e3cbae1708..7f0edb1869 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 e3b3c8daec..b78a17e4f9 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/53] 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 7f0edb1869..ada735e576 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/53] 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 1f3b4586df..9044c3ce5e 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/53] 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 ada735e576..83ee116515 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/53] try --- rust/Earthfile | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/rust/Earthfile b/rust/Earthfile index cd1d38dfe0..ad579aaa9d 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/53] wip --- rust/Earthfile | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/rust/Earthfile b/rust/Earthfile index ad579aaa9d..cd1d38dfe0 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/53] 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 83ee116515..4e204b5ab2 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 b78a17e4f9..a6ef55a02b 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/53] 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 91e85f6cd1..c33ccfc2db 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 16822f6efe..f5f94bfd9b 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 4e204b5ab2..9b4c5e70a2 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/53] 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 f5f94bfd9b..16822f6efe 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 9b4c5e70a2..ef3e7456cd 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 0000000000..d68ab42e43 --- /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/53] 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 6f6d356fea..35bc38ada0 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)); From eafd5387f5d4c8e21feb2497d397c7a6564ca866 Mon Sep 17 00:00:00 2001 From: Mr-Leshiy Date: Tue, 1 Oct 2024 13:53:41 +0300 Subject: [PATCH 37/53] move tally module into the seprate dir --- rust/catalyst-voting/src/{tally.rs => tally/mod.rs} | 0 1 file changed, 0 insertions(+), 0 deletions(-) rename rust/catalyst-voting/src/{tally.rs => tally/mod.rs} (100%) diff --git a/rust/catalyst-voting/src/tally.rs b/rust/catalyst-voting/src/tally/mod.rs similarity index 100% rename from rust/catalyst-voting/src/tally.rs rename to rust/catalyst-voting/src/tally/mod.rs From ed25f6c09d92b8789bf6dd7d0b4009ff65c77b46 Mon Sep 17 00:00:00 2001 From: Mr-Leshiy Date: Tue, 1 Oct 2024 17:13:21 +0300 Subject: [PATCH 38/53] add new proof.rs --- rust/catalyst-voting/src/crypto/elgamal.rs | 22 +++++++++++++++++++++- rust/catalyst-voting/src/tally/mod.rs | 2 ++ rust/catalyst-voting/src/tally/proof.rs | 15 +++++++++++++++ 3 files changed, 38 insertions(+), 1 deletion(-) create mode 100644 rust/catalyst-voting/src/tally/proof.rs diff --git a/rust/catalyst-voting/src/crypto/elgamal.rs b/rust/catalyst-voting/src/crypto/elgamal.rs index c33ccfc2db..510d5688f2 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::{Add, Mul}; +use std::ops::{Add, Deref, Mul}; use rand_core::CryptoRngCore; @@ -19,6 +19,14 @@ pub struct PublicKey(GroupElement); #[derive(Debug, Clone, PartialEq, Eq)] pub struct Ciphertext(GroupElement, GroupElement); +impl Deref for SecretKey { + type Target = Scalar; + + fn deref(&self) -> &Self::Target { + &self.0 + } +} + impl SecretKey { /// Generate a random `SecretKey` value from the random number generator. pub fn generate(rng: &mut R) -> Self { @@ -38,6 +46,18 @@ impl Ciphertext { pub(crate) fn zero() -> Self { Ciphertext(GroupElement::zero(), GroupElement::zero()) } + + /// Get the first element of the `Ciphertext`. + #[allow(dead_code)] + pub(crate) fn first(&self) -> &GroupElement { + &self.0 + } + + /// Get the second element of the `Ciphertext`. + #[allow(dead_code)] + pub(crate) fn second(&self) -> &GroupElement { + &self.1 + } } /// Given a `message` represented as a `Scalar`, return a ciphertext using the diff --git a/rust/catalyst-voting/src/tally/mod.rs b/rust/catalyst-voting/src/tally/mod.rs index a6ef55a02b..007acac86c 100644 --- a/rust/catalyst-voting/src/tally/mod.rs +++ b/rust/catalyst-voting/src/tally/mod.rs @@ -1,5 +1,7 @@ //! Module containing all primitives related to the tally process. +pub mod proof; + use std::ops::{Add, Mul}; use crate::{ diff --git a/rust/catalyst-voting/src/tally/proof.rs b/rust/catalyst-voting/src/tally/proof.rs new file mode 100644 index 0000000000..a214bc6482 --- /dev/null +++ b/rust/catalyst-voting/src/tally/proof.rs @@ -0,0 +1,15 @@ +//! Tally proof generation and verification procedures. +//! It allows to transparently verify the correctness of decryption tally procedure. + +use rand_core::CryptoRngCore; + +use super::EncryptedTally; +use crate::SecretKey; + +/// Generates a tally proof. +/// More detailed described [here](https://input-output-hk.github.io/catalyst-voices/architecture/08_concepts/voting_transaction/crypto/#tally-proof) +#[allow(clippy::module_name_repetitions)] +pub fn generate_tally_proof( + _tally_result: &EncryptedTally, _secret_key: &SecretKey, _rng: &mut R, +) { +} From 5918f0d6f462ac962ddff981aa693686b43ea950 Mon Sep 17 00:00:00 2001 From: Mr-Leshiy Date: Tue, 1 Oct 2024 18:31:22 +0300 Subject: [PATCH 39/53] refactor --- .../src/crypto/{group => }/babystep_giantstep.rs | 2 +- rust/catalyst-voting/src/crypto/group/mod.rs | 2 -- rust/catalyst-voting/src/crypto/mod.rs | 1 + rust/catalyst-voting/src/tally/mod.rs | 3 ++- 4 files changed, 4 insertions(+), 4 deletions(-) rename rust/catalyst-voting/src/crypto/{group => }/babystep_giantstep.rs (99%) diff --git a/rust/catalyst-voting/src/crypto/group/babystep_giantstep.rs b/rust/catalyst-voting/src/crypto/babystep_giantstep.rs similarity index 99% rename from rust/catalyst-voting/src/crypto/group/babystep_giantstep.rs rename to rust/catalyst-voting/src/crypto/babystep_giantstep.rs index 35bc38ada0..891b2695a4 100644 --- a/rust/catalyst-voting/src/crypto/group/babystep_giantstep.rs +++ b/rust/catalyst-voting/src/crypto/babystep_giantstep.rs @@ -3,7 +3,7 @@ use std::collections::HashMap; -use super::{GroupElement, Scalar}; +use super::group::{GroupElement, Scalar}; /// Default balance value. /// Make steps asymmetric, in order to better use caching of baby steps. diff --git a/rust/catalyst-voting/src/crypto/group/mod.rs b/rust/catalyst-voting/src/crypto/group/mod.rs index afea38ca87..d76e332903 100644 --- a/rust/catalyst-voting/src/crypto/group/mod.rs +++ b/rust/catalyst-voting/src/crypto/group/mod.rs @@ -1,8 +1,6 @@ //! Group definitions used in voting protocol. //! For more information, see: -mod babystep_giantstep; mod ristretto255; -pub(crate) use babystep_giantstep::BabyStepGiantStep; pub(crate) use ristretto255::{GroupElement, Scalar}; diff --git a/rust/catalyst-voting/src/crypto/mod.rs b/rust/catalyst-voting/src/crypto/mod.rs index 16822f6efe..c8e83c7360 100644 --- a/rust/catalyst-voting/src/crypto/mod.rs +++ b/rust/catalyst-voting/src/crypto/mod.rs @@ -1,4 +1,5 @@ //! Crypto primitives which are used by voting protocol. +pub(crate) mod babystep_giantstep; pub(crate) mod elgamal; pub(crate) mod group; diff --git a/rust/catalyst-voting/src/tally/mod.rs b/rust/catalyst-voting/src/tally/mod.rs index 007acac86c..81cd10990e 100644 --- a/rust/catalyst-voting/src/tally/mod.rs +++ b/rust/catalyst-voting/src/tally/mod.rs @@ -6,8 +6,9 @@ use std::ops::{Add, Mul}; use crate::{ crypto::{ + babystep_giantstep::BabyStepGiantStep, elgamal::{decrypt, Ciphertext, SecretKey}, - group::{BabyStepGiantStep, Scalar}, + group::Scalar, }, voter::EncryptedVote, }; From 87af867d331ccd768cbe423c53e767cc71fd639c Mon Sep 17 00:00:00 2001 From: Mr-Leshiy Date: Tue, 1 Oct 2024 19:19:59 +0300 Subject: [PATCH 40/53] add to_bytes, from_bytes functions for Scalar and GroupElement --- .../src/crypto/group/ristretto255.rs | 40 ++++++++++++++++++- 1 file changed, 39 insertions(+), 1 deletion(-) diff --git a/rust/catalyst-voting/src/crypto/group/ristretto255.rs b/rust/catalyst-voting/src/crypto/group/ristretto255.rs index 3f2a718dba..5167a52bf4 100644 --- a/rust/catalyst-voting/src/crypto/group/ristretto255.rs +++ b/rust/catalyst-voting/src/crypto/group/ristretto255.rs @@ -9,7 +9,7 @@ use std::{ use curve25519_dalek::{ constants::{RISTRETTO_BASEPOINT_POINT, RISTRETTO_BASEPOINT_TABLE}, - ristretto::RistrettoPoint as Point, + ristretto::{CompressedRistretto, RistrettoPoint as Point}, scalar::Scalar as IScalar, traits::Identity, }; @@ -67,6 +67,16 @@ impl Scalar { pub fn inverse(&self) -> Scalar { Scalar(self.0.invert()) } + + /// Convert this `Scalar` to its underlying sequence of bytes. + pub fn to_bytes(&self) -> [u8; 32] { + self.0.to_bytes() + } + + /// Attempt to construct a `Scalar` from a canonical byte representation. + pub fn from_bytes(bytes: [u8; 32]) -> Option { + IScalar::from_canonical_bytes(bytes).map(Scalar).into() + } } impl GroupElement { @@ -77,6 +87,19 @@ impl GroupElement { pub fn zero() -> Self { GroupElement(Point::identity()) } + + /// Convert this `GroupElement` to its underlying sequence of bytes. + /// Always encode the compressed value. + pub fn to_bytes(&self) -> [u8; 32] { + self.0.compress().to_bytes() + } + + /// Attempt to construct a `Scalar` from a compressed value byte representation. + pub fn from_bytes(bytes: &[u8; 32]) -> Option { + Some(GroupElement( + CompressedRistretto::from_slice(bytes).ok()?.decompress()?, + )) + } } // `std::ops` traits implementations @@ -152,6 +175,21 @@ mod tests { } } + #[proptest] + fn scalar_to_bytes_from_bytes_test(e1: Scalar) { + let bytes = e1.to_bytes(); + let e2 = Scalar::from_bytes(bytes).unwrap(); + assert_eq!(e1, e2); + } + + #[proptest] + fn group_element_to_bytes_from_bytes_test(e: Scalar) { + let ge1 = GroupElement::GENERATOR.mul(&e); + let bytes = ge1.to_bytes(); + let ge2 = GroupElement::from_bytes(&bytes).unwrap(); + assert_eq!(ge1, ge2); + } + #[proptest] fn scalar_arithmetic_tests(e1: Scalar, e2: Scalar, e3: Scalar) { assert_eq!(&(&e1 + &e2) + &e3, &e1 + &(&e2 + &e3)); From 5c399b9e01a814ba47bad5d5623a95c3770b6ccb Mon Sep 17 00:00:00 2001 From: Mr-Leshiy Date: Tue, 1 Oct 2024 20:00:31 +0300 Subject: [PATCH 41/53] add zk_dl_equality.rs --- rust/catalyst-voting/Cargo.toml | 1 + rust/catalyst-voting/src/crypto/mod.rs | 1 + .../src/crypto/zk_dl_equality.rs | 40 +++++++++++++++++++ 3 files changed, 42 insertions(+) create mode 100644 rust/catalyst-voting/src/crypto/zk_dl_equality.rs diff --git a/rust/catalyst-voting/Cargo.toml b/rust/catalyst-voting/Cargo.toml index 9044c3ce5e..9f31c6b2c0 100644 --- a/rust/catalyst-voting/Cargo.toml +++ b/rust/catalyst-voting/Cargo.toml @@ -14,6 +14,7 @@ workspace = true thiserror = "1.0.56" rand_core = "0.6.4" curve25519-dalek = { version = "4.0" } +blake2b_simd = "1.0.2" [dev-dependencies] proptest = {version = "1.5.0" } diff --git a/rust/catalyst-voting/src/crypto/mod.rs b/rust/catalyst-voting/src/crypto/mod.rs index c8e83c7360..a2de849859 100644 --- a/rust/catalyst-voting/src/crypto/mod.rs +++ b/rust/catalyst-voting/src/crypto/mod.rs @@ -3,3 +3,4 @@ pub(crate) mod babystep_giantstep; pub(crate) mod elgamal; pub(crate) mod group; +pub(crate) mod zk_dl_equality; diff --git a/rust/catalyst-voting/src/crypto/zk_dl_equality.rs b/rust/catalyst-voting/src/crypto/zk_dl_equality.rs new file mode 100644 index 0000000000..51f50602ba --- /dev/null +++ b/rust/catalyst-voting/src/crypto/zk_dl_equality.rs @@ -0,0 +1,40 @@ +//! Non-interactive Zero Knowledge proof of Discrete Logarithm +//! Equality (DLEQ). +//! +//! The proof is the following: +//! +//! `NIZK{(base_1, base_2, point_1, point_2), (dlog): point_1 = base_1^dlog AND point_2 = +//! base_2^dlog}` +//! +//! which makes the statement, the two bases `base_1` and `base_2`, and the two +//! points `point_1` and `point_2`. The witness, on the other hand +//! is the discrete logarithm, `dlog`. + +#![allow(dead_code, unused_variables)] + +use super::group::{GroupElement, Scalar}; + +/// DLEQ proof struct +pub struct DleqProof(Scalar, Scalar); + +/// Generates a DLEQ proof. +pub fn generate_dleq_proof( + base_1: &GroupElement, base_2: &GroupElement, point_1: &GroupElement, point_2: &GroupElement, + dlog: &Scalar, randomness: &Scalar, +) -> DleqProof { + let a_1 = base_1 * randomness; + let a_2 = base_2 * randomness; + + let mut blake2b_hasher = blake2b_simd::State::new(); + blake2b_hasher.update(&a_1.to_bytes()); + blake2b_hasher.update(&a_2.to_bytes()); + blake2b_hasher.update(&point_1.to_bytes()); + blake2b_hasher.update(&point_2.to_bytes()); + let hash = blake2b_hasher.finalize(); + hash.as_bytes(); + + let challenge = Scalar::zero(); + let response = &(dlog * &challenge) + randomness; + + DleqProof(challenge, response) +} From dde81b84070c121cc59a88c36408120665c5eb9c Mon Sep 17 00:00:00 2001 From: Mr-Leshiy Date: Wed, 2 Oct 2024 10:25:43 +0300 Subject: [PATCH 42/53] add hash module --- rust/catalyst-voting/src/crypto/hash.rs | 61 +++++++++++++++++++++++++ rust/catalyst-voting/src/crypto/mod.rs | 1 + 2 files changed, 62 insertions(+) create mode 100644 rust/catalyst-voting/src/crypto/hash.rs diff --git a/rust/catalyst-voting/src/crypto/hash.rs b/rust/catalyst-voting/src/crypto/hash.rs new file mode 100644 index 0000000000..0e6763d48e --- /dev/null +++ b/rust/catalyst-voting/src/crypto/hash.rs @@ -0,0 +1,61 @@ +//! Blake2b-256 hash implementation. + +#![allow(dead_code)] + +/// Blake2b-256 hasher instance. +pub(crate) struct Blake2b256Hasher(blake2b_simd::State); + +impl Blake2b256Hasher { + /// Create a new `Blake2b256Hasher`. + pub(crate) fn new() -> Self { + Self( + blake2b_simd::Params::new() + .hash_length(Blake2b256::HASH_SIZE) + .to_state(), + ) + } + + /// Incrementally add bytes to the hasher. + pub(crate) fn update(&mut self, bytes: &[u8]) { + self.0.update(bytes); + } + + /// Finalize the state and return a `Hash`. + pub(crate) fn finalize(self) -> Blake2b256 { + let hash = self.0.finalize(); + Blake2b256::from_bytes_unchecked(hash.as_bytes()) + } +} + +/// Blake2b-256 hash instance. +#[derive(Clone, Debug, PartialEq, Eq, Hash)] +pub(crate) struct Blake2b256([u8; Self::HASH_SIZE]); + +impl Blake2b256 { + /// Blake2b-256 hash size. + const HASH_SIZE: usize = 32; + + /// Create a new `Blake2b256` from bytes. + /// It does not validate the size of the bytes, so all checks should be done by + /// the caller. + fn from_bytes_unchecked(bytes: &[u8]) -> Self { + let mut hash_bytes = [0; Self::HASH_SIZE]; + hash_bytes.copy_from_slice(bytes); + + Self(hash_bytes) + } + + /// Calculate a new `Blake2b256` from bytes. + pub(crate) fn hash(bytes: &[u8]) -> Self { + let hash = blake2b_simd::Params::new() + .hash_length(Self::HASH_SIZE) + .hash(bytes); + + Self::from_bytes_unchecked(hash.as_bytes()) + } + + /// Return the hash bytes. + pub(crate) fn to_bytes(&self) -> [u8; Self::HASH_SIZE] { + self.0 + } +} diff --git a/rust/catalyst-voting/src/crypto/mod.rs b/rust/catalyst-voting/src/crypto/mod.rs index a2de849859..69d41e87d6 100644 --- a/rust/catalyst-voting/src/crypto/mod.rs +++ b/rust/catalyst-voting/src/crypto/mod.rs @@ -3,4 +3,5 @@ pub(crate) mod babystep_giantstep; pub(crate) mod elgamal; pub(crate) mod group; +pub(crate) mod hash; pub(crate) mod zk_dl_equality; From e7d50791799946ffdf263e8136c50596387a00d5 Mon Sep 17 00:00:00 2001 From: Mr-Leshiy Date: Wed, 2 Oct 2024 11:07:31 +0300 Subject: [PATCH 43/53] update hash implementation --- rust/catalyst-voting/Cargo.toml | 2 +- .../src/crypto/group/ristretto255.rs | 7 ++ rust/catalyst-voting/src/crypto/hash.rs | 65 ++++++++----------- .../src/crypto/zk_dl_equality.rs | 21 +++--- 4 files changed, 48 insertions(+), 47 deletions(-) diff --git a/rust/catalyst-voting/Cargo.toml b/rust/catalyst-voting/Cargo.toml index 9f31c6b2c0..e26728cea5 100644 --- a/rust/catalyst-voting/Cargo.toml +++ b/rust/catalyst-voting/Cargo.toml @@ -13,7 +13,7 @@ workspace = true [dependencies] thiserror = "1.0.56" rand_core = "0.6.4" -curve25519-dalek = { version = "4.0" } +curve25519-dalek = { version = "4.0", features = ["digest"] } blake2b_simd = "1.0.2" [dev-dependencies] diff --git a/rust/catalyst-voting/src/crypto/group/ristretto255.rs b/rust/catalyst-voting/src/crypto/group/ristretto255.rs index 5167a52bf4..cc0ce54209 100644 --- a/rust/catalyst-voting/src/crypto/group/ristretto255.rs +++ b/rust/catalyst-voting/src/crypto/group/ristretto255.rs @@ -9,6 +9,7 @@ use std::{ use curve25519_dalek::{ constants::{RISTRETTO_BASEPOINT_POINT, RISTRETTO_BASEPOINT_TABLE}, + digest::{consts::U64, Digest}, ristretto::{CompressedRistretto, RistrettoPoint as Point}, scalar::Scalar as IScalar, traits::Identity, @@ -77,6 +78,12 @@ impl Scalar { pub fn from_bytes(bytes: [u8; 32]) -> Option { IScalar::from_canonical_bytes(bytes).map(Scalar).into() } + + /// Generate a `Scalar` from a hash digest. + pub fn from_hash(hash: D) -> Scalar + where D: Digest { + Scalar(IScalar::from_hash(hash)) + } } impl GroupElement { diff --git a/rust/catalyst-voting/src/crypto/hash.rs b/rust/catalyst-voting/src/crypto/hash.rs index 0e6763d48e..77e65173ec 100644 --- a/rust/catalyst-voting/src/crypto/hash.rs +++ b/rust/catalyst-voting/src/crypto/hash.rs @@ -2,60 +2,51 @@ #![allow(dead_code)] -/// Blake2b-256 hasher instance. -pub(crate) struct Blake2b256Hasher(blake2b_simd::State); +use curve25519_dalek::digest::{ + consts::U64, typenum::Unsigned, FixedOutput, HashMarker, Output, OutputSizeUser, Update, +}; -impl Blake2b256Hasher { +/// Blake2b-512 hasher instance. +pub(crate) struct Blake2b512Hasher(blake2b_simd::State); + +impl Blake2b512Hasher { /// Create a new `Blake2b256Hasher`. pub(crate) fn new() -> Self { Self( blake2b_simd::Params::new() - .hash_length(Blake2b256::HASH_SIZE) + .hash_length(Self::output_size()) .to_state(), ) } +} - /// Incrementally add bytes to the hasher. - pub(crate) fn update(&mut self, bytes: &[u8]) { - self.0.update(bytes); - } +// Implementation of the `digest::Digest` trait for `Blake2b256Hasher`. - /// Finalize the state and return a `Hash`. - pub(crate) fn finalize(self) -> Blake2b256 { - let hash = self.0.finalize(); - Blake2b256::from_bytes_unchecked(hash.as_bytes()) +impl Default for Blake2b512Hasher { + fn default() -> Self { + Self::new() } } -/// Blake2b-256 hash instance. -#[derive(Clone, Debug, PartialEq, Eq, Hash)] -pub(crate) struct Blake2b256([u8; Self::HASH_SIZE]); - -impl Blake2b256 { - /// Blake2b-256 hash size. - const HASH_SIZE: usize = 32; - - /// Create a new `Blake2b256` from bytes. - /// It does not validate the size of the bytes, so all checks should be done by - /// the caller. - fn from_bytes_unchecked(bytes: &[u8]) -> Self { - let mut hash_bytes = [0; Self::HASH_SIZE]; - hash_bytes.copy_from_slice(bytes); - - Self(hash_bytes) +impl Update for Blake2b512Hasher { + fn update(&mut self, data: &[u8]) { + self.0.update(data); } +} - /// Calculate a new `Blake2b256` from bytes. - pub(crate) fn hash(bytes: &[u8]) -> Self { - let hash = blake2b_simd::Params::new() - .hash_length(Self::HASH_SIZE) - .hash(bytes); +impl OutputSizeUser for Blake2b512Hasher { + type OutputSize = U64; - Self::from_bytes_unchecked(hash.as_bytes()) + fn output_size() -> usize { + Self::OutputSize::USIZE } +} - /// Return the hash bytes. - pub(crate) fn to_bytes(&self) -> [u8; Self::HASH_SIZE] { - self.0 +impl FixedOutput for Blake2b512Hasher { + fn finalize_into(self, out: &mut Output) { + let hash = self.0.finalize(); + out.copy_from_slice(hash.as_bytes()); } } + +impl HashMarker for Blake2b512Hasher {} diff --git a/rust/catalyst-voting/src/crypto/zk_dl_equality.rs b/rust/catalyst-voting/src/crypto/zk_dl_equality.rs index 51f50602ba..391e617a7f 100644 --- a/rust/catalyst-voting/src/crypto/zk_dl_equality.rs +++ b/rust/catalyst-voting/src/crypto/zk_dl_equality.rs @@ -12,7 +12,12 @@ #![allow(dead_code, unused_variables)] -use super::group::{GroupElement, Scalar}; +use curve25519_dalek::digest::Update; + +use super::{ + group::{GroupElement, Scalar}, + hash::Blake2b512Hasher, +}; /// DLEQ proof struct pub struct DleqProof(Scalar, Scalar); @@ -25,15 +30,13 @@ pub fn generate_dleq_proof( let a_1 = base_1 * randomness; let a_2 = base_2 * randomness; - let mut blake2b_hasher = blake2b_simd::State::new(); - blake2b_hasher.update(&a_1.to_bytes()); - blake2b_hasher.update(&a_2.to_bytes()); - blake2b_hasher.update(&point_1.to_bytes()); - blake2b_hasher.update(&point_2.to_bytes()); - let hash = blake2b_hasher.finalize(); - hash.as_bytes(); + let blake2b_hasher = Blake2b512Hasher::new() + .chain(a_1.to_bytes()) + .chain(a_2.to_bytes()) + .chain(point_1.to_bytes()) + .chain(point_2.to_bytes()); - let challenge = Scalar::zero(); + let challenge = Scalar::from_hash(blake2b_hasher); let response = &(dlog * &challenge) + randomness; DleqProof(challenge, response) From c275f5aa757285db450d5d38cdb011358da60b32 Mon Sep 17 00:00:00 2001 From: Mr-Leshiy Date: Wed, 2 Oct 2024 11:53:28 +0300 Subject: [PATCH 44/53] add dleq verify function, add tests --- .../src/crypto/group/ristretto255.rs | 9 ++ rust/catalyst-voting/src/crypto/hash.rs | 6 +- .../src/crypto/zk_dl_equality.rs | 82 +++++++++++++++++-- 3 files changed, 87 insertions(+), 10 deletions(-) diff --git a/rust/catalyst-voting/src/crypto/group/ristretto255.rs b/rust/catalyst-voting/src/crypto/group/ristretto255.rs index cc0ce54209..092bec59a6 100644 --- a/rust/catalyst-voting/src/crypto/group/ristretto255.rs +++ b/rust/catalyst-voting/src/crypto/group/ristretto255.rs @@ -163,6 +163,14 @@ impl Sub<&Scalar> for &Scalar { } } +impl Sub<&GroupElement> for &GroupElement { + type Output = GroupElement; + + fn sub(self, other: &GroupElement) -> GroupElement { + GroupElement(self.0 + (-other.0)) + } +} + #[cfg(test)] mod tests { use proptest::{ @@ -219,6 +227,7 @@ mod tests { let ge3 = GroupElement::GENERATOR.mul(&(&e1 + &e2)); assert_eq!(&ge1 + &ge2, ge3); + assert_eq!(&(&ge1 + &ge2) - &ge2, ge1); let ge = GroupElement::GENERATOR.mul(&e1).mul(&e1.inverse()); assert_eq!(ge, GroupElement::GENERATOR); diff --git a/rust/catalyst-voting/src/crypto/hash.rs b/rust/catalyst-voting/src/crypto/hash.rs index 77e65173ec..f767f0afbd 100644 --- a/rust/catalyst-voting/src/crypto/hash.rs +++ b/rust/catalyst-voting/src/crypto/hash.rs @@ -1,17 +1,15 @@ //! Blake2b-256 hash implementation. -#![allow(dead_code)] - use curve25519_dalek::digest::{ consts::U64, typenum::Unsigned, FixedOutput, HashMarker, Output, OutputSizeUser, Update, }; /// Blake2b-512 hasher instance. -pub(crate) struct Blake2b512Hasher(blake2b_simd::State); +pub struct Blake2b512Hasher(blake2b_simd::State); impl Blake2b512Hasher { /// Create a new `Blake2b256Hasher`. - pub(crate) fn new() -> Self { + pub fn new() -> Self { Self( blake2b_simd::Params::new() .hash_length(Self::output_size()) diff --git a/rust/catalyst-voting/src/crypto/zk_dl_equality.rs b/rust/catalyst-voting/src/crypto/zk_dl_equality.rs index 391e617a7f..c1e0765480 100644 --- a/rust/catalyst-voting/src/crypto/zk_dl_equality.rs +++ b/rust/catalyst-voting/src/crypto/zk_dl_equality.rs @@ -30,14 +30,84 @@ pub fn generate_dleq_proof( let a_1 = base_1 * randomness; let a_2 = base_2 * randomness; + let challenge = calculate_challenge(base_1, base_2, point_1, point_2, &a_1, &a_2); + let response = &(dlog * &challenge) + randomness; + + DleqProof(challenge, response) +} + +/// Verify a DLEQ proof. +pub fn verify_dleq_proof( + proof: &DleqProof, base_1: &GroupElement, base_2: &GroupElement, point_1: &GroupElement, + point_2: &GroupElement, +) -> bool { + let r1 = base_1 * &proof.1; + let r2 = base_2 * &proof.1; + let a_1 = &r1 - &(point_1 * &proof.0); + let a_2 = &r2 - &(point_2 * &proof.0); + + let challenge = calculate_challenge(base_1, base_2, point_1, point_2, &a_1, &a_2); + challenge == proof.0 +} + +/// Calculates the challenge value. +/// Its a hash value of all provided elements represented as `Scalar`. +fn calculate_challenge( + base_1: &GroupElement, base_2: &GroupElement, point_1: &GroupElement, point_2: &GroupElement, + a_1: &GroupElement, a_2: &GroupElement, +) -> Scalar { let blake2b_hasher = Blake2b512Hasher::new() - .chain(a_1.to_bytes()) - .chain(a_2.to_bytes()) + .chain(base_1.to_bytes()) + .chain(base_2.to_bytes()) .chain(point_1.to_bytes()) - .chain(point_2.to_bytes()); + .chain(point_2.to_bytes()) + .chain(a_1.to_bytes()) + .chain(a_2.to_bytes()); - let challenge = Scalar::from_hash(blake2b_hasher); - let response = &(dlog * &challenge) + randomness; + Scalar::from_hash(blake2b_hasher) +} - DleqProof(challenge, response) +#[cfg(test)] +mod tests { + use std::ops::Mul; + + use test_strategy::proptest; + + use super::*; + + #[proptest] + fn zk_dleq_test(e1: Scalar, e2: Scalar, dlog1: Scalar, dlog2: Scalar, randomness: Scalar) { + let base_1 = GroupElement::GENERATOR.mul(&e1); + let base_2 = GroupElement::GENERATOR.mul(&e2); + + let point_1 = base_1.mul(&dlog1); + let point_2 = base_2.mul(&dlog1); + + let proof = generate_dleq_proof(&base_1, &base_2, &point_1, &point_2, &dlog1, &randomness); + assert!(verify_dleq_proof( + &proof, &base_1, &base_2, &point_1, &point_2 + )); + + // use different discrete logarithm for both points + let point_1 = base_1.mul(&dlog2); + let point_2 = base_2.mul(&dlog2); + + let proof = generate_dleq_proof(&base_1, &base_2, &point_1, &point_2, &dlog1, &randomness); + assert!(!verify_dleq_proof( + &proof, &base_1, &base_2, &point_1, &point_2 + )); + + // use different discrete logarithm across points + let point_1 = base_1.mul(&dlog1); + let point_2 = base_2.mul(&dlog2); + + let proof = generate_dleq_proof(&base_1, &base_2, &point_1, &point_2, &dlog1, &randomness); + assert!(!verify_dleq_proof( + &proof, &base_1, &base_2, &point_1, &point_2 + )); + let proof = generate_dleq_proof(&base_1, &base_2, &point_1, &point_2, &dlog2, &randomness); + assert!(!verify_dleq_proof( + &proof, &base_1, &base_2, &point_1, &point_2 + )); + } } From ae6c00cd3efcf878402391777155da2392e7287e Mon Sep 17 00:00:00 2001 From: Mr-Leshiy Date: Wed, 2 Oct 2024 12:43:55 +0300 Subject: [PATCH 45/53] implement tally proof generation and tally proof verification --- rust/catalyst-voting/src/crypto/elgamal.rs | 8 ++++ .../src/crypto/zk_dl_equality.rs | 8 +--- rust/catalyst-voting/src/tally/proof.rs | 47 +++++++++++++++++-- 3 files changed, 54 insertions(+), 9 deletions(-) diff --git a/rust/catalyst-voting/src/crypto/elgamal.rs b/rust/catalyst-voting/src/crypto/elgamal.rs index 510d5688f2..785c516a29 100644 --- a/rust/catalyst-voting/src/crypto/elgamal.rs +++ b/rust/catalyst-voting/src/crypto/elgamal.rs @@ -27,6 +27,14 @@ impl Deref for SecretKey { } } +impl Deref for PublicKey { + type Target = GroupElement; + + fn deref(&self) -> &Self::Target { + &self.0 + } +} + impl SecretKey { /// Generate a random `SecretKey` value from the random number generator. pub fn generate(rng: &mut R) -> Self { diff --git a/rust/catalyst-voting/src/crypto/zk_dl_equality.rs b/rust/catalyst-voting/src/crypto/zk_dl_equality.rs index c1e0765480..84c5d90e3b 100644 --- a/rust/catalyst-voting/src/crypto/zk_dl_equality.rs +++ b/rust/catalyst-voting/src/crypto/zk_dl_equality.rs @@ -10,8 +10,6 @@ //! points `point_1` and `point_2`. The witness, on the other hand //! is the discrete logarithm, `dlog`. -#![allow(dead_code, unused_variables)] - use curve25519_dalek::digest::Update; use super::{ @@ -41,10 +39,8 @@ pub fn verify_dleq_proof( proof: &DleqProof, base_1: &GroupElement, base_2: &GroupElement, point_1: &GroupElement, point_2: &GroupElement, ) -> bool { - let r1 = base_1 * &proof.1; - let r2 = base_2 * &proof.1; - let a_1 = &r1 - &(point_1 * &proof.0); - let a_2 = &r2 - &(point_2 * &proof.0); + let a_1 = &(base_1 * &proof.1) - &(point_1 * &proof.0); + let a_2 = &(base_2 * &proof.1) - &(point_2 * &proof.0); let challenge = calculate_challenge(base_1, base_2, point_1, point_2, &a_1, &a_2); challenge == proof.0 diff --git a/rust/catalyst-voting/src/tally/proof.rs b/rust/catalyst-voting/src/tally/proof.rs index a214bc6482..8182ac374d 100644 --- a/rust/catalyst-voting/src/tally/proof.rs +++ b/rust/catalyst-voting/src/tally/proof.rs @@ -1,15 +1,56 @@ //! Tally proof generation and verification procedures. //! It allows to transparently verify the correctness of decryption tally procedure. +use std::ops::Mul; + use rand_core::CryptoRngCore; use super::EncryptedTally; -use crate::SecretKey; +use crate::{ + crypto::{ + group::{GroupElement, Scalar}, + zk_dl_equality::{generate_dleq_proof, verify_dleq_proof, DleqProof}, + }, + PublicKey, SecretKey, +}; + +/// Tally proof struct. +#[allow(clippy::module_name_repetitions)] +pub struct TallyProof(DleqProof); /// Generates a tally proof. /// More detailed described [here](https://input-output-hk.github.io/catalyst-voices/architecture/08_concepts/voting_transaction/crypto/#tally-proof) #[allow(clippy::module_name_repetitions)] pub fn generate_tally_proof( - _tally_result: &EncryptedTally, _secret_key: &SecretKey, _rng: &mut R, -) { + encrypted_tally: &EncryptedTally, secret_key: &SecretKey, rng: &mut R, +) -> TallyProof { + let randomness = Scalar::random(rng); + let e1 = encrypted_tally.0.first(); + let d = e1.mul(secret_key); + + let proof = generate_dleq_proof( + &GroupElement::GENERATOR, + e1, + &secret_key.public_key(), + &d, + secret_key, + &randomness, + ); + + TallyProof(proof) +} + +/// Verifies a tally proof. +/// More detailed described [here](https://input-output-hk.github.io/catalyst-voices/architecture/08_concepts/voting_transaction/crypto/#tally-proof) +#[must_use] +#[allow(clippy::module_name_repetitions)] +pub fn verify_tally_proof( + encrypted_tally: &EncryptedTally, tally: u64, public_key: &PublicKey, proof: &TallyProof, +) -> bool { + let tally = Scalar::from(tally); + let e1 = encrypted_tally.0.first(); + let e2 = encrypted_tally.0.second(); + let d = &GroupElement::GENERATOR.mul(&tally) - e2; + + verify_dleq_proof(&proof.0, &GroupElement::GENERATOR, e1, public_key, &d) } From 0a9ac9b4f3e95adff0177291bc85d2dfb211b325 Mon Sep 17 00:00:00 2001 From: Mr-Leshiy Date: Wed, 2 Oct 2024 14:59:26 +0300 Subject: [PATCH 46/53] update voting_test with the tally proofs, fix verify_tally_proof --- rust/catalyst-voting/src/lib.rs | 4 ++-- rust/catalyst-voting/src/tally/proof.rs | 2 +- rust/catalyst-voting/tests/voting_test.rs | 16 +++++++++++++++- 3 files changed, 18 insertions(+), 4 deletions(-) diff --git a/rust/catalyst-voting/src/lib.rs b/rust/catalyst-voting/src/lib.rs index ef3e7456cd..4d0ab83b91 100644 --- a/rust/catalyst-voting/src/lib.rs +++ b/rust/catalyst-voting/src/lib.rs @@ -79,5 +79,5 @@ pub mod tally; pub mod voter; pub use crypto::elgamal::{PublicKey, SecretKey}; -pub use tally::{decrypt_tally, tally, DecryptionTallySetup}; -pub use voter::{encrypt_vote, EncryptionRandomness, Vote}; +pub use tally::{proof::*, *}; +pub use voter::*; diff --git a/rust/catalyst-voting/src/tally/proof.rs b/rust/catalyst-voting/src/tally/proof.rs index 8182ac374d..8417bf9fa7 100644 --- a/rust/catalyst-voting/src/tally/proof.rs +++ b/rust/catalyst-voting/src/tally/proof.rs @@ -50,7 +50,7 @@ pub fn verify_tally_proof( let tally = Scalar::from(tally); let e1 = encrypted_tally.0.first(); let e2 = encrypted_tally.0.second(); - let d = &GroupElement::GENERATOR.mul(&tally) - e2; + let d = e2 - &GroupElement::GENERATOR.mul(&tally); verify_dleq_proof(&proof.0, &GroupElement::GENERATOR, e1, public_key, &d) } diff --git a/rust/catalyst-voting/tests/voting_test.rs b/rust/catalyst-voting/tests/voting_test.rs index d68ab42e43..95750395bc 100644 --- a/rust/catalyst-voting/tests/voting_test.rs +++ b/rust/catalyst-voting/tests/voting_test.rs @@ -1,7 +1,8 @@ //! A general voting integration test, which performs a full voting procedure. use catalyst_voting::{ - decrypt_tally, encrypt_vote, tally, DecryptionTallySetup, EncryptionRandomness, SecretKey, Vote, + decrypt_tally, encrypt_vote, generate_tally_proof, tally, verify_tally_proof, + DecryptionTallySetup, EncryptionRandomness, SecretKey, Vote, }; use proptest::prelude::ProptestConfig; use test_strategy::{proptest, Arbitrary}; @@ -50,11 +51,24 @@ fn voting_test(voters: [Voter; 100]) { let total_voting_power = voting_powers.iter().sum(); let decryption_tally_setup = DecryptionTallySetup::new(total_voting_power).unwrap(); + let tally_proofs: Vec<_> = encrypted_tallies + .iter() + .map(|t| generate_tally_proof(t, &election_secret_key, &mut rng)) + .collect(); + let decrypted_tallies: Vec<_> = encrypted_tallies .iter() .map(|t| decrypt_tally(t, &election_secret_key, &decryption_tally_setup).unwrap()) .collect(); + // verify tally proofs + let is_ok = tally_proofs + .iter() + .zip(encrypted_tallies.iter()) + .zip(decrypted_tallies.iter()) + .all(|((p, enc_t), t)| verify_tally_proof(enc_t, *t, &election_public_key, p)); + assert!(is_ok); + let expected_tallies: Vec<_> = (0..VOTING_OPTIONS) .map(|i| { voters From ee09463b34d50593da86b6414d282872a96b3fce Mon Sep 17 00:00:00 2001 From: Mr-Leshiy Date: Wed, 2 Oct 2024 15:05:31 +0300 Subject: [PATCH 47/53] remove uneeded comment --- rust/catalyst-voting/tests/voting_test.rs | 1 - 1 file changed, 1 deletion(-) diff --git a/rust/catalyst-voting/tests/voting_test.rs b/rust/catalyst-voting/tests/voting_test.rs index 95750395bc..a17aa81cdc 100644 --- a/rust/catalyst-voting/tests/voting_test.rs +++ b/rust/catalyst-voting/tests/voting_test.rs @@ -61,7 +61,6 @@ fn voting_test(voters: [Voter; 100]) { .map(|t| decrypt_tally(t, &election_secret_key, &decryption_tally_setup).unwrap()) .collect(); - // verify tally proofs let is_ok = tally_proofs .iter() .zip(encrypted_tallies.iter()) From 1ca194c4586855c58023f500653f348fb8e4e4d3 Mon Sep 17 00:00:00 2001 From: Mr-Leshiy Date: Wed, 2 Oct 2024 16:48:00 +0300 Subject: [PATCH 48/53] fix --- rust/catalyst-voting/src/lib.rs | 4 +- rust/catalyst-voting/src/tally.rs | 124 ------------------------------ 2 files changed, 2 insertions(+), 126 deletions(-) delete mode 100644 rust/catalyst-voting/src/tally.rs diff --git a/rust/catalyst-voting/src/lib.rs b/rust/catalyst-voting/src/lib.rs index 4d0ab83b91..714331b647 100644 --- a/rust/catalyst-voting/src/lib.rs +++ b/rust/catalyst-voting/src/lib.rs @@ -75,8 +75,8 @@ //! ``` mod crypto; -pub mod tally; -pub mod voter; +mod tally; +mod voter; pub use crypto::elgamal::{PublicKey, SecretKey}; pub use tally::{proof::*, *}; diff --git a/rust/catalyst-voting/src/tally.rs b/rust/catalyst-voting/src/tally.rs deleted file mode 100644 index a6ef55a02b..0000000000 --- a/rust/catalyst-voting/src/tally.rs +++ /dev/null @@ -1,124 +0,0 @@ -//! Module containing all primitives related to the tally process. - -use std::ops::{Add, Mul}; - -use crate::{ - crypto::{ - elgamal::{decrypt, Ciphertext, SecretKey}, - group::{BabyStepGiantStep, Scalar}, - }, - voter::EncryptedVote, -}; - -/// An important decryption tally setup, which holds an important precomputed data needed -/// for decryption. -pub struct DecryptionTallySetup { - /// `BabyStepGiantStep` setup - discrete_log_setup: BabyStepGiantStep, -} - -/// A representation of the encrypted tally. -#[allow(clippy::module_name_repetitions)] -pub struct EncryptedTally(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 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(total_voting_power: u64) -> Result { - 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}.")] - 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::zero(); - - let res = ciphertexts_per_voting_option - .iter() - .zip(voting_powers.iter()) - .map(|(ciphertext, voting_power)| { - let voting_power_scalar = Scalar::from(*voting_power); - ciphertext.mul(&voting_power_scalar) - }) - .fold(zero_ciphertext, |acc, c| acc.add(&c)); - - Ok(EncryptedTally(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` -#[allow(clippy::module_name_repetitions)] -pub fn decrypt_tally( - tally_result: &EncryptedTally, secret_key: &SecretKey, setup: &DecryptionTallySetup, -) -> 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 55334564a1497a8af9202895c1848690d320e295 Mon Sep 17 00:00:00 2001 From: Mr-Leshiy Date: Wed, 2 Oct 2024 16:54:12 +0300 Subject: [PATCH 49/53] fix --- rust/catalyst-voting/src/crypto/elgamal.rs | 12 +- .../src/crypto/group/babystep_giantstep.rs | 123 ------------------ rust/catalyst-voting/src/crypto/group/mod.rs | 1 - 3 files changed, 5 insertions(+), 131 deletions(-) delete mode 100644 rust/catalyst-voting/src/crypto/group/babystep_giantstep.rs diff --git a/rust/catalyst-voting/src/crypto/elgamal.rs b/rust/catalyst-voting/src/crypto/elgamal.rs index 785c516a29..1fbb5c057a 100644 --- a/rust/catalyst-voting/src/crypto/elgamal.rs +++ b/rust/catalyst-voting/src/crypto/elgamal.rs @@ -51,19 +51,17 @@ 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 { + pub fn zero() -> Self { Ciphertext(GroupElement::zero(), GroupElement::zero()) } /// Get the first element of the `Ciphertext`. - #[allow(dead_code)] - pub(crate) fn first(&self) -> &GroupElement { + pub fn first(&self) -> &GroupElement { &self.0 } /// Get the second element of the `Ciphertext`. - #[allow(dead_code)] - pub(crate) fn second(&self) -> &GroupElement { + pub fn second(&self) -> &GroupElement { &self.1 } } @@ -71,7 +69,7 @@ impl Ciphertext { /// Given a `message` represented as a `Scalar`, return a ciphertext using the /// lifted ``ElGamal`` mechanism. /// Returns a ciphertext of type `Ciphertext`. -pub(crate) fn encrypt(message: &Scalar, public_key: &PublicKey, randomness: &Scalar) -> 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) @@ -79,7 +77,7 @@ pub(crate) fn encrypt(message: &Scalar, public_key: &PublicKey, randomness: &Sca /// Decrypt ``ElGamal`` `Ciphertext`, returns the original message represented as a /// `GroupElement`. -pub(crate) fn decrypt(cipher: &Ciphertext, secret_key: &SecretKey) -> GroupElement { +pub fn decrypt(cipher: &Ciphertext, secret_key: &SecretKey) -> GroupElement { &(&cipher.0 * &secret_key.0.negate()) + &cipher.1 } diff --git a/rust/catalyst-voting/src/crypto/group/babystep_giantstep.rs b/rust/catalyst-voting/src/crypto/group/babystep_giantstep.rs deleted file mode 100644 index 35bc38ada0..0000000000 --- a/rust/catalyst-voting/src/crypto/group/babystep_giantstep.rs +++ /dev/null @@ -1,123 +0,0 @@ -//! Implementation of baby-step 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 `table` for the baby-step 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 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-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 - /// 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`. - /// - /// # Errors - /// - `BabyStepError` - pub fn new(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. - /// - /// # 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) { - 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) - } -} - -#[cfg(test)] -mod tests { - use std::ops::Mul; - - use test_strategy::proptest; - - 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(2..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/mod.rs b/rust/catalyst-voting/src/crypto/group/mod.rs index eaaad084ce..d76e332903 100644 --- a/rust/catalyst-voting/src/crypto/group/mod.rs +++ b/rust/catalyst-voting/src/crypto/group/mod.rs @@ -1,7 +1,6 @@ //! Group definitions used in voting protocol. //! For more information, see: -mod babystep_giantstep; mod ristretto255; pub(crate) use ristretto255::{GroupElement, Scalar}; From 359cf53a4fd5e06d5c29e370c3c1d328847a94e2 Mon Sep 17 00:00:00 2001 From: Mr-Leshiy Date: Wed, 2 Oct 2024 16:57:15 +0300 Subject: [PATCH 50/53] fix spelling --- .config/dictionaries/project.dic | 1 + rust/catalyst-voting/src/crypto/zk_dl_equality.rs | 2 ++ 2 files changed, 3 insertions(+) diff --git a/.config/dictionaries/project.dic b/.config/dictionaries/project.dic index 2f3da07262..babfae1b2d 100644 --- a/.config/dictionaries/project.dic +++ b/.config/dictionaries/project.dic @@ -51,6 +51,7 @@ dbsync dcbor decompressor delegators +dleq dockerhub Dominik dotenv diff --git a/rust/catalyst-voting/src/crypto/zk_dl_equality.rs b/rust/catalyst-voting/src/crypto/zk_dl_equality.rs index 84c5d90e3b..deadec18c2 100644 --- a/rust/catalyst-voting/src/crypto/zk_dl_equality.rs +++ b/rust/catalyst-voting/src/crypto/zk_dl_equality.rs @@ -10,6 +10,8 @@ //! points `point_1` and `point_2`. The witness, on the other hand //! is the discrete logarithm, `dlog`. +// cspell: words NIZK dlog + use curve25519_dalek::digest::Update; use super::{ From a0e25ea06f05f1f1d14cb60f0341e104939e96ca Mon Sep 17 00:00:00 2001 From: Mr-Leshiy Date: Wed, 2 Oct 2024 16:59:48 +0300 Subject: [PATCH 51/53] fix comment --- rust/catalyst-voting/src/crypto/zk_dl_equality.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/rust/catalyst-voting/src/crypto/zk_dl_equality.rs b/rust/catalyst-voting/src/crypto/zk_dl_equality.rs index deadec18c2..a77cd3e4b3 100644 --- a/rust/catalyst-voting/src/crypto/zk_dl_equality.rs +++ b/rust/catalyst-voting/src/crypto/zk_dl_equality.rs @@ -49,7 +49,7 @@ pub fn verify_dleq_proof( } /// Calculates the challenge value. -/// Its a hash value of all provided elements represented as `Scalar`. +/// Its a hash value represented as `Scalar` of all provided elements. fn calculate_challenge( base_1: &GroupElement, base_2: &GroupElement, point_1: &GroupElement, point_2: &GroupElement, a_1: &GroupElement, a_2: &GroupElement, From 23b0bdc0873faea0671fb3a8e78b3bc60044eb72 Mon Sep 17 00:00:00 2001 From: Mr-Leshiy Date: Wed, 2 Oct 2024 17:31:53 +0300 Subject: [PATCH 52/53] update rust docs --- rust/catalyst-voting/src/lib.rs | 12 ++++++++++++ 1 file changed, 12 insertions(+) diff --git a/rust/catalyst-voting/src/lib.rs b/rust/catalyst-voting/src/lib.rs index 714331b647..bdfab2caaa 100644 --- a/rust/catalyst-voting/src/lib.rs +++ b/rust/catalyst-voting/src/lib.rs @@ -58,6 +58,11 @@ //! }) //! .collect(); //! +//! let tally_proofs: Vec<_> = encrypted_tallies +//! .iter() +//! .map(|t| generate_tally_proof(t, &election_secret_key, &mut rng)) +//! .collect(); +//! //! let decryption_tally_setup = DecryptionTallySetup::new( //! voter_1.voting_power + voter_2.voting_power + voter_3.voting_power, //! ) @@ -67,6 +72,13 @@ //! .map(|t| decrypt_tally(t, &election_secret_key, &decryption_tally_setup).unwrap()) //! .collect(); //! +//! let is_ok = tally_proofs +//! .iter() +//! .zip(encrypted_tallies.iter()) +//! .zip(decrypted_tallies.iter()) +//! .all(|((p, enc_t), t)| verify_tally_proof(enc_t, *t, &election_public_key, p)); +//! assert!(is_ok); +//! //! assert_eq!(decrypted_tallies, vec![ //! voter_1.voting_power, //! voter_2.voting_power, From 9fd9e0c144e1984e8096ae8d4e57f14e5fc7b192 Mon Sep 17 00:00:00 2001 From: Mr-Leshiy Date: Wed, 2 Oct 2024 17:37:44 +0300 Subject: [PATCH 53/53] fix rustdoc tests --- 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 bdfab2caaa..0387354ba9 100644 --- a/rust/catalyst-voting/src/lib.rs +++ b/rust/catalyst-voting/src/lib.rs @@ -2,8 +2,8 @@ //! //! ```rust //! use catalyst_voting::{ -//! decrypt_tally, encrypt_vote, tally, DecryptionTallySetup, EncryptionRandomness, SecretKey, -//! Vote, +//! decrypt_tally, encrypt_vote, generate_tally_proof, tally, verify_tally_proof, +//! DecryptionTallySetup, EncryptionRandomness, SecretKey, Vote, //! }; //! //! struct Voter {