plutus/
cbor.rs

1use num_bigint::{BigInt, Sign};
2
3extern crate alloc;
4
5use crate::{Datum, Datum::*, MapDatumEntry};
6
7use minicbor::{
8	data::{IanaTag, Tag},
9	encode,
10	encode::{Encoder, Write},
11};
12
13impl<C> encode::Encode<C> for Datum {
14	fn encode<W: Write>(
15		&self,
16		e: &mut Encoder<W>,
17		ctx: &mut C,
18	) -> Result<(), encode::Error<W::Error>> {
19		encode_datum(self, e, ctx)
20	}
21}
22
23fn encode_datum<C, W: Write>(
24	datum: &Datum,
25	e: &mut Encoder<W>,
26	ctx: &mut C,
27) -> Result<(), encode::Error<W::Error>> {
28	match datum {
29		IntegerDatum(bi) => encode_integer(bi, e, ctx),
30		ByteStringDatum(bytes) => encode_bytestring(bytes, e, ctx),
31		ConstructorDatum { constructor, fields } => {
32			encode_constructor(*constructor, fields, e, ctx)
33		},
34		ListDatum(items) => encode_linear_seq(items, e, ctx),
35		MapDatum(entries) => encode_entries(entries, e, ctx),
36	}
37}
38
39fn encode_integer<W: Write, C>(
40	i: &BigInt,
41	e: &mut Encoder<W>,
42	ctx: &mut C,
43) -> Result<(), encode::Error<W::Error>> {
44	match i64::try_from(i) {
45		Ok(num) => {
46			e.i64(num)?;
47			Ok(())
48		},
49		_ => {
50			let (sign, bytes) = i.to_bytes_be();
51			match sign {
52				Sign::Plus => e.tag(IanaTag::PosBignum)?,
53				Sign::Minus => e.tag(IanaTag::NegBignum)?,
54				Sign::NoSign => unreachable!("NoSign is not possible here, since 0 is a valid i64"),
55			};
56			encode_bytestring(&bytes, e, ctx)
57		},
58	}
59}
60
61const BYTES_CHUNK_SIZE_LIMIT: usize = 64;
62
63fn encode_bytestring<W: Write, C>(
64	bytes: &[u8],
65	e: &mut Encoder<W>,
66	_ctx: &mut C,
67) -> Result<(), encode::Error<W::Error>> {
68	if bytes.len() <= BYTES_CHUNK_SIZE_LIMIT {
69		e.bytes(bytes)?;
70		Ok(())
71	} else {
72		e.begin_bytes()?;
73		for chunk in bytes.chunks(BYTES_CHUNK_SIZE_LIMIT) {
74			e.bytes(chunk)?;
75		}
76		e.end()?;
77		Ok(())
78	}
79}
80
81/// The only specification is the plutus-core source code
82/// Please refer to: https://github.com/input-output-hk/plutus/blob/ab4d2cc43b642d8ddc43180d1ca1c40937b1e629/plutus-core/plutus-core/src/PlutusCore/Data.hs#L71
83fn encode_constructor<W: Write, C>(
84	constructor: u64,
85	fields: &[Datum],
86	e: &mut Encoder<W>,
87	ctx: &mut C,
88) -> Result<(), encode::Error<W::Error>> {
89	if constructor < 7 {
90		e.tag(constructor_small_tag(constructor))?;
91		encode_linear_seq(fields, e, ctx)
92	} else if (7..128).contains(&constructor) {
93		e.tag(constructor_big_tag(constructor))?;
94		encode_linear_seq(fields, e, ctx)
95	} else {
96		e.tag(CONSTRUCTOR_OVER_LIMIT_TAG)?;
97		encode_constructor_over_limit_fields(constructor, fields, e, ctx)
98	}
99}
100
101fn encode_linear_seq<W: Write, C>(
102	ds: &[Datum],
103	e: &mut Encoder<W>,
104	ctx: &mut C,
105) -> Result<(), encode::Error<W::Error>> {
106	//Do it like Plutus
107	// "spec": https://github.com/input-output-hk/plutus/blob/ab4d2cc43b642d8ddc43180d1ca1c40937b1e629/plutus-core/plutus-core/src/PlutusCore/Data.hs#L71
108	if ds.is_empty() {
109		e.array(0)?;
110	} else {
111		e.begin_array()?;
112		for d in ds {
113			encode_datum(d, e, ctx)?;
114		}
115		e.end()?;
116	}
117	Ok(())
118}
119
120fn encode_constructor_over_limit_fields<W: Write, C>(
121	constructor: u64,
122	ds: &[Datum],
123	e: &mut Encoder<W>,
124	ctx: &mut C,
125) -> Result<(), encode::Error<W::Error>> {
126	//This is two elements array where the first item is constructor value and the second is encoded sequence of datums
127	e.array(2)?;
128	e.u64(constructor)?;
129	encode_linear_seq(ds, e, ctx)
130}
131
132fn encode_entries<W: Write, C>(
133	entries: &[MapDatumEntry],
134	e: &mut Encoder<W>,
135	ctx: &mut C,
136) -> Result<(), encode::Error<W::Error>> {
137	//Do it like Plutus
138	let len = entries.len();
139	e.map(len as u64)?;
140	for entry in entries {
141		encode_datum(&entry.key, e, ctx)?;
142		encode_datum(&entry.value, e, ctx)?;
143	}
144	Ok(())
145}
146
147fn constructor_small_tag(value: u64) -> Tag {
148	Tag::new(value + 121)
149}
150
151fn constructor_big_tag(value: u64) -> Tag {
152	Tag::new(1280 + value - 7)
153}
154
155const CONSTRUCTOR_OVER_LIMIT_TAG: Tag = Tag::new(102);
156
157#[cfg(test)]
158mod tests {
159	use crate::{Datum, Datum::*, MapDatumEntry};
160	pub use alloc::{vec, vec::Vec};
161	use num_bigint::BigInt;
162	use num_traits::Num;
163
164	#[test]
165	fn integer_datums_to_cbor() {
166		let small_int_narrow = IntegerDatum(BigInt::from(7u8));
167		let small_int_wide = IntegerDatum(BigInt::from(7i64));
168		let small_int_negative = IntegerDatum(BigInt::from(-9i64));
169		let big_int_positive =
170			IntegerDatum(BigInt::from_str_radix("123456789123456789", 10).unwrap());
171		let big_int_negative =
172			IntegerDatum(BigInt::from_str_radix("-123456789123456789", 10).unwrap());
173
174		test(small_int_narrow, "07");
175		test(small_int_wide, "07");
176		test(small_int_negative, "28");
177		test(big_int_positive, "1b01b69b4bacd05f15");
178		test(big_int_negative, "3b01b69b4bacd05f14");
179	}
180
181	#[test]
182	fn bytestring_datums_to_cbor() {
183		let empty = ByteStringDatum(Vec::new());
184		let shorter_than_64 = ByteStringDatum(Vec::from([65u8; 10]));
185		let just_64 = ByteStringDatum(Vec::from([66u8; 64]));
186		let longer_than_64 = ByteStringDatum(Vec::from([67u8; 65]));
187
188		test(empty, "40");
189		test(shorter_than_64, "4a41414141414141414141");
190		test(
191			just_64,
192			"584042424242424242424242424242424242424242424242424242424242424242424242424242424242424242424242424242424242424242424242424242424242",
193		);
194		test(
195			longer_than_64,
196			"5f5840434343434343434343434343434343434343434343434343434343434343434343434343434343434343434343434343434343434343434343434343434343434143ff",
197		);
198	}
199
200	#[test]
201	fn constructor_datums_to_cbor() {
202		fn make_fields(num: u64) -> Vec<Datum> {
203			let mut v = Vec::new();
204			for i in 1..(num + 1) {
205				v.push(IntegerDatum(BigInt::from(i)))
206			}
207			v
208		}
209
210		let small_empty = ConstructorDatum { constructor: 2, fields: Vec::new() };
211		let small_non_empty = ConstructorDatum { constructor: 2, fields: make_fields(3) };
212		let big_empty = ConstructorDatum { constructor: 34, fields: Vec::new() };
213		let big_non_empty = ConstructorDatum { constructor: 34, fields: make_fields(2) };
214		let over_limit_empty = ConstructorDatum { constructor: 130, fields: Vec::new() };
215		let over_limit_non_empty = ConstructorDatum { constructor: 130, fields: make_fields(4) };
216
217		test(small_empty, "d87b80");
218		test(small_non_empty, "d87b9f010203ff");
219		test(big_empty, "d9051b80");
220		test(big_non_empty, "d9051b9f0102ff");
221		test(over_limit_empty, "d86682188280");
222		test(over_limit_non_empty, "d8668218829f01020304ff");
223	}
224
225	#[test]
226	fn list_datums_to_cbor() {
227		test(ListDatum(vec![]), "80");
228		test(ListDatum(vec![ByteStringDatum(hex::decode("deadbeef").unwrap())]), "9f44deadbeefff");
229		test(
230			ListDatum(vec![
231				ByteStringDatum(hex::decode("deadbeef").unwrap()),
232				Datum::integer(13),
233				ListDatum(vec![
234					Datum::integer(42),
235					ByteStringDatum(hex::decode("0102030405").unwrap()),
236				]),
237			]),
238			"9f44deadbeef0d9f182a450102030405ffff",
239		);
240	}
241
242	#[test]
243	fn map_datums_to_cbor() {
244		test(MapDatum(vec![]), "a0");
245		test(
246			MapDatum(vec![
247				MapDatumEntry { key: Datum::integer(99), value: Datum::integer(101) },
248				MapDatumEntry {
249					key: ByteStringDatum(hex::decode("deadbeef").unwrap()),
250					value: Datum::integer(3),
251				},
252			]),
253			"a21863186544deadbeef03",
254		);
255		test(
256			MapDatum(
257				(1..14)
258					.map(|i| MapDatumEntry { key: Datum::integer(i), value: Datum::integer(1) })
259					.collect(),
260			),
261			"ad0101020103010401050106010701080109010a010b010c010d01",
262		);
263	}
264
265	fn test(d: Datum, expected_hex: &str) {
266		assert_eq!(hex::encode(minicbor::to_vec(d).unwrap()), expected_hex);
267	}
268}