Skip to main content

amaru_kernel/cardano/memoized/
plutus_data.rs

1// Copyright 2025 PRAGMA
2//
3// Licensed under the Apache License, Version 2.0 (the "License");
4// you may not use this file except in compliance with the License.
5// You may obtain a copy of the License at
6//
7//     http://www.apache.org/licenses/LICENSE-2.0
8//
9// Unless required by applicable law or agreed to in writing, software
10// distributed under the License is distributed on an "AS IS" BASIS,
11// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12// See the License for the specific language governing permissions and
13// limitations under the License.
14
15use crate::{Bytes, Hash, Hasher, KeepRaw, PlutusData, cbor, utils::string::blanket_try_from_hex_bytes};
16
17#[derive(Debug, Clone, PartialEq, Eq, serde::Deserialize, serde::Serialize)]
18#[serde(try_from = "&str")]
19#[serde(into = "String")]
20pub struct MemoizedPlutusData {
21    original_bytes: Bytes,
22    // NOTE: This field isn't meant to be public, nor should we create any direct mutable
23    // references to it. Reason being that this object is mostly meant to be read-only, and any
24    // change to the 'data' should be reflected onto the 'original_bytes'.
25    data: PlutusData,
26}
27
28impl MemoizedPlutusData {
29    pub fn new(data: PlutusData) -> Result<Self, String> {
30        let mut buf = Vec::new();
31        cbor::encode(&data, &mut buf).map_err(|_| "failed to encode PlutusData".to_string())?;
32
33        Ok(Self { original_bytes: Bytes::from(buf), data })
34    }
35
36    pub fn original_bytes(&self) -> &[u8] {
37        &self.original_bytes
38    }
39
40    pub fn hash(&self) -> Hash<32> {
41        Hasher::<256>::hash(&self.original_bytes)
42    }
43}
44
45impl AsRef<PlutusData> for MemoizedPlutusData {
46    fn as_ref(&self) -> &PlutusData {
47        &self.data
48    }
49}
50
51impl From<MemoizedPlutusData> for String {
52    fn from(plutus_data: MemoizedPlutusData) -> Self {
53        hex::encode(&plutus_data.original_bytes[..])
54    }
55}
56
57impl<'b> From<KeepRaw<'b, PlutusData>> for MemoizedPlutusData {
58    fn from(data: KeepRaw<'b, PlutusData>) -> Self {
59        Self { original_bytes: Bytes::from(data.raw_cbor().to_vec()), data: data.unwrap() }
60    }
61}
62
63impl TryFrom<&str> for MemoizedPlutusData {
64    type Error = String;
65
66    fn try_from(s: &str) -> Result<Self, Self::Error> {
67        blanket_try_from_hex_bytes(s, |original_bytes, data| Self { original_bytes, data })
68    }
69}
70
71impl TryFrom<String> for MemoizedPlutusData {
72    type Error = String;
73
74    fn try_from(s: String) -> Result<Self, Self::Error> {
75        Self::try_from(s.as_str())
76    }
77}
78
79impl TryFrom<Vec<u8>> for MemoizedPlutusData {
80    type Error = cbor::decode::Error;
81
82    fn try_from(bytes: Vec<u8>) -> Result<Self, Self::Error> {
83        let data = cbor::decode(&bytes)?;
84        Ok(Self { original_bytes: Bytes::from(bytes), data })
85    }
86}
87
88impl<'b, C> cbor::Decode<'b, C> for MemoizedPlutusData {
89    fn decode(d: &mut cbor::Decoder<'b>, ctx: &mut C) -> Result<Self, cbor::decode::Error> {
90        let keep_raw: KeepRaw<'b, PlutusData> = d.decode_with(ctx)?;
91        Ok(Self::from(keep_raw))
92    }
93}
94
95impl<C> cbor::Encode<C> for MemoizedPlutusData {
96    fn encode<W: cbor::encode::Write>(
97        &self,
98        e: &mut cbor::Encoder<W>,
99        _ctx: &mut C,
100    ) -> Result<(), cbor::encode::Error<W::Error>> {
101        e.writer_mut().write_all(&self.original_bytes[..]).map_err(cbor::encode::Error::write)
102    }
103}
104
105#[cfg(test)]
106pub(crate) mod tests {
107    use pallas_primitives::{self as pallas, BigInt, BoundedBytes, KeyValuePairs};
108    use proptest::{prelude::*, strategy::Just};
109
110    use super::*;
111    use crate::{Constr, MaybeIndefArray, to_cbor};
112
113    // NOTE: We do not use Pallas' PlutusData because it doesn't respect the
114    // encoding expressed by the types for Map, but forces definite encoding.
115    #[derive(Debug, Clone)]
116    enum PlutusData {
117        Constr(Constr<PlutusData>),
118        Map(KeyValuePairs<PlutusData, PlutusData>),
119        BigInt(BigInt),
120        BoundedBytes(BoundedBytes),
121        Array(MaybeIndefArray<PlutusData>),
122    }
123    impl From<PlutusData> for pallas::PlutusData {
124        fn from(data: PlutusData) -> Self {
125            match data {
126                PlutusData::BigInt(i) => Self::BigInt(i),
127                PlutusData::BoundedBytes(i) => Self::BoundedBytes(i),
128                PlutusData::Array(xs) => Self::Array(match xs {
129                    MaybeIndefArray::Def(xs) => MaybeIndefArray::Def(xs.into_iter().map(|x| x.into()).collect()),
130                    MaybeIndefArray::Indef(xs) => MaybeIndefArray::Indef(xs.into_iter().map(|x| x.into()).collect()),
131                }),
132                PlutusData::Map(xs) => Self::Map(match xs {
133                    KeyValuePairs::Def(xs) => {
134                        KeyValuePairs::Def(xs.into_iter().map(|(k, v)| (k.into(), v.into())).collect())
135                    }
136                    KeyValuePairs::Indef(xs) => {
137                        KeyValuePairs::Indef(xs.into_iter().map(|(k, v)| (k.into(), v.into())).collect())
138                    }
139                }),
140                PlutusData::Constr(Constr { tag, any_constructor, fields }) => Self::Constr(Constr {
141                    tag,
142                    any_constructor,
143                    fields: match fields {
144                        MaybeIndefArray::Def(xs) => MaybeIndefArray::Def(xs.into_iter().map(|x| x.into()).collect()),
145                        MaybeIndefArray::Indef(xs) => {
146                            MaybeIndefArray::Indef(xs.into_iter().map(|x| x.into()).collect())
147                        }
148                    },
149                }),
150            }
151        }
152    }
153
154    impl<C> cbor::encode::Encode<C> for PlutusData {
155        fn encode<W: cbor::encode::Write>(
156            &self,
157            e: &mut cbor::Encoder<W>,
158            ctx: &mut C,
159        ) -> Result<(), cbor::encode::Error<W::Error>> {
160            match self {
161                Self::Constr(a) => {
162                    e.encode_with(a, ctx)?;
163                }
164                Self::Map(a) => {
165                    e.encode_with(a, ctx)?;
166                }
167                Self::BigInt(a) => {
168                    e.encode_with(a, ctx)?;
169                }
170                Self::BoundedBytes(a) => {
171                    e.encode_with(a, ctx)?;
172                }
173                Self::Array(a) => {
174                    e.encode_with(a, ctx)?;
175                }
176            };
177
178            Ok(())
179        }
180    }
181
182    prop_compose! {
183        pub(crate) fn any_bounded_bytes()(
184            bytes in any::<Vec<u8>>(),
185        ) -> BoundedBytes {
186            BoundedBytes::from(bytes)
187        }
188    }
189
190    pub(crate) fn any_bigint() -> impl Strategy<Value = BigInt> {
191        prop_oneof![
192            any::<i64>().prop_map(|i| BigInt::Int(i.into())),
193            any_bounded_bytes().prop_map(BigInt::BigUInt),
194            any_bounded_bytes().prop_map(BigInt::BigNInt),
195        ]
196    }
197
198    fn any_constr(depth: u8) -> impl Strategy<Value = Constr<PlutusData>> {
199        let any_constr_tag = prop_oneof![
200            (Just(102), any::<u64>().prop_map(Some)),
201            (121_u64..=127, Just(None)),
202            (1280_u64..=1400, Just(None))
203        ];
204
205        let any_fields = prop::collection::vec(any_plutus_data(depth - 1), 0..depth as usize);
206
207        (any_constr_tag, any_fields, any::<bool>()).prop_map(|((tag, any_constructor), fields, is_def)| Constr {
208            tag,
209            any_constructor,
210            fields: if is_def { MaybeIndefArray::Def(fields) } else { MaybeIndefArray::Indef(fields) },
211        })
212    }
213
214    fn any_plutus_data(depth: u8) -> BoxedStrategy<PlutusData> {
215        let int = any_bigint().prop_map(PlutusData::BigInt);
216
217        let bytes = any_bounded_bytes().prop_map(PlutusData::BoundedBytes);
218
219        if depth > 0 {
220            let constr = any_constr(depth).prop_map(PlutusData::Constr);
221
222            let array = (any::<bool>(), prop::collection::vec(any_plutus_data(depth - 1), 0..depth as usize)).prop_map(
223                |(is_def, xs)| {
224                    PlutusData::Array(if is_def { MaybeIndefArray::Def(xs) } else { MaybeIndefArray::Indef(xs) })
225                },
226            );
227
228            let map = (
229                any::<bool>(),
230                prop::collection::vec((any_plutus_data(depth - 1), any_plutus_data(depth - 1)), 0..depth as usize),
231            )
232                .prop_map(|(is_def, kvs)| {
233                    PlutusData::Map(if is_def { KeyValuePairs::Def(kvs) } else { KeyValuePairs::Indef(kvs) })
234                });
235
236            prop_oneof![int, bytes, constr, array, map].boxed()
237        } else {
238            prop_oneof![int, bytes].boxed()
239        }
240    }
241
242    proptest! {
243        #[test]
244        fn roundtrip_hex_encoded_str(original_data in any_plutus_data(3)) {
245            let original_bytes = to_cbor(&original_data);
246            let result = MemoizedPlutusData::try_from(hex::encode(&original_bytes)).unwrap();
247
248            assert_eq!(result.as_ref(), &pallas::PlutusData::from(original_data));
249            assert_eq!(result.original_bytes(), &original_bytes);
250        }
251    }
252
253    proptest! {
254        #[test]
255        fn roundtrip_cbor(original_data in any_plutus_data(3)) {
256            let original_bytes = to_cbor(&original_data);
257            let raw: KeepRaw<'_, pallas::PlutusData> = cbor::decode(&original_bytes).unwrap();
258            let result: MemoizedPlutusData = raw.into();
259
260            assert_eq!(result.as_ref(), &pallas::PlutusData::from(original_data));
261            assert_eq!(result.original_bytes(), &original_bytes);
262        }
263    }
264
265    #[test]
266    fn invalid_string() {
267        assert!(MemoizedPlutusData::try_from("foo".to_string()).is_err());
268    }
269}