Skip to main content

ratify_protocol/
canonical.rs

1//! Canonical JSON serialization per Ratify Protocol SPEC ยง6.
2//!
3//! Every implementation MUST produce byte-identical output for the same
4//! input or signatures will not verify across languages.
5//!
6//! Rules:
7//!   - Object members in lex order (byte order on UTF-8), RECURSIVELY.
8//!   - No whitespace between tokens. No trailing newline.
9//!   - UTF-8 encoding.
10//!   - Integers as shortest decimal.
11//!   - Byte arrays as base64-standard strings with padding.
12//!   - '<', '>', '&' pass through unmodified.
13//!   - U+2028 / U+2029 escape as \\u2028 / \\u2029 (matches Go behavior).
14//!   - Minimum string escaping per RFC 8259.
15
16use base64::{engine::general_purpose::STANDARD, Engine as _};
17use serde_json::Value;
18
19/// Canonical JSON-encode a serde_json::Value.
20pub fn canonical_json(value: &Value) -> Vec<u8> {
21    let mut out = String::new();
22    encode_value(value, &mut out);
23    out.into_bytes()
24}
25
26fn encode_value(v: &Value, out: &mut String) {
27    match v {
28        Value::Null => out.push_str("null"),
29        Value::Bool(true) => out.push_str("true"),
30        Value::Bool(false) => out.push_str("false"),
31        Value::Number(n) => out.push_str(&encode_number(n)),
32        Value::String(s) => encode_string(s, out),
33        Value::Array(arr) => {
34            out.push('[');
35            for (i, item) in arr.iter().enumerate() {
36                if i > 0 {
37                    out.push(',');
38                }
39                encode_value(item, out);
40            }
41            out.push(']');
42        }
43        Value::Object(obj) => {
44            // Collect keys and sort lex-order.
45            let mut keys: Vec<&String> = obj.keys().collect();
46            keys.sort();
47            out.push('{');
48            let mut first = true;
49            for k in keys {
50                let val = &obj[k];
51                // Skip nulls โ€” matches Go's omitempty for optional fields.
52                if val.is_null() {
53                    continue;
54                }
55                if !first {
56                    out.push(',');
57                }
58                encode_string(k, out);
59                out.push(':');
60                encode_value(val, out);
61                first = false;
62            }
63            out.push('}');
64        }
65    }
66}
67
68// encode_number reproduces the Go / TS / Python policy: integers and
69// integer-valued floats serialize as shortest decimal integer (no "500.0"),
70// non-integer floats serialize via their default textual representation.
71// Without this, serde_json's f64 default emits "500.0" where the other
72// implementations emit "500", breaking cross-SDK byte identicality.
73fn encode_number(n: &serde_json::Number) -> String {
74    if let Some(i) = n.as_i64() {
75        return i.to_string();
76    }
77    if let Some(u) = n.as_u64() {
78        return u.to_string();
79    }
80    if let Some(f) = n.as_f64() {
81        if f.is_finite() && f.fract() == 0.0 && f.abs() < 1e15 {
82            // Whole-number f64 โ†’ integer form.
83            return (f as i64).to_string();
84        }
85        return f.to_string();
86    }
87    n.to_string()
88}
89
90fn encode_string(s: &str, out: &mut String) {
91    out.push('"');
92    for c in s.chars() {
93        match c {
94            '"' => out.push_str("\\\""),
95            '\\' => out.push_str("\\\\"),
96            '\u{0008}' => out.push_str("\\b"),
97            '\u{0009}' => out.push_str("\\t"),
98            '\u{000A}' => out.push_str("\\n"),
99            '\u{000C}' => out.push_str("\\f"),
100            '\u{000D}' => out.push_str("\\r"),
101            '\u{2028}' => out.push_str("\\u2028"),
102            '\u{2029}' => out.push_str("\\u2029"),
103            c if (c as u32) < 0x20 => {
104                out.push_str(&format!("\\u{:04x}", c as u32));
105            }
106            // Everything else passes through unmodified (NO HTML escape).
107            c => out.push(c),
108        }
109    }
110    out.push('"');
111}
112
113/// Standard base64 encode with padding.
114pub fn base64_std_encode(data: &[u8]) -> String {
115    STANDARD.encode(data)
116}
117
118/// Standard base64 decode.
119pub fn base64_std_decode(s: &str) -> Result<Vec<u8>, base64::DecodeError> {
120    STANDARD.decode(s)
121}
122
123/// Lowercase hex.
124pub fn hex_encode(data: &[u8]) -> String {
125    hex::encode(data)
126}
127
128/// Lower- or upper-case hex.
129pub fn hex_decode(s: &str) -> Result<Vec<u8>, hex::FromHexError> {
130    hex::decode(s)
131}
132
133/// serde helper to (de)serialize Vec<u8> as base64-standard strings.
134pub mod base64_bytes {
135    use super::{base64_std_decode, base64_std_encode};
136    use serde::{de::Error, Deserialize, Deserializer, Serializer};
137
138    pub fn serialize<S>(bytes: &Vec<u8>, serializer: S) -> Result<S::Ok, S::Error>
139    where
140        S: Serializer,
141    {
142        serializer.serialize_str(&base64_std_encode(bytes))
143    }
144
145    pub fn deserialize<'de, D>(deserializer: D) -> Result<Vec<u8>, D::Error>
146    where
147        D: Deserializer<'de>,
148    {
149        let s = String::deserialize(deserializer)?;
150        base64_std_decode(&s).map_err(|e| D::Error::custom(format!("base64 decode: {e}")))
151    }
152}