Skip to main content

amaru_kernel/cardano/memoized/
native_script.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, KeepRaw, NativeScript, cbor, from_cbor, utils::string::blanket_try_from_hex_bytes};
16
17#[derive(Debug, Clone, PartialEq, Eq, serde::Deserialize, serde::Serialize)]
18#[serde(try_from = "&str")]
19pub struct MemoizedNativeScript {
20    original_bytes: Bytes,
21    // NOTE: This field isn't meant to be public, nor should we create any direct mutable
22    // references to it. Reason being that this object is mostly meant to be read-only, and any
23    // change to the 'expr' should be reflected onto the 'original_bytes'.
24    expr: NativeScript,
25}
26
27impl MemoizedNativeScript {
28    pub fn original_bytes(&self) -> &[u8] {
29        &self.original_bytes
30    }
31}
32
33impl AsRef<NativeScript> for MemoizedNativeScript {
34    fn as_ref(&self) -> &NativeScript {
35        &self.expr
36    }
37}
38
39impl TryFrom<Bytes> for MemoizedNativeScript {
40    type Error = String;
41
42    fn try_from(original_bytes: Bytes) -> Result<Self, Self::Error> {
43        let expr = from_cbor(&original_bytes).ok_or_else(|| "invalid serialized native script".to_string())?;
44
45        Ok(Self { original_bytes, expr })
46    }
47}
48
49impl From<KeepRaw<'_, NativeScript>> for MemoizedNativeScript {
50    fn from(script: KeepRaw<'_, NativeScript>) -> Self {
51        Self { original_bytes: Bytes::from(script.raw_cbor().to_vec()), expr: script.unwrap() }
52    }
53}
54
55impl TryFrom<&str> for MemoizedNativeScript {
56    type Error = String;
57
58    fn try_from(s: &str) -> Result<Self, Self::Error> {
59        blanket_try_from_hex_bytes(s, |original_bytes, expr| Self { original_bytes, expr })
60    }
61}
62
63impl TryFrom<String> for MemoizedNativeScript {
64    type Error = String;
65
66    fn try_from(s: String) -> Result<Self, Self::Error> {
67        Self::try_from(s.as_str())
68    }
69}
70
71impl<'b, C> cbor::Decode<'b, C> for MemoizedNativeScript {
72    fn decode(d: &mut cbor::Decoder<'b>, ctx: &mut C) -> Result<Self, cbor::decode::Error> {
73        let start_pos = d.position();
74        let expr: NativeScript = d.decode_with(ctx)?;
75        let end_pos = d.position();
76        let original_bytes = Bytes::from(d.input()[start_pos..end_pos].to_vec());
77
78        Ok(Self { original_bytes, expr })
79    }
80}
81
82impl<C> cbor::Encode<C> for MemoizedNativeScript {
83    fn encode<W: cbor::encode::Write>(
84        &self,
85        e: &mut cbor::Encoder<W>,
86        _ctx: &mut C,
87    ) -> Result<(), cbor::encode::Error<W::Error>> {
88        e.writer_mut().write_all(&self.original_bytes[..]).map_err(cbor::encode::Error::write)
89    }
90}
91
92#[cfg(test)]
93mod tests {
94    use pallas_primitives::conway as pallas;
95    use proptest::prelude::*;
96
97    use super::*;
98    use crate::{Hash, MaybeIndefArray, cbor, size::KEY, to_cbor};
99
100    // NOTE: Not using Pallas' type because (a) it has a serialization bug we still need to fix
101    // and, (b) it doesn't let us encode native script using unusual encoding choices (e.g. indef
102    // vs def arrays).
103    #[derive(Debug, Clone)]
104    enum NativeScript {
105        ScriptPubkey(Hash<KEY>),
106        ScriptAll(MaybeIndefArray<NativeScript>),
107        ScriptAny(MaybeIndefArray<NativeScript>),
108        ScriptNOfK(u32, MaybeIndefArray<NativeScript>),
109        InvalidBefore(u64),
110        InvalidHereafter(u64),
111    }
112
113    impl From<NativeScript> for pallas::NativeScript {
114        fn from(script: NativeScript) -> Self {
115            match script {
116                NativeScript::ScriptPubkey(sig) => Self::ScriptPubkey(sig),
117                NativeScript::ScriptAll(sigs) => Self::ScriptAll(sigs.to_vec().into_iter().map(|s| s.into()).collect()),
118                NativeScript::ScriptAny(sigs) => Self::ScriptAny(sigs.to_vec().into_iter().map(|s| s.into()).collect()),
119                NativeScript::ScriptNOfK(n, sigs) => {
120                    Self::ScriptNOfK(n, sigs.to_vec().into_iter().map(|s| s.into()).collect())
121                }
122                NativeScript::InvalidBefore(n) => Self::InvalidBefore(n),
123                NativeScript::InvalidHereafter(n) => Self::InvalidHereafter(n),
124            }
125        }
126    }
127
128    impl<C> cbor::encode::Encode<C> for NativeScript {
129        fn encode<W: cbor::encode::Write>(
130            &self,
131            e: &mut cbor::Encoder<W>,
132            ctx: &mut C,
133        ) -> Result<(), cbor::encode::Error<W::Error>> {
134            match self {
135                Self::ScriptPubkey(sig) => {
136                    e.array(2)?;
137                    e.encode_with(0, ctx)?;
138                    e.encode_with(sig, ctx)?;
139                }
140                Self::ScriptAll(sigs) => {
141                    e.array(2)?;
142                    e.encode_with(1, ctx)?;
143                    e.encode_with(sigs, ctx)?;
144                }
145                Self::ScriptAny(sigs) => {
146                    e.array(2)?;
147                    e.encode_with(2, ctx)?;
148                    e.encode_with(sigs, ctx)?;
149                }
150                Self::ScriptNOfK(n, sigs) => {
151                    e.array(3)?;
152                    e.encode_with(3, ctx)?;
153                    e.encode_with(n, ctx)?;
154                    e.encode_with(sigs, ctx)?;
155                }
156                Self::InvalidBefore(n) => {
157                    e.array(2)?;
158                    e.encode_with(4, ctx)?;
159                    e.encode_with(n, ctx)?;
160                }
161                Self::InvalidHereafter(n) => {
162                    e.array(2)?;
163                    e.encode_with(5, ctx)?;
164                    e.encode_with(n, ctx)?;
165                }
166            };
167
168            Ok(())
169        }
170    }
171
172    prop_compose! {
173        pub(crate) fn any_key_hash()(bytes in any::<[u8; 28]>()) -> Hash<28> {
174            Hash::from(bytes)
175        }
176    }
177
178    fn any_native_script(depth: u8) -> BoxedStrategy<NativeScript> {
179        let sig = any_key_hash().prop_map(NativeScript::ScriptPubkey);
180        let before = any::<u64>().prop_map(NativeScript::InvalidBefore);
181        let after = any::<u64>().prop_map(NativeScript::InvalidHereafter);
182        if depth > 0 {
183            let all = (any::<bool>(), prop::collection::vec(any_native_script(depth - 1), 0..depth as usize)).prop_map(
184                |(is_def, sigs)| {
185                    NativeScript::ScriptAll(if is_def {
186                        MaybeIndefArray::Def(sigs)
187                    } else {
188                        MaybeIndefArray::Indef(sigs)
189                    })
190                },
191            );
192
193            let some = (any::<bool>(), prop::collection::vec(any_native_script(depth - 1), 0..depth as usize))
194                .prop_map(|(is_def, sigs)| {
195                    NativeScript::ScriptAny(if is_def {
196                        MaybeIndefArray::Def(sigs)
197                    } else {
198                        MaybeIndefArray::Indef(sigs)
199                    })
200                });
201
202            let n_of_k =
203                (any::<bool>(), any::<u32>(), prop::collection::vec(any_native_script(depth - 1), 0..depth as usize))
204                    .prop_map(|(is_def, n, sigs)| {
205                        NativeScript::ScriptNOfK(
206                            n,
207                            if is_def { MaybeIndefArray::Def(sigs) } else { MaybeIndefArray::Indef(sigs) },
208                        )
209                    });
210
211            prop_oneof![sig, before, after, all, some, n_of_k,].boxed()
212        } else {
213            prop_oneof![sig, before, after].boxed()
214        }
215    }
216
217    proptest! {
218        #[test]
219        fn roundtrip_hex_encoded_str(original_script in any_native_script(3)) {
220            let original_bytes = to_cbor(&original_script);
221            let result = MemoizedNativeScript::try_from(hex::encode(&original_bytes)).unwrap();
222
223            assert_eq!(result.as_ref(), &pallas::NativeScript::from(original_script));
224            assert_eq!(result.original_bytes(), &original_bytes);
225        }
226    }
227
228    proptest! {
229        #[test]
230        fn roundtrip_cbor(original_script in any_native_script(3)) {
231            let original_bytes = to_cbor(&original_script);
232            let raw: KeepRaw<'_, pallas::NativeScript> = cbor::decode(&original_bytes).unwrap();
233            let result: MemoizedNativeScript = raw.into();
234
235            assert_eq!(result.as_ref(), &pallas::NativeScript::from(original_script));
236            assert_eq!(result.original_bytes(), &original_bytes);
237        }
238    }
239}