Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
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
5 changes: 5 additions & 0 deletions rust/catalyst-contest/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -13,3 +13,8 @@ license.workspace = true
workspace = true

[dependencies]
minicbor = { version = "0.25.1", features = ["alloc", "derive", "half"] }

cbork-utils = { version = "0.0.2", git = "https://github.com/input-output-hk/catalyst-libs.git", tag = "cbork-utils-v0.0.2" }
# TODO: FIXME: Use tag instead of branch!
catalyst-voting = { version = "0.0.1", git = "https://github.com/input-output-hk/catalyst-libs.git", branch = "cbor-encoding-for-catalyst-voting-types" }
153 changes: 153 additions & 0 deletions rust/catalyst-contest/src/choices.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,153 @@
//! Voters Choices.

use catalyst_voting::crypto::elgamal::Ciphertext;
use cbork_utils::decode_helper::decode_array_len;
use minicbor::{Decode, Decoder, Encode, Encoder, encode::Write};

use crate::row_proof::RowProof;

/// Voters Choices.
///
/// The CDDL schema:
/// ```cddl
/// choices = [ 0, clear-choices ] /
/// [ 1, elgamal-ristretto255-encrypted-choices ]
///
/// clear-choices = ( +clear-choice )
///
/// clear-choice = int
///
/// elgamal-ristretto255-encrypted-choices = [
/// [+ elgamal-ristretto255-encrypted-choice]
/// ? row-proof
/// ]
/// ```
#[derive(Clone, Debug, PartialEq, Eq, Hash)]
pub enum Choices {
/// A universal unencrypted set of choices.
Clear(Vec<i64>),
/// ElGamal/Ristretto255 encrypted choices.
Encrypted {
/// ElGamal/Ristretto255 encrypted choices.
choices: Vec<Ciphertext>,
/// A universal encrypted row proof.
row_proof: Option<RowProof>,
},
}

impl Decode<'_, ()> for Choices {
fn decode(
d: &mut Decoder<'_>,
ctx: &mut (),
) -> Result<Self, minicbor::decode::Error> {
let len = decode_array_len(d, "choices")?;
if len < 2 {
return Err(minicbor::decode::Error::message(format!(
"Unexpected choices array length {len}, expected at least 2"
)));
}
match u8::decode(d, ctx)? {
0 => Ok(Self::Clear(<Vec<i64>>::decode(d, ctx)?)),
1 => {
let len = decode_array_len(d, "elgamal-ristretto255-encrypted-choices")?;
if !(1..=2).contains(&len) {
return Err(minicbor::decode::Error::message(format!(
"Unexpected elgamal-ristretto255-encrypted-choices array length {len}, expected 1 or 2"
)));
}
let choices = <Vec<Ciphertext>>::decode(d, ctx)?;
let mut row_proof = None;
if len == 2 {
row_proof = Some(RowProof::decode(d, ctx)?);
}
Ok(Self::Encrypted { choices, row_proof })
},
val => {
Err(minicbor::decode::Error::message(format!(
"Unexpected choices value: {val}"
)))
},
}
}
}

impl Encode<()> for Choices {
fn encode<W: Write>(
&self,
e: &mut Encoder<W>,
ctx: &mut (),
) -> Result<(), minicbor::encode::Error<W::Error>> {
match self {
Choices::Clear(choices) => {
e.array(choices.len() as u64 + 1)?;
0.encode(e, ctx)?;
for choice in choices {
choice.encode(e, ctx)?;
}
},
Choices::Encrypted { choices, row_proof } => {
e.array(2)?;
1.encode(e, ctx)?;
e.array(choices.len() as u64 + u64::from(row_proof.is_some()))?;
choices.encode(e, ctx)?;
if let Some(row_proof) = row_proof {
row_proof.encode(e, ctx)?;
}
},
}
Ok(())
}
}

#[cfg(test)]
mod tests {
use super::*;
use crate::row_proof::{
ProofAnnouncement, ProofAnnouncementElement, ProofResponse, ProofScalar,
SingleSelectionProof,
};

#[test]
fn clear_roundtrip() {
let original = Choices::Clear(vec![1, 2, 3]);
let mut buffer = Vec::new();
original
.encode(&mut Encoder::new(&mut buffer), &mut ())
.unwrap();
let decoded = Choices::decode(&mut Decoder::new(&buffer), &mut ()).unwrap();
assert_eq!(original, decoded);
}

#[test]
fn elgamal_ristretto255_roundtrip() {
let bytes = [
1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20, 21, 22, 23, 24,
25, 26, 27, 28, 29, 30, 31, 32,
];
let original = Choices::Encrypted {
choices: vec![],
row_proof: Some(RowProof {
selections: vec![SingleSelectionProof {
announcement: ProofAnnouncement(
ProofAnnouncementElement(bytes),
ProofAnnouncementElement(bytes),
ProofAnnouncementElement(bytes),
),
choice: Ciphertext::zero(),
response: ProofResponse(
ProofScalar(bytes),
ProofScalar(bytes),
ProofScalar(bytes),
),
}],
scalar: ProofScalar(bytes),
}),
};
let mut buffer = Vec::new();
original
.encode(&mut Encoder::new(&mut buffer), &mut ())
.unwrap();
let decoded = Choices::decode(&mut Decoder::new(&buffer), &mut ()).unwrap();
assert_eq!(original, decoded);
}
}
184 changes: 184 additions & 0 deletions rust/catalyst-contest/src/contest_ballot.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,184 @@
//! An individual Ballot cast in a Contest by a registered user.

