Skip to content

Commit d4dc27a

Browse files
authored
feat(rust/vote-tx-v2): Finish GeneralisedTx CBOR decoding functionality (#83)
* added new field `event` * add signature field * wip * fix * fix spelling
1 parent 405f07a commit d4dc27a

File tree

4 files changed

+194
-23
lines changed

4 files changed

+194
-23
lines changed

rust/vote-tx-v1/src/decoding.rs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
//! V1 transaction objects decoding implementation.
2+
//! <https://input-output-hk.github.io/catalyst-libs/architecture/08_concepts/catalyst_voting/abnf/jorm.abnf>
23
34
use std::io::Read;
45

rust/vote-tx-v2/Cargo.toml

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -15,10 +15,12 @@ workspace = true
1515

1616
[dependencies]
1717
anyhow = "1.0.89"
18-
minicbor = { version = "0.25.1", features = ["alloc"] }
18+
minicbor = { version = "0.25.1", features = ["alloc", "half"] }
19+
coset = { version = "0.3.8" }
1920

2021
[dev-dependencies]
2122
proptest = { version = "1.5.0" }
23+
proptest-derive = { version = "0.5.0" }
2224
# Potentially it could be replaced with using `proptest::property_test` attribute macro,
2325
# after this PR will be merged https://github.com/proptest-rs/proptest/pull/523
2426
test-strategy = "0.4.0"

rust/vote-tx-v2/src/decoding.rs

Lines changed: 150 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,15 @@
11
//! CBOR encoding and decoding implementation.
22
//! <https://input-output-hk.github.io/catalyst-libs/architecture/08_concepts/catalyst_voting/cddl/gen_vote_tx.cddl>
33
4+
use coset::CborSerializable;
45
use minicbor::{
56
data::{IanaTag, Tag},
67
Decode, Decoder, Encode, Encoder,
78
};
89

9-
use crate::{Choice, GeneralizedTx, Proof, PropId, TxBody, Uuid, Vote, VoterData};
10+
use crate::{
11+
Choice, EventKey, EventMap, GeneralizedTx, Proof, PropId, TxBody, Uuid, Vote, VoterData,
12+
};
1013

1114
/// UUID CBOR tag <https://www.iana.org/assignments/cbor-tags/cbor-tags.xhtml/>.
1215
const CBOR_UUID_TAG: u64 = 37;
@@ -15,10 +18,10 @@ const CBOR_UUID_TAG: u64 = 37;
1518
const VOTE_LEN: u64 = 3;
1619

1720
/// `TxBody` array struct length
18-
const TX_BODY_LEN: u64 = 3;
21+
const TX_BODY_LEN: u64 = 4;
1922

2023
/// `GeneralizedTx` array struct length
21-
const GENERALIZED_TX_LEN: u64 = 1;
24+
const GENERALIZED_TX_LEN: u64 = 2;
2225

2326
impl Decode<'_, ()> for GeneralizedTx {
2427
fn decode(d: &mut Decoder<'_>, (): &mut ()) -> Result<Self, minicbor::decode::Error> {
@@ -29,7 +32,19 @@ impl Decode<'_, ()> for GeneralizedTx {
2932
};
3033

3134
let tx_body = TxBody::decode(d, &mut ())?;
32-
Ok(Self { tx_body })
35+
36+
let signature = {
37+
let sign_bytes = read_cbor_bytes(d)
38+
.map_err(|_| minicbor::decode::Error::message("missing `signature` field"))?;
39+
let mut sign = coset::CoseSign::from_slice(&sign_bytes).map_err(|_| {
40+
minicbor::decode::Error::message("`signature` must be COSE_Sign encoded object")
41+
})?;
42+
// We don't need to hold the original encoded data of the COSE protected header
43+
sign.protected.original_data = None;
44+
sign
45+
};
46+
47+
Ok(Self { tx_body, signature })
3348
}
3449
}
3550

@@ -39,6 +54,16 @@ impl Encode<()> for GeneralizedTx {
3954
) -> Result<(), minicbor::encode::Error<W::Error>> {
4055
e.array(GENERALIZED_TX_LEN)?;
4156
self.tx_body.encode(e, &mut ())?;
57+
58+
let sign_bytes = self
59+
.signature
60+
.clone()
61+
.to_vec()
62+
.map_err(minicbor::encode::Error::message)?;
63+
e.writer_mut()
64+
.write_all(&sign_bytes)
65+
.map_err(minicbor::encode::Error::write)?;
66+
4267
Ok(())
4368
}
4469
}
@@ -52,10 +77,12 @@ impl Decode<'_, ()> for TxBody {
5277
};
5378

