diff --git a/crates/common/trie/error.rs b/crates/common/trie/error.rs index e778d1b89b6..3ae654c8b92 100644 --- a/crates/common/trie/error.rs +++ b/crates/common/trie/error.rs @@ -33,6 +33,8 @@ pub enum InconsistentTreeError { RootNotFound(H256), #[error("Root node not found")] RootNotFoundNoHash, + #[error("Node count mismatch during validation: {0}")] + NodeCountMismatch(NodeCountMismatchData), } #[derive(Debug)] @@ -52,3 +54,57 @@ impl std::fmt::Display for ExtensionNodeErrorData { ) } } + +#[derive(Debug, Clone, Copy)] +pub enum NodeType { + Branch, + Extension, + Leaf, +} + +impl std::fmt::Display for NodeType { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + match self { + NodeType::Branch => write!(f, "Branch"), + NodeType::Extension => write!(f, "Extension"), + NodeType::Leaf => write!(f, "Leaf"), + } + } +} + +#[derive(Debug)] +pub struct NodeCountMismatchData { + /// The count mismatch: positive = missing nodes, negative = extra nodes + pub expected_count: isize, + /// Path to the last successfully validated node + pub last_valid_path: Option, + pub last_node_type: Option, + pub nodes_traversed: usize, + /// Hash of the last validated node (for cross-referencing) + pub last_node_hash: Option, +} + +impl std::fmt::Display for NodeCountMismatchData { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + let mismatch = if self.expected_count > 0 { + format!("Expected {} more node(s)", self.expected_count) + } else { + format!("Found {} unexpected node(s)", self.expected_count.abs()) + }; + let node_type = self + .last_node_type + .map(|t| t.to_string()) + .unwrap_or_else(|| "unknown".to_string()); + let hash = self + .last_node_hash + .as_ref() + .map(|h| format!("{:#x}", h)) + .unwrap_or_else(|| "unknown".to_string()); + + write!( + f, + "{}. Traversed {} nodes. Last valid node: {} at path {:?} (hash: {})", + mismatch, self.nodes_traversed, node_type, self.last_valid_path, hash + ) + } +} diff --git a/crates/common/trie/trie.rs b/crates/common/trie/trie.rs index e273972ee38..5cf8a634d26 100644 --- a/crates/common/trie/trie.rs +++ b/crates/common/trie/trie.rs @@ -28,7 +28,9 @@ pub use self::{ node_hash::NodeHash, }; -pub use self::error::{ExtensionNodeErrorData, InconsistentTreeError, TrieError}; +pub use self::error::{ + ExtensionNodeErrorData, InconsistentTreeError, NodeCountMismatchData, NodeType, TrieError, +}; use self::{node::LeafNode, trie_iter::TrieIterator}; use ethrex_rlp::decode::RLPDecode; @@ -533,11 +535,39 @@ impl Trie { /// /// This is used internally with debug assertions to check the status of the trie /// after syncing operations. - /// Note: this operation validates the hashes because the iterator uses - /// get_node_checked. We shouldn't downgrade that to the unchecked version + /// + /// # Returns + /// + /// Returns `Ok(())` if the trie structure is consistent. + /// + /// Returns `Err(TrieError::InconsistentTree(NodeCountMismatch))` if there is a + /// mismatch between expected and actual node counts. The error includes diagnostic + /// information: path to the last validated node, node type, hash, and traversal count. + /// + /// # Note + /// + /// This operation validates the hashes because the iterator uses + /// `get_node_checked`. We shouldn't downgrade that to the unchecked version. pub fn validate(self) -> Result<(), TrieError> { - let mut expected_count = if self.root.is_valid() { 1 } else { 0 }; - for (_, node) in self.into_iter() { + let mut expected_count: isize = if self.root.is_valid() { 1 } else { 0 }; + + // Track context for error reporting + let mut last_valid_path: Option = None; + let mut last_node_type: Option = None; + let mut last_node_hash: Option = None; + let mut nodes_traversed = 0; + + for (path, node) in self.into_iter() { + // Capture context before processing this node + last_valid_path = Some(path.clone()); + last_node_type = Some(match &node { + Node::Branch(_) => NodeType::Branch, + Node::Extension(_) => NodeType::Extension, + Node::Leaf(_) => NodeType::Leaf, + }); + last_node_hash = Some(node.compute_hash().finalize()); + nodes_traversed += 1; + expected_count -= 1; match node { Node::Branch(branch_node) => { @@ -545,7 +575,7 @@ impl Trie { .choices .iter() .filter(|child| child.is_valid()) - .count(); + .count() as isize; } Node::Extension(_) => { expected_count += 1; @@ -553,9 +583,16 @@ impl Trie { Node::Leaf(_) => {} } } + if expected_count != 0 { - return Err(TrieError::Verify(format!( - "Node count mismatch, expected {expected_count} more" + return Err(TrieError::InconsistentTree(Box::new( + InconsistentTreeError::NodeCountMismatch(NodeCountMismatchData { + expected_count, + last_valid_path, + last_node_type, + nodes_traversed, + last_node_hash, + }), ))); } Ok(())