Skip to main content

pallas_primitives/alonzo/
native_script.rs

1//! The Shelley native script, unchanged through Conway. Everything about
2//! its lifecycle lives in [`crate::native_script`].
3
4use super::NativeScript;
5use crate::native_script::impl_native_script;
6
7impl_native_script!(NativeScript);
8
9#[cfg(test)]
10mod tests {
11    use super::*;
12    use pallas_codec::minicbor::{self, Decoder, Encoder};
13    use proptest::prelude::*;
14
15    fn scripts() -> impl Strategy<Value = NativeScript> {
16        prop_oneof![
17            any::<[u8; 28]>().prop_map(|x| NativeScript::ScriptPubkey(x.into())),
18            any::<u64>().prop_map(NativeScript::InvalidBefore),
19            any::<u64>().prop_map(NativeScript::InvalidHereafter),
20        ]
21        .prop_recursive(4, 64, 8, |inner| {
22            prop_oneof![
23                prop::collection::vec(inner.clone(), 0..5).prop_map(NativeScript::ScriptAll),
24                prop::collection::vec(inner.clone(), 0..5).prop_map(NativeScript::ScriptAny),
25                (
26                    prop_oneof![
27                        Just(i64::MIN),
28                        Just(-1i64),
29                        Just(0i64),
30                        Just(i64::MAX),
31                        any::<i64>(),
32                    ],
33                    prop::collection::vec(inner, 0..5),
34                )
35                    .prop_map(|(n, xs)| NativeScript::ScriptNOfK(n, xs)),
36            ]
37        })
38    }
39
40    proptest! {
41        #[test]
42        fn round_trips_through_cbor(script in scripts()) {
43            let bytes = minicbor::to_vec(&script).unwrap();
44            let decoded: NativeScript = minicbor::decode(&bytes).unwrap();
45            prop_assert!(decoded == script);
46            prop_assert_eq!(minicbor::to_vec(&decoded).unwrap(), bytes);
47            prop_assert!(script.clone() == script);
48        }
49    }
50
51    #[test]
52    fn matches_ledger_cddl_wire_format() {
53        let key = [0xab; 28];
54        let key_hex = "ab".repeat(28);
55        let cases = [
56            (
57                NativeScript::ScriptPubkey(key.into()),
58                format!("8200581c{key_hex}"),
59            ),
60            (
61                NativeScript::ScriptAll(vec![
62                    NativeScript::InvalidBefore(1),
63                    NativeScript::InvalidHereafter(2),
64                ]),
65                "820182820401820502".to_string(),
66            ),
67            (NativeScript::ScriptAny(vec![]), "820280".to_string()),
68            (
69                NativeScript::ScriptNOfK(
70                    2,
71                    vec![
72                        NativeScript::ScriptPubkey(key.into()),
73                        NativeScript::ScriptAny(vec![NativeScript::InvalidBefore(0)]),
74                    ],
75                ),
76                format!("830302828200581c{key_hex}820281820400"),
77            ),
78            (NativeScript::InvalidBefore(1000), "82041903e8".to_string()),
79            (
80                NativeScript::InvalidHereafter(1 << 32),
81                "82051b0000000100000000".to_string(),
82            ),
83        ];
84        for (script, hex) in cases {
85            let bytes = hex::decode(&hex).unwrap();
86            assert_eq!(minicbor::to_vec(&script).unwrap(), bytes, "{hex}");
87            let decoded: NativeScript = minicbor::decode(&bytes).unwrap();
88            assert!(decoded == script, "{hex}");
89        }
90    }
91
92    fn deep_cbor(depth: usize) -> Vec<u8> {
93        let mut bytes = Vec::new();
94        for i in 0..depth {
95            match i % 3 {
96                0 => bytes.extend_from_slice(&[0x82, 1, 0x81]),
97                1 => bytes.extend_from_slice(&[0x82, 2, 0x81]),
98                _ => bytes.extend_from_slice(&[0x83, 3, 1, 0x81]),
99            }
100        }
101        bytes.extend_from_slice(&[0x82, 4, 0]);
102        bytes
103    }
104
105    fn small_stack(f: impl FnOnce() + Send + 'static) {
106        std::thread::Builder::new()
107            .stack_size(128 * 1024)
108            .spawn(f)
109            .unwrap()
110            .join()
111            .unwrap();
112    }
113
114    #[test]
115    fn deep_script_lifecycle_on_small_stack() {
116        small_stack(|| {
117            let bytes = deep_cbor(20_000);
118            let script: NativeScript = minicbor::decode(&bytes).unwrap();
119            let cloned = script.clone();
120            assert!(script == cloned);
121            assert_eq!(minicbor::to_vec(&cloned).unwrap(), bytes);
122            let mut different_bytes = bytes.clone();
123            *different_bytes.last_mut().unwrap() = 1;
124            let different: NativeScript = minicbor::decode(&different_bytes).unwrap();
125            assert!(script != different);
126
127            let mut short_buffer = [0u8; 16];
128            assert!(
129                Encoder::new(short_buffer.as_mut_slice())
130                    .encode(&script)
131                    .is_err()
132            );
133            // All three trees are dropped on this same small stack.
134        });
135    }
136
137    #[test]
138    fn malformed_deep_script_cleans_up_completed_children() {
139        small_stack(|| {
140            // An outer All has a fully decoded deep child followed by a
141            // malformed sibling. Error cleanup must drop that completed tree.
142            let mut bytes = vec![0x82, 1, 0x82];
143            bytes.extend(deep_cbor(20_000));
144            bytes.extend_from_slice(&[0x82, 0, 0x40]); // pubkey of incorrect size
145            assert!(minicbor::decode::<NativeScript>(&bytes).is_err());
146
147            // Also exercise cleanup of thousands of incomplete parent frames.
148            let mut bytes = deep_cbor(20_000);
149            bytes.pop();
150            assert!(minicbor::decode::<NativeScript>(&bytes).is_err());
151        });
152    }
153
154    #[test]
155    fn preserves_permissive_array_decoding() {
156        use NativeScript::*;
157        let cases = [
158            ("820180", ScriptAll(vec![]), "820180"),
159            ("82029fff", ScriptAny(vec![]), "820280"),
160            ("83030080", ScriptNOfK(0, vec![]), "83030080"),
161            (
162                "82019f820400820501ff",
163                ScriptAll(vec![InvalidBefore(0), InvalidHereafter(1)]),
164                "820182820400820501",
165            ),
166            ("830400f6", InvalidBefore(0), "820400"),
167            (
168                "830181830501f68100",
169                ScriptAll(vec![InvalidHereafter(1)]),
170                "820181820501",
171            ),
172        ];
173        for (input, expected, canonical) in cases {
174            let bytes = hex::decode(input).unwrap();
175            let mut d = Decoder::new(&bytes);
176            let script: NativeScript = d.decode().unwrap();
177            assert_eq!(d.position(), bytes.len(), "{input}");
178            assert!(script == expected, "{input}");
179            assert_eq!(
180                minicbor::to_vec(&script).unwrap(),
181                hex::decode(canonical).unwrap(),
182                "{input}"
183            );
184        }
185    }
186
187    #[test]
188    fn n_of_k_threshold_is_signed() {
189        // Preprod tx b1db2a411cb651a413840d3c8b112895a5bda2519a8ba6a372b8dd1ffc7746c2
190        // carries exactly this script, with a threshold of -1.
191        let bytes = hex::decode(
192            "830320828200581c3118644aa21ba172c82732ce80d1c94cdcb5f2e8891e1ad2645707188200581ce07caf4bf751495f75774ace30552441e4df84d141e5d1f5029cb04d",
193        )
194        .unwrap();
195        let script: NativeScript = minicbor::decode(&bytes).unwrap();
196        let NativeScript::ScriptNOfK(n, scripts) = &script else {
197            panic!("expected ScriptNOfK, got {script:?}");
198        };
199        assert_eq!(*n, -1);
200        assert_eq!(scripts.len(), 2);
201        assert_eq!(minicbor::to_vec(&script).unwrap(), bytes);
202    }
203
204    #[test]
205    fn malformed_arrays_are_rejected_without_reserving_claimed_length() {
206        for bytes in [
207            "80",
208            "8100",
209            "820301",
210            "9f0400ff", // missing fields / indefinite variant
211            "820680",
212            "822080",                 // unknown positive / negative variant
213            "82019bffffffffffffffff", // enormous truncated child list
214            "82019f820400",           // missing indefinite child-list break
215            "830181820400",           // missing trailing field after a completed child
216        ] {
217            let bytes = hex::decode(bytes).unwrap();
218            assert!(minicbor::decode::<NativeScript>(&bytes).is_err());
219        }
220    }
221
222    #[test]
223    fn equality_checks_variant_payload_order_and_arity() {
224        let a = NativeScript::InvalidBefore(1);
225        let b = NativeScript::InvalidHereafter(1);
226        assert!(a != b);
227        assert!(NativeScript::ScriptAll(vec![]) != NativeScript::ScriptAny(vec![]));
228        assert!(NativeScript::ScriptNOfK(1, vec![]) != NativeScript::ScriptNOfK(2, vec![]));
229        assert!(
230            NativeScript::ScriptAll(vec![a.clone(), b.clone()])
231                != NativeScript::ScriptAll(vec![b, a.clone()])
232        );
233        assert!(NativeScript::ScriptAll(vec![a]) != NativeScript::ScriptAll(vec![]));
234        assert!(
235            NativeScript::ScriptPubkey([0; 28].into())
236                != NativeScript::ScriptPubkey([1; 28].into())
237        );
238    }
239}