5479
let vote_type = Uuid::decode(d, &mut ())?;
80+
let event = EventMap::decode(d, &mut ())?;
5581
let votes = Vec::<Vote>::decode(d, &mut ())?;
5682
let voter_data = VoterData::decode(d, &mut ())?;
5783
Ok(Self {
5884
vote_type,
85+
event,
5986
votes,
6087
voter_data,
6188
})
@@ -68,12 +95,81 @@ impl Encode<()> for TxBody {
6895
) -> Result<(), minicbor::encode::Error<W::Error>> {
6996
e.array(TX_BODY_LEN)?;
7097
self.vote_type.encode(e, &mut ())?;
98+
self.event.encode(e, &mut ())?;
7199
self.votes.encode(e, &mut ())?;
72100
self.voter_data.encode(e, &mut ())?;
73101
Ok(())
74102
}
75103
}
76104

105+
impl Decode<'_, ()> for EventMap {
106+
fn decode(d: &mut Decoder<'_>, (): &mut ()) -> Result<Self, minicbor::decode::Error> {
107+
let Some(len) = d.map()? else {
108+
return Err(minicbor::decode::Error::message(
109+
"must be a defined sized map",
110+
));
111+
};
112+
113+
let map = (0..len)
114+
.map(|_| {
115+
let key = EventKey::decode(d, &mut ())?;
116+
117+
let value = read_cbor_bytes(d).map_err(|_| {
118+
minicbor::decode::Error::message("missing event map `value` field")
119+
})?;
120+
Ok((key, value))
121+
})
122+
.collect::<Result<_, _>>()?;
123+
124+
Ok(EventMap(map))
125+
}
126+
}
127+
128+
impl Encode<()> for EventMap {
129+
fn encode<W: minicbor::encode::Write>(
130+
&self, e: &mut Encoder<W>, (): &mut (),
131+
) -> Result<(), minicbor::encode::Error<W::Error>> {
132+
e.map(self.0.len() as u64)?;
133+
134+
for (key, value) in &self.0 {
135+
key.encode(e, &mut ())?;
136+
137+
e.writer_mut()
138+
.write_all(value)
139+
.map_err(minicbor::encode::Error::write)?;
140+
}
141+
142+
Ok(())
143+
}
144+
}
145+
146+
impl Decode<'_, ()> for EventKey {
147+
fn decode(d: &mut Decoder<'_>, (): &mut ()) -> Result<Self, minicbor::decode::Error> {
148+
let pos = d.position();
149+
// try to decode as int
150+
if let Ok(i) = d.int() {
151+
Ok(EventKey::Int(i))
152+
} else {
153+
// try to decode as text
154+
d.set_position(pos);
155+
let str = d.str()?;
156+
Ok(EventKey::Text(str.to_string()))
157+
}
158+
}
159+
}
160+
161+
impl Encode<()> for EventKey {
162+
fn encode<W: minicbor::encode::Write>(
163+
&self, e: &mut Encoder<W>, (): &mut (),
164+
) -> Result<(), minicbor::encode::Error<W::Error>> {
165+
match self {
166+
EventKey::Int(i) => e.int(*i)?,
167+
EventKey::Text(s) => e.str(s)?,
168+
};
169+
Ok(())
170+
}
171+
}
172+
77173
impl Decode<'_, ()> for VoterData {
78174
fn decode(d: &mut Decoder<'_>, (): &mut ()) -> Result<Self, minicbor::decode::Error> {
79175
let tag = d.tag()?;
@@ -238,9 +334,23 @@ impl Encode<()> for PropId {
238334
}
239335
}
240336

