Skip to content

Batch EIP-2537 pairing check Miller loops and use affine MSM - #12329

Merged
Marchhill merged 8 commits into
masterfrom
perf/bls-batched-miller-loop
Jul 21, 2026
Merged

Batch EIP-2537 pairing check Miller loops and use affine MSM#12329
Marchhill merged 8 commits into
masterfrom
perf/bls-batched-miller-loop

Conversation

@Marchhill

@Marchhill Marchhill commented Jul 7, 2026

Copy link
Copy Markdown
Contributor

Related PR: NethermindEth/blst-bindings#9

Changes

Adopts new Nethermind.Crypto.Bls 1.1.0-preview.199 APIs (NethermindEth/blst-bindings#9) in the EIP-2537 precompiles:

  • BLS12_PAIRING_CHECK: pairs are decoded into contiguous affine buffers (validated in parallel, as the MSM precompiles already do) and the product of Miller loops is computed in a single batched MillerLoopN call that shares the Fp12 squarings across pairs — measured 1.14×–1.45× end-to-end pairing-check speedup for 2–16 pairs — replacing the serial per-pair MillerLoop + GT multiplication. Pairs containing an infinity point contribute e(x, y) = 1 and are excluded from the batch; their points are still fully validated, preserving exact consensus semantics (validation order, error results, and gas are unchanged).
  • BLS12_G1MSM / BLS12_G2MSM: points are decoded directly into affine layout (smaller rented buffers) and multiplied with MultiMultAffine, skipping the internal Jacobian→affine batch conversion.

Types of changes

What types of changes does your code introduce?

  • Optimization

Testing

Requires testing

  • Yes

If yes, did you write tests?

  • Yes

Notes on testing

New regression tests: all-infinity pairing check input returns 1; subgroup-invalid G1/G2 points in infinity-paired slots are still rejected; all-infinity multi-point MSM returns infinity. Full EIP-2537 vector suites (1073 tests), the EIP-2537 gas tests, and the full Nethermind.Evm.Test suite pass; zkevm configurations verified to compile.

Remarks

The referenced Nethermind.Crypto.Bls 1.1.0-preview.199 is published on the nugettest.org staging feed (already a configured package source in this repo), so CI restores normally. ⚠️ Before merging: NethermindEth/blst-bindings#9 must be merged and a final package published to nuget.org, then the version pin, nuget.config mapping, and packages.lock.json updated accordingly.

…affine MSM

Adopt the new Nethermind.Crypto.Bls 1.1.0-preview.197 APIs:

- BLS12_PAIRING_CHECK: decode all pairs into contiguous affine buffers
  (validated in parallel like the MSM precompiles), then compute the
  product of Miller loops in a single batched MillerLoopN call sharing
  the Fp12 squarings across pairs, instead of one MillerLoop + GT
  multiplication per pair. Pairs containing a point at infinity
  contribute e(x, y) = 1 and are excluded from the batch, but both of
  their points are still validated and subgroup-checked as before.
- BLS12_G1MSM / BLS12_G2MSM: decode points directly into affine layout
  via the new raw affine Decode (shrinking the rented point buffers
  from P1.Sz/P2.Sz to P1Affine.Sz/P2Affine.Sz longs per slot) and call
  MultiMultAffine, skipping the internal Jacobian-to-affine batch
  conversion that MultiMult performed.

Validation order and error results are unchanged. Add regression tests
for all-infinity pairing input, subgroup-invalid points in infinity
pairs, and all-infinity multi-point MSM.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
@github-actions github-actions Bot added dependencies Pull requests that update a dependency file eip evm performance is good labels Jul 7, 2026
@claude

claude Bot commented Jul 7, 2026

Copy link
Copy Markdown
Contributor

Claude finished @Marchhill's task in 6m 39s —— View job


Review: Batch EIP-2537 pairing check Miller loops and use affine MSM

  • Gather context (diff, Result type, concurrency flags, MSM precedent, zkevm variant, package/lock)
  • Analyze correctness / consensus semantics
  • Analyze concurrency & thread-safety
  • Analyze performance & repo-rule adherence
  • Post findings

I reviewed the full diff against origin/master. This is a well-executed, consensus-careful optimization. No Critical/High/Medium code defects found — the batched pairing check and affine MSM preserve exact EIP-2537 semantics. Findings below are Low/informational plus the acknowledged draft blocker.


Correctness — verified equivalent to the serial implementation

  • Infinity detection now uses the raw all-zero check (ContainsAnyExcept((byte)0)) instead of decoded IsInf(). For valid inputs these are equivalent: the point at infinity has exactly one canonical encoding (all zeros), and a non-infinity point always has a nonzero coordinate (e.g. (0, 2) on G1 still has nonzero y). Padding-only nonzero bytes flip the point into the "real" branch where ValidRawFp then rejects it — same reject as before.
  • Validation of infinity-paired points is preserved. A pair excluded from the Miller loop (dest == -1) still decodes both points to scratch and runs on-curve + subgroup checks. This is the crucial difference from the MSM path (where dest == -1 can only arise from an all-zero, always-valid point and is skipped). The new Subgroup_invalid_point_in_infinity_pair_is_rejected cases cover exactly this — good catch to test it.
  • Argument order preserved: acc.MillerLoopN(g2Points, g1Points, npairs) matches the old p.MillerLoop(y /*G2*/, x /*G1*/).
  • Math equivalent: both old and new do product-of-Miller-loops followed by a single FinalExp().IsOne(). e(a,b)·e(c,d) = FinalExp(ML(a,b)·ML(c,d)).
  • Buffer layout/sizing correct: npairs real pairs land in contiguous slots 0..npairs-1; buffers sized nItems * Sz ≥ npairs * Sz; MSM DecodeAndCheckSubgroupG1/G2 and buffer allocations consistently switched to *Affine.Sz.
  • Empty input still rejected by ValidateInputLength (non-empty, multiple of 384), so nItems ≥ 1; npairs == 0 only via all-infinity input (tested).

Concurrency / thread-safety — no consensus risk

  • The Parallel.For + shared Result result pattern is identical to the existing G1/G2 MSM decode (not new to this PR). result is only ever written with failures (if (!local)), never back to Success, and the Parallel.For completion is a full barrier — so after the loop result != Success iff at least one pair was invalid. No lost-failure and no spurious-success scenario, hence no accept/reject divergence from the serial path.
  • Result is a readonly record struct { ResultType; string? Error }. Concurrent struct writes are technically non-atomic, but every writer stores ResultType.Failure + a valid non-null Error, so any torn read still yields a failure. The precompile's success/failure is what's consensus-relevant; the specific error string is not.
  • pairDestinations / inputData are read-only during the parallel phase; each iteration writes a disjoint buffer slot or a per-call stackalloc scratch. No aliasing.

Performance / repo rules

  • Zero-init-skipped rented buffers with a clear rationale comment — consistent with the existing MSM comments. [SkipLocalsInit], stackalloc, and affine layout all reduce work; matches .agents/rules/performance.md.
  • Style is consistent with surrounding code (spelled-out types, is-patterns, no LINQ in hot path).

Findings

Low — npairs == 0 relies on unspecified MillerLoopN(0) semantics (Bls12381PairingCheckPrecompile.cs:82-83, inline comment posted)
acc is stackalloc'd under [SkipLocalsInit]; correctness for the all-infinity case depends on MillerLoopN(..., 0) writing the Fp12 identity into uninitialized memory. It's covered by All_infinity_input_returns_one, so it's correct today, but the old code made this explicit via GT.One(...). Consider an explicit npairs == 0 → return one short-circuit (like the MSM npoints == 0 guard) to remove the implicit dependency and skip the call entirely. Not blocking.

Low — TryDecodeRaw duplication. There are now four near-identical TryDecodeRaw bodies (G1/G1Affine, G2/G2Affine) differing only by the point type. Acceptable given the types share no common base/interface, but worth a note against the DRY guidance in .agents/rules/coding-style.md.

Known blocker (already acknowledged in the PR description) — unpublished dependency.
Nethermind.Crypto.Bls 1.1.0-preview.197 (NethermindEth/blst-bindings#9) is not on any feed, and the packages.lock.json contentHash (oJSeU9EOzpLwQUnJt5xh…) is from a locally built package. CI restore will fail until the package is published and the pin + lock hashes are refreshed. The PR is explicitly marked Draft — do not merge yet, so this is expected.


Verdict

The code changes are sound and consensus-safe; I found no defect that should block merge on its own. The only thing preventing merge is the explicitly-acknowledged draft state / unpublished dependency. Once 1.1.0-preview.197 is published and the lock file is regenerated against the real feed, this is good to go (the two Low items are optional).

I could not run the build/tests here (the unpublished package makes dotnet restore fail, matching the PR's own note), so I relied on static analysis plus the PR's stated test results (1073 EIP-2537 vectors + gas tests + full Nethermind.Evm.Test).

Temporary until the final package ships to nuget.org

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
@kamilchodola

Copy link
Copy Markdown
Contributor

Gas-benchmark result: PR #12329 (batched pairing Miller loop + affine MSM)

Config: osaka-repricings-v1.2.0 fixture data, gas-benchmarks main branch, chain=mainnet embedded client config (real Nethermind mainnet ruleset, perf-devnet-3 snapshot), repricings_compute/perf-devnet-3, filter=bls12_g1_msm,bls12_g2_msm,bls12_pairing (all gas sizes, 130 tests).
Master: nethermindeth/nethermind:master-diag · PR: perf/bls-batched-miller-loop
4 master runs vs 4 PR runs (a 5th pair hit a transient runner snapshot-mount flake, excluded rather than retried). Per test: max-trimmed average of the remaining 3 runs. Zero exceptions across all 8 runs.

Pairing check — matches the PR's claimed mechanism almost exactly

num_pairs avg Δ (across 10 gas sizes)
1 +0.6% (noise — nothing to batch with a single pair)
3 −21.6%
6 −30.4%
12 −36.0%
24 −37.6%

Exactly the expected shape for "share the Fp12 squarings across pairs": zero speedup at 1 pair (nothing to share), then a smooth, monotonic increase as pair count grows. The PR description's stated range (1.14×–1.45× ≈ −12% to −31%, for 2–16 pairs) undersells it a bit at the higher end (tested up to 24 pairs here), but direction and shape are a clean match. Zero individual test regressed by more than 5% across the full 130-test set.

G1MSM / G2MSM — real but smaller, and non-monotonic

k (points) G1MSM Δ G2MSM Δ
1 +1.7% +2.6%
16 −0.6% −0.7%
64 −32.5% −6.0%
128 −7.8% −8.9%

Mostly a modest, real win (~0 to −9%), consistent with "decode directly into affine layout, skip the Jacobian→affine batch conversion" being a smaller, more fixed-cost optimization than pairing's batching. The G1MSM k=64 outlier (−32.5%) is internally solid — reproducible within ~1pp across all 10 gas sizes at that point count — but doesn't fit the surrounding pattern (k=16 and k=128 are far smaller, and G2MSM at the same k=64 only shows −6%). Flagging this as an interesting, real, reproducible data point rather than something explained by the PR's headline optimization — possibly a bucket-size/algorithm-selection threshold in the underlying MSM implementation specific to G1 at that count. Might be worth a look.

Overall

Master 1132ms → PR 976ms average across all 130 tests, −13.8% aggregate, driven mainly by pairing. Unlike contention-focused optimizations (e.g. #12284), this shows up cleanly in a serial, single-threaded benchmark because it's a pure compute-path change — no concurrency dependency to obscure the signal.


🤖 Generated with Claude Code gas-benchmark skill

@github-actions

github-actions Bot commented Jul 8, 2026

Copy link
Copy Markdown
Contributor

EXPB Benchmark Comparison

Run: View workflow run

superblocks

Scenario: nethermind-flat-superblocks-perf-bls-batched-miller-loop-delay0s

Client Processing (SSE)

Metric PR Master (cached) Delta
AVG (ms) 1084.30 1023.93 +5.90%
MEDIAN (ms) 971.8 971.2 +0.06%
P90 (ms) 1477.4 1415.4 +4.38%
P95 (ms) 1893.8 1696.1 +11.66%
P99 (ms) 5478.2 2183.6 +150.88%
MIN (ms) 678.4 647.0 +4.85%
MAX (ms) 5478.2 2183.6 +150.88%
K6 TTFB
Metric PR Master (cached) Delta
AVG (ms) 1321.38 1225.01 +7.87%
MEDIAN (ms) 1117.15 1099.51 +1.60%
P90 (ms) 1844.04 1672.86 +10.23%
P95 (ms) 2334.74 1884.83 +23.87%
P99 (ms) 5824.49 3743.31 +55.60%
MIN (ms) 767.06 740.14 +3.64%
MAX (ms) 5989.38 4551.76 +31.58%

realblocks

Scenario: nethermind-flat-realblocks-perf-bls-batched-miller-loop-delay0s

Client Processing (SSE)

Metric PR Master (cached) Delta
AVG (ms) 25.75 25.72 +0.12%
MEDIAN (ms) 22.1 21.8 +1.38%
P90 (ms) 42.9 42.4 +1.18%
P95 (ms) 48.8 50.4 -3.17%
P99 (ms) 106.3 102.6 +3.61%
MIN (ms) 0.3 0.3 +0.00%
MAX (ms) 205.6 204.7 +0.44%
K6 TTFB
Metric PR Master (cached) Delta
AVG (ms) 29.37 29.10 +0.93%
MEDIAN (ms) 25.50 25.43 +0.28%
P90 (ms) 46.86 46.33 +1.14%
P95 (ms) 53.96 54.10 -0.26%
P99 (ms) 109.03 103.86 +4.98%
MIN (ms) 0.89 0.87 +2.30%
MAX (ms) 210.10 208.36 +0.84%

@kamilchodola

kamilchodola commented Jul 10, 2026

Copy link
Copy Markdown
Contributor

EXPB reproducible benchmarks — PR vs master (fusaka 1k + fusaka full 6.1k + 10k Cancun realblocks, 5×/side + dotTrace)

Master (e8587429bb) was merged into the branch first (635b523a23 — it was 43 commits behind, which confounded an earlier comparison; see archaeology below). Both images therefore share the same master base; the only delta is this PR's changes (EIP-2537 C# + Nethermind.Crypto.Bls 1.0.5 → 1.1.0-preview.199).

Setup: EXPB reproducible-benchmarks workflow (fusaka payload set from feat/expand-expb-workflow), flat layout, cold replay, per-payload SSE client_metric timings (Nethermind-internal processing time), single runner. Three suites, 5×/side each + dotTrace 1×/side each:

  • fusaka 1k: 1,000 real post-Fusaka mainnet blocks 25,490,001–25,491,000 (avg 30.4M gas, max 60M)
  • fusaka full set: amount=10000, capped by the dataset at 6,099 blocks (25,490,001–~25,496,100)
  • realblocks 10k: 10,000 Cancun-era mainnet blocks from ~22,360,000 (amount=10000)

The PR and master 5× iterations interleaved on the runner (alternating PR/master), which makes the A/Bs drift-resistant.

TL;DR

  1. Performance-neutral on all three suites. fusaka 1k: +0.29% AVG (t = 1.1, n.s.); realblocks 10k: −0.02% AVG (t = 0.02); fusaka 6.1k: raw +2.1% AVG is a persist-stall lottery artifact (13 blocks >1s in two tier-boundary windows, the stall landing on alternating sides) — with those excluded the paired per-block delta is +0.68% AVG / +0.20 ms median-block, below the suite's n=5 resolving power.
  2. These suites barely exercise EIP-2537, so they are collateral-regression checks, not a demonstration of the intended win: Cancun-era blocks predate Pectra (precompiles don't exist yet); the first 1,000 fusaka blocks contain zero BLS12-381 frames; the full 6.1k range finally shows real Bls12381PairingCheck/FpToG1 calls — totalling ~10–50 ms out of ~250 s of block processing (~0.02%), far below anything that could move AVG. (Qualitative plus: the PR's new parallel pair-validation path demonstrably executes on real mainnet blocks.) The 1.14×–1.45× pairing-check speedup is only observable in the microbenchmarks in the PR description, or end-to-end via the gas-benchmarks BLS families.
  3. dotTrace profiles are flat on all suites: identical hot-spot rankings, function-level deltas mixed-sign noise.

fusaka realblocks (1k blocks, ms)

run PR master
1 33.91 33.96
2 33.78 33.57
3 33.88 33.81
4 34.00 33.60
5 33.77 33.91
AVG 33.87 ± 0.09 33.77 ± 0.18
metric PR master delta
AVG 33.87 33.77 +0.29%, t = 1.1 (n.s.)
MEDIAN 29.74 29.68 +0.20%, t = 0.4 (n.s.)
P99 119.9 120.9 −0.81%, t = −1.0 (n.s.)

realblocks 10k (Cancun-era, ms)

run PR master
1 25.43 25.72
2 25.26 25.33
3 25.13 24.53
4 25.14 24.70
5 24.46 25.17
AVG 25.08 ± 0.37 25.09 ± 0.48
metric PR master delta
AVG 25.08 25.09 −0.02%, t = −0.02 (n.s.)
MEDIAN 20.30 20.30 ±0.00%
P99 106.2 105.4 +0.74%, t = 0.5 (n.s.)

fusaka full set (6,099 blocks, ms)

run PR master
1 40.54 40.31
2 41.24 40.07
3 41.80 41.24
4 40.58 40.31
5 41.48 39.48
AVG 41.13 ± 0.55 40.28 ± 0.63
metric PR master delta
AVG (raw) 41.13 40.28 +2.10%, t = 2.3 — see below
MEDIAN 30.70 30.54 +0.5%, t = 1.7 (n.s.)
P99 158.4 156.7 +1.1% (n.s.)

The raw AVG gap is dominated by the known flat-layout "Persisting StateId" tier-boundary stalls on longer replay horizons: every run hits 1–6 s outlier blocks (per-run MAX 0.7–6.1 s), and in the paired per-block data the >1 s stalls cluster in two windows (25,492,48x and 25,495,15x–18x) landing on alternating sides per block — e.g. block 25,492,484 stalls on PR (1,106 vs 35 ms) while 25,492,486 stalls on master (44 vs 1,101 ms). Excluding those 13 lottery blocks: PR 39.71 vs master 39.44 (+0.68%), median per-block delta +0.20 ms, delta distribution p25 = −1.1% / p75 = +2.6% — below the resolving power of 5 runs on this noisier horizon, and inconsistent in direction with the other two suites.

All 30 measurement runs clean: full payload count (999 / 6,099 / 9,999 SSE-measured per run), no Nethermind Exception, no Invalid Block, clean shutdown.

dotTrace (1× per image per suite)

Full-run sampling profiles, XML reports diffed with scripts/dottrace-report.sh compare:

  • fusaka: PR 34.90 vs master 34.96 AVG under profiler. Hot spots identical (thread-wait primitives, DbOnTheRocks.Get, GC poll, SecP256k1.RecoverKeyFromCompact, KeccakF1600Avx512F). Largest named-function deltas ±5%, mixed sign — noise. The only frame matching *PairingCheck* on either side is BN254 (alt_bn128); nothing from Nethermind.Crypto.Bls / BLS12-381 appears at all.
  • realblocks 10k: PR 25.26 vs master 25.13 AVG under profiler. Deltas mixed sign with no systematic direction (WaitNative +2.2%, PollGCWorker −5.9%, DbOnTheRocks.Get +3.9% vs GetCStyleWithColumnFamily −2.5%, ZeroMemoryInternal −14%). Zero EIP-2537 frames, as expected for pre-Pectra blocks.
  • fusaka 6.1k: PR 42.70 vs master 41.90 AVG under profiler (both hit the multi-second stall blocks, so this horizon's profile diff is noisier; deltas remain mixed-sign). First real EIP-2537 frames observed: Bls12381PairingCheckPrecompile.Run (master ~10 ms total / PR ~21 ms + 21 ms in its parallel-validation lambda, 1–4 profiler samples each) and Bls12381FpToG1Precompile.Run (~11 ms) — ~0.02% of runtime, statistically invisible in AVG but proof the new code path runs correctly on real mainnet traffic.

Infra note: the PR-side 10k dotTrace CI run is marked "failure" — that is the workflow's blanket Exception gate tripping on an expb payload-server (Python) BrokenPipeError during teardown, after all 10,000 blocks had completed (metrics artifact intact: COUNT=9999, AVG 25.26). Nethermind logs are clean. The XML report was regenerated locally from the uploaded .dtp snapshot with the same Reporter pattern the workflow uses.

Pre-merge measurement archaeology (why the first comparison was discarded)

Before the merge, the PR base was 43 commits behind master and a naive PR-vs-master 5×5 showed a spurious +3.99% AVG / +11.9% P99 "regression" (t = 20.8), reproducible and image-tied per an interleaved A/B/A. A four-arm decomposition on the fusaka suite (merge-base parent / parent+package-bump-only / full PR / current master) attributed all of it to master-side improvements landed after the PR branched (#12368 et al.): PR vs its own parent = +0.16% (t = 0.7, n.s.), package bump alone = neutral, master vs parent = −3.7% AVG / −11% P99. Runs: parent 5× 29130451772, pkgbump 2× 29130456814, pre-merge PR 5× 29128615696, old-master 5× 29128620946, A/B/A 29129880556/29129885744/29129890763. Superseded by the post-merge results above.

Run inventory (post-merge)

purpose run
fusaka PR 5× 29146724360
fusaka master 5× 29146727283
fusaka dotTrace PR / master 29146895129 / 29146897887
realblocks-10k PR 5× 29146900566
realblocks-10k master 5× 29146903134
realblocks-10k dotTrace PR / master 29149070190 / 29149072957
fusaka-6.1k PR 5× 29149803298
fusaka-6.1k master 5× 29149805464
fusaka-6.1k dotTrace PR / master 29149807574 / 29149809898

Helper branches (this workflow version benchmarks the dispatched ref): perf/bls-batched-miller-loop-fusaka-expb = merged PR head + fusaka-workflow commit; test/expb-fusaka-master-baseline = master e8587429bb + fusaka-workflow commit.

Merge note for the author: the packages.lock.json conflict was resolved by regenerating the Runner lock on top of master (dotnet restore --force-evaluate); the resulting diff vs master is exclusively the Nethermind.Crypto.Bls entries.

🤖 Generated with Claude Code

kamilchodola and others added 5 commits July 11, 2026 10:48
# Conflicts:
#	src/Nethermind/Nethermind.Runner/packages.lock.json
Avoids relying on MillerLoopN(0) writing the identity into the
uninitialized stackalloc buffer and skips the final exponentiation

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…safe

Consensus-critical concurrency invariant: the result write races across
Parallel.For workers, but every writer stores a failure and only the
atomically written ResultType gates the outcome, so the post-barrier
result is a failure iff any pair failed to validate.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
- merge latest master into the branch (packages.lock.json conflict resolved
  + regenerated)
- Nethermind.Crypto.Bls 1.1.0-preview.199 (nugettest.org) -> 1.1.0 (nuget.org)
- drop the temporary nugettest.org package-source mapping for Nethermind.Crypto.Bls

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Collapse the multi-line scratch-buffer / pair-compaction / concurrency notes
to concise one-liners.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
@Marchhill
Marchhill marked this pull request as ready for review July 20, 2026 14:25
@claude

claude Bot commented Jul 20, 2026

Copy link
Copy Markdown
Contributor

Claude finished @Marchhill's task in 3m 19s —— View job


Review: Batch EIP-2537 pairing check Miller loops and use affine MSM

  • Gather context (diff, precompiles, MSM, package/lock)
  • Correctness / consensus semantics
  • Concurrency & thread-safety
  • Package/lock & dependency state
  • Post findings

I re-reviewed the full diff against origin/master, focusing on whether anything changed since the earlier review and whether the acknowledged blocker is resolved. No Critical/High/Medium findings. The batched pairing check and affine MSM preserve exact EIP-2537 semantics, and the two items from the prior review are now addressed.


Prior blockers — both resolved ✅

  • Unpublished dependency (was the only merge blocker). The pin is now the production Nethermind.Crypto.Bls 1.1.0 (Directory.Packages.props), the packages.lock.json entries carry a real resolved: 1.1.0 + contentHash, and Bls resolves from nuget.org (* mapping) — the nugettest.org source no longer maps it. All 16 CI checks pass (Build solutions, Nethermind/Ethereum tests, Flat DB, extra variants, stateless, lint, format), so dotnet restore and the full test suites now work against the real feed.
  • npairs == 0 relying on MillerLoopN(0) semantics. Fixed in f309406: the all-infinity case now returns an explicit 1 after validation and skips the final exponentiation, removing the implicit dependency on uninitialized-buffer behavior. The early return correctly stays after the decode/validate loop so infinity-paired invalid points are still rejected.

Correctness — verified equivalent to the serial implementation

  • Infinity detection equivalence holds. The raw all-zero check (ContainsAnyExcept(0)) is equivalent to the old decoded IsInf() for consensus purposes: a canonical infinity encodes all-zero; a non-infinity valid point has a nonzero field byte; a point with nonzero padding only flips into the "real" branch where ValidRawFp rejects it — same reject as before. Accept/reject outcome is identical to master on every input class.
  • Infinity-paired points are still fully validated (dest == -1 decodes both points to per-thread scratch and runs on-curve + subgroup checks) — the crucial difference from the MSM path, and exactly what the new Subgroup_invalid_point_in_infinity_pair_is_rejected cases lock in.
  • Argument order (MillerLoopN(g2Points, g1Points, npairs)) and math (product-of-Miller-loops → single FinalExp().IsOne()) match the serial path. Buffer sizing (nItems * *Affine.Sz), slot compaction (0..npairs-1), and the MSM switch to MultiMultAffine are all internally consistent.

Concurrency / thread-safety — no consensus risk

  • The Parallel.For + shared Result pattern mirrors the existing MSM decode. Workers only ever store a failure (never back to Success); the loop is a full barrier, so post-loop result != Success iff some pair was invalid — no lost-failure / spurious-success divergence. A torn struct write can only combine two Failure values, so a torn read still yields a failure; the specific error string isn't consensus-relevant. Each worker writes a disjoint slot or per-thread stackalloc scratch — no aliasing.

Findings

Low — TryDecodeRaw duplication. Four near-identical bodies (G1/G1Affine, G2/G2Affine) differ only by point type; the affine and non-affine G1 bodies are now byte-for-byte identical (Eip2537.cs:173 vs :202). Acceptable given the Bls point types share no common base/interface, but worth a note against the DRY guidance in .agents/rules/coding-style.md. Not blocking.


Verdict

Sound, consensus-safe, and well-tested (all-infinity → 1; invalid points in infinity slots rejected; all-infinity MSM → infinity; full 1073-vector suite + gas tests + Nethermind.Evm.Test green in CI). The gas-benchmark data confirms the intended win (−21% to −38% on pairing checks for 3–24 pairs, zero regression >5% across 130 tests) with performance-neutral end-to-end replay. The prior merge blocker is gone. Mergeable — the one remaining item is an optional Low DRY note.

The four TryDecodeRaw overloads (G1/G1Affine, G2/G2Affine) had near-identical
bodies. Extract the length/field-element/infinity validation into a shared
ValidateRawPoint helper, leaving thin per-type wrappers for the Zero/Decode/
OnCurve calls (the point types share no common interface). Behaviour unchanged.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
@Marchhill
Marchhill requested a review from kamilchodola July 21, 2026 11:52

@benaadams benaadams left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

(Nit, optional) Four near-identical TryDecodeRaw overloads now exist (G1/G2 × Jacobian/affine); ValidateRawPoint dedupes the bulk and the ref-struct bindings make further dedup awkward, so acceptable as is.

@Marchhill
Marchhill merged commit bcc44ae into master Jul 21, 2026
686 of 688 checks passed
@Marchhill
Marchhill deleted the perf/bls-batched-miller-loop branch July 21, 2026 23:46
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

dependencies Pull requests that update a dependency file eip evm performance is good

Projects

None yet

Development

Successfully merging this pull request may close these issues.

4 participants