Skip to main content

proofborne_core/
canonical.rs

1use serde_json::Value;
2
3/// Serializes JSON deterministically by sorting object keys recursively.
4///
5/// The format intentionally stays within JSON rather than relying on Rust's map
6/// iteration order. It is the hash preimage format for schema version 1.
7pub fn canonical_json_bytes(value: &Value) -> Vec<u8> {
8    let mut output = Vec::new();
9    write_value(value, &mut output);
10    output
11}
12
13fn write_value(value: &Value, output: &mut Vec<u8>) {
14    match value {
15        Value::Null => output.extend_from_slice(b"null"),
16        Value::Bool(value) => output.extend_from_slice(if *value { b"true" } else { b"false" }),
17        Value::Number(value) => output.extend_from_slice(value.to_string().as_bytes()),
18        Value::String(value) => {
19            let encoded =
20                serde_json::to_string(value).expect("serializing a JSON string cannot fail");
21            output.extend_from_slice(encoded.as_bytes());
22        }
23        Value::Array(values) => {
24            output.push(b'[');
25            for (index, value) in values.iter().enumerate() {
26                if index > 0 {
27                    output.push(b',');
28                }
29                write_value(value, output);
30            }
31            output.push(b']');
32        }
33        Value::Object(values) => {
34            output.push(b'{');
35            let mut keys: Vec<_> = values.keys().collect();
36            keys.sort_unstable();
37            for (index, key) in keys.iter().enumerate() {
38                if index > 0 {
39                    output.push(b',');
40                }
41                let encoded =
42                    serde_json::to_string(key).expect("serializing a JSON key cannot fail");
43                output.extend_from_slice(encoded.as_bytes());
44                output.push(b':');
45                write_value(&values[*key], output);
46            }
47            output.push(b'}');
48        }
49    }
50}
51
52/// Returns a lowercase BLAKE3 digest for arbitrary bytes.
53pub fn hash_bytes(bytes: impl AsRef<[u8]>) -> String {
54    blake3::hash(bytes.as_ref()).to_hex().to_string()
55}
56
57/// Returns a lowercase BLAKE3 digest for canonical JSON.
58pub fn hash_json(value: &Value) -> String {
59    hash_bytes(canonical_json_bytes(value))
60}
61
62#[cfg(test)]
63mod tests {
64    use serde_json::json;
65
66    use super::*;
67
68    #[test]
69    fn object_key_order_does_not_change_hash() {
70        let left = json!({"z": 1, "a": {"two": 2, "one": 1}});
71        let right = json!({"a": {"one": 1, "two": 2}, "z": 1});
72        assert_eq!(canonical_json_bytes(&left), canonical_json_bytes(&right));
73        assert_eq!(hash_json(&left), hash_json(&right));
74    }
75
76    #[test]
77    fn array_order_changes_hash() {
78        assert_ne!(hash_json(&json!([1, 2])), hash_json(&json!([2, 1])));
79    }
80}