Skip to content
7 changes: 6 additions & 1 deletion rust/catalyst-voting/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,10 @@ license.workspace = true
[lints]
workspace = true

[[bench]]
name = "vote_protocol"
harness = false

[dependencies]
anyhow = "1.0.89"
rand_core = { version = "0.6.4", features = ["getrandom"] }
Expand All @@ -21,7 +25,8 @@ ed25519-dalek = { version = "2.1.1", features = ["rand_core"] }
blake2b_simd = "1.0.2"

[dev-dependencies]
proptest = {version = "1.5.0" }
criterion = "0.5.1"
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"
195 changes: 195 additions & 0 deletions rust/catalyst-voting/benches/vote_protocol.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,195 @@
//! `catalyst_voting::vote_protocol` benchmark
#![allow(
missing_docs,
clippy::missing_docs_in_private_items,
clippy::unwrap_used,
clippy::similar_names
)]

use catalyst_voting::{
crypto::default_rng,
vote_protocol::{
committee::{ElectionPublicKey, ElectionSecretKey},
tally::{
decrypt_tally,
proof::{generate_tally_proof, verify_tally_proof},
tally, DecryptionTallySetup,
},
voter::{
encrypt_vote,
proof::{generate_voter_proof, verify_voter_proof, VoterProofCommitment},
Vote,
},
},
};
use criterion::{criterion_group, criterion_main, Criterion};
use proptest::{
prelude::{any, Strategy},
strategy::ValueTree,
test_runner::TestRunner,
};
use test_strategy::Arbitrary;

const VOTING_OPTIONS: usize = 3;
const VOTERS_NUMBER: usize = 100;

#[derive(Arbitrary, Debug)]
struct Voter {
voting_power: u32,
#[strategy(0..VOTING_OPTIONS)]
choice: usize,
}

fn initial_setup() -> (
Vec<usize>,
Vec<u64>,
ElectionSecretKey,
ElectionPublicKey,
VoterProofCommitment,
) {
let mut runner = TestRunner::default();

let (choices, voting_powers) = any::<[Voter; VOTERS_NUMBER]>()
.prop_map(|voter| {
(
voter.iter().map(|v| v.choice).collect(),
voter.iter().map(|v| v.voting_power.into()).collect(),
)
})
.new_tree(&mut runner)
.unwrap()
.current();

let election_secret_key = ElectionSecretKey::random_with_default_rng();
let voter_proof_commitment = VoterProofCommitment::random_with_default_rng();
let election_public_key = election_secret_key.public_key();

(
choices,
voting_powers,
election_secret_key,
election_public_key,
voter_proof_commitment,
)
}

#[allow(clippy::too_many_lines)]
fn vote_protocol_benches(c: &mut Criterion) {
let mut group = c.benchmark_group("vote protocol benchmark");
group.sample_size(10);

let (choices, voting_powers, election_secret_key, election_public_key, voter_proof_commitment) =
initial_setup();
let votes: Vec<_> = choices
.iter()
.map(|choice| Vote::new(*choice, VOTING_OPTIONS).unwrap())
.collect();
let mut rng = default_rng();

let mut encrypted_votes = Vec::new();
let mut randomness = Vec::new();
group.bench_function("vote encryption", |b| {
b.iter(|| {
(encrypted_votes, randomness) = votes
.iter()
.map(|vote| encrypt_vote(vote, &election_public_key, &mut rng))
.unzip();
});
});

let mut voter_proofs = Vec::new();
group.bench_function("voter proof generation", |b| {
b.iter(|| {
voter_proofs = votes
.iter()
.zip(encrypted_votes.iter())
.zip(randomness.iter())
.map(|((v, enc_v), r)| {
generate_voter_proof(
v,
enc_v.clone(),
r.clone(),
&election_public_key,
&voter_proof_commitment,
&mut rng,
)
.unwrap()
})
.collect();
});
});

group.bench_function("voter proof verification", |b| {
b.iter(|| {
let is_ok = voter_proofs
.iter()
.zip(encrypted_votes.iter())
.all(|(p, enc_v)| {
verify_voter_proof(
enc_v.clone(),
&election_public_key,
&voter_proof_commitment,
p,
)
});
assert!(is_ok);
});
});

let mut encrypted_tallies = Vec::new();
group.bench_function("tally", |b| {
b.iter(|| {
encrypted_tallies = (0..VOTING_OPTIONS)
.map(|voting_option| {
tally(voting_option, &encrypted_votes, &voting_powers).unwrap()
})
.collect();
});
});

let total_voting_power = voting_powers.iter().sum();
let mut decryption_tally_setup = None;
group.bench_function("decryption tally setup initialization", |b| {
b.iter(|| {
decryption_tally_setup = Some(DecryptionTallySetup::new(total_voting_power).unwrap());
});
});
let decryption_tally_setup = decryption_tally_setup.unwrap();

let mut decrypted_tallies = Vec::new();
group.bench_function("decrypt tally", |b| {
b.iter(|| {
decrypted_tallies = encrypted_tallies
.iter()
.map(|t| decrypt_tally(t, &election_secret_key, &decryption_tally_setup).unwrap())
.collect();
});
});

let mut tally_proofs = Vec::new();
group.bench_function("tally proof generation", |b| {
b.iter(|| {
tally_proofs = encrypted_tallies
.iter()
.map(|t| generate_tally_proof(t, &election_secret_key, &mut rng))
.collect();
});
});

group.bench_function("tally proof verification", |b| {
b.iter(|| {
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);
});
});

