feat: Block hash calculation v2, continuity validation and refactoring - #2583
feat: Block hash calculation v2, continuity validation and refactoring#2583grshv-cw wants to merge 25 commits into
Conversation
There was a problem hiding this comment.
Summary
Solid high-risk refactor with good coverage on the critical paths: block hash v2 computation, continuity enforcement (last_saved), pending-session serialization, and BLOCKHASH opcode support. The new tests exercise both correctness and concurrency-sensitive behavior (unsaved sealed tip, legacy hash compatibility, parent continuity rejection, and session locking), and I did not find a concrete blocking issue in the provided diff context.
Notable strengths:
- Continuity checks moved to an authoritative save boundary and reused as preflight where needed.
BLOCKHASHnow fails explicitly on missing historical hash instead of silently returning zero.- Session-based guard model reduces lock-order footguns across executor/miner/storage.
- E2E additions validate cross-node hash consistency and opcode behavior.
PR Reviewer Guide 🔍Here are some key observations to aid the review process:
|
PR Code Suggestions ✨Explore these optional code suggestions:
|
There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: 8c840834f7
ℹ️ About Codex in GitHub
Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you
- Open a pull request for review
- Mark a draft as ready
- Comment "@codex review".
If Codex has suggestions, it will comment; otherwise it will react with 👍.
Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".
There was a problem hiding this comment.
Summary
Solid refactor introducing block hash V2 computation, unified continuity validation via last_saved, PendingSession-based concurrency control, and the BlockHashRing cache for the BLOCKHASH opcode.
Architecture assessment
-
PendingSession+PendingBlockGuardis the right model: one session lock spans the full pending-block lifecycle (set header → execute → seal → finish), replacing the old scatter of separate locks (save_execution,mine,mine_and_commit). The session serializes writers viapending_sessionmutex, andseal_local/seal_externalconsume the session so the guard is released at the right boundary. Lock order infake_leader(transaction → mine_and_commit → session) matches the RPC path. Tests confirm serialization. -
Continuity validation moved from block-number-only checks to
validate_saved_continuity(last_saved, block)which checks bothnumber == expectedandparent_hash == expected. Thelast_savedin-memory tracker is initialized fromread_chain_tip()at startup and advanced only after RocksDB writes succeed. The duplicate-save detection (oldBlockConflictcheck) is preserved implicitly: saving block N whenlast_savedis at block N yields aMinedNumberConflict. -
BlockHashRing is a clean fixed-capacity ring indexed by
number % capacity. Two numbers share a slot only whencapacityapart, so in-window entries are never evicted. The offline importer'sconfigured + batch_size × (queue_size + 2)sizing ensures the unsaved backlog is unevictable. -
BLOCKHASH opcode returning
Err(BlockHashMissing)instead ofOk(B256::ZERO)is a deliberate design choice. The EVM pre-filters the request range before callingblock_hash_ref, so the database only sees requests for blocks that should exist. Surfacing an error for a genuinely missing block within the window is safer for a permissioned chain than masking it as zero.
Codex P1 concern (block 0 empty-chain init)
The Codex review flagged that BlockReference::genesis() as the latest_sealed fallback for empty chains causes the pending header to become block 1. This is incorrect: pending_block_number = saved_tip.map_or(BlockNumber::ZERO, ...), so the pending header starts at block 0. Block::from_pending ignores parent_hash for genesis (number is zero), and validate_saved_continuity(None, ...) expects parent_hash = Hash::ZERO for block 0. The latest_sealed pointing to genesis is intentional — it provides the genesis hash as the parent reference for block 1 once genesis has been saved. No issue.
Test coverage
Good coverage on the critical paths: V2 hash field sensitivity, V1/V2 fallback in apply_external, genesis legacy hash, sealed-tip-before-save readability, cache-clear survival, permanent storage fallback, parent continuity rejection, session serialization, and the full E2E suite for leader/follower hash consistency and BLOCKHASH opcode behavior. The new blockhash matrix entry in CI ensures cross-node hash agreement is continuously validated.
User description
Main
Progress model
Stratus tracks two process-local progress tips:
latest_sealedis the execution tip. Temporary storage owns its latest in-process state overlay and final hash. Its hash is the parent used to build the next header; after startup, the durable underlying state still comes from RocksDB.last_savedis the durable tip.StratusStoragestores only its block number and hash because the complete block and state already live in RocksDB.On a populated database, both are initialized from the latest permanent block. On an empty database, temporary storage starts from the canonical sealed genesis while
last_savedremains empty until genesis is persisted.Normal leader and follower flows seal and save sequentially, so the tips usually match. The offline importer deliberately pipelines execution and persistence, allowing
latest_sealedto run ahead.flowchart LR subgraph durable [Permanent progress] P0["Block N-1"] --> P1["Block N: last_saved"] end subgraph backlog [Offline FIFO backlog] Q1["Block N+1"] --> Q2["Block N+2"] end T["Block N+3: latest_sealed"] P1 --> Q1 Q2 --> TThese are not independent chains. Permanent progress must always be an ordered prefix of sealed progress.
Hash schemes
Genesis retains the legacy V1 hash:
Locally sealed non-genesis blocks use V2:
Reexecution validates that the imported hash is V2 or the temporary V1 compatibility hash. Replication receives a prebuilt block and relies on the universal saved-chain continuity checks.
Guarded sealing
PendingSessionowns aPendingBlockGuard, which wraps temporary storage'spending_sessionmutex colocated withInMemoryChainState. Callers use session methods instead of manually pairing lock-acquiring methods with variants that accept an existing guard.One session spans the complete pending-block lifecycle:
Local sealing and reexecution use the snapshot to build a block without destroying pending state. Local sealing calculates V2; reexecution validates the external V2 or V1 hash. If external validation fails, pending remains unchanged. Replication skips this snapshot-and-build step because it receives a complete block.
Once the block is accepted,
finish_pending_blockusesstd::mem::replaceto move the original pending state intolatest_sealed, attaches the final hash, and creates the next pending state.Pending and latest sealed state share one
RwLock<InMemoryChainState>, so the move, hash update, and next-pending creation are one atomic write. Another pending session cannot start untilPendingSessionis dropped or consumed by sealing.Local synchronous modes also retain the existing
mine_and_commitmutex from sealing through persistence. It guarantees that concurrent local triggers cannot seal blocks in one order and race to save them in another order. Offline importer does not use this mutex; its single executor, FIFO channel, and single saver provide ordering.Saving and continuity
save_blockis the authoritative continuity boundary for leader mining, follower reexecution, follower replication, fake leader, and offline import.Before saving block
N, it validates:RocksDB is written only after these checks pass.
last_savedadvances only after the write succeeds, and no permanent read is required during each save.Online follower modes perform the same check as a read-only preflight before emitting Kafka events.
save_blockrepeats it authoritatively before persistence.External reexecution calculates the block locally and accepts the external V2 hash or the temporary V1 compatibility hash. Replication receives a prebuilt block, but both modes still pass through the universal saved-chain continuity check.
If the process restarts, sealed-but-unsaved work is discarded. The durable tip is loaded from RocksDB, temporary state resumes from it, and the unsaved range is executed again.
Block-hash lookup
The block-hash cache does not determine chain progress or parent continuity. Its normal role is accelerating the EVM
BLOCKHASHopcode.flowchart LR EVM["EVM BLOCKHASH"] --> TempCheck{"Latest sealed and unsaved?"} TempCheck -->|"yes"| Result["Block hash"] TempCheck -->|"no"| Cache["Block-hash cache"] Cache -->|"hit"| Result Cache -->|"miss"| Permanent["Permanent storage"] Permanent --> Result Queue["Offline sealed backlog"] -. "temporary workaround" .-> CacheLookup order is:
last_saved.The normal cache default is 256 entries and administrators may set it to zero. Importer-offline always adds capacity for its bounded sealed-but-unsaved backlog:
The extra two batches cover one batch being built by the executor and one being processed by the saver.
Older unsaved offline blocks are temporarily dependent on this cache because permanent storage cannot serve them yet. Remove this workaround when importer-offline is removed.
Lock roles
PendingSession: exposes pending-header setup, execution append, and sealing under one temporary-storage session lock.PendingBlockGuard: private capability held byPendingSessionand passed only to lower storage layers.TransactionGuard: lets fake leader hold the executor transaction mutex before miner locks, matching RPC lock order and preventing deadlock.mine_and_commit: preserves seal-to-save ordering for synchronous local modes.commit: serializes permanent writes.last_saved: serializes durable continuity validation and advancement.transient_state_lock: preserves consistency between permanent writes and latest account/slot caches.PR Type
Enhancement, Tests, Documentation
Description
Add V2 block-hash algorithm and GENESIS V1 support
Enforce block continuity checks on save
Introduce
PendingSessionwithPendingBlockGuardImplement block-hash cache and BLOCKHASH opcode
Add integration and unit tests for blockhash
Update docs on continuity and hash schemes
Diagram Walkthrough
flowchart LR P1["last_saved tip"] -- "validate_next_saved_block" --> Save["save_block"] Temp["latest_sealed tip"] --> Pub["publish_block_hash"] EVM["EVM BLOCKHASH"] --> Check{"block == latest unsaved?"} Check -- yes --> Temp Check -- no --> Cache["block_hash_cache"] Cache -- hit --> Result["return hash"] Cache -- miss --> Perm["RocksPermanentStorage"] Perm --> ResultFile Walkthrough
13 files
Enforce block continuity and block-hash cachingIntroduce `PendingSession` API and guardAdd `InMemoryChainState` and guard lockingImplement V2 hash and pending→mined conversionSupport session-based execution and guardExpose `PendingBlockGuard` in temp storageInit temp storage from permanent chain tipAdd `new_at` constructor with timestampAdd `parent_hash` getterExpose `read_chain_tip` returning `BlockReference`Validate continuity before replication saveIntegrate session sealing in fake-leader importerExpand block_hash_cache for offline importer1 files
Add block-hash cache with configurable capacity2 files
Reset header hash/parent defaults to zeroUse `miner.reset_to_genesis` in RPC reset1 files
Add `ParentHashConflict` and `BlockHashMissing`2 files
Refactor tests to use session sealing helperAdd leader/follower block-hash integration tests1 files
Add docs on continuity and hash schemes8 files