diff --git a/rust/catalyst-contest/Cargo.toml b/rust/catalyst-contest/Cargo.toml index 2f17d8eec9..2e33ee3c52 100644 --- a/rust/catalyst-contest/Cargo.toml +++ b/rust/catalyst-contest/Cargo.toml @@ -13,3 +13,6 @@ 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" } diff --git a/rust/catalyst-contest/src/choices.rs b/rust/catalyst-contest/src/choices.rs new file mode 100644 index 0000000000..3c0cbafaa6 --- /dev/null +++ b/rust/catalyst-contest/src/choices.rs @@ -0,0 +1,155 @@ +//! Voters Choices. + +use cbork_utils::decode_helper::decode_array_len; +use minicbor::{Decode, Decoder, Encode, Encoder, encode::Write}; + +use crate::{elgamal_ristretto255_choice::ElgamalRistretto255Choice, 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), + /// ElGamal/Ristretto255 encrypted choices. + ElgamalRistretto255 { + /// ElGamal/Ristretto255 encrypted choices. + choices: Vec, + /// A universal encrypted row proof. + row_proof: Option, + }, +} + +impl Decode<'_, ()> for Choices { + fn decode( + d: &mut Decoder<'_>, + ctx: &mut (), + ) -> Result { + 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(>::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 = >::decode(d, ctx)?; + let mut row_proof = None; + if len == 2 { + row_proof = Some(RowProof::decode(d, ctx)?); + } + Ok(Self::ElgamalRistretto255 { choices, row_proof }) + }, + val => { + Err(minicbor::decode::Error::message(format!( + "Unexpected choices value: {val}" + ))) + }, + } + } +} + +impl Encode<()> for Choices { + fn encode( + &self, + e: &mut Encoder, + ctx: &mut (), + ) -> Result<(), minicbor::encode::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::ElgamalRistretto255 { 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::ElgamalRistretto255 { + choices: vec![], + row_proof: Some(RowProof { + selections: vec![SingleSelectionProof { + announcement: ProofAnnouncement( + ProofAnnouncementElement(bytes), + ProofAnnouncementElement(bytes), + ProofAnnouncementElement(bytes), + ), + choice: ElgamalRistretto255Choice { + c1: bytes, + c2: bytes, + }, + 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); + } +} diff --git a/rust/catalyst-contest/src/column_proof.rs b/rust/catalyst-contest/src/column_proof.rs new file mode 100644 index 0000000000..c959823e52 --- /dev/null +++ b/rust/catalyst-contest/src/column_proof.rs @@ -0,0 +1,68 @@ +//! A universal encrypted column proof. + +use cbork_utils::decode_helper::decode_array_len; +use minicbor::{Decode, Decoder, Encode, Encoder, encode::Write}; + +/// A length of the underlying CBOR array. +const ARRAY_LEN: u64 = 2; + +/// A universal encrypted column proof. +/// +/// The CDDL schema: +/// ```cddl +/// column-proof = [ uint, [ +undefined ] ] +/// ``` +#[derive(Clone, Debug, PartialEq, Eq, Hash)] +pub struct ColumnProof(pub u64); + +impl Decode<'_, ()> for ColumnProof { + fn decode( + d: &mut Decoder<'_>, + ctx: &mut (), + ) -> Result { + let len = decode_array_len(d, "column proof")?; + if len != ARRAY_LEN { + return Err(minicbor::decode::Error::message(format!( + "Unexpected column proof array length {len}, expected {ARRAY_LEN}" + ))); + } + let val = u64::decode(d, ctx)?; + + let len = decode_array_len(d, "column proof undefined part")?; + for _ in 0..len { + d.undefined()?; + } + + Ok(ColumnProof(val)) + } +} + +impl Encode<()> for ColumnProof { + fn encode( + &self, + e: &mut Encoder, + ctx: &mut (), + ) -> Result<(), minicbor::encode::Error> { + e.array(ARRAY_LEN)?; + self.0.encode(e, ctx)?; + e.array(1)?; + e.undefined()?; + Ok(()) + } +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn roundtrip() { + let original = ColumnProof(1); + let mut buffer = Vec::new(); + original + .encode(&mut Encoder::new(&mut buffer), &mut ()) + .unwrap(); + let decoded = ColumnProof::decode(&mut Decoder::new(&buffer), &mut ()).unwrap(); + assert_eq!(original, decoded); + } +} diff --git a/rust/catalyst-contest/src/contest_ballot.rs b/rust/catalyst-contest/src/contest_ballot.rs new file mode 100644 index 0000000000..b5afa09c5d --- /dev/null +++ b/rust/catalyst-contest/src/contest_ballot.rs @@ -0,0 +1,180 @@ +//! 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, ColumnProof, EncryptedChoices, MatrixProof}; + +/// 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, + /// A universal encrypted column proof. + pub column_proof: Option, + /// A universal encrypted matrix proof. + pub matrix_proof: Option, + /// An encrypted voter choice payload. + pub voter_choices: Option, +} + +impl Decode<'_, ()> for ContentBallot { + fn decode( + d: &mut Decoder<'_>, + ctx: &mut (), + ) -> Result { + use minicbor::data::Type; + + let len = decode_map_len(d, "content ballot")?; + + let mut choices = BTreeMap::new(); + let mut column_proof = None; + let mut 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" => column_proof = Some(ColumnProof::decode(d, ctx)?), + "matrix-proof" => matrix_proof = Some(MatrixProof::decode(d, ctx)?), + "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( + &self, + e: &mut Encoder, + _ctx: &mut (), + ) -> Result<(), minicbor::encode::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 super::*; + use crate::{ + EncryptedBlock, RowProof, + elgamal_ristretto255_choice::ElgamalRistretto255Choice, + 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::ElgamalRistretto255 { + choices: vec![ElgamalRistretto255Choice { + c1: bytes, + c2: bytes, + }], + row_proof: None, + }), + (3, Choices::ElgamalRistretto255 { + choices: vec![ElgamalRistretto255Choice { + c1: bytes, + c2: bytes, + }], + row_proof: Some(RowProof { + selections: vec![SingleSelectionProof { + announcement: ProofAnnouncement( + ProofAnnouncementElement(bytes), + ProofAnnouncementElement(bytes), + ProofAnnouncementElement(bytes), + ), + choice: ElgamalRistretto255Choice { + c1: bytes, + c2: bytes, + }, + response: ProofResponse( + ProofScalar(bytes), + ProofScalar(bytes), + ProofScalar(bytes), + ), + }], + scalar: ProofScalar(bytes), + }), + }), + ] + .into(), + column_proof: Some(ColumnProof(1)), + matrix_proof: Some(MatrixProof(2)), + 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); + } +} diff --git a/rust/catalyst-contest/src/elgamal_ristretto255_choice.rs b/rust/catalyst-contest/src/elgamal_ristretto255_choice.rs new file mode 100644 index 0000000000..4b31d82c84 --- /dev/null +++ b/rust/catalyst-contest/src/elgamal_ristretto255_choice.rs @@ -0,0 +1,76 @@ +//! An elgamal encrypted ciphertext. + +use cbork_utils::decode_helper::decode_array_len; +use minicbor::{Decode, Decoder, Encode, Encoder, encode::Write}; + +/// An elgamal encrypted ciphertext `(c1, c2)`. +/// +/// The CDDL schema: +/// ```cddl +/// elgamal-ristretto255-encrypted-choice = [ +/// c1: elgamal-ristretto255-group-element +/// c2: elgamal-ristretto255-group-element +/// ] +/// +/// elgamal-ristretto255-group-element = bytes .size 32 +/// ``` +#[derive(Clone, Debug, PartialEq, Eq, Hash)] +pub struct ElgamalRistretto255Choice { + /// An individual Elgamal group element that composes the elgamal cipher text. + pub c1: [u8; 32], + /// An individual Elgamal group element that composes the elgamal cipher text. + pub c2: [u8; 32], +} + +impl Decode<'_, ()> for ElgamalRistretto255Choice { + fn decode( + d: &mut Decoder<'_>, + ctx: &mut (), + ) -> Result { + let len = decode_array_len(d, "elgamal ristretto255 choice")?; + if len != 2 { + return Err(minicbor::decode::Error::message(format!( + "Unexpected elgamal ristretto255 choice array length: {len}, expected 2" + ))); + } + let c1 = <[u8; 32]>::decode(d, ctx)?; + let c2 = <[u8; 32]>::decode(d, ctx)?; + Ok(ElgamalRistretto255Choice { c1, c2 }) + } +} + +impl Encode<()> for ElgamalRistretto255Choice { + fn encode( + &self, + e: &mut Encoder, + ctx: &mut (), + ) -> Result<(), minicbor::encode::Error> { + e.array(2)?; + self.c1.encode(e, ctx)?; + self.c2.encode(e, ctx) + } +} + +#[cfg(test)] +mod tests { + use super::*; + + #[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 = ElgamalRistretto255Choice { + c1: bytes, + c2: bytes, + }; + let mut buffer = Vec::new(); + original + .encode(&mut Encoder::new(&mut buffer), &mut ()) + .unwrap(); + let decoded = + ElgamalRistretto255Choice::decode(&mut Decoder::new(&buffer), &mut ()).unwrap(); + assert_eq!(original, decoded); + } +} diff --git a/rust/catalyst-contest/src/encrypted_choices.rs b/rust/catalyst-contest/src/encrypted_choices.rs new file mode 100644 index 0000000000..8d0245cbd0 --- /dev/null +++ b/rust/catalyst-contest/src/encrypted_choices.rs @@ -0,0 +1,117 @@ +//! Encrypted voter choices. + +use cbork_utils::decode_helper::decode_array_len; +use minicbor::{Decode, Decoder, Encode, Encoder, encode::Write}; + +/// A length of the encrypted block array. +const ENCRYPTED_BLOCK_ARRAY_LEN: u64 = 16; + +/// Encrypted voter choices. +/// +/// The CDDL schema: +/// ```cddl +/// voter-choice = [ 0, aes-ctr-encrypted-choices ] +/// +/// aes-ctr-encrypted-choices = +aes-ctr-encrypted-block +/// ``` +#[derive(Clone, Debug, PartialEq, Eq, Hash)] +pub struct EncryptedChoices(pub Vec); + +/// An AES-CTR encrypted data block. +/// +/// The CDDL schema: +/// ```cddl +/// aes-ctr-encrypted-block = bytes .size 16 +/// ``` +#[derive(Clone, Debug, PartialEq, Eq, Hash)] +pub struct EncryptedBlock(pub [u8; ENCRYPTED_BLOCK_ARRAY_LEN as usize]); + +impl Decode<'_, ()> for EncryptedChoices { + fn decode( + d: &mut Decoder<'_>, + ctx: &mut (), + ) -> Result { + let len = decode_array_len(d, "encrypted choices")?; + if len < 2 { + return Err(minicbor::decode::Error::message(format!( + "Unexpected encrypted choices array length: {len}, expected at least 2" + ))); + } + let version = u64::decode(d, ctx)?; + if version != 0 { + return Err(minicbor::decode::Error::message(format!( + "Unexpected encrypted choices version value: {version}, expected 0" + ))); + } + + let mut blocks = Vec::with_capacity(len as usize - 1); + for _ in 1..len { + blocks.push(EncryptedBlock::decode(d, ctx)?); + } + + Ok(Self(blocks)) + } +} + +impl Encode<()> for EncryptedChoices { + fn encode( + &self, + e: &mut Encoder, + ctx: &mut (), + ) -> Result<(), minicbor::encode::Error> { + e.array(self.0.len() as u64 + 1)?; + 0.encode(e, ctx)?; + for block in &self.0 { + block.encode(e, ctx)?; + } + Ok(()) + } +} + +impl Decode<'_, ()> for EncryptedBlock { + fn decode( + d: &mut Decoder<'_>, + ctx: &mut (), + ) -> Result { + <[u8; ENCRYPTED_BLOCK_ARRAY_LEN as usize]>::decode(d, ctx).map(Self) + } +} + +impl Encode<()> for EncryptedBlock { + fn encode( + &self, + e: &mut Encoder, + ctx: &mut (), + ) -> Result<(), minicbor::encode::Error> { + self.0.encode(e, ctx) + } +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn encrypted_block_roundtrip() { + let original = EncryptedBlock([2, 4, 6, 8, 10, 12, 14, 16, 18, 20, 22, 24, 26, 28, 30, 32]); + let mut buffer = Vec::new(); + original + .encode(&mut Encoder::new(&mut buffer), &mut ()) + .unwrap(); + let decoded = EncryptedBlock::decode(&mut Decoder::new(&buffer), &mut ()).unwrap(); + assert_eq!(original, decoded); + } + + #[test] + fn encrypted_choices_roundtrip() { + let original = EncryptedChoices(vec![EncryptedBlock([ + 2, 4, 6, 8, 10, 12, 14, 16, 18, 20, 22, 24, 26, 28, 30, 32, + ])]); + let mut buffer = Vec::new(); + original + .encode(&mut Encoder::new(&mut buffer), &mut ()) + .unwrap(); + let decoded = EncryptedChoices::decode(&mut Decoder::new(&buffer), &mut ()).unwrap(); + assert_eq!(original, decoded); + } +} diff --git a/rust/catalyst-contest/src/lib.rs b/rust/catalyst-contest/src/lib.rs index 096d5740eb..12c18d75ca 100644 --- a/rust/catalyst-contest/src/lib.rs +++ b/rust/catalyst-contest/src/lib.rs @@ -3,3 +3,24 @@ //! See the [documentation] for more information. //! //! [documentation]: https://docs.dev.projectcatalyst.io/libs/main/architecture/08_concepts/signed_doc/docs/contest_ballot/ + +// TODO: FIXME: +//#![allow(unused_variables)] + +mod choices; +mod column_proof; +mod contest_ballot; +mod elgamal_ristretto255_choice; +mod encrypted_choices; +mod matrix_proof; +mod row_proof; + +pub use crate::{ + choices::Choices, + column_proof::ColumnProof, + contest_ballot::ContentBallot, + elgamal_ristretto255_choice::ElgamalRistretto255Choice, + encrypted_choices::{EncryptedBlock, EncryptedChoices}, + matrix_proof::MatrixProof, + row_proof::RowProof, +}; diff --git a/rust/catalyst-contest/src/matrix_proof.rs b/rust/catalyst-contest/src/matrix_proof.rs new file mode 100644 index 0000000000..d55b679577 --- /dev/null +++ b/rust/catalyst-contest/src/matrix_proof.rs @@ -0,0 +1,62 @@ +//! A universal encrypted matrix proof. + +use cbork_utils::decode_helper::decode_array_len; +use minicbor::{Decode, Decoder, Encode, Encoder, encode::Write}; + +/// A length of the underlying CBOR array. +const ARRAY_LEN: u64 = 2; + +/// A universal encrypted matrix proof. +/// +/// The CDDL schema: +/// ```cddl +/// matrix-proof = [ uint, undefined ] +/// ``` +#[derive(Clone, Debug, PartialEq, Eq, Hash)] +pub struct MatrixProof(pub u64); + +impl Decode<'_, ()> for MatrixProof { + fn decode( + d: &mut Decoder<'_>, + ctx: &mut (), + ) -> Result { + let len = decode_array_len(d, "matrix proof")?; + if len != ARRAY_LEN { + return Err(minicbor::decode::Error::message(format!( + "Unexpected matrix proof array length {len}, expected {ARRAY_LEN}" + ))); + } + let val = u64::decode(d, ctx)?; + d.undefined()?; + Ok(Self(val)) + } +} + +impl Encode<()> for MatrixProof { + fn encode( + &self, + e: &mut Encoder, + ctx: &mut (), + ) -> Result<(), minicbor::encode::Error> { + e.array(ARRAY_LEN)?; + self.0.encode(e, ctx)?; + e.undefined()?; + Ok(()) + } +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn roundtrip() { + let original = MatrixProof(1); + let mut buffer = Vec::new(); + original + .encode(&mut Encoder::new(&mut buffer), &mut ()) + .unwrap(); + let decoded = MatrixProof::decode(&mut Decoder::new(&buffer), &mut ()).unwrap(); + assert_eq!(original, decoded); + } +} diff --git a/rust/catalyst-contest/src/row_proof.rs b/rust/catalyst-contest/src/row_proof.rs new file mode 100644 index 0000000000..153f219a88 --- /dev/null +++ b/rust/catalyst-contest/src/row_proof.rs @@ -0,0 +1,342 @@ +//! A universal encrypted row proof. + +use cbork_utils::decode_helper::decode_array_len; +use minicbor::{Decode, Decoder, Encode, Encoder, encode::Write}; + +use crate::ElgamalRistretto255Choice; + +/// A length of the underlying CBOR array of the `ProofScalar` type. +const SCALAR_PROOF_LEN: u64 = 32; + +/// A length of the underlying CBOR array of the `ProofAnnouncementElement` type. +const PROOF_ANNOUNCEMENT_ELEMENT_LEN: u64 = 32; + +/// A minimal length (number of elements) of the +/// `zkproof-elgamal-ristretto255-unit-vector-with-single-selection` array. +/// +/// +/// The number of elements consists of the following: +/// - 7 (zkproof-elgamal-ristretto255-unit-vector-with-single-selection-item) +/// - 3 (zkproof-elgamal-announcement = x3 zkproof-elgamal-group-element) +/// - 1 (elgamal-ristretto255-encrypted-choice) +/// - 3 (zkproof-ed25519-r-response = x3 zkproof-ed25519-scalar) +/// - 1 (zkproof-ed25519-scalar) +const MIN_SELECTION_LEN: u64 = 8; + +/// A universal encrypted row proof. +/// +/// The CDDL schema: +/// ```cddl +/// row-proof = [0, zkproof-elgamal-ristretto255-unit-vector-with-single-selection ] +/// +/// zkproof-elgamal-ristretto255-unit-vector-with-single-selection = [ +zkproof-elgamal-ristretto255-unit-vector-with-single-selection-item, zkproof-ed25519-scalar ] +/// ``` +#[derive(Clone, Debug, PartialEq, Eq, Hash)] +pub struct RowProof { + /// A list of a single selection proofs. + pub selections: Vec, + /// An individual Ed25519 scalar used in ZK proofs. + pub scalar: ProofScalar, +} + +/// A proof that the row is a unit vector with a single selection. +/// +/// The CDDL schema: +/// ```cddl +/// zkproof-elgamal-ristretto255-unit-vector-with-single-selection-item = ( zkproof-elgamal-announcement, ~elgamal-ristretto255-encrypted-choice, zkproof-ed25519-r-response ) +/// ``` +#[derive(Clone, Debug, PartialEq, Eq, Hash)] +pub struct SingleSelectionProof { + /// A ZK proof announcement values for Elgamal. + pub announcement: ProofAnnouncement, + /// An elgamal encrypted ciphertext. + pub choice: ElgamalRistretto255Choice, + /// A ZK proof response values for Ed25519. + pub response: ProofResponse, +} + +/// An individual Ed25519 scalar used in ZK proofs. +/// +/// The CDDL schema: +/// ```cddl +/// zkproof-ed25519-scalar = bytes .size 32 +/// ``` +#[derive(Clone, Debug, PartialEq, Eq, Hash)] +pub struct ProofScalar(pub [u8; SCALAR_PROOF_LEN as usize]); + +/// A ZK proof announcement values for Elgamal. +/// +/// The CDDL schema: +/// ```cddl +/// zkproof-elgamal-announcement = ( zkproof-elgamal-group-element, zkproof-elgamal-group-element, zkproof-elgamal-group-element ) +/// +/// zkproof-elgamal-group-element = bytes .size 32 +/// ``` +#[derive(Clone, Debug, PartialEq, Eq, Hash)] +pub struct ProofAnnouncement( + pub ProofAnnouncementElement, + pub ProofAnnouncementElement, + pub ProofAnnouncementElement, +); + +/// An individual Elgamal group element used in ZK proofs. +/// +/// The CDDL schema: +/// ```cddl +/// zkproof-elgamal-group-element = bytes .size 32 +/// ``` +#[derive(Clone, Debug, PartialEq, Eq, Hash)] +pub struct ProofAnnouncementElement(pub [u8; PROOF_ANNOUNCEMENT_ELEMENT_LEN as usize]); + +/// A ZK proof response values for Ed25519. +/// +/// The CDDL schema: +/// +/// ```cddl +/// zkproof-ed25519-r-response = ( zkproof-ed25519-scalar, zkproof-ed25519-scalar, zkproof-ed25519-scalar ) +/// ``` +#[derive(Clone, Debug, PartialEq, Eq, Hash)] +pub struct ProofResponse(pub ProofScalar, pub ProofScalar, pub ProofScalar); + +impl Decode<'_, ()> for RowProof { + fn decode( + d: &mut Decoder<'_>, + ctx: &mut (), + ) -> Result { + let len = decode_array_len(d, "row proof")?; + if len != 2 { + return Err(minicbor::decode::Error::message(format!( + "Unexpected row proof array length {len}, expected 2" + ))); + } + let version = u64::decode(d, ctx)?; + if version != 0 { + return Err(minicbor::decode::Error::message(format!( + "Unexpected row proof version value: {version}, expected 0" + ))); + } + + let len = decode_array_len(d, "row proof single selection")?; + if len < MIN_SELECTION_LEN || !len.is_multiple_of(MIN_SELECTION_LEN) { + return Err(minicbor::decode::Error::message(format!( + "Unexpected row proof single selection array length {len}, expected multiplier of {MIN_SELECTION_LEN}" + ))); + } + + let mut selections = Vec::with_capacity(len as usize - 1); + for _ in 0..len / MIN_SELECTION_LEN { + selections.push(SingleSelectionProof::decode(d, ctx)?); + } + let scalar = ProofScalar::decode(d, ctx)?; + + Ok(Self { selections, scalar }) + } +} + +impl Encode<()> for RowProof { + fn encode( + &self, + e: &mut Encoder, + ctx: &mut (), + ) -> Result<(), minicbor::encode::Error> { + e.array(2)?; + 0.encode(e, ctx)?; + + e.array(MIN_SELECTION_LEN * self.selections.len() as u64 + 1)?; + for selection in &self.selections { + selection.encode(e, ctx)?; + } + self.scalar.encode(e, ctx) + } +} + +impl Decode<'_, ()> for SingleSelectionProof { + fn decode( + d: &mut Decoder<'_>, + ctx: &mut (), + ) -> Result { + let announcement = ProofAnnouncement::decode(d, ctx)?; + let choice = ElgamalRistretto255Choice::decode(d, ctx)?; + let response = ProofResponse::decode(d, ctx)?; + + Ok(Self { + announcement, + choice, + response, + }) + } +} + +impl Encode<()> for SingleSelectionProof { + fn encode( + &self, + e: &mut Encoder, + ctx: &mut (), + ) -> Result<(), minicbor::encode::Error> { + self.announcement.encode(e, ctx)?; + self.choice.encode(e, ctx)?; + self.response.encode(e, ctx) + } +} + +impl Decode<'_, ()> for ProofScalar { + fn decode( + d: &mut Decoder<'_>, + ctx: &mut (), + ) -> Result { + <[u8; SCALAR_PROOF_LEN as usize]>::decode(d, ctx).map(Self) + } +} + +impl Encode<()> for ProofScalar { + fn encode( + &self, + e: &mut Encoder, + ctx: &mut (), + ) -> Result<(), minicbor::encode::Error> { + self.0.encode(e, ctx) + } +} + +impl Decode<'_, ()> for ProofAnnouncement { + fn decode( + d: &mut Decoder<'_>, + ctx: &mut (), + ) -> Result { + Ok(Self( + ProofAnnouncementElement::decode(d, ctx)?, + ProofAnnouncementElement::decode(d, ctx)?, + ProofAnnouncementElement::decode(d, ctx)?, + )) + } +} + +impl Encode<()> for ProofAnnouncement { + fn encode( + &self, + e: &mut Encoder, + ctx: &mut (), + ) -> Result<(), minicbor::encode::Error> { + self.0.encode(e, ctx)?; + self.1.encode(e, ctx)?; + self.2.encode(e, ctx) + } +} + +impl Decode<'_, ()> for ProofResponse { + fn decode( + d: &mut Decoder<'_>, + ctx: &mut (), + ) -> Result { + Ok(Self( + ProofScalar::decode(d, ctx)?, + ProofScalar::decode(d, ctx)?, + ProofScalar::decode(d, ctx)?, + )) + } +} + +impl Encode<()> for ProofResponse { + fn encode( + &self, + e: &mut Encoder, + ctx: &mut (), + ) -> Result<(), minicbor::encode::Error> { + self.0.encode(e, ctx)?; + self.1.encode(e, ctx)?; + self.2.encode(e, ctx) + } +} + +impl Decode<'_, ()> for ProofAnnouncementElement { + fn decode( + d: &mut Decoder<'_>, + ctx: &mut (), + ) -> Result { + <[u8; PROOF_ANNOUNCEMENT_ELEMENT_LEN as usize]>::decode(d, ctx).map(Self) + } +} + +impl Encode<()> for ProofAnnouncementElement { + fn encode( + &self, + e: &mut Encoder, + ctx: &mut (), + ) -> Result<(), minicbor::encode::Error> { + self.0.encode(e, ctx) + } +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn row_proof_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 = RowProof { + selections: vec![SingleSelectionProof { + announcement: ProofAnnouncement( + ProofAnnouncementElement(bytes), + ProofAnnouncementElement(bytes), + ProofAnnouncementElement(bytes), + ), + choice: ElgamalRistretto255Choice { + c1: bytes, + c2: bytes, + }, + 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 = RowProof::decode(&mut Decoder::new(&buffer), &mut ()).unwrap(); + assert_eq!(original, decoded); + } + + #[test] + fn single_selection_proof_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 = SingleSelectionProof { + announcement: ProofAnnouncement( + ProofAnnouncementElement(bytes), + ProofAnnouncementElement(bytes), + ProofAnnouncementElement(bytes), + ), + choice: ElgamalRistretto255Choice { + c1: bytes, + c2: bytes, + }, + response: ProofResponse(ProofScalar(bytes), ProofScalar(bytes), ProofScalar(bytes)), + }; + let mut buffer = Vec::new(); + original + .encode(&mut Encoder::new(&mut buffer), &mut ()) + .unwrap(); + let decoded = SingleSelectionProof::decode(&mut Decoder::new(&buffer), &mut ()).unwrap(); + assert_eq!(original, decoded); + } + + #[test] + fn proof_scalar_roundtrip() { + let original = ProofScalar([ + 0, 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, + ]); + let mut buffer = Vec::new(); + original + .encode(&mut Encoder::new(&mut buffer), &mut ()) + .unwrap(); + let decoded = ProofScalar::decode(&mut Decoder::new(&buffer), &mut ()).unwrap(); + assert_eq!(original, decoded); + } +}