337+
/// Reads CBOR bytes from the decoder and returns them as bytes.
338+
fn read_cbor_bytes(d: &mut Decoder<'_>) -> Result<Vec<u8>, minicbor::decode::Error> {
339+
let start = d.position();
340+
d.skip()?;
341+
let end = d.position();
342+
let bytes = d
343+
.input()
344+
.get(start..end)
345+
.ok_or(minicbor::decode::Error::end_of_input())?
346+
.to_vec();
347+
Ok(bytes)
348+
}
349+
241350
#[cfg(test)]
242351
mod tests {
243352
use proptest::{prelude::any_with, sample::size_range};
353+
use proptest_derive::Arbitrary;
244354
use test_strategy::proptest;
245355

246356
use super::*;
@@ -249,6 +359,13 @@ mod tests {
249359
type PropChoice = Vec<u8>;
250360
type PropVote = (Vec<PropChoice>, Vec<u8>, Vec<u8>);
251361

362+
#[derive(Debug, Arbitrary)]
363+
enum PropEventKey {
364+
Text(String),
365+
U64(u64),
366+
I64(i64),
367+
}
368+
252369
#[proptest]
253370
fn generalized_tx_from_bytes_to_bytes_test(
254371
vote_type: Vec<u8>,
@@ -262,25 +379,39 @@ mod tests {
262379
),
263380
)))]
264381
votes: Vec<PropVote>,
382+
event: Vec<(PropEventKey, u64)>,
265383
voter_data: Vec<u8>,
266384
) {
267-
let generalized_tx = GeneralizedTx {
268-
tx_body: TxBody {
269-
vote_type: Uuid(vote_type),
270-
votes: votes
271-
.into_iter()
272-
.map(|(choices, proof, prop_id)| {
273-
Vote {
274-
choices: choices.into_iter().map(Choice).collect(),
275-
proof: Proof(proof),
276-
prop_id: PropId(prop_id),
277-
}
278-
})
279-
.collect(),
280-
voter_data: VoterData(voter_data),
281-
},
385+
let event = event
386+
.into_iter()
387+
.map(|(key, val)| {
388+
let key = match key {
389+
PropEventKey::Text(key) => EventKey::Text(key),
390+
PropEventKey::U64(val) => EventKey::Int(val.into()),
391+
PropEventKey::I64(val) => EventKey::Int(val.into()),
392+
};
393+
let value = val.to_bytes().unwrap();
394+
(key, value)
395+
})
396+
.collect();
397+
let tx_body = TxBody {
398+
vote_type: Uuid(vote_type),
399+
event: EventMap(event),
400+
votes: votes
401+
.into_iter()
402+
.map(|(choices, proof, prop_id)| {
403+
Vote {
404+
choices: choices.into_iter().map(Choice).collect(),
405+
proof: Proof(proof),
406+
prop_id: PropId(prop_id),
407+
}
408+
})
409+
.collect(),
410+
voter_data: VoterData(voter_data),
282411
};
283412

413+
let generalized_tx = GeneralizedTx::new(tx_body);
414+
284415
let bytes = generalized_tx.to_bytes().unwrap();
285416
let decoded = GeneralizedTx::from_bytes(&bytes).unwrap();
286417
assert_eq!(generalized_tx, decoded);

rust/vote-tx-v2/src/lib.rs

Lines changed: 40 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,23 +1,29 @@
11
//! A Catalyst vote transaction v1 object, structured following this
22
//! [spec](https://input-output-hk.github.io/catalyst-libs/architecture/08_concepts/catalyst_voting/v2/)
33
4+
// cspell: words Coap
5+
46
use anyhow::anyhow;
5-
use minicbor::{Decode, Decoder, Encode, Encoder};
7+
use minicbor::{data::Int, Decode, Decoder, Encode, Encoder};
68

79
mod decoding;
810

911
/// A generalized tx struct.
10-
#[derive(Debug, Clone, PartialEq, Eq)]
12+
#[derive(Debug, Clone, PartialEq)]
1113
pub struct GeneralizedTx {
12-
/// `tx-body`
14+
/// `tx-body` field
1315
tx_body: TxBody,
16+
/// `signature` field
17+
signature: coset::CoseSign,
1418
}
1519

1620
/// A tx body struct.
1721
#[derive(Debug, Clone, PartialEq, Eq)]
1822
pub struct TxBody {
1923
/// `vote-type` field
2024
vote_type: Uuid,
25+
/// `event` field
26+
event: EventMap,
2127
/// `votes` field
2228
votes: Vec<Vote>,
2329
/// `voter-data` field
@@ -35,6 +41,19 @@ pub struct Vote {
3541
prop_id: PropId,
3642
}
3743

44+
/// A CBOR map
45+
#[derive(Debug, Clone, PartialEq, Eq)]
46+
pub struct EventMap(Vec<(EventKey, Vec<u8>)>);
47+
48+
/// An `event-key` type definition.
49+
#[derive(Debug, Clone, PartialEq, Eq)]
50+
pub enum EventKey {
51+
/// CBOR `int` type
52+
Int(Int),
53+
/// CBOR `text` type
54+
Text(String),
55+
}
56+
3857
/// A UUID struct.
3958
#[derive(Debug, Clone, PartialEq, Eq)]
4059
pub struct Uuid(Vec<u8>);
@@ -55,6 +74,24 @@ pub struct Proof(Vec<u8>);
5574
#[derive(Debug, Clone, PartialEq, Eq)]
5675
pub struct PropId(Vec<u8>);
5776

77+
impl GeneralizedTx {
78+
/// Creates a new `GeneralizedTx` struct.
79+
#[must_use]
80+
pub fn new(tx_body: TxBody) -> Self {
81+
let signature = coset::CoseSignBuilder::new()
82+
.protected(Self::cose_protected_header())
83+
.build();
84+
Self { tx_body, signature }
85+
}
86+
87+
/// Returns the COSE protected header.
88+
fn cose_protected_header() -> coset::Header {
89+
coset::HeaderBuilder::new()
90+
.content_format(coset::iana::CoapContentFormat::Cbor)
91+
.build()
92+
}
93+
}
94+
5895
/// Cbor encodable and decodable type trait.
5996
pub trait Cbor<'a>: Encode<()> + Decode<'a, ()> {
6097
/// Encodes to CBOR encoded bytes.

0 commit comments

Comments
 (0)