From 32c08b7ddb9234fa743ee39b51ea9ec0627c3c24 Mon Sep 17 00:00:00 2001 From: Brian Picciano Date: Wed, 4 Feb 2026 11:57:36 +0100 Subject: [PATCH] fix(trie): Guard against infinite loop in proof_v2 (#21789) --- crates/evm/execution-errors/src/trie.rs | 7 +++++++ crates/trie/parallel/src/root.rs | 3 +++ crates/trie/trie/src/proof_v2/mod.rs | 25 +++++++++++++++++++++++-- 3 files changed, 33 insertions(+), 2 deletions(-) diff --git a/crates/evm/execution-errors/src/trie.rs b/crates/evm/execution-errors/src/trie.rs index 9d477e5519..e3ee403d45 100644 --- a/crates/evm/execution-errors/src/trie.rs +++ b/crates/evm/execution-errors/src/trie.rs @@ -57,6 +57,12 @@ pub enum StateProofError { /// RLP decoding error. #[error(transparent)] Rlp(#[from] alloy_rlp::Error), + /// Trie inconsistency detected during proof calculation. + /// + /// This occurs when cached trie nodes disagree with the leaf data, causing + /// proof calculation to be unable to make forward progress. + #[error("trie inconsistency: {0}")] + TrieInconsistency(alloc::string::String), } impl From for ProviderError { @@ -64,6 +70,7 @@ impl From for ProviderError { match value { StateProofError::Database(error) => Self::Database(error), StateProofError::Rlp(error) => Self::Rlp(error), + StateProofError::TrieInconsistency(msg) => Self::Database(DatabaseError::Other(msg)), } } } diff --git a/crates/trie/parallel/src/root.rs b/crates/trie/parallel/src/root.rs index 72f62b56fa..cffdf083bf 100644 --- a/crates/trie/parallel/src/root.rs +++ b/crates/trie/parallel/src/root.rs @@ -264,6 +264,9 @@ impl From for ParallelStateRootError { match error { StateProofError::Database(err) => Self::Provider(ProviderError::Database(err)), StateProofError::Rlp(err) => Self::Provider(ProviderError::Rlp(err)), + StateProofError::TrieInconsistency(msg) => { + Self::Provider(ProviderError::TrieWitnessError(msg)) + } } } } diff --git a/crates/trie/trie/src/proof_v2/mod.rs b/crates/trie/trie/src/proof_v2/mod.rs index 0696ca1668..381ddaa445 100644 --- a/crates/trie/trie/src/proof_v2/mod.rs +++ b/crates/trie/trie/src/proof_v2/mod.rs @@ -17,7 +17,7 @@ use alloy_trie::{BranchNodeCompact, TrieMask}; use reth_execution_errors::trie::StateProofError; use reth_trie_common::{BranchNode, BranchNodeMasks, Nibbles, ProofTrieNode, RlpNode, TrieNode}; use std::cmp::Ordering; -use tracing::{instrument, trace}; +use tracing::{error, instrument, trace}; mod value; pub use value::*; @@ -1159,13 +1159,16 @@ where trace!(target: TRACE_TARGET, "Starting loop"); loop { + // Save the previous lower bound to detect forward progress. + let prev_uncalculated_lower_bound = uncalculated_lower_bound; + // Determine the range of keys of the overall trie which need to be re-computed. let Some((calc_lower_bound, calc_upper_bound)) = self.next_uncached_key_range( &mut targets, trie_cursor_state, &sub_trie_targets.prefix, sub_trie_upper_bound.as_ref(), - uncalculated_lower_bound, + prev_uncalculated_lower_bound, )? else { // If `next_uncached_key_range` determines that there can be no more keys then @@ -1173,6 +1176,24 @@ where break; }; + // Forward-progress guard: detect trie inconsistencies that would cause infinite loops. + // If `next_uncached_key_range` returns a range that starts before the previous + // lower bound, we've gone backwards and would loop forever. + // + // This can specifically happen when there is a cached branch which shouldn't exist, or + // if state mask bit is set on a cached branch which shouldn't be. + if let Some(prev_lower) = prev_uncalculated_lower_bound.as_ref() && + calc_lower_bound < *prev_lower + { + let msg = format!( + "next_uncached_key_range went backwards: calc_lower={calc_lower_bound:?} < \ + prev_lower={prev_lower:?}, calc_upper={calc_upper_bound:?}, prefix={:?}", + sub_trie_targets.prefix, + ); + error!(target: TRACE_TARGET, "{msg}"); + return Err(StateProofError::TrieInconsistency(msg)); + } + // Calculate the trie for that range of keys self.calculate_key_range( value_encoder,