Repository navigation
perf(beacon): stop re-verifying stored custody columns at block import - #760
MegaRedHand wants to merge 2 commits into
Conversation
The mainnet profile showed about 8 ms of CPU per block in verify_cell_kzg_proof_batch under on_block, plus a DB read and SSZ decode of every custody column, all repeating work each sidecar already paid for on arrival: DataColumns only receives sidecars the p2p layer accepted (gossip validation or column::chain_checks, which run the structural check, the inclusion proof and the KZG batch), keyed by the root of the header those checks judged. Add DataAvailability::ColumnsVerified, a caller's promise that every sampled column was verified, and have data_availability_for return it once the custody columns are present, without reading them. DataAvailability::Columns keeps the full spec check, so the fork_choice fixtures still exercise it. The gloas envelope path gets the same treatment through check_execution_payload_envelope_columns_verified. The debug-build safety net stays on the write side (on_checked_data_columns re-runs chain_checks). Mirrors lighthouse's verified-column types, which carry the proof of verification so import never re-runs KZG.
|
Combined result for the four Lighthouse ports (#759 pubkey index, #760 column KZG, #761 progressive target balances, #762 tree-backed participation), squashed onto Offline import replay on ethlambda-5, same three mainnet corpora, both legs built at the commit and run with
Per STF step, ordinary blocks (n=162): Live on the ethlambda-2 mainnet follower (nightly 1d06bf9): steady |
|
Per-PR attribution on
#760 does not run in the replay (no custody columns), so the second step is #761's. Regressions found by the per-step split, and what the follow-up commits (now pushed to #761 and #762) did about them:
ethlambda-2 (mainnet follower) runs nightly 2b51a33 since 2026-11-03 14:42 UTC. |
…events layout (#765) Opt-in per-block telemetry: with --telemetry.clickhouse-url (or ETHLAMBDA_TELEMETRY_CLICKHOUSE_URL), a node writes one ClickHouse row per point in each block's life, and per data column arrival, in Silver's block_events table layout, so both clients' timings can be compared block by block from one table. Off by default: without the flag, emit builds nothing. See docs/telemetry.md. Cherry-picked from feat/clickhouse-block-events at 119e70e. Adapted to three nightly-only changes the PR's base (main 4992d8d) lacks: - #755 sends engine_newPayload from a task while the state transition runs speculatively, so the `engine` pair is only the wait left afterwards. The el_sent and el_verdict rows now come from new engine_sent/engine_answered instants that bracket the request itself, and the speculative transition writes stf_spec_start/stf_spec_end. docs/telemetry.md describes the overlap. - #760 answers a block whose stored columns are complete with DataAvailability::ColumnsVerified, which also counts as columns required, so such a block writes da_available rather than da_check_end. - #754 checks a block's sidecars as one group with a batched KZG, so the column rows are written from GroupVerdicts::record, once each sidecar's verdict is known.
…ility exhaustively Two rows depended on facts the compiler could not keep true: - el_sent and el_verdict read the `engine` section pair, whose meaning is the time the import waited for the answer. That equals the request's own round trip only while nothing runs during it; an import that overlaps the state transition with the request (as nightly's #755 does) would turn both rows into the wait without any type changing. The answer now carries its own sent/answered instants and verdict (`EngineAnswer`). - da_available vs da_check_end came from `matches!(..., Columns(_))`, so a new kind of availability evidence silently counts as nothing required. Merging into nightly hit exactly that with #760's `ColumnsVerified`. An exhaustive match makes a new variant a compile error here. Also warn at startup when the ClickHouse URL sends credentials over plain http to a host that is not loopback (suggested in review).
…events layout (#765): review fixes fix(telemetry): give the el rows their own instants and match availability exhaustively - el_sent and el_verdict come from an `EngineAnswer` (sent, answered, verdict) instead of the `engine` section pair, which is the time the import waited and, with #755's overlap, not the request's round trip. - da_available vs da_check_end comes from an exhaustive match on the availability evidence, so a new variant is a compile error rather than a silent "nothing required". - A startup warning when the ClickHouse URL sends credentials over plain http to a host that is not loopback. Updates nightly's #765 from feat/clickhouse-block-events at 119e70e to f2eb961. On nightly the answer's instants are the speculative path's request (spawned) and answer (received) instants, which replaces the nightly-only engine_sent/engine_answered fields, and the exhaustive match also counts #760's ColumnsVerified as columns required.
🗒️ Description / Motivation
At block import,
data_availability_forread every custody column sidecar back fromDataColumns(decoding each), and fork choice'sis_data_available_columnsthen re-ran the structural check and the KZG cell batch on each of them. Every column inDataColumnswas already fully verified in p2p before the chain actor stored it, so this repeated work on the import thread: ~8 ms of CPU per block inverify_cell_kzg_proof_batchunderon_blockon the mainnet follower, plus the reads. Lighthouse verifies a column once and carries that in its type (KzgVerifiedCustodyDataColumn->AvailableBlock).What Changed
fork_choice.rs: newDataAvailability::ColumnsVerified, documented as a promise its constructor makes (every sampled sidecar passedverify_data_column_sidecar, the inclusion proof and the KZG batch, and is stored under the block's root).is_data_available_columnsaccepts it without running anything;Columns(sidecars)still runs the full spec check, so the fork_choice fixtures keep exercising it.crates/blockchain/src/lib.rs:data_availability_forreturnsColumnsVerifiedoncecustody_columns_presentholds, with no sidecar reads or decodes.check_execution_payload_envelope_columns_verifiedskips only the availability step;beacon_envelope.rsno longer reads the sampled columns back.docs/data_storage.mdnote.Correctness / Behavior Guarantees
Audit before the change:
DataColumnsiskeep_data_column, reached only throughon_checked_data_columns(NewDataColumnSidecars).Accept(cheap + stateful checks: signature, proposer, inclusion proof, KZG); gossipQueue/Overloaded, fetched columns and replayed parked columns throughcolumn::chain_checks_for.publish_beacon_block) are built locally and handed over unchecked; debug builds still re-check every write (on_checked_data_columnsrunschain_checks_forunderdebug_assertions).PendingDataColumns, which the availability gate never reads.Benchmarks
The import replay cannot measure this:
BlockChainServer::for_replayhas an empty custody set, so neither path reads or verifies a column (the leg came out -2.1%, consistent across rounds, but its code does not run there). The expected saving is the ~8 ms of import-thread CPU per block seen in the live perf profile, plus the sidecar decodes; it needs a live measurement.Tests Added / Run
lib.rs: complete custody columns yieldColumnsVerified; undecodable stored rows still yieldColumnsVerified(proves nothing is read); a missing column still yieldsNone.cargo test --profile release-fast -p ethlambda-state-transition --lib(630),-p ethlambda-blockchain --lib(293), and with--features beacon-spec-tests(336, gloas envelope fixtures)fork_choice/206,networking/34Related Issues / PRs
✅ Verification Checklist
make fmt— cleanmake lint(clippy with-D warnings) — cleanmake test— not run; the crate lib tests and spec filters above were run instead