boundgraph_core/
canonical.rs1use ciborium::Value;
19use serde::Serialize;
20use serde::de::DeserializeOwned;
21
22use crate::digest::Digest;
23
24#[derive(Debug, thiserror::Error)]
26pub enum CanonicalError {
27 #[error("could not encode value as canonical CBOR: {0}")]
29 Encode(String),
30
31 #[error("could not decode canonical CBOR: {0}")]
33 Decode(String),
34}
35
36pub 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
45pub 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
53pub trait Canonical: Serialize {
58 fn canonical_bytes(&self) -> Result<Vec<u8>, CanonicalError> {
60 to_canonical_cbor(self)
61 }
62
63 fn digest(&self) -> Result<Digest, CanonicalError> {
65 Ok(Digest::of(&self.canonical_bytes()?))
66 }
67}
68
69impl<T: Serialize + ?Sized> Canonical for T {}
70
71fn 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
79fn 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 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 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}