Skip to content

perf(core): drop the per-state-read heap allocations in the witness path - #198

Merged
flyq merged 7 commits into
mainfrom
liquan/perf/witness-state-read-allocations
Sep 24, 2026
Merged

flyq merged 7 commits into
mainfrom
liquan/perf/witness-state-read-allocations

Conversation

@flyq

@flyq flyq commented Sep 3, 2026 •

Copy link
Copy Markdown
Contributor

Summary

PR 5/6 of the #170 split. #197 has merged and main is merged in as of 960a3bd, so this now targets main directly. Removes the heap allocations on the per-state-read path of block replay. Listed in vincent's review of #170 as "A hot-path optimization" worth its own line.

This lookup runs once per unique account and storage slot touched by a block, so it is the hottest thing in WitnessDatabase.

  • WitnessDatabase reads go through salt's find(), which hands back the SaltValue (an inline array) instead of plain_value()'s freshly heap-allocated copy of the value bytes. Callers decode from .value() in place.
  • Key encodings are stack-allocated: PlainKey::account_key_bytes (a borrow of the address) and PlainKey::storage_key_bytes (a FixedBytes<52>) replace PlainKey::{Account,Storage}(..).encode()'s Vec. encode() itself delegates to them, so there is one definition of each encoding. bucket_id_for_account / bucket_id_for_slot use them too — that is one allocation per call removed on another per-read path.
  • LightWitness::metadata stops cloning the SaltValue it only reads.

Net: two heap allocations per witnessed state read, plus one per bucket_id_for_* call.

Correctness

The interesting claim is that find() returns the same bytes plain_value() did. That is pinned by the mainnet fixture replays (validate_block_mainnet_fixtures, validate_block_deriving_updates_mainnet_fixtures): a read that returned different bytes changes execution and fails the post-state-root check on every fixture.

stack_key_encodings_match_encode pins the narrower claim: each encoder produces one specific byte layout. It asserts the literal bytes rather than one encoder against encode() — while encode() delegates, comparing the two passes by construction and pins nothing, so a drift in the shared primary would go unnoticed. Mutation-checked: swapping the concatenation order in storage_key_bytes fails the test.

Follow-ups (deliberately not in this PR)

Two siblings of the same root cause are left alone, both because they reach outside this diff:

  • salt's FIXME at state.rs:232 names two copy sources on the read path. This PR removes the second (salt_val.value().to_vec()). The first is StateReader::value returning SaltValue by value, which lands here as a 94-byte clone per read; removing it means changing that trait to return a borrow and bumping the tag.
  • LightWitnessExecutor::direct_lookup_tbl allocates one Vec per witness entry at build time (light_witness.rs:253) — the same "plain key on the heap" shape, at the construction site. Unifying it with the stack encoders needs an enum key or a raw-entry lookup, since a padded 52-byte inline key cannot Borrow<[u8]> for a 20-byte account key.

Testing

cargo fmt --all --check, cargo clippy --workspace --all-targets --all-features (0 warnings), cargo sort --check, full workspace suite 490 passed / 0 failed, cargo test -p stateless-core --no-default-features --lib --no-run clean.

A quality pass landed in cccd964: the encoder docs now state the rule by ownership (own the key → encode(); borrow it for a lookup → the byte encoders) instead of by "hot path", which is the question a contributor can actually answer; and the encoder test asserts literal bytes instead of comparing an encoder against the caller that forwards to it.

flyq and others added 5 commits September 3, 2026 19:05
Deletes the second sources of truth and the dead parameters/accessors the
/simplify review found, with no observable change on canonical-block paths.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01CHVpMMX9N69sUNbuKgpBVY
Deletes the `writer` parameter threaded through `validate_block`,
`validate_block_deriving_updates`, `replay_block` and `verify_and_replay`,
and with it EIP-3155 trace output. A feature deletion, not a cleanup.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01CHVpMMX9N69sUNbuKgpBVY
…s_used from the executor

