Skip to content

Commit

Permalink
feat(rust/vote-tx-v2): Feat add initial CBOR decoding/encoding implem…
Browse files Browse the repository at this point in the history
…entation of the generalised vote tx (#81)

* add vote-tx-v2 crate

* rename jormungandr-vote-tx to vote-tx-v1

* add Vote CBOR encoding/decoding

* wip

* wip

* wip

* wip

* add proptest for Vote type

* replace ciborium usage with minicbor

* add new TxBody struct, cleanup gen_vote_tx.cddl

* add TxBody CBOR decoding/encoding impl

* add Cbor trait

* add GeneralisedTx struct

* wip

* wip

* fix spelling

* fix doctest

* add array lenth validation
  • Loading branch information
Mr-Leshiy authored Nov 13, 2024
1 parent 8f9f071 commit 9142501
Show file tree
Hide file tree
Showing 12 changed files with 448 additions and 11 deletions.
2 changes: 2 additions & 0 deletions .github/workflows/semantic_pull_request.yml
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@ tx-body<choice-t, proof-t, prop-id-t> = [
vote-type
event,
votes<choice-t, proof-t, prop-id-t>,
voters-data,
voter-data,
]

vote-type = UUID ; e.g. Public or Private vote
Expand All @@ -25,7 +25,7 @@ choice<choice-t> = #6.24(bytes .cbor choice-t) ; encoded-cbor
proof<proof-t> = #6.24(bytes .cbor proof-t) ; encoded-cbor
prop-id<prop-id-t> = #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
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand Down
4 changes: 3 additions & 1 deletion rust/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -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]
Expand Down
4 changes: 2 additions & 2 deletions rust/Earthfile
Original file line number Diff line number Diff line change
Expand Up @@ -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 \
.
Expand Down Expand Up @@ -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/[^\./]+" \
Expand Down
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
[package]
name = "jormungandr-vote-tx"
name = "vote-tx-v1"
version = "0.0.1"
edition.workspace = true
authors.workspace = true
Expand Down
File renamed without changes.
Original file line number Diff line number Diff line change
@@ -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;
Expand Down Expand Up @@ -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 {
Expand Down
File renamed without changes.
24 changes: 24 additions & 0 deletions rust/vote-tx-v2/Cargo.toml
Original file line number Diff line number Diff line change
@@ -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"
268 changes: 268 additions & 0 deletions rust/vote-tx-v2/src/decoding.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,268 @@
//! CBOR encoding and decoding implementation.
//! <https://input-output-hk.github.io/catalyst-libs/architecture/08_concepts/catalyst_voting/cddl/gen_vote_tx.cddl>
use minicbor::{
data::{IanaTag, Tag},
Decode, Decoder, Encode, Encoder,
};

use crate::{Choice, GeneralizedTx, Proof, PropId, TxBody, Uuid, Vote, VoterData};

/// UUID CBOR tag <https://www.iana.org/assignments/cbor-tags/cbor-tags.xhtml/>.
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<Self, minicbor::decode::Error> {
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<W: minicbor::encode::Write>(
&self, e: &mut Encoder<W>, (): &mut (),
) -> Result<(), minicbor::encode::Error<W::Error>> {
e.array(GENERALIZED_TX_LEN)?;
self.tx_body.encode(e, &mut ())?;
Ok(())
}
}

impl Decode<'_, ()> for TxBody {
fn decode(d: &mut Decoder<'_>, (): &mut ()) -> Result<Self, minicbor::decode::Error> {
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::<Vote>::decode(d, &mut ())?;
let voter_data = VoterData::decode(d, &mut ())?;
Ok(Self {
vote_type,
votes,
voter_data,
})
}
}

impl Encode<()> for TxBody {
fn encode<W: minicbor::encode::Write>(
&self, e: &mut Encoder<W>, (): &mut (),
) -> Result<(), minicbor::encode::Error<W::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<Self, minicbor::decode::Error> {
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<W: minicbor::encode::Write>(
&self, e: &mut minicbor::Encoder<W>, (): &mut (),
) -> Result<(), minicbor::encode::Error<W::Error>> {
e.tag(IanaTag::Cbor.tag())?;
e.bytes(&self.0)?;
Ok(())
}
}

impl Decode<'_, ()> for Uuid {
fn decode(d: &mut Decoder<'_>, (): &mut ()) -> Result<Self, minicbor::decode::Error> {
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<W: minicbor::encode::Write>(
&self, e: &mut minicbor::Encoder<W>, (): &mut (),
) -> Result<(), minicbor::encode::Error<W::Error>> {
e.tag(Tag::new(CBOR_UUID_TAG))?;
e.bytes(&self.0)?;
Ok(())
}
}

impl Decode<'_, ()> for Vote {
fn decode(d: &mut Decoder<'_>, (): &mut ()) -> Result<Self, minicbor::decode::Error> {
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::<Choice>::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<W: minicbor::encode::Write>(
&self, e: &mut minicbor::Encoder<W>, (): &mut (),
) -> Result<(), minicbor::encode::Error<W::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<Self, minicbor::decode::Error> {
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<W: minicbor::encode::Write>(
&self, e: &mut minicbor::Encoder<W>, (): &mut (),
) -> Result<(), minicbor::encode::Error<W::Error>> {
e.tag(IanaTag::Cbor.tag())?;
e.bytes(&self.0)?;
Ok(())
}
}

impl Decode<'_, ()> for Proof {
fn decode(d: &mut Decoder<'_>, (): &mut ()) -> Result<Self, minicbor::decode::Error> {
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<W: minicbor::encode::Write>(
&self, e: &mut minicbor::Encoder<W>, (): &mut (),
) -> Result<(), minicbor::encode::Error<W::Error>> {
e.tag(IanaTag::Cbor.tag())?;
e.bytes(&self.0)?;
Ok(())
}
}

impl Decode<'_, ()> for PropId {
fn decode(d: &mut Decoder<'_>, (): &mut ()) -> Result<Self, minicbor::decode::Error> {
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<W: minicbor::encode::Write>(
&self, e: &mut minicbor::Encoder<W>, (): &mut (),
) -> Result<(), minicbor::encode::Error<W::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);
}
}
Loading

0 comments on commit 9142501

Please sign in to comment.