perf(trie): Don't filter proofs in v2 if sparse trie as cache is enabled (#21811)

This commit is contained in:
Brian Picciano
2026-02-05 12:14:55 +01:00
committed by GitHub
parent c9cc118def
commit a92aca2549
2 changed files with 90 additions and 3 deletions

View File

@@ -563,7 +563,7 @@ where
from_multi_proof, from_multi_proof,
proof_worker_handle, proof_worker_handle,
trie_metrics.clone(), trie_metrics.clone(),
sparse_state_trie, sparse_state_trie.with_skip_proof_node_filtering(true),
chunk_size, chunk_size,
)) ))
}; };

View File

@@ -47,6 +47,10 @@ pub struct SparseStateTrie<
storage: StorageTries<S>, storage: StorageTries<S>,
/// Flag indicating whether trie updates should be retained. /// Flag indicating whether trie updates should be retained.
retain_updates: bool, retain_updates: bool,
/// When true, skip filtering of V2 proof nodes that have already been revealed.
/// This is useful when the sparse trie is being reused across blocks and already
/// tracks revealed nodes internally.
skip_proof_node_filtering: bool,
/// Reusable buffer for RLP encoding of trie accounts. /// Reusable buffer for RLP encoding of trie accounts.
account_rlp_buf: Vec<u8>, account_rlp_buf: Vec<u8>,
/// Holds data that should be dropped after final state root is calculated. /// Holds data that should be dropped after final state root is calculated.
@@ -67,6 +71,7 @@ where
revealed_account_paths: Default::default(), revealed_account_paths: Default::default(),
storage: Default::default(), storage: Default::default(),
retain_updates: false, retain_updates: false,
skip_proof_node_filtering: false,
account_rlp_buf: Vec::with_capacity(TRIE_ACCOUNT_RLP_MAX_SIZE), account_rlp_buf: Vec::with_capacity(TRIE_ACCOUNT_RLP_MAX_SIZE),
deferred_drops: DeferredDrops::default(), deferred_drops: DeferredDrops::default(),
#[cfg(feature = "metrics")] #[cfg(feature = "metrics")]
@@ -119,6 +124,16 @@ impl<A, S> SparseStateTrie<A, S> {
self self
} }
/// Set whether to skip filtering of V2 proof nodes.
///
/// When true, `reveal_*_v2_proof_nodes` methods will pass all nodes directly to the
/// sparse trie without filtering already-revealed paths. This is useful when the
/// sparse trie is being reused across blocks and handles node deduplication internally.
pub const fn with_skip_proof_node_filtering(mut self, skip: bool) -> Self {
self.skip_proof_node_filtering = skip;
self
}
/// Takes the data structures for deferred dropping. /// Takes the data structures for deferred dropping.
/// ///
/// This allows the caller to drop the buffers later, avoiding expensive deallocations while /// This allows the caller to drop the buffers later, avoiding expensive deallocations while
@@ -381,6 +396,7 @@ where
use reth_primitives_traits::ParallelBridgeBuffered; use reth_primitives_traits::ParallelBridgeBuffered;
let retain_updates = self.retain_updates; let retain_updates = self.retain_updates;
let skip_filtering = self.skip_proof_node_filtering;
// Process all storage trie revealings in parallel, having first removed the // Process all storage trie revealings in parallel, having first removed the
// `reveal_nodes` tracking and `RevealableSparseTrie`s for each account from their // `reveal_nodes` tracking and `RevealableSparseTrie`s for each account from their
@@ -403,6 +419,7 @@ where
&mut trie, &mut trie,
&mut bufs, &mut bufs,
retain_updates, retain_updates,
skip_filtering,
); );
(account, result, revealed_nodes, trie, bufs) (account, result, revealed_nodes, trie, bufs)
}) })
@@ -493,8 +510,33 @@ where
/// so no separate masks map is needed. /// so no separate masks map is needed.
pub fn reveal_account_v2_proof_nodes( pub fn reveal_account_v2_proof_nodes(
&mut self, &mut self,
nodes: Vec<ProofTrieNode>, mut nodes: Vec<ProofTrieNode>,
) -> SparseStateTrieResult<()> { ) -> SparseStateTrieResult<()> {
if self.skip_proof_node_filtering {
let capacity = estimate_v2_proof_capacity(&nodes);
#[cfg(feature = "metrics")]
self.metrics.increment_total_account_nodes(nodes.len() as u64);
let root_node = nodes.iter().find(|n| n.path.is_empty());
let trie = if let Some(root_node) = root_node {
trace!(target: "trie::sparse", ?root_node, "Revealing root account node from V2 proof");
self.state.reveal_root(
root_node.node.clone(),
root_node.masks,
self.retain_updates,
)?
} else {
self.state.as_revealed_mut().ok_or(SparseTrieErrorKind::Blind)?
};
trie.reserve_nodes(capacity);
trace!(target: "trie::sparse", total_nodes = ?nodes.len(), "Revealing account nodes from V2 proof (unfiltered)");
trie.reveal_nodes(&mut nodes)?;
self.deferred_drops.proof_nodes_bufs.push(nodes);
return Ok(())
}
let FilteredV2ProofNodes { root_node, mut nodes, new_nodes, metric_values: _metric_values } = let FilteredV2ProofNodes { root_node, mut nodes, new_nodes, metric_values: _metric_values } =
filter_revealed_v2_proof_nodes(nodes, &mut self.revealed_account_paths)?; filter_revealed_v2_proof_nodes(nodes, &mut self.revealed_account_paths)?;
@@ -539,6 +581,7 @@ where
trie, trie,
&mut self.deferred_drops.proof_nodes_bufs, &mut self.deferred_drops.proof_nodes_bufs,
self.retain_updates, self.retain_updates,
self.skip_proof_node_filtering,
)?; )?;
#[cfg(feature = "metrics")] #[cfg(feature = "metrics")]
@@ -554,12 +597,33 @@ where
/// designed to handle a variety of associated public functions. /// designed to handle a variety of associated public functions.
fn reveal_storage_v2_proof_nodes_inner( fn reveal_storage_v2_proof_nodes_inner(
account: B256, account: B256,
nodes: Vec<ProofTrieNode>, mut nodes: Vec<ProofTrieNode>,
revealed_nodes: &mut HashSet<Nibbles>, revealed_nodes: &mut HashSet<Nibbles>,
trie: &mut RevealableSparseTrie<S>, trie: &mut RevealableSparseTrie<S>,
bufs: &mut Vec<Vec<ProofTrieNode>>, bufs: &mut Vec<Vec<ProofTrieNode>>,
retain_updates: bool, retain_updates: bool,
skip_filtering: bool,
) -> SparseStateTrieResult<ProofNodesMetricValues> { ) -> SparseStateTrieResult<ProofNodesMetricValues> {
if skip_filtering {
let capacity = estimate_v2_proof_capacity(&nodes);
let metric_values =
ProofNodesMetricValues { total_nodes: nodes.len(), skipped_nodes: 0 };
let root_node = nodes.iter().find(|n| n.path.is_empty());
let trie = if let Some(root_node) = root_node {
trace!(target: "trie::sparse", ?account, ?root_node, "Revealing root storage node from V2 proof");
trie.reveal_root(root_node.node.clone(), root_node.masks, retain_updates)?
} else {
trie.as_revealed_mut().ok_or(SparseTrieErrorKind::Blind)?
};
trie.reserve_nodes(capacity);
trace!(target: "trie::sparse", ?account, total_nodes = ?nodes.len(), "Revealing storage nodes from V2 proof (unfiltered)");
trie.reveal_nodes(&mut nodes)?;
bufs.push(nodes);
return Ok(metric_values)
}
let FilteredV2ProofNodes { root_node, mut nodes, new_nodes, metric_values } = let FilteredV2ProofNodes { root_node, mut nodes, new_nodes, metric_values } =
filter_revealed_v2_proof_nodes(nodes, revealed_nodes)?; filter_revealed_v2_proof_nodes(nodes, revealed_nodes)?;
@@ -1533,6 +1597,29 @@ struct FilteredV2ProofNodes {
metric_values: ProofNodesMetricValues, metric_values: ProofNodesMetricValues,
} }
/// Calculates capacity estimation for V2 proof nodes without filtering.
///
/// This counts nodes and their children (for branch and extension nodes) to provide
/// proper capacity hints for `reserve_nodes`. Used when `skip_proof_node_filtering` is
/// enabled and no filtering is performed.
fn estimate_v2_proof_capacity(nodes: &[ProofTrieNode]) -> usize {
let mut capacity = nodes.len();
for node in nodes {
match &node.node {
TrieNode::Branch(branch) => {
capacity += branch.state_mask.count_ones() as usize;
}
TrieNode::Extension(_) => {
capacity += 1;
}
_ => {}
};
}
capacity
}
/// Filters V2 proof nodes that are already revealed, separates the root node if present, and /// Filters V2 proof nodes that are already revealed, separates the root node if present, and
/// returns additional information about the number of total, skipped, and new nodes. /// returns additional information about the number of total, skipped, and new nodes.
/// ///