Skip to main content

pallas_primitives/alonzo/
json.rs

1use std::ops::Deref;
2
3use pallas_codec::tree::{IndexedNode, Visit, fold_tree, walk_tree};
4use serde_json::{Value, json};
5
6use crate::ToCanonicalJson;
7
8impl<A> super::Constr<A> {
9    pub fn constructor_value(&self) -> Option<u64> {
10        match self.tag {
11            121..=127 => Some(self.tag - 121),
12            1280..=1400 => Some(self.tag - 1280 + 7),
13            102 => self.any_constructor,
14            _ => None,
15        }
16    }
17}
18
19/// An object whose values are moved in. `json!` would re-serialize them,
20/// which recurses through the whole subtree.
21fn object(entries: impl IntoIterator<Item = (&'static str, Value)>) -> Value {
22    let mut map = serde_json::Map::new();
23    for (key, value) in entries {
24        map.insert(key.to_string(), value);
25    }
26    Value::Object(map)
27}
28
29/// The JSON of a leaf datum, shared by both renderings.
30fn plutus_leaf_json(x: &super::PlutusData) -> Option<serde_json::Value> {
31    let value = match x {
32        super::PlutusData::BigInt(int) => match int {
33            super::BigInt::Int(n) => match i64::try_from(*n.deref()) {
34                Ok(x) => json!({ "int": x }),
35                Err(_) => {
36                    json!({ "bignint": hex::encode(i128::from(*n.deref()).to_be_bytes()) })
37                }
38            },
39            // WARNING / TODO: the CDDL shows a bignum variants of arbitrary length expressed as
40            // bytes, but I can't find the corresponding mapping to JSON in the
41            // Haskell implementation. Not sure what I'm missing. For the time
42            // being, I'll invent a new JSON expression that uses hex strings as
43            // a way to express the values.
44            super::BigInt::BigUInt(x) => json!({ "biguint": hex::encode(x.as_slice())}),
45            super::BigInt::BigNInt(x) => json!({ "bignint": hex::encode(x.as_slice())}),
46        },
47        super::PlutusData::BoundedBytes(x) => json!({ "bytes": hex::encode(x.as_slice())}),
48        _ => return None,
49    };
50    Some(value)
51}
52
53// infered from https://github.com/input-output-hk/cardano-node/blob/c1efb2f97134c0607c982246a36e3da7266ac194/cardano-api/src/Cardano/Api/ScriptData.hs#L254
54impl ToCanonicalJson for super::PlutusData {
55    /// Building the tree is stack-safe, but the returned `serde_json::Value`
56    /// is not: serializing, cloning, comparing and dropping it recurse per
57    /// nesting level, and a chain-deep datum overflows a 2 MiB stack doing
58    /// so. Use [`to_json_string`](ToCanonicalJson::to_json_string) when the
59    /// depth is not under your control.
60    fn to_json(&self) -> serde_json::Value {
61        use super::PlutusData;
62
63        fold_tree(self, |node, children: Vec<Value>| match node {
64            PlutusData::Constr(x) => object([
65                ("constructor", json!(x.constructor_value())),
66                ("fields", Value::Array(children)),
67            ]),
68            PlutusData::Map(_) => {
69                let mut children = children.into_iter();
70                let mut map = Vec::with_capacity(children.len() / 2);
71                while let (Some(k), Some(v)) = (children.next(), children.next()) {
72                    map.push(object([("k", k), ("v", v)]));
73                }
74                object([("map", Value::Array(map))])
75            }
76            PlutusData::Array(_) => object([("list", Value::Array(children))]),
77            leaf => plutus_leaf_json(leaf).expect("leaf variant"),
78        })
79    }
80
81    /// Writes the JSON text directly, never building a `serde_json::Value`,
82    /// so no step of rendering a chain-deep datum can overflow the stack.
83    fn to_json_string(&self) -> String {
84        use std::fmt::Write;
85
86        use super::PlutusData;
87
88        let mut out = String::new();
89        // Children rendered so far of each open container, innermost last;
90        // a map alternates keys and values and needs to know which is next.
91        let mut rendered: Vec<usize> = Vec::new();
92        walk_tree::<_, std::fmt::Error>(self, |visit| match visit {
93            Visit::Enter(PlutusData::Constr(x)) => {
94                rendered.push(0);
95                match x.constructor_value() {
96                    Some(n) => write!(out, r#"{{"constructor":{n},"fields":["#),
97                    None => write!(out, r#"{{"constructor":null,"fields":["#),
98                }
99            }
100            Visit::Enter(node @ PlutusData::Map(_)) => {
101                rendered.push(0);
102                write!(out, r#"{{"map":["#)?;
103                if node.child_count() > 0 {
104                    write!(out, r#"{{"k":"#)?;
105                }
106                Ok(())
107            }
108            Visit::Enter(PlutusData::Array(_)) => {
109                rendered.push(0);
110                write!(out, r#"{{"list":["#)
111            }
112            Visit::Enter(leaf) => {
113                write!(out, "{}", plutus_leaf_json(leaf).expect("leaf variant"))
114            }
115            Visit::Between(PlutusData::Map(_)) => {
116                let count = rendered.last_mut().expect("inside a map");
117                *count += 1;
118                if count.is_multiple_of(2) {
119                    write!(out, r#"}},{{"k":"#)
120                } else {
121                    write!(out, r#","v":"#)
122                }
123            }
124            Visit::Between(_) => write!(out, ","),
125            Visit::Exit(node @ PlutusData::Map(_)) => {
126                rendered.pop();
127                if node.child_count() > 0 {
128                    write!(out, "}}]}}")
129                } else {
130                    write!(out, "]}}")
131                }
132            }
133            Visit::Exit(PlutusData::Constr(_) | PlutusData::Array(_)) => {
134                rendered.pop();
135                write!(out, "]}}")
136            }
137            Visit::Exit(_) => Ok(()),
138        })
139        .expect("writing to a String cannot fail");
140        out
141    }
142}
143
144impl ToCanonicalJson for super::NativeScript {
145    /// Building the tree is stack-safe, but the returned `serde_json::Value`
146    /// is not: serializing, cloning, comparing and dropping it recurse per
147    /// nesting level, and a chain-deep script overflows a 2 MiB stack doing
148    /// so. Use [`to_json_string`](ToCanonicalJson::to_json_string) when the
149    /// depth is not under your control.
150    fn to_json(&self) -> serde_json::Value {
151        use super::NativeScript;
152
153        fold_tree(self, |node, children: Vec<Value>| match node {
154            NativeScript::ScriptPubkey(x) => json!({ "keyHash": x.to_string(), "type": "sig"}),
155            NativeScript::ScriptAll(_) => {
156                object([("scripts", Value::Array(children)), ("type", json!("all"))])
157            }
158            NativeScript::ScriptAny(_) => {
159                object([("scripts", Value::Array(children)), ("type", json!("any"))])
160            }
161            NativeScript::ScriptNOfK(n, _) => object([
162                ("required", json!(n)),
163                ("scripts", Value::Array(children)),
164                ("type", json!("atLeast")),
165            ]),
166            NativeScript::InvalidBefore(slot) => json!({ "type": "after", "slot": slot }),
167            NativeScript::InvalidHereafter(slot) => json!({"type": "before", "slot": slot }),
168        })
169    }
170
171    /// Writes the JSON text directly, never building a `serde_json::Value`,
172    /// so no step of rendering a chain-deep script can overflow the stack.
173    fn to_json_string(&self) -> String {
174        use std::fmt::Write;
175
176        use super::NativeScript;
177
178        let mut out = String::new();
179        walk_tree::<_, std::fmt::Error>(self, |visit| match visit {
180            Visit::Enter(NativeScript::ScriptPubkey(x)) => {
181                write!(out, r#"{{"keyHash":"{x}","type":"sig"}}"#)
182            }
183            Visit::Enter(NativeScript::ScriptAll(_) | NativeScript::ScriptAny(_)) => {
184                write!(out, r#"{{"scripts":["#)
185            }
186            Visit::Enter(NativeScript::ScriptNOfK(n, _)) => {
187                write!(out, r#"{{"required":{n},"scripts":["#)
188            }
189            Visit::Enter(NativeScript::InvalidBefore(slot)) => {
190                write!(out, r#"{{"slot":{slot},"type":"after"}}"#)
191            }
192            Visit::Enter(NativeScript::InvalidHereafter(slot)) => {
193                write!(out, r#"{{"slot":{slot},"type":"before"}}"#)
194            }
195            Visit::Between(_) => write!(out, ","),
196            Visit::Exit(NativeScript::ScriptAll(_)) => write!(out, r#"],"type":"all"}}"#),
197            Visit::Exit(NativeScript::ScriptAny(_)) => write!(out, r#"],"type":"any"}}"#),
198            Visit::Exit(NativeScript::ScriptNOfK(..)) => write!(out, r#"],"type":"atLeast"}}"#),
199            Visit::Exit(_) => Ok(()),
200        })
201        .expect("writing to a String cannot fail");
202        out
203    }
204}
205
206#[cfg(test)]
207mod tests {
208    use pallas_codec::minicbor;
209
210    use crate::{ToCanonicalJson, alonzo::Block};
211
212    type BlockWrapper<'a> = (u16, Block<'a>);
213
214    #[test]
215    fn test_datums_serialize_as_expected() {
216        let test_blocks = [(
217            include_str!("../../../test_data/alonzo9.block"),
218            include_str!("../../../test_data/alonzo9.datums"),
219        )];
220
221        for (idx, (block_str, jsonl_str)) in test_blocks.iter().enumerate() {
222            println!("decoding json block {}", idx + 1);
223
224            let bytes = hex::decode(block_str).unwrap_or_else(|_| panic!("bad block file {idx}"));
225
226            let (_, block): BlockWrapper = minicbor::decode(&bytes[..])
227                .unwrap_or_else(|_| panic!("error decoding cbor for file {idx}"));
228
229            let mut datums = jsonl_str.lines();
230
231            for ws in block.transaction_witness_sets.iter() {
232                if let Some(pds) = &ws.plutus_data {
233                    for pd in pds.iter() {
234                        let expected: serde_json::Value =
235                            serde_json::from_str(datums.next().unwrap()).unwrap();
236                        let current = pd.to_json();
237                        assert_eq!(current, expected);
238
239                        let text = pd.to_json_string();
240                        let parsed: serde_json::Value = serde_json::from_str(&text).unwrap();
241                        assert_eq!(parsed, expected);
242                        assert_eq!(text, current.to_string());
243                    }
244                }
245            }
246        }
247    }
248
249    #[test]
250    fn test_native_scripts_serialize_as_expected() {
251        let test_blocks = [(
252            include_str!("../../../test_data/alonzo9.block"),
253            include_str!("../../../test_data/alonzo9.native"),
254        )];
255
256        for (idx, (block_str, jsonl_str)) in test_blocks.iter().enumerate() {
257            println!("decoding json block {}", idx + 1);
258
259            let bytes = hex::decode(block_str).unwrap_or_else(|_| panic!("bad block file {idx}"));
260
261            let (_, block): BlockWrapper = minicbor::decode(&bytes[..])
262                .unwrap_or_else(|_| panic!("error decoding cbor for file {idx}"));
263
264            let mut scripts = jsonl_str.lines();
265
266            for ws in block.transaction_witness_sets.iter() {
267                if let Some(nss) = &ws.native_script {
268                    for ns in nss.iter() {
269                        let expected: serde_json::Value =
270                            serde_json::from_str(scripts.next().unwrap()).unwrap();
271                        let current = ns.to_json();
272                        assert_eq!(current, expected);
273
274                        let text: serde_json::Value =
275                            serde_json::from_str(&ns.to_json_string()).unwrap();
276                        assert_eq!(text, expected);
277                    }
278                }
279            }
280        }
281    }
282
283    #[test]
284    fn native_script_json_preserves_mixed_shape_trees() {
285        use crate::alonzo::NativeScript;
286        use serde_json::json;
287
288        // Width > 1 at more than one level: the iterative conversion pairs
289        // mapped children with source children positionally.
290        let script = NativeScript::ScriptNOfK(
291            2,
292            vec![
293                NativeScript::ScriptPubkey([1; 28].into()),
294                NativeScript::ScriptAll(vec![
295                    NativeScript::ScriptPubkey([2; 28].into()),
296                    NativeScript::ScriptAny(vec![
297                        NativeScript::InvalidBefore(100),
298                        NativeScript::InvalidHereafter(200),
299                    ]),
300                ]),
301                NativeScript::ScriptPubkey([3; 28].into()),
302            ],
303        );
304
305        let expected = json!({
306            "type": "atLeast",
307            "required": 2,
308            "scripts": [
309                { "type": "sig", "keyHash": hex::encode([1u8; 28]) },
310                { "type": "all", "scripts": [
311                    { "type": "sig", "keyHash": hex::encode([2u8; 28]) },
312                    { "type": "any", "scripts": [
313                        { "type": "after", "slot": 100 },
314                        { "type": "before", "slot": 200 },
315                    ]},
316                ]},
317                { "type": "sig", "keyHash": hex::encode([3u8; 28]) },
318            ],
319        });
320
321        assert_eq!(script.to_json(), expected);
322
323        let text: serde_json::Value = serde_json::from_str(&script.to_json_string()).unwrap();
324        assert_eq!(text, expected);
325    }
326
327    fn deep_datum_shapes(depth: usize) -> Vec<(crate::PlutusData, String)> {
328        use crate::{BigInt, Constr, Int, KeyValuePairs, MaybeIndefArray, PlutusData};
329
330        let int = |n: i64| PlutusData::BigInt(BigInt::Int(Int::from(n)));
331        let mut constr = int(0);
332        let mut map = int(0);
333        let mut list = int(0);
334        for _ in 0..depth {
335            constr = PlutusData::Constr(Constr {
336                tag: 121,
337                any_constructor: None,
338                fields: MaybeIndefArray::Indef(vec![constr]),
339            });
340            map = PlutusData::Map(KeyValuePairs::Def(vec![(int(1), map)]));
341            list = PlutusData::Array(MaybeIndefArray::Def(vec![list]));
342        }
343        let leaf = r#"{"int":0}"#;
344        vec![
345            (
346                constr,
347                format!(
348                    "{}{leaf}{}",
349                    r#"{"constructor":0,"fields":["#.repeat(depth),
350                    "]}".repeat(depth)
351                ),
352            ),
353            (
354                map,
355                format!(
356                    "{}{leaf}{}",
357                    r#"{"map":[{"k":{"int":1},"v":"#.repeat(depth),
358                    "}]}".repeat(depth)
359                ),
360            ),
361            (
362                list,
363                format!(
364                    "{}{leaf}{}",
365                    r#"{"list":["#.repeat(depth),
366                    "]}".repeat(depth)
367                ),
368            ),
369        ]
370    }
371
372    #[test]
373    fn plutus_data_json_preserves_mixed_shapes() {
374        use crate::PlutusData;
375
376        // Constr(102, 5) [Map [(1, [])], []_, Map [], b"", 2^64, Constr 0 []]
377        let bytes = hex::decode("d866820586a101809fffa040c249010000000000000000d87980").unwrap();
378        let data: PlutusData = minicbor::decode(&bytes).unwrap();
379        let expected = r#"{"constructor":5,"fields":[{"map":[{"k":{"int":1},"v":{"list":[]}}]},{"list":[]},{"map":[]},{"bytes":""},{"biguint":"010000000000000000"},{"constructor":0,"fields":[]}]}"#;
380        assert_eq!(data.to_json_string(), expected);
381        assert_eq!(data.to_json().to_string(), expected);
382    }
383
384    #[test]
385    fn plutus_data_json_string_handles_deeply_nested_data_on_a_small_stack() {
386        std::thread::Builder::new()
387            .stack_size(128 * 1024)
388            .spawn(|| {
389                for (data, expected) in deep_datum_shapes(20_000) {
390                    // PlutusData's Drop still recurses; leak the value.
391                    let data = std::mem::ManuallyDrop::new(data);
392                    assert_eq!(data.to_json_string(), expected);
393                }
394            })
395            .unwrap()
396            .join()
397            .unwrap();
398    }
399
400    #[test]
401    fn plutus_data_json_handles_deeply_nested_data_on_a_small_stack() {
402        std::thread::Builder::new()
403            .stack_size(128 * 1024)
404            .spawn(|| {
405                for (data, _) in deep_datum_shapes(20_000) {
406                    let data = std::mem::ManuallyDrop::new(data);
407                    // Neither the datum nor serde_json::Value has a
408                    // stack-safe Drop; leak both, the conversion is the test.
409                    let json = std::mem::ManuallyDrop::new(data.to_json());
410
411                    let mut depth = 0;
412                    let mut cursor: &serde_json::Value = &json;
413                    loop {
414                        let next = if let Some(fields) = cursor.get("fields") {
415                            fields.get(0)
416                        } else if let Some(map) = cursor.get("map") {
417                            map.get(0).and_then(|pair| pair.get("v"))
418                        } else if let Some(list) = cursor.get("list") {
419                            list.get(0)
420                        } else {
421                            break;
422                        };
423                        cursor = next.expect("container carries a child");
424                        depth += 1;
425                    }
426                    assert_eq!(depth, 20_000);
427                    assert_eq!(cursor["int"], 0);
428                }
429            })
430            .unwrap()
431            .join()
432            .unwrap();
433    }
434
435    #[test]
436    fn native_script_json_string_handles_deeply_nested_scripts_on_a_small_stack() {
437        use crate::alonzo::NativeScript;
438
439        std::thread::Builder::new()
440            .stack_size(128 * 1024)
441            .spawn(|| {
442                let mut script = NativeScript::ScriptPubkey([0; 28].into());
443                for _ in 0..20_000 {
444                    script = NativeScript::ScriptAll(vec![script]);
445                }
446
447                let expected = format!(
448                    "{}{{\"keyHash\":\"{}\",\"type\":\"sig\"}}{}",
449                    r#"{"scripts":["#.repeat(20_000),
450                    hex::encode([0u8; 28]),
451                    r#"],"type":"all"}"#.repeat(20_000),
452                );
453                assert_eq!(script.to_json_string(), expected);
454            })
455            .unwrap()
456            .join()
457            .unwrap();
458    }
459
460    #[test]
461    fn native_script_json_handles_deeply_nested_scripts_on_a_small_stack() {
462        use crate::alonzo::NativeScript;
463
464        // Depth and stack size are load-bearing: the recursive conversion
465        // (one call frame per level) aborts here.
466        std::thread::Builder::new()
467            .stack_size(128 * 1024)
468            .spawn(|| {
469                let mut script = NativeScript::ScriptPubkey([0; 28].into());
470                for _ in 0..20_000 {
471                    script = NativeScript::ScriptAll(vec![script]);
472                }
473
474                // serde_json::Value has no stack-safe Drop, so dropping a
475                // chain this deep (including on a failed assertion) would
476                // abort. Leak it: this test is only about the conversion.
477                let json = std::mem::ManuallyDrop::new(script.to_json());
478
479                let mut depth = 0;
480                let mut cursor: &serde_json::Value = &json;
481                while let Some(scripts) = cursor.get("scripts") {
482                    cursor = scripts.get(0).expect("all must carry a child");
483                    depth += 1;
484                }
485                assert_eq!(depth, 20_000);
486                assert_eq!(cursor["type"], "sig");
487            })
488            .unwrap()
489            .join()
490            .unwrap();
491    }
492}