content_addressable/merkle.rs
1//! A content-addressed Merkle-DAG node — payload plus ordered parent links,
2//! identified by the [`ContentId`] of the whole (payload **and** parents).
3//!
4//! # Why this exists
5//!
6//! The "conversation = Merkle DAG of content-addressed events" design (and any
7//! tamper-evident event log built on this crate) needs one shared node shape:
8//! a payload, the set of causal parent links, and a self-derived id over both.
9//! Each event's identity is the [`ContentId`] of the *canonical node including
10//! its parent ids*, so the parent links are part of the hashed body. That is
11//! what makes the DAG tamper-evident: you cannot alter a parent's bytes without
12//! changing its id, which changes every descendant's id.
13//!
14//! Without a shared type, every downstream (agent-mesh, kyln, future callers)
15//! re-derives the same shape by hand and risks diverging on the one thing that
16//! must be uniform: *how parents serialize and order*. [`MerkleNode`] ships that
17//! building block once, correctly:
18//!
19//! - Parents live in a [`BTreeSet<ContentId>`], so they are **deduplicated** and
20//! **deterministically ordered** by [`ContentId`]'s content-derived [`Ord`].
21//! Equal parent sets always produce equal bytes regardless of insertion order.
22//! - Each parent serializes as a dag-cbor **tag-42 link** (the frozen
23//! [`ContentId`] serde form), so a node's parents are real IPLD links.
24//! - The id is derived through the existing [`ContentAddressable`] trait via
25//! [`to_canonical_dagcbor`](crate::canonical::to_canonical_dagcbor) — no
26//! bespoke canonicalization.
27//!
28//! # Frozen bytes
29//!
30//! The **envelope** of a [`MerkleNode`] is frozen: the dag-cbor map
31//! `{"parents": [tag-42 links, sorted by CID bytes, deduplicated], "payload": …}`
32//! and the CID derived from it. Payload bytes are frozen for payloads inside the
33//! crate's canonical data model (`Ipld` and types serializing to it), not for an
34//! arbitrary `T`. The byte-level specification, including which layer rejects
35//! which malformed input, is `docs/MERKLE_WIRE.md`; `tests/merkle_vectors.json`
36//! pins seven cases and both language gates rebuild them. The key strings
37//! `payload` / `parents` are load-bearing for every id. Changing the envelope is
38//! a major version bump.
39
40use std::collections::BTreeSet;
41
42use serde::de::DeserializeOwned;
43use serde::{Deserialize, Serialize};
44
45use crate::canonical;
46use crate::content_id::ContentId;
47use crate::error::ContentError;
48use crate::trait_def::ContentAddressable;
49
50/// A content-addressed node in a Merkle DAG: a `payload` plus the set of causal
51/// `parents`, identified by the [`ContentId`] of the whole.
52///
53/// `MerkleNode<T>` is generic over a [`Serialize`] payload `T`. Its id —
54/// [`content_id`](ContentAddressable::content_id), exposed here as
55/// [`id`](Self::id) — is derived from **both** the payload and the parent links,
56/// so it is exactly the agent-mesh conversation-event id (the [`ContentId`] of
57/// the canonical event including its parent cids).
58///
59/// # Parents are an ordered set, not a list
60///
61/// `parents` is a [`BTreeSet<ContentId>`] rather than a `Vec<ContentId>` on
62/// purpose. A `BTreeSet` gives:
63///
64/// - **free deduplication** — a parent listed twice is one link, and
65/// - **deterministic order** — iteration follows [`ContentId`]'s content-derived
66/// [`Ord`], so the same parent multiset always serializes to the same bytes,
67/// regardless of the order the caller inserted them.
68///
69/// `serde` encodes the set as a CBOR array, and each element serializes as a
70/// dag-cbor **tag-42 link** via [`ContentId`]'s frozen serde — so a node's
71/// parents land on the wire as a deterministically-ordered array of real IPLD
72/// links. A `Vec<ContentId>` is rejected precisely because it would let two
73/// semantically-equal nodes hash differently by parent order, breaking the
74/// content-address contract.
75///
76/// # Self-derived id
77///
78/// `MerkleNode<T>` implements [`ContentAddressable`] with the one-line
79/// [`canonical_form`](ContentAddressable::canonical_form) deferring to
80/// [`to_canonical_dagcbor`](crate::canonical::to_canonical_dagcbor), so
81/// [`content_id`](crate::ContentAddressable::content_id) / [`verify`](crate::ContentAddressable::verify) come for free and the id is over the *whole* node
82/// including its parent links.
83///
84/// # Frozen bytes
85///
86/// The serialized bytes are frozen and pinned by `tests/merkle_vectors.json`; see
87/// the [module docs](self#frozen-bytes). The `payload` / `parents` key strings
88/// are part of every node's id.
89///
90/// # Examples
91///
92/// ```
93/// use content_addressable::merkle::MerkleNode;
94/// use content_addressable::ContentAddressable;
95///
96/// // A genesis node: no parents.
97/// let root = MerkleNode::genesis("hello");
98/// let root_id = root.id().unwrap();
99///
100/// // A child node linking the root as its parent. Its id binds both the
101/// // payload and the parent link.
102/// let child = MerkleNode::new("world", [root_id]);
103/// assert!(child.parents().contains(&root_id));
104/// assert!(child.verify(&child.id().unwrap()).unwrap());
105/// ```
106#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
107#[serde(bound(serialize = "T: Serialize", deserialize = "T: DeserializeOwned"))]
108pub struct MerkleNode<T> {
109 /// The node's content.
110 ///
111 /// The field *key string* `"payload"` is load-bearing for the node's id and
112 /// part of the frozen byte layout — see the [module docs](self#frozen-bytes).
113 pub payload: T,
114 /// The causal parent links, as a deterministically-ordered, deduplicated
115 /// set of [`ContentId`]s.
116 ///
117 /// Each element serializes as a dag-cbor tag-42 link. The field *key string*
118 /// `"parents"` is load-bearing for the node's id and part of the
119 /// frozen byte layout — see the [module docs](self#frozen-bytes).
120 pub parents: BTreeSet<ContentId>,
121}
122
123impl<T> MerkleNode<T> {
124 /// Build a node from a payload and an iterator of parent ids.
125 ///
126 /// The parents are collected into a [`BTreeSet`], so duplicate ids collapse
127 /// and insertion order does not affect the node's bytes or its
128 /// [`id`](Self::id). Pass an empty iterator (e.g. `[]`) for a node with no
129 /// parents, or use [`genesis`](Self::genesis) for clarity.
130 pub fn new(payload: T, parents: impl IntoIterator<Item = ContentId>) -> Self {
131 Self {
132 payload,
133 parents: parents.into_iter().collect(),
134 }
135 }
136
137 /// Build a genesis (root) node — a node with no parents.
138 ///
139 /// Equivalent to [`new`](Self::new) with an empty parent set, named for the
140 /// common DAG-root case. A genesis node's id differs from the id of a
141 /// same-payload node that *has* a parent, because the parent links are part
142 /// of the hashed body.
143 pub fn genesis(payload: T) -> Self {
144 Self {
145 payload,
146 parents: BTreeSet::new(),
147 }
148 }
149
150 /// Borrow the node's payload.
151 pub fn payload(&self) -> &T {
152 &self.payload
153 }
154
155 /// Borrow the node's parent link set.
156 ///
157 /// The set is ordered by [`ContentId`]'s content-derived [`Ord`].
158 pub fn parents(&self) -> &BTreeSet<ContentId> {
159 &self.parents
160 }
161}
162
163impl<T: Serialize> MerkleNode<T> {
164 /// The node's content id — the [`ContentId`] of the whole node, payload and
165 /// parent links together.
166 ///
167 /// A convenience alias for [`ContentAddressable::content_id`]; this is the
168 /// agent-mesh conversation-event id.
169 ///
170 /// # Errors
171 ///
172 /// Propagates any error from
173 /// [`canonical_form`](ContentAddressable::canonical_form) (i.e. an encoding
174 /// failure).
175 pub fn id(&self) -> Result<ContentId, ContentError> {
176 self.content_id()
177 }
178}
179
180impl<T: Serialize> ContentAddressable for MerkleNode<T> {
181 fn canonical_form(&self) -> Result<Vec<u8>, ContentError> {
182 canonical::to_canonical_dagcbor(self)
183 }
184}
185
186#[cfg(test)]
187mod tests {
188 use super::*;
189
190 /// A small concrete payload exercising the `T: Serialize + DeserializeOwned`
191 /// path (a node carrying a map-bearing struct).
192 #[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
193 struct Event {
194 author: String,
195 body: String,
196 }
197
198 fn event(author: &str, body: &str) -> Event {
199 Event {
200 author: author.to_string(),
201 body: body.to_string(),
202 }
203 }
204
205 /// A stand-in parent id, minted from canonical bytes the same way the rest
206 /// of the crate does. The exact bytes don't matter — only that it is a real
207 /// [`ContentId`] that serializes as a tag-42 link.
208 fn id_for(payload: &str) -> ContentId {
209 MerkleNode::genesis(payload.to_string()).id().unwrap()
210 }
211
212 #[test]
213 fn content_id_is_deterministic() {
214 // Same payload + same parents => same id (and same bytes).
215 let p = id_for("parent");
216 let a = MerkleNode::new(event("alice", "hi"), [p]);
217 let b = MerkleNode::new(event("alice", "hi"), [p]);
218 assert_eq!(
219 a.id().unwrap(),
220 b.id().unwrap(),
221 "equal nodes must produce equal ids"
222 );
223 assert_eq!(
224 a.canonical_form().unwrap(),
225 b.canonical_form().unwrap(),
226 "equal nodes must produce equal canonical bytes"
227 );
228 }
229
230 #[test]
231 fn different_payload_changes_id() {
232 // The id binds the payload: changing it changes the id.
233 let p = id_for("parent");
234 let a = MerkleNode::new(event("alice", "hi"), [p]);
235 let b = MerkleNode::new(event("alice", "bye"), [p]);
236 assert_ne!(
237 a.id().unwrap(),
238 b.id().unwrap(),
239 "a different payload must change the node id"
240 );
241 }
242
243 #[test]
244 fn different_parents_change_id() {
245 // The id binds the parent links: changing/adding/removing one changes it.
246 let payload = event("alice", "hi");
247 let p1 = id_for("p1");
248 let p2 = id_for("p2");
249
250 let one = MerkleNode::new(payload.clone(), [p1]);
251 let other = MerkleNode::new(payload.clone(), [p2]);
252 assert_ne!(
253 one.id().unwrap(),
254 other.id().unwrap(),
255 "changing a parent id must change the node id"
256 );
257
258 let with_two = MerkleNode::new(payload.clone(), [p1, p2]);
259 assert_ne!(
260 one.id().unwrap(),
261 with_two.id().unwrap(),
262 "adding a parent must change the node id"
263 );
264 }
265
266 #[test]
267 fn genesis_differs_from_node_with_parent() {
268 // A genesis (no-parent) node and a same-payload node WITH a parent have
269 // different ids, because parent links are part of the hashed body.
270 let payload = event("alice", "hi");
271 let g = MerkleNode::genesis(payload.clone());
272 assert!(g.parents().is_empty(), "genesis node has no parents");
273
274 let parent = id_for("parent");
275 let child = MerkleNode::new(payload.clone(), [parent]);
276 assert_ne!(
277 g.id().unwrap(),
278 child.id().unwrap(),
279 "a genesis node must differ from a same-payload node with a parent"
280 );
281
282 // And `new` with an empty parent iterator equals `genesis`.
283 let g2 = MerkleNode::new(payload, std::iter::empty());
284 assert_eq!(
285 g.id().unwrap(),
286 g2.id().unwrap(),
287 "new(payload, []) must equal genesis(payload)"
288 );
289 }
290
291 #[test]
292 fn parents_encode_as_tag42_links() {
293 // Each parent must serialize as a dag-cbor tag-42 link: the 0xd8 0x2a
294 // tag head appears exactly once per parent in the canonical bytes.
295 // (Mirrors lib.rs's content_id_serializes_as_dagcbor_link golden.)
296 let p1 = id_for("p1");
297 let p2 = id_for("p2");
298 let node = MerkleNode::new(event("alice", "hi"), [p1, p2]);
299 let bytes = node.canonical_form().unwrap();
300
301 let tag_count = bytes.windows(2).filter(|w| *w == [0xd8, 0x2a]).count();
302 assert_eq!(
303 tag_count, 2,
304 "each of the two parents must be a dag-cbor tag-42 link"
305 );
306
307 // A genesis node has no links at all.
308 let g = MerkleNode::genesis(event("alice", "hi"));
309 let g_bytes = g.canonical_form().unwrap();
310 let g_count = g_bytes.windows(2).filter(|w| *w == [0xd8, 0x2a]).count();
311 assert_eq!(g_count, 0, "a genesis node carries no tag-42 links");
312 }
313
314 #[test]
315 fn btreeset_ordering_makes_id_insertion_order_independent() {
316 // Building a node with parents inserted in opposite orders yields the
317 // same id and the same canonical bytes — the BTreeSet sorts them.
318 let p1 = id_for("p1");
319 let p2 = id_for("p2");
320 let p3 = id_for("p3");
321
322 let forward = MerkleNode::new(event("alice", "hi"), [p1, p2, p3]);
323 let reverse = MerkleNode::new(event("alice", "hi"), [p3, p2, p1]);
324 assert_eq!(
325 forward.id().unwrap(),
326 reverse.id().unwrap(),
327 "parent insertion order must not affect the node id"
328 );
329 assert_eq!(
330 forward.canonical_form().unwrap(),
331 reverse.canonical_form().unwrap(),
332 "parent insertion order must not affect the canonical bytes"
333 );
334
335 // Duplicates collapse, too.
336 let dup = MerkleNode::new(event("alice", "hi"), [p1, p1, p2, p3, p2]);
337 assert_eq!(
338 forward.id().unwrap(),
339 dup.id().unwrap(),
340 "duplicate parents must collapse in the BTreeSet"
341 );
342 }
343
344 #[test]
345 fn small_dag_b_references_a() {
346 // The agent-mesh conversation-event shape: node B references node A's id
347 // as a parent. B's id is stable, and A is recoverable as a link inside
348 // B's parents.
349 let a = MerkleNode::genesis(event("alice", "first"));
350 let a_id = a.id().unwrap();
351
352 let b = MerkleNode::new(event("bob", "reply"), [a_id]);
353 let b_id_1 = b.id().unwrap();
354 let b_id_2 = b.id().unwrap();
355 assert_eq!(b_id_1, b_id_2, "B's id is stable");
356
357 // A is recoverable as a link from B.
358 assert!(
359 b.parents().contains(&a_id),
360 "A's id must be present in B's parent links"
361 );
362 assert_eq!(b.parents().len(), 1, "B has exactly one parent link (A)");
363 }
364
365 #[test]
366 fn roundtrip_and_verify() {
367 // A node survives to_canonical_dagcbor -> from_canonical_dagcbor_checked for a
368 // concrete payload, and verify() returns Ok(true) against its own id.
369 let parent = id_for("parent");
370 let node = MerkleNode::new(event("alice", "hi"), [parent]);
371 let id = node.id().unwrap();
372
373 assert!(
374 node.verify(&id).unwrap(),
375 "a node must verify against its own id"
376 );
377
378 let bytes = node.canonical_form().unwrap();
379 let back: MerkleNode<Event> = canonical::from_canonical_dagcbor_checked(&bytes).unwrap();
380 assert_eq!(node, back, "a node must survive a dag-cbor roundtrip");
381 assert!(
382 back.verify(&id).unwrap(),
383 "the round-tripped node must still verify against the original id"
384 );
385 }
386}