Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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
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