use std::collections::BTreeMap;

use cbork_utils::decode_helper::decode_map_len;
use minicbor::{Decode, Decoder, Encode, Encoder, encode::Write};

use crate::{Choices, EncryptedChoices};

/// An individual Ballot cast in a Contest by a registered user.
///
/// The CDDL schema:
/// ```cddl
/// contest-ballot-payload = {
/// + uint => choices
/// ? "column-proof" : column-proof
/// ? "matrix-proof" : matrix-proof
/// ? "voter-choice" : voter-choice
/// }
/// ```
#[derive(Clone, Debug, PartialEq, Eq, Hash)]
pub struct ContentBallot {
/// A map of voters choices.
pub choices: BTreeMap<u64, Choices>,
/// A universal encrypted column proof.
///
/// This is a placeholder for now and should always be `None`.
pub column_proof: Option<()>,
/// A universal encrypted matrix proof.
///
/// This is a placeholder for now and should always be `None`.
pub matrix_proof: Option<()>,
/// An encrypted voter choice payload.
pub voter_choices: Option<EncryptedChoices>,
}

impl Decode<'_, ()> for ContentBallot {
fn decode(
d: &mut Decoder<'_>,
ctx: &mut (),
) -> Result<Self, minicbor::decode::Error> {
use minicbor::data::Type;

let len = decode_map_len(d, "content ballot")?;

let mut choices = BTreeMap::new();
let column_proof = None;
let matrix_proof = None;
let mut voter_choices = None;
for _ in 0..len {
match d.datatype()? {
Type::U64 => {
let key = d.u64()?;
let val = Choices::decode(d, ctx)?;
choices.insert(key, val);
},
Type::String => {
match d.str()? {
"column-proof" => {
return Err(minicbor::decode::Error::message(
"column-proof is a placeholder and shouldn't be used",
));
},
"matrix-proof" => {
return Err(minicbor::decode::Error::message(
"matrix-proof is a placeholder and shouldn't be used",
));
},
"voter-choices" => voter_choices = Some(EncryptedChoices::decode(d, ctx)?),
key => {
return Err(minicbor::decode::Error::message(format!(
"Unexpected content ballot key value: {key:?}"
)));
},
}
},
t => {
return Err(minicbor::decode::Error::message(format!(
"Unexpected content ballot key type: {t:?}"
)));
},
}
}

Ok(Self {
choices,
column_proof,
matrix_proof,
voter_choices,
})
}
}

impl Encode<()> for ContentBallot {
fn encode<W: Write>(
&self,
e: &mut Encoder<W>,
_ctx: &mut (),
) -> Result<(), minicbor::encode::Error<W::Error>> {
let len = self.choices.len() as u64
+ u64::from(self.column_proof.is_some())
+ u64::from(self.matrix_proof.is_some())
+ u64::from(self.voter_choices.is_some());
e.map(len)?;

for (&key, val) in &self.choices {
e.u64(key)?.encode(val)?;
}
if let Some(column_proof) = self.column_proof.as_ref() {
e.str("column-proof")?.encode(column_proof)?;
}
if let Some(matrix_proof) = self.matrix_proof.as_ref() {
e.str("matrix-proof")?.encode(matrix_proof)?;
}
if let Some(voter_choices) = self.voter_choices.as_ref() {
e.str("voter-choices")?.encode(voter_choices)?;
}

Ok(())
}
}

#[cfg(test)]
mod tests {
use catalyst_voting::crypto::elgamal::Ciphertext;

use super::*;
use crate::{
EncryptedBlock, RowProof,
row_proof::{
ProofAnnouncement, ProofAnnouncementElement, ProofResponse, ProofScalar,
SingleSelectionProof,
},
};

#[test]
fn roundtrip() {
let bytes = [
1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20, 21, 22, 23, 24,
25, 26, 27, 28, 29, 30, 31, 32,
];
let original = ContentBallot {
choices: [
(1, Choices::Clear(vec![1, 2, 3, -4, -5])),
(2, Choices::Encrypted {
choices: vec![Ciphertext::zero()],
row_proof: None,
}),
(3, Choices::Encrypted {
choices: vec![Ciphertext::zero()],
row_proof: Some(RowProof {
selections: vec![SingleSelectionProof {
announcement: ProofAnnouncement(
ProofAnnouncementElement(bytes),
ProofAnnouncementElement(bytes),
ProofAnnouncementElement(bytes),
),
choice: Ciphertext::zero(),
response: ProofResponse(
ProofScalar(bytes),
ProofScalar(bytes),
ProofScalar(bytes),
),
}],
scalar: ProofScalar(bytes),
}),
}),
]
.into(),
column_proof: None,
matrix_proof: None,
voter_choices: Some(EncryptedChoices(vec![
EncryptedBlock([0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15]),
EncryptedBlock([1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16]),
])),
};
let mut buffer = Vec::new();
original
.encode(&mut Encoder::new(&mut buffer), &mut ())
.unwrap();
let decoded = ContentBallot::decode(&mut Decoder::new(&buffer), &mut ()).unwrap();
assert_eq!(original, decoded);
}
}
Loading