Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
41 commits
Select commit Hold shift + click to select a range
05af510
initialize a new crate
Mr-Leshiy Sep 24, 2024
6935a1b
add intentionally failed test
Mr-Leshiy Sep 24, 2024
78f601f
fix CI
Mr-Leshiy Sep 24, 2024
9423567
fix
Mr-Leshiy Sep 24, 2024
5d6b95b
fix
Mr-Leshiy Sep 24, 2024
b8d7ece
update vscode setting.recommended.json
Mr-Leshiy Sep 24, 2024
d644602
Merge branch 'main' into feat/voting-crate-setup
Mr-Leshiy Sep 24, 2024
e70f571
Merge branch 'main' into feat/el-gamal
Mr-Leshiy Sep 25, 2024
0803b50
add a basic interfaces for the vote part
Mr-Leshiy Sep 25, 2024
0ea84b3
add basic elgamal encryption based on the ristretto255 group
Mr-Leshiy Sep 25, 2024
91106ff
add arithmetic tests for ristretto255
Mr-Leshiy Sep 26, 2024
e1b8251
fix tests
Mr-Leshiy Sep 26, 2024
6c2d961
wip
Mr-Leshiy Sep 26, 2024
594d114
add decryption algorithm, add tests
Mr-Leshiy Sep 26, 2024
c1b749d
fix CI
Mr-Leshiy Sep 26, 2024
0a5bb99
remove unused std_ops_gen
Mr-Leshiy Sep 26, 2024
0348b6c
add new voter module
Mr-Leshiy Sep 27, 2024
9d9ddd6
add EncryptionRandomness random generation
Mr-Leshiy Sep 27, 2024
c342a1e
add a tally function
Mr-Leshiy Sep 27, 2024
bc658d6
Merge branch 'main' into feat/tally
Mr-Leshiy Sep 27, 2024
7eacd5b
fix
Mr-Leshiy Sep 27, 2024
15d97c6
wip
Mr-Leshiy Sep 27, 2024
dd1b1a3
add a babystep implementation
Mr-Leshiy Sep 28, 2024
a3c4d61
wip
Mr-Leshiy Sep 28, 2024
dcd1484
refactor, add decrypt_tally_result
Mr-Leshiy Sep 28, 2024
4936687
wip
Mr-Leshiy Sep 28, 2024
f572ad3
wip
Mr-Leshiy Sep 28, 2024
81a6323
add voting test
Mr-Leshiy Sep 28, 2024
f988417
remove rayon dependency for now
Mr-Leshiy Sep 29, 2024
37cf886
fix spelling, remove rayon
Mr-Leshiy Sep 30, 2024
24da2bc
fix
Mr-Leshiy Sep 30, 2024
804c723
remove unused anyhow dep
Mr-Leshiy Sep 30, 2024
921ad46
intentionally break the test
Mr-Leshiy Sep 30, 2024
fa63fe1
try
Mr-Leshiy Sep 30, 2024
c54f806
wip
Mr-Leshiy Sep 30, 2024
c444a02
update DecryptionTallySetup interface
Mr-Leshiy Sep 30, 2024
a63d11a
add doctest example
Mr-Leshiy Sep 30, 2024
688f5b7
refactor, make voting_test as integration test
Mr-Leshiy Sep 30, 2024
8466187
fix baby_step_giant_step_test
Mr-Leshiy Oct 1, 2024
ef104e2
Merge branch 'main' into feat/tally
Mr-Leshiy Oct 2, 2024
2c4b6bf
Merge branch 'main' into feat/tally
Mr-Leshiy Oct 2, 2024
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions .config/dictionaries/project.dic
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ Arissara
asyncio
Attributes
auditability
babystep
backpressure
bech
bimap
Expand All @@ -34,6 +35,7 @@ Chotivichit
chrono
cids
ciphertext
ciphertexts
codegen
codepoints
coti
Expand Down Expand Up @@ -83,6 +85,7 @@ futimens
genhtml
GETFL
getres
giantstep
gmtime
gossipsub
happ
Expand Down
7 changes: 5 additions & 2 deletions rust/catalyst-voting/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -11,9 +11,12 @@ license.workspace = true
workspace = true

[dependencies]
anyhow = "1.0.71"
thiserror = "1.0.56"
rand_core = "0.6.4"
curve25519-dalek = { version = "4.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` attribute macro,
# after this PR will be merged https://github.com/proptest-rs/proptest/pull/523
test-strategy = "0.4.0"
59 changes: 53 additions & 6 deletions rust/catalyst-voting/src/crypto/elgamal.rs
Original file line number Diff line number Diff line change
@@ -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;

Expand All @@ -21,38 +21,63 @@ pub struct Ciphertext(GroupElement, GroupElement);

impl SecretKey {
/// Generate a random `SecretKey` value from the random number generator.
pub fn random<R: CryptoRngCore>(rng: &mut R) -> Self {
pub fn generate<R: CryptoRngCore>(rng: &mut R) -> Self {
Self(Scalar::random(rng))
}

/// Generate a corresponding `PublicKey`.
#[must_use]
pub fn public_key(&self) -> PublicKey {
PublicKey(GroupElement::GENERATOR.mul(&self.0))
}
}

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`.
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)
}

/// 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::{
arbitrary::any,
prelude::{Arbitrary, BoxedStrategy, Strategy},
property_test,
};
use test_strategy::proptest;

use super::*;

Expand All @@ -65,7 +90,29 @@ mod tests {
}
}

#[property_test]
#[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,
) {
Expand Down
123 changes: 123 additions & 0 deletions rust/catalyst-voting/src/crypto/group/babystep_giantstep.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,123 @@
//! 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<GroupElement, u64>,
/// 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<u64>) -> Result<Self, BabyStepError> {
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<u64, BabyStepError> {
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);
}
}
5 changes: 3 additions & 2 deletions rust/catalyst-voting/src/crypto/group/mod.rs
Original file line number Diff line number Diff line change
@@ -1,7 +1,8 @@
//! Group definitions used in voting protocol.
//! For more information, see: <https://input-output-hk.github.io/catalyst-voices/architecture/08_concepts/voting_transaction/crypto/#a-group-definition>

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};
22 changes: 18 additions & 4 deletions rust/catalyst-voting/src/crypto/group/ristretto255.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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},
Expand All @@ -26,6 +29,12 @@ impl From<u64> for Scalar {
}
}

impl Hash for GroupElement {
fn hash<H: std::hash::Hasher>(&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<R: CryptoRngCore>(rng: &mut R) -> Self {
Expand All @@ -44,6 +53,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)
Expand Down Expand Up @@ -124,8 +138,8 @@ mod tests {
use proptest::{
arbitrary::any,
prelude::{Arbitrary, BoxedStrategy, Strategy},
property_test,
};
use test_strategy::proptest;

use super::*;

Expand All @@ -138,7 +152,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);
Expand All @@ -150,7 +164,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);
Expand Down
4 changes: 2 additions & 2 deletions rust/catalyst-voting/src/crypto/mod.rs
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
//! Crypto primitives which are used by voting protocol.

mod elgamal;
mod group;
pub(crate) mod elgamal;
pub(crate) mod group;
Loading