Repository navigation
perf(crypto): resolve registry public keys through a validator-index table - #743
MegaRedHand wants to merge 5 commits into
Conversation
…table Every signature check resolved each signer through a sharded SipHash map keyed by the 48 compressed bytes: a hash, a read lock and a probe per key. Lookups cost 0.5-0.7x of the block's pairing (ratios on a loaded M3). Callers that read a key from the registry now pass an IndexedPubkey. The cache first reads a set-once slot by index, with no lock and no hash, and trusts it only when the bytes stored in the slot equal the bytes the caller read. A mismatch (another fork or spec-fixture state naming a different key at that index), an empty slot, or an index past the bound falls back to the byte-keyed map, so the soundness argument of keying by bytes is unchanged. A key resolved by index is stored in the table and not the map, which keeps the per-key memory at or below the map's. The key-taking functions are generic over a sealed PubkeySource, so callers holding plain BlsPubkey slices compile unchanged.
… lookup Registry keys are often first seen through byte-path callers (subnet attestations, sync committee messages). Without promotion, every later index lookup of such a key missed the table and paid the map lookup too, so the table never filled for them. An index lookup that finds its slot empty and then hits the map with the same bytes now copies the point into the slot (set-once, first writer wins). The map keeps its entry, since byte-path callers cannot see the table and would have to validate again; such a key is held twice.
…table (#743) perf(crypto): resolve registry public keys through a validator-index table Every signature check resolved each signer through a sharded SipHash map keyed by the 48 compressed bytes: a hash, a read lock and a probe per key. Lookups cost 0.5-0.7x of the block's pairing (ratios on a loaded M3). Callers that read a key from the registry now pass an IndexedPubkey. The cache first reads a set-once slot by index, with no lock and no hash, and trusts it only when the bytes stored in the slot equal the bytes the caller read. A mismatch (another fork or spec-fixture state naming a different key at that index), an empty slot, or an index past the bound falls back to the byte-keyed map, so the soundness argument of keying by bytes is unchanged. A key resolved by index is stored in the table and not the map, which keeps the per-key memory at or below the map's. The key-taking functions are generic over a sealed PubkeySource, so callers holding plain BlsPubkey slices compile unchanged. perf(crypto): copy byte-path keys into the index table on first index lookup Registry keys are often first seen through byte-path callers (subnet attestations, sync committee messages). Without promotion, every later index lookup of such a key missed the table and paid the map lookup too, so the table never filled for them. An index lookup that finds its slot empty and then hits the map with the same bytes now copies the point into the slot (set-once, first writer wins). The map keeps its entry, since byte-path callers cannot see the table and would have to validate again; such a key is held twice. Cherry-picked from perf/index-keyed-pubkey-cache at a2156d3.
Gossip and the Beacon API still resolved registry keys through the byte-keyed map, so every key import had already put in the index table was validated a second time on its first gossip sighting and then held in both structures: within a day the map refilled with nearly every active key, about doubling the cache's memory. Every site that reads a key from the registry by index now passes the index, leaving the byte path to keys with no registry index. The entries gauge now says which structure holds a key, a new gauge counts the table's allocated chunks (its memory follows those, not its entries), and the docs no longer claim the table costs at most what the map did.
🤖 Claude Code ReviewReview of PR 743: index-keyed pubkey cacheThe change looks sound. I read the diff only and did not build it or run the tests. Correctness: no blocking issues
Minor points
Overall this is good to merge once you've considered Point 3. Automated review by Claude (Anthropic) · sonnet · custom prompt |
🤖 Kimi Code ReviewI'll review this PR which introduces an index-keyed pubkey cache to optimize BLS validation performance. This is a significant optimization for consensus-layer hot paths. Overall AssessmentThe PR introduces a two-tier pubkey cache (index table + byte-keyed map) to avoid hashing for registry keys. The design is sound but I have several concerns about edge cases, metrics correctness, and potential race conditions. Critical Issues1. Race condition in
|
| Severity | Count | Items |
|---|---|---|
| Critical | 0 | — |
| High | 0 | — |
| Medium | 2 | Item 1 (race/metrics skew), Item 5 (promotion overhead) |
| Low | 4 | Items 7-9 (naming), Item 13 (error message) |
| Info | 2 | Item 3 (portability), Item 11 (test gap) |
The PR is well-designed and correct for consensus safety. The two-tier cache design with bytes verification on index lookup is the right approach. Main actionable items are code clarity (naming) and acknowledging that the table_entries metric may undercount under race.
Automated review by Kimi (Moonshot AI) · kimi-k2.6 · custom prompt
🤖 Codex Code ReviewLooks good overall. The core change — switching single-signer verification call sites from raw
I did not find a concrete bug or security issue in the diff. If you want, I can do a second pass focused only on adversarial-state / malformed-signature edge cases in the new pubkey-cache tests. Automated review by OpenAI Codex · gpt-5.4 · custom prompt |
…table (#743): review fixes perf(beacon): look up registry keys by index on the gossip and API paths Gossip and the Beacon API still resolved registry keys through the byte-keyed map, so every key import had already put in the index table was validated a second time on its first gossip sighting and then held in both structures: within a day the map refilled with nearly every active key, about doubling the cache's memory. Every site that reads a key from the registry by index now passes the index, leaving the byte path to keys with no registry index. The entries gauge now says which structure holds a key, a new gauge counts the table's allocated chunks (its memory follows those, not its entries), and the docs no longer claim the table costs at most what the map did. Updates nightly's #743 from perf/index-keyed-pubkey-cache at a2156d3 to f733e97.
Stack 4/4 of the BLS verification work: (a) batched key sums -> (b) per-block signature batch -> (c) committee key sums -> (d) index-keyed pubkey cache. Based on (c).
What changed
IndexedPubkey { index, pubkey }and a sealed traitPubkeySource, implemented forBlsPubkey(byte path) andIndexedPubkey(index path).verify,fast_aggregate_verify,eth_fast_aggregate_verify,key_validate,eth_aggregate_pubkeys,SignatureSet::single,SignatureSet::fast_aggregate,AggregatePubkey::sumandAggregatePubkey::minusare generic over it, so existing&[BlsPubkey]callers compile unchanged. (aggregate_verifystays byte-only: no caller knows indices there.)PubkeyCachegains an index table: lazily allocated chunks of 4096OnceLock<(BlsPubkey, PublicKey)>, bounded by the existingMAX_CACHED_PUBKEYS. Reads take no lock and no hash. Entries are set once; the first writer wins.IndexedPubkey:indexed_attestation_signature_set_with_domainbuilders,indexed_payload_attestation_signature_set,attesters_aggregate_pubkey(and soattestation_signature_set),warm_key_sums, randao, the proposer block signature, proposer slashing headers, and voluntary exits (phase0, deneb, electra);from_bls_pubkeyis not a registry key), the sync aggregate and sync contributions (keys come from the sync committee's own key list), gloas builder keys (builder registry, not validator).lean_beacon_pubkey_cache_entriesgains astructurelabel (map,table), and the newlean_beacon_pubkey_cache_index_chunkscounts allocated table chunks, which is what the table's memory follows.Why
Pubkey lookups through the sharded SipHash map cost 0.5-0.7x of the block's pairing (measured on a heavily loaded M3, ratios only; absolutes to be taken on a quiet x86 host). The index table replaces hash + read lock + probe with one array read and a 48-byte compare.
Memory
Oncestate, padded to alignment 8The table's cost follows the registry's index span, not the number of keys in it: a chunk is allocated whole on its first key, and exited validators are spread through every chunk. On a registry of about 2.4M indices with about 1.2M active, every chunk is allocated, about 350 MiB, against about 290 MiB for the map holding the active keys on the parent branch: about +57 MiB. If that is too much, slots holding a pointer to a boxed entry would cut the empty slots to 16 B (about 230 MiB in total) at the price of one pointer chase per hit.
An earlier revision of this PR left gossip on the byte path. Gossip then validated every key import had already put in the table a second time and held it in both structures, so the map refilled with nearly every active key within about a day, about doubling the cache. Every registry read now passes its index.
Behavior notes
No input accepted before is rejected or the other way round. Spec fixtures reuse indices with different keys across states in one process, so the full spec run exercises the mismatch fallback continuously.
Tests
cargo test -p ethlambda-crypto --profile release-fast --lib(new: a byte-path key is copied into the table on its first index lookup, hit after warm and not in the map, different key at a warmed index falls back and verifies, index past the bound, a cold lookup allocates nothing, invalid keys cached nowhere, concurrent readers and writers agree withkey_validate).cargo test -p ethlambda-state-transition --profile release-fast --lib --features beacon-spec-tests: 658 passed.beacon_spec_testsfilters:/attestation/356,/sync_aggregate/182,sanity/595,random/1969 passed (175 ignored); full unfiltered: 7493 passed, 977 ignored, 0 failed.cargo fmt --all; clippy-D warningsclean onethlambda-cryptoandethlambda-state-transition.cargo clippy --profile release-fast --workspace --all-targets -- -D warnings: clean (rpc, p2p and blockchain compile against the generic signatures).Rerun after the review fixes, on the stack's tip (all four PRs,
--profile release-fast):ethlambda-crypto --lib47 passed, 14 ignored;ethlambda-state-transition --lib --features beacon-spec-tests(with the BLS fixture vectors) 658 passed; storage 213, blockchain 291, p2p 362, rpc 335 passed; fullbeacon_spec_tests7493 passed, 977 ignored, 0 failed;make lintclean, also on a trial merge of currentmain.