docs: update Secp256k1.kt with instruction-level C comparison

Add precise per-operation cost analysis from comparing our Kotlin
implementation with bitcoin-core/secp256k1's C source:

- doublePoint: 1,516 insns (Kotlin) vs 530 insns (C) = 2.9×
  - mul/sqr accounts for 76% of gap (UMULH+MUL+carry vs single MUL)
  - add/neg/half accounts for 24% (reduceSelf vs lazy magnitude tracking)
- Update performance numbers to Java 21 results (verify 3.4×, sign 1.1×)
- Document all optimizations implemented during this session
- Note that lazy reduction penalty (4.2× on cheap ops) is the main
  remaining algorithmic opportunity, but requires 5×52-bit limb change

https://claude.ai/code/session_01BhU63WUe9AhikZxRdw3Lpg
This commit is contained in:
Claude
2026-04-06 19:58:51 +00:00
parent f9ec821107
commit 2bf911379f
@@ -36,37 +36,48 @@ import com.vitorpamplona.quartz.utils.sha256.sha256
* - [privKeyTweakAdd]: BIP-32 key derivation (NIP-06)
* - [pubKeyTweakMul] / [ecdhXOnly]: ECDH shared secrets (NIP-04, NIP-44)
*
* Performance on JVM (vs native C/JNI secp256k1, well-warmed):
* verify ~5,600 ops/s (4.4× native) — Strauss + GLV + wNAF-12
* sign ~16,700 ops/s (1.5× native) — comb method (cached pubkey)
* pubCreate ~23,400 ops/s (2.2× native) — comb method, 3 doublings
* ECDH ~7,100 ops/s (3.9× native) — GLV + wNAF-5, effective-affine
* compress ~4M ops/s (2× FASTER) — pure Kotlin, no JNI overhead
* secKeyVerify ~5.7M ops/s (FASTER) — scalar range check, no JNI
* Performance on Java 21 (vs native C/JNI secp256k1, well-warmed):
* verify ~8,000 ops/s (3.4× native) — Strauss + GLV + wNAF-12
* sign ~26,000 ops/s (1.1× native) — comb method (cached pubkey)
* pubCreate ~36,000 ops/s (1.6× native) — comb method, 3 doublings
* ECDH ~11,000 ops/s (2.8× native) — GLV + wNAF-5, effective-affine
* compress ~7M ops/s (1.7× FASTER) — pure Kotlin, no JNI overhead
* secKeyVerify ~8M ops/s (1.2× FASTER) — scalar range check, no JNI
*
* Architecture:
* Field arithmetic uses 4×64-bit limbs (LongArray(4)) with Math.multiplyHigh
* for 64×64→128-bit products (16 products per field multiply). On JVM 9+,
* multiplyHigh maps to a single hardware instruction (IMULH/SMULH). The main
* gap vs native C is per-field-op cost: unsignedMultiplyHigh requires 5 JVM
* instructions per product vs C's single MULQ with native uint128.
* Field arithmetic uses 4×64-bit limbs (LongArray(4)) with Math.unsignedMultiplyHigh
* (Java 18+, single UMULH instruction) for 64×64→128-bit products. 16 products per
* field multiply vs C's 25 (5×52-bit limbs), but each C product is a single native
* 128-bit MUL instruction vs our UMULH + MUL + carry propagation (~7 insns total).
*
* Per-doublePoint cost analysis (instruction-level, vs C libsecp256k1):
* mul/sqr (7 ops): Kotlin ~1,204 insns vs C ~455 insns (2.6× — UMULH overhead)
* add/neg/half: Kotlin ~312 insns vs C ~75 insns (4.2× — no lazy reduction)
* Total: Kotlin ~1,516 insns vs C ~530 insns (2.9× — matches benchmarks)
*
* Optimizations implemented (matching or adapted from libsecp256k1):
* - Math.unsignedMultiplyHigh (Java 18+): eliminates 4-insn signed→unsigned correction
* - GLV endomorphism: splits 256-bit scalars into 2×128-bit halves
* - wNAF encoding: windowed non-adjacent form for sparse addition patterns
* - Comb method: generator multiplication with only 3 doublings (Hamburg 2012)
* - Strauss/Shamir: interleaved multi-scalar multiplication for verification
* - Effective-affine: batch-inverts wNAF tables to affine for cheaper mixed adds
* - Effective-affine: batch-inverts wNAF tables for cheaper mixed adds (saves ~4M/add)
* - Shared Z inversion: GLV table pairs share Z coords, one inversion for both
* - Batch inversion: Montgomery's trick (1 inv + 3(n-1) muls for n inversions)
* - ThreadLocal elimination: pre-fetched scratch buffers avoid ~500 lookups/op
* - Pre-allocated scratch: ThreadLocal PointScratch eliminates ~130 allocs/operation
* - Dedicated squaring: 10 products vs 16 for general multiplication
* - secp256k1-specific reduceSelf: single branch on a[3]==-1 (>99.99% fast path)
*
* Differences from C libsecp256k1 (due to JVM constraints):
* - No lazy reduction (4×64 limbs have no headroom; C uses 5×52 with 12-bit spare)
* - Fermat inversion (255 sqr + 15 mul) instead of safegcd (safegcd is slower on
* JVM due to 128-bit arithmetic overhead in the inner divstep matrix multiply)
* - WINDOW_G=12 instead of 15 (JVM heap-allocated tables cause cache pressure
* at larger sizes; C uses contiguous compile-time .rodata arrays)
* - No lazy reduction (4×64 limbs have no headroom; C's 5×52 limbs have 12-bit spare
* capacity per limb, allowing 3-8 chained add/sub without normalizing — this accounts
* for 24% of the remaining per-operation gap)
* - Fermat inversion (255 sqr + 15 mul) instead of safegcd (safegcd is slower on JVM
* due to 128-bit arithmetic overhead in the inner divstep matrix multiply)
* - WINDOW_G=12 instead of 15 (JVM heap-allocated tables cause cache pressure at
* larger sizes; C uses contiguous compile-time .rodata arrays)
* - No constant-time guarantees (not needed for Nostr — secrets are nonces, not
* long-term keys exposed to timing side-channels)
*/
object Secp256k1 {
// ==================== Cached BIP-340 tag hash prefixes ====================