Skip to content

perf(crypto): decode, pair and check cells through silver_crypto_simd - #764

Closed
MegaRedHand wants to merge 2 commits into
nightlyfrom
perf/silver-crypto-simd
Closed

MegaRedHand wants to merge 2 commits into
nightlyfrom
perf/silver-crypto-simd

Conversation

@MegaRedHand

@MegaRedHand MegaRedHand commented Nov 3, 2026 •

Copy link
Copy Markdown
Collaborator

Motivation

silver_crypto_simd (the crypto crate of gattaca-com/silver) runs BLS12-381 point decoding, subgroup checks, pairing products and the PeerDAS cell operations eight lanes at a time on AVX-512 IFMA. Every Zen 4/5 host we run has IFMA. On other CPUs it falls back to blst and portable Rust through the same API. Its decoders return blst's own verdict on every input and its cell verifier returns the spec's, with Lean proofs for the IFMA routes (see the crate README for what they cover).

Based on nightly (2b51a33), as asked. Two commits: the swap, then the batch made parallel after the first benchmark showed it slower than blst.

What moves, what stays

The crate does not replace every blst or c-kzg API, so both stay as dependencies.

Function Before After
KeyValidate on cache misses blst key_validate, one key per rayon task uncompress_in_g1 in chunks of KEY_DECODE_CHUNK across rayon, identity refused by us
signature decode (sig_validate(_, false)) blst uncompress_in_g2
verify, fast_aggregate_verify, aggregate_verify, SignatureSet::verify blst pairing PairingProduct / two_pairs_are_one
verify_signature_sets (block batch) blst verify_multiple_aggregate_signatures (on blst's thread pool) random 64-bit weights kept; chunks of LANES - 1 sets checked in parallel on rayon, each with its own G2Point::weighted_sum signature sum, weighted keys and PairingProduct (one 8-lane Miller pass)
aggregate (signatures) blst decode + sum silver batch decode, blst sum
key sums, committee sums, signing, keygen blst blst (no faster route in the crate)
compute_cells c-kzg CellExtender
verify_cell_kzg_proof_batch c-kzg CellBatchVerifier
commitments, blob proofs, compute_cells_and_kzg_proofs, recover_cells_and_kzg_proofs c-kzg c-kzg

Two semantic differences, handled

  1. Identity public key. silver's PairingProduct drops any pair that holds the identity, and an empty product is true. blst refused an identity key (BLST_PK_IS_INFINITY). Without a guard, a cancelling key set (P, -P, both valid keys) would verify under the identity signature. core_verify and the batch now refuse the identity key before any pairing. The existing keys_that_cancel_never_verify_alone_or_in_a_batch and a_key_and_its_negation_sum_to_the_identity_and_never_verify tests cover it.
  2. Err vs Ok(false) for cell batches. CellBatchVerifier::verify answers false for malformed input, where the spec raises. verify_cell_kzg_proof_batch returns Ok(true) on true. On false it runs the spec's own input assertions in the spec's order (lengths, cell_index < CELLS_PER_EXT_BLOB, commitments and proofs decode, cells canonical) and returns Err if one fails, else Ok(false). That second pass only runs on a rejection.

Other changes

  • beacon::kzg now has its own Cell = [u8; BYTES_PER_CELL]. It is converted to c-kzg's type only at the calls into c-kzg, so the hot verify path no longer copies through c_kzg::Cell. Callers in fork_choice.rs, data_columns.rs, gloas_block_production.rs and the spec runner were updated.
  • The verifier and extender take &mut self and precompute tables on construction, so they live in a small Pool (a Mutex<Vec<_>>, take and return). Design choice to review: POOL_MAX_IDLE idle instances at most, and a verifier goes back only if its batch had at most MAX_RETAINED_BATCH_CELLS cells. A verifier keeps its buffers at its largest batch, so this keeps a whole-block batch from pinning its buffers.
  • A cache-miss set no larger than one chunk is now decoded on the calling thread instead of going through rayon's pool.
  • Cargo.lock only gains the silver_crypto_simd entry. Its only dependency is blst 0.3.16, which we already have.

Tests

  • New differential tests compare each swapped function with blst or c-kzg directly: key and signature decoding on valid, flag-flipped, identity, malformed-identity, past-modulus and random encodings (batch and single); verify / fast_aggregate_verify / aggregate_verify at signer counts 1 to 17; verify_signature_sets at 2 to 17 sets with a bad set in several positions; compute_cells; verify_cell_kzg_proof_batch on 15 valid and broken batches, comparing Ok(true) / Ok(false) / Err. They run the IFMA kernels on an IFMA host and the fallback elsewhere.
  • Locally (aarch64, fallback path): beacon spec tests on mainnet, 7493 passed, 0 failed; KZG and BLS fixture vectors (cryptography-specs v0.1.0) pass; ethlambda-crypto, ethlambda-state-transition, ethlambda-p2p lib tests pass; clippy clean.
  • On ethlambda-5 (Zen 5, IFMA path): differential tests at b6819ef (ethlambda-crypto 45, ethlambda-state-transition 699, ethlambda-p2p 371 at 7bacadc) and silver's own suite with SILVER_REQUIRE_IFMA=1 (119 passed, so the IFMA kernels really ran) all pass.

Benchmarks (ethlambda-5, Ryzen 9700X, node stopped)

Setup. ethlambda-5 (Ryzen 7 9700X, Zen 5, AVX-512 IFMA). The node was stopped 15:09:37Z to 16:00:29Z on 2026-11-03. The control is nightly 2b51a33; the candidate is this PR at b6819ef. Both were built with cargo build --release --locked.

  • Microbench: an untracked integration test calls beacon::bls and beacon::kzg. The same file was built against both trees (release-fast) and run for 3 rounds with the legs alternated, 9 repetitions per case. The table shows the median of the per-round medians.
  • Replay: benchmark import replay on three mainnet corpora, --block-delay 1000, legs rotated. "warm" runs pass --warm-committee-sums (3 rounds, 18 runs); "cold" runs omit it (2 rounds, 12 runs), so the first timed block validates every key it meets. "Steady" means ordinary blocks after the run's first epoch.

Microbench, µs per call

case nightly this PR ratio
verify, key cached 538 382 0.71x
fast_aggregate_verify, 64 keys cached 556 400 0.72x
fast_aggregate_verify, 512 keys cached 601 466 0.78x
verify_signature_sets, 12 sets 999 758 0.76x
verify_signature_sets, 64 sets 2,642 2,467 0.93x
aggregate, 64 signatures 2,874 685 0.24x
aggregate, 512 signatures 23,040 5,489 0.24x
eth_aggregate_pubkeys, 16,384 unseen keys 69,468 16,640 0.24x
eth_aggregate_pubkeys, 256 unseen keys 1,063 712 0.67x
key_validate, one unseen key 31.7 33.8 1.07x
verify_cell_kzg_proof_batch, 1 column × 6 blobs 2,407 1,259 0.52x
verify_cell_kzg_proof_batch, 1 column × 21 blobs 4,319 1,570 0.36x
verify_cell_kzg_proof_batch, 8 columns × 21 blobs 12,671 3,351 0.26x
verify_cell_kzg_proof_batch, 128 columns × 6 blobs 43,710 11,023 0.25x
compute_cells, one blob 1,782 102 0.06x

Import replay, STF ms per block (mean / p50)

nightly this PR delta (mean)
warm, steady (n=222): STF 17.8 / 17.6 17.5 / 17.2 −0.4 (−2.1%)
warm, steady: batch_signatures 1.18 / 1.15 0.86 / 0.82 −0.3 (−27%)
warm, steady: block_signature 1.57 / 1.57 1.35 / 1.32 −0.2 (−14%)
cold, first block (n=6): STF 298.0 / 296.6 216.4 / 216.1 −81.6 (−27%)
cold, first block: attestations 126.1 / 125.5 44.4 / 44.0 −81.7 (−65%)
cold, steady (n=148): STF 28.4 / 24.9 30.2 / 25.9 +1.8 (+6.2%)

Notes:

  • The block replay barely moves, because BLS is now a small share of nightly's ~18 ms STF: about 3 ms across batch_signatures and block_signature, and those drop by a quarter. The headline gains are the first block after a start (unseen keys decoded eight per step) and KZG, which the replay does not exercise (it has no custody columns). In a live node KZG means gossip columns (a 21-blob column goes from 4.3 to 1.6 ms) and compute_cells for getBlobs/block production (17x).
  • Cold steady +1.8 ms is all in execution_payload (+2.1 ms), a phase that times withdrawals and the payload and calls no BLS or KZG code. Warm runs show +0.3 ms mean (+0.05 ms p50) there, and corpus C moves the other way (−1.3 ms STF). I read it as variance, but I have not explained it.
  • Single unseen key decode is 7% slower. For one key, silver's IFMA build uses its portable decoder, and blst is slightly faster at that. A possible follow-up is to send miss sets smaller than a lane group to uncompress_in_g1_blst (the same verdict).
  • The first version of the batch was a regression: 2.2x slower than blst at 12 sets and 4.1x at 64, because blst spreads the sets over its thread pool. b6819ef checks the batch in parallel chunks of LANES - 1 sets, which gives the numbers above.

Known CI failure

silver_crypto_simd 0.1.0 was published 2026-11-03, so make cooldown-check fails until the 14-day cooldown ends on 2026-11-17. I left it failing: neither bypassing the check nor pinning a path dependency is right.

Questions for review

  • Is a crate published on 2026-11-03 at 0.1 (README: "the API may change before 1.0") acceptable as a consensus-critical dependency once the cooldown passes?
  • Pool sizing (POOL_MAX_IDLE, MAX_RETAINED_BATCH_CELLS): keep, or size from available_parallelism?

silver_crypto_simd (gattaca-com/silver's crypto crate) runs BLS12-381 point
decoding, subgroup checks, pairing products and the PeerDAS cell operations
eight lanes at a time on AVX-512 IFMA, which every Zen 4/5 host we run has,
and falls back to blst and portable Rust elsewhere through the same API. Its
decoders return blst's own verdict on every input, and its cell verifier the
spec's, so it can sit behind the existing spec-named functions unchanged.

BLS (`ethlambda_crypto::bls`): key and signature decoding, `verify`,
`fast_aggregate_verify`, `aggregate_verify` and the block batch now go through
it; cache misses are decoded in chunks of `KEY_DECODE_CHUNK` across rayon.
Point sums, committee sums, signing and key generation stay on blst, since
the crate has no faster route for them. Its pairing product drops a pair
holding the identity where blst refused an identity key, so `core_verify` and
the batch refuse that key themselves before any pairing (the `P, -P`
cancellation tests cover it).

KZG (`beacon::kzg`): `compute_cells` and `verify_cell_kzg_proof_batch` run on
it; commitments, blob proofs, cell proofs and recovery stay on c-kzg. Its
verifier answers false for malformed input, so a rejection is followed by the
spec's own input assertions to keep `Err` (fixture `output: null`) apart from
`Ok(false)`. The module now has its own `Cell` byte array, converted to
c-kzg's type only at calls into c-kzg. The verifier and extender take
`&mut self` and are costly to build, so they live in small pools.

Differential tests compare every swapped function with blst or c-kzg on
valid, corrupt, off-subgroup and malformed inputs; on an IFMA host they
exercise the vector kernels.
blst's batch spread each set's hashing, key weighting and Miller loop over
its thread pool; the first silver version did all of it on the calling
thread and measured 2.2x slower than blst at 12 sets and 4.1x at 64 on
ethlambda-5. A batch is now cut into chunks of `LANES - 1` sets, each its own
random linear combination (fresh weights, own signature sum, own final
exponentiation), checked in parallel with each chunk's hashes in parallel
too. A chunk plus its signature-sum pair fills one pass of the Miller-loop
lanes, and soundness holds per chunk, since a forged set must still pass its
own chunk's check.
@MegaRedHand MegaRedHand added performance Performance improvements or possible performance improvements beacon Ethereum Beacon Chain client labels Nov 3, 2026
@MegaRedHand

Copy link
Copy Markdown
Collaborator Author

Superseded by #772, which merges this backend swap under the Silver-style usage patterns of #771 (hash cache, batches grouped by message, registry warm-up, gossip batching). Benchmarks of all three legs against nightly are in #771 and #772.

@MegaRedHand MegaRedHand closed this Nov 3, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

beacon Ethereum Beacon Chain client performance Performance improvements or possible performance improvements

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant