Skip to main content

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}