Skip to content

feat: Block hash calculation v2, continuity validation and refactoring - #2583

Open
grshv-cw wants to merge 25 commits into
mainfrom
block-hash-calculation
Open

feat: Block hash calculation v2, continuity validation and refactoring#2583
grshv-cw wants to merge 25 commits into
mainfrom
block-hash-calculation

Conversation

@grshv-cw

@grshv-cw grshv-cw commented Aug 5, 2026

Copy link
Copy Markdown
Contributor

User description

Main

  • Added new hash calculation algorithm
  • Followers now checks blocks continuity
  • Implemented blockhash getter for BLOCKHASH opcode

Progress model

Stratus tracks two process-local progress tips:

  • latest_sealed is 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_saved is the durable tip. StratusStorage stores 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_saved remains 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_sealed to 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 --> T
Loading

These are not independent chains. Permanent progress must always be an ordered prefix of sealed progress.

Hash schemes

Genesis retains the legacy V1 hash:

V1 = keccak256(number as 8-byte big-endian)

Locally sealed non-genesis blocks use V2:

V2 = keccak256(
    number as 8-byte big-endian
    || timestamp as 8-byte big-endian
    || transactions_root
    || parent_hash
)

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

PendingSession owns a PendingBlockGuard, which wraps temporary storage's pending_session mutex colocated with InMemoryChainState. 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:

set pending header when importing
→ execute and save transactions
→ snapshot pending state
→ build the block and determine its final hash
→ finish pending state

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_block uses std::mem::replace to move the original pending state into latest_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 until PendingSession is dropped or consumed by sealing.

Local synchronous modes also retain the existing mine_and_commit mutex 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_block is the authoritative continuity boundary for leader mining, follower reexecution, follower replication, fake leader, and offline import.

Before saving block N, it validates:

N == last_saved.number + 1
N.parent_hash == last_saved.hash

RocksDB is written only after these checks pass. last_saved advances 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_block repeats 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 BLOCKHASH opcode.

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" .-> Cache
Loading

Lookup order is:

  1. The latest sealed block, but only while it is ahead of last_saved.
  2. The block-hash cache.
  3. Permanent storage.

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:

configured capacity + batch_size × (queue_size + 2)

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 by PendingSession and 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 PendingSession with PendingBlockGuard

  • Implement 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 --> Result
Loading

File Walkthrough

Relevant files
Enhancement
13 files
stratus_storage.rs
Enforce block continuity and block-hash caching                   
+319/-45
miner.rs
Introduce `PendingSession` API and guard                                 
+214/-64
transaction.rs
Add `InMemoryChainState` and guard locking                             
+117/-77
block.rs
Implement V2 hash and pending→mined conversion                     
+166/-22
executor.rs
Support session-based execution and guard                               
+39/-6   
mod.rs
Expose `PendingBlockGuard` in temp storage                             
+28/-5   
mod.rs
Init temp storage from permanent chain tip                             
+9/-8     
pending_block.rs
Add `new_at` constructor with timestamp                                   
+12/-0   
external_block.rs
Add `parent_hash` getter                                                                 
+5/-0     
rocks_permanent.rs
Expose `read_chain_tip` returning `BlockReference`             
+5/-0     
replication.rs
Validate continuity before replication save                           
+1/-0     
fake_leader.rs
Integrate session sealing in fake-leader importer               
+8/-5     
importer_offline.rs
Expand block_hash_cache for offline importer                         
+12/-4   
Configuration changes
1 files
cache.rs
Add block-hash cache with configurable capacity                   
+66/-3   
Bug fix
2 files
block_header.rs
Reset header hash/parent defaults to zero                               
+6/-9     
rpc_server.rs
Use `miner.reset_to_genesis` in RPC reset                               
+1/-1     
Error handling
1 files
stratus_error.rs
Add `ParentHashConflict` and `BlockHashMissing`                   
+9/-0     
Tests
2 files
mod.rs
Refactor tests to use session sealing helper                         
+31/-12 
leader-follower-blockhash.test.ts
Add leader/follower block-hash integration tests                 
+159/-0 
Documentation
1 files
continuity.md
Add docs on continuity and hash schemes                                   
+146/-0 
Additional files
8 files
e2e-leader-follower.yml +11/-1   
TestBlockHash.sol +47/-0   
e2e-blockhash.test.ts +122/-0 
rpc.ts +7/-0     
evm.rs +14/-4   
execution.rs +20/-12 
genesis.rs +2/-1     
mod.rs +24/-0   

@grshv-cw
grshv-cw requested a review from a team as a code owner August 5, 2026 16:33

@cloudwalk-review-agent cloudwalk-review-agent Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.
  • BLOCKHASH now 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.

@github-actions

github-actions Bot commented Aug 5, 2026

Copy link
Copy Markdown
Contributor

PR Reviewer Guide 🔍

Here are some key observations to aid the review process:

⏱️ Estimated effort to review: 5 🔵🔵🔵🔵🔵
🧪 PR contains tests
🔒 No security concerns identified
⚡ No major issues detected

@github-actions

github-actions Bot commented Aug 5, 2026

Copy link
Copy Markdown
Contributor

PR Code Suggestions ✨

Explore these optional code suggestions:

CategorySuggestion                                                                                                                                    Impact
Possible issue
Return zero on missing block hash

The EVM BLOCKHASH opcode should return zero when the requested block is outside its
accessible window rather than bubbling up an error. Replace the Err arm to return
Ok(B256::ZERO) to match Ethereum semantics.

src/eth/executor/evm.rs [517-527]

 fn block_hash_ref(&self, number: u64) -> Result<B256, Self::Error> {
     let number = BlockNumber::from(number);
     match self.storage.read_block_hash(number)? {
         Some(hash) => Ok(hash.into()),
-        None => Err(StorageError::BlockHashMissing { number }.into()),
+        None => Ok(B256::ZERO),
     }
 }
Suggestion importance[1-10]: 9

__

Why: Changing the None arm to return Ok(B256::ZERO) aligns BLOCKHASH opcode behavior with Ethereum semantics by returning zero for inaccessible blocks, a critical fix for VM correctness.

High

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 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".

Comment thread src/eth/storage/temporary/mod.rs Outdated

@cloudwalk-review-agent cloudwalk-review-agent Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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 + PendingBlockGuard is 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 via pending_session mutex, and seal_local/seal_external consume the session so the guard is released at the right boundary. Lock order in fake_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 both number == expected and parent_hash == expected. The last_saved in-memory tracker is initialized from read_chain_tip() at startup and advanced only after RocksDB writes succeed. The duplicate-save detection (old BlockConflict check) is preserved implicitly: saving block N when last_saved is at block N yields a MinedNumberConflict.

  • BlockHashRing is a clean fixed-capacity ring indexed by number % capacity. Two numbers share a slot only when capacity apart, so in-window entries are never evicted. The offline importer's configured + batch_size × (queue_size + 2) sizing ensures the unsaved backlog is unevictable.

  • BLOCKHASH opcode returning Err(BlockHashMissing) instead of Ok(B256::ZERO) is a deliberate design choice. The EVM pre-filters the request range before calling block_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.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant