diff --git a/.github/workflows/semantic_pull_request.yml b/.github/workflows/semantic_pull_request.yml index b788d8a508f..bc8ae9585e6 100644 --- a/.github/workflows/semantic_pull_request.yml +++ b/.github/workflows/semantic_pull_request.yml @@ -19,6 +19,8 @@ jobs: rust/c509-certificate rust/cardano-chain-follower rust/catalyst-voting + rust/vote-tx-v1 + rust/vote-tx-v2 rust/cbork rust/hermes-ipfs dart 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 67635a0b206..693e151dabb 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 @@ -7,7 +7,7 @@ tx-body = [ vote-type event, votes, - voters-data, + voter-data, ] vote-type = UUID ; e.g. Public or Private vote @@ -25,7 +25,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 -voters-data = encoded-cbor +voter-data = 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/gen_vote_tx.md b/docs/src/architecture/08_concepts/catalyst_voting/gen_vote_tx.md index 1a2b70f6fdf..cc467ff19f3 100644 --- a/docs/src/architecture/08_concepts/catalyst_voting/gen_vote_tx.md +++ b/docs/src/architecture/08_concepts/catalyst_voting/gen_vote_tx.md @@ -42,7 +42,7 @@ Vote: so it's redundant to provide an additional identifier for the proposal, so it could be placed `null`. -`voters-data` - an any additional voter's specific data. +`voter-data` - an any additional voter's specific data. ### Transaction signing diff --git a/rust/Cargo.toml b/rust/Cargo.toml index 94ed9ac812e..4bf0c71f2bf 100644 --- a/rust/Cargo.toml +++ b/rust/Cargo.toml @@ -7,7 +7,9 @@ members = [ "cbork", "cbork-abnf-parser", "cbork-cddl-parser", - "catalyst-voting", "jormungandr-vote-tx", + "catalyst-voting", + "vote-tx-v1", + "vote-tx-v2", ] [workspace.package] diff --git a/rust/Earthfile b/rust/Earthfile index e0747f2b27e..17b681eeaa9 100644 --- a/rust/Earthfile +++ b/rust/Earthfile @@ -10,7 +10,7 @@ COPY_SRC: .cargo .config \ c509-certificate \ cardano-chain-follower \ - catalyst-voting jormungandr-vote-tx \ + catalyst-voting vote-tx-v1 vote-tx-v2 \ cbork cbork-abnf-parser cbork-cddl-parser \ hermes-ipfs \ . @@ -53,7 +53,7 @@ build: --cmd="/scripts/std_build.py" \ --args1="--libs=c509-certificate --libs=cardano-chain-follower --libs=hermes-ipfs" \ --args2="--libs=cbork-cddl-parser --libs=cbork-abnf-parser" \ - --args3="--libs=catalyst-voting --libs=jormungandr-vote-tx" \ + --args3="--libs=catalyst-voting --libs=vote-tx-v1 --libs=vote-tx-v2" \ --args4="--bins=cbork/cbork" \ --args5="--cov_report=$HOME/build/coverage-report.info" \ --output="release/[^\./]+" \ diff --git a/rust/jormungandr-vote-tx/Cargo.toml b/rust/vote-tx-v1/Cargo.toml similarity index 94% rename from rust/jormungandr-vote-tx/Cargo.toml rename to rust/vote-tx-v1/Cargo.toml index 42a2088acba..e99a5acab00 100644 --- a/rust/jormungandr-vote-tx/Cargo.toml +++ b/rust/vote-tx-v1/Cargo.toml @@ -1,5 +1,5 @@ [package] -name = "jormungandr-vote-tx" +name = "vote-tx-v1" version = "0.0.1" edition.workspace = true authors.workspace = true diff --git a/rust/jormungandr-vote-tx/src/decoding.rs b/rust/vote-tx-v1/src/decoding.rs similarity index 100% rename from rust/jormungandr-vote-tx/src/decoding.rs rename to rust/vote-tx-v1/src/decoding.rs diff --git a/rust/jormungandr-vote-tx/src/lib.rs b/rust/vote-tx-v1/src/lib.rs similarity index 98% rename from rust/jormungandr-vote-tx/src/lib.rs rename to rust/vote-tx-v1/src/lib.rs index c8859180aa9..c7f46ac6750 100644 --- a/rust/jormungandr-vote-tx/src/lib.rs +++ b/rust/vote-tx-v1/src/lib.rs @@ -1,12 +1,12 @@ -//! A Jörmungandr transaction object structured following this -//! [spec](https://input-output-hk.github.io/catalyst-libs/architecture/08_concepts/catalyst_voting/jorm/) +//! A Catalyst v1 (Jörmungandr) vote transaction object, structured following this +//! [spec](https://input-output-hk.github.io/catalyst-libs/architecture/08_concepts/catalyst_voting/v1/) //! //! ```rust //! use catalyst_voting::{ //! crypto::{ed25519::PrivateKey, rng::default_rng}, //! vote_protocol::committee::ElectionSecretKey, //! }; -//! use jormungandr_vote_tx::Tx; +//! use vote_tx_v1::Tx; //! //! let vote_plan_id = [0u8; 32]; //! let proposal_index = 0u8; @@ -65,7 +65,7 @@ use catalyst_voting::{ }, }; -/// A v1 (Jörmungandr) transaction struct +/// A v1 (Jörmungandr) vote transaction struct #[derive(Debug, Clone, PartialEq, Eq)] #[must_use] pub struct Tx { diff --git a/rust/jormungandr-vote-tx/src/utils.rs b/rust/vote-tx-v1/src/utils.rs similarity index 100% rename from rust/jormungandr-vote-tx/src/utils.rs rename to rust/vote-tx-v1/src/utils.rs diff --git a/rust/vote-tx-v2/Cargo.toml b/rust/vote-tx-v2/Cargo.toml new file mode 100644 index 00000000000..badce38a19b --- /dev/null +++ b/rust/vote-tx-v2/Cargo.toml @@ -0,0 +1,24 @@ +[package] +name = "vote-tx-v2" +version = "0.1.0" +edition.workspace = true +authors.workspace = true +homepage.workspace = true +repository.workspace = true +license.workspace = true + +[lib] +crate-type = ["lib", "cdylib"] + +[lints] +workspace = true + +[dependencies] +anyhow = "1.0.89" +proptest = { version = "1.5.0" } +minicbor = { version = "0.25.1", features = ["alloc"] } + +[dev-dependencies] +# 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 new file mode 100644 index 00000000000..775dd5db6d8 --- /dev/null +++ b/rust/vote-tx-v2/src/decoding.rs @@ -0,0 +1,268 @@ +//! CBOR encoding and decoding implementation. +//! + +use minicbor::{ + data::{IanaTag, Tag}, + Decode, Decoder, Encode, Encoder, +}; + +use crate::{Choice, 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 = 3; + +/// `GeneralizedTx` array struct length +const GENERALIZED_TX_LEN: u64 = 1; + +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 ())?; + Ok(Self { tx_body }) + } +} + +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 ())?; + 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 votes = Vec::::decode(d, &mut ())?; + let voter_data = VoterData::decode(d, &mut ())?; + Ok(Self { + vote_type, + 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.votes.encode(e, &mut ())?; + self.voter_data.encode(e, &mut ())?; + 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(()) + } +} + +#[cfg(test)] +mod tests { + use test_strategy::proptest; + + use super::*; + use crate::Cbor; + + #[proptest] + fn generalized_tx_from_bytes_to_bytes_test(generalized_tx: GeneralizedTx) { + let bytes = generalized_tx.to_bytes().unwrap(); + let decoded = GeneralizedTx::from_bytes(&bytes).unwrap(); + assert_eq!(generalized_tx, decoded); + } + + #[proptest] + fn tx_body_from_bytes_to_bytes_test(tx_body: TxBody) { + let bytes = tx_body.to_bytes().unwrap(); + let decoded = TxBody::from_bytes(&bytes).unwrap(); + assert_eq!(tx_body, decoded); + } + + #[proptest] + fn vote_from_bytes_to_bytes_test(vote: Vote) { + let bytes = vote.to_bytes().unwrap(); + let decoded = Vote::from_bytes(&bytes).unwrap(); + assert_eq!(vote, decoded); + } +} diff --git a/rust/vote-tx-v2/src/lib.rs b/rust/vote-tx-v2/src/lib.rs new file mode 100644 index 00000000000..a169a329695 --- /dev/null +++ b/rust/vote-tx-v2/src/lib.rs @@ -0,0 +1,141 @@ +//! A Catalyst vote transaction v1 object, structured following this +//! [spec](https://input-output-hk.github.io/catalyst-libs/architecture/08_concepts/catalyst_voting/v2/) + +use anyhow::anyhow; +use minicbor::{Decode, Decoder, Encode, Encoder}; + +mod decoding; + +/// A generalized tx struct. +#[derive(Debug, Clone, PartialEq, Eq)] +pub struct GeneralizedTx { + /// `tx-body` + tx_body: TxBody, +} + +/// A tx body struct. +#[derive(Debug, Clone, PartialEq, Eq)] +pub struct TxBody { + /// `vote-type` field + vote_type: Uuid, + /// `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 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); + +/// Cbor encodable and decodable type trait. +pub trait Cbor<'a>: Encode<()> + Decode<'a, ()> { + /// Encodes to CBOR encoded bytes. + /// + /// # Errors + /// - Cannot encode + fn to_bytes(&self) -> anyhow::Result> { + let mut bytes = Vec::new(); + let mut e = Encoder::new(&mut bytes); + self.encode(&mut e, &mut ()) + .map_err(|e| anyhow!("Cannot encode `{}`, {e}.", std::any::type_name::()))?; + 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 ()) + .map_err(|e| anyhow!("Cannot decode `{}`, {e}.", std::any::type_name::()))?; + Ok(res) + } +} + +impl<'a, T> Cbor<'a> for T where T: Encode<()> + Decode<'a, ()> {} + +#[allow(missing_docs, clippy::missing_docs_in_private_items)] +mod arbitrary_impl { + use proptest::{ + prelude::{any, any_with, Arbitrary, BoxedStrategy, Strategy}, + sample::size_range, + }; + + use super::{Choice, GeneralizedTx, Proof, PropId, TxBody, Uuid, Vote, VoterData}; + + impl Arbitrary for GeneralizedTx { + type Parameters = (); + type Strategy = BoxedStrategy; + + fn arbitrary_with((): Self::Parameters) -> Self::Strategy { + any::().prop_map(|tx_body| Self { tx_body }).boxed() + } + } + + impl Arbitrary for TxBody { + type Parameters = (); + type Strategy = BoxedStrategy; + + fn arbitrary_with((): Self::Parameters) -> Self::Strategy { + any::<(Vec, Vec, Vec)>() + .prop_map(|(vote_type, votes, voters_data)| { + Self { + vote_type: Uuid(vote_type), + votes, + voter_data: VoterData(voters_data), + } + }) + .boxed() + } + } + + impl Arbitrary for Vote { + type Parameters = (); + type Strategy = BoxedStrategy; + + fn arbitrary_with((): Self::Parameters) -> Self::Strategy { + any_with::<(Vec>, Vec, Vec)>(( + (size_range(1..10usize), Default::default()), + Default::default(), + Default::default(), + )) + .prop_map(|(choices, proof, prop_id)| { + Self { + choices: choices.into_iter().map(Choice).collect(), + proof: Proof(proof), + prop_id: PropId(prop_id), + } + }) + .boxed() + } + } +}