perf: implement comb method for G multiplication — 2x faster pubkeyCreate/sign
The comb method (Hamburg 2012) replaces GLV+wNAF for generator multiplication. Instead of ~130 doublings + ~32 additions, it arranges the 256 scalar bits into a 4×66 matrix and processes each of 4 rows with 11 table lookups. Only 3 doublings are needed between rows. Algorithm: COMB_BLOCKS=11, COMB_TEETH=6, COMB_SPACING=4 - 11 blocks × 64 entries = 704 affine points (~45KB), lazily precomputed - Per mulG: 3 doublings + ~43 mixed additions ≈ 464 M-equiv - Previous GLV+wNAF: ~130 doublings + ~32 additions ≈ 1,035 M-equiv - Theoretical speedup: 2.2× (measured: 2.0-2.1×) Table construction uses an efficient Gray-code-like ordering: each successive mask differs by one bit, so each entry is built from the previous with one point addition instead of summing all teeth from scratch. Benchmark improvements: pubkeyCreate: 8,383 → 17,654 ops/s (2.1×, now 3.6× vs native) signSchnorr (cached): 7,411 → 13,642 ops/s (1.8×, now 2.3× vs native) signSchnorr: 3,257 → 5,856 ops/s (1.8×, now 4.8× vs native) compressedPubKeyFor: 8,475 → 16,695 ops/s (2.0×, now 3.1× vs native) https://claude.ai/code/session_01BhU63WUe9AhikZxRdw3Lpg
This commit is contained in:
@@ -180,6 +180,71 @@ internal object ECPoint {
|
||||
}
|
||||
}
|
||||
|
||||
// ==================== Comb Method for G Multiplication ====================
|
||||
//
|
||||
// The comb method (Hamburg 2012) is much faster than windowed scalar multiplication
|
||||
// for a fixed base point because it avoids most doublings. Instead of processing the
|
||||
// scalar bit-by-bit with doublings between each, it arranges the scalar bits into a
|
||||
// 2D matrix (SPACING rows × BLOCKS*TEETH columns) and processes each row with table
|
||||
// lookups. Only SPACING-1 doublings are needed between rows.
|
||||
//
|
||||
// With BLOCKS=11, TEETH=6, SPACING=4:
|
||||
// - Doublings: 3 (vs ~130 for GLV+wNAF)
|
||||
// - Additions: ~43 mixed (11 blocks × ~98% non-zero × 4 rows)
|
||||
// - Total: ~43 mixed additions + 3 doublings ≈ 464 M-equiv
|
||||
// - vs GLV+wNAF: ~130 doublings + ~32 additions ≈ 1,035 M-equiv (2.2× faster)
|
||||
//
|
||||
// The table has 11 blocks × 64 entries = 704 affine points (~45KB), lazily computed.
|
||||
|
||||
private const val COMB_BLOCKS = 11
|
||||
private const val COMB_TEETH = 6
|
||||
private const val COMB_SPACING = 4
|
||||
private const val COMB_POINTS = 1 shl COMB_TEETH // 64
|
||||
|
||||
private val combTable: Array<AffinePoint> by lazy { buildCombTable() }
|
||||
|
||||
private fun buildCombTable(): Array<AffinePoint> {
|
||||
// Tooth base points: toothG[i] = 2^(i * SPACING) * G
|
||||
val numTeeth = COMB_BLOCKS * COMB_TEETH
|
||||
val toothG = Array(numTeeth) { MutablePoint() }
|
||||
toothG[0].setAffine(GX, GY)
|
||||
for (i in 1 until numTeeth) {
|
||||
toothG[i].copyFrom(toothG[i - 1])
|
||||
repeat(COMB_SPACING) { doublePoint(toothG[i], toothG[i]) }
|
||||
}
|
||||
|
||||
// For each block, build all 2^TEETH combinations of its teeth
|
||||
val tableSize = COMB_BLOCKS * COMB_POINTS
|
||||
val jac = Array(tableSize) { MutablePoint() }
|
||||
val tmp = MutablePoint()
|
||||
|
||||
for (b in 0 until COMB_BLOCKS) {
|
||||
val base = b * COMB_POINTS
|
||||
jac[base].setInfinity()
|
||||
for (m in 1 until COMB_POINTS) {
|
||||
val changedBit = Integer.numberOfTrailingZeros(m)
|
||||
if (m and (m - 1) == 0) {
|
||||
jac[base + m].copyFrom(toothG[b * COMB_TEETH + changedBit])
|
||||
} else {
|
||||
val prev = m xor (1 shl changedBit)
|
||||
addPoints(tmp, jac[base + prev], toothG[b * COMB_TEETH + changedBit])
|
||||
jac[base + m].copyFrom(tmp)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return Array(tableSize) { i ->
|
||||
if (jac[i].isInfinity()) {
|
||||
AffinePoint(IntArray(8), IntArray(8))
|
||||
} else {
|
||||
val x = IntArray(8)
|
||||
val y = IntArray(8)
|
||||
toAffine(jac[i], x, y)
|
||||
AffinePoint(x, y)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// ==================== Thread-local scratch buffers ====================
|
||||
|
||||
/**
|
||||
@@ -436,12 +501,14 @@ internal object ECPoint {
|
||||
}
|
||||
|
||||
/**
|
||||
* Generator multiplication: out = scalar · G.
|
||||
* Generator multiplication using the comb method: out = scalar · G.
|
||||
*
|
||||
* 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.
|
||||
* Arranges the 256 scalar bits into a 2D matrix (4 rows × 66 columns) and
|
||||
* processes each row with 11 table lookups (one per block of 6 bits).
|
||||
* Only 3 doublings are needed between rows, vs ~130 for GLV+wNAF.
|
||||
*
|
||||
* Total: ~43 mixed additions + 3 doublings ≈ 464 M-equiv
|
||||
* (vs ~1,035 for the previous GLV+wNAF approach — 2.2× faster)
|
||||
*/
|
||||
fun mulG(
|
||||
out: MutablePoint,
|
||||
@@ -452,28 +519,28 @@ internal object ECPoint {
|
||||
return
|
||||
}
|
||||
|
||||
val split = Glv.splitScalar(scalar)
|
||||
val wnaf1 = Glv.wnaf(split.k1, WINDOW_G, 129)
|
||||
val wnaf2 = Glv.wnaf(split.k2, WINDOW_G, 129)
|
||||
|
||||
val gOdd = gOddTable
|
||||
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--
|
||||
}
|
||||
|
||||
val table = combTable
|
||||
out.setInfinity()
|
||||
val tmp = MutablePoint()
|
||||
val negY = IntArray(8)
|
||||
|
||||
for (i in bits - 1 downTo 0) {
|
||||
doublePoint(out, out)
|
||||
addWnafMixed(out, tmp, negY, wnaf1, i, gOdd, split.negK1)
|
||||
addWnafMixed(out, tmp, negY, wnaf2, i, gLam, split.negK2)
|
||||
for (combOff in COMB_SPACING - 1 downTo 0) {
|
||||
if (combOff < COMB_SPACING - 1) {
|
||||
doublePoint(out, out)
|
||||
}
|
||||
for (block in 0 until COMB_BLOCKS) {
|
||||
var mask = 0
|
||||
for (tooth in 0 until COMB_TEETH) {
|
||||
val bitPos = (block * COMB_TEETH + tooth) * COMB_SPACING + combOff
|
||||
if (bitPos < 256 && U256.testBit(scalar, bitPos)) {
|
||||
mask = mask or (1 shl tooth)
|
||||
}
|
||||
}
|
||||
if (mask != 0) {
|
||||
val entry = table[block * COMB_POINTS + mask]
|
||||
addMixed(tmp, out, entry.x, entry.y)
|
||||
out.copyFrom(tmp)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user