perf(ecc): compute the slope directly in affine Add and Double - #873
Draft
4waan wants to merge 6 commits into
Draft
perf(ecc): compute the slope directly in affine Add and Double#8734waan wants to merge 6 commits into
4waan wants to merge 6 commits into
Conversation
Affine Add and Double built a Jacobian point and then called FromJacobian that inverts. since that result is affine the inversion is paid on every call and the Jacobian detour only adds multiplications. Computing the slope directly costs the same single inversion: Add: 4M+2S + 1I+3M+1S -> 1I+2M+1S Double: 1M+5S + 1I+3M+1S -> 1I+2M+2S Double also called FromAffine immediately before DoubleMixed which overwrites all three coordinates that call was dead. Both defer every write to p until a and b have been read for the last time, since callers alias the destination. Double now guards y=0 explicitly. The Jacobian path returned Z=2y=0 there, which FromJacobian mapped to the point at infinity; the affine slope (3x²+a)/2y is undefined. One IsZero covers both the point at infinity, encoded as (0,0), and the points of order 2, which are on E(Fp) for bls12-377 G1, bls24-315 G1 and bw6-761 G1. The ~cost comments counted only the Jacobian half and understated both functions.. they now include the inversion.
bw6-761 G1 is Y²=X³-1, so (1,0) is on the curve and has order 2. Check that Double, and Add of the point to itself, return the point at infinity for a point with Y=0. The property falsifies on the first sample against an implementation that only guards the point at infinity.
jacExtended.add computed A=U2-U1 and B=S2-S1 for the degenerate-case test and then recomputed both as P and R; keep EFD's P/R names for the values that were already there. Jacobian.fromJacExtended used two Muls per coordinate where its unsafeFromJacExtended twin uses Square+Mul. Affine.fromJacExtended took two inversions. ZZ³=ZZZ², so with w=ZZ/ZZZ we have w²=1/ZZ, and one inversion plus 3M+1S is enough.
The doc said the threshold is 80 while the code has branched on 160 for G2 since Consensys-Incorporated#710, on all 7 curves with a G2. Generate the doc text and the branch from the same template variable so they cannot drift apart again.
The property added with the affine Add/Double rewrite draws an arbitrary x and asserts Double((x,0)) is infinity. That pins the y==0 guard, and it falsifies on the first sample against a Double that guards only the point at infinity, but (x,0) is on the curve only when x^3+ax+b=0, so for a random x it essentially never is. The comment claimed otherwise. Say what the property actually pins, and add TestG1Order2 for the curves that have a genuine point of order 2: bls12-377 and bls24-315, where G1 is Y^2=X^3+1 and the point is (-1,0), and bw6-761, where G1 is Y^2=X^3-1 and it is (1,0). Those are the only three of the eleven curves whose G1 has one: for the other a=0 curves -b is not a cube in Fp, and secp256r1 and stark-curve have cofactor 1. The test asserts the point is on the curve, is not infinity and is not in the r-torsion subgroup, then that Double and Add(P,P) return infinity including when the destination aliases the operand, and that (P+G)-G = P.
…ions Now that the slope is computed directly, the Jacobian round trip that Add and Double used to perform is the natural oracle. Keep it in the test file and compare coordinate for coordinate, so a divergence in how the point at infinity is encoded fails as well. The domain covers ordinary pairs drawn from a randomized walk through the subgroup, P+P, P+(-P), an operand at infinity on either side, y=0 (the vertical tangent, which the Jacobian doubling absorbed as Z3=2y=0), points off the curve, the on-curve point of order 2 on the curves that have one, and every way the destination can alias an operand. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
4waan
marked this pull request as draft
August 25, 2026 18:26
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
description
{Point}Affine.Addand{Point}Affine.Doublebuild a Jacobian point and then callFromJacobianthat invertsZ. Affine result means inversion is paid on every call whatever we do. Jacobian coordinates pay off when an inversion can be deferred across a chain of operations & here the chain has length 1, so the round only adds multiplications.Affine.AddFromJacobian)Affine.DoubleFromJacobian)removes 5M + 2S from
Addand 2M + 4S fromDouble, using the affine formulas from EFD. The approach of #509 and #510, which moved these functions to mixed Jacobian arithmetic.. the inversion they left inFromJacobianis the one the slope can now share.The existing
~Cost:comments counted only the Jacobian half of each function and so understated both they now include the inversion.dead call:
Affine.Doublealso calledq.FromAffine(a)immediately beforeq.DoubleMixed(a)overwriting all three coordinatesThese are the affine entry points used by callers (holding a single pair of points), which cannot kill an inversion. go-ethereum's EIP-196 ECADD and EIP-2537 G1ADD/G2ADD precompiles call
Affine.Adddirectly.Aliasing
instead of through a local Jacobian point both rewrites now write to
pdirectly..p.Add(p, q)andp.Double(p)still gonna work. go-ethereum's G1ADD callsp0.Add(p0, p1).Points of order 2
Doubleneeds one guard that the Jacobian version got for free. Wheny = 0the tangent is vertical and[2]a = O; the Jacobian path producedZ = 2y = 0whichFromJacobianmapped to the point at infinity. The affine slope(3x²+a)/2yis undefined there, so the case is now explicit. OneIsZerocovers both the point at infinity, encoded as(0,0), and the points of order 2.(Such points aint in the r torsion subgroup--- but they are on E(𝔽p) for 3 of the supported curves.
x³ + b = 0has a root in 𝔽p for G1 in all bls12-377 , bls24-315 and bw6-761.. Without the guard,Double((1,0))would return(-2,0), which is not on the curve.)Also in this change
Same file, all either free or strictly less work:
jacExtended.addcomputedA = U2-U1andB = S2-S1for the degenerate-case test,then recomputed both as
PandR. The two subtractions are dropped by keepingEFD's
P/Rnames for the values that were already there.Jacobian.fromJacExtendedused twoMuls per coordinate where itsunsafeFromJacExtendedtwin usesSquare+Mul. Both are the same now.Affine.fromJacExtendedtook two inversions. SinceZZ³ = ZZZ², withw = ZZ/ZZZwe have
w² = 1/ZZ, so one inversion plus 3M + 1S does it.IsInSubGroupBatchG2's doc says the threshold is 80 while the code has branched on160 since feat: batch subgroup membership testing #710 introduced it, on all 7 curves with a G2. The template now derives
the doc text and the branch from one value so they cannot drift again.
Type of change
(Perf only and no API change)
Add,Doubleand the helpers return the same values as before on every input, including the point at infinity.Testing:
go test -short ./ecc/...passes: 173 packages, all 11 curves.go test -tags=puregoon bn254, bls12-381, bw6-761 and secp256r1 (thea = -3path through the doubling slope).
Test{Point}AffineOps: doubling a point withY = 0returns the point at infinity, and so does adding it to itself. It falsifies on
the first sample against an implementation that only guards the point at
infinity.
300 random G1 pairs and 200 G2 pairs on bn254, plus bw6-761 G1 including its
point of order 2, comparing
Add,Double, both aliasing patterns, the pointat infinity on either side,
P + (-P)andP + P. Bit-identical throughout.go generate ./...is repeatable.GOOS=linux GOARCH=386andlinux/arm64build.Benchmarking:
mac M2 air, darwin/arm64,
-count=10, benchstat. The library's own benchmarks, which alias the destination (a.Add(&a, &g1GenAff),a.Double(&a)):G2 gains more because an 𝔽p2 mul is 3 𝔽p muls and an 𝔽p2 square is 2--> the removed ops weigh more against the same single inversion. The absolute saving matches the op count.. bls12-381 G1 saves 403ns for 5M + 2S at ~53ns per 𝔽p mul.
As an independent check that the delta survives a real call site, with decoding and encoding around it, go-ethereum's precompile benchmarks with its gnark-crypto pointed at this branch,
-count=6:(5
matter_g1_add_*cases: -7.1% to -8.0%; 5matter_g2_add_*: -13.4% to -14.9%.)Checklist:
internal/generator/ecc/template/point.go.tmpland its test template while the other36 files are
go generate ./...output)golangci-lintdoes not output errors locallyNote
High Risk
Changes the group law for affine Add/Double on every supported curve, which is security-critical ECC arithmetic used by precompiles and callers that alias destinations.
Overview
Affine
Add/Doublenow compute the slope in place instead of mixed Jacobian add/double plusFromJacobian. The inversion was already required for an affine result, so the Jacobian round-trip only added multiplications (~5M+2S off Add, ~2M+4S off Double). Results are written after operands are fully read sop.Add(p, q)andp.Double(p)still work.Doublenow guardsy = 0(vertical tangent → infinity). Jacobian doubling got that for free viaZ = 2y. Without the guard, on-curve order-2 points on some G1s (BLS12-377, BLS24-315, BW6-761) would leave the curve.Also cheaper
fromJacExtended(one inversion for affine;Square+Mulfor Jacobian) andjacExtended.addno longer recomputesP/R.IsInSubGroupBatchG2docs now match the 160-point branch.Tests pin the new formulas against the old Jacobian path (including aliasing, infinity, off-curve,
y=0) and add order-2 coverage where those points exist.Reviewed by Cursor Bugbot for commit 29c19c2. Bugbot is set up for automated code reviews on this repo. Configure here.