Skip to content

Fix inconsistent Eth block visibility caused by missing/stale block-n…#1818

Merged
librelois merged 17 commits intomasterfrom
elois/fix
Feb 12, 2026
Merged

Fix inconsistent Eth block visibility caused by missing/stale block-n…#1818
librelois merged 17 commits intomasterfrom
elois/fix

Conversation

@librelois
Copy link
Copy Markdown
Member

@librelois librelois commented Feb 12, 2026

Goal of the Changes

Fix inconsistent latest Ethereum RPC behavior in KV-backed Frontier by ensuring block-number mappings stay canonical and recoverable during indexing lag/reorg windows, so APIs like eth_getBlockByNumber("latest"), eth_blockNumber, eth_coinbase, eth_getLogs, and filter polling remain aligned.

Rollout notes

Existing gaps are repaired by background batch plus on-read repair; recovery may be gradual on large affected ranges.

What Reviewers Need to Know

  • RPC consistency changes:
    • eth_getBlockByNumber("latest") now resolves via backend.latest_block_hash() and canonical number->hash repair logic instead of relying on client best-head assumptions.
    • eth_blockNumber and eth_coinbase now use the same indexed head source to avoid returning data from blocks not yet visible to latest.
    • Filter code now caps polling/ranges to latest indexed height and updates last_poll from that same capped height, preventing skipped logs when best head is ahead of indexed head.
  • Canonical mapping hardening in KV backend / mapping sync:
    • Introduces canonical pointer metadata (LATEST_CANONICAL_INDEXED_BLOCK) and repair cursor (CANONICAL_NUMBER_REPAIR_CURSOR).
    • Writes number mappings only for canonical blocks during sync.
    • Adds targeted repair paths:
      • on new-best/reorg events (canonical-gated remap),
      • background batch repair when idle (cursor-based retry of unresolved heights).
    • Allows controlled writes with NumberMappingWrite::{Write, Skip} to avoid corrupting number mappings with non-canonical blocks.
  • Backend API extension:
    • Adds set_block_hash_by_number to backend trait (default no-op; KV implements mutation, SQL keeps no-op) to support RPC-side self-healing.
  • No RPC method shape changes (JSON-RPC surface remains the same).
  • Operational/migration impact:
    • New KV metadata keys are introduced and populated lazily; no manual operator migration steps required.
    • Behavior is improved for long-running/reorg-heavy nodes, especially where mapping-sync lags.

Testing

  • Rust unit tests added/updated around:
    • canonical number mapping repair (missing/stale mapping repair),
    • non-canonical new-best pointer safety,
    • repair cursor retry behavior.
  • TS tests:
    • Added ts-tests/tests/test-latest-block-consistency.ts covering startup, reorg, lag, and multi-RPC consistency scenarios.
    • Extended ts-tests/tests/test-filter-api.ts with a regression test for log filter polling under indexing lag.

…umber mappings in KV backend, while keeping recovery fast on long-running chains.

Fix inconsistent Ethereum block visibility caused by missing/stale block-number mappings in KV backend, while keeping recovery fast on long-running chains.
@librelois librelois requested a review from sorpaas as a code owner February 12, 2026 16:06
@coderabbitai
Copy link
Copy Markdown

coderabbitai bot commented Feb 12, 2026

Note

Reviews paused

It looks like this branch is under active development. To avoid overwhelming you with review comments due to an influx of new commits, CodeRabbit has automatically paused this review. You can configure this behavior by changing the reviews.auto_review.auto_pause_after_reviewed_commits setting.

Use the following commands to manage reviews:

  • @coderabbitai resume to resume automatic reviews.
  • @coderabbitai review to trigger a single review.

Use the checkboxes below for quick actions:

  • ▶️ Resume reviews
  • 🔍 Trigger review
📝 Walkthrough

Walkthrough

This PR introduces a block number ↔ Ethereum hash mapping persistence and repair mechanism. It adds a new backend method set_block_hash_by_number, implements recovery logic with bounded probing, adds batch repair during sync, and enhances RPC layer to automatically repair stale mappings during canonical hash resolution.

Changes

