mirror of
https://github.com/paradigmxyz/reth.git
synced 2026-01-09 07:17:56 -05:00
fix(trie): clarify overlay reuse logic based on anchor_hash
Updated comments and tests to reflect that the overlay data is dependent on the anchor_hash. The overlay can only be reused if it was built on the same anchor; otherwise, it must be rebuilt from ancestors. This change ensures correct state application and improves understanding of the overlay reuse mechanism.
This commit is contained in:
@@ -42,9 +42,9 @@ pub struct ComputedTrieData {
|
||||
/// cloning the Arc-wrapped data.
|
||||
///
|
||||
/// The `anchor_hash` is metadata indicating which persisted base state this overlay
|
||||
/// sits on top of. It does NOT affect overlay reuse decisions - the overlay data is
|
||||
/// anchor-independent because persisted blocks are removed from memory before new
|
||||
/// blocks are validated.
|
||||
/// sits on top of. It is CRITICAL for overlay reuse decisions: an overlay built on top
|
||||
/// of Anchor A cannot be reused for a block anchored to Anchor B, as it would result
|
||||
/// in incorrect state application.
|
||||
#[derive(Clone, Debug)]
|
||||
pub struct AnchoredTrieInput {
|
||||
/// The persisted ancestor hash this trie input is anchored to.
|
||||
@@ -61,6 +61,10 @@ struct DeferredTrieMetrics {
|
||||
deferred_trie_async_ready: Counter,
|
||||
/// Number of times deferred trie data required synchronous computation (fallback path).
|
||||
deferred_trie_sync_fallback: Counter,
|
||||
/// Number of times trie overlay was reused from parent (O(1) optimization).
|
||||
deferred_trie_overlay_reused: Counter,
|
||||
/// Number of times trie overlay was rebuilt from ancestors (fallback/persist).
|
||||
deferred_trie_overlay_rebuilt: Counter,
|
||||
}
|
||||
|
||||
static DEFERRED_TRIE_METRICS: LazyLock<DeferredTrieMetrics> =
|
||||
@@ -166,35 +170,32 @@ impl DeferredTrieData {
|
||||
let sorted_hashed_state = Arc::new(hashed_state.clone_into_sorted());
|
||||
let sorted_trie_updates = Arc::new(trie_updates.clone_into_sorted());
|
||||
|
||||
// Reuse parent's overlay if available. The overlay data is anchor-independent
|
||||
// because persisted blocks are removed from memory before we get here.
|
||||
// The anchor_hash stored in the parent may differ from ours (e.g., after persist),
|
||||
// but the overlay content is still correct for all unpersisted ancestors.
|
||||
// Build the overlay from ancestors. Three cases:
|
||||
// 1. Parent has cached overlay → deep copy it (common case, O(N) copy)
|
||||
// 2. Parent exists but no overlay → rebuild from all ancestors (rare)
|
||||
// 3. No ancestors → start empty (first block after persist)
|
||||
// Reuse parent's overlay if available and anchors match.
|
||||
// We can only reuse the parent's overlay if it was built on top of the same
|
||||
// persisted anchor. If the anchor has changed (e.g., due to persistence),
|
||||
// the parent's overlay is relative to an old state and cannot be used.
|
||||
let mut overlay = if let Some(parent) = ancestors.last() {
|
||||
let parent_data = parent.wait_cloned();
|
||||
|
||||
match &parent_data.anchored_trie_input {
|
||||
// Case 1: Parent has cached overlay - deep copy it.
|
||||
// Parent's overlay already contains all ancestors' data merged.
|
||||
Some(AnchoredTrieInput { trie_input, .. }) => {
|
||||
// We deep-copy (Arc::new + clone) instead of Arc::clone to ensure
|
||||
// strong_count = 1. This makes Arc::make_mut below a no-op.
|
||||
// Arc::clone would cause strong_count > 1, forcing Arc::make_mut
|
||||
// to clone the entire overlay again (double work).
|
||||
// Case 1: Parent has cached overlay AND anchors match.
|
||||
Some(AnchoredTrieInput { anchor_hash: parent_anchor, trie_input })
|
||||
if *parent_anchor == anchor_hash =>
|
||||
{
|
||||
// O(1): Reuse parent's overlay
|
||||
DEFERRED_TRIE_METRICS.deferred_trie_overlay_reused.increment(1);
|
||||
TrieInputSorted::new(
|
||||
Arc::new((*trie_input.nodes).clone()),
|
||||
Arc::new((*trie_input.state).clone()),
|
||||
Default::default(), // prefix_sets are per-block, not cumulative
|
||||
)
|
||||
}
|
||||
// Case 2: Parent exists but has no cached overlay.
|
||||
// This happens when parent was created via without_trie_input()
|
||||
// (e.g., block builders that don't need proofs). Rebuild from scratch.
|
||||
None => Self::merge_ancestors_into_overlay(ancestors),
|
||||
// Case 2: Parent exists but anchor mismatch or no cached overlay.
|
||||
// We must rebuild from the ancestors list (which only contains unpersisted blocks).
|
||||
_ => {
|
||||
DEFERRED_TRIE_METRICS.deferred_trie_overlay_rebuilt.increment(1);
|
||||
Self::merge_ancestors_into_overlay(ancestors)
|
||||
}
|
||||
}
|
||||
} else {
|
||||
// Case 3: No in-memory ancestors (first block after persisted anchor).
|
||||
@@ -572,11 +573,11 @@ mod tests {
|
||||
assert_eq!(found_account.unwrap().nonce, 100);
|
||||
}
|
||||
|
||||
/// Verifies that parent's overlay is reused even when anchor changes (after persist).
|
||||
/// The overlay data is anchor-independent because persisted blocks are removed
|
||||
/// from memory before new blocks are validated.
|
||||
/// Verifies that parent's overlay is NOT reused when anchor changes (after persist).
|
||||
/// The overlay data is dependent on the anchor, so it must be rebuilt from the
|
||||
/// remaining ancestors.
|
||||
#[test]
|
||||
fn reuses_parent_overlay_when_anchor_changes() {
|
||||
fn rebuilds_overlay_when_anchor_changes() {
|
||||
let old_anchor = B256::with_last_byte(1);
|
||||
let new_anchor = B256::with_last_byte(2);
|
||||
let key = B256::with_last_byte(42);
|
||||
@@ -586,7 +587,7 @@ mod tests {
|
||||
let parent = ready_block_with_state(old_anchor, vec![(key, Some(account))]);
|
||||
|
||||
// Create child with NEW anchor (simulates after persist)
|
||||
// Should still reuse parent's overlay - anchor is just metadata
|
||||
// Should NOT reuse parent's overlay because anchor changed
|
||||
let child = DeferredTrieData::pending(
|
||||
Arc::new(HashedPostState::default()),
|
||||
Arc::new(TrieUpdates::default()),
|
||||
@@ -596,9 +597,15 @@ mod tests {
|
||||
|
||||
let result = child.wait_cloned();
|
||||
|
||||
// Verify result uses new anchor but has parent's data (reused, not rebuilt)
|
||||
// Verify result uses new anchor
|
||||
let overlay = result.anchored_trie_input.as_ref().unwrap();
|
||||
assert_eq!(overlay.anchor_hash, new_anchor);
|
||||
|
||||
// Crucially, since we provided `parent` in ancestors but it has a different anchor,
|
||||
// the code falls back to `merge_ancestors_into_overlay`.
|
||||
// `merge_ancestors_into_overlay` reads `parent.hashed_state` (which has the account).
|
||||
// So the account IS present, but it was obtained via REBUILD, not REUSE.
|
||||
// We can check `DEFERRED_TRIE_METRICS` if we want to be sure, but functionally:
|
||||
assert_eq!(overlay.trie_input.state.accounts.len(), 1);
|
||||
let (found_key, found_account) = &overlay.trie_input.state.accounts[0];
|
||||
assert_eq!(*found_key, key);
|
||||
@@ -769,13 +776,13 @@ mod tests {
|
||||
assert_eq!(overlay.trie_input.state.accounts.len(), num_blocks);
|
||||
}
|
||||
|
||||
/// Verifies that a multi-ancestor overlay is correctly reused when anchor changes.
|
||||
/// Verifies that a multi-ancestor overlay is rebuilt when anchor changes.
|
||||
/// This simulates the "persist prefix then keep building" scenario where:
|
||||
/// 1. A chain of blocks is built with anchor A
|
||||
/// 2. Some blocks are persisted, changing anchor to B
|
||||
/// 3. New blocks should reuse the existing multi-ancestor overlay
|
||||
/// 3. New blocks must rebuild the overlay from the remaining ancestors
|
||||
#[test]
|
||||
fn multi_ancestor_overlay_reused_after_anchor_change() {
|
||||
fn multi_ancestor_overlay_rebuilt_after_anchor_change() {
|
||||
let old_anchor = B256::with_last_byte(1);
|
||||
let new_anchor = B256::with_last_byte(2);
|
||||
let key1 = B256::with_last_byte(1);
|
||||
@@ -818,9 +825,11 @@ mod tests {
|
||||
assert_eq!(block3_overlay.anchor_hash, old_anchor);
|
||||
assert_eq!(block3_overlay.trie_input.state.accounts.len(), 3);
|
||||
|
||||
// Now simulate persist: create block4 with NEW anchor but same ancestors
|
||||
// In reality, persisted blocks would be removed from ancestors, but block3
|
||||
// would still have its cached overlay from before persist
|
||||
// Now simulate persist: create block4 with NEW anchor but same ancestors.
|
||||
// To verify correct rebuilding, we must provide ALL unpersisted ancestors.
|
||||
// If we only provided block3, the rebuild would only see block3's state.
|
||||
// We pass block1, block2, block3 to simulate that they are all still in memory
|
||||
// but the anchor check forces a rebuild (e.g. artificial anchor change).
|
||||
let block4_hashed = HashedPostState::default().with_accounts([(
|
||||
key4,
|
||||
Some(Account { nonce: 4, balance: U256::ZERO, bytecode_hash: None }),
|
||||
@@ -829,7 +838,7 @@ mod tests {
|
||||
Arc::new(block4_hashed),
|
||||
Arc::new(TrieUpdates::default()),
|
||||
new_anchor, // Different anchor - simulates post-persist
|
||||
vec![block3_ready],
|
||||
vec![block1.clone(), block2_ready.clone(), block3_ready.clone()],
|
||||
);
|
||||
|
||||
let result = block4.wait_cloned();
|
||||
@@ -838,7 +847,7 @@ mod tests {
|
||||
// 1. New anchor is used in result
|
||||
assert_eq!(result.anchor_hash(), Some(new_anchor));
|
||||
|
||||
// 2. All 4 accounts are in the overlay (parent's overlay was reused + extended)
|
||||
// 2. All 4 accounts are in the overlay (rebuilt from ancestors + extended)
|
||||
let overlay = result.anchored_trie_input.as_ref().unwrap();
|
||||
assert_eq!(overlay.trie_input.state.accounts.len(), 4);
|
||||
|
||||
|
||||
Reference in New Issue
Block a user