Skip to content

Commit 63c61b2

Browse files
authored
feat(l1): deep-reorg orchestration (#6689)
> Part of the deep-reorg support effort tracked in #6685. This is PR 3 of 4, stacked on #6687. Wires up the orchestration layer; the deep-reorg path is scaffolded but dormant until PR 4 lifts the cap. ## Summary - Adds `install_overlay_for_reorg`, `abort_reorg`, `clear_reorg_overlay`, `is_state_in_layer_cache`, and `highest_state_history_block_number` to `store.rs`. - Section 9 reconciliation in `apply_trie_updates`: on the first new-chain commit after a deep reorg, overlay-only entries are folded into the write batch, the old `STATE_HISTORY[T..=D]` range is deleted, and the overlay is cleared. Disk advances from the OLD chain's edge directly to the new tip in one atomic write. - `reorg_in_progress: AtomicBool` + `ReorgGuard` RAII in `Blockchain`; concurrent FCUs short-circuit to `SYNCING` while a reorg apply pass is in flight. - `apply_fork_choice_with_deep_reorg` in `fork_choice.rs`: routes `StateNotReachable` into `reorg_apply_deep`, all other results fall through unchanged. `AbortReorgGuard` resets the cache on any failure or panic. - Engine API `fork_choice.rs` handler switched to the new wrapper (takes `&context.blockchain`). `try_execute_payload` stashes blocks and returns `ACCEPTED` when the parent is known but its state is not materialized; a later `forkchoiceUpdated` drives the apply. - Removes stale `#[allow(dead_code)]` markers from layering.rs now that the overlay accessors are consumed. - 4 storage unit tests for the new primitives; 2 `ReorgGuard` tests including panic-unwind. ## What's in this PR - `crates/storage/store.rs`: `install_overlay_for_reorg`, `abort_reorg`, `clear_reorg_overlay`, `is_state_in_layer_cache`, `highest_state_history_block_number`, Section 9 reconciliation inside `apply_trie_updates`. 4 new tests. - `crates/blockchain/blockchain.rs`: `reorg_in_progress: AtomicBool`, `ReorgGuard`, `enter_reorg`, `is_reorg_in_progress`, `store()` accessor. 2 new tests. - `crates/blockchain/fork_choice.rs`: `apply_fork_choice_with_deep_reorg`, `reorg_apply_deep`, `AbortReorgGuard`, `map_chain_error_for_fcu`. Head is included in the replay iterator via `chain(once((head.number, head_hash)))`, fixing the missing-head bug from #6561. - `crates/networking/rpc/engine/fork_choice.rs`: switched to `apply_fork_choice_with_deep_reorg`. - `crates/networking/rpc/engine/payload.rs`: ACCEPTED stash path for unmaterialized parent state. - `crates/networking/rpc/types/payload.rs`: `PayloadStatus::accepted()` constructor. - `crates/storage/layering.rs`: drop stale `#[allow(dead_code)]` attrs; `len`/`is_empty` keep theirs (PR 4 target). ## What's NOT in this PR - The 128-block `REORG_DEPTH_LIMIT` cap is not lifted; the deep-reorg path is unreachable on mainline until PR 4. Behavior for end users is unchanged after this merge. - `tooling/reorgs` deep-reorg simulator scenario: deferred to PR 4 (requires a CLI knob or cap removal to be end-to-end reachable). - `map_chain_error_for_fcu` collapses all `ChainError` variants to `StateNotReachable`; a richer mapping with `InvalidAncestor` + last-valid-hash is follow-up. - Crash-during-reconciliation injection test (open question in #6685): deferred. - Historical state RPC queries: not in #6685's scope. ## Test plan - `cargo build --workspace` clean. - `cargo clippy --workspace --no-deps -- -D warnings` clean. - `cargo fmt --check` clean. - `cargo test -p ethrex-storage` (45/45 pass, includes 4 new deep-reorg primitive tests). - `cargo test -p ethrex-blockchain` (2/2 new `ReorgGuard` tests pass, including panic-unwind). ## References - Tracking issue: #6685 - PR 1 (state-history journal): #6686 - PR 2 (overlay + read cascade): #6687
1 parent 6bd0bde commit 63c61b2

9 files changed

Lines changed: 1105 additions & 36 deletions

File tree

Cargo.lock

Lines changed: 1 addition & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

crates/blockchain/Cargo.toml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -36,6 +36,7 @@ tokio-util.workspace = true
3636
[dev-dependencies]
3737
tokio = { workspace = true, features = ["full"] }
3838
ethrex-levm.workspace = true
39+
tempfile.workspace = true
3940

4041
[lib]
4142
path = "./blockchain.rs"

crates/blockchain/blockchain.rs

Lines changed: 106 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -219,6 +219,12 @@ pub struct Blockchain {
219219
/// Set to true after initial sync completes, never reset to false.
220220
/// Does not reflect whether an ongoing sync is in progress.
221221
is_synced: AtomicBool,
222+
/// Set while a deep-reorg apply pass is in flight (issue #6685). Concurrent
223+
/// FCUs from the engine API short-circuit to SYNCING while this is set,
224+
/// and journal pruning in `forkchoice_update_inner` defers until the apply
225+
/// pass clears it. Managed via [`enter_reorg`](Self::enter_reorg) which
226+
/// returns a [`ReorgGuard`] RAII that clears the flag on drop.
227+
reorg_in_progress: AtomicBool,
222228
/// Configuration options for blockchain behavior.
223229
pub options: BlockchainOptions,
224230
/// Cache of recently built payloads.
@@ -268,6 +274,21 @@ impl std::fmt::Debug for PrewarmedCache {
268274
}
269275
}
270276

277+
/// RAII guard that clears [`Blockchain::reorg_in_progress`] on drop. Returned by
278+
/// [`Blockchain::enter_reorg`]. Ensures the flag is reset on every exit path of
279+
/// the apply pass, including panics that unwind through the guard.
280+
pub struct ReorgGuard<'a> {
281+
blockchain: &'a Blockchain,
282+
}
283+
284+
impl Drop for ReorgGuard<'_> {
285+
fn drop(&mut self) {
286+
self.blockchain
287+
.reorg_in_progress
288+
.store(false, Ordering::Release);
289+
}
290+
}
291+
271292
/// Configuration options for the blockchain.
272293
#[derive(Debug, Clone)]
273294
pub struct BlockchainOptions {
@@ -433,6 +454,7 @@ impl Blockchain {
433454
storage: store,
434455
mempool: Mempool::new(blockchain_opts.max_mempool_size),
435456
is_synced: AtomicBool::new(false),
457+
reorg_in_progress: AtomicBool::new(false),
436458
payloads: Arc::new(TokioMutex::new(Vec::new())),
437459
options: blockchain_opts,
438460
merkle_pool: Self::build_merkle_pool(),
@@ -454,6 +476,7 @@ impl Blockchain {
454476
storage: store,
455477
mempool: Mempool::new(MAX_MEMPOOL_SIZE_DEFAULT),
456478
is_synced: AtomicBool::new(false),
479+
reorg_in_progress: AtomicBool::new(false),
457480
payloads: Arc::new(TokioMutex::new(Vec::new())),
458481
options: BlockchainOptions::default(),
459482
merkle_pool: pool,
@@ -466,6 +489,7 @@ impl Blockchain {
466489
storage: store,
467490
mempool: Mempool::new(MAX_MEMPOOL_SIZE_DEFAULT),
468491
is_synced: AtomicBool::new(false),
492+
reorg_in_progress: AtomicBool::new(false),
469493
payloads: Arc::new(TokioMutex::new(Vec::new())),
470494
options: BlockchainOptions::default(),
471495
merkle_pool: Self::build_merkle_pool(),
@@ -493,6 +517,33 @@ impl Blockchain {
493517
Ok(())
494518
}
495519

520+
/// Returns a reference to the underlying [`Store`]. Used by the deep-reorg
521+
/// orchestrator to drive the storage-side primitives.
522+
pub fn store(&self) -> &Store {
523+
&self.storage
524+
}
525+
526+
/// Returns `true` while a deep-reorg apply pass is in flight. Set by
527+
/// [`enter_reorg`](Self::enter_reorg); the engine API's FCU handler should
528+
/// short-circuit to SYNCING when this is `true`.
529+
pub fn is_reorg_in_progress(&self) -> bool {
530+
self.reorg_in_progress.load(Ordering::Acquire)
531+
}
532+
533+
/// Attempts to mark the start of a deep-reorg apply pass. Returns a RAII
534+
/// guard that clears the flag on drop (success, error, or panic) if no other
535+
/// pass is in flight, or `None` if one already is. This is a test-and-set:
536+
/// the flag transition `false -> true` is atomic, so two concurrent FCUs
537+
/// that both reach the deep path can never both acquire the guard. The loser
538+
/// short-circuits to SYNCING and the CL retries. At most one apply pass runs
539+
/// at a time.
540+
pub fn enter_reorg(&self) -> Option<ReorgGuard<'_>> {
541+
self.reorg_in_progress
542+
.compare_exchange(false, true, Ordering::AcqRel, Ordering::Acquire)
543+
.is_ok()
544+
.then_some(ReorgGuard { blockchain: self })
545+
}
546+
496547
/// Executes a block withing a new vm instance and state
497548
fn execute_block(
498549
&self,
@@ -4541,3 +4592,58 @@ pub fn compute_sharded_storage_root(
45414592

45424593
Ok((root_hash.finalize(&NativeCrypto), nodes))
45434594
}
4595+
4596+
#[cfg(test)]
4597+
mod reorg_guard_tests {
4598+
use super::*;
4599+
use ethrex_storage::{EngineType, Store};
4600+
4601+
fn make_blockchain() -> Blockchain {
4602+
let dir = tempfile::tempdir().unwrap();
4603+
let store = Store::new(dir.path(), EngineType::InMemory).unwrap();
4604+
Blockchain::default_with_store(store)
4605+
}
4606+
4607+
/// `enter_reorg` SHALL set the flag; the returned guard SHALL clear it on drop.
4608+
#[test]
4609+
fn reorg_guard_sets_and_clears_flag() {
4610+
let blockchain = make_blockchain();
4611+
assert!(!blockchain.is_reorg_in_progress(), "flag starts false");
4612+
4613+
{
4614+
let guard = blockchain.enter_reorg();
4615+
assert!(guard.is_some(), "first enter_reorg must acquire the guard");
4616+
assert!(
4617+
blockchain.is_reorg_in_progress(),
4618+
"flag must be set while guard is alive"
4619+
);
4620+
// A second attempt while the first guard is alive must fail the
4621+
// test-and-set and return None.
4622+
assert!(
4623+
blockchain.enter_reorg().is_none(),
4624+
"concurrent enter_reorg must not acquire a second guard"
4625+
);
4626+
}
4627+
assert!(
4628+
!blockchain.is_reorg_in_progress(),
4629+
"flag must clear when guard drops"
4630+
);
4631+
}
4632+
4633+
/// The guard SHALL clear the flag even when its scope ends via panic unwinding.
4634+
#[test]
4635+
fn reorg_guard_clears_flag_on_panic() {
4636+
let blockchain = make_blockchain();
4637+
let result = std::panic::catch_unwind(std::panic::AssertUnwindSafe(|| {
4638+
let _guard = blockchain.enter_reorg();
4639+
assert!(blockchain.is_reorg_in_progress());
4640+
panic!("simulated apply failure");
4641+
}));
4642+
// guard is dropped by the unwind; flag must be clear below.
4643+
assert!(result.is_err(), "panic must propagate out of the scope");
4644+
assert!(
4645+
!blockchain.is_reorg_in_progress(),
4646+
"flag must be cleared after a panicking guard scope"
4647+
);
4648+
}
4649+
}

0 commit comments

Comments
 (0)