Cohort / File(s) Summary
Backend Trait
client/api/src/backend.rs, client/db/src/sql/mod.rs
New async method set_block_hash_by_number added to trait with default no-op implementations in SQL backend and trait definition.
KV Mapping Database
client/db/src/kv/mod.rs
Major refactor: replaces MAX_WALKBACK_DEPTH with INDEXED_RECOVERY_SCAN_LIMIT; introduces NumberMappingWrite enum for conditional writes; adds CANONICAL_NUMBER_REPAIR_CURSOR tracking; implements set_block_hash_by_number, canonical_number_repair_cursor, and set_canonical_number_repair_cursor methods; updates recovery logic with min_block_hint parameter; modifies write_hashes signature to accept write mode.
RPC Resolution & Repair
client/rpc/src/eth/mod.rs
Introduces resolve_canonical_substrate_hash_by_number function to compute canonical hashes with automatic mapping repair; refactors block_info_by_number to use resolved hashes; adds tests for missing and stale mapping repair scenarios.
Mapping Sync Worker
client/mapping-sync/src/kv/mod.rs, client/mapping-sync/src/kv/worker.rs
Adds CANONICAL_NUMBER_REPAIR_BATCH_SIZE constant; extends sync_block with write_number_mapping parameter; introduces repair_canonical_number_mappings_batch public function for batch repair; implements reorg remapping during sync; adds tests for skip write mode and cursor advancement.
Call Site Updates
client/cli/src/frontier_db_cmd/mapping_db.rs, client/rpc/src/lib.rs
Updates write_hashes calls to explicitly pass NumberMappingWrite::Write mode; no logic changes.
Test Consistency
ts-tests/tests/test-latest-block-consistency.ts, ts-tests/tests/test-gas.ts
Relaxes block number assertions to accommodate non-deterministic advancement; adds reorg preservation test; replaces hard-coded gas constants with dynamic buffering; introduces progressive rather than exact sequential checks.

Sequence Diagram

sequenceDiagram
    participant SW as Sync Worker
    participant MC as Mapping<br/>Cursor
    participant MDB as Mapping DB
    participant RPC as RPC Handler
    participant Storage
    
    SW->>SW: sync_one_block<br/>(determine canonical)
    SW->>MDB: write_hashes<br/>(with Write mode)
    MDB->>Storage: persist mapping
    
    SW->>SW: detect reorg
    SW->>MDB: repair_canonical<br/>_number_mappings_batch
    
    MDB->>MC: canonical_number_repair_cursor()
    MC-->>MDB: block_hint
    
    MDB->>Storage: find_latest_indexed<br/>_canonical_block
    Storage-->>MDB: (block_num, hash)
    
    MDB->>MDB: set_block_hash_by_number<br/>(repair stale mapping)
    MDB->>MC: set_canonical_number<br/>_repair_cursor
    MC->>Storage: persist cursor
    
    RPC->>MDB: resolve_canonical<br/>_substrate_hash_by_number
    MDB->>Storage: check mapping
    alt mapping stale/missing
        MDB->>Storage: read Ethereum block
        MDB->>MDB: set_block_hash_by_number<br/>(repair)
    end
    MDB-->>RPC: canonical_hash
    RPC-->>RPC: block_info_by_number
Loading

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~60 minutes

Possibly related PRs

Suggested reviewers

  • sorpaas
  • arturgontijo
🚥 Pre-merge checks | ✅ 3 | ❌ 1
❌ Failed checks (1 warning)
Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 28.57% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (3 passed)
Check name Status Explanation
Title check ✅ Passed The title accurately describes the main focus: fixing inconsistent Eth block visibility caused by missing/stale block-number mappings, which is the core objective across all changed files.
Description check ✅ Passed The description comprehensively covers the changes' goals, RPC behavior updates, canonical mapping hardening, backend API extensions, testing approach, and rollout notes, all directly related to the changeset.
Merge Conflict Detection ✅ Passed ✅ No merge conflicts detected when merging into master

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Post copyable unit tests in a comment
  • Commit unit tests in branch elois/fix

Comment @coderabbitai help to get the list of available commands and usage tips.

Copy link
Copy Markdown

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 1

🤖 Fix all issues with AI agents
In `@client/db/src/kv/mod.rs`:
- Around line 153-160: Run rustfmt (cargo fmt) to fix formatting in the block
that computes fallback and calls
self.mapping.set_latest_canonical_indexed_block; ensure the let fallback line
and the if block are formatted according to rustfmt (no extra indentation or
misplaced braces) so the block using block_number, INDEXED_RECOVERY_SCAN_LIMIT,
and self.mapping.set_latest_canonical_indexed_block(...) matches project style
and passes CI.

