Skip to content

refactor(l1): dedup the canonical-block-hash read onto load_canonical_block_hash - #7189

Open
ilitteri wants to merge 1 commit into
mainfrom
dedup-canonical-block-hash-read
Open

refactor(l1): dedup the canonical-block-hash read onto load_canonical_block_hash#7189
ilitteri wants to merge 1 commit into
mainfrom
dedup-canonical-block-hash-read

Conversation

@ilitteri

Copy link
Copy Markdown
Collaborator

Motivation

The exact CANONICAL_BLOCK_HASHES read — begin_read()? → get(CANONICAL_BLOCK_HASHES, block_number.to_le_bytes())? → H256::decode → transpose → map_err(StoreError::from) — was written out three times in crates/storage/store.rs: inside get_canonical_block_hash's spawn_blocking closure, in get_canonical_block_hash_sync, and in the private load_canonical_block_hash.

Triplicated read logic is a liability: a future change to the key encoding, the value decoding, or the error mapping has to be applied in three places, and the sync variant's post-fast-path body being byte-identical to load_canonical_block_hash invites the copies to drift apart silently.

Description

Both public variants now keep their latest_block_header fast path and delegate the actual DB read to the existing private load_canonical_block_hash, mirroring the get_block_header_by_hashload_block_header_by_hash split already used elsewhere in this file. Net −17 lines, one concern, no behavior change.

  • get_canonical_block_hash (async): the spawn_blocking closure now captures a Store clone and calls store.load_canonical_block_hash(block_number). Store is Clone (Arcs plus plain data), and the clone happens once per call, not in any loop. The JoinError mapping .map_err(|e| StoreError::Custom(format!("Task panicked: {}", e)))? is unchanged.
  • get_canonical_block_hash_sync: after the fast path, the body is now self.load_canonical_block_hash(block_number).
  • load_canonical_block_hash itself is untouched: no fast path was added to it, because the load_* family deliberately bypasses the latest_block_header cache to serve init/genesis paths. The fast paths stay in the two public functions, so hot callers (is_canonical_sync, the BLOCKHASH-opcode path in blockchain/vm.rs) keep their cache hit.
  • The fourth inline occurrence of this read pattern, in get_block_bodies, is intentionally left alone: it amortizes a single read transaction across a loop, which delegating per-iteration would undo.

If this change were wrong, one of these would have to be true:

  • load_canonical_block_hash reads a different table, key encoding, or decoding than the inlined copies did — it is byte-identical to the removed code.
  • The async variant's error surface changed — the closure still returns Result<Option<BlockHash>, StoreError> through spawn_blocking, and the JoinError arm is character-for-character the same.
  • Store could not be moved into spawn_blocking — it is Clone and all fields are Send + Sync; the crate compiles under clippy --all-targets -D warnings.
  • A caller depended on the removed fast-path behavior differing between the variants — both fast paths are preserved verbatim.

How to test

  • cargo fmt -p ethrex-storage — no diff after the change.
  • cargo clippy -p ethrex-storage --all-targets -- -D warnings — clean.
  • cargo test -p ethrex-storage — 91 passed, 0 failed; doctests 1 passed, 1 ignored.

Public signatures are unchanged, so no dependent crate is affected.

Checklist

  • Updated STORE_SCHEMA_VERSION (crates/storage/lib.rs) if the PR includes breaking changes to the Store requiring a re-sync. — No Store schema change (pure read-path dedup), so STORE_SCHEMA_VERSION is untouched.

…_block_hash

The exact CANONICAL_BLOCK_HASHES read (begin_read -> get -> H256::decode)
was inlined three times: in get_canonical_block_hash (inside its
spawn_blocking closure), in get_canonical_block_hash_sync, and in the
private load_canonical_block_hash. Both public variants now keep their
latest_block_header fast path and delegate the DB read to
load_canonical_block_hash, mirroring the existing
get_block_header_by_hash / load_block_header_by_hash split. The async
variant clones the Store into the spawn_blocking closure (Store is
Clone over Arcs) and keeps the same JoinError -> StoreError::Custom
mapping. No behavior change.
@github-actions

Copy link
Copy Markdown

⚠️ Known Issues — intentionally skipped tests

Source: docs/known_issues.md

rpc-compat log-bearing cases excluded

Where: KNOWN_EXCLUDED_TESTS in .github/scripts/check-hive-results.sh counts out
eight hive rpc-compat cases — the four eth_getLogs cases, eth_getBlockReceipts/get-block-receipts-latest,
and three eth_getTransactionReceipt cases. They are exactly the cases whose recorded
response contains at least one log object; every case with an empty log array still runs.
Note this leaves eth_getLogs with no rpc-compat coverage at all, since all four of its
cases are in the set.