Both are definitional rewrites of security-critical derivations, argued in the
PR body: `trie_hash()` is `keccak256(encoded_2718())`, and mega-evm's
`gas_used` is the last receipt's cumulative gas.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01CHVpMMX9N69sUNbuKgpBVY
Block integrity verification becomes the RPC retry loop's finalize step and
runs on the blocking pool; the advancer's pre-advance hooks and store commit
run there too. Panic semantics are preserved and pinned by tests.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01CHVpMMX9N69sUNbuKgpBVY
`WitnessDatabase` reads go through salt's `find()` with in-place decoding and
stack-allocated key encodings; `LightWitness::metadata` stops cloning the
`SaltValue` it only reads.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01CHVpMMX9N69sUNbuKgpBVY
@mega-maxwell

mega-maxwell Bot commented Sep 3, 2026 •

Copy link
Copy Markdown
Contributor

Claude review status

Living comment — rewritten in place. The review workflow keeps this single comment up to date instead of posting a new one each round, so it always describes the latest reviewed commit and the earlier text is intentionally gone. No reply is needed here; reply to a finding in its own review thread, and answer an open question in a reply on this PR. The next review round reconciles your answer.

✅ Review clean

Last reviewed: 960a3bdd..cccd964e · updated 2026-09-20T07:33:07+00:00

New this round: 0 finding(s), 0 question(s) · Resolved this round: 0 · Open questions: 0

@codecov

codecov Bot commented Sep 3, 2026 •

Copy link
Copy Markdown

Codecov Report

❌ Patch coverage is 97.05882% with 1 line in your changes missing coverage. Please review.
✅ Project coverage is 92.6%. Comparing base (d68f547) to head (cccd964).

Files with missing lines Patch % Lines
crates/stateless-core/src/light_witness.rs 0.0% 1 Missing ⚠️

☔ View full report in Codecov by Harness.
📢 Have feedback on the report? Share it here.

🚀 New features to boost your workflow:
  • ❄️ Test Analytics: Detect flaky tests, report on failures, and find test suite problems.

@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: 50741dc320

ℹ️ About Codex in GitHub

Codex has been enabled to automatically 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 👍.

When you sign up for Codex through ChatGPT, Codex can also answer questions or update the PR, like "@codex address that feedback".

@flyq
flyq marked this pull request as draft September 4, 2026 00:28
Base automatically changed from liquan/perf/blocking-work-off-the-runtime to main September 20, 2026 06:18
flyq and others added 2 commits September 20, 2026 14:42
…tate-read-allocations

# Conflicts:
#	bin/stateless-validator/src/app.rs
#	bin/stateless-validator/src/chain_sync.rs
#	bin/stateless-validator/src/metrics.rs
#	bin/stateless-validator/src/runner.rs
#	bin/stateless-validator/src/validator_db.rs
#	bin/stateless-validator/tests/integration.rs
#	crates/stateless-common/Cargo.toml
#	crates/stateless-common/src/metrics.rs
#	crates/stateless-common/src/rpc_client.rs
#	crates/stateless-core/src/chain_spec.rs
#	crates/stateless-core/src/db.rs
#	crates/stateless-core/src/executor.rs
#	crates/stateless-core/src/pipeline/advancer.rs
#	crates/stateless-core/src/pipeline/fetcher.rs
#	crates/stateless-core/src/pipeline/tests.rs
#	crates/stateless-r2/src/fetch.rs
… test bite

The encoder docs justified themselves as "for per-state-read hot paths", which
asks the next contributor an unmeasurable question; the decidable one is whether
the caller needs to own the key. encode() is now documented as the owned form
derived from the borrowing primaries.

stack_key_encodings_match_encode compared each encoder against encode(), which
delegates to it — true by construction, so it killed no mutation. It now asserts
the literal byte layout, which catches a swapped concatenation (verified: the
mutation fails the new test and passes the old one).

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
@flyq
flyq marked this pull request as ready for review September 20, 2026 07:32
@flyq
flyq merged commit 551df81 into main Sep 24, 2026
25 checks passed
@flyq
flyq deleted the liquan/perf/witness-state-read-allocations branch September 24, 2026 04:19
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.

3 participants