diff --git a/docs/src/architecture/08_concepts/catalyst_voting/cddl/gen_vote_tx.cddl b/docs/src/architecture/08_concepts/catalyst_voting/cddl/gen_vote_tx.cddl index 693e151dabb..c343b5658ba 100644 --- a/docs/src/architecture/08_concepts/catalyst_voting/cddl/gen_vote_tx.cddl +++ b/docs/src/architecture/08_concepts/catalyst_voting/cddl/gen_vote_tx.cddl @@ -1,18 +1,22 @@ -gen-vote-tx = [ - tx-body, +; All encoders/decoders of this specification must follow deterministic cbor encoding rules +; https://datatracker.ietf.org/doc/html/draft-ietf-cbor-cde-06 + +gen-vote-tx = [ + tx-body, signature ] -tx-body = [ - vote-type +tx-body = [ + vote-type, event, votes, - voter-data, + voter-data, ] vote-type = UUID ; e.g. Public or Private vote -event = { * event-key => any } +event = { * event-key => event-value } event-key = int / text +event-value = any votes = [+ vote] vote = [ @@ -25,7 +29,7 @@ choice = #6.24(bytes .cbor choice-t) ; encoded-cbor proof = #6.24(bytes .cbor proof-t) ; encoded-cbor prop-id = #6.24(bytes .cbor prop-id-t) ; encoded-cbor -voter-data = encoded-cbor +voter-data = #6.24(bytes .cbor voter-data-t) ; encoded-cbor UUID = #6.37(bytes) ; UUID type signature = #6.98(cose.COSE_Sign) ; COSE signature diff --git a/docs/src/architecture/08_concepts/catalyst_voting/cddl/gen_vote_tx_cose_payload.cddl b/docs/src/architecture/08_concepts/catalyst_voting/cddl/gen_vote_tx_cose_payload.cddl index f432c574042..e0a996f19c2 100644 --- a/docs/src/architecture/08_concepts/catalyst_voting/cddl/gen_vote_tx_cose_payload.cddl +++ b/docs/src/architecture/08_concepts/catalyst_voting/cddl/gen_vote_tx_cose_payload.cddl @@ -1,2 +1,5 @@ +; All encoders/decoders of this specification must follow deterministic cbor encoding rules +; https://datatracker.ietf.org/doc/html/draft-ietf-cbor-cde-06 + cose-payload = blake2b-256 blake2b-256 = #6.32782(bytes .size 32) ; Blake2b-256 hash bytes diff --git a/docs/src/architecture/08_concepts/catalyst_voting/cddl/vote_tx_v2_private.cddl b/docs/src/architecture/08_concepts/catalyst_voting/cddl/vote_tx_v2_private.cddl index 23c4d5ea9f7..8499c7ddf0f 100644 --- a/docs/src/architecture/08_concepts/catalyst_voting/cddl/vote_tx_v2_private.cddl +++ b/docs/src/architecture/08_concepts/catalyst_voting/cddl/vote_tx_v2_private.cddl @@ -1,4 +1,7 @@ -vote-tx-v2 = gen-vote-tx +; All encoders/decoders of this specification must follow deterministic cbor encoding rules +; https://datatracker.ietf.org/doc/html/draft-ietf-cbor-cde-06 + +vote-tx-v2 = gen-vote-tx choice-data = ciphertext ciphertext = [group-element, group-element] diff --git a/docs/src/architecture/08_concepts/catalyst_voting/cddl/vote_tx_v2_public.cddl b/docs/src/architecture/08_concepts/catalyst_voting/cddl/vote_tx_v2_public.cddl index de273ef4f0b..8c5ef356160 100644 --- a/docs/src/architecture/08_concepts/catalyst_voting/cddl/vote_tx_v2_public.cddl +++ b/docs/src/architecture/08_concepts/catalyst_voting/cddl/vote_tx_v2_public.cddl @@ -1,4 +1,7 @@ -vote-tx-v2-public = gen-vote-tx +; All encoders/decoders of this specification must follow deterministic cbor encoding rules +; https://datatracker.ietf.org/doc/html/draft-ietf-cbor-cde-06 + +vote-tx-v2-public = gen-vote-tx choice-data = uint proof-data = undefined diff --git a/docs/src/architecture/08_concepts/immutable_ledger/cddl/block.cddl b/docs/src/architecture/08_concepts/immutable_ledger/cddl/block.cddl index d65f0fc78a8..1c459ba02e2 100644 --- a/docs/src/architecture/08_concepts/immutable_ledger/cddl/block.cddl +++ b/docs/src/architecture/08_concepts/immutable_ledger/cddl/block.cddl @@ -1,3 +1,6 @@ +; All encoders/decoders of this specification must follow deterministic cbor encoding rules +; https://datatracker.ietf.org/doc/html/draft-ietf-cbor-cde-06 + block = [ block-header, block-data, diff --git a/docs/src/architecture/08_concepts/immutable_ledger/cddl/genesis_to_prev_hash.cddl b/docs/src/architecture/08_concepts/immutable_ledger/cddl/genesis_to_prev_hash.cddl index c30ecbf9950..7d0d525063b 100644 --- a/docs/src/architecture/08_concepts/immutable_ledger/cddl/genesis_to_prev_hash.cddl +++ b/docs/src/architecture/08_concepts/immutable_ledger/cddl/genesis_to_prev_hash.cddl @@ -1,3 +1,6 @@ +; All encoders/decoders of this specification must follow deterministic cbor encoding rules +; https://datatracker.ietf.org/doc/html/draft-ietf-cbor-cde-06 + genesis-to-prev-hash = [ chain-id: ULID, timestamp: #6.1(uint .ge 1722470400), ; Epoch-based date/time diff --git a/docs/src/architecture/08_concepts/immutable_ledger/cddl/hash.cddl b/docs/src/architecture/08_concepts/immutable_ledger/cddl/hash.cddl index 32ca39ae720..18c0d6e4a16 100644 --- a/docs/src/architecture/08_concepts/immutable_ledger/cddl/hash.cddl +++ b/docs/src/architecture/08_concepts/immutable_ledger/cddl/hash.cddl @@ -1,3 +1,6 @@ +; All encoders/decoders of this specification must follow deterministic cbor encoding rules +; https://datatracker.ietf.org/doc/html/draft-ietf-cbor-cde-06 + hash-bytes = ( #6.32781(bytes) / ; Blake3 hash #6.32782(bytes) / ; Blake2b hash diff --git a/rust/vote-tx-v2/src/decoding.rs b/rust/vote-tx-v2/src/decoding.rs deleted file mode 100644 index 213323cfce8..00000000000 --- a/rust/vote-tx-v2/src/decoding.rs +++ /dev/null @@ -1,419 +0,0 @@ -//! CBOR encoding and decoding implementation. -//! - -use coset::CborSerializable; -use minicbor::{ - data::{IanaTag, Tag}, - Decode, Decoder, Encode, Encoder, -}; - -use crate::{ - Choice, EventKey, EventMap, GeneralizedTx, Proof, PropId, TxBody, Uuid, Vote, VoterData, -}; - -/// UUID CBOR tag . -const CBOR_UUID_TAG: u64 = 37; - -/// `Vote` array struct length -const VOTE_LEN: u64 = 3; - -/// `TxBody` array struct length -const TX_BODY_LEN: u64 = 4; - -/// `GeneralizedTx` array struct length -const GENERALIZED_TX_LEN: u64 = 2; - -impl Decode<'_, ()> for GeneralizedTx { - fn decode(d: &mut Decoder<'_>, (): &mut ()) -> Result { - let Some(GENERALIZED_TX_LEN) = d.array()? else { - return Err(minicbor::decode::Error::message(format!( - "must be a defined sized array with {GENERALIZED_TX_LEN} entries" - ))); - }; - - let tx_body = TxBody::decode(d, &mut ())?; - - let signature = { - let sign_bytes = read_cbor_bytes(d) - .map_err(|_| minicbor::decode::Error::message("missing `signature` field"))?; - let mut sign = coset::CoseSign::from_slice(&sign_bytes).map_err(|_| { - minicbor::decode::Error::message("`signature` must be COSE_Sign encoded object") - })?; - // We don't need to hold the original encoded data of the COSE protected header - sign.protected.original_data = None; - sign - }; - - Ok(Self { tx_body, signature }) - } -} - -impl Encode<()> for GeneralizedTx { - fn encode( - &self, e: &mut Encoder, (): &mut (), - ) -> Result<(), minicbor::encode::Error> { - e.array(GENERALIZED_TX_LEN)?; - self.tx_body.encode(e, &mut ())?; - - let sign_bytes = self - .signature - .clone() - .to_vec() - .map_err(minicbor::encode::Error::message)?; - e.writer_mut() - .write_all(&sign_bytes) - .map_err(minicbor::encode::Error::write)?; - - Ok(()) - } -} - -impl Decode<'_, ()> for TxBody { - fn decode(d: &mut Decoder<'_>, (): &mut ()) -> Result { - let Some(TX_BODY_LEN) = d.array()? else { - return Err(minicbor::decode::Error::message(format!( - "must be a defined sized array with {GENERALIZED_TX_LEN} entries" - ))); - }; - - let vote_type = Uuid::decode(d, &mut ())?; - let event = EventMap::decode(d, &mut ())?; - let votes = Vec::::decode(d, &mut ())?; - let voter_data = VoterData::decode(d, &mut ())?; - Ok(Self { - vote_type, - event, - votes, - voter_data, - }) - } -} - -impl Encode<()> for TxBody { - fn encode( - &self, e: &mut Encoder, (): &mut (), - ) -> Result<(), minicbor::encode::Error> { - e.array(TX_BODY_LEN)?; - self.vote_type.encode(e, &mut ())?; - self.event.encode(e, &mut ())?; - self.votes.encode(e, &mut ())?; - self.voter_data.encode(e, &mut ())?; - Ok(()) - } -} - -impl Decode<'_, ()> for EventMap { - fn decode(d: &mut Decoder<'_>, (): &mut ()) -> Result { - let Some(len) = d.map()? else { - return Err(minicbor::decode::Error::message( - "must be a defined sized map", - )); - }; - - let map = (0..len) - .map(|_| { - let key = EventKey::decode(d, &mut ())?; - - let value = read_cbor_bytes(d).map_err(|_| { - minicbor::decode::Error::message("missing event map `value` field") - })?; - Ok((key, value)) - }) - .collect::>()?; - - Ok(EventMap(map)) - } -} - -impl Encode<()> for EventMap { - fn encode( - &self, e: &mut Encoder, (): &mut (), - ) -> Result<(), minicbor::encode::Error> { - e.map(self.0.len() as u64)?; - - for (key, value) in &self.0 { - key.encode(e, &mut ())?; - - e.writer_mut() - .write_all(value) - .map_err(minicbor::encode::Error::write)?; - } - - Ok(()) - } -} - -impl Decode<'_, ()> for EventKey { - fn decode(d: &mut Decoder<'_>, (): &mut ()) -> Result { - let pos = d.position(); - // try to decode as int - if let Ok(i) = d.int() { - Ok(EventKey::Int(i)) - } else { - // try to decode as text - d.set_position(pos); - let str = d.str()?; - Ok(EventKey::Text(str.to_string())) - } - } -} - -impl Encode<()> for EventKey { - fn encode( - &self, e: &mut Encoder, (): &mut (), - ) -> Result<(), minicbor::encode::Error> { - match self { - EventKey::Int(i) => e.int(*i)?, - EventKey::Text(s) => e.str(s)?, - }; - Ok(()) - } -} - -impl Decode<'_, ()> for VoterData { - fn decode(d: &mut Decoder<'_>, (): &mut ()) -> Result { - let tag = d.tag()?; - let expected_tag = minicbor::data::IanaTag::Cbor.tag(); - if expected_tag != tag { - return Err(minicbor::decode::Error::message(format!( - "tag value must be: {}, provided: {}", - expected_tag.as_u64(), - tag.as_u64(), - ))); - } - let choice = d.bytes()?.to_vec(); - Ok(Self(choice)) - } -} - -impl Encode<()> for VoterData { - fn encode( - &self, e: &mut minicbor::Encoder, (): &mut (), - ) -> Result<(), minicbor::encode::Error> { - e.tag(IanaTag::Cbor.tag())?; - e.bytes(&self.0)?; - Ok(()) - } -} - -impl Decode<'_, ()> for Uuid { - fn decode(d: &mut Decoder<'_>, (): &mut ()) -> Result { - let tag = d.tag()?; - if CBOR_UUID_TAG != tag.as_u64() { - return Err(minicbor::decode::Error::message(format!( - "tag value must be: {CBOR_UUID_TAG}, provided: {}", - tag.as_u64(), - ))); - } - let choice = d.bytes()?.to_vec(); - Ok(Self(choice)) - } -} - -impl Encode<()> for Uuid { - fn encode( - &self, e: &mut minicbor::Encoder, (): &mut (), - ) -> Result<(), minicbor::encode::Error> { - e.tag(Tag::new(CBOR_UUID_TAG))?; - e.bytes(&self.0)?; - Ok(()) - } -} - -impl Decode<'_, ()> for Vote { - fn decode(d: &mut Decoder<'_>, (): &mut ()) -> Result { - let Some(VOTE_LEN) = d.array()? else { - return Err(minicbor::decode::Error::message(format!( - "must be a defined sized array with {VOTE_LEN} entries" - ))); - }; - - let choices = Vec::::decode(d, &mut ())?; - if choices.is_empty() { - return Err(minicbor::decode::Error::message( - "choices array must has at least one entry", - )); - } - let proof = Proof::decode(d, &mut ())?; - let prop_id = PropId::decode(d, &mut ())?; - Ok(Self { - choices, - proof, - prop_id, - }) - } -} - -impl Encode<()> for Vote { - fn encode( - &self, e: &mut minicbor::Encoder, (): &mut (), - ) -> Result<(), minicbor::encode::Error> { - e.array(VOTE_LEN)?; - self.choices.encode(e, &mut ())?; - self.proof.encode(e, &mut ())?; - self.prop_id.encode(e, &mut ())?; - Ok(()) - } -} - -impl Decode<'_, ()> for Choice { - fn decode(d: &mut Decoder<'_>, (): &mut ()) -> Result { - let tag = d.tag()?; - let expected_tag = minicbor::data::IanaTag::Cbor.tag(); - if expected_tag != tag { - return Err(minicbor::decode::Error::message(format!( - "tag value must be: {}, provided: {}", - expected_tag.as_u64(), - tag.as_u64(), - ))); - } - let choice = d.bytes()?.to_vec(); - Ok(Self(choice)) - } -} - -impl Encode<()> for Choice { - fn encode( - &self, e: &mut minicbor::Encoder, (): &mut (), - ) -> Result<(), minicbor::encode::Error> { - e.tag(IanaTag::Cbor.tag())?; - e.bytes(&self.0)?; - Ok(()) - } -} - -impl Decode<'_, ()> for Proof { - fn decode(d: &mut Decoder<'_>, (): &mut ()) -> Result { - let tag = d.tag()?; - let expected_tag = minicbor::data::IanaTag::Cbor.tag(); - if expected_tag != tag { - return Err(minicbor::decode::Error::message(format!( - "tag value must be: {}, provided: {}", - expected_tag.as_u64(), - tag.as_u64(), - ))); - } - let choice = d.bytes()?.to_vec(); - Ok(Self(choice)) - } -} - -impl Encode<()> for Proof { - fn encode( - &self, e: &mut minicbor::Encoder, (): &mut (), - ) -> Result<(), minicbor::encode::Error> { - e.tag(IanaTag::Cbor.tag())?; - e.bytes(&self.0)?; - Ok(()) - } -} - -impl Decode<'_, ()> for PropId { - fn decode(d: &mut Decoder<'_>, (): &mut ()) -> Result { - let tag = d.tag()?; - let expected_tag = IanaTag::Cbor.tag(); - if expected_tag != tag { - return Err(minicbor::decode::Error::message(format!( - "tag value must be: {}, provided: {}", - expected_tag.as_u64(), - tag.as_u64(), - ))); - } - let choice = d.bytes()?.to_vec(); - Ok(Self(choice)) - } -} - -impl Encode<()> for PropId { - fn encode( - &self, e: &mut minicbor::Encoder, (): &mut (), - ) -> Result<(), minicbor::encode::Error> { - e.tag(IanaTag::Cbor.tag())?; - e.bytes(&self.0)?; - Ok(()) - } -} - -/// Reads CBOR bytes from the decoder and returns them as bytes. -fn read_cbor_bytes(d: &mut Decoder<'_>) -> Result, minicbor::decode::Error> { - let start = d.position(); - d.skip()?; - let end = d.position(); - let bytes = d - .input() - .get(start..end) - .ok_or(minicbor::decode::Error::end_of_input())? - .to_vec(); - Ok(bytes) -} - -#[cfg(test)] -mod tests { - use proptest::{prelude::any_with, sample::size_range}; - use proptest_derive::Arbitrary; - use test_strategy::proptest; - - use super::*; - use crate::Cbor; - - type PropChoice = Vec; - type PropVote = (Vec, Vec, Vec); - - #[derive(Debug, Arbitrary)] - enum PropEventKey { - Text(String), - U64(u64), - I64(i64), - } - - #[proptest] - fn generalized_tx_from_bytes_to_bytes_test( - vote_type: Vec, - // generates a votes in range from 1 to 10, and choices in range from 1 to 10 - #[strategy(any_with::>(( - size_range(1..10usize), - ( - (size_range(1..10usize), Default::default()), - Default::default(), - Default::default(), - ), - )))] - votes: Vec, - event: Vec<(PropEventKey, u64)>, - voter_data: Vec, - ) { - let event = event - .into_iter() - .map(|(key, val)| { - let key = match key { - PropEventKey::Text(key) => EventKey::Text(key), - PropEventKey::U64(val) => EventKey::Int(val.into()), - PropEventKey::I64(val) => EventKey::Int(val.into()), - }; - let value = val.to_bytes().unwrap(); - (key, value) - }) - .collect(); - let tx_body = TxBody { - vote_type: Uuid(vote_type), - event: EventMap(event), - votes: votes - .into_iter() - .map(|(choices, proof, prop_id)| { - Vote { - choices: choices.into_iter().map(Choice).collect(), - proof: Proof(proof), - prop_id: PropId(prop_id), - } - }) - .collect(), - voter_data: VoterData(voter_data), - }; - - let generalized_tx = GeneralizedTx::new(tx_body); - - let bytes = generalized_tx.to_bytes().unwrap(); - let decoded = GeneralizedTx::from_bytes(&bytes).unwrap(); - assert_eq!(generalized_tx, decoded); - } -} diff --git a/rust/vote-tx-v2/src/encoded_cbor.rs b/rust/vote-tx-v2/src/encoded_cbor.rs new file mode 100644 index 00000000000..981fc12737c --- /dev/null +++ b/rust/vote-tx-v2/src/encoded_cbor.rs @@ -0,0 +1,46 @@ +//! An encoded CBOR (tag 24) struct + +use minicbor::{data::Tag, Decode, Decoder, Encode}; + +use crate::Cbor; + +/// encoded-cbor CBOR tag . +const ENCODED_CBOR_TAG: u64 = 24; + +/// An encoded CBOR struct, CBOR tag 24. +#[derive(Debug, Clone, PartialEq)] +pub struct EncodedCbor(pub T) +where T: for<'a> Cbor<'a>; + +impl Decode<'_, ()> for EncodedCbor +where T: for<'a> Cbor<'a> +{ + fn decode(d: &mut Decoder<'_>, (): &mut ()) -> Result { + let tag = d.tag()?; + if ENCODED_CBOR_TAG != tag.as_u64() { + return Err(minicbor::decode::Error::message(format!( + "tag value must be: {ENCODED_CBOR_TAG}, provided: {}", + tag.as_u64(), + ))); + } + let cbor_bytes = d.bytes()?.to_vec(); + let cbor = T::from_bytes(&cbor_bytes).map_err(minicbor::decode::Error::message)?; + Ok(Self(cbor)) + } +} + +impl Encode<()> for EncodedCbor +where T: for<'a> Cbor<'a> +{ + fn encode( + &self, e: &mut minicbor::Encoder, (): &mut (), + ) -> Result<(), minicbor::encode::Error> { + e.tag(Tag::new(ENCODED_CBOR_TAG))?; + let cbor_bytes = self + .0 + .to_bytes() + .map_err(minicbor::encode::Error::message)?; + e.bytes(&cbor_bytes)?; + Ok(()) + } +} diff --git a/rust/vote-tx-v2/src/gen_tx/builder.rs b/rust/vote-tx-v2/src/gen_tx/builder.rs new file mode 100644 index 00000000000..ab6cf9e264a --- /dev/null +++ b/rust/vote-tx-v2/src/gen_tx/builder.rs @@ -0,0 +1,100 @@ +//! A Catalyst generalized vote transaction builder + +use anyhow::ensure; + +use super::{cose_protected_header, EventKey, EventMap, GeneralizedTx, TxBody, Vote, VoterData}; +use crate::{encoded_cbor::EncodedCbor, uuid::Uuid, Cbor}; + +/// `GeneralizedTx` builder struct +#[allow(clippy::module_name_repetitions)] +pub struct GeneralizedTxBuilder +where + ChoiceT: for<'a> Cbor<'a>, + ProofT: for<'a> Cbor<'a>, + PropIdT: for<'a> Cbor<'a>, + VoterDataT: for<'a> Cbor<'a>, +{ + /// The `vote_type` field + vote_type: Uuid, + /// The `event` field + event: EventMap, + /// The `votes` field + votes: Vec>, + /// The `voter_data` field + voter_data: VoterData, + /// The `signature` builder field + sign_builder: coset::CoseSignBuilder, +} + +impl + GeneralizedTxBuilder +where + ChoiceT: for<'a> Cbor<'a> + Clone, + ProofT: for<'a> Cbor<'a> + Clone, + PropIdT: for<'a> Cbor<'a> + Clone, + VoterDataT: for<'a> Cbor<'a> + Clone, +{ + /// Creates a new `GeneralizedTxBuilder` struct + #[must_use] + pub fn new(vote_type: Uuid, voter_data: VoterData) -> Self { + let event = EventMap::default(); + let votes = Vec::default(); + let sign_builder = coset::CoseSignBuilder::new().protected(cose_protected_header()); + Self { + vote_type, + event, + votes, + voter_data, + sign_builder, + } + } + + /// Adds an `EventMap` entry to the `event` field. + /// + /// # Errors + pub fn with_event(mut self, key: EventKey, value: ValueT) -> anyhow::Result + where ValueT: for<'a> Cbor<'a> + Clone { + let value = value.to_bytes()?; + self.event.0.push((key, value)); + Ok(self) + } + + /// Adds a `Vote` entry to the `votes` field. + /// + /// # Errors + /// - `choices` array must has at least one entry- + pub fn with_vote( + mut self, choices: Vec, proof: ProofT, prop_id: PropIdT, + ) -> anyhow::Result { + ensure!( + !choices.is_empty(), + "`choices` array must has at least one entry" + ); + self.votes.push(Vote { + choices: choices.into_iter().map(EncodedCbor).collect(), + proof: EncodedCbor(proof), + prop_id: EncodedCbor(prop_id), + }); + Ok(self) + } + + /// Builds a new `GeneralizedTx` object. + /// + /// # Errors + /// - `votes` array must has at least one entry + pub fn build(self) -> anyhow::Result> { + ensure!( + !self.votes.is_empty(), + "`votes` array must has at least one entry" + ); + + let tx_body = TxBody { + vote_type: self.vote_type, + event: self.event, + votes: self.votes, + voter_data: self.voter_data, + }; + let signature = self.sign_builder.build(); + Ok(GeneralizedTx { tx_body, signature }) + } +} diff --git a/rust/vote-tx-v2/src/gen_tx/event_map.rs b/rust/vote-tx-v2/src/gen_tx/event_map.rs new file mode 100644 index 00000000000..91b6947c4ae --- /dev/null +++ b/rust/vote-tx-v2/src/gen_tx/event_map.rs @@ -0,0 +1,86 @@ +//! A generalized tx event map struct. + +use minicbor::{data::Int, Decode, Decoder, Encode, Encoder}; + +use super::read_cbor_bytes; + +/// A CBOR map +#[derive(Debug, Clone, PartialEq, Default)] +pub struct EventMap(pub(super) Vec<(EventKey, Vec)>); + +/// An `event-key` type definition. +#[derive(Debug, Clone, PartialEq)] +pub enum EventKey { + /// CBOR `int` type + Int(Int), + /// CBOR `text` type + Text(String), +} + +impl Decode<'_, ()> for EventMap { + fn decode(d: &mut Decoder<'_>, (): &mut ()) -> Result { + let Some(len) = d.map()? else { + return Err(minicbor::decode::Error::message( + "must be a defined sized map", + )); + }; + + let map = (0..len) + .map(|_| { + let key = EventKey::decode(d, &mut ())?; + + let value = read_cbor_bytes(d).map_err(|_| { + minicbor::decode::Error::message("missing event map `value` field") + })?; + Ok((key, value)) + }) + .collect::>()?; + + Ok(EventMap(map)) + } +} + +impl Encode<()> for EventMap { + fn encode( + &self, e: &mut Encoder, (): &mut (), + ) -> Result<(), minicbor::encode::Error> { + e.map(self.0.len() as u64)?; + + for (key, value) in &self.0 { + key.encode(e, &mut ())?; + + e.writer_mut() + .write_all(value) + .map_err(minicbor::encode::Error::write)?; + } + + Ok(()) + } +} + +impl Decode<'_, ()> for EventKey { + fn decode(d: &mut Decoder<'_>, (): &mut ()) -> Result { + let pos = d.position(); + // try to decode as int + if let Ok(i) = d.int() { + Ok(EventKey::Int(i)) + } else { + // try to decode as text + d.set_position(pos); + let str = d.str()?; + Ok(EventKey::Text(str.to_string())) + } + } +} + +impl Encode<()> for EventKey { + fn encode( + &self, e: &mut Encoder, (): &mut (), + ) -> Result<(), minicbor::encode::Error> { + match self { + EventKey::Int(i) => e.int(*i)?, + EventKey::Text(s) => e.str(s)?, + }; + Ok(()) + } +} diff --git a/rust/vote-tx-v2/src/gen_tx/mod.rs b/rust/vote-tx-v2/src/gen_tx/mod.rs new file mode 100644 index 00000000000..e142a9b7b15 --- /dev/null +++ b/rust/vote-tx-v2/src/gen_tx/mod.rs @@ -0,0 +1,311 @@ +//! A Catalyst generalized vote transaction object, structured following this +//! [spec](https://input-output-hk.github.io/catalyst-libs/architecture/08_concepts/catalyst_voting/gen_vote_tx/) + +// cspell: words Coap + +mod builder; +mod event_map; +mod tx_body; +mod vote; + +pub use builder::GeneralizedTxBuilder; +use coset::CborSerializable; +pub use event_map::{EventKey, EventMap}; +use minicbor::{Decode, Decoder, Encode, Encoder}; +pub use tx_body::{TxBody, VoterData}; +pub use vote::{Choice, Proof, PropId, Vote}; + +use crate::Cbor; + +/// A generalized tx struct. +#[derive(Debug, Clone, PartialEq)] +pub struct GeneralizedTx +where + ChoiceT: for<'a> Cbor<'a>, + ProofT: for<'a> Cbor<'a>, + PropIdT: for<'a> Cbor<'a>, + VoterDataT: for<'a> Cbor<'a>, +{ + /// `tx-body` field + tx_body: TxBody, + /// `signature` field + signature: coset::CoseSign, +} + +/// `GeneralizedTx` array struct length +const GENERALIZED_TX_LEN: u64 = 2; + +impl Decode<'_, ()> + for GeneralizedTx +where + ChoiceT: for<'a> Cbor<'a>, + ProofT: for<'a> Cbor<'a>, + PropIdT: for<'a> Cbor<'a>, + VoterDataT: for<'a> Cbor<'a>, +{ + fn decode(d: &mut Decoder<'_>, (): &mut ()) -> Result { + let Some(GENERALIZED_TX_LEN) = d.array()? else { + return Err(minicbor::decode::Error::message(format!( + "must be a defined sized array with {GENERALIZED_TX_LEN} entries" + ))); + }; + + let tx_body = TxBody::decode(d, &mut ())?; + + let signature = { + let sign_bytes = read_cbor_bytes(d) + .map_err(|_| minicbor::decode::Error::message("missing `signature` field"))?; + let mut sign = coset::CoseSign::from_slice(&sign_bytes).map_err(|_| { + minicbor::decode::Error::message("`signature` must be COSE_Sign encoded object") + })?; + // We don't need to hold the original encoded data of the COSE protected header + sign.protected.original_data = None; + + if sign.protected.header != cose_protected_header() { + return Err(minicbor::decode::Error::message( + "invalid `signature` COSE_Sign protected header", + )); + } + + sign + }; + + Ok(Self { tx_body, signature }) + } +} + +impl Encode<()> + for GeneralizedTx +where + ChoiceT: for<'a> Cbor<'a>, + ProofT: for<'a> Cbor<'a>, + PropIdT: for<'a> Cbor<'a>, + VoterDataT: for<'a> Cbor<'a>, +{ + fn encode( + &self, e: &mut Encoder, (): &mut (), + ) -> Result<(), minicbor::encode::Error> { + e.array(GENERALIZED_TX_LEN)?; + self.tx_body.encode(e, &mut ())?; + + let sign_bytes = self + .signature + .clone() + .to_vec() + .map_err(minicbor::encode::Error::message)?; + e.writer_mut() + .write_all(&sign_bytes) + .map_err(minicbor::encode::Error::write)?; + + Ok(()) + } +} + +/// Reads CBOR bytes from the decoder and returns them as bytes. +fn read_cbor_bytes(d: &mut Decoder<'_>) -> Result, minicbor::decode::Error> { + let start = d.position(); + d.skip()?; + let end = d.position(); + let bytes = d + .input() + .get(start..end) + .ok_or(minicbor::decode::Error::end_of_input())? + .to_vec(); + Ok(bytes) +} + +/// Returns the COSE protected header for `GeneralizedTx` signature. +fn cose_protected_header() -> coset::Header { + coset::HeaderBuilder::new() + .content_format(coset::iana::CoapContentFormat::Cbor) + .build() +} + +#[cfg(test)] +mod tests { + use proptest::{prelude::any_with, sample::size_range}; + use proptest_derive::Arbitrary; + use test_strategy::proptest; + + use super::*; + use crate::{encoded_cbor::EncodedCbor, uuid::Uuid}; + + type ChoiceT = Vec; + type ProofT = Vec; + type PropIdT = Vec; + type VoterDataT = Vec; + + type PropVote = (Vec, ProofT, PropIdT); + + #[derive(Debug, Arbitrary)] + enum PropEventKey { + Text(String), + U64(u64), + I64(i64), + } + + impl From for EventKey { + fn from(key: PropEventKey) -> Self { + match key { + PropEventKey::Text(text) => EventKey::Text(text), + PropEventKey::U64(val) => EventKey::Int(val.into()), + PropEventKey::I64(val) => EventKey::Int(val.into()), + } + } + } + + #[proptest] + fn generalized_tx_from_bytes_to_bytes_test( + vote_type: Vec, + // generates a votes in range from 1 to 10, and choices in range from 1 to 10 + #[strategy(any_with::>(( + size_range(1..10usize), + ( + (size_range(1..10usize), Default::default()), + Default::default(), + Default::default(), + ), + )))] + votes: Vec, + event: Vec<(PropEventKey, u64)>, + voter_data: Vec, + ) { + let event = event + .into_iter() + .map(|(key, val)| { + let key = key.into(); + let value = val.to_bytes().unwrap(); + (key, value) + }) + .collect(); + let votes = votes + .into_iter() + .map(|(choices, proof, prop_id)| { + Vote { + choices: choices.into_iter().map(EncodedCbor).collect(), + proof: EncodedCbor(proof), + prop_id: EncodedCbor(prop_id), + } + }) + .collect(); + let tx_body = TxBody { + vote_type: Uuid(vote_type), + event: EventMap(event), + votes, + voter_data: EncodedCbor(voter_data), + }; + let signature = coset::CoseSignBuilder::new() + .protected(cose_protected_header()) + .build(); + + let generalized_tx = GeneralizedTx { tx_body, signature }; + let bytes = generalized_tx.to_bytes().unwrap(); + let decoded = GeneralizedTx::from_bytes(&bytes).unwrap(); + assert_eq!(generalized_tx, decoded); + } + + #[proptest] + fn generalized_tx_with_empty_votes_from_bytes_to_bytes_test( + vote_type: Vec, event: Vec<(PropEventKey, u64)>, voter_data: Vec, + ) { + let event: Vec<_> = event + .into_iter() + .map(|(key, val)| { + let key = key.into(); + let value = val.to_bytes().unwrap(); + (key, value) + }) + .collect(); + + let empty_votes = Vec::>::new(); + let tx_body = TxBody { + vote_type: Uuid(vote_type.clone()), + event: EventMap(event.clone()), + votes: empty_votes, + voter_data: EncodedCbor(voter_data.clone()), + }; + let signature = coset::CoseSignBuilder::new() + .protected(cose_protected_header()) + .build(); + + let generalized_tx = GeneralizedTx { tx_body, signature }; + let bytes = generalized_tx.to_bytes().unwrap(); + assert!(GeneralizedTx::::from_bytes(&bytes).is_err()); + } + + #[proptest] + fn generalized_tx_with_empty_choices_from_bytes_to_bytes_test( + vote_type: Vec, votes: Vec, event: Vec<(PropEventKey, u64)>, + voter_data: Vec, + ) { + let event: Vec<_> = event + .into_iter() + .map(|(key, val)| { + let key = key.into(); + let value = val.to_bytes().unwrap(); + (key, value) + }) + .collect(); + + let votes_with_empty_choices = votes + .into_iter() + .map(|(_, proof, prop_id)| { + Vote { + choices: Vec::>::new(), + proof: EncodedCbor(proof), + prop_id: EncodedCbor(prop_id), + } + }) + .collect(); + let tx_body = TxBody { + vote_type: Uuid(vote_type), + event: EventMap(event), + votes: votes_with_empty_choices, + voter_data: EncodedCbor(voter_data), + }; + let signature = coset::CoseSignBuilder::new() + .protected(cose_protected_header()) + .build(); + + let generalized_tx = GeneralizedTx { tx_body, signature }; + let bytes = generalized_tx.to_bytes().unwrap(); + assert!(GeneralizedTx::::from_bytes(&bytes).is_err()); + } + + #[proptest] + fn generalized_tx_with_wrong_signature_from_bytes_to_bytes_test( + vote_type: Vec, votes: Vec, event: Vec<(PropEventKey, u64)>, + voter_data: Vec, + ) { + let event: Vec<_> = event + .into_iter() + .map(|(key, val)| { + let key = key.into(); + let value = val.to_bytes().unwrap(); + (key, value) + }) + .collect(); + + let votes = votes + .into_iter() + .map(|(choices, proof, prop_id)| { + Vote { + choices: choices.into_iter().map(EncodedCbor).collect(), + proof: EncodedCbor(proof), + prop_id: EncodedCbor(prop_id), + } + }) + .collect(); + let tx_body = TxBody { + vote_type: Uuid(vote_type), + event: EventMap(event), + votes, + voter_data: EncodedCbor(voter_data), + }; + let signature = coset::CoseSignBuilder::new().build(); + + let generalized_tx = GeneralizedTx { tx_body, signature }; + let bytes = generalized_tx.to_bytes().unwrap(); + assert!(GeneralizedTx::::from_bytes(&bytes).is_err()); + } +} diff --git a/rust/vote-tx-v2/src/gen_tx/tx_body.rs b/rust/vote-tx-v2/src/gen_tx/tx_body.rs new file mode 100644 index 00000000000..c129a3e15c3 --- /dev/null +++ b/rust/vote-tx-v2/src/gen_tx/tx_body.rs @@ -0,0 +1,84 @@ +//! A generalized tx body struct. + +use minicbor::{Decode, Decoder, Encode, Encoder}; + +use super::{EventMap, Vote}; +use crate::{encoded_cbor::EncodedCbor, uuid::Uuid, Cbor}; + +/// `TxBody` array struct length +const TX_BODY_LEN: u64 = 4; + +/// A voter's data type. +pub type VoterData = EncodedCbor; + +/// A tx body struct. +#[derive(Debug, Clone, PartialEq)] +pub struct TxBody +where + ChoiceT: for<'a> Cbor<'a>, + ProofT: for<'a> Cbor<'a>, + PropIdT: for<'a> Cbor<'a>, + VoterDataT: for<'a> Cbor<'a>, +{ + /// `vote-type` field + pub(super) vote_type: Uuid, + /// `event` field + pub(super) event: EventMap, + /// `votes` field + pub(super) votes: Vec>, + /// `voter-data` field + pub(super) voter_data: VoterData, +} + +impl Decode<'_, ()> + for TxBody +where + ChoiceT: for<'a> Cbor<'a>, + ProofT: for<'a> Cbor<'a>, + PropIdT: for<'a> Cbor<'a>, + VoterDataT: for<'a> Cbor<'a>, +{ + fn decode(d: &mut Decoder<'_>, (): &mut ()) -> Result { + let Some(TX_BODY_LEN) = d.array()? else { + return Err(minicbor::decode::Error::message(format!( + "must be a defined sized array with {TX_BODY_LEN} entries" + ))); + }; + + let vote_type = Uuid::decode(d, &mut ())?; + let event = EventMap::decode(d, &mut ())?; + let votes = Vec::>::decode(d, &mut ())?; + if votes.is_empty() { + return Err(minicbor::decode::Error::message( + "votes array must has at least one entry", + )); + } + let voter_data = VoterData::decode(d, &mut ())?; + Ok(Self { + vote_type, + event, + votes, + voter_data, + }) + } +} + +impl Encode<()> + for TxBody +where + ChoiceT: for<'a> Cbor<'a>, + ProofT: for<'a> Cbor<'a>, + PropIdT: for<'a> Cbor<'a>, + VoterDataT: for<'a> Cbor<'a>, +{ + fn encode( + &self, e: &mut Encoder, (): &mut (), + ) -> Result<(), minicbor::encode::Error> { + e.array(TX_BODY_LEN)?; + self.vote_type.encode(e, &mut ())?; + self.event.encode(e, &mut ())?; + self.votes.encode(e, &mut ())?; + self.voter_data.encode(e, &mut ())?; + Ok(()) + } +} diff --git a/rust/vote-tx-v2/src/gen_tx/vote.rs b/rust/vote-tx-v2/src/gen_tx/vote.rs new file mode 100644 index 00000000000..cd5c3c56eea --- /dev/null +++ b/rust/vote-tx-v2/src/gen_tx/vote.rs @@ -0,0 +1,77 @@ +//! A generalized tx vote struct. + +use minicbor::{Decode, Decoder, Encode}; + +use crate::{encoded_cbor::EncodedCbor, Cbor}; + +/// `Vote` array struct length +const VOTE_LEN: u64 = 3; + +/// A vote choice type. +pub type Choice = EncodedCbor; +/// A vote proof type. +pub type Proof = EncodedCbor; +/// A vote prop-id type. +pub type PropId = EncodedCbor; + +/// A vote struct. +#[derive(Debug, Clone, PartialEq)] +pub struct Vote +where + ChoiceT: for<'a> Cbor<'a>, + ProofT: for<'a> Cbor<'a>, + PropIdT: for<'a> Cbor<'a>, +{ + /// `choices` field + pub(super) choices: Vec>, + /// `proof` field + pub(super) proof: Proof, + /// `prop-id` field + pub(super) prop_id: PropId, +} + +impl Decode<'_, ()> for Vote +where + ChoiceT: for<'a> Cbor<'a>, + ProofT: for<'a> Cbor<'a>, + PropIdT: for<'a> Cbor<'a>, +{ + fn decode(d: &mut Decoder<'_>, (): &mut ()) -> Result { + let Some(VOTE_LEN) = d.array()? else { + return Err(minicbor::decode::Error::message(format!( + "must be a defined sized array with {VOTE_LEN} entries" + ))); + }; + + let choices = Vec::>::decode(d, &mut ())?; + if choices.is_empty() { + return Err(minicbor::decode::Error::message( + "choices array must has at least one entry", + )); + } + let proof = Proof::decode(d, &mut ())?; + let prop_id = PropId::decode(d, &mut ())?; + Ok(Self { + choices, + proof, + prop_id, + }) + } +} + +impl Encode<()> for Vote +where + ChoiceT: for<'a> Cbor<'a>, + ProofT: for<'a> Cbor<'a>, + PropIdT: for<'a> Cbor<'a>, +{ + fn encode( + &self, e: &mut minicbor::Encoder, (): &mut (), + ) -> Result<(), minicbor::encode::Error> { + e.array(VOTE_LEN)?; + self.choices.encode(e, &mut ())?; + self.proof.encode(e, &mut ())?; + self.prop_id.encode(e, &mut ())?; + Ok(()) + } +} diff --git a/rust/vote-tx-v2/src/lib.rs b/rust/vote-tx-v2/src/lib.rs index 4d3bfa887c7..2cc28f79e94 100644 --- a/rust/vote-tx-v2/src/lib.rs +++ b/rust/vote-tx-v2/src/lib.rs @@ -1,103 +1,35 @@ -//! A Catalyst vote transaction v1 object, structured following this -//! [spec](https://input-output-hk.github.io/catalyst-libs/architecture/08_concepts/catalyst_voting/v2/) +//! A Catalyst vote transaction v2 objects, structured following this +//! [spec](https://input-output-hk.github.io/catalyst-libs/architecture/08_concepts/catalyst_voting/) // cspell: words Coap use anyhow::anyhow; -use minicbor::{data::Int, Decode, Decoder, Encode, Encoder}; +use minicbor::{Decode, Decoder, Encode, Encoder}; -mod decoding; - -/// A generalized tx struct. -#[derive(Debug, Clone, PartialEq)] -pub struct GeneralizedTx { - /// `tx-body` field - tx_body: TxBody, - /// `signature` field - signature: coset::CoseSign, -} - -/// A tx body struct. -#[derive(Debug, Clone, PartialEq, Eq)] -pub struct TxBody { - /// `vote-type` field - vote_type: Uuid, - /// `event` field - event: EventMap, - /// `votes` field - votes: Vec, - /// `voter-data` field - voter_data: VoterData, -} - -/// A vote struct. -#[derive(Debug, Clone, PartialEq, Eq)] -pub struct Vote { - /// `choices` field - choices: Vec, - /// `proof` field - proof: Proof, - /// `prop-id` field - prop_id: PropId, -} - -/// A CBOR map -#[derive(Debug, Clone, PartialEq, Eq)] -pub struct EventMap(Vec<(EventKey, Vec)>); - -/// An `event-key` type definition. -#[derive(Debug, Clone, PartialEq, Eq)] -pub enum EventKey { - /// CBOR `int` type - Int(Int), - /// CBOR `text` type - Text(String), -} - -/// A UUID struct. -#[derive(Debug, Clone, PartialEq, Eq)] -pub struct Uuid(Vec); - -/// A voter's data struct. -#[derive(Debug, Clone, PartialEq, Eq)] -pub struct VoterData(Vec); - -/// A choice struct. -#[derive(Debug, Clone, PartialEq, Eq)] -pub struct Choice(Vec); - -/// A proof struct. -#[derive(Debug, Clone, PartialEq, Eq)] -pub struct Proof(Vec); - -/// A prop id struct. -#[derive(Debug, Clone, PartialEq, Eq)] -pub struct PropId(Vec); - -impl GeneralizedTx { - /// Creates a new `GeneralizedTx` struct. - #[must_use] - pub fn new(tx_body: TxBody) -> Self { - let signature = coset::CoseSignBuilder::new() - .protected(Self::cose_protected_header()) - .build(); - Self { tx_body, signature } - } - - /// Returns the COSE protected header. - fn cose_protected_header() -> coset::Header { - coset::HeaderBuilder::new() - .content_format(coset::iana::CoapContentFormat::Cbor) - .build() - } -} +pub mod encoded_cbor; +pub mod gen_tx; +pub mod public_tx; +pub mod uuid; /// Cbor encodable and decodable type trait. -pub trait Cbor<'a>: Encode<()> + Decode<'a, ()> { +pub trait Cbor<'a> { /// Encodes to CBOR encoded bytes. /// /// # Errors /// - Cannot encode + fn to_bytes(&self) -> anyhow::Result>; + + /// Decodes from the CBOR encoded bytes. + /// + /// # Errors + /// - Cannot decode + fn from_bytes(bytes: &'a [u8]) -> anyhow::Result + where Self: Sized; +} + +impl<'a, T> Cbor<'a> for T +where T: Encode<()> + Decode<'a, ()> +{ fn to_bytes(&self) -> anyhow::Result> { let mut bytes = Vec::new(); let mut e = Encoder::new(&mut bytes); @@ -106,10 +38,6 @@ pub trait Cbor<'a>: Encode<()> + Decode<'a, ()> { Ok(bytes) } - /// Decodes from the CBOR encoded bytes. - /// - /// # Errors - /// - Cannot decode fn from_bytes(bytes: &'a [u8]) -> anyhow::Result { let mut decoder = Decoder::new(bytes); let res = Self::decode(&mut decoder, &mut ()) @@ -117,5 +45,3 @@ pub trait Cbor<'a>: Encode<()> + Decode<'a, ()> { Ok(res) } } - -impl<'a, T> Cbor<'a> for T where T: Encode<()> + Decode<'a, ()> {} diff --git a/rust/vote-tx-v2/src/public_tx/mod.rs b/rust/vote-tx-v2/src/public_tx/mod.rs new file mode 100644 index 00000000000..af2262c5851 --- /dev/null +++ b/rust/vote-tx-v2/src/public_tx/mod.rs @@ -0,0 +1,84 @@ +//! A Catalyst public vote transaction v2 object, structured following this +//! [spec](https://input-output-hk.github.io/catalyst-libs/architecture/08_concepts/catalyst_voting/v2/#public-vote) + +mod vote; + +use std::ops::{Deref, DerefMut}; + +use minicbor::{Decode, Encode}; +pub use vote::{Choice, Proof, PropId}; + +use crate::{gen_tx::GeneralizedTx, Cbor}; + +/// A public vote tx struct. +#[derive(Debug, Clone, PartialEq)] +pub struct PublicTx(GeneralizedTx) +where VoteDataT: for<'a> Cbor<'a>; + +impl Deref for PublicTx +where VoteDataT: for<'a> Cbor<'a> +{ + type Target = GeneralizedTx; + + fn deref(&self) -> &Self::Target { + &self.0 + } +} + +impl DerefMut for PublicTx +where VoteDataT: for<'a> Cbor<'a> +{ + fn deref_mut(&mut self) -> &mut Self::Target { + &mut self.0 + } +} + +impl Decode<'_, ()> for PublicTx +where VoteDataT: for<'a> Cbor<'a> +{ + fn decode(d: &mut minicbor::Decoder<'_>, (): &mut ()) -> Result { + let gen_tx = GeneralizedTx::decode(d, &mut ())?; + Ok(Self(gen_tx)) + } +} + +impl Encode<()> for PublicTx +where VoteDataT: for<'a> Cbor<'a> +{ + fn encode( + &self, e: &mut minicbor::Encoder, ctx: &mut (), + ) -> Result<(), minicbor::encode::Error> { + self.0.encode(e, ctx) + } +} + +#[cfg(test)] +mod tests { + use proptest::sample::size_range; + use test_strategy::proptest; + + use super::*; + use crate::{encoded_cbor::EncodedCbor, gen_tx::GeneralizedTxBuilder, uuid::Uuid}; + + #[proptest] + fn public_tx_from_bytes_to_bytes_test( + vote_type: Vec, voter_data: Vec, + #[any(size_range(1..10_usize).lift())] choices: Vec, prop_id: Vec, + ) { + let gen_tx_builder = GeneralizedTxBuilder::::new( + Uuid(vote_type), + EncodedCbor(voter_data), + ); + let choices = choices.into_iter().map(Choice).collect(); + let gen_tx = gen_tx_builder + .with_vote(choices, Proof, Uuid(prop_id)) + .unwrap() + .build() + .unwrap(); + let public_tx = PublicTx(gen_tx); + + let bytes = public_tx.to_bytes().unwrap(); + let decoded = PublicTx::from_bytes(&bytes).unwrap(); + assert_eq!(public_tx, decoded); + } +} diff --git a/rust/vote-tx-v2/src/public_tx/vote.rs b/rust/vote-tx-v2/src/public_tx/vote.rs new file mode 100644 index 00000000000..35f4fa89b51 --- /dev/null +++ b/rust/vote-tx-v2/src/public_tx/vote.rs @@ -0,0 +1,47 @@ +//! A public vote tx vote objects. + +use minicbor::{Decode, Encode}; + +use crate::uuid::Uuid; + +/// A public voting choice struct. +#[derive(Debug, Clone, PartialEq)] +pub struct Choice(pub u64); + +/// A public voting proof struct, CBOR `undefined`. +#[derive(Debug, Clone, PartialEq)] +pub struct Proof; + +/// A public voting proposal id struct. +pub type PropId = Uuid; + +impl Decode<'_, ()> for Choice { + fn decode(d: &mut minicbor::Decoder<'_>, (): &mut ()) -> Result { + let choice = d.u64()?; + Ok(Self(choice)) + } +} + +impl Encode<()> for Choice { + fn encode( + &self, e: &mut minicbor::Encoder, (): &mut (), + ) -> Result<(), minicbor::encode::Error> { + self.0.encode(e, &mut ()) + } +} + +impl Decode<'_, ()> for Proof { + fn decode(d: &mut minicbor::Decoder<'_>, (): &mut ()) -> Result { + d.undefined()?; + Ok(Self) + } +} + +impl Encode<()> for Proof { + fn encode( + &self, e: &mut minicbor::Encoder, (): &mut (), + ) -> Result<(), minicbor::encode::Error> { + e.undefined()?; + Ok(()) + } +} diff --git a/rust/vote-tx-v2/src/uuid.rs b/rust/vote-tx-v2/src/uuid.rs new file mode 100644 index 00000000000..6bb3b5d5170 --- /dev/null +++ b/rust/vote-tx-v2/src/uuid.rs @@ -0,0 +1,34 @@ +//! A CBOR encoded/decoded UUID struct. + +use minicbor::{data::Tag, Decode, Decoder, Encode}; + +/// UUID CBOR tag . +const UUID_TAG: u64 = 37; + +/// A UUID struct, CBOR tag 37. +#[derive(Debug, Clone, PartialEq)] +pub struct Uuid(pub Vec); + +impl Decode<'_, ()> for Uuid { + fn decode(d: &mut Decoder<'_>, (): &mut ()) -> Result { + let tag = d.tag()?; + if UUID_TAG != tag.as_u64() { + return Err(minicbor::decode::Error::message(format!( + "tag value must be: {UUID_TAG}, provided: {}", + tag.as_u64(), + ))); + } + let choice = d.bytes()?.to_vec(); + Ok(Self(choice)) + } +} + +impl Encode<()> for Uuid { + fn encode( + &self, e: &mut minicbor::Encoder, (): &mut (), + ) -> Result<(), minicbor::encode::Error> { + e.tag(Tag::new(UUID_TAG))?; + e.bytes(&self.0)?; + Ok(()) + } +}