Why: ethrex populates blockTimestamp on log objects, as geth, besu, nethermind, reth
and erigon all do. hive's rpc-compat compares responses byte-exactly (jsondiff.FullMatch;
the lenient checkJSONStructure path applies only to cases upstream marks speconly), and
the corpus is pinned to execution-apis d08382ae (2025-02-10), whose recordings predate the
field — it entered the schema in execution-apis#639 and the fixtures in #846 (2026-07-22).
So the extra key cannot match, and this is a property of the pin rather than of the response.

The pin cannot move, and this is not temporary. The pin sits one commit before
execution-apis#627, which moved the test chain to a pre-merge genesis: the current corpus has
~36 proof-of-work blocks before its terminal total difficulty. ethrex does not support
pre-merge chains and will not, so importing that chain.rlp fails at block 1 —
validate_block_header has no pre-London base-fee path. Every revision carrying
blockTimestamp in its fixtures also carries that chain, so there is no revision that
satisfies both. Nor can the corpus be patched locally: rpc-compat's Dockerfile clones
ethereum/execution-apis by hard-coded URL, so the branch buildarg cannot point at a fork.

Coverage: the field itself is pinned by
block_timestamp_is_on_the_log_and_not_on_the_receipt in
crates/networking/rpc/types/receipt.rs, which asserts it is present on each log and absent
from the receipt level.

Removal: delete the entries if ethrex ever gains pre-merge chain import, or if upstream
marks these cases speconly so they are type-checked instead of compared byte-for-byte.


The stateless schema id does not identify the encoding

Where: STATELESS_INPUT_SCHEMA_ID in crates/common/types/stateless_ssz.rs.

Upstream keeps the stateless input schema id at 0x1501
(fork_index 0x15 << 8 | revision 0x01) across incompatible body changes. Three
encodings have now shipped under it: tests-zkevm@v0.6.2, then #3248 + #3278,
then #3356, which moved state, codes and public_keys from SszList to
ProgressiveList. ethrex speaks the last one.

The consequence is that the 2-byte prefix cannot be used to detect a stale or
mismatched bundle. A wrong-dialect input is accepted by the id check and then
fails later — in SSZ decode, or on a root that does not match — rather than being
rejected up front for what it is. only_amsterdam_schema_id_decodes therefore
proves less than its name suggests.

Worth raising upstream: a revision field that does not move across a body change
provides no version negotiation at all.


ZisK guest program hash changes with the unsync_cell gate

Where: crates/common/types/block.rs, transaction.rs.

The gate on the single-threaded unsync_cell::OnceCell moved from
all(feature = "eip-8025", target_arch = "riscv64") to
all(feature = "zisk", target_arch = "riscv64") when the eip-8025 feature was removed.

The guest ELFs were previously built --features "<zkvm>-build-elf,ci", which never enabled
eip-8025, so they compiled the atomic once_cell variant. bin/zisk/Cargo.toml does enable
ethrex-common/zisk, so the ZisK guest now compiles the unsafe impl Sync cell instead.
That changes the ELF bytes and therefore the program hash and verification key.

This is intended (the guest is single-threaded, so the unsync cell is sound and cheaper), but it
is a VK change rather than a no-op refactor, and the diffstat presents it as a file rename
(eip8025_cell.rsunsync_cell.rs). Anyone pinning a ZisK VK across this change must
re-register it. The stateless-validator crate now forwards ethrex-common/zisk from its own
zisk feature so the two ZisK guests do not disagree on the cell type.


Release signing key is an unprotected repository secret

Where: .github/workflows/tag_release.yaml.

MINISIGN_SECRET_KEY is a plain repository secret. There is no environment: on
finalize-release or dry-run-release-assets, and gh api repos/lambdaclass/ethrex/rulesets
shows only branch-targeted rulesets, so the github.ref_type == 'tag' condition is a workflow
check rather than an enforced boundary: anyone who can push a tag can reach the signing key.

This is a repository-settings change, not a code change, so it is recorded here rather than
fixed in the tree. Recommended:

  1. Move MINISIGN_SECRET_KEY / MINISIGN_PASSWORD into a GitHub Environment with required
    reviewers, and add environment: to the two jobs that sign.
  2. Add a ruleset targeting refs/tags/v* restricting who may create release tags.

Until then, the compromise of that key is silent and durable: signatures would still verify
against the committed .github/minisign.pub.

@github-actions github-actions Bot added the L1 Ethereum client label Aug 21, 2026
@github-actions

Copy link
Copy Markdown

Lines of code report

Total lines added: 0
Total lines removed: 17
Total lines changed: 17

Detailed view
+--------------------------------+-------+------+
| File                           | Lines | Diff |
+--------------------------------+-------+------+
| ethrex/crates/storage/store.rs | 5090  | -17  |
+--------------------------------+-------+------+

@ilitteri
ilitteri marked this pull request as ready for review August 21, 2026 15:17
@ilitteri
ilitteri requested a review from a team as a code owner August 21, 2026 15:17
@ethrex-project-sync ethrex-project-sync Bot moved this to In Review in ethrex_l1 Aug 21, 2026
@github-actions

Copy link
Copy Markdown

🤖 Kimi Code Review

File: crates/storage/store.rs

Review Summary: This is a good refactoring that eliminates duplicate database access logic. No correctness issues found, but one minor performance consideration regarding struct cloning.

Detailed Feedback:

  1. Code Duplication (Positive)

    • The extraction of the canonical block hash lookup into load_canonical_block_hash correctly eliminates duplication between the async (line ~1212) and sync (line ~1597) code paths. This reduces maintenance burden and ensures consistent error handling.
  2. Clone Cost (Minor)

    • Line 1212: let store = self.clone(); clones the entire Store struct rather than just self.backend as in the previous implementation.
    • If Store contains large non-Arc fields beyond the backend, this could increase memory pressure slightly. Verify that Store uses Arc for its internal state or consider reverting to self.backend.clone() if the struct is expensive to clone.
  3. Error Handling Consistency

    • The refactor assumes load_canonical_block_hash returns Result<Option<H256>, StoreError> with equivalent H256::decode and map_err(StoreError::from) logic. Ensure the helper method preserves the exact same RLP decoding error mapping to avoid subtle changes in error variants returned to callers.
  4. Async/Sync Separation

    • The async method (first hunk) correctly maintains the tokio::task::spawn_blocking boundary to avoid blocking the async runtime during database I/O.
    • The sync method (second hunk) correctly calls the helper directly without spawning a task, maintaining its synchronous nature.

Verdict: Approved with the suggestion to verify that Store::clone is inexpensive (ideally O(1) via Arc).


Automated review by Kimi (Moonshot AI) · kimi-k2.5 · custom prompt

@github-actions

Copy link
Copy Markdown

🤖 Claude Code Review

Confirmed: line 714's occurrence in get_block_bodies is a batch read within a loop, correctly left untouched per the PR description. The refactor is clean and correct.

Review

This is a small, well-scoped refactor with no behavioral change. Verified against the current file:

  • load_canonical_block_hash (store.rs:4201-4213) is byte-identical to what was previously inlined in both get_canonical_block_hash and get_canonical_block_hash_sync.
  • Both public methods correctly preserve their latest_block_header fast-path checks before delegating (store.rs:1208-1213, 1593-1597).
  • Store is #[derive(Debug, Clone)] over Arc-backed fields (store.rs:196-197), so cloning it into the spawn_blocking closure is cheap and sound — no more expensive than the previous self.backend.clone().
  • The JoinError → StoreError::Custom mapping is unchanged.
  • The one remaining inline occurrence (in get_block_bodies, store.rs:714) is correctly left alone since it amortizes a single read transaction across a loop — delegating per-iteration would add per-call transaction overhead.
  • No other inlined copies of this read pattern were missed (checked all CANONICAL_BLOCK_HASHES occurrences).

No correctness, security, or performance issues found. The change reduces duplication and improves maintainability without introducing risk.


Automated review by Claude (Anthropic) · sonnet · custom prompt

@github-actions

Copy link
Copy Markdown

🤖 Codex Code Review

  1. Performance: crates/storage/store.rs now clones the entire Store before spawn_blocking, where the old code cloned only backend. get_canonical_block_hash() is on a hot read path (get_block_by_number, RPC lookups, chain traversal), so this adds avoidable per-call work and extra Arc/PathBuf/sender cloning for no behavioral gain. Prefer capturing self.backend.clone() in the closure, or make load_canonical_block_hash a backend-focused helper.

No correctness, consensus, gas-accounting, or security regressions stood out in this diff otherwise. The helper extraction itself is behavior-preserving and makes the sync/async paths consistent.


Automated review by OpenAI Codex · gpt-5.4 · custom prompt

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

Labels

L1 Ethereum client

Projects

Status: In Review

Development

Successfully merging this pull request may close these issues.

1 participant