Comment thread client/db/src/kv/mod.rs Outdated
Copy link
Copy Markdown

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 1

🤖 Fix all issues with AI agents
In `@client/db/src/kv/mod.rs`:
- Around line 133-143: The current min_block_hint can be greater than the scan
start (start_block) causing an empty recovery scan; clamp or drop the hint
before calling find_latest_indexed_canonical_block. After computing
min_block_hint (from mapping.latest_canonical_indexed_block_number and
best_number) ensure you compare it to start_block and if min_block_hint >
start_block set the hint to None (or clamp it to start_block) so
find_latest_indexed_canonical_block receives a valid lower-bound; adjust the
code paths around min_block_hint, latest_canonical_indexed_block_number, and the
call to find_latest_indexed_canonical_block accordingly.

Comment thread client/db/src/kv/mod.rs
Copy link
Copy Markdown

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
client/mapping-sync/src/kv/mod.rs (1)

368-406: ⚠️ Potential issue | 🟠 Major

Avoid repairing number mappings for non-canonical blocks when is_new_best is import-time only.

is_new_best can be true for a block that was best at import but is no longer canonical by sync time. In that case, repair_canonical_number_mapping_for_hash can overwrite canonical mappings with a non-canonical hash, undoing the earlier skip logic. Consider gating the “latest canonical indexed” update and repair logic on a canonical check at sync time.

🛠️ Proposed fix
 	if is_new_best {
-		let block_number: u64 = (*operating_header.number()).unique_saturated_into();
-		frontier_backend
-			.mapping()
-			.set_latest_canonical_indexed_block(block_number)?;
+		let block_number: u64 = (*operating_header.number()).unique_saturated_into();
+		let is_canonical_now = client
+			.hash(block_number.unique_saturated_into())
+			.map_err(|e| format!("{e:?}"))?
+			== Some(hash);
+		if !is_canonical_now {
+			log::debug!(
+				target: "mapping-sync",
+				"Skipping canonical remap for non-canonical block #{} ({:?})",
+				operating_header.number(),
+				hash,
+			);
+		} else {
+			frontier_backend
+				.mapping()
+				.set_latest_canonical_indexed_block(block_number)?;
 
-		let mut reorg_remapped = 0u64;
-		if repair_canonical_number_mapping_for_hash(
-			client,
-			storage_override.as_ref(),
-			frontier_backend,
-			hash,
-		)?
-		.is_some()
-		{
-			reorg_remapped = reorg_remapped.saturating_add(1);
-		}
-		if let Some(info) = reorg_info.as_ref() {
-			for enacted_hash in &info.enacted {
-				if repair_canonical_number_mapping_for_hash(
-					client,
-					storage_override.as_ref(),
-					frontier_backend,
-					*enacted_hash,
-				)?
-				.is_some()
-				{
-					reorg_remapped = reorg_remapped.saturating_add(1);
-				}
-			}
-		}
-		log::debug!(
-			target: "mapping-sync",
-			"Reorg canonical remap touched {reorg_remapped} blocks at new best {:?}",
-			hash,
-		);
+			let mut reorg_remapped = 0u64;
+			if repair_canonical_number_mapping_for_hash(
+				client,
+				storage_override.as_ref(),
+				frontier_backend,
+				hash,
+			)?
+			.is_some()
+			{
+				reorg_remapped = reorg_remapped.saturating_add(1);
+			}
+			if let Some(info) = reorg_info.as_ref() {
+				for enacted_hash in &info.enacted {
+					if repair_canonical_number_mapping_for_hash(
+						client,
+						storage_override.as_ref(),
+						frontier_backend,
+						*enacted_hash,
+					)?
+					.is_some()
+					{
+						reorg_remapped = reorg_remapped.saturating_add(1);
+					}
+				}
+			}
+			log::debug!(
+				target: "mapping-sync",
+				"Reorg canonical remap touched {reorg_remapped} blocks at new best {:?}",
+				hash,
+			);
+		}
 	}

@librelois
Copy link
Copy Markdown
Member Author

@coderabbitai review

@coderabbitai
Copy link
Copy Markdown

coderabbitai bot commented Feb 12, 2026

✅ Actions performed

Review triggered.

