fix: Match CBOR encoding of plutus data with the haskell implementation. (#212)
* Add failing cbor rountrip test * Encode lists like haskell does * Encode plutus data bytestrings as haskell does That is: - as bytestring for up to 64 bytes length - as an indefinite bytestring made of 64 byte chunks, last one can be shorter
This commit is contained in:
parent
832e3ec777
commit
dadb39de43
2 changed files with 153 additions and 27 deletions
|
|
@ -786,6 +786,88 @@ impl fmt::Display for Bytes {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Defined to encode PlutusData bytestring as it is done in the canonical plutus implementation
|
||||||
|
#[derive(Serialize, Deserialize, Clone, Debug, PartialEq, Eq, PartialOrd, Ord)]
|
||||||
|
#[serde(into = "String")]
|
||||||
|
#[serde(try_from = "String")]
|
||||||
|
pub struct PlutusBytes(Vec<u8>);
|
||||||
|
|
||||||
|
impl From<Vec<u8>> for PlutusBytes {
|
||||||
|
fn from(xs: Vec<u8>) -> Self {
|
||||||
|
PlutusBytes(xs)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl From<PlutusBytes> for Vec<u8> {
|
||||||
|
fn from(b: PlutusBytes) -> Self {
|
||||||
|
b.0
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Deref for PlutusBytes {
|
||||||
|
type Target = Vec<u8>;
|
||||||
|
|
||||||
|
fn deref(&self) -> &Self::Target {
|
||||||
|
&self.0
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl TryFrom<String> for PlutusBytes {
|
||||||
|
type Error = hex::FromHexError;
|
||||||
|
|
||||||
|
fn try_from(value: String) -> Result<Self, Self::Error> {
|
||||||
|
let v = hex::decode(value)?;
|
||||||
|
Ok(PlutusBytes(v))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl From<PlutusBytes> for String {
|
||||||
|
fn from(b: PlutusBytes) -> Self {
|
||||||
|
hex::encode(b.deref())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl fmt::Display for PlutusBytes {
|
||||||
|
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
|
||||||
|
let bytes: Vec<u8> = self.clone().into();
|
||||||
|
|
||||||
|
f.write_str(&hex::encode(bytes))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<C> Encode<C> for PlutusBytes {
|
||||||
|
fn encode<W: minicbor::encode::Write>(
|
||||||
|
&self,
|
||||||
|
e: &mut minicbor::Encoder<W>,
|
||||||
|
_: &mut C,
|
||||||
|
) -> Result<(), minicbor::encode::Error<W::Error>> {
|
||||||
|
// we match the haskell implementation by encoding bytestrings longer than 64 bytes as indefinite lists of bytes
|
||||||
|
const CHUNK_SIZE: usize = 64;
|
||||||
|
let bs: &Vec<u8> = self.deref();
|
||||||
|
if bs.len() <= 64 {
|
||||||
|
e.bytes(bs)?;
|
||||||
|
} else {
|
||||||
|
e.begin_bytes()?;
|
||||||
|
for b in bs.chunks(CHUNK_SIZE) {
|
||||||
|
e.bytes(b)?;
|
||||||
|
}
|
||||||
|
e.end()?;
|
||||||
|
}
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<'b, C> minicbor::decode::Decode<'b, C> for PlutusBytes {
|
||||||
|
fn decode(d: &mut minicbor::Decoder<'b>, _: &mut C) -> Result<Self, minicbor::decode::Error> {
|
||||||
|
let mut res = Vec::new();
|
||||||
|
for chunk in d.bytes_iter()? {
|
||||||
|
let bs = chunk?;
|
||||||
|
res.extend_from_slice(bs);
|
||||||
|
}
|
||||||
|
Ok(PlutusBytes::from(res))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
#[derive(
|
#[derive(
|
||||||
Serialize, Deserialize, Clone, Copy, Encode, Decode, Debug, PartialEq, Eq, PartialOrd, Ord,
|
Serialize, Deserialize, Clone, Copy, Encode, Decode, Debug, PartialEq, Eq, PartialOrd, Ord,
|
||||||
)]
|
)]
|
||||||
|
|
|
||||||
|
|
@ -7,7 +7,9 @@ use serde::{Deserialize, Serialize};
|
||||||
use pallas_codec::minicbor::{data::Tag, Decode, Encode};
|
use pallas_codec::minicbor::{data::Tag, Decode, Encode};
|
||||||
use pallas_crypto::hash::Hash;
|
use pallas_crypto::hash::Hash;
|
||||||
|
|
||||||
use pallas_codec::utils::{Bytes, Int, KeepRaw, KeyValuePairs, MaybeIndefArray, Nullable};
|
use pallas_codec::utils::{
|
||||||
|
Bytes, Int, KeepRaw, KeyValuePairs, MaybeIndefArray, Nullable, PlutusBytes,
|
||||||
|
};
|
||||||
|
|
||||||
// required for derive attrs to work
|
// required for derive attrs to work
|
||||||
use pallas_codec::minicbor;
|
use pallas_codec::minicbor;
|
||||||
|
|
@ -917,7 +919,7 @@ pub enum PlutusData {
|
||||||
Constr(Constr<PlutusData>),
|
Constr(Constr<PlutusData>),
|
||||||
Map(KeyValuePairs<PlutusData, PlutusData>),
|
Map(KeyValuePairs<PlutusData, PlutusData>),
|
||||||
BigInt(BigInt),
|
BigInt(BigInt),
|
||||||
BoundedBytes(Bytes),
|
BoundedBytes(PlutusBytes),
|
||||||
Array(Vec<PlutusData>),
|
Array(Vec<PlutusData>),
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -960,7 +962,7 @@ impl<'b, C> minicbor::decode::Decode<'b, C> for PlutusData {
|
||||||
full.extend(slice?);
|
full.extend(slice?);
|
||||||
}
|
}
|
||||||
|
|
||||||
Ok(Self::BoundedBytes(Bytes::from(full)))
|
Ok(Self::BoundedBytes(PlutusBytes::from(full)))
|
||||||
}
|
}
|
||||||
minicbor::data::Type::Array | minicbor::data::Type::ArrayIndef => {
|
minicbor::data::Type::Array | minicbor::data::Type::ArrayIndef => {
|
||||||
Ok(Self::Array(d.decode_with(ctx)?))
|
Ok(Self::Array(d.decode_with(ctx)?))
|
||||||
|
|
@ -973,6 +975,25 @@ impl<'b, C> minicbor::decode::Decode<'b, C> for PlutusData {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn encode_list<C, W: minicbor::encode::Write, A: minicbor::encode::Encode<C>>(
|
||||||
|
a: &Vec<A>,
|
||||||
|
e: &mut minicbor::Encoder<W>,
|
||||||
|
ctx: &mut C,
|
||||||
|
) -> Result<(), minicbor::encode::Error<W::Error>> {
|
||||||
|
// Mimics default haskell list encoding from cborg:
|
||||||
|
// We use indef array for non-empty arrays but definite 0-length array for empty
|
||||||
|
if a.is_empty() {
|
||||||
|
e.array(0)?;
|
||||||
|
} else {
|
||||||
|
e.begin_array()?;
|
||||||
|
for v in a {
|
||||||
|
e.encode_with(v, ctx)?;
|
||||||
|
}
|
||||||
|
e.end()?;
|
||||||
|
}
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
impl<C> minicbor::encode::Encode<C> for PlutusData {
|
impl<C> minicbor::encode::Encode<C> for PlutusData {
|
||||||
fn encode<W: minicbor::encode::Write>(
|
fn encode<W: minicbor::encode::Write>(
|
||||||
&self,
|
&self,
|
||||||
|
|
@ -984,13 +1005,13 @@ impl<C> minicbor::encode::Encode<C> for PlutusData {
|
||||||
e.encode_with(a, ctx)?;
|
e.encode_with(a, ctx)?;
|
||||||
}
|
}
|
||||||
Self::Map(a) => {
|
Self::Map(a) => {
|
||||||
// we use indef array by default to match the approach used by the cardano-cli
|
// we use definite array to match the approach used by haskell's plutus implementation
|
||||||
e.begin_map()?;
|
// https://github.com/input-output-hk/plutus/blob/9538fc9829426b2ecb0628d352e2d7af96ec8204/plutus-core/plutus-core/src/PlutusCore/Data.hs#L152
|
||||||
|
e.map(a.len().try_into().unwrap())?;
|
||||||
for (k, v) in a.iter() {
|
for (k, v) in a.iter() {
|
||||||
k.encode(e, ctx)?;
|
k.encode(e, ctx)?;
|
||||||
v.encode(e, ctx)?;
|
v.encode(e, ctx)?;
|
||||||
}
|
}
|
||||||
e.end()?;
|
|
||||||
}
|
}
|
||||||
Self::BigInt(a) => {
|
Self::BigInt(a) => {
|
||||||
e.encode_with(a, ctx)?;
|
e.encode_with(a, ctx)?;
|
||||||
|
|
@ -999,14 +1020,13 @@ impl<C> minicbor::encode::Encode<C> for PlutusData {
|
||||||
e.encode_with(a, ctx)?;
|
e.encode_with(a, ctx)?;
|
||||||
}
|
}
|
||||||
Self::Array(a) => {
|
Self::Array(a) => {
|
||||||
// we use indef array by default to match the approach used by the cardano-cli
|
// we use definite array for empty array or indef array otherwise to match haskell implementation
|
||||||
e.begin_array()?;
|
// https://github.com/input-output-hk/plutus/blob/9538fc9829426b2ecb0628d352e2d7af96ec8204/plutus-core/plutus-core/src/PlutusCore/Data.hs#L153
|
||||||
for v in a.iter() {
|
// default encoder for a list:
|
||||||
e.encode_with(v, ctx)?;
|
// https://github.com/well-typed/cborg/blob/4bdc818a1f0b35f38bc118a87944630043b58384/serialise/src/Codec/Serialise/Class.hs#L181
|
||||||
}
|
encode_list(a, e, ctx)?;
|
||||||
e.end()?;
|
|
||||||
}
|
}
|
||||||
}
|
};
|
||||||
|
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
@ -1066,26 +1086,21 @@ where
|
||||||
|
|
||||||
match self.tag {
|
match self.tag {
|
||||||
102 => {
|
102 => {
|
||||||
|
// definite length array here
|
||||||
|
// https://github.com/input-output-hk/plutus/blob/9538fc9829426b2ecb0628d352e2d7af96ec8204/plutus-core/plutus-core/src/PlutusCore/Data.hs#L152
|
||||||
e.array(2)?;
|
e.array(2)?;
|
||||||
e.encode_with(self.any_constructor.unwrap_or_default(), ctx)?;
|
e.encode_with(self.any_constructor.unwrap_or_default(), ctx)?;
|
||||||
|
|
||||||
// we use indef array by default to match the approach used by the cardano-cli
|
// we use definite array for empty array or indef array otherwise to match haskell implementation
|
||||||
e.begin_array()?;
|
// https://github.com/input-output-hk/plutus/blob/9538fc9829426b2ecb0628d352e2d7af96ec8204/plutus-core/plutus-core/src/PlutusCore/Data.hs#L144
|
||||||
for v in self.fields.iter() {
|
// default encoder for a list:
|
||||||
e.encode_with(v, ctx)?;
|
// https://github.com/well-typed/cborg/blob/4bdc818a1f0b35f38bc118a87944630043b58384/serialise/src/Codec/Serialise/Class.hs#L181
|
||||||
}
|
encode_list(&self.fields, e, ctx)?;
|
||||||
e.end()?;
|
|
||||||
|
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
_ => {
|
_ => {
|
||||||
// we use indef array by default to match the approach used by the cardano-cli
|
// we use definite array for empty array or indef array otherwise to match haskell implementation. See above reference.
|
||||||
e.begin_array()?;
|
encode_list(&self.fields, e, ctx)?;
|
||||||
for v in self.fields.iter() {
|
|
||||||
e.encode_with(v, ctx)?;
|
|
||||||
}
|
|
||||||
e.end()?;
|
|
||||||
|
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -1466,6 +1481,8 @@ pub struct MintedTx<'b> {
|
||||||
mod tests {
|
mod tests {
|
||||||
use pallas_codec::minicbor::{self, to_vec};
|
use pallas_codec::minicbor::{self, to_vec};
|
||||||
|
|
||||||
|
use crate::{alonzo::PlutusData, Fragment};
|
||||||
|
|
||||||
use super::{Header, MintedBlock};
|
use super::{Header, MintedBlock};
|
||||||
|
|
||||||
type BlockWrapper<'b> = (u16, MintedBlock<'b>);
|
type BlockWrapper<'b> = (u16, MintedBlock<'b>);
|
||||||
|
|
@ -1552,4 +1569,31 @@ mod tests {
|
||||||
assert!(bytes.eq(&bytes2), "re-encoded bytes didn't match original");
|
assert!(bytes.eq(&bytes2), "re-encoded bytes didn't match original");
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn plutus_data_isomorphic_decoding_encoding() {
|
||||||
|
let datas = [
|
||||||
|
// unit = Constr 0 []
|
||||||
|
"d87980",
|
||||||
|
// pltmap = Map [(I 1, unit), (I 2, pltlist)]
|
||||||
|
"a201d87980029f000102ff",
|
||||||
|
// pltlist = List [I 0, I 1, I 2]
|
||||||
|
"9f000102ff",
|
||||||
|
// Constr 5 [pltmap, Constr 5 [Map [(pltmap, toData True), (pltlist, pltmap), (List [], List [I 1])], unit, toData (0, 1)]]
|
||||||
|
"d87e9fa201d87980029f000102ffd87e9fa3a201d87980029f000102ffd87a809f000102ffa201d87980029f000102ff809f01ffd87980d8799f0001ffffff",
|
||||||
|
// Constr 5 [List [], List [I 1], Map [], Map [(I 1, unit), (I 2, Constr 2 [I 2])]]
|
||||||
|
"d87e9f809f01ffa0a201d8798002d87b9f02ffff",
|
||||||
|
// B (B.replicate 32 105)
|
||||||
|
"58206969696969696969696969696969696969696969696969696969696969696969",
|
||||||
|
// B (B.replicate 67 105)
|
||||||
|
"5f58406969696969696969696969696969696969696969696969696969696969696969696969696969696969696969696969696969696969696969696969696969696943696969ff",
|
||||||
|
// B B.empty
|
||||||
|
"40"
|
||||||
|
];
|
||||||
|
for data_hex in datas {
|
||||||
|
let data_bytes = hex::decode(data_hex).unwrap();
|
||||||
|
let data = PlutusData::decode_fragment(&data_bytes).unwrap();
|
||||||
|
assert_eq!(data.encode_fragment().unwrap(), data_bytes);
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue