proofborne-core 0.1.0-alpha.1

Versioned contracts, events, provider types, and proof graph for Proofborne
Documentation
use serde_json::Value;

/// Serializes JSON deterministically by sorting object keys recursively.
///
/// The format intentionally stays within JSON rather than relying on Rust's map
/// iteration order. It is the hash preimage format for schema version 1.
pub fn canonical_json_bytes(value: &Value) -> Vec<u8> {
    let mut output = Vec::new();
    write_value(value, &mut output);
    output
}

fn write_value(value: &Value, output: &mut Vec<u8>) {
    match value {
        Value::Null => output.extend_from_slice(b"null"),
        Value::Bool(value) => output.extend_from_slice(if *value { b"true" } else { b"false" }),
        Value::Number(value) => output.extend_from_slice(value.to_string().as_bytes()),
        Value::String(value) => {
            let encoded =
                serde_json::to_string(value).expect("serializing a JSON string cannot fail");
            output.extend_from_slice(encoded.as_bytes());
        }
        Value::Array(values) => {
            output.push(b'[');
            for (index, value) in values.iter().enumerate() {
                if index > 0 {
                    output.push(b',');
                }
                write_value(value, output);
            }
            output.push(b']');
        }
        Value::Object(values) => {
            output.push(b'{');
            let mut keys: Vec<_> = values.keys().collect();
            keys.sort_unstable();
            for (index, key) in keys.iter().enumerate() {
                if index > 0 {
                    output.push(b',');
                }
                let encoded =
                    serde_json::to_string(key).expect("serializing a JSON key cannot fail");
                output.extend_from_slice(encoded.as_bytes());
                output.push(b':');
                write_value(&values[*key], output);
            }
            output.push(b'}');
        }
    }
}

/// Returns a lowercase BLAKE3 digest for arbitrary bytes.
pub fn hash_bytes(bytes: impl AsRef<[u8]>) -> String {
    blake3::hash(bytes.as_ref()).to_hex().to_string()
}

/// Returns a lowercase BLAKE3 digest for canonical JSON.
pub fn hash_json(value: &Value) -> String {
    hash_bytes(canonical_json_bytes(value))
}

#[cfg(test)]
mod tests {
    use serde_json::json;

    use super::*;

    #[test]
    fn object_key_order_does_not_change_hash() {
        let left = json!({"z": 1, "a": {"two": 2, "one": 1}});
        let right = json!({"a": {"one": 1, "two": 2}, "z": 1});
        assert_eq!(canonical_json_bytes(&left), canonical_json_bytes(&right));
        assert_eq!(hash_json(&left), hash_json(&right));
    }

    #[test]
    fn array_order_changes_hash() {
        assert_ne!(hash_json(&json!([1, 2])), hash_json(&json!([2, 1])));
    }
}