mirror of
https://github.com/vitorpamplona/amethyst.git
synced 2026-08-09 16:14:40 +00:00
perf: apply GLV+wNAF to mul and mulG — all scalar muls now half-speed
Previously only mulDoubleG (verification) used GLV endomorphism + wNAF-5. The generic mul() and mulG() still used the old 4-bit window with 256 doublings and full Jacobian addition. Now all three scalar multiplication functions use GLV to split scalars into ~128-bit halves. mul() (arbitrary point, used by pubKeyTweakMul/ECDH): - Was: 4-bit window, 256 doublings, 15-entry Jacobian table, ~60 full adds - Now: GLV split + wNAF-5, ~130 doublings, 2×8-entry Jacobian tables mulG() (generator point, used by pubkeyCreate and signSchnorr): - Was: 4-bit window, 256 doublings, precomputed affine table, ~60 mixed adds - Now: GLV split + wNAF-5, ~130 doublings, precomputed affine G + λ(G) tables Benchmark improvements: pubkeyCreate: 4,391 → 6,689 ops/s (52% faster, 12.5x → 7.7x) pubKeyTweakMul: 3,427 → 5,233 ops/s (53% faster, 8.6x → 6.5x) pubKeyTweakMulCompact:2,941 → 4,593 ops/s (56% faster, 8.7x → 5.6x) signSchnorr: 1,178 → 1,516 ops/s (29% faster, 23.1x → 17.3x) compressedPubKeyFor: 4,358 → 6,919 ops/s (59% faster, 12.3x → 7.1x) https://claude.ai/code/session_01BhU63WUe9AhikZxRdw3Lpg
This commit is contained in:
@@ -358,13 +358,12 @@ internal object ECPoint {
|
||||
/**
|
||||
* General scalar multiplication: out = scalar · p.
|
||||
*
|
||||
* Uses a 4-bit windowed method: precomputes [1P, 2P, ..., 16P], then processes
|
||||
* the scalar 4 bits (one nibble) at a time from MSB to LSB:
|
||||
* for each nibble: double 4 times, then add table[nibble] if non-zero.
|
||||
* Uses GLV endomorphism + wNAF-5 to halve doublings from 256 to ~130:
|
||||
* scalar = k₁ + k₂·λ (mod n), where k₁, k₂ are ~128 bits
|
||||
* scalar·P = k₁·P + k₂·λ(P), with λ(P) = (β·X, Y, Z)
|
||||
*
|
||||
* This requires 64 iterations with 4 doublings each (256 total) and ~60 additions
|
||||
* (on average 15/16 of nibbles are non-zero). All operations use full Jacobian
|
||||
* arithmetic since the P table is built on-the-fly without inversions.
|
||||
* The two ~128-bit half-scalars are wNAF-5 encoded and processed in a single
|
||||
* pass of ~130 shared doublings. P-side uses Jacobian tables (no inversions).
|
||||
*/
|
||||
fun mul(
|
||||
out: MutablePoint,
|
||||
@@ -376,30 +375,57 @@ internal object ECPoint {
|
||||
return
|
||||
}
|
||||
|
||||
val table = Array(16) { MutablePoint() }
|
||||
table[0].copyFrom(p)
|
||||
for (i in 1 until 16) addPoints(table[i], table[i - 1], p)
|
||||
val w = 5
|
||||
val tableSize = 1 shl (w - 2) // 8 entries
|
||||
|
||||
// Split scalar via GLV: scalar = k₁ + k₂·λ
|
||||
val split = Glv.splitScalar(scalar)
|
||||
val wnaf1 = Glv.wnaf(split.k1, w, 129)
|
||||
val wnaf2 = Glv.wnaf(split.k2, w, 129)
|
||||
|
||||
// P odd-multiples [1P, 3P, 5P, ..., 15P] via 2P stepping
|
||||
val p2 = MutablePoint()
|
||||
doublePoint(p2, p)
|
||||
val pOdd = Array(tableSize) { MutablePoint() }
|
||||
pOdd[0].copyFrom(p)
|
||||
for (i in 1 until tableSize) addPoints(pOdd[i], pOdd[i - 1], p2)
|
||||
|
||||
// λ(P) odd-multiples: (β·X, Y, Z) in Jacobian
|
||||
val pLamOdd =
|
||||
Array(tableSize) { i ->
|
||||
val lp = MutablePoint()
|
||||
FieldP.mul(lp.x, pOdd[i].x, Glv.BETA)
|
||||
pOdd[i].y.copyInto(lp.y)
|
||||
pOdd[i].z.copyInto(lp.z)
|
||||
lp
|
||||
}
|
||||
|
||||
// Find highest non-zero digit
|
||||
var bits = maxOf(wnaf1.size, wnaf2.size)
|
||||
while (bits > 0) {
|
||||
val b = bits - 1
|
||||
if ((b < wnaf1.size && wnaf1[b] != 0) || (b < wnaf2.size && wnaf2[b] != 0)) break
|
||||
bits--
|
||||
}
|
||||
|
||||
out.setInfinity()
|
||||
val tmp = MutablePoint()
|
||||
for (nibbleIdx in 63 downTo 0) {
|
||||
val negJac = MutablePoint()
|
||||
|
||||
for (i in bits - 1 downTo 0) {
|
||||
doublePoint(out, out)
|
||||
doublePoint(out, out)
|
||||
doublePoint(out, out)
|
||||
doublePoint(out, out)
|
||||
val nib = U256.getNibble(scalar, nibbleIdx)
|
||||
if (nib != 0) {
|
||||
addPoints(tmp, out, table[nib - 1])
|
||||
out.copyFrom(tmp)
|
||||
}
|
||||
addWnafJacobian(out, tmp, negJac, wnaf1, i, pOdd, split.negK1)
|
||||
addWnafJacobian(out, tmp, negJac, wnaf2, i, pLamOdd, split.negK2)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Generator multiplication: out = scalar · G.
|
||||
*
|
||||
* Same 4-bit windowed algorithm as [mul], but uses the precomputed affine G table
|
||||
* for faster mixed addition (8M+3S instead of 11M+5S per addition).
|
||||
* Uses GLV endomorphism + wNAF-5 with precomputed affine G and λ(G) tables:
|
||||
* scalar = k₁ + k₂·λ, then scalar·G = k₁·G + k₂·λ(G)
|
||||
* Both tables are cached (lazy static), so no per-call table building.
|
||||
* Mixed Jacobian+Affine addition (8M+3S) for all lookups.
|
||||
*/
|
||||
fun mulG(
|
||||
out: MutablePoint,
|
||||
@@ -409,19 +435,30 @@ internal object ECPoint {
|
||||
out.setInfinity()
|
||||
return
|
||||
}
|
||||
val table = gTable
|
||||
|
||||
val w = 5
|
||||
val split = Glv.splitScalar(scalar)
|
||||
val wnaf1 = Glv.wnaf(split.k1, w, 129)
|
||||
val wnaf2 = Glv.wnaf(split.k2, w, 129)
|
||||
|
||||
val gOdd = Array(8) { gTable[it * 2] }
|
||||
val gLam = gLamTable
|
||||
|
||||
var bits = maxOf(wnaf1.size, wnaf2.size)
|
||||
while (bits > 0) {
|
||||
val b = bits - 1
|
||||
if ((b < wnaf1.size && wnaf1[b] != 0) || (b < wnaf2.size && wnaf2[b] != 0)) break
|
||||
bits--
|
||||
}
|
||||
|
||||
out.setInfinity()
|
||||
val tmp = MutablePoint()
|
||||
for (nibbleIdx in 63 downTo 0) {
|
||||
val negY = IntArray(8)
|
||||
|
||||
for (i in bits - 1 downTo 0) {
|
||||
doublePoint(out, out)
|
||||
doublePoint(out, out)
|
||||
doublePoint(out, out)
|
||||
doublePoint(out, out)
|
||||
val nib = U256.getNibble(scalar, nibbleIdx)
|
||||
if (nib != 0) {
|
||||
addMixed(tmp, out, table[nib - 1].x, table[nib - 1].y)
|
||||
out.copyFrom(tmp)
|
||||
}
|
||||
addWnafMixed(out, tmp, negY, wnaf1, i, gOdd, split.negK1)
|
||||
addWnafMixed(out, tmp, negY, wnaf2, i, gLam, split.negK2)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user