diff --git a/rust/catalyst-voting/Cargo.toml b/rust/catalyst-voting/Cargo.toml index 866437d4e2..df6e2aa051 100644 --- a/rust/catalyst-voting/Cargo.toml +++ b/rust/catalyst-voting/Cargo.toml @@ -11,7 +11,7 @@ license.workspace = true workspace = true [dependencies] -thiserror = "1.0.64" +anyhow = "1.0.89" rand_core = "0.6.4" curve25519-dalek = { version = "4.1.3", features = ["digest"] } blake2b_simd = "1.0.2" diff --git a/rust/catalyst-voting/src/crypto/babystep_giantstep.rs b/rust/catalyst-voting/src/crypto/babystep_giantstep.rs index a5fceb2feb..89b8436fae 100644 --- a/rust/catalyst-voting/src/crypto/babystep_giantstep.rs +++ b/rust/catalyst-voting/src/crypto/babystep_giantstep.rs @@ -3,6 +3,8 @@ use std::collections::HashMap; +use anyhow::{bail, ensure}; + use crate::crypto::group::{GroupElement, Scalar}; /// Default balance value. @@ -22,16 +24,6 @@ pub struct BabyStepGiantStep { 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. /// @@ -47,16 +39,15 @@ impl BabyStepGiantStep { /// `baby_step_giant_step` function for the same `max_value`. /// /// # Errors - /// - `BabyStepError` - pub fn new(max_log_value: u64, balance: Option) -> Result { + /// - Maximum value and balance must be greater than zero. + pub fn new(max_log_value: u64, balance: Option) -> anyhow::Result { let balance = balance.unwrap_or(DEFAULT_BALANCE); - if balance == 0 || max_log_value == 0 { - return Err(BabyStepError::InvalidMaxValueOrBalance( - max_log_value, - balance, - )); - } + ensure!( + balance != 0 && max_log_value != 0, + "Maximum value and balance must be greater than zero, + provided max value: {max_log_value} and balance: {balance}." + ); #[allow( clippy::cast_possible_truncation, @@ -85,8 +76,8 @@ impl BabyStepGiantStep { /// Solve the discrete log using baby step giant step algorithm. /// /// # Errors - /// - `BabyStepError` - pub fn discrete_log(&self, mut point: GroupElement) -> Result { + /// - Max log value exceeded. + pub fn discrete_log(&self, mut point: GroupElement) -> anyhow::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; @@ -94,9 +85,12 @@ impl BabyStepGiantStep { } 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) + bail!("Max log value exceeded. + Means that the actual discrete log for the provided group element is higher than the provided `max_log_value`." + ) } } diff --git a/rust/catalyst-voting/src/crypto/elgamal/decoding.rs b/rust/catalyst-voting/src/crypto/elgamal/decoding.rs new file mode 100644 index 0000000000..e2d82309ce --- /dev/null +++ b/rust/catalyst-voting/src/crypto/elgamal/decoding.rs @@ -0,0 +1,96 @@ +//! Elgamal objects decoding implementation + +use anyhow::anyhow; + +use super::{Ciphertext, GroupElement, PublicKey, Scalar, SecretKey}; + +impl PublicKey { + /// `PublicKey` bytes size + pub const BYTES_SIZE: usize = GroupElement::BYTES_SIZE; + + /// Convert this `PublicKey` to its underlying sequence of bytes. + #[must_use] + pub fn to_bytes(&self) -> [u8; Self::BYTES_SIZE] { + self.0.to_bytes() + } + + /// Attempt to construct a `PublicKey` from a byte representation. + /// + /// # Errors + /// - Cannot decode group element field. + pub fn from_bytes(bytes: &[u8; Self::BYTES_SIZE]) -> anyhow::Result { + GroupElement::from_bytes(bytes).map(Self) + } +} + +impl SecretKey { + /// `SecretKey` bytes size + pub const BYTES_SIZE: usize = Scalar::BYTES_SIZE; + + /// Convert this `SecretKey` to its underlying sequence of bytes. + #[must_use] + pub fn to_bytes(&self) -> [u8; Self::BYTES_SIZE] { + self.0.to_bytes() + } + + /// Attempt to construct a `SecretKey` from a byte representation. + /// + /// # Errors + /// - Cannot decode scalar field. + pub fn from_bytes(bytes: [u8; Self::BYTES_SIZE]) -> anyhow::Result { + Scalar::from_bytes(bytes).map(Self) + } +} + +impl Ciphertext { + /// `Ciphertext` bytes size + pub const BYTES_SIZE: usize = GroupElement::BYTES_SIZE * 2; + + /// Convert this `Ciphertext` to its underlying sequence of bytes. + pub fn to_bytes(&self) -> [u8; Self::BYTES_SIZE] { + let mut res = [0; Self::BYTES_SIZE]; + res[0..32].copy_from_slice(&self.0.to_bytes()); + res[32..64].copy_from_slice(&self.1.to_bytes()); + res + } + + /// Attempt to construct a `Ciphertext` from a byte representation. + /// + /// # Errors + /// - Cannot decode group element field. + #[allow(clippy::unwrap_used)] + pub fn from_bytes(bytes: &[u8; Self::BYTES_SIZE]) -> anyhow::Result { + Ok(Self( + GroupElement::from_bytes(bytes[0..32].try_into().unwrap()) + .map_err(|_| anyhow!("Cannot decode first group element field."))?, + GroupElement::from_bytes(bytes[32..64].try_into().unwrap()) + .map_err(|_| anyhow!("Cannot decode second group element field."))?, + )) + } +} + +#[cfg(test)] +mod tests { + use test_strategy::proptest; + + use super::*; + + #[proptest] + fn keys_to_bytes_from_bytes_test(s1: SecretKey) { + let bytes = s1.to_bytes(); + let s2 = SecretKey::from_bytes(bytes).unwrap(); + assert_eq!(s1, s2); + + let p1 = s1.public_key(); + let bytes = p1.to_bytes(); + let p2 = PublicKey::from_bytes(&bytes).unwrap(); + assert_eq!(p1, p2); + } + + #[proptest] + fn ciphertext_to_bytes_from_bytes_test(c1: Ciphertext) { + let bytes = c1.to_bytes(); + let c2 = Ciphertext::from_bytes(&bytes).unwrap(); + assert_eq!(c1, c2); + } +} diff --git a/rust/catalyst-voting/src/crypto/elgamal.rs b/rust/catalyst-voting/src/crypto/elgamal/mod.rs similarity index 92% rename from rust/catalyst-voting/src/crypto/elgamal.rs rename to rust/catalyst-voting/src/crypto/elgamal/mod.rs index 737c02a7d9..aefed68f66 100644 --- a/rust/catalyst-voting/src/crypto/elgamal.rs +++ b/rust/catalyst-voting/src/crypto/elgamal/mod.rs @@ -1,6 +1,8 @@ //! Implementation of the lifted ``ElGamal`` crypto system, and combine with `ChaCha` //! stream cipher to produce a hybrid encryption scheme. +mod decoding; + use std::ops::{Add, Deref, Mul}; use rand_core::CryptoRngCore; @@ -116,6 +118,17 @@ mod tests { } } + impl Arbitrary for Ciphertext { + type Parameters = (); + type Strategy = BoxedStrategy; + + fn arbitrary_with((): Self::Parameters) -> Self::Strategy { + any::<(GroupElement, GroupElement)>() + .prop_map(|(g1, g2)| Ciphertext(g1, g2)) + .boxed() + } + } + #[proptest] fn ciphertext_add_test(e1: Scalar, e2: Scalar, e3: Scalar, e4: Scalar) { let g1 = GroupElement::GENERATOR.mul(&e1); diff --git a/rust/catalyst-voting/src/crypto/group/ristretto255/decoding.rs b/rust/catalyst-voting/src/crypto/group/ristretto255/decoding.rs new file mode 100644 index 0000000000..4076ee9b09 --- /dev/null +++ b/rust/catalyst-voting/src/crypto/group/ristretto255/decoding.rs @@ -0,0 +1,71 @@ +//! ristretto255 objects decoding implementation + +use anyhow::anyhow; +use curve25519_dalek::{ristretto::CompressedRistretto, scalar::Scalar as IScalar}; + +use super::{GroupElement, Scalar}; + +impl Scalar { + /// `Scalar` bytes size + pub const BYTES_SIZE: usize = 32; + + /// Attempt to construct a `Scalar` from a canonical byte representation. + /// + /// # Errors + /// - Cannot decode scalar. + pub fn from_bytes(bytes: [u8; Self::BYTES_SIZE]) -> anyhow::Result { + IScalar::from_canonical_bytes(bytes) + .map(Scalar) + .into_option() + .ok_or(anyhow!("Cannot decode scalar.")) + } + + /// Convert this `Scalar` to its underlying sequence of bytes. + pub fn to_bytes(&self) -> [u8; Self::BYTES_SIZE] { + self.0.to_bytes() + } +} + +impl GroupElement { + /// `Scalar` bytes size + pub const BYTES_SIZE: usize = 32; + + /// Attempt to construct a `Scalar` from a compressed value byte representation. + /// + /// # Errors + /// - Cannot decode group element. + pub fn from_bytes(bytes: &[u8; Self::BYTES_SIZE]) -> anyhow::Result { + Ok(GroupElement( + CompressedRistretto::from_slice(bytes)? + .decompress() + .ok_or(anyhow!("Cannot decode group element."))?, + )) + } + + /// Convert this `GroupElement` to its underlying sequence of bytes. + /// Always encode the compressed value. + pub fn to_bytes(&self) -> [u8; Self::BYTES_SIZE] { + self.0.compress().to_bytes() + } +} + +#[cfg(test)] +mod tests { + use test_strategy::proptest; + + use super::*; + + #[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(ge1: GroupElement) { + let bytes = ge1.to_bytes(); + let ge2 = GroupElement::from_bytes(&bytes).unwrap(); + assert_eq!(ge1, ge2); + } +} diff --git a/rust/catalyst-voting/src/crypto/group/ristretto255.rs b/rust/catalyst-voting/src/crypto/group/ristretto255/mod.rs similarity index 79% rename from rust/catalyst-voting/src/crypto/group/ristretto255.rs rename to rust/catalyst-voting/src/crypto/group/ristretto255/mod.rs index 71615aadbe..b636f8a864 100644 --- a/rust/catalyst-voting/src/crypto/group/ristretto255.rs +++ b/rust/catalyst-voting/src/crypto/group/ristretto255/mod.rs @@ -2,6 +2,8 @@ // cspell: words BASEPOINT +mod decoding; + use std::{ hash::Hash, ops::{Add, Mul, Sub}, @@ -10,7 +12,7 @@ use std::{ use curve25519_dalek::{ constants::{RISTRETTO_BASEPOINT_POINT, RISTRETTO_BASEPOINT_TABLE}, digest::{consts::U64, Digest}, - ristretto::{CompressedRistretto, RistrettoPoint as Point}, + ristretto::RistrettoPoint as Point, scalar::Scalar as IScalar, traits::Identity, }; @@ -69,16 +71,6 @@ impl 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() - } - /// Generate a `Scalar` from a hash digest. pub fn from_hash(hash: D) -> Scalar where D: Digest { @@ -94,19 +86,6 @@ 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 @@ -201,21 +180,6 @@ 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)); diff --git a/rust/catalyst-voting/src/crypto/mod.rs b/rust/catalyst-voting/src/crypto/mod.rs index 42ef8bbdd0..32efae4341 100644 --- a/rust/catalyst-voting/src/crypto/mod.rs +++ b/rust/catalyst-voting/src/crypto/mod.rs @@ -1,8 +1,8 @@ //! Crypto primitives which are used by voting protocol. -pub(crate) mod babystep_giantstep; -pub(crate) mod elgamal; -pub(crate) mod group; -pub(crate) mod hash; -pub(crate) mod zk_dl_equality; -pub(crate) mod zk_unit_vector; +pub mod babystep_giantstep; +pub mod elgamal; +pub mod group; +pub mod hash; +pub mod zk_dl_equality; +pub mod zk_unit_vector; diff --git a/rust/catalyst-voting/src/crypto/zk_unit_vector/decoding.rs b/rust/catalyst-voting/src/crypto/zk_unit_vector/decoding.rs new file mode 100644 index 0000000000..82b1229fb3 --- /dev/null +++ b/rust/catalyst-voting/src/crypto/zk_unit_vector/decoding.rs @@ -0,0 +1,170 @@ +//! ZK Unit Vector objects decoding implementation + +use std::io::Read; + +use anyhow::anyhow; + +use super::{Announcement, Ciphertext, GroupElement, ResponseRandomness, Scalar, UnitVectorProof}; +use crate::utils::read_array; + +impl UnitVectorProof { + /// Get an underlying vector length. + /// + /// **Note** each vector field has the same length. + pub fn size(&self) -> usize { + self.0.len() + } + + /// Decode `UnitVectorProof` from bytes. + /// + /// # Errors + /// - Cannot decode announcement value. + /// - Cannot decode ciphertext value. + /// - Cannot decode response randomness value. + /// - Cannot decode scalar value. + pub fn from_bytes(reader: &mut R, len: usize) -> anyhow::Result { + let ann = (0..len) + .map(|i| { + let bytes = read_array(reader)?; + Announcement::from_bytes(&bytes) + .map_err(|e| anyhow!("Cannot decode announcement at {i}, error: {e}.")) + }) + .collect::>()?; + let dl = (0..len) + .map(|i| { + let bytes = read_array(reader)?; + Ciphertext::from_bytes(&bytes) + .map_err(|e| anyhow!("Cannot decode ciphertext at {i}, error: {e}.")) + }) + .collect::>()?; + let rr = (0..len) + .map(|i| { + let bytes = read_array(reader)?; + ResponseRandomness::from_bytes(&bytes) + .map_err(|e| anyhow!("Cannot decode response randomness at {i}, error: {e}.")) + }) + .collect::>()?; + + let bytes = read_array(reader)?; + let scalar = + Scalar::from_bytes(bytes).map_err(|_| anyhow!("Cannot decode scalar field."))?; + Ok(Self(ann, dl, rr, scalar)) + } + + /// Get a deserialized bytes size + #[must_use] + fn bytes_size(&self) -> usize { + Scalar::BYTES_SIZE + + self.0.len() * Announcement::BYTES_SIZE + + self.0.len() * Ciphertext::BYTES_SIZE + + self.0.len() * ResponseRandomness::BYTES_SIZE + } + + /// Encode `EncryptedVote` tos bytes. + #[must_use] + pub fn to_bytes(&self) -> Vec { + let mut res = Vec::with_capacity(self.bytes_size()); + self.0 + .iter() + .for_each(|c| res.extend_from_slice(&c.to_bytes())); + self.1 + .iter() + .for_each(|c| res.extend_from_slice(&c.to_bytes())); + self.2 + .iter() + .for_each(|c| res.extend_from_slice(&c.to_bytes())); + res.extend_from_slice(&self.3.to_bytes()); + res + } +} + +impl Announcement { + /// `Announcement` bytes size + pub const BYTES_SIZE: usize = GroupElement::BYTES_SIZE * 3; + + /// Decode `Announcement` from bytes. + /// + /// # Errors + /// - `AnnouncementDecodingError` + #[allow(clippy::unwrap_used)] + pub fn from_bytes(bytes: &[u8; Self::BYTES_SIZE]) -> anyhow::Result { + let i = GroupElement::from_bytes(bytes[0..32].try_into().unwrap()) + .map_err(|_| anyhow!("Cannot decode `i` group element field."))?; + let b = GroupElement::from_bytes(bytes[32..64].try_into().unwrap()) + .map_err(|_| anyhow!("Cannot decode `b` group element field."))?; + let a = GroupElement::from_bytes(bytes[64..96].try_into().unwrap()) + .map_err(|_| anyhow!("Cannot decode `a` group element field."))?; + Ok(Self { i, b, a }) + } + + /// Encode `Announcement` tos bytes. + #[must_use] + pub fn to_bytes(&self) -> [u8; Self::BYTES_SIZE] { + let mut res = [0; 96]; + res[0..32].copy_from_slice(&self.i.to_bytes()); + res[32..64].copy_from_slice(&self.b.to_bytes()); + res[64..96].copy_from_slice(&self.a.to_bytes()); + res + } +} + +impl ResponseRandomness { + /// `ResponseRandomness` bytes size + pub const BYTES_SIZE: usize = Scalar::BYTES_SIZE * 3; + + /// Decode `ResponseRandomness` from bytes. + /// + /// # Errors + /// - Cannot decode scalar field. + #[allow(clippy::unwrap_used)] + pub fn from_bytes(bytes: &[u8; Self::BYTES_SIZE]) -> anyhow::Result { + let z = Scalar::from_bytes(bytes[0..32].try_into().unwrap()) + .map_err(|_| anyhow!("Cannot decode `z` scalar field."))?; + let w = Scalar::from_bytes(bytes[32..64].try_into().unwrap()) + .map_err(|_| anyhow!("Cannot decode `w` scalar field."))?; + let v = Scalar::from_bytes(bytes[64..96].try_into().unwrap()) + .map_err(|_| anyhow!("Cannot decode `v` scalar field."))?; + Ok(Self { z, w, v }) + } + + /// Encode `ResponseRandomness` tos bytes. + #[must_use] + pub fn to_bytes(&self) -> [u8; Self::BYTES_SIZE] { + let mut res = [0; 96]; + res[0..32].copy_from_slice(&self.z.to_bytes()); + res[32..64].copy_from_slice(&self.w.to_bytes()); + res[64..96].copy_from_slice(&self.v.to_bytes()); + res + } +} + +#[cfg(test)] +mod tests { + use test_strategy::proptest; + + use super::*; + + #[proptest] + fn proof_to_bytes_from_bytes_test( + #[strategy(0..5usize)] _size: usize, #[any(#_size)] p1: UnitVectorProof, + ) { + let bytes = p1.to_bytes(); + assert_eq!(bytes.len(), p1.bytes_size()); + let p2 = UnitVectorProof::from_bytes(&mut bytes.as_slice(), p1.size()).unwrap(); + assert_eq!(p1, p2); + } + + #[proptest] + fn announcement_to_bytes_from_bytes_test(a1: Announcement) { + let bytes = a1.to_bytes(); + let a2 = Announcement::from_bytes(&bytes).unwrap(); + assert_eq!(a1, a2); + } + + #[proptest] + fn response_randomness_to_bytes_from_bytes_test(r1: ResponseRandomness) { + let bytes = r1.to_bytes(); + let r2 = ResponseRandomness::from_bytes(&bytes).unwrap(); + assert_eq!(r1, r2); + } +} diff --git a/rust/catalyst-voting/src/crypto/zk_unit_vector/mod.rs b/rust/catalyst-voting/src/crypto/zk_unit_vector/mod.rs index 8dd849864b..1f2c21bb42 100644 --- a/rust/catalyst-voting/src/crypto/zk_unit_vector/mod.rs +++ b/rust/catalyst-voting/src/crypto/zk_unit_vector/mod.rs @@ -7,6 +7,7 @@ // cspell: words Zhang, Oliynykov, Balogum mod challenges; +mod decoding; mod polynomial; mod randomness_announcements; mod utils; @@ -25,6 +26,7 @@ use crate::crypto::{ }; /// Unit vector proof struct +#[derive(Debug, Clone, PartialEq, Eq)] pub struct UnitVectorProof( Vec, Vec, @@ -232,12 +234,33 @@ fn check_2( #[cfg(test)] mod tests { - use proptest::sample::size_range; + use proptest::{ + prelude::{any_with, Arbitrary, BoxedStrategy, Strategy}, + sample::size_range, + }; use rand_core::OsRng; use test_strategy::proptest; use super::{super::elgamal::SecretKey, *}; + impl Arbitrary for UnitVectorProof { + type Parameters = usize; + type Strategy = BoxedStrategy; + + fn arbitrary_with(size: Self::Parameters) -> Self::Strategy { + any_with::<( + Vec<((Announcement, Ciphertext), ResponseRandomness)>, + Scalar, + )>(((size_range(size), (((), ()), ())), ())) + .prop_map(|(val, scalar)| { + let (vec, rr): (Vec<_>, Vec<_>) = val.into_iter().unzip(); + let (an, cipher) = vec.into_iter().unzip(); + Self(an, cipher, rr, scalar) + }) + .boxed() + } + } + fn is_unit_vector(vector: &[Scalar]) -> bool { let ones = vector.iter().filter(|s| s == &&Scalar::one()).count(); let zeros = vector.iter().filter(|s| s == &&Scalar::zero()).count(); diff --git a/rust/catalyst-voting/src/crypto/zk_unit_vector/randomness_announcements.rs b/rust/catalyst-voting/src/crypto/zk_unit_vector/randomness_announcements.rs index 38951ead3b..4771a04a4a 100644 --- a/rust/catalyst-voting/src/crypto/zk_unit_vector/randomness_announcements.rs +++ b/rust/catalyst-voting/src/crypto/zk_unit_vector/randomness_announcements.rs @@ -9,7 +9,7 @@ use rand_core::CryptoRngCore; use crate::crypto::group::{GroupElement, Scalar}; /// Randomness generated in the proof, used for the hiding property. -#[derive(Debug)] +#[derive(Clone, Debug, Eq, PartialEq)] pub struct BlindingRandomness { pub(crate) alpha: Scalar, pub(crate) betta: Scalar, @@ -30,6 +30,7 @@ impl BlindingRandomness { /// First announcement, formed by I, B, A group elements. These group elements /// are the commitments of the binary representation of the unit vector index. +#[derive(Clone, Debug, Eq, PartialEq)] pub struct Announcement { pub(crate) i: GroupElement, pub(crate) b: GroupElement, @@ -103,4 +104,26 @@ mod tests { .boxed() } } + + impl Arbitrary for Announcement { + type Parameters = (); + type Strategy = BoxedStrategy; + + fn arbitrary_with((): Self::Parameters) -> Self::Strategy { + any::<(GroupElement, GroupElement, GroupElement)>() + .prop_map(|(i, b, a)| Announcement { i, b, a }) + .boxed() + } + } + + impl Arbitrary for ResponseRandomness { + type Parameters = (); + type Strategy = BoxedStrategy; + + fn arbitrary_with((): Self::Parameters) -> Self::Strategy { + any::<(Scalar, Scalar, Scalar)>() + .prop_map(|(z, w, v)| ResponseRandomness { z, w, v }) + .boxed() + } + } } diff --git a/rust/catalyst-voting/src/lib.rs b/rust/catalyst-voting/src/lib.rs index 16debee045..b494392750 100644 --- a/rust/catalyst-voting/src/lib.rs +++ b/rust/catalyst-voting/src/lib.rs @@ -1,94 +1,8 @@ //! Voting primitives which are used among Catalyst ecosystem. -//! -//! ```rust -//! use catalyst_voting::{ -//! tally::{ -//! decrypt_tally, -//! proof::{generate_tally_proof, verify_tally_proof}, -//! tally, DecryptionTallySetup, -//! }, -//! voter::{encrypt_vote, Vote}, -//! SecretKey, -//! }; -//! -//! struct Voter { -//! voting_power: u64, -//! choice: usize, -//! } -//! -//! let mut rng = rand_core::OsRng; -//! let voting_options = 3; -//! let election_secret_key = SecretKey::random(&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 (encrypted_vote_1, voter_randomness_1) = -//! encrypt_vote(&vote_1, &election_public_key, &mut rng); -//! let (encrypted_vote_2, voter_randomness_2) = -//! encrypt_vote(&vote_2, &election_public_key, &mut rng); -//! let (encrypted_vote_3, voter_randomness_3) = -//! encrypt_vote(&vote_3, &election_public_key, &mut rng); -//! 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 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, -//! ) -//! .unwrap(); -//! let decrypted_tallies: Vec<_> = encrypted_tallies -//! .iter() -//! .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, -//! voter_3.voting_power -//! ]); -//! ``` mod crypto; -pub mod tally; -pub mod voter; +pub mod txs; +mod utils; +pub mod vote_protocol; pub use crypto::elgamal::{PublicKey, SecretKey}; diff --git a/rust/catalyst-voting/src/txs/mod.rs b/rust/catalyst-voting/src/txs/mod.rs new file mode 100644 index 0000000000..71e3abe603 --- /dev/null +++ b/rust/catalyst-voting/src/txs/mod.rs @@ -0,0 +1,3 @@ +//! A catalyst transaction objects implementation + +pub mod v1; diff --git a/rust/catalyst-voting/src/txs/v1/decoding.rs b/rust/catalyst-voting/src/txs/v1/decoding.rs new file mode 100644 index 0000000000..2562830225 --- /dev/null +++ b/rust/catalyst-voting/src/txs/v1/decoding.rs @@ -0,0 +1,181 @@ +//! V1 transaction objects decoding implementation. + +use std::io::Read; + +use anyhow::{anyhow, bail, ensure}; + +use super::{EncryptedVote, PublicKey, Tx, Vote, VoterProof}; +use crate::utils::{read_array, read_be_u32, read_be_u64, read_be_u8}; + +/// Jörmungandr tx fragment tag. +const FRAGMENT_TAG: u8 = 11; +/// Jörmungandr tx input tag. +const INPUT_TAG: u8 = 0xFF; +/// Jörmungandr tx number of inputs. +const NUMBER_OF_INPUTS: u8 = 1; +/// Jörmungandr tx number of outputs. +const NUMBER_OF_OUTPUTS: u8 = 0; +/// Jörmungandr tx padding tag. +const PADDING_TAG: u8 = 0; +/// Jörmungandr tx private vote tag. +const PRIVATE_VOTE_TAG: u8 = 2; +/// Jörmungandr tx public vote tag. +const PUBLIC_VOTE_TAG: u8 = 1; + +impl Tx { + /// Convert this `Tx` to its underlying sequence of bytes. + #[must_use] + #[allow(clippy::cast_possible_truncation)] + pub fn to_bytes(&self) -> Vec { + // Initialize already with the padding tag `0` and fragment tag `11`. + let mut tx_body = vec![PADDING_TAG, FRAGMENT_TAG]; + + tx_body.extend_from_slice(&self.vote_plan_id); + tx_body.push(self.proposal_index); + + match &self.vote { + Vote::Public(vote) => { + // Public vote tag + tx_body.push(PUBLIC_VOTE_TAG); + tx_body.push(*vote); + }, + Vote::Private(vote, proof) => { + // Private vote tag + tx_body.push(PRIVATE_VOTE_TAG); + tx_body.push(vote.size() as u8); + tx_body.extend_from_slice(&vote.to_bytes()); + + tx_body.push(proof.size() as u8); + tx_body.extend_from_slice(&proof.to_bytes()); + }, + } + + // Zeros block date + tx_body.extend_from_slice(&[0u8; 8]); + // Number of inputs + tx_body.push(NUMBER_OF_INPUTS); + // Number of outputs + tx_body.push(NUMBER_OF_OUTPUTS); + // Input tag + tx_body.push(INPUT_TAG); + // Zero value + tx_body.extend_from_slice(&[0u8; 8]); + + tx_body.extend_from_slice(&self.public_key.to_bytes()); + + // Add the size of decoded bytes to the beginning. + let mut res = (tx_body.len() as u32).to_be_bytes().to_vec(); + res.append(&mut tx_body); + res + } + + /// Attempt to construct a `Tx` from a byte representation. + /// + /// # Errors + /// - Invalid padding tag field value. + /// - Invalid fragment tag field value. + /// - Invalid encrypted vote. + /// - Invalid voter proof. + /// - Invalid vote tag value. + /// - Invalid public key. + #[allow(clippy::indexing_slicing)] + pub fn from_bytes(reader: &mut R) -> anyhow::Result { + // Skip tx size field + read_be_u32(reader)?; + + let padding_tag = read_be_u8(reader)?; + ensure!( + padding_tag == PADDING_TAG, + "Invalid padding tag field value, must be equals to {PADDING_TAG}, provided: {padding_tag}.", + ); + + let fragment_tag = read_be_u8(reader)?; + ensure!( + fragment_tag == FRAGMENT_TAG, + "Invalid fragment tag field value, must be equals to {FRAGMENT_TAG}, provided: {fragment_tag}.", + ); + + let vote_plan_id = read_array(reader)?; + + let proposal_index = read_be_u8(reader)?; + + let vote_tag = read_be_u8(reader)?; + let vote = match vote_tag { + PUBLIC_VOTE_TAG => { + let vote = read_be_u8(reader)?; + Vote::Public(vote) + }, + PRIVATE_VOTE_TAG => { + let size = read_be_u8(reader)?; + let vote = EncryptedVote::from_bytes(reader, size.into()) + .map_err(|e| anyhow!("Invalid encrypted vote, error: {e}."))?; + + let size = read_be_u8(reader)?; + let proof = VoterProof::from_bytes(reader, size.into()) + .map_err(|e| anyhow!("Invalid voter proof, error: {e}."))?; + + Vote::Private(vote, proof) + }, + tag => { + bail!( + "Invalid vote tag value, must be equals to {PUBLIC_VOTE_TAG} or {PRIVATE_VOTE_TAG}, provided: {tag}" + ) + }, + }; + + // skip block date (epoch and slot) + read_be_u64(reader)?; + + let inputs_amount = read_be_u8(reader)?; + ensure!( + inputs_amount == NUMBER_OF_INPUTS, + "Invalid number of inputs, expected: {NUMBER_OF_INPUTS}, provided: {inputs_amount}", + ); + + let outputs_amount = read_be_u8(reader)?; + ensure!( + outputs_amount == NUMBER_OF_OUTPUTS, + "Invalid number of outputs, expected: {NUMBER_OF_OUTPUTS}, provided: {outputs_amount}", + ); + + let input_tag = read_be_u8(reader)?; + ensure!( + input_tag == INPUT_TAG, + "Invalid input tag, expected: {INPUT_TAG}, provided: {input_tag}", + ); + + // skip value + read_be_u64(reader)?; + + let public_key_bytes = read_array(reader)?; + let public_key = PublicKey::from_bytes(&public_key_bytes) + .map_err(|e| anyhow!("Invalid public key, error: {e}."))?; + + Ok(Self { + vote_plan_id, + proposal_index, + vote, + public_key, + }) + } +} + +#[cfg(test)] +mod tests { + use test_strategy::proptest; + + use super::*; + + #[proptest] + #[allow(clippy::indexing_slicing)] + fn tx_to_bytes_from_bytes_test(t1: Tx) { + let bytes = t1.to_bytes(); + + // verify correctness serializing tx size field + let size = u32::from_be_bytes(bytes[0..4].try_into().unwrap()); + assert_eq!(size as usize, bytes.len() - 4); + + let t2 = Tx::from_bytes(&mut bytes.as_slice()).unwrap(); + assert_eq!(t1, t2); + } +} diff --git a/rust/catalyst-voting/src/txs/v1/mod.rs b/rust/catalyst-voting/src/txs/v1/mod.rs new file mode 100644 index 0000000000..a78a1db10c --- /dev/null +++ b/rust/catalyst-voting/src/txs/v1/mod.rs @@ -0,0 +1,80 @@ +//! A Jörmungandr transaction object structured following this [spec](https://input-output-hk.github.io/catalyst-voices/architecture/08_concepts/voting_transaction/transaction/#v1-jormungandr) + +#![allow(unused_variables, dead_code)] + +mod decoding; + +use crate::{ + vote_protocol::voter::{proof::VoterProof, EncryptedVote}, + PublicKey, +}; + +/// A v1 (Jörmungandr) transaction struct +#[derive(Debug, Clone, PartialEq, Eq)] +pub struct Tx { + /// Vote plan id + vote_plan_id: [u8; 32], + /// Proposal index + proposal_index: u8, + /// Vote + vote: Vote, + /// Public key + public_key: PublicKey, +} + +/// Vote struct +#[derive(Debug, Clone, PartialEq, Eq)] +pub enum Vote { + /// Public voting choice + Public(u8), + /// Private (encrypted) voting choice + Private(EncryptedVote, VoterProof), +} + +#[cfg(test)] +mod tests { + use proptest::prelude::{any, any_with, Arbitrary, BoxedStrategy, Strategy}; + + use super::*; + use crate::SecretKey; + + impl Arbitrary for Tx { + type Parameters = (); + type Strategy = BoxedStrategy; + + fn arbitrary_with((): Self::Parameters) -> Self::Strategy { + any::<([u8; 32], u8, Vote, SecretKey)>() + .prop_map(|(vote_plan_id, proposal_index, vote, s)| { + Tx { + vote_plan_id, + proposal_index, + vote, + public_key: s.public_key(), + } + }) + .boxed() + } + } + + impl Arbitrary for Vote { + type Parameters = (); + type Strategy = BoxedStrategy; + + fn arbitrary_with((): Self::Parameters) -> Self::Strategy { + any::() + .prop_flat_map(|b| { + if b { + any::().prop_map(Vote::Public).boxed() + } else { + any::<(u8, u8)>() + .prop_flat_map(|(s1, s2)| { + any_with::<(EncryptedVote, VoterProof)>((s1.into(), s2.into())) + .prop_map(|(v, p)| Vote::Private(v, p)) + }) + .boxed() + } + }) + .boxed() + } + } +} diff --git a/rust/catalyst-voting/src/utils.rs b/rust/catalyst-voting/src/utils.rs new file mode 100644 index 0000000000..393c00fb71 --- /dev/null +++ b/rust/catalyst-voting/src/utils.rs @@ -0,0 +1,35 @@ +//! Utility functions. + +use std::io::Read; + +/// Read a single byte from the reader. +#[inline] +pub(crate) fn read_be_u8(reader: &mut R) -> anyhow::Result { + let mut buf = [0u8; 1]; + reader.read_exact(&mut buf)?; + Ok(u8::from_be_bytes(buf)) +} + +/// Read a big-endian u32 from the reader. +#[inline] +pub(crate) fn read_be_u32(reader: &mut R) -> anyhow::Result { + let mut buf = [0u8; 4]; + reader.read_exact(&mut buf)?; + Ok(u32::from_be_bytes(buf)) +} + +/// Read a big-endian u64 from the reader. +#[inline] +pub(crate) fn read_be_u64(reader: &mut R) -> anyhow::Result { + let mut buf = [0u8; 8]; + reader.read_exact(&mut buf)?; + Ok(u64::from_be_bytes(buf)) +} + +/// Read a N-byte array from the reader. +#[inline] +pub(crate) fn read_array(reader: &mut R) -> anyhow::Result<[u8; N]> { + let mut buf = [0u8; N]; + reader.read_exact(&mut buf)?; + Ok(buf) +} diff --git a/rust/catalyst-voting/src/vote_protocol/mod.rs b/rust/catalyst-voting/src/vote_protocol/mod.rs new file mode 100644 index 0000000000..16fb14d7bc --- /dev/null +++ b/rust/catalyst-voting/src/vote_protocol/mod.rs @@ -0,0 +1,93 @@ +//! An implementation of the voting protocol described in this [spec](https://input-output-hk.github.io/catalyst-voices/architecture/08_concepts/voting_transaction/crypto/) +//! +//! ```rust +//! use catalyst_voting::{ +//! vote_protocol::{ +//! tally::{ +//! decrypt_tally, +//! proof::{generate_tally_proof, verify_tally_proof}, +//! tally, DecryptionTallySetup, +//! }, +//! voter::{encrypt_vote, Vote}, +//! }, +//! SecretKey, +//! }; +//! +//! struct Voter { +//! voting_power: u64, +//! choice: usize, +//! } +//! +//! let mut rng = rand_core::OsRng; +//! let voting_options = 3; +//! let election_secret_key = SecretKey::random(&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 (encrypted_vote_1, voter_randomness_1) = +//! encrypt_vote(&vote_1, &election_public_key, &mut rng); +//! let (encrypted_vote_2, voter_randomness_2) = +//! encrypt_vote(&vote_2, &election_public_key, &mut rng); +//! let (encrypted_vote_3, voter_randomness_3) = +//! encrypt_vote(&vote_3, &election_public_key, &mut rng); +//! 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 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, +//! ) +//! .unwrap(); +//! let decrypted_tallies: Vec<_> = encrypted_tallies +//! .iter() +//! .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, +//! voter_3.voting_power +//! ]); +//! ``` + +pub mod tally; +pub mod voter; diff --git a/rust/catalyst-voting/src/tally/mod.rs b/rust/catalyst-voting/src/vote_protocol/tally/mod.rs similarity index 58% rename from rust/catalyst-voting/src/tally/mod.rs rename to rust/catalyst-voting/src/vote_protocol/tally/mod.rs index 81cd10990e..7e29cc2cae 100644 --- a/rust/catalyst-voting/src/tally/mod.rs +++ b/rust/catalyst-voting/src/vote_protocol/tally/mod.rs @@ -4,13 +4,15 @@ pub mod proof; use std::ops::{Add, Mul}; +use anyhow::{anyhow, ensure}; + use crate::{ crypto::{ babystep_giantstep::BabyStepGiantStep, elgamal::{decrypt, Ciphertext, SecretKey}, group::Scalar, }, - voter::EncryptedVote, + vote_protocol::voter::EncryptedVote, }; /// An important decryption tally setup, which holds an important precomputed data needed @@ -24,14 +26,6 @@ pub struct DecryptionTallySetup { #[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` @@ -41,46 +35,38 @@ impl DecryptionTallySetup { /// `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)?; + /// - Total voting power must more than 0. + pub fn new(total_voting_power: u64) -> anyhow::Result { + let discrete_log_setup = + BabyStepGiantStep::new(total_voting_power, None).map_err(|_| { + anyhow!("Total voting power must more than 0, provided: {total_voting_power}") + })?; 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` +/// - Votes and voting power length mismatch. +/// - Invalid encrypted vote at index `i`. Does not have a ciphertext for the voting +/// option `voting_option`. 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(), - )); - } +) -> anyhow::Result { + ensure!( + votes.len() == voting_powers.len(), + "Votes and voting power length mismatch. Votes amount: {0}. Voting powers amount: {1}.", + 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))?; + .ok_or(anyhow!("Invalid encrypted vote at index {i}. Does not have a ciphertext for the voting option {voting_option}.") )?; ciphertexts_per_voting_option.push(ciphertext); } @@ -98,30 +84,21 @@ pub fn tally( 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` +/// - Cannot decrypt tally result. Provided an invalid secret key or invalid encrypted +/// tally result. #[allow(clippy::module_name_repetitions)] pub fn decrypt_tally( tally_result: &EncryptedTally, secret_key: &SecretKey, setup: &DecryptionTallySetup, -) -> Result { +) -> anyhow::Result { let ge = decrypt(&tally_result.0, secret_key); let res = setup .discrete_log_setup .discrete_log(ge) - .map_err(|_| DecryptTallyError::CannotDecryptTallyResult)?; + .map_err(|_| anyhow!("Cannot decrypt tally result. Provided an invalid secret key or invalid encrypted tally result."))?; Ok(res) } diff --git a/rust/catalyst-voting/src/tally/proof.rs b/rust/catalyst-voting/src/vote_protocol/tally/proof.rs similarity index 100% rename from rust/catalyst-voting/src/tally/proof.rs rename to rust/catalyst-voting/src/vote_protocol/tally/proof.rs diff --git a/rust/catalyst-voting/src/vote_protocol/voter/decoding.rs b/rust/catalyst-voting/src/vote_protocol/voter/decoding.rs new file mode 100644 index 0000000000..f4f08cc866 --- /dev/null +++ b/rust/catalyst-voting/src/vote_protocol/voter/decoding.rs @@ -0,0 +1,96 @@ +//! Voter objects decoding implementation. + +use std::io::Read; + +use anyhow::anyhow; + +use super::{proof::VoterProof, EncryptedVote}; +use crate::{ + crypto::{elgamal::Ciphertext, zk_unit_vector::UnitVectorProof}, + utils::read_array, +}; + +impl EncryptedVote { + /// Get an underlying vector length. + pub(crate) fn size(&self) -> usize { + self.0.len() + } + + /// Decode `EncryptedVote` from bytes. + /// + /// # Errors + /// - Cannot decode ciphertext. + pub fn from_bytes(reader: &mut R, size: usize) -> anyhow::Result { + let ciphertexts = (0..size) + .map(|i| { + let bytes = read_array(reader)?; + Ciphertext::from_bytes(&bytes) + .map_err(|e| anyhow!("Cannot decode ciphertext at {i}, error: {e}")) + }) + .collect::>()?; + + Ok(Self(ciphertexts)) + } + + /// Get a deserialized bytes size + #[must_use] + pub fn bytes_size(&self) -> usize { + self.0.len() * Ciphertext::BYTES_SIZE + } + + /// Encode `EncryptedVote` tos bytes. + #[must_use] + pub fn to_bytes(&self) -> Vec { + let mut res = Vec::with_capacity(self.bytes_size()); + self.0 + .iter() + .for_each(|c| res.extend_from_slice(&c.to_bytes())); + res + } +} + +impl VoterProof { + /// Get an underlying vector length. + /// + /// **Note** each vector field has the same length. + #[must_use] + pub fn size(&self) -> usize { + self.0.size() + } + + /// Decode `VoterProof` from bytes. + /// + /// # Errors + /// - Cannot decode announcement value. + /// - Cannot decode ciphertext value. + /// - Cannot decode response randomness value. + /// - Cannot decode scalar value. + pub fn from_bytes(reader: &mut R, len: usize) -> anyhow::Result { + UnitVectorProof::from_bytes(reader, len).map(Self) + } + + /// Encode `EncryptedVote` tos bytes. + #[must_use] + pub fn to_bytes(&self) -> Vec { + self.0.to_bytes() + } +} + +#[cfg(test)] +mod tests { + use std::io::Cursor; + + use test_strategy::proptest; + + use super::*; + + #[proptest] + fn encrypted_vote_to_bytes_from_bytes_test( + #[strategy(0..5usize)] _size: usize, #[any(#_size)] vote1: EncryptedVote, + ) { + let bytes = vote1.to_bytes(); + assert_eq!(bytes.len(), vote1.bytes_size()); + let vote2 = EncryptedVote::from_bytes(&mut Cursor::new(bytes), vote1.size()).unwrap(); + assert_eq!(vote1, vote2); + } +} diff --git a/rust/catalyst-voting/src/voter/mod.rs b/rust/catalyst-voting/src/vote_protocol/voter/mod.rs similarity index 81% rename from rust/catalyst-voting/src/voter/mod.rs rename to rust/catalyst-voting/src/vote_protocol/voter/mod.rs index e22d777c3a..788bcca294 100644 --- a/rust/catalyst-voting/src/voter/mod.rs +++ b/rust/catalyst-voting/src/vote_protocol/voter/mod.rs @@ -1,7 +1,9 @@ //! Module containing all primitives related to the voter. +mod decoding; pub mod proof; +use anyhow::ensure; use rand_core::CryptoRngCore; use crate::crypto::{ @@ -43,26 +45,15 @@ 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 fn new(choice: usize, voting_options: usize) -> Result { - if choice >= voting_options { - return Err(VoteError::IncorrectChoiceError(choice, voting_options)); - } + /// - Invalid voting choice, the value of `choice`, should be less than the number + /// of `voting_options`. + pub fn new(choice: usize, voting_options: usize) -> anyhow::Result { + ensure!(choice < voting_options,"Invalid voting choice, the value of choice: {choice}, should be less than the number of voting options: {voting_options}." ); Ok(Vote { choice, @@ -106,8 +97,24 @@ pub fn encrypt_vote( #[cfg(test)] mod tests { + use proptest::{ + prelude::{any_with, Arbitrary, BoxedStrategy, Strategy}, + sample::size_range, + }; + use super::*; + impl Arbitrary for EncryptedVote { + type Parameters = usize; + type Strategy = BoxedStrategy; + + fn arbitrary_with(size: Self::Parameters) -> Self::Strategy { + any_with::>((size_range(size), ())) + .prop_map(Self) + .boxed() + } + } + #[test] fn vote_test() { let voting_options = 3; diff --git a/rust/catalyst-voting/src/voter/proof.rs b/rust/catalyst-voting/src/vote_protocol/voter/proof.rs similarity index 65% rename from rust/catalyst-voting/src/voter/proof.rs rename to rust/catalyst-voting/src/vote_protocol/voter/proof.rs index 8df75db4b0..f9a70b50e8 100644 --- a/rust/catalyst-voting/src/voter/proof.rs +++ b/rust/catalyst-voting/src/vote_protocol/voter/proof.rs @@ -3,6 +3,7 @@ use std::ops::Mul; +use anyhow::ensure; use rand_core::CryptoRngCore; use super::{EncryptedVote, EncryptionRandomness, Vote}; @@ -16,7 +17,8 @@ use crate::{ /// Tally proof struct. #[allow(clippy::module_name_repetitions)] -pub struct VoterProof(UnitVectorProof); +#[derive(Debug, Clone, PartialEq, Eq)] +pub struct VoterProof(pub(super) UnitVectorProof); /// Voter proof commitment struct. pub struct VoterProofCommitment(GroupElement); @@ -28,31 +30,25 @@ impl VoterProofCommitment { } } -/// Generate voter proof error -#[derive(thiserror::Error, Debug)] -pub enum GenerateVoterProofError { - /// Arguments mismatch - #[error("Provided arguments mismatch. Size of the provided `vote`: {0}, `encrypted_vote: {1}` and `randomness`: {2} must be equal with each other.")] - ArgumentsMismatch(usize, usize, usize), -} - /// Generates a voter proof. /// More detailed described [here](https://input-output-hk.github.io/catalyst-voices/architecture/08_concepts/voting_transaction/crypto/#voters-proof) /// /// # Errors -/// - `GenerateVoterProofError` +/// - Provided arguments mismatch. Size of the provided `vote`, `encrypted_vote` and +/// `randomness` must be equal with each other. #[allow(clippy::module_name_repetitions)] pub fn generate_voter_proof( vote: &Vote, encrypted_vote: EncryptedVote, randomness: EncryptionRandomness, public_key: &PublicKey, commitment: &VoterProofCommitment, rng: &mut R, -) -> Result { - if vote.voting_options != encrypted_vote.0.len() || vote.voting_options != randomness.0.len() { - return Err(GenerateVoterProofError::ArgumentsMismatch( - vote.voting_options, - encrypted_vote.0.len(), - randomness.0.len(), - )); - } +) -> anyhow::Result { + ensure!( + vote.voting_options == encrypted_vote.0.len() && vote.voting_options == randomness.0.len(), + "Provided arguments mismatch. + Size of the provided `vote`: {0}, `encrypted_vote: {1}` and `randomness`: {2} must be equal with each other.", + vote.voting_options, + encrypted_vote.0.len(), + randomness.0.len(), + ); let proof = generate_unit_vector_proof( &vote.to_unit_vector(), @@ -75,3 +71,19 @@ pub fn verify_voter_proof( ) -> bool { verify_unit_vector_proof(&proof.0, encrypted_vote.0, public_key, &commitment.0) } + +#[cfg(test)] +mod tests { + use proptest::prelude::{any_with, Arbitrary, BoxedStrategy, Strategy}; + + use super::*; + + impl Arbitrary for VoterProof { + type Parameters = usize; + type Strategy = BoxedStrategy; + + fn arbitrary_with(size: Self::Parameters) -> Self::Strategy { + any_with::(size).prop_map(Self).boxed() + } + } +} diff --git a/rust/catalyst-voting/tests/voting_test.rs b/rust/catalyst-voting/tests/voting_test.rs index 75752d524b..1cd5a05878 100644 --- a/rust/catalyst-voting/tests/voting_test.rs +++ b/rust/catalyst-voting/tests/voting_test.rs @@ -1,15 +1,17 @@ //! A general voting integration test, which performs a full voting procedure. use catalyst_voting::{ - tally::{ - decrypt_tally, - proof::{generate_tally_proof, verify_tally_proof}, - tally, DecryptionTallySetup, - }, - voter::{ - encrypt_vote, - proof::{generate_voter_proof, verify_voter_proof, VoterProofCommitment}, - Vote, + vote_protocol::{ + tally::{ + decrypt_tally, + proof::{generate_tally_proof, verify_tally_proof}, + tally, DecryptionTallySetup, + }, + voter::{ + encrypt_vote, + proof::{generate_voter_proof, verify_voter_proof, VoterProofCommitment}, + Vote, + }, }, SecretKey, };