Note: CodeRabbit is an incremental review system and does not re-review already reviewed commits. This command is applicable only when automatic reviews are paused.

Copy link
Copy Markdown

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (2)
ts-tests/tests/test-gas.ts (2)

241-259: ⚠️ Potential issue | 🟡 Minor

Use let instead of var to avoid variable redeclaration.

Static analysis correctly flags that i is redeclared at line 259. Using var hoists the variable to function scope, causing unintended shadowing. Use let for proper block scoping.

Proposed fix
-		for (var i = 0; i < callsPerBlock; i++) {
+		for (let i = 0; i < callsPerBlock; i++) {
-		for (var i = 0; i < transfersPerBlock + extraTransfers; i++) {
+		for (let i = 0; i < transfersPerBlock + extraTransfers; i++) {

362-380: ⚠️ Potential issue | 🟡 Minor

Use let instead of var to avoid variable redeclaration.

Same issue as in the previous test block - static analysis flags i redeclaration at line 380.

Proposed fix
-		for (var i = 0; i < callsPerBlock; i++) {
+		for (let i = 0; i < callsPerBlock; i++) {
-		for (var i = 0; i < transfersPerBlock + extraTransfers; i++) {
+		for (let i = 0; i < transfersPerBlock + extraTransfers; i++) {
🧹 Nitpick comments (1)
client/mapping-sync/src/kv/mod.rs (1)

208-275: Consider adding early termination when first_unresolved is set.

The current implementation continues iterating through the entire batch even after encountering the first unresolved block, but subsequent unresolved blocks won't update first_unresolved (due to get_or_insert). While this maximizes repair in a single pass, it means the cursor will be set to retry from the first unresolved block, potentially re-processing already-repaired blocks in the next batch.

This trade-off is acceptable for gradual recovery, but consider whether early termination on consecutive unresolved blocks would be more efficient for scenarios where a large range is unindexed.

@librelois librelois merged commit d1112f3 into master Feb 12, 2026
7 checks passed
@librelois librelois deleted the elois/fix branch February 12, 2026 21:59
librelois added a commit to moonbeam-foundation/frontier that referenced this pull request Feb 12, 2026
polkadot-evm#1818)

* Fix inconsistent Eth block visibility caused by missing/stale block-number mappings in KV backend, while keeping recovery fast on long-running chains.

Fix inconsistent Ethereum block visibility caused by missing/stale block-number mappings in KV backend, while keeping recovery fast on long-running chains.

* rustfmt

* repair number mappings and stabilize latest-indexed recovery

* Fix KV mapping sync to write block-number mappings only for canonical blocks and auto-repair stale entries

* rustfmt

* clippy lints

* Guard is_new_best remap logic with sync-time canonical check

* Harden canonical mapping sync against stale import-time best and stabilize latest-index pointer

* improve sync and add tests

* Fix pointer initialization & adjust reorg test scenario

* Change repair cursor behavior so blocks that cannot currently be repaired are retried (or persist unresolved ranges), instead of always advancing past them

* clippy lint

* prettier

* fix test gas

* ts-tests: wait for chain head block in `createAndFinalizeBlock` to reduce CI flakiness

* ts-tests: harden pov-size and newHeads tests against CI timing races

* improve tests
librelois added a commit to moonbeam-foundation/frontier that referenced this pull request Feb 12, 2026
polkadot-evm#1818)

* Fix inconsistent Eth block visibility caused by missing/stale block-number mappings in KV backend, while keeping recovery fast on long-running chains.

Fix inconsistent Ethereum block visibility caused by missing/stale block-number mappings in KV backend, while keeping recovery fast on long-running chains.

* rustfmt

* repair number mappings and stabilize latest-indexed recovery

* Fix KV mapping sync to write block-number mappings only for canonical blocks and auto-repair stale entries

* rustfmt

* clippy lints

* Guard is_new_best remap logic with sync-time canonical check

* Harden canonical mapping sync against stale import-time best and stabilize latest-index pointer

* improve sync and add tests

* Fix pointer initialization & adjust reorg test scenario

* Change repair cursor behavior so blocks that cannot currently be repaired are retried (or persist unresolved ranges), instead of always advancing past them

* clippy lint

* prettier

* fix test gas

* ts-tests: wait for chain head block in `createAndFinalizeBlock` to reduce CI flakiness

* ts-tests: harden pov-size and newHeads tests against CI timing races

* improve tests
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