From 58a9593716ff27e891f777c09407831252f83748 Mon Sep 17 00:00:00 2001 From: Mr-Leshiy Date: Thu, 14 Nov 2024 14:26:44 +0200 Subject: [PATCH 1/5] added new field `event` --- rust/vote-tx-v1/src/decoding.rs | 1 + rust/vote-tx-v2/Cargo.toml | 3 +- rust/vote-tx-v2/src/decoding.rs | 103 +++++++++++++++++++++++++++++++- rust/vote-tx-v2/src/lib.rs | 17 +++++- 4 files changed, 119 insertions(+), 5 deletions(-) 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..530991d1955 100644 --- a/rust/vote-tx-v2/Cargo.toml +++ b/rust/vote-tx-v2/Cargo.toml @@ -15,10 +15,11 @@ workspace = true [dependencies] anyhow = "1.0.89" -minicbor = { version = "0.25.1", features = ["alloc"] } +minicbor = { version = "0.25.1", features = ["alloc", "half"] } [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..ae2619d44ae 100644 --- a/rust/vote-tx-v2/src/decoding.rs +++ b/rust/vote-tx-v2/src/decoding.rs @@ -2,11 +2,13 @@ //! use minicbor::{ - data::{IanaTag, Tag}, + data::{IanaTag, Tag, Token}, Decode, Decoder, Encode, Encoder, }; -use crate::{Choice, GeneralizedTx, Proof, PropId, TxBody, Uuid, Vote, VoterData}; +use crate::{ + Cbor, Choice, EventKey, EventMap, GeneralizedTx, Proof, PropId, TxBody, Uuid, Vote, VoterData, +}; /// UUID CBOR tag . const CBOR_UUID_TAG: u64 = 37; @@ -15,7 +17,7 @@ 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; @@ -52,10 +54,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 +72,82 @@ 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 = Token::decode(d, &mut ())?.to_bytes().map_err(|_| { + minicbor::decode::Error::message( + "`minicbor::Token` encoding/decoding issue, must be CBOR encodable", + ) + })?; + 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 ())?; + let token = Token::from_bytes(value).map_err(|_| { + minicbor::encode::Error::message("Invalid map `value` bytes, must be cbor encoded") + })?; + token.encode(e, &mut ())?; + } + + 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()?; @@ -241,6 +315,7 @@ impl Encode<()> for PropId { #[cfg(test)] mod tests { use proptest::{prelude::any_with, sample::size_range}; + use proptest_derive::Arbitrary; use test_strategy::proptest; use super::*; @@ -249,6 +324,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,11 +344,26 @@ mod tests { ), )))] 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 = Token::U64(val).to_bytes().unwrap(); + (key, value) + }) + .collect(); + let generalized_tx = GeneralizedTx { tx_body: TxBody { vote_type: Uuid(vote_type), + event: EventMap(event), votes: votes .into_iter() .map(|(choices, proof, prop_id)| { diff --git a/rust/vote-tx-v2/src/lib.rs b/rust/vote-tx-v2/src/lib.rs index 3c3a57546ec..a4311206f8f 100644 --- a/rust/vote-tx-v2/src/lib.rs +++ b/rust/vote-tx-v2/src/lib.rs @@ -2,7 +2,7 @@ //! [spec](https://input-output-hk.github.io/catalyst-libs/architecture/08_concepts/catalyst_voting/v2/) use anyhow::anyhow; -use minicbor::{Decode, Decoder, Encode, Encoder}; +use minicbor::{data::Int, Decode, Decoder, Encode, Encoder}; mod decoding; @@ -18,6 +18,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 +37,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 defintion. +#[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); From 950f69684c6c24629d0cc98a3b8a4b585899f583 Mon Sep 17 00:00:00 2001 From: Mr-Leshiy Date: Thu, 14 Nov 2024 18:47:57 +0200 Subject: [PATCH 2/5] add signature field --- rust/vote-tx-v2/Cargo.toml | 1 + rust/vote-tx-v2/src/decoding.rs | 92 ++++++++++++++++++++++----------- rust/vote-tx-v2/src/lib.rs | 6 ++- 3 files changed, 67 insertions(+), 32 deletions(-) diff --git a/rust/vote-tx-v2/Cargo.toml b/rust/vote-tx-v2/Cargo.toml index 530991d1955..ab901a7a6a6 100644 --- a/rust/vote-tx-v2/Cargo.toml +++ b/rust/vote-tx-v2/Cargo.toml @@ -16,6 +16,7 @@ workspace = true [dependencies] anyhow = "1.0.89" minicbor = { version = "0.25.1", features = ["alloc", "half"] } +coset = { version = "0.3.8" } [dev-dependencies] proptest = { version = "1.5.0" } diff --git a/rust/vote-tx-v2/src/decoding.rs b/rust/vote-tx-v2/src/decoding.rs index ae2619d44ae..8849ff3ac99 100644 --- a/rust/vote-tx-v2/src/decoding.rs +++ b/rust/vote-tx-v2/src/decoding.rs @@ -1,13 +1,14 @@ //! CBOR encoding and decoding implementation. //! +use coset::CborSerializable; use minicbor::{ - data::{IanaTag, Tag, Token}, + data::{IanaTag, Tag}, Decode, Decoder, Encode, Encoder, }; use crate::{ - Cbor, Choice, EventKey, EventMap, GeneralizedTx, Proof, PropId, TxBody, Uuid, Vote, VoterData, + Choice, EventKey, EventMap, GeneralizedTx, Proof, PropId, TxBody, Uuid, Vote, VoterData, }; /// UUID CBOR tag . @@ -20,7 +21,7 @@ const VOTE_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 { @@ -31,7 +32,16 @@ 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"))?; + coset::CoseSign::from_slice(&sign_bytes).map_err(|_| { + minicbor::decode::Error::message("`signature` must be COSE_Sign encoded object") + })? + }; + + Ok(Self { tx_body, signature }) } } @@ -41,6 +51,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(()) } } @@ -90,10 +110,9 @@ impl Decode<'_, ()> for EventMap { let map = (0..len) .map(|_| { let key = EventKey::decode(d, &mut ())?; - let value = Token::decode(d, &mut ())?.to_bytes().map_err(|_| { - minicbor::decode::Error::message( - "`minicbor::Token` encoding/decoding issue, must be CBOR encodable", - ) + + let value = read_cbor_bytes(d).map_err(|_| { + minicbor::decode::Error::message("missing event map `value` field") })?; Ok((key, value)) }) @@ -111,10 +130,10 @@ impl Encode<()> for EventMap { for (key, value) in &self.0 { key.encode(e, &mut ())?; - let token = Token::from_bytes(value).map_err(|_| { - minicbor::encode::Error::message("Invalid map `value` bytes, must be cbor encoded") - })?; - token.encode(e, &mut ())?; + + e.writer_mut() + .write_all(value) + .map_err(minicbor::encode::Error::write)?; } Ok(()) @@ -312,6 +331,19 @@ 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}; @@ -355,29 +387,29 @@ mod tests { PropEventKey::U64(val) => EventKey::Int(val.into()), PropEventKey::I64(val) => EventKey::Int(val.into()), }; - let value = Token::U64(val).to_bytes().unwrap(); + let value = val.to_bytes().unwrap(); (key, value) }) .collect(); - - let generalized_tx = GeneralizedTx { - 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 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 signature = coset::CoseSign::default(); + 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); diff --git a/rust/vote-tx-v2/src/lib.rs b/rust/vote-tx-v2/src/lib.rs index a4311206f8f..81587680202 100644 --- a/rust/vote-tx-v2/src/lib.rs +++ b/rust/vote-tx-v2/src/lib.rs @@ -7,10 +7,12 @@ 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. From 29a3ee49740416cc9e0177099a9547632db95f35 Mon Sep 17 00:00:00 2001 From: Mr-Leshiy Date: Thu, 14 Nov 2024 18:55:53 +0200 Subject: [PATCH 3/5] wip --- rust/vote-tx-v2/src/decoding.rs | 3 +-- rust/vote-tx-v2/src/lib.rs | 18 ++++++++++++++++++ 2 files changed, 19 insertions(+), 2 deletions(-) diff --git a/rust/vote-tx-v2/src/decoding.rs b/rust/vote-tx-v2/src/decoding.rs index 8849ff3ac99..f2c53fa7e1a 100644 --- a/rust/vote-tx-v2/src/decoding.rs +++ b/rust/vote-tx-v2/src/decoding.rs @@ -407,8 +407,7 @@ mod tests { voter_data: VoterData(voter_data), }; - let signature = coset::CoseSign::default(); - let generalized_tx = GeneralizedTx { tx_body, signature }; + let generalized_tx = GeneralizedTx::new(tx_body); let bytes = generalized_tx.to_bytes().unwrap(); let decoded = GeneralizedTx::from_bytes(&bytes).unwrap(); diff --git a/rust/vote-tx-v2/src/lib.rs b/rust/vote-tx-v2/src/lib.rs index 81587680202..9d542f88a11 100644 --- a/rust/vote-tx-v2/src/lib.rs +++ b/rust/vote-tx-v2/src/lib.rs @@ -72,6 +72,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. From 73ad0375d8977cda307e1a0670d5f22759fc90e8 Mon Sep 17 00:00:00 2001 From: Mr-Leshiy Date: Thu, 14 Nov 2024 19:03:32 +0200 Subject: [PATCH 4/5] fix --- rust/vote-tx-v2/src/decoding.rs | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/rust/vote-tx-v2/src/decoding.rs b/rust/vote-tx-v2/src/decoding.rs index f2c53fa7e1a..b8e78038687 100644 --- a/rust/vote-tx-v2/src/decoding.rs +++ b/rust/vote-tx-v2/src/decoding.rs @@ -36,9 +36,12 @@ impl Decode<'_, ()> for GeneralizedTx { let signature = { let sign_bytes = read_cbor_bytes(d) .map_err(|_| minicbor::decode::Error::message("missing `signature` field"))?; - coset::CoseSign::from_slice(&sign_bytes).map_err(|_| { + let mut sign = coset::CoseSign::from_slice(&sign_bytes).map_err(|_| { minicbor::decode::Error::message("`signature` must be COSE_Sign encoded object") - })? + })?; + // We dont need to hold the original encoded data of the COSE protected header + sign.protected.original_data = None; + sign }; Ok(Self { tx_body, signature }) From 5fa07468ed4c860348f0370e063469a624af5674 Mon Sep 17 00:00:00 2001 From: Mr-Leshiy Date: Fri, 15 Nov 2024 10:54:07 +0200 Subject: [PATCH 5/5] fix spelling --- rust/vote-tx-v2/src/decoding.rs | 2 +- rust/vote-tx-v2/src/lib.rs | 4 +++- 2 files changed, 4 insertions(+), 2 deletions(-) diff --git a/rust/vote-tx-v2/src/decoding.rs b/rust/vote-tx-v2/src/decoding.rs index b8e78038687..213323cfce8 100644 --- a/rust/vote-tx-v2/src/decoding.rs +++ b/rust/vote-tx-v2/src/decoding.rs @@ -39,7 +39,7 @@ impl Decode<'_, ()> for GeneralizedTx { let mut sign = coset::CoseSign::from_slice(&sign_bytes).map_err(|_| { minicbor::decode::Error::message("`signature` must be COSE_Sign encoded object") })?; - // We dont need to hold the original encoded data of the COSE protected header + // We don't need to hold the original encoded data of the COSE protected header sign.protected.original_data = None; sign }; diff --git a/rust/vote-tx-v2/src/lib.rs b/rust/vote-tx-v2/src/lib.rs index 9d542f88a11..4d3bfa887c7 100644 --- a/rust/vote-tx-v2/src/lib.rs +++ b/rust/vote-tx-v2/src/lib.rs @@ -1,6 +1,8 @@ //! 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::{data::Int, Decode, Decoder, Encode, Encoder}; @@ -43,7 +45,7 @@ pub struct Vote { #[derive(Debug, Clone, PartialEq, Eq)] pub struct EventMap(Vec<(EventKey, Vec)>); -/// An `event-key` type defintion. +/// An `event-key` type definition. #[derive(Debug, Clone, PartialEq, Eq)] pub enum EventKey { /// CBOR `int` type