Repository navigation
perf(beacon): verify a block's signatures in one random-weight batch - #738
MegaRedHand wants to merge 6 commits into
Conversation
Block import paid one pairing per signature: the proposer's, the RANDAO reveal, every attestation, the sync aggregate and each slashing, exit and BLS change. A pairing is dominated by its final exponentiation, which a batch pays once, so a block with a dozen signature sets checks in a fraction of the time. ethlambda-crypto gains SignatureSet and verify_signature_sets: resolved checks combined with unpredictable non-zero 64-bit weights from the OS-seeded CSPRNG (without them two invalid signatures can cancel). The state transition builds each set where the specification verifies it and pushes it into a SignatureBatch that the block driver verifies once processing ends. A failing batch re-checks sets one by one so the error still names the first bad signature with its usual label. Signatures whose failure rejects the block are deferred; deposit signatures, whose failure only skips the deposit, stay immediate, as does epoch processing. process_block keeps its meaning and signature, apply_block adds the proposer's signature to the same batch. The operation packer in block production uses an immediate batch so a rejected candidate still writes nothing. The attestation validity helpers are split into a set builder plus the existing bool function, so gossip and fork choice are untouched.
SignatureSet::fast_aggregate now sums its keys with part a's batched sum_validated_points instead of a sequential AggregatePublicKey::aggregate, and the identity-key check in verify_signature_sets documents that blst rejects the same input.
…738) perf(beacon): verify a block's signatures in one random-weight batch Block import paid one pairing per signature: the proposer's, the RANDAO reveal, every attestation, the sync aggregate and each slashing, exit and BLS change. A pairing is dominated by its final exponentiation, which a batch pays once, so a block with a dozen signature sets checks in a fraction of the time. ethlambda-crypto gains SignatureSet and verify_signature_sets: resolved checks combined with unpredictable non-zero 64-bit weights from the OS-seeded CSPRNG (without them two invalid signatures can cancel). The state transition builds each set where the specification verifies it and pushes it into a SignatureBatch that the block driver verifies once processing ends. A failing batch re-checks sets one by one so the error still names the first bad signature with its usual label. Signatures whose failure rejects the block are deferred; deposit signatures, whose failure only skips the deposit, stay immediate, as does epoch processing. process_block keeps its meaning and signature, apply_block adds the proposer's signature to the same batch. The operation packer in block production uses an immediate batch so a rejected candidate still writes nothing. The attestation validity helpers are split into a set builder plus the existing bool function, so gossip and fork choice are untouched. Cherry-picked from perf/block-signature-batch at 7736808.
…tch unverified A block fetched by request reaches the state transition without gossip's precheck, and with the proposer's signature in the body's batch a block its proposer never signed was only rejected after the whole body ran. Check that one signature on its own again, before the body, as before the batch existed. A deferring SignatureBatch could also be built and dropped without its sets ever being checked, turning any function that takes one into "valid except for its signatures". Outside the crate, a deferring batch now only comes from SignatureBatch::deferred, which verifies once its closure returns. Also cover an infinity signature inside a batch and keys that cancel.
🤖 Codex Code ReviewLooks good overall. The PR is a sensible refactor toward reusable signature batching, and I did not spot a consensus-breaking issue in the changed paths.
No requested changes from me. If you want, I can also do a second pass specifically on whether PR 738 needs new regression tests around gloas block production and attestation batching. Automated review by OpenAI Codex · gpt-5.4 · custom prompt |
🤖 Claude Code ReviewReview: PR 738, per-block BLS signature batchI read the new Overall: the design is sound and I found no blocking issues. Soundness of the batch
Deferral semantics
Minor points (non-blocking)
LGTM, pending CI on the spec fixtures for both presets. Automated review by Claude (Anthropic) · sonnet · custom prompt |
🤖 Kimi Code ReviewI'll review this PR which implements BLS signature batching for block processing. This is a significant optimization and correctness change for consensus-layer signature verification. High-Level AssessmentThe PR introduces deferred signature batching across all beacon state transition forks, replacing individual pairing checks with a single random-linear-combination batch verification. This is a well-known optimization (2-3x faster for typical blocks), but requires careful handling because:
Detailed Review1. Core Batching Logic (
|
| Operation | Deferred? | Immediate fallback? | Correct? |
|---|---|---|---|
| Block proposer | No | N/A | Yes (early reject) |
| RANDAO | Yes | process_randao |
Yes |
| Attestation (all forks) | Yes | indexed_attestation_signature_set → verify |
Yes |
| Sync aggregate (non-empty) | Yes | SignatureSet::verify |
Yes |
| Sync aggregate (empty) | No (infinity check) | N/A | Yes |
| Proposer slashing | Yes | SignatureSet::verify |
Yes |
| Attester slashing | Yes | indexed_attestation_signature_set → verify |
Yes |
| Voluntary exit | Yes | SignatureSet::verify |
Yes |
| BLS to execution change | Yes | SignatureSet::verify |
Yes |
| Deposit | No | N/A | Yes (bad deposit skipped, not rejected) |
| Execution payload bid (gloas) | Yes | SignatureSet::verify |
Yes |
| Payload attestation (gloas) | Yes | SignatureSet::verify |
Yes |
All correct. Deposits remain immediate because a bad signature deposit is skipped (not rejected), which changes state transition behavior.
Performance Assessment
The batch verification uses blst's verify_multiple_aggregate_signatures with:
- One Miller loop per signature set
- One final exponentiation total
- Random 64-bit weights
For a typical block with ~16 attestations + RANDAO + sync aggregate + exits:
- Before: ~19 full pairings = ~19 final exponentiations
- After: ~19 Miller loops + 1 final exponentiation ≈ 3-4x faster
The SignatureSet construction validates keys/signatures once, avoiding re-validation in the batch. Good.
Final Verdict
Approve with minor suggestions.
The PR is correct, well-tested, and implements a significant optimization safely. The deferred batch pattern with immediate fallback for block production is sound.
Suggested changes:
-
Document
INFINITY_FLAGsource (crates/common/crypto/src/bls/batch.rs:75):/// The bit of a compressed G1 point's first byte that marks the point at /// infinity (Zcash BLS12-381 §1.2: "The most significant three bits of /// u[0] are used to unambiguously represent the underlying element"). const INFINITY_FLAG: u8 = 0b0100_0000;
-
Add safety comment (
crates/blockchain/state_transition/src/beacon/stf/signatures.rs:142):// Unreachable with correct BLS batch verification: if the batch failed, // at least one individual set must fail. This is a defense-in-depth panic. verify(false, last)
-
Clarify test wrapper (
crates/blockchain/state_transition/tests/beacon_spec/operations.rs:174-192):
Add comment explaining theunhandledpattern for operation-name errors vs. state-transition errors. -
Consider whether
process_block_and_verifyshould be the pattern for all forks, not just gloas
Automated review by Kimi (Moonshot AI) · kimi-k2.6 · custom prompt
…738): review fixes fix(beacon): check the proposer's signature first and never drop a batch unverified A block fetched by request reaches the state transition without gossip's precheck, and with the proposer's signature in the body's batch a block its proposer never signed was only rejected after the whole body ran. Check that one signature on its own again, before the body, as before the batch existed. A deferring SignatureBatch could also be built and dropped without its sets ever being checked, turning any function that takes one into "valid except for its signatures". Outside the crate, a deferring batch now only comes from SignatureBatch::deferred, which verifies once its closure returns. Also cover an infinity signature inside a batch and keys that cancel. Updates nightly's #738 from perf/block-signature-batch at 7736808 to e6eeb5b.
…and resources (#756) The import replay reported `stf` as one number, which is most of a mainnet block's import but does not say which step of the transition to attack, nor whether an epoch-crossing block, a block after skipped slots and an ordinary block differ. The beacon STF had no internal timing at all. lean_beacon_stf_phase_seconds{context,phase} is observed around the steps of the transition: process_slots (excluding epoch work), process_epoch, the block signature, header (including the body hash it commits to), randao, eth1 data, execution payload (gloas: parent payload, withdrawals, bid), each operation list, the sync aggregate and the post-state root check. One Instant pair per step, none per attestation; an empty list observes nothing. The phase list is generated with the enum that the code observes through, so the two cannot disagree. The epoch precompute worker runs process_epoch on another thread while an import runs, and the replay reads the histogram before and after each block, so one series would charge the worker to whichever block was importing. The series is split by a context label instead: only fork_choice's transition_block sets `import` (a thread-local guard, since the steps are observed deep inside the per-fork block processing), and every other caller writes `other`. The replay reads only `import`. The replay report (schema_version 2) gains per block: the STF steps, the stf time outside any step, a class (epoch_boundary, after_skip, ordinary), process CPU seconds and RSS; and per run: p95 and p99, a per-class summary and the deltas of the cache lookup counters, so precompute hit rates appear in it. Consensus behavior is unchanged. Adapted onto nightly: block processing now defers every signature but the proposer's into one `SignatureBatch` (#738), so the old inline checks the timers wrapped no longer exist. Every call site keeps nightly's `signatures` argument and order, with the timers wrapped around the same steps as before; the proposer signature stays immediate and is still timed as `block_signature`. A new phase `batch_signatures` times `SignatureBatch::verify`, the one random-weight check of everything the steps deferred, so the verify cost that used to sit inside `randao`, the operation lists and `sync_aggregate` is now its own series. Those phases now cover building the signature sets and the state changes, not verification, and docs/metrics.md and docs/benchmarking.md say so. The phase is declared in the `StfPhase` enum, so the label list stays generated from it. replay.rs keeps both `--warm-committee-sums` (warm-up before the timers start) and the report changes; the report test for warm_committee_sums gets the new `Caches` argument. Verdicts, order and errors are nightly's. docs/benchmarking.md keeps #755's paragraph on the overlapped transition ahead of this section. Cherry-picked from perf/beacon-stf-phase-timing at c891134, adapted in 5c385e084.
Block import benchmark (offline replay of mainnet blocks, steady state)
Method
|
Stack 2/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 (a).
What changed and why
Block import checked every signature with its own pairing: the proposer's, the RANDAO reveal, each attestation, the sync aggregate and any slashings, exits and BLS changes. A pairing check is dominated by its final exponentiation, which a batch pays once for all sets.
ethlambda-crypto: newbls/batch.rswithSignatureSet(single,fast_aggregate,verify) andverify_signature_sets. A set is the resolved inputs of a check (validated and summed key, message, validated signature); a batch isverify_multiple_aggregate_signatureswith 64-bit non-zero weights fromrand's OS-seeded CSPRNG. Random weights are required: without them two invalid signatures can be crafted to cancel; with unpredictable 64-bit weights a forger succeeds with probability about 2^-64. A single set skips the weights. A set whose summed key is the identity fails the batch explicitly, as it fails alone.ethlambda-state-transition: newstf/signatures.rswithSignatureBatch. Operations build their set where the specification verifies it and push it with the same label the immediate check used; on a batch failure the sets are re-checked one by one and the first failing label is the rejection. Outside the crate there are only two ways to get a batch, and both check every set:SignatureBatch::deferred(|batch| ...)verifies once the closure returns, andimmediate()checks at push time (see below).new()andverify()are crate-internal, so a caller cannot drop a deferring batch unverified.stf::block::process_blockkeeps its signature and still means "the block is valid": it runs the body insideSignatureBatch::deferred, so the batch is verified before it returns.apply_blockchecks the proposer's block signature on its own first, as before this PR: a block fetched by request reaches the STF without gossip'sprecheck_block, so a block its proposer never signed is still rejected after one pairing rather than after the whole body. The body's batch is verified before the state root is computed.builder(...).is_some_and(|set| set.verify())). Every structural check stays immediate; gossip, fork choice and block production keep calling the bool functions.Stacked on part a (batched key sums):
SignatureSet::fast_aggregatesums its keys withsum_validated_points. blst already rejects an infinity public key on both the single and the batch path; the explicit identity check inverify_signature_setsstates that rule and changes no behavior.Deferred and immediate signatures
Deferred (failure rejects the block): RANDAO reveal (
block.rs), proposer slashing headers (operations.rs, shared by gloas), attester slashings (phase0, electra, gloas), attestations (phase0, altair, deneb, electra, gloas), voluntary exits (phase0, deneb, electra), BLS to execution changes (capella.rs), sync aggregate (altair.rs; empty participants keep the infinity-signature rule and push nothing), gloas execution payload bid, gloas payload attestations.Immediate, unchanged (failure changes state but does not reject): every deposit signature (
operations.rs,electra.rs,epoch/electra.rs) and the gloas builder deposit signature. The gloas envelope signature is not reachable fromprocess_block.Behavior notes
Errnow can leave the state partly mutated by operations whose signature was checked at the end. Import runs the transition on a clone (transition_block,fork_choice.rs) and every caller discards the state after an error.SignatureBatch::immediate(), which checks at push time and keeps that guarantee.gloas::process_block_and_verifyis the helper for the direct gloas drivers in block production.Immediate, as on
main: the proposer's block signature (apply_block), so a forged block stays cheap to reject.Measured
On a loaded M3, the pairing per block took 2.7-4x less wall time with one batch. That ratio mixes two effects: fewer final exponentiations, and blst's
verify_multiple_aggregate_signaturesspreading the Miller loops over its own pool where the old per-set check used one worker. CPU time drops too: profiling a Hoodi block replay on a Ryzen 9700X (the whole stack, warm caches) puts pairing and hash-to-curve at 7.9 ms CPU per block before and 2.4 ms after. That run predates this PR's review fix, which takes the proposer's signature back out of the batch, so it now costs about one more pairing per block than measured.Tests
cargo test -p ethlambda-crypto --profile release-fast --lib: the batch tests cover a valid batch of ten, one bad among ten, swapped signatures, empty batch, single set, repeated messages, mixed single and aggregate, agreement withbls::verifyandbls::fast_aggregate_verify,Nonefor invalid inputs, weights never zero, an infinity signature among valid sets, and keys that cancel ([P, -P]never verifies, alone or in a batch, with the identity signature or a real one;[P, P, -P]verifies underP's signature).cargo test -p ethlambda-state-transition --profile release-fast --lib --features beacon-spec-tests: 655 passed, 0 failed, rerun after the merge (includes the BLS fixture vectors and the new tests: a bad RANDAO reveal is deferred and rejected by the batch, a valid one passes,RandaoVerification::Skippushes nothing, deferred and immediate failures carry the same label, one bad signature among several rejects the batch).cargo test -p ethlambda-state-transition --profile release-fast --test beacon_spec_tests --features beacon-spec-tests(unfiltered, mainnet preset): 7493 passed, 0 failed, 977 ignored. The operation runner builds a batch around each operation and treats anErrfromverify()as the rejection.beacon_spec_testsafter the merge:/attestation/356 passed,/sync_aggregate/182 passed,sanity/595 passed, 0 failed.cargo clippy --profile release-fast -p ethlambda-crypto -p ethlambda-state-transition -p ethlambda-rpc --all-targets -D warningsclean,cargo fmt --allapplied.On this branch after its review fixes: crypto
--lib32 passed; state-transition--lib655 passed;beacon_spec_testsoperations/2141,sanity/595,finality/41,fork_choice/206,random/1969 (175 ignored),transition/172,sync/6 passed; the rpc validator-client tests pass.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.