group.finish();
}

criterion_group!(benches, vote_protocol_benches);

criterion_main!(benches);
24 changes: 18 additions & 6 deletions rust/catalyst-voting/src/crypto/zk_unit_vector/decoding.rs
Original file line number Diff line number Diff line change
Expand Up @@ -27,22 +27,34 @@ impl UnitVectorProof {
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}."))
Announcement::from_bytes(&bytes).map_err(|e| {
anyhow!(
"Cannot decode announcement at {i}, \
error: {e}."
)
})
})
.collect::<anyhow::Result<_>>()?;
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}."))
Ciphertext::from_bytes(&bytes).map_err(|e| {
anyhow!(
"Cannot decode ciphertext at {i}, \
error: {e}."
)
})
})
.collect::<anyhow::Result<_>>()?;
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}."))
ResponseRandomness::from_bytes(&bytes).map_err(|e| {
anyhow!(
"Cannot decode response randomness at {i}, \
error: {e}."
)
})
})
.collect::<anyhow::Result<_>>()?;

Expand Down
21 changes: 14 additions & 7 deletions rust/catalyst-voting/src/txs/v1/decoding.rs
Original file line number Diff line number Diff line change
Expand Up @@ -112,14 +112,16 @@ impl Tx {
let padding_tag = read_be_u8(reader).map_err(|_| anyhow!("Missing padding tag field."))?;
ensure!(
padding_tag == PADDING_TAG,
"Invalid padding tag field value, must be equals to {PADDING_TAG}, provided: {padding_tag}.",
"Invalid padding tag field value, must be equals to {PADDING_TAG}, \
provided: {padding_tag}.",
);

let fragment_tag =
read_be_u8(reader).map_err(|_| anyhow!("Missing fragment tag field."))?;
ensure!(
fragment_tag == FRAGMENT_TAG,
"Invalid fragment tag field value, must be equals to {FRAGMENT_TAG}, provided: {fragment_tag}.",
"Invalid fragment tag field value, must be equals to {FRAGMENT_TAG}, \
provided: {fragment_tag}.",
);

let vote_plan_id =
Expand Down Expand Up @@ -148,7 +150,8 @@ impl Tx {
},
tag => {
bail!(
"Invalid vote tag value, must be equals to {PUBLIC_VOTE_TAG} or {PRIVATE_VOTE_TAG}, provided: {tag}"
"Invalid vote tag value, \
must be equals to {PUBLIC_VOTE_TAG} or {PRIVATE_VOTE_TAG}, provided: {tag}"
)
},
};
Expand All @@ -160,20 +163,23 @@ impl Tx {
read_be_u8(reader).map_err(|_| anyhow!("Missing inputs amount field."))?;
ensure!(
inputs_amount == NUMBER_OF_INPUTS,
"Invalid number of inputs, expected: {NUMBER_OF_INPUTS}, provided: {inputs_amount}",
"Invalid number of inputs, expected: {NUMBER_OF_INPUTS}, \
provided: {inputs_amount}",
);

let outputs_amount =
read_be_u8(reader).map_err(|_| anyhow!("Missing outputs amount field."))?;
ensure!(
outputs_amount == NUMBER_OF_OUTPUTS,
"Invalid number of outputs, expected: {NUMBER_OF_OUTPUTS}, provided: {outputs_amount}",
"Invalid number of outputs, expected: {NUMBER_OF_OUTPUTS}, \
provided: {outputs_amount}",
);

let input_tag = read_be_u8(reader).map_err(|_| anyhow!("Missing input tag field."))?;
ensure!(
input_tag == INPUT_TAG,
"Invalid input tag, expected: {INPUT_TAG}, provided: {input_tag}",
"Invalid input tag, expected: {INPUT_TAG}, \
provided: {input_tag}",
);

// skip value
Expand All @@ -187,7 +193,8 @@ impl Tx {
let witness_tag = read_be_u8(reader).map_err(|_| anyhow!("Missing witness tag field."))?;
ensure!(
witness_tag == WITNESS_TAG,
"Invalid witness tag, expected: {WITNESS_TAG}, provided: {witness_tag}",
"Invalid witness tag, expected: {WITNESS_TAG}, \
provided: {witness_tag}",
);

// Skip nonce field
Expand Down
30 changes: 18 additions & 12 deletions rust/catalyst-voting/src/vote_protocol/tally/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -58,18 +58,22 @@ pub fn tally(
) -> anyhow::Result<EncryptedTally> {
ensure!(
votes.len() == voting_powers.len(),
"Votes and voting power length mismatch. Votes amount: {0}. Voting powers amount: {1}.",
"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(anyhow!("Invalid encrypted vote at index {i}. Does not have a ciphertext for the voting option {voting_option}.") )?;
ciphertexts_per_voting_option.push(ciphertext);
}
let ciphertexts_per_voting_option = votes
.iter()
.enumerate()
.map(|(i, v)| {
v.get_ciphertext_for_choice(voting_option).ok_or(anyhow!(
"Invalid encrypted vote at index {i}. \
Does not have a ciphertext for the voting option {voting_option}."
))
})
.collect::<anyhow::Result<Vec<_>>>()?;

let zero_ciphertext = Ciphertext::zero();

Expand Down Expand Up @@ -97,9 +101,11 @@ pub fn decrypt_tally(
) -> anyhow::Result<u64> {
let ge = decrypt(&tally_result.0, &secret_key.0);

let res = setup
.discrete_log_setup
.discrete_log(ge)
.map_err(|_| anyhow!("Cannot decrypt tally result. Provided an invalid secret key or invalid encrypted tally result."))?;
let res = setup.discrete_log_setup.discrete_log(ge).map_err(|_| {
anyhow!(
"Cannot decrypt tally result. \
Provided an invalid secret key or invalid encrypted tally result."
)
})?;
Ok(res)
}
6 changes: 3 additions & 3 deletions rust/catalyst-voting/tests/voting_test.rs
Original file line number Diff line number Diff line change
Expand Up @@ -17,17 +17,17 @@ use proptest::prelude::ProptestConfig;
use test_strategy::{proptest, Arbitrary};

const VOTING_OPTIONS: usize = 3;
const VOTERS_NUMBER: usize = 100;

#[derive(Arbitrary, Debug)]
struct Voter {
voting_power: u32,
// range from 0 to `VOTING_OPTIONS`
#[strategy(0..3_usize)]
#[strategy(0..VOTING_OPTIONS)]
choice: usize,
}

#[proptest(ProptestConfig::with_cases(1))]
fn voting_test(voters: [Voter; 100]) {
fn voting_test(voters: [Voter; VOTERS_NUMBER]) {
let election_secret_key = ElectionSecretKey::random_with_default_rng();
let election_public_key = election_secret_key.public_key();
let voter_proof_commitment = VoterProofCommitment::random_with_default_rng();
Expand Down
Loading