diff --git a/rust/vote-tx-v1/src/decoding.rs b/rust/vote-tx-v1/src/decoding.rs index 3e45e66f406..6d97a9b5c70 100644 --- a/rust/vote-tx-v1/src/decoding.rs +++ b/rust/vote-tx-v1/src/decoding.rs @@ -1,4 +1,5 @@ //! V1 transaction objects decoding implementation. +//! use std::io::Read; diff --git a/rust/vote-tx-v2/Cargo.toml b/rust/vote-tx-v2/Cargo.toml index 663d2aa7cf8..ab901a7a6a6 100644 --- a/rust/vote-tx-v2/Cargo.toml +++ b/rust/vote-tx-v2/Cargo.toml @@ -15,10 +15,12 @@ workspace = true [dependencies] anyhow = "1.0.89" -minicbor = { version = "0.25.1", features = ["alloc"] } +minicbor = { version = "0.25.1", features = ["alloc", "half"] } +coset = { version = "0.3.8" } [dev-dependencies] proptest = { version = "1.5.0" } +proptest-derive = { version = "0.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" diff --git a/rust/vote-tx-v2/src/decoding.rs b/rust/vote-tx-v2/src/decoding.rs index 65e4bbb1443..213323cfce8 100644 --- a/rust/vote-tx-v2/src/decoding.rs +++ b/rust/vote-tx-v2/src/decoding.rs @@ -1,12 +1,15 @@ //! CBOR encoding and decoding implementation. //! +use coset::CborSerializable; use minicbor::{ data::{IanaTag, Tag}, Decode, Decoder, Encode, Encoder, }; -use crate::{Choice, GeneralizedTx, Proof, PropId, TxBody, Uuid, Vote, VoterData}; +use crate::{ + Choice, EventKey, EventMap, GeneralizedTx, Proof, PropId, TxBody, Uuid, Vote, VoterData, +}; /// UUID CBOR tag . const CBOR_UUID_TAG: u64 = 37; @@ -15,10 +18,10 @@ const CBOR_UUID_TAG: u64 = 37; const VOTE_LEN: u64 = 3; /// `TxBody` array struct length -const TX_BODY_LEN: u64 = 3; +const TX_BODY_LEN: u64 = 4; /// `GeneralizedTx` array struct length -const GENERALIZED_TX_LEN: u64 = 1; +const GENERALIZED_TX_LEN: u64 = 2; impl Decode<'_, ()> for GeneralizedTx { fn decode(d: &mut Decoder<'_>, (): &mut ()) -> Result { @@ -29,7 +32,19 @@ impl Decode<'_, ()> for GeneralizedTx { }; let tx_body = TxBody::decode(d, &mut ())?; - Ok(Self { tx_body }) + + 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 }) } } @@ -39,6 +54,16 @@ impl Encode<()> for GeneralizedTx { ) -> 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(()) } } @@ -52,10 +77,12 @@ impl Decode<'_, ()> for TxBody { }; 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, }) @@ -68,12 +95,81 @@ impl Encode<()> for TxBody { ) -> 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()?; @@ -238,9 +334,23 @@ impl Encode<()> for PropId { } } +/// 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::*; @@ -249,6 +359,13 @@ mod tests { 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, @@ -262,25 +379,39 @@ mod tests { ), )))] votes: Vec, + event: Vec<(PropEventKey, u64)>, voter_data: Vec, ) { - let generalized_tx = GeneralizedTx { - tx_body: TxBody { - vote_type: Uuid(vote_type), - 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 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/lib.rs b/rust/vote-tx-v2/src/lib.rs index 3c3a57546ec..4d3bfa887c7 100644 --- a/rust/vote-tx-v2/src/lib.rs +++ b/rust/vote-tx-v2/src/lib.rs @@ -1,16 +1,20 @@ //! A Catalyst vote transaction v1 object, structured following this //! [spec](https://input-output-hk.github.io/catalyst-libs/architecture/08_concepts/catalyst_voting/v2/) +// cspell: words Coap + use anyhow::anyhow; -use minicbor::{Decode, Decoder, Encode, Encoder}; +use minicbor::{data::Int, Decode, Decoder, Encode, Encoder}; mod decoding; /// A generalized tx struct. -#[derive(Debug, Clone, PartialEq, Eq)] +#[derive(Debug, Clone, PartialEq)] pub struct GeneralizedTx { - /// `tx-body` + /// `tx-body` field tx_body: TxBody, + /// `signature` field + signature: coset::CoseSign, } /// A tx body struct. @@ -18,6 +22,8 @@ pub struct GeneralizedTx { pub struct TxBody { /// `vote-type` field vote_type: Uuid, + /// `event` field + event: EventMap, /// `votes` field votes: Vec, /// `voter-data` field @@ -35,6 +41,19 @@ pub struct Vote { 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); @@ -55,6 +74,24 @@ pub struct Proof(Vec); #[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() + } +} + /// Cbor encodable and decodable type trait. pub trait Cbor<'a>: Encode<()> + Decode<'a, ()> { /// Encodes to CBOR encoded bytes.