Skip to main content

reth_trie_sparse/
debug_recorder.rs

1//! Debug recorder for tracking mutating operations on sparse tries.
2//!
3//! This module is only available with the `trie-debug` feature and provides
4//! infrastructure for recording all mutations to a [`crate::ArenaParallelSparseTrie`]
5//! for post-hoc debugging of state root mismatches.
6
7use alloc::{string::String, vec::Vec};
8use alloy_primitives::{hex, Bytes, B256};
9use alloy_trie::nodes::TrieNode;
10use reth_trie_common::Nibbles;
11use serde::Serialize;
12
13/// Records mutating operations performed on a sparse trie in the order they occurred.
14#[derive(Debug, Clone, Default, PartialEq, Eq, Serialize)]
15pub struct TrieDebugRecorder {
16    ops: Vec<RecordedOp>,
17}
18
19impl TrieDebugRecorder {
20    /// Creates a new empty recorder.
21    pub fn new() -> Self {
22        Self::default()
23    }
24
25    /// Clears all recorded operations.
26    pub fn reset(&mut self) {
27        self.ops.clear();
28    }
29
30    /// Records a single operation.
31    pub fn record(&mut self, op: RecordedOp) {
32        self.ops.push(op);
33    }
34
35    /// Returns a reference to the recorded operations.
36    pub fn ops(&self) -> &[RecordedOp] {
37        &self.ops
38    }
39
40    /// Takes and returns the recorded operations, leaving the recorder empty.
41    pub fn take_ops(&mut self) -> Vec<RecordedOp> {
42        core::mem::take(&mut self.ops)
43    }
44
45    /// Returns `true` if no operations have been recorded.
46    pub const fn is_empty(&self) -> bool {
47        self.ops.is_empty()
48    }
49}
50
51/// A mutating operation recorded from a sparse trie.
52#[derive(Debug, Clone, PartialEq, Eq, Serialize)]
53pub enum RecordedOp {
54    /// Records a `reveal_nodes` call with the nodes that were revealed.
55    RevealNodes {
56        /// The proof trie nodes that were revealed.
57        nodes: Vec<ProofTrieNodeRecord>,
58    },
59    /// Records an `update_leaves` call with the leaf updates.
60    UpdateLeaves {
61        /// The leaf updates that were applied.
62        updates: Vec<(B256, LeafUpdateRecord)>,
63        /// Proof targets returned via the callback as `(key, min_len)` pairs.
64        proof_targets: Vec<(B256, u8)>,
65    },
66    /// Records an `update_subtrie_hashes` call.
67    UpdateSubtrieHashes,
68    /// Records a `root()` call.
69    Root,
70    /// Records a `set_root` call with the root node that was set.
71    SetRoot {
72        /// The root trie node that was set.
73        node: ProofTrieNodeRecord,
74    },
75    /// Records a `prune` call. Emitted before the post-prune `SetRoot`/`RevealNodes`
76    /// so consumers know the following initial state is the result of pruning.
77    Prune,
78}
79
80/// A serializable record of a proof trie node.
81#[derive(Debug, Clone, PartialEq, Eq, Serialize)]
82pub struct ProofTrieNodeRecord {
83    /// The nibble path of the node.
84    pub path: Nibbles,
85    /// The trie node.
86    pub node: TrieNodeRecord,
87    /// The branch node masks `(hash_mask, tree_mask)` stored as raw `u16` values, if present.
88    pub masks: Option<(u16, u16)>,
89    /// The short key (extension key) of a branch node. When present, the branch's logical path
90    /// is `path` extended by this key. The arena trie merges extension nodes into their child
91    /// branch, so this replaces separate `Extension` node records.
92    #[serde(skip_serializing_if = "Option::is_none")]
93    pub short_key: Option<Nibbles>,
94    /// The node's state (`Revealed`, `Cached`, or `Dirty`), if known.
95    #[serde(skip_serializing_if = "Option::is_none")]
96    pub state: Option<NodeStateRecord>,
97}
98
99/// A serializable record of a node's state, mirroring the arena trie's `ArenaSparseNodeState`.
100#[derive(Debug, Clone, PartialEq, Eq, Serialize)]
101pub enum NodeStateRecord {
102    /// The node has been revealed but its RLP encoding is not cached.
103    Revealed,
104    /// The node has a cached RLP encoding that is still valid.
105    Cached {
106        /// The cached RLP-encoded node, hex-encoded.
107        rlp_node: String,
108    },
109    /// The node has been modified and its RLP encoding needs recomputation.
110    Dirty,
111}
112
113impl ProofTrieNodeRecord {
114    /// Creates a record from a [`reth_trie_common::ProofTrieNode`].
115    pub fn from_proof_trie_node(node: &reth_trie_common::ProofTrieNode) -> Self {
116        Self {
117            path: node.path,
118            node: TrieNodeRecord(node.node.clone()),
119            masks: node.masks.map(|masks| (masks.hash_mask.get(), masks.tree_mask.get())),
120            short_key: None,
121            state: None,
122        }
123    }
124
125    /// Creates a record from a [`reth_trie_common::ProofTrieNodeV2`].
126    pub fn from_proof_trie_node_v2(node: &reth_trie_common::ProofTrieNodeV2) -> Self {
127        use reth_trie_common::TrieNodeV2;
128        let (trie_node, short_key) = match &node.node {
129            TrieNodeV2::EmptyRoot => (TrieNode::EmptyRoot, None),
130            TrieNodeV2::Leaf(leaf) => (TrieNode::Leaf(leaf.clone()), None),
131            TrieNodeV2::Extension(ext) => (TrieNode::Extension(ext.clone()), None),
132            TrieNodeV2::Branch(branch) => (
133                TrieNode::Branch(alloy_trie::nodes::BranchNode::new(
134                    branch.stack.clone(),
135                    branch.state_mask,
136                )),
137                (!branch.key.is_empty()).then_some(branch.key),
138            ),
139        };
140        Self {
141            path: node.path,
142            node: TrieNodeRecord(trie_node),
143            masks: node.masks.map(|masks| (masks.hash_mask.get(), masks.tree_mask.get())),
144            short_key,
145            state: None,
146        }
147    }
148}
149
150/// A newtype wrapper around [`TrieNode`] with custom serialization that hex-encodes byte fields
151/// (leaf values, branch stack entries, extension child pointers) instead of serializing them as
152/// raw integer arrays.
153#[derive(Debug, Clone, PartialEq, Eq)]
154pub struct TrieNodeRecord(pub TrieNode);
155
156impl From<TrieNode> for TrieNodeRecord {
157    fn from(node: TrieNode) -> Self {
158        Self(node)
159    }
160}
161
162impl Serialize for TrieNodeRecord {
163    fn serialize<S: serde::Serializer>(&self, serializer: S) -> Result<S::Ok, S::Error> {
164        use serde::ser::SerializeStructVariant;
165        match &self.0 {
166            TrieNode::EmptyRoot => serializer.serialize_unit_variant("TrieNode", 0, "EmptyRoot"),
167            TrieNode::Branch(branch) => {
168                let stack_hex: Vec<String> =
169                    branch.stack.iter().map(|n| hex::encode(n.as_ref())).collect();
170                let mut sv = serializer.serialize_struct_variant("TrieNode", 1, "Branch", 2)?;
171                sv.serialize_field("stack", &stack_hex)?;
172                sv.serialize_field("state_mask", &branch.state_mask.get())?;
173                sv.end()
174            }
175            TrieNode::Extension(ext) => {
176                let mut sv = serializer.serialize_struct_variant("TrieNode", 2, "Extension", 2)?;
177                sv.serialize_field("key", &ext.key)?;
178                sv.serialize_field("child", &hex::encode(ext.child.as_ref()))?;
179                sv.end()
180            }
181            TrieNode::Leaf(leaf) => {
182                let mut sv = serializer.serialize_struct_variant("TrieNode", 3, "Leaf", 2)?;
183                sv.serialize_field("key", &leaf.key)?;
184                sv.serialize_field("value", &hex::encode(&leaf.value))?;
185                sv.end()
186            }
187        }
188    }
189}
190
191/// A serializable record of a leaf update, mirroring [`crate::LeafUpdate`].
192#[derive(Debug, Clone, PartialEq, Eq, Serialize)]
193pub enum LeafUpdateRecord {
194    /// The leaf value was changed to the given RLP-encoded value.
195    Changed(Bytes),
196    /// The leaf value was touched but the new value is not yet known.
197    Touched,
198}
199
200impl From<&crate::LeafUpdate> for LeafUpdateRecord {
201    fn from(update: &crate::LeafUpdate) -> Self {
202        match update {
203            crate::LeafUpdate::Changed(value) => Self::Changed(value.clone().into()),
204            crate::LeafUpdate::Touched => Self::Touched,
205        }
206    }
207}