Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
42 changes: 28 additions & 14 deletions crates/tn-reth/src/env/helpers.rs
Original file line number Diff line number Diff line change
Expand Up @@ -596,20 +596,34 @@ impl CanonicalExecutionReader for RethEnv {
/// whether `number` is *durably* canonical, so an in-memory-tip-aware read would defeat the
/// question by reporting speculatively executed blocks as confirmed.
fn canonical_execution_hash(&self, number: BlockNumber) -> Option<B256> {
// A read error (e.g. a transient provider/DB error) is treated as "not confirmed" rather
// than as a canonical match, so the caller keeps its conservative fork handling.
self.sealed_header_by_number(number)
.inspect_err(|error| {
tracing::debug!(
target: "tn::reth",
?error,
number,
"canonical_execution_hash: canonical DB read failed; treating as unconfirmed"
);
})
.ok()
.flatten()
.map(|header| header.hash())
// A snapshot-restored datadir scaffolds the whole region below its restored-state floor
// `B`: zero-hash dummy headers below the window and real-hashed but stateless window
// headers within it. None of these are blocks this node executed, so confirming one as
// canonical would let a peer's header that references a scaffold height clear the
// execution-result check — a zero-hash dummy reads back as its own `B256::ZERO` hash and
// matches a poison `latest_execution_block`. The region below the floor is refused here,
// mirroring `read_only_state_db`'s floor guard, so the DB fallback can never attest it.
self.inner.restored_state_floor.filter(|floor| number < *floor).map_or_else(
// not below the floor (or a normally-synced node): answer from the canonical DB.
// A read error (e.g. a transient provider/DB error) is treated as "not confirmed"
// rather than as a canonical match, so the caller keeps its conservative fork handling.
|| {
self.sealed_header_by_number(number)
.inspect_err(|error| {
tracing::debug!(
target: "tn::reth",
?error,
number,
"canonical_execution_hash: canonical DB read failed; treating as unconfirmed"
);
})
.ok()
.flatten()
.map(|header| header.hash())
},
// below the restored-state floor: the scaffold region, never attested as canonical.
|_floor| None,
)
}
}

Expand Down
24 changes: 24 additions & 0 deletions crates/tn-reth/src/snapshot.rs
Original file line number Diff line number Diff line change
Expand Up @@ -2536,6 +2536,7 @@ mod tests {
#[tokio::test]
async fn scaffold_persists_restored_state_floor_and_env_refuses_below_it() -> eyre::Result<()> {
use crate::error::StateReadError;
use tn_types::CanonicalExecutionReader;

let chain: Arc<RethChainSpec> = Arc::new(test_genesis().into());
let dst_dir = TempDir::new()?;
Expand Down Expand Up @@ -2632,6 +2633,29 @@ mod tests {
"the floor must not refuse a pin at the floor itself, got: {err}"
);

// #1323: the CanonicalExecutionReader DB fallback (consensus `wait_for_execution`) must
// refuse the whole scaffold region below the floor. Below `B` it attests no block — not
// even a window-interior height whose header carries a real hash, and certainly not a
// zero-hash dummy that would otherwise read back as its own `B256::ZERO`. So a peer's
// header referencing a scaffold `latest_execution_block` can never clear the execution
// check on a restored node, which is what let the poison certify network-wide. At the
// floor the reader answers truthfully from the DB.
assert_eq!(
env.canonical_execution_hash(2),
None,
"a below-floor height must never be attested as canonical execution"
);
assert_eq!(
env.canonical_execution_hash(3),
None,
"a window-interior height below B must never be attested as canonical execution"
);
assert_eq!(
env.canonical_execution_hash(5),
Some(h5.hash()),
"the block at the floor must resolve to its real canonical execution hash"
);

Ok(())
}

Expand Down
Loading