Skip to main content

boundgraph_core/
canonical.rs

1//! Canonical CBOR encoding.
2//!
3//! Every BoundGraph primitive is content-addressed by the digest of its canonical
4//! encoding, so the encoding must be a *function* of the value: the same
5//! logical value must always produce byte-identical output, on any platform,
6//! in any process.
7//!
8//! `serde` alone does not give that. It emits struct fields in declaration
9//! order and emits map entries in iteration order, so two equal values can
10//! encode differently. We therefore serialise to a [`ciborium::Value`] tree,
11//! rewrite it into deterministic form, and only then emit bytes.
12//!
13//! Deterministic form follows RFC 8949 §4.2.1: map keys are sorted bytewise
14//! and lexicographically **by their own encoded representation**, not by their
15//! logical value. Sorting by logical value would order `1` before `"a"` in one
16//! encoder and after it in another; sorting by encoded bytes is unambiguous.
17
18use ciborium::Value;
19use serde::Serialize;
20use serde::de::DeserializeOwned;
21
22use crate::digest::Digest;
23
24/// Something went wrong turning a value into (or back out of) canonical bytes.
25#[derive(Debug, thiserror::Error)]
26pub enum CanonicalError {
27    /// The value could not be represented as CBOR.
28    #[error("could not encode value as canonical CBOR: {0}")]
29    Encode(String),
30
31    /// The bytes were not valid CBOR, or did not match the target type.
32    #[error("could not decode canonical CBOR: {0}")]
33    Decode(String),
34}
35
36/// Encode `value` as canonical CBOR.
37///
38/// The output is deterministic: equal values always produce equal bytes.
39pub fn to_canonical_cbor<T: Serialize + ?Sized>(value: &T) -> Result<Vec<u8>, CanonicalError> {
40    let tree = Value::serialized(value).map_err(|e| CanonicalError::Encode(e.to_string()))?;
41    let tree = canonicalize(tree)?;
42    write(&tree)
43}
44
45/// Decode canonical CBOR produced by [`to_canonical_cbor`].
46pub fn from_canonical_cbor<T: DeserializeOwned>(bytes: &[u8]) -> Result<T, CanonicalError> {
47    let tree: Value =
48        ciborium::from_reader(bytes).map_err(|e| CanonicalError::Decode(e.to_string()))?;
49    tree.deserialized()
50        .map_err(|e| CanonicalError::Decode(e.to_string()))
51}
52
53/// Blanket helper: the content address of any serialisable value.
54///
55/// This is the single definition of identity in BoundGraph. Two values share a
56/// digest if and only if they encode to the same canonical bytes.
57pub trait Canonical: Serialize {
58    /// The canonical CBOR encoding of this value.
59    fn canonical_bytes(&self) -> Result<Vec<u8>, CanonicalError> {
60        to_canonical_cbor(self)
61    }
62
63    /// The `blake3` digest of this value's canonical encoding.
64    fn digest(&self) -> Result<Digest, CanonicalError> {
65        Ok(Digest::of(&self.canonical_bytes()?))
66    }
67}
68
69impl<T: Serialize + ?Sized> Canonical for T {}
70
71// ── internals ───────────────────────────────────────────────────────────────
72
73fn write(tree: &Value) -> Result<Vec<u8>, CanonicalError> {
74    let mut out = Vec::new();
75    ciborium::into_writer(tree, &mut out).map_err(|e| CanonicalError::Encode(e.to_string()))?;
76    Ok(out)
77}
78
79/// Rewrite a CBOR tree into deterministic form, depth first.
80fn canonicalize(value: Value) -> Result<Value, CanonicalError> {
81    Ok(match value {
82        Value::Array(items) => Value::Array(
83            items
84                .into_iter()
85                .map(canonicalize)
86                .collect::<Result<Vec<_>, _>>()?,
87        ),
88
89        Value::Map(entries) => {
90            // Canonicalise children first so nested keys are themselves in
91            // deterministic form before we encode them for sorting.
92            let mut prepared = Vec::with_capacity(entries.len());
93            for (k, v) in entries {
94                let k = canonicalize(k)?;
95                let v = canonicalize(v)?;
96                let sort_key = write(&k)?;
97                prepared.push((sort_key, k, v));
98            }
99            prepared.sort_by(|a, b| a.0.cmp(&b.0));
100            Value::Map(prepared.into_iter().map(|(_, k, v)| (k, v)).collect())
101        }
102
103        Value::Tag(tag, inner) => Value::Tag(tag, Box::new(canonicalize(*inner)?)),
104
105        other => other,
106    })
107}
108
109#[cfg(test)]
110mod tests {
111    use super::*;
112    use std::collections::BTreeMap;
113
114    #[test]
115    fn map_keys_are_sorted_by_encoded_bytes() {
116        // Insertion order here is deliberately not sorted order.
117        let mut a: BTreeMap<String, u32> = BTreeMap::new();
118        a.insert("zebra".into(), 1);
119        a.insert("alpha".into(), 2);
120
121        let mut b: BTreeMap<String, u32> = BTreeMap::new();
122        b.insert("alpha".into(), 2);
123        b.insert("zebra".into(), 1);
124
125        assert_eq!(
126            to_canonical_cbor(&a).unwrap(),
127            to_canonical_cbor(&b).unwrap()
128        );
129    }
130
131    #[test]
132    fn encoding_is_stable_across_calls() {
133        let v = vec![("k", 1u8), ("j", 2u8)];
134        let first = to_canonical_cbor(&v).unwrap();
135        for _ in 0..16 {
136            assert_eq!(first, to_canonical_cbor(&v).unwrap());
137        }
138    }
139
140    #[test]
141    fn round_trip_preserves_value() {
142        let v: Vec<(String, u64)> = vec![("a".into(), 1), ("b".into(), 2)];
143        let bytes = to_canonical_cbor(&v).unwrap();
144        let back: Vec<(String, u64)> = from_canonical_cbor(&bytes).unwrap();
145        assert_eq!(v, back);
146    }
147
148    #[test]
149    fn nested_maps_are_canonicalised_too() {
150        let mut inner_a: BTreeMap<String, u8> = BTreeMap::new();
151        inner_a.insert("y".into(), 1);
152        inner_a.insert("x".into(), 2);
153        let mut outer_a: BTreeMap<String, BTreeMap<String, u8>> = BTreeMap::new();
154        outer_a.insert("outer".into(), inner_a);
155
156        let mut inner_b: BTreeMap<String, u8> = BTreeMap::new();
157        inner_b.insert("x".into(), 2);
158        inner_b.insert("y".into(), 1);
159        let mut outer_b: BTreeMap<String, BTreeMap<String, u8>> = BTreeMap::new();
160        outer_b.insert("outer".into(), inner_b);
161
162        assert_eq!(
163            to_canonical_cbor(&outer_a).unwrap(),
164            to_canonical_cbor(&outer_b).unwrap()
165        );
166    }
167}