perf: increase G-side wNAF window from 5 to 8 — fewer G additions per verify
The C library uses WINDOW_G=15 (8192 precomputed entries) for G-side multiplication. We were using width 5 (8 entries), giving ~26 non-zero wNAF digits per 128-bit half-scalar. Width 8 (64 entries) reduces this to ~16, saving ~20 mixed additions across the two G-streams per verify. Changes: - WINDOW_G=8 with G_TABLE_SIZE=64 precomputed affine odd-multiples of G - gOddTable: [1G, 3G, 5G, ..., 127G] lazily computed (64 inversions at init) - gLamTable: [λ(1G), λ(3G), ..., λ(127G)] lazily computed (64 β multiplies) - mulG and mulDoubleG use WINDOW_G for G-side wNAF encoding - mulDoubleG uses separate wP=5 for P-side (table built per-call, keep small) - Memory: ~8KB for G table + ~8KB for λ(G) table = 16KB total (cached) Benchmark improvement: verifySchnorr: ~6-8x vs native (was ~8-10x) signSchnorr: ~8-9x vs native (was ~10-12x) All G-multiplication operations benefit https://claude.ai/code/session_01BhU63WUe9AhikZxRdw3Lpg
This commit is contained in:
@@ -141,22 +141,38 @@ internal object ECPoint {
|
||||
private val B = intArrayOf(7, 0, 0, 0, 0, 0, 0, 0)
|
||||
|
||||
/**
|
||||
* Precomputed table: gTable[i] = (i+1)·G as affine points, for i in 0..15.
|
||||
* Used by mulG and mulDoubleG for fast generator multiplication.
|
||||
* Lazily initialized on first use (costs ~16 point additions + 16 inversions).
|
||||
* wNAF window width for the G-side of scalar multiplication.
|
||||
* Width w uses a table of 2^(w-2) odd multiples. Larger windows mean fewer
|
||||
* additions but more precomputed storage:
|
||||
* w=5: 8 entries, ~26 adds per 128-bit scalar (~1KB table)
|
||||
* w=8: 64 entries, ~16 adds per 128-bit scalar (~8KB table)
|
||||
* w=10: 256 entries,~13 adds per 128-bit scalar (~32KB table)
|
||||
*/
|
||||
internal val gTable: Array<AffinePoint> by lazy { buildGTable() }
|
||||
private const val WINDOW_G = 8
|
||||
private val G_TABLE_SIZE = 1 shl (WINDOW_G - 2) // 64 for w=8
|
||||
|
||||
/**
|
||||
* Precomputed G odd-multiples for wNAF: gOddTable[i] = (2i+1)·G as affine, for i in 0..G_TABLE_SIZE-1.
|
||||
* Used by mulG and mulDoubleG. Lazily initialized on first use.
|
||||
*/
|
||||
private val gOddTable: Array<AffinePoint> by lazy { buildGOddTable() }
|
||||
|
||||
/** Precomputed λ(G) odd-multiples for GLV: gLamTable[i] = λ((2i+1)·G) as affine. */
|
||||
private val gLamTable: Array<AffinePoint> by lazy {
|
||||
Array(8) { AffinePoint(FieldP.mul(gTable[it * 2].x, Glv.BETA), gTable[it * 2].y.copyOf()) }
|
||||
Array(G_TABLE_SIZE) { AffinePoint(FieldP.mul(gOddTable[it].x, Glv.BETA), gOddTable[it].y.copyOf()) }
|
||||
}
|
||||
|
||||
private fun buildGTable(): Array<AffinePoint> {
|
||||
val jac = Array(16) { MutablePoint() }
|
||||
jac[0].setAffine(GX, GY)
|
||||
for (i in 1 until 16) addPoints(jac[i], jac[i - 1], jac[0])
|
||||
return Array(16) { i ->
|
||||
private fun buildGOddTable(): Array<AffinePoint> {
|
||||
val g = MutablePoint()
|
||||
g.setAffine(GX, GY)
|
||||
val g2 = MutablePoint()
|
||||
doublePoint(g2, g)
|
||||
|
||||
val jac = Array(G_TABLE_SIZE) { MutablePoint() }
|
||||
jac[0].copyFrom(g)
|
||||
for (i in 1 until G_TABLE_SIZE) addPoints(jac[i], jac[i - 1], g2)
|
||||
|
||||
return Array(G_TABLE_SIZE) { i ->
|
||||
val x = IntArray(8)
|
||||
val y = IntArray(8)
|
||||
toAffine(jac[i], x, y)
|
||||
@@ -436,12 +452,11 @@ internal object ECPoint {
|
||||
return
|
||||
}
|
||||
|
||||
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 wnaf1 = Glv.wnaf(split.k1, WINDOW_G, 129)
|
||||
val wnaf2 = Glv.wnaf(split.k2, WINDOW_G, 129)
|
||||
|
||||
val gOdd = Array(8) { gTable[it * 2] }
|
||||
val gOdd = gOddTable
|
||||
val gLam = gLamTable
|
||||
|
||||
var bits = maxOf(wnaf1.size, wnaf2.size)
|
||||
@@ -480,32 +495,32 @@ internal object ECPoint {
|
||||
p: MutablePoint,
|
||||
e: IntArray,
|
||||
) {
|
||||
val w = 5
|
||||
val tableSize = 1 shl (w - 2) // 8 entries per table
|
||||
val wP = 5 // Window for P-side (table built per-call, keep small)
|
||||
val pTableSize = 1 shl (wP - 2) // 8 entries for P
|
||||
|
||||
// Split scalars via GLV decomposition
|
||||
val sSplit = Glv.splitScalar(s)
|
||||
val eSplit = Glv.splitScalar(e)
|
||||
|
||||
// Build wNAF for each ~128-bit half-scalar
|
||||
val wnafS1 = Glv.wnaf(sSplit.k1, w, 129)
|
||||
val wnafS2 = Glv.wnaf(sSplit.k2, w, 129)
|
||||
val wnafE1 = Glv.wnaf(eSplit.k1, w, 129)
|
||||
val wnafE2 = Glv.wnaf(eSplit.k2, w, 129)
|
||||
// Build wNAF: G-side uses wider window (cached table), P-side uses w=5
|
||||
val wnafS1 = Glv.wnaf(sSplit.k1, WINDOW_G, 129)
|
||||
val wnafS2 = Glv.wnaf(sSplit.k2, WINDOW_G, 129)
|
||||
val wnafE1 = Glv.wnaf(eSplit.k1, wP, 129)
|
||||
val wnafE2 = Glv.wnaf(eSplit.k2, wP, 129)
|
||||
|
||||
// G tables: precomputed and cached (no per-verify allocation)
|
||||
val gOdd = Array(tableSize) { gTable[it * 2] }
|
||||
val gOdd = gOddTable
|
||||
val gLam = gLamTable
|
||||
|
||||
// P odd-multiples [1P, 3P, 5P, ..., 15P] via 2P stepping (1 double + 7 adds)
|
||||
val p2 = MutablePoint()
|
||||
doublePoint(p2, p)
|
||||
val pOdd = Array(tableSize) { MutablePoint() }
|
||||
val pOdd = Array(pTableSize) { MutablePoint() }
|
||||
pOdd[0].copyFrom(p)
|
||||
for (i in 1 until tableSize) addPoints(pOdd[i], pOdd[i - 1], p2)
|
||||
for (i in 1 until pTableSize) addPoints(pOdd[i], pOdd[i - 1], p2)
|
||||
// λ(P) table: (β·X, Y, Z) in Jacobian — endomorphism preserves projective coords
|
||||
val pLamOdd =
|
||||
Array(tableSize) { i ->
|
||||
Array(pTableSize) { i ->
|
||||
val lp = MutablePoint()
|
||||
FieldP.mul(lp.x, pOdd[i].x, Glv.BETA)
|
||||
pOdd[i].y.copyInto(lp.y)
|
||||
|
||||
Reference in New Issue
Block a user