Skip to content

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

Draft
Marchhill wants to merge 2 commits into
masterfrom
perf/bls-batched-miller-loop
Draft

Batch EIP-2537 pairing check Miller loops and use affine MSM#12329
Marchhill wants to merge 2 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.

🤖 Generated with Claude Code

…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).

Comment on lines +82 to +83
GT acc = new(stackalloc long[GT.Sz]);
acc.MillerLoopN(g2Points, g1Points, npairs);

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Low / robustness. acc is stackalloc'd under [SkipLocalsInit] (uninitialized), and when npairs == 0 the result is correct only if MillerLoopN(..., 0) writes the Fp12 identity into acc rather than leaving it untouched. This is a new, implicit dependency on the binding's behavior for the empty-product case — the old code made it explicit with GT.One(...).

It is covered by the new All_infinity_input_returns_one regression test, so it's correct today. But since a silent regression here would be consensus-affecting, consider making it explicit and cheaper — mirroring the npoints == 0 short-circuit the MSM precompiles already use:

GT acc = new(stackalloc long[GT.Sz]);
if (npairs == 0)
{
    byte[] one = new byte[32];
    one[31] = 1;
    return one;
}
acc.MillerLoopN(g2Points, g1Points, npairs);

Not blocking — just removes reliance on unspecified MillerLoopN(0) semantics.

Temporary until the final package ships to nuget.org

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
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.

1 participant