Skip to content

Commit 0a5fd6e

Browse files
authored
feat(rust/catalyst-voting): Jormungandr transaction serde (#58)
* move vote protocol under the vote_protocol mod * add jormungandr tx struct * add CipherText serde * add EncryptedVote decoding functionality * add new deserializers * refactor * wip * wip * replace thiserror with anyhow * move decoding functionalities to separate module * wip * add test * fix * refactor * fix tests * wip * wip * fix spelling
1 parent 430de06 commit 0a5fd6e

File tree

22 files changed

+1001
-247
lines changed

22 files changed

+1001
-247
lines changed

rust/catalyst-voting/Cargo.toml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,7 @@ license.workspace = true
1111
workspace = true
1212

1313
[dependencies]
14-
thiserror = "1.0.64"
14+
anyhow = "1.0.89"
1515
rand_core = "0.6.4"
1616
curve25519-dalek = { version = "4.1.3", features = ["digest"] }
1717
blake2b_simd = "1.0.2"

rust/catalyst-voting/src/crypto/babystep_giantstep.rs

Lines changed: 15 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,8 @@
33
44
use std::collections::HashMap;
55

6+
use anyhow::{bail, ensure};
7+
68
use crate::crypto::group::{GroupElement, Scalar};
79

810
/// Default balance value.
@@ -22,16 +24,6 @@ pub struct BabyStepGiantStep {
2224
giant_step: GroupElement,
2325
}
2426

25-
#[derive(thiserror::Error, Debug)]
26-
pub enum BabyStepError {
27-
/// Invalid max value or balance
28-
#[error("Maximum value and balance must be greater than zero, provided max value: {0} and balance: {1}.")]
29-
InvalidMaxValueOrBalance(u64, u64),
30-
/// Max value exceeded
31-
#[error("Max log value exceeded. Means that the actual discrete log for the provided group element is higher than the provided `max_log_value`.")]
32-
MaxLogExceeded,
33-
}
34-
3527
impl BabyStepGiantStep {
3628
/// Creates a new setup for the baby-step giant-step algorithm.
3729
///
@@ -47,16 +39,15 @@ impl BabyStepGiantStep {
4739
/// `baby_step_giant_step` function for the same `max_value`.
4840
///
4941
/// # Errors
50-
/// - `BabyStepError`
51-
pub fn new(max_log_value: u64, balance: Option<u64>) -> Result<Self, BabyStepError> {
42+
/// - Maximum value and balance must be greater than zero.
43+
pub fn new(max_log_value: u64, balance: Option<u64>) -> anyhow::Result<Self> {
5244
let balance = balance.unwrap_or(DEFAULT_BALANCE);
5345

54-
if balance == 0 || max_log_value == 0 {
55-
return Err(BabyStepError::InvalidMaxValueOrBalance(
56-
max_log_value,
57-
balance,
58-
));
59-
}
46+
ensure!(
47+
balance != 0 && max_log_value != 0,
48+
"Maximum value and balance must be greater than zero,
49+
provided max value: {max_log_value} and balance: {balance}."
50+
);
6051

6152
#[allow(
6253
clippy::cast_possible_truncation,
@@ -85,18 +76,21 @@ impl BabyStepGiantStep {
8576
/// Solve the discrete log using baby step giant step algorithm.
8677
///
8778
/// # Errors
88-
/// - `BabyStepError`
89-
pub fn discrete_log(&self, mut point: GroupElement) -> Result<u64, BabyStepError> {
79+
/// - Max log value exceeded.
80+
pub fn discrete_log(&self, mut point: GroupElement) -> anyhow::Result<u64> {
9081
for baby_step in 0..=self.baby_step_size {
9182
if let Some(x) = self.table.get(&point) {
9283
let r = baby_step * self.baby_step_size + x;
9384
return Ok(r);
9485
}
9586
point = &point + &self.giant_step;
9687
}
88+
9789
// If we get here, the point is not in the table
9890
// So we exceeded the maximum value of the discrete log
99-
Err(BabyStepError::MaxLogExceeded)
91+
bail!("Max log value exceeded.
92+
Means that the actual discrete log for the provided group element is higher than the provided `max_log_value`."
93+
)
10094
}
10195
}
10296

Lines changed: 96 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,96 @@
1+
//! Elgamal objects decoding implementation
2+
3+
use anyhow::anyhow;
4+
5+
use super::{Ciphertext, GroupElement, PublicKey, Scalar, SecretKey};
6+
7+
impl PublicKey {
8+
/// `PublicKey` bytes size
9+
pub const BYTES_SIZE: usize = GroupElement::BYTES_SIZE;
10+
11+
/// Convert this `PublicKey` to its underlying sequence of bytes.
12+
#[must_use]
13+
pub fn to_bytes(&self) -> [u8; Self::BYTES_SIZE] {
14+
self.0.to_bytes()
15+
}
16+
17+
/// Attempt to construct a `PublicKey` from a byte representation.
18+
///
19+
/// # Errors
20+
/// - Cannot decode group element field.
21+
pub fn from_bytes(bytes: &[u8; Self::BYTES_SIZE]) -> anyhow::Result<Self> {
22+
GroupElement::from_bytes(bytes).map(Self)
23+
}
24+
}
25+
26+
impl SecretKey {
27+
/// `SecretKey` bytes size
28+
pub const BYTES_SIZE: usize = Scalar::BYTES_SIZE;
29+
30+
/// Convert this `SecretKey` to its underlying sequence of bytes.
31+
#[must_use]
32+
pub fn to_bytes(&self) -> [u8; Self::BYTES_SIZE] {
33+
self.0.to_bytes()
34+
}
35+
36+
/// Attempt to construct a `SecretKey` from a byte representation.
37+
///
38+
/// # Errors
39+
/// - Cannot decode scalar field.
40+
pub fn from_bytes(bytes: [u8; Self::BYTES_SIZE]) -> anyhow::Result<Self> {
41+
Scalar::from_bytes(bytes).map(Self)
42+
}
43+
}
44+
45+
impl Ciphertext {
46+
/// `Ciphertext` bytes size
47+
pub const BYTES_SIZE: usize = GroupElement::BYTES_SIZE * 2;
48+
49+
/// Convert this `Ciphertext` to its underlying sequence of bytes.
50+
pub fn to_bytes(&self) -> [u8; Self::BYTES_SIZE] {
51+
let mut res = [0; Self::BYTES_SIZE];
52+
res[0..32].copy_from_slice(&self.0.to_bytes());
53+
res[32..64].copy_from_slice(&self.1.to_bytes());
54+
res
55+
}
56+
57+
/// Attempt to construct a `Ciphertext` from a byte representation.
58+
///
59+
/// # Errors
60+
/// - Cannot decode group element field.
61+
#[allow(clippy::unwrap_used)]
62+
pub fn from_bytes(bytes: &[u8; Self::BYTES_SIZE]) -> anyhow::Result<Self> {
63+
Ok(Self(
64+
GroupElement::from_bytes(bytes[0..32].try_into().unwrap())
65+
.map_err(|_| anyhow!("Cannot decode first group element field."))?,
66+
GroupElement::from_bytes(bytes[32..64].try_into().unwrap())
67+
.map_err(|_| anyhow!("Cannot decode second group element field."))?,
68+
))
69+
}
70+
}
71+
72+
#[cfg(test)]
73+
mod tests {
74+
use test_strategy::proptest;
75+
76+
use super::*;
77+
78+
#[proptest]
79+
fn keys_to_bytes_from_bytes_test(s1: SecretKey) {
80+
let bytes = s1.to_bytes();
81+
let s2 = SecretKey::from_bytes(bytes).unwrap();
82+
assert_eq!(s1, s2);
83+
84+
let p1 = s1.public_key();
85+
let bytes = p1.to_bytes();
86+
let p2 = PublicKey::from_bytes(&bytes).unwrap();
87+
assert_eq!(p1, p2);
88+
}
89+
90+
#[proptest]
91+
fn ciphertext_to_bytes_from_bytes_test(c1: Ciphertext) {
92+
let bytes = c1.to_bytes();
93+
let c2 = Ciphertext::from_bytes(&bytes).unwrap();
94+
assert_eq!(c1, c2);
95+
}
96+
}

rust/catalyst-voting/src/crypto/elgamal.rs renamed to rust/catalyst-voting/src/crypto/elgamal/mod.rs

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,8 @@
11
//! Implementation of the lifted ``ElGamal`` crypto system, and combine with `ChaCha`
22
//! stream cipher to produce a hybrid encryption scheme.
33
4+
mod decoding;
5+
46
use std::ops::{Add, Deref, Mul};
57

68
use rand_core::CryptoRngCore;
@@ -116,6 +118,17 @@ mod tests {
116118
}
117119
}
118120

121+
impl Arbitrary for Ciphertext {
122+
type Parameters = ();
123+
type Strategy = BoxedStrategy<Self>;
124+
125+
fn arbitrary_with((): Self::Parameters) -> Self::Strategy {
126+
any::<(GroupElement, GroupElement)>()
127+
.prop_map(|(g1, g2)| Ciphertext(g1, g2))
128+
.boxed()
129+
}
130+
}
131+
119132
#[proptest]
120133
fn ciphertext_add_test(e1: Scalar, e2: Scalar, e3: Scalar, e4: Scalar) {
121134
let g1 = GroupElement::GENERATOR.mul(&e1);
Lines changed: 71 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,71 @@
1+
//! ristretto255 objects decoding implementation
2+
3+
use anyhow::anyhow;
4+
use curve25519_dalek::{ristretto::CompressedRistretto, scalar::Scalar as IScalar};
5+
6+
use super::{GroupElement, Scalar};
7+
8+
impl Scalar {
9+
/// `Scalar` bytes size
10+
pub const BYTES_SIZE: usize = 32;
11+
12+
/// Attempt to construct a `Scalar` from a canonical byte representation.
13+
///
14+
/// # Errors
15+
/// - Cannot decode scalar.
16+
pub fn from_bytes(bytes: [u8; Self::BYTES_SIZE]) -> anyhow::Result<Scalar> {
17+
IScalar::from_canonical_bytes(bytes)
18+
.map(Scalar)
19+
.into_option()
20+
.ok_or(anyhow!("Cannot decode scalar."))
21+
}
22+
23+
/// Convert this `Scalar` to its underlying sequence of bytes.
24+
pub fn to_bytes(&self) -> [u8; Self::BYTES_SIZE] {
25+
self.0.to_bytes()
26+
}
27+
}
28+
29+
impl GroupElement {
30+
/// `Scalar` bytes size
31+
pub const BYTES_SIZE: usize = 32;
32+
33+
/// Attempt to construct a `Scalar` from a compressed value byte representation.
34+
///
35+
/// # Errors
36+
/// - Cannot decode group element.
37+
pub fn from_bytes(bytes: &[u8; Self::BYTES_SIZE]) -> anyhow::Result<Self> {
38+
Ok(GroupElement(
39+
CompressedRistretto::from_slice(bytes)?
40+
.decompress()
41+
.ok_or(anyhow!("Cannot decode group element."))?,
42+
))
43+
}
44+
45+
/// Convert this `GroupElement` to its underlying sequence of bytes.
46+
/// Always encode the compressed value.
47+
pub fn to_bytes(&self) -> [u8; Self::BYTES_SIZE] {
48+
self.0.compress().to_bytes()
49+
}
50+
}
51+
52+
#[cfg(test)]
53+
mod tests {
54+
use test_strategy::proptest;
55+
56+
use super::*;
57+
58+
#[proptest]
59+
fn scalar_to_bytes_from_bytes_test(e1: Scalar) {
60+
let bytes = e1.to_bytes();
61+
let e2 = Scalar::from_bytes(bytes).unwrap();
62+
assert_eq!(e1, e2);
63+
}
64+
65+
#[proptest]
66+
fn group_element_to_bytes_from_bytes_test(ge1: GroupElement) {
67+
let bytes = ge1.to_bytes();
68+
let ge2 = GroupElement::from_bytes(&bytes).unwrap();
69+
assert_eq!(ge1, ge2);
70+
}
71+
}

rust/catalyst-voting/src/crypto/group/ristretto255.rs renamed to rust/catalyst-voting/src/crypto/group/ristretto255/mod.rs

Lines changed: 3 additions & 39 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,8 @@
22
33
// cspell: words BASEPOINT
44

5+
mod decoding;
6+
57
use std::{
68
hash::Hash,
79
ops::{Add, Mul, Sub},
@@ -10,7 +12,7 @@ use std::{
1012
use curve25519_dalek::{
1113
constants::{RISTRETTO_BASEPOINT_POINT, RISTRETTO_BASEPOINT_TABLE},
1214
digest::{consts::U64, Digest},
13-
ristretto::{CompressedRistretto, RistrettoPoint as Point},
15+
ristretto::RistrettoPoint as Point,
1416
scalar::Scalar as IScalar,
1517
traits::Identity,
1618
};
@@ -69,16 +71,6 @@ impl Scalar {
6971
Scalar(self.0.invert())
7072
}
7173

72-
/// Convert this `Scalar` to its underlying sequence of bytes.
73-
pub fn to_bytes(&self) -> [u8; 32] {
74-
self.0.to_bytes()
75-
}
76-
77-
/// Attempt to construct a `Scalar` from a canonical byte representation.
78-
pub fn from_bytes(bytes: [u8; 32]) -> Option<Scalar> {
79-
IScalar::from_canonical_bytes(bytes).map(Scalar).into()
80-
}
81-
8274
/// Generate a `Scalar` from a hash digest.
8375
pub fn from_hash<D>(hash: D) -> Scalar
8476
where D: Digest<OutputSize = U64> {
@@ -94,19 +86,6 @@ impl GroupElement {
9486
pub fn zero() -> Self {
9587
GroupElement(Point::identity())
9688
}
97-
98-
/// Convert this `GroupElement` to its underlying sequence of bytes.
99-
/// Always encode the compressed value.
100-
pub fn to_bytes(&self) -> [u8; 32] {
101-
self.0.compress().to_bytes()
102-
}
103-
104-
/// Attempt to construct a `Scalar` from a compressed value byte representation.
105-
pub fn from_bytes(bytes: &[u8; 32]) -> Option<Self> {
106-
Some(GroupElement(
107-
CompressedRistretto::from_slice(bytes).ok()?.decompress()?,
108-
))
109-
}
11089
}
11190

11291
// `std::ops` traits implementations
@@ -201,21 +180,6 @@ mod tests {
201180
}
202181
}
203182

204-
#[proptest]
205-
fn scalar_to_bytes_from_bytes_test(e1: Scalar) {
206-
let bytes = e1.to_bytes();
207-
let e2 = Scalar::from_bytes(bytes).unwrap();
208-
assert_eq!(e1, e2);
209-
}
210-
211-
#[proptest]
212-
fn group_element_to_bytes_from_bytes_test(e: Scalar) {
213-
let ge1 = GroupElement::GENERATOR.mul(&e);
214-
let bytes = ge1.to_bytes();
215-
let ge2 = GroupElement::from_bytes(&bytes).unwrap();
216-
assert_eq!(ge1, ge2);
217-
}
218-
219183
#[proptest]
220184
fn scalar_arithmetic_tests(e1: Scalar, e2: Scalar, e3: Scalar) {
221185
assert_eq!(&(&e1 + &e2) + &e3, &e1 + &(&e2 + &e3));
Lines changed: 6 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,8 @@
11
//! Crypto primitives which are used by voting protocol.
22
3-
pub(crate) mod babystep_giantstep;
4-
pub(crate) mod elgamal;
5-
pub(crate) mod group;
6-
pub(crate) mod hash;
7-
pub(crate) mod zk_dl_equality;
8-
pub(crate) mod zk_unit_vector;
3+
pub mod babystep_giantstep;
4+
pub mod elgamal;
5+
pub mod group;
6+
pub mod hash;
7+
pub mod zk_dl_equality;
8+
pub mod zk_unit_vector;

0 commit comments

Comments
 (0)