feat: migrate secp256k1 from LongArray to Fe4/Wide8 structs

Full migration of the secp256k1 library from LongArray(4)/LongArray(8)
to Fe4/Wide8 struct types with @JvmField named Long fields. This
eliminates all array bounds checks from the hot path.

Files migrated (13 source + 7 test + 2 benchmark):
  - U256.kt, FieldP.kt, ScalarN.kt, Glv.kt, ECPoint.kt
  - FieldMulPlatform.kt (expect + 3 actuals), FieldMulFused.kt
  - PointTypes.kt (MutablePoint, AffinePoint, PointScratch)
  - KeyCodec.kt, Secp256k1.kt
  - All test files and benchmarks

Bytecode impact:
  Before: 464 laload/lastore (bounds-checked) in core arithmetic
  After:  0 laload/lastore, all getfield/putfield (no checks)

The public API (Secp256k1 object) is unchanged - it still accepts
and returns ByteArray. Fe4 conversion happens at the API boundary
via U256.fromBytes()/U256.toBytes().

All secp256k1 unit tests pass on JVM.

https://claude.ai/code/session_01Sxi6Gpxbstuj3Y8TBY7XrU
This commit is contained in:
Claude
2026-04-10 03:30:53 +00:00
parent ea8b693785
commit a2ba64baba
23 changed files with 1391 additions and 1925 deletions
@@ -43,18 +43,18 @@ package com.vitorpamplona.quartz.utils.secp256k1
* - The fused function stays within ART's inlining budget
*/
internal actual fun fieldMulReduce(
out: LongArray,
a: LongArray,
b: LongArray,
w: LongArray,
out: Fe4,
a: Fe4,
b: Fe4,
w: Wide8,
) {
fieldMulReduceWith(out, a, b, w) { x, y -> unsignedMultiplyHighFallback(x, y) }
}
internal actual fun fieldSqrReduce(
out: LongArray,
a: LongArray,
w: LongArray,
out: Fe4,
a: Fe4,
w: Wide8,
) {
fieldSqrReduceWith(out, a, w) { x, y -> unsignedMultiplyHighFallback(x, y) }
}
@@ -51,14 +51,14 @@ internal object ECPoint {
// ==================== Generator point G ====================
val GX =
longArrayOf(
Fe4(
6481385041966929816L,
188021827762530521L,
6170039885052185351L,
8772561819708210092L,
)
val GY =
longArrayOf(
Fe4(
-7185545363635252040L,
-209500633525038055L,
6747795201694173352L,
@@ -66,7 +66,7 @@ internal object ECPoint {
)
/** Curve constant b = 7 in y² = x³ + 7. */
private val B = longArrayOf(7L, 0L, 0L, 0L)
private val B = Fe4(7L, 0L, 0L, 0L)
// Thread-local scratch — declared before precomputed tables because
// buildGOddTable() and buildCombTable() use doublePoint/addPoints which
@@ -119,7 +119,7 @@ internal object ECPoint {
private const val P_TABLE_CACHE_MASK = P_TABLE_CACHE_SIZE - 1
private class CachedPTable(
val px: LongArray, // x-coordinate of the point (cache key, 4 limbs)
val px: Fe4, // x-coordinate of the point (cache key, 4 limbs)
val pOdd: Array<AffinePoint>, // 8 affine odd-multiples of P
val pLamOdd: Array<AffinePoint>, // 8 affine odd-multiples of λ(P)
)
@@ -127,7 +127,7 @@ internal object ECPoint {
private val pTableCache = arrayOfNulls<CachedPTable>(P_TABLE_CACHE_SIZE)
/** Hash a field element to a cache slot index. */
private fun cacheSlot(px: LongArray): Int = (px[0].toInt() xor px[1].toInt().shl(3)) and P_TABLE_CACHE_MASK
private fun cacheSlot(px: Fe4): Int = (px.l0.toInt() xor px.l1.toInt().shl(3)) and P_TABLE_CACHE_MASK
private fun buildGOddTable(): Array<AffinePoint> {
val g = MutablePoint()
@@ -153,21 +153,21 @@ internal object ECPoint {
// Step 1: compute prefix products of Z coordinates
// prods[i] = z[0] * z[1] * ... * z[i]
val prods = Array(n) { LongArray(4) }
jac[0].z.copyInto(prods[0], 0, 0, 4)
val prods = Array(n) { Fe4() }
prods[0].copyFrom(jac[0].z)
for (i in 1 until n) {
FieldP.mul(prods[i], prods[i - 1], jac[i].z)
}
// Step 2: invert the total product
val inv = LongArray(4)
val inv = Fe4()
FieldP.inv(inv, prods[n - 1])
// Step 3: recover individual inverses by multiplying back
// zInv[i] = product_inv * prods[i-1] = 1 / z[i]
val zInv = LongArray(4)
val zInv2 = LongArray(4)
val zInv3 = LongArray(4)
val zInv = Fe4()
val zInv2 = Fe4()
val zInv3 = Fe4()
val result = Array(n) { AffinePoint() }
for (i in n - 1 downTo 1) {
@@ -245,10 +245,10 @@ internal object ECPoint {
return Array(tableSize) { i ->
if (jac[i].isInfinity()) {
AffinePoint(LongArray(4), LongArray(4))
AffinePoint(Fe4(), Fe4())
} else {
val x = LongArray(4)
val y = LongArray(4)
val x = Fe4()
val y = Fe4()
toAffine(jac[i], x, y)
AffinePoint(x, y)
}
@@ -319,16 +319,16 @@ internal object ECPoint {
fun addMixed(
out: MutablePoint,
p: MutablePoint,
qx: LongArray,
qy: LongArray,
qx: Fe4,
qy: Fe4,
) = addMixed(out, p, qx, qy, scratch.get())
/** addMixed with caller-provided scratch (hot path — no ThreadLocal lookup). */
fun addMixed(
out: MutablePoint,
p: MutablePoint,
qx: LongArray,
qy: LongArray,
qx: Fe4,
qy: Fe4,
s: PointScratch,
) {
if (p.isInfinity()) {
@@ -344,9 +344,9 @@ internal object ECPoint {
FieldP.mul(t[3], qy, t[1], w) // S₂ = qy·Z₁³ (S₁ = Y₁ since Z₂=1)
FieldP.sub(t[4], t[2], p.x) // H = U₂ - U₁
if (U256.isZero(t[4])) {
if (t[4].isZero()) {
FieldP.sub(t[5], t[3], p.y)
if (U256.isZero(t[5])) doublePoint(out, p, s) else out.setInfinity()
if (t[5].isZero()) doublePoint(out, p, s) else out.setInfinity()
return
}
@@ -454,10 +454,10 @@ internal object ECPoint {
fun mul(
out: MutablePoint,
p: MutablePoint,
scalar: LongArray,
scalar: Fe4,
s: PointScratch = scratch.get(),
) {
if (U256.isZero(scalar) || p.isInfinity()) {
if (scalar.isZero() || p.isInfinity()) {
out.setInfinity()
return
}
@@ -472,7 +472,7 @@ internal object ECPoint {
val wnaf2 = s.wnaf2
// P odd-multiples [1P, 3P, 5P, ..., 15P] via 2P stepping (Jacobian)
// Uses pre-allocated tables from PointScratch to avoid ~80 LongArray allocs
// Uses pre-allocated tables from PointScratch to avoid ~80 Fe4 allocs
doublePoint(s.p2, p, s)
val pOddJac = s.pOddJac
pOddJac[0].copyFrom(p)
@@ -482,8 +482,8 @@ internal object ECPoint {
val pLamOddJac = s.pLamOddJac
for (i in 0 until tableSize) {
FieldP.mul(pLamOddJac[i].x, pOddJac[i].x, Glv.BETA, s.w)
pOddJac[i].y.copyInto(pLamOddJac[i].y, 0, 0, 4)
pOddJac[i].z.copyInto(pLamOddJac[i].z, 0, 0, 4)
pLamOddJac[i].y.copyFrom(pOddJac[i].y)
pLamOddJac[i].z.copyFrom(pOddJac[i].z)
}
// Effective-affine: batch-convert with shared Z inversion
@@ -536,10 +536,10 @@ internal object ECPoint {
*/
fun mulG(
out: MutablePoint,
scalar: LongArray,
scalar: Fe4,
s: PointScratch = scratch.get(),
) {
if (U256.isZero(scalar)) {
if (scalar.isZero()) {
out.setInfinity()
return
}
@@ -593,9 +593,9 @@ internal object ECPoint {
*/
fun mulDoubleG(
out: MutablePoint,
s: LongArray,
s: Fe4,
p: MutablePoint,
e: LongArray,
e: Fe4,
sc: PointScratch = scratch.get(),
) {
val wP = 5 // Window for P-side (table built per-call, keep small)
@@ -639,8 +639,8 @@ internal object ECPoint {
val pLamOddJac = sc.pLamOddJac
for (i in 0 until pTableSize) {
FieldP.mul(pLamOddJac[i].x, pOddJac[i].x, Glv.BETA, sc.w)
pOddJac[i].y.copyInto(pLamOddJac[i].y, 0, 0, 4)
pOddJac[i].z.copyInto(pLamOddJac[i].z, 0, 0, 4)
pLamOddJac[i].y.copyFrom(pOddJac[i].y)
pLamOddJac[i].z.copyFrom(pOddJac[i].z)
}
// Batch-convert to affine (into scratch arrays)
batchToAffinePair(pOddJac, pLamOddJac, sc.pOddAff, sc.pLamOddAff, sc)
@@ -769,7 +769,7 @@ internal object ECPoint {
private fun addWnafMixedPP(
cur: MutablePoint,
alt: MutablePoint,
negY: LongArray,
negY: Fe4,
wnafDigits: IntArray,
bitIndex: Int,
table: Array<AffinePoint>,
@@ -816,21 +816,21 @@ internal object ECPoint {
val w = s.w
// Prefix products of Z coordinates
val cumZ = Array(n) { LongArray(4) }
U256.copyInto(cumZ[0], points[0].z)
val cumZ = Array(n) { Fe4() }
cumZ[0].copyFrom(points[0].z)
for (i in 1 until n) {
FieldP.mul(cumZ[i], cumZ[i - 1], points[i].z, w)
}
// Invert the total product
val inv = LongArray(4)
val inv = Fe4()
FieldP.inv(inv, cumZ[n - 1])
// Recover individual Z inverses and convert to affine
val result = Array(n) { AffinePoint() }
val zInv = LongArray(4)
val zInv2 = LongArray(4)
val zInv3 = LongArray(4)
val zInv = Fe4()
val zInv2 = Fe4()
val zInv3 = Fe4()
for (i in n - 1 downTo 1) {
// zInv = inv * cumZ[i-1] gives Z[i]^{-1}
@@ -870,7 +870,7 @@ internal object ECPoint {
// Build prefix products of Z (shared between a and b)
val cumZ = s.cumZ
a[0].z.copyInto(cumZ[0], 0, 0, 4)
cumZ[0].copyFrom(a[0].z)
for (i in 1 until n) {
FieldP.mul(cumZ[i], cumZ[i - 1], a[i].z, w)
}
@@ -910,13 +910,13 @@ internal object ECPoint {
/** Convert Jacobian → affine (convenience, allocates temps). For one-time init paths. */
fun toAffine(
p: MutablePoint,
outX: LongArray,
outY: LongArray,
outX: Fe4,
outY: Fe4,
): Boolean {
if (p.isInfinity()) return false
val zInv = LongArray(4)
val zInv2 = LongArray(4)
val zInv3 = LongArray(4)
val zInv = Fe4()
val zInv2 = Fe4()
val zInv3 = Fe4()
FieldP.inv(zInv, p.z)
FieldP.sqr(zInv2, zInv)
FieldP.mul(zInv3, zInv2, zInv)
@@ -928,8 +928,8 @@ internal object ECPoint {
/** Convert Jacobian → affine using pre-allocated scratch (hot path). */
fun toAffine(
p: MutablePoint,
outX: LongArray,
outY: LongArray,
outX: Fe4,
outY: Fe4,
s: PointScratch,
): Boolean {
if (p.isInfinity()) return false
@@ -944,7 +944,7 @@ internal object ECPoint {
/** Convert Jacobian → affine x-only using pre-allocated scratch (hot path). */
fun toAffineX(
p: MutablePoint,
outX: LongArray,
outX: Fe4,
s: PointScratch,
): Boolean {
if (p.isInfinity()) return false
@@ -50,6 +50,8 @@ import kotlin.jvm.JvmField
* Mutable 256-bit field element using 4 named Long fields in little-endian order.
* l0 = least significant 64 bits, l3 = most significant 64 bits.
*
* Used for both field elements (mod p) and scalars (mod n) — same 4×64-bit layout.
*
* @JvmField eliminates virtual getter/setter generation — direct field access.
*/
internal class Fe4(
@@ -67,12 +69,30 @@ internal class Fe4(
l3 = other.l3
}
fun copyOf(): Fe4 = Fe4(l0, l1, l2, l3)
fun setZero() {
l0 = 0L
l1 = 0L
l2 = 0L
l3 = 0L
}
/** Copy this Fe4's limbs into the first 4 elements of a LongArray. */
fun copyIntoArray(dest: LongArray) {
dest[0] = l0
dest[1] = l1
dest[2] = l2
dest[3] = l3
}
/** Load from a LongArray's first 4 elements. */
fun loadFromArray(src: LongArray) {
l0 = src[0]
l1 = src[1]
l2 = src[2]
l3 = src[3]
}
}
/**
@@ -1,599 +0,0 @@
/*
* Copyright (c) 2025 Vitor Pamplona
*
* Permission is hereby granted, free of charge, to any person obtaining a copy of
* this software and associated documentation files (the "Software"), to deal in
* the Software without restriction, including without limitation the rights to use,
* copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the
* Software, and to permit persons to whom the Software is furnished to do so,
* subject to the following conditions:
*
* The above copyright notice and this permission notice shall be included in all
* copies or substantial portions of the Software.
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS
* FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR
* COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN
* AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
* WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
*/
package com.vitorpamplona.quartz.utils.secp256k1
// =====================================================================================
// FIELD OPERATIONS ON Fe4 (struct-based, zero bounds checks)
//
// Mirror implementations of U256 and FieldP operations using Fe4/Wide8 instead of
// LongArray(4)/LongArray(8). The arithmetic is identical; only the access pattern
// differs (field access vs array indexing).
//
// These are used for benchmarking to measure the impact of eliminating array bounds
// checks on various platforms (JVM HotSpot, Android ART, Kotlin/Native LLVM).
// =====================================================================================
/**
* 256-bit unsigned arithmetic on Fe4 (struct-based, no bounds checks).
*/
internal object Fe4U256 {
/** out = a + b. Returns carry (0 or 1). */
fun addTo(
out: Fe4,
a: Fe4,
b: Fe4,
): Int {
var s1: Long
var s2: Long
var c1: Long
var c2: Long
// Limb 0
s1 = a.l0 + b.l0
c1 = if (uLtInline(s1, a.l0)) 1L else 0L
out.l0 = s1
var carry = c1
// Limb 1
s1 = a.l1 + b.l1
c1 = if (uLtInline(s1, a.l1)) 1L else 0L
s2 = s1 + carry
c2 = if (uLtInline(s2, s1)) 1L else 0L
out.l1 = s2
carry = c1 + c2
// Limb 2
s1 = a.l2 + b.l2
c1 = if (uLtInline(s1, a.l2)) 1L else 0L
s2 = s1 + carry
c2 = if (uLtInline(s2, s1)) 1L else 0L
out.l2 = s2
carry = c1 + c2
// Limb 3
s1 = a.l3 + b.l3
c1 = if (uLtInline(s1, a.l3)) 1L else 0L
s2 = s1 + carry
c2 = if (uLtInline(s2, s1)) 1L else 0L
out.l3 = s2
carry = c1 + c2
return carry.toInt()
}
/** out = a - b. Returns borrow (0 or 1). */
fun subTo(
out: Fe4,
a: Fe4,
b: Fe4,
): Int {
var d1: Long
var d2: Long
var c1: Long
var c2: Long
// Limb 0
d1 = a.l0 - b.l0
c1 = if (uLtInline(a.l0, b.l0)) 1L else 0L
out.l0 = d1
var borrow = c1
// Limb 1
d1 = a.l1 - b.l1
c1 = if (uLtInline(a.l1, b.l1)) 1L else 0L
d2 = d1 - borrow
c2 = if (uLtInline(d1, borrow)) 1L else 0L
out.l1 = d2
borrow = c1 + c2
// Limb 2
d1 = a.l2 - b.l2
c1 = if (uLtInline(a.l2, b.l2)) 1L else 0L
d2 = d1 - borrow
c2 = if (uLtInline(d1, borrow)) 1L else 0L
out.l2 = d2
borrow = c1 + c2
// Limb 3
d1 = a.l3 - b.l3
c1 = if (uLtInline(a.l3, b.l3)) 1L else 0L
d2 = d1 - borrow
c2 = if (uLtInline(d1, borrow)) 1L else 0L
out.l3 = d2
borrow = c1 + c2
return borrow.toInt()
}
/** 4×4 schoolbook multiplication: w = a × b (512-bit result). */
@Suppress("LongMethod")
fun mulWide(
w: Wide8,
a: Fe4,
b: Fe4,
) {
val a0 = a.l0
val a1 = a.l1
val a2 = a.l2
val a3 = a.l3
val b0 = b.l0
val b1 = b.l1
val b2 = b.l2
val b3 = b.l3
var lo: Long
var hi: Long
var prev: Long
var s: Long
var c1: Long
var c2: Long
var carry: Long
// Row 0: a0 × [b0,b1,b2,b3]
lo = a0 * b0
w.l0 = lo
carry = unsignedMultiplyHigh(a0, b0)
lo = a0 * b1
s = lo + carry
c1 = if (uLtInline(s, lo)) 1L else 0L
w.l1 = s
carry = unsignedMultiplyHigh(a0, b1) + c1
lo = a0 * b2
s = lo + carry
c1 = if (uLtInline(s, lo)) 1L else 0L
w.l2 = s
carry = unsignedMultiplyHigh(a0, b2) + c1
lo = a0 * b3
s = lo + carry
c1 = if (uLtInline(s, lo)) 1L else 0L
w.l3 = s
w.l4 = unsignedMultiplyHigh(a0, b3) + c1
// Row 1: a1 × [b0,b1,b2,b3]
lo = a1 * b0
hi = unsignedMultiplyHigh(a1, b0)
prev = w.l1
s = prev + lo
c1 = if (uLtInline(s, prev)) 1L else 0L
w.l1 = s
carry = hi + c1
lo = a1 * b1
hi = unsignedMultiplyHigh(a1, b1)
prev = w.l2
s = prev + lo
c1 = if (uLtInline(s, prev)) 1L else 0L
s += carry
c2 = if (uLtInline(s, carry)) 1L else 0L
w.l2 = s
carry = hi + c1 + c2
lo = a1 * b2
hi = unsignedMultiplyHigh(a1, b2)
prev = w.l3
s = prev + lo
c1 = if (uLtInline(s, prev)) 1L else 0L
s += carry
c2 = if (uLtInline(s, carry)) 1L else 0L
w.l3 = s
carry = hi + c1 + c2
lo = a1 * b3
hi = unsignedMultiplyHigh(a1, b3)
prev = w.l4
s = prev + lo
c1 = if (uLtInline(s, prev)) 1L else 0L
s += carry
c2 = if (uLtInline(s, carry)) 1L else 0L
w.l4 = s
w.l5 = hi + c1 + c2
// Row 2: a2 × [b0,b1,b2,b3]
lo = a2 * b0
hi = unsignedMultiplyHigh(a2, b0)
prev = w.l2
s = prev + lo
c1 = if (uLtInline(s, prev)) 1L else 0L
w.l2 = s
carry = hi + c1
lo = a2 * b1
hi = unsignedMultiplyHigh(a2, b1)
prev = w.l3
s = prev + lo
c1 = if (uLtInline(s, prev)) 1L else 0L
s += carry
c2 = if (uLtInline(s, carry)) 1L else 0L
w.l3 = s
carry = hi + c1 + c2
lo = a2 * b2
hi = unsignedMultiplyHigh(a2, b2)
prev = w.l4
s = prev + lo
c1 = if (uLtInline(s, prev)) 1L else 0L
s += carry
c2 = if (uLtInline(s, carry)) 1L else 0L
w.l4 = s
carry = hi + c1 + c2
lo = a2 * b3
hi = unsignedMultiplyHigh(a2, b3)
prev = w.l5
s = prev + lo
c1 = if (uLtInline(s, prev)) 1L else 0L
s += carry
c2 = if (uLtInline(s, carry)) 1L else 0L
w.l5 = s
w.l6 = hi + c1 + c2
// Row 3: a3 × [b0,b1,b2,b3]
lo = a3 * b0
hi = unsignedMultiplyHigh(a3, b0)
prev = w.l3
s = prev + lo
c1 = if (uLtInline(s, prev)) 1L else 0L
w.l3 = s
carry = hi + c1
lo = a3 * b1
hi = unsignedMultiplyHigh(a3, b1)
prev = w.l4
s = prev + lo
c1 = if (uLtInline(s, prev)) 1L else 0L
s += carry
c2 = if (uLtInline(s, carry)) 1L else 0L
w.l4 = s
carry = hi + c1 + c2
lo = a3 * b2
hi = unsignedMultiplyHigh(a3, b2)
prev = w.l5
s = prev + lo
c1 = if (uLtInline(s, prev)) 1L else 0L
s += carry
c2 = if (uLtInline(s, carry)) 1L else 0L
w.l5 = s
carry = hi + c1 + c2
lo = a3 * b3
hi = unsignedMultiplyHigh(a3, b3)
prev = w.l6
s = prev + lo
c1 = if (uLtInline(s, prev)) 1L else 0L
s += carry
c2 = if (uLtInline(s, carry)) 1L else 0L
w.l6 = s
w.l7 = hi + c1 + c2
}
/** Dedicated squaring: w = a² (512-bit result). */
@Suppress("LongMethod")
fun sqrWide(
w: Wide8,
a: Fe4,
) {
val a0 = a.l0
val a1 = a.l1
val a2 = a.l2
val a3 = a.l3
var lo: Long
var hi: Long
var prev: Long
var s: Long
var c1: Long
var c2: Long
var carry: Long
var v: Long
// Pass 1: cross-products a[i]*a[j] for i < j
w.l0 = 0L
lo = a0 * a1
w.l1 = lo
carry = unsignedMultiplyHigh(a0, a1)
lo = a0 * a2
s = lo + carry
c1 = if (uLtInline(s, lo)) 1L else 0L
w.l2 = s
carry = unsignedMultiplyHigh(a0, a2) + c1
lo = a0 * a3
s = lo + carry
c1 = if (uLtInline(s, lo)) 1L else 0L
w.l3 = s
w.l4 = unsignedMultiplyHigh(a0, a3) + c1
lo = a1 * a2
hi = unsignedMultiplyHigh(a1, a2)
prev = w.l3
s = prev + lo
c1 = if (uLtInline(s, prev)) 1L else 0L
w.l3 = s
carry = hi + c1
lo = a1 * a3
hi = unsignedMultiplyHigh(a1, a3)
prev = w.l4
s = prev + lo
c1 = if (uLtInline(s, prev)) 1L else 0L
s += carry
c2 = if (uLtInline(s, carry)) 1L else 0L
w.l4 = s
w.l5 = hi + c1 + c2
lo = a2 * a3
hi = unsignedMultiplyHigh(a2, a3)
prev = w.l5
s = prev + lo
c1 = if (uLtInline(s, prev)) 1L else 0L
w.l5 = s
w.l6 = hi + c1
// Pass 2: double all cross-products (shift left by 1 bit)
v = w.l1
w.l1 = v shl 1
var shiftCarry = v ushr 63
v = w.l2
w.l2 = (v shl 1) or shiftCarry
shiftCarry = v ushr 63
v = w.l3
w.l3 = (v shl 1) or shiftCarry
shiftCarry = v ushr 63
v = w.l4
w.l4 = (v shl 1) or shiftCarry
shiftCarry = v ushr 63
v = w.l5
w.l5 = (v shl 1) or shiftCarry
shiftCarry = v ushr 63
v = w.l6
w.l6 = (v shl 1) or shiftCarry
shiftCarry = v ushr 63
w.l7 = shiftCarry
// Pass 3: add diagonal products a[i]²
lo = a0 * a0
hi = unsignedMultiplyHigh(a0, a0)
w.l0 = lo
s = w.l1 + hi
c1 = if (uLtInline(s, w.l1)) 1L else 0L
w.l1 = s
var dCarry = c1
lo = a1 * a1
hi = unsignedMultiplyHigh(a1, a1)
s = w.l2 + lo
c1 = if (uLtInline(s, w.l2)) 1L else 0L
s += dCarry
c2 = if (uLtInline(s, dCarry)) 1L else 0L
w.l2 = s
prev = w.l3 + hi
val c3a = if (uLtInline(prev, w.l3)) 1L else 0L
prev += c1 + c2
val c4a = if (uLtInline(prev, c1 + c2)) 1L else 0L
w.l3 = prev
dCarry = c3a + c4a
lo = a2 * a2
hi = unsignedMultiplyHigh(a2, a2)
s = w.l4 + lo
c1 = if (uLtInline(s, w.l4)) 1L else 0L
s += dCarry
c2 = if (uLtInline(s, dCarry)) 1L else 0L
w.l4 = s
prev = w.l5 + hi
val c3b = if (uLtInline(prev, w.l5)) 1L else 0L
prev += c1 + c2
val c4b = if (uLtInline(prev, c1 + c2)) 1L else 0L
w.l5 = prev
dCarry = c3b + c4b
lo = a3 * a3
hi = unsignedMultiplyHigh(a3, a3)
s = w.l6 + lo
c1 = if (uLtInline(s, w.l6)) 1L else 0L
s += dCarry
c2 = if (uLtInline(s, dCarry)) 1L else 0L
w.l6 = s
prev = w.l7 + hi
prev += c1 + c2
w.l7 = prev
}
}
/**
* Field arithmetic mod p using Fe4 (struct-based, no bounds checks).
*/
internal object Fe4FieldP {
private const val P0 = -4294968273L // 0xFFFFFFFEFFFFFC2F
fun reduceSelf(a: Fe4) {
if (a.l3 == -1L && a.l2 == -1L && a.l1 == -1L &&
(a.l0 xor Long.MIN_VALUE) >= (P0 xor Long.MIN_VALUE)
) {
a.l0 -= P0
a.l1 = 0L
a.l2 = 0L
a.l3 = 0L
}
}
/** Reduce 512-bit value mod p. */
@Suppress("LongMethod")
fun reduceWide(
out: Fe4,
w: Wide8,
) {
val c = 4294968273L // 2^32 + 977
var hcLo: Long
var hcHi: Long
var s1: Long
var s2: Long
var c1: Long
var c2: Long
// Round 1: acc = lo + hi × C
hcLo = w.l4 * c
hcHi = unsignedMultiplyHigh(w.l4, c)
s1 = w.l0 + hcLo
c1 = if (uLtInline(s1, w.l0)) 1L else 0L
out.l0 = s1
var carry = hcHi + c1
hcLo = w.l5 * c
hcHi = unsignedMultiplyHigh(w.l5, c)
s1 = w.l1 + hcLo
c1 = if (uLtInline(s1, w.l1)) 1L else 0L
s2 = s1 + carry
c2 = if (uLtInline(s2, s1)) 1L else 0L
out.l1 = s2
carry = hcHi + c1 + c2
hcLo = w.l6 * c
hcHi = unsignedMultiplyHigh(w.l6, c)
s1 = w.l2 + hcLo
c1 = if (uLtInline(s1, w.l2)) 1L else 0L
s2 = s1 + carry
c2 = if (uLtInline(s2, s1)) 1L else 0L
out.l2 = s2
carry = hcHi + c1 + c2
hcLo = w.l7 * c
hcHi = unsignedMultiplyHigh(w.l7, c)
s1 = w.l3 + hcLo
c1 = if (uLtInline(s1, w.l3)) 1L else 0L
s2 = s1 + carry
c2 = if (uLtInline(s2, s1)) 1L else 0L
out.l3 = s2
carry = hcHi + c1 + c2
// Round 2: fold carry × C
if (carry != 0L) {
val ccLo = carry * c
val ccHi = unsignedMultiplyHigh(carry, c)
s1 = out.l0 + ccLo
c1 = if (uLtInline(s1, out.l0)) 1L else 0L
out.l0 = s1
var prop = ccHi + c1
if (prop != 0L) {
s1 = out.l1 + prop
prop = if (uLtInline(s1, out.l1)) 1L else 0L
out.l1 = s1
if (prop != 0L) {
s1 = out.l2 + prop
prop = if (uLtInline(s1, out.l2)) 1L else 0L
out.l2 = s1
if (prop != 0L) {
s1 = out.l3 + prop
prop = if (uLtInline(s1, out.l3)) 1L else 0L
out.l3 = s1
}
}
}
if (prop != 0L) {
s1 = out.l0 + c
c1 = if (uLtInline(s1, out.l0)) 1L else 0L
out.l0 = s1
if (c1 != 0L) {
out.l1++
if (out.l1 == 0L) {
out.l2++
if (out.l2 == 0L) out.l3++
}
}
}
}
reduceSelf(out)
}
/** out = (a + b) mod p. */
fun add(
out: Fe4,
a: Fe4,
b: Fe4,
) {
val carry = Fe4U256.addTo(out, a, b)
if (carry != 0) {
val s1 = out.l0 + 4294968273L
val c1 = if (uLtInline(s1, out.l0)) 1L else 0L
out.l0 = s1
if (c1 != 0L) {
out.l1++
if (out.l1 == 0L) {
out.l2++
if (out.l2 == 0L) out.l3++
}
}
}
reduceSelf(out)
}
/** out = (a - b) mod p. */
fun sub(
out: Fe4,
a: Fe4,
b: Fe4,
) {
val borrow = Fe4U256.subTo(out, a, b)
if (borrow != 0) {
val s0 = out.l0 + P0
val c0 = if (uLtInline(s0, out.l0)) 1L else 0L
out.l0 = s0
if (c0 == 0L) {
if (out.l1 != 0L) {
out.l1--
} else {
out.l1 = -1L
if (out.l2 != 0L) {
out.l2--
} else {
out.l2 = -1L
out.l3--
}
}
}
}
}
/** out = (a × b) mod p using caller-provided wide buffer. */
fun mul(
out: Fe4,
a: Fe4,
b: Fe4,
w: Wide8,
) {
Fe4U256.mulWide(w, a, b)
reduceWide(out, w)
}
/** out = a² mod p using caller-provided wide buffer. */
fun sqr(
out: Fe4,
a: Fe4,
w: Wide8,
) {
Fe4U256.sqrWide(w, a)
reduceWide(out, w)
}
}
@@ -90,19 +90,19 @@ private inline fun umulhFused(
*/
@Suppress("LongMethod")
internal fun fieldMulReduceFused(
out: LongArray,
a: LongArray,
b: LongArray,
w: LongArray,
out: Fe4,
a: Fe4,
b: Fe4,
w: Wide8,
) {
val a0 = a[0]
val a1 = a[1]
val a2 = a[2]
val a3 = a[3]
val b0 = b[0]
val b1 = b[1]
val b2 = b[2]
val b3 = b[3]
val a0 = a.l0
val a1 = a.l1
val a2 = a.l2
val a3 = a.l3
val b0 = b.l0
val b1 = b.l1
val b2 = b.l2
val b3 = b.l3
var s: Long = 0L
var c1: Long
var c2: Long
@@ -110,135 +110,135 @@ internal fun fieldMulReduceFused(
// Row 0: a0 × [b0,b1,b2,b3]
mulFull(a0, b0) { lo, hi ->
w[0] = lo
w.l0 = lo
carry = hi
}
mulFull(a0, b1) { lo, hi ->
s = lo + carry
c1 = if (uLtInline(s, lo)) 1L else 0L
w[1] = s
w.l1 = s
carry = hi + c1
}
mulFull(a0, b2) { lo, hi ->
s = lo + carry
c1 = if (uLtInline(s, lo)) 1L else 0L
w[2] = s
w.l2 = s
carry = hi + c1
}
mulFull(a0, b3) { lo, hi ->
s = lo + carry
c1 = if (uLtInline(s, lo)) 1L else 0L
w[3] = s
w[4] = hi + c1
w.l3 = s
w.l4 = hi + c1
}
// Row 1: a1 × [b0,b1,b2,b3]
mulFull(a1, b0) { lo, hi ->
val prev = w[1]
val prev = w.l1
s = prev + lo
c1 = if (uLtInline(s, prev)) 1L else 0L
w[1] = s
w.l1 = s
carry = hi + c1
}
mulFull(a1, b1) { lo, hi ->
val prev = w[2]
val prev = w.l2
s = prev + lo
c1 = if (uLtInline(s, prev)) 1L else 0L
s += carry
c2 = if (uLtInline(s, carry)) 1L else 0L
w[2] = s
w.l2 = s
carry = hi + c1 + c2
}
mulFull(a1, b2) { lo, hi ->
val prev = w[3]
val prev = w.l3
s = prev + lo
c1 = if (uLtInline(s, prev)) 1L else 0L
s += carry
c2 = if (uLtInline(s, carry)) 1L else 0L
w[3] = s
w.l3 = s
carry = hi + c1 + c2
}
mulFull(a1, b3) { lo, hi ->
val prev = w[4]
val prev = w.l4
s = prev + lo
c1 = if (uLtInline(s, prev)) 1L else 0L
s += carry
c2 = if (uLtInline(s, carry)) 1L else 0L
w[4] = s
w[5] = hi + c1 + c2
w.l4 = s
w.l5 = hi + c1 + c2
}
// Row 2: a2 × [b0,b1,b2,b3]
mulFull(a2, b0) { lo, hi ->
val prev = w[2]
val prev = w.l2
s = prev + lo
c1 = if (uLtInline(s, prev)) 1L else 0L
w[2] = s
w.l2 = s
carry = hi + c1
}
mulFull(a2, b1) { lo, hi ->
val prev = w[3]
val prev = w.l3
s = prev + lo
c1 = if (uLtInline(s, prev)) 1L else 0L
s += carry
c2 = if (uLtInline(s, carry)) 1L else 0L
w[3] = s
w.l3 = s
carry = hi + c1 + c2
}
mulFull(a2, b2) { lo, hi ->
val prev = w[4]
val prev = w.l4
s = prev + lo
c1 = if (uLtInline(s, prev)) 1L else 0L
s += carry
c2 = if (uLtInline(s, carry)) 1L else 0L
w[4] = s
w.l4 = s
carry = hi + c1 + c2
}
mulFull(a2, b3) { lo, hi ->
val prev = w[5]
val prev = w.l5
s = prev + lo
c1 = if (uLtInline(s, prev)) 1L else 0L
s += carry
c2 = if (uLtInline(s, carry)) 1L else 0L
w[5] = s
w[6] = hi + c1 + c2
w.l5 = s
w.l6 = hi + c1 + c2
}
// Row 3: a3 × [b0,b1,b2,b3]
mulFull(a3, b0) { lo, hi ->
val prev = w[3]
val prev = w.l3
s = prev + lo
c1 = if (uLtInline(s, prev)) 1L else 0L
w[3] = s
w.l3 = s
carry = hi + c1
}
mulFull(a3, b1) { lo, hi ->
val prev = w[4]
val prev = w.l4
s = prev + lo
c1 = if (uLtInline(s, prev)) 1L else 0L
s += carry
c2 = if (uLtInline(s, carry)) 1L else 0L
w[4] = s
w.l4 = s
carry = hi + c1 + c2
}
mulFull(a3, b2) { lo, hi ->
val prev = w[5]
val prev = w.l5
s = prev + lo
c1 = if (uLtInline(s, prev)) 1L else 0L
s += carry
c2 = if (uLtInline(s, carry)) 1L else 0L
w[5] = s
w.l5 = s
carry = hi + c1 + c2
}
mulFull(a3, b3) { lo, hi ->
val prev = w[6]
val prev = w.l6
s = prev + lo
c1 = if (uLtInline(s, prev)) 1L else 0L
s += carry
c2 = if (uLtInline(s, carry)) 1L else 0L
w[6] = s
w[7] = hi + c1 + c2
w.l6 = s
w.l7 = hi + c1 + c2
}
// Reduction uses umulh only (lo computed with hardware *)
@@ -251,84 +251,84 @@ internal fun fieldMulReduceFused(
*/
@Suppress("LongMethod")
internal fun fieldSqrReduceFused(
out: LongArray,
a: LongArray,
w: LongArray,
out: Fe4,
a: Fe4,
w: Wide8,
) {
val a0 = a[0]
val a1 = a[1]
val a2 = a[2]
val a3 = a[3]
val a0 = a.l0
val a1 = a.l1
val a2 = a.l2
val a3 = a.l3
var s: Long = 0L
var c1: Long
var c2: Long
var carry: Long = 0L
// Pass 1: cross-products a[i]*a[j] for i < j
w[0] = 0L
w.l0 = 0L
mulFull(a0, a1) { lo, hi ->
w[1] = lo
w.l1 = lo
carry = hi
}
mulFull(a0, a2) { lo, hi ->
s = lo + carry
c1 = if (uLtInline(s, lo)) 1L else 0L
w[2] = s
w.l2 = s
carry = hi + c1
}
mulFull(a0, a3) { lo, hi ->
s = lo + carry
c1 = if (uLtInline(s, lo)) 1L else 0L
w[3] = s
w[4] = hi + c1
w.l3 = s
w.l4 = hi + c1
}
mulFull(a1, a2) { lo, hi ->
val prev = w[3]
val prev = w.l3
s = prev + lo
c1 = if (uLtInline(s, prev)) 1L else 0L
w[3] = s
w.l3 = s
carry = hi + c1
}
mulFull(a1, a3) { lo, hi ->
val prev = w[4]
val prev = w.l4
s = prev + lo
c1 = if (uLtInline(s, prev)) 1L else 0L
s += carry
c2 = if (uLtInline(s, carry)) 1L else 0L
w[4] = s
w[5] = hi + c1 + c2
w.l4 = s
w.l5 = hi + c1 + c2
}
mulFull(a2, a3) { lo, hi ->
val prev = w[5]
val prev = w.l5
s = prev + lo
c1 = if (uLtInline(s, prev)) 1L else 0L
w[5] = s
w[6] = hi + c1
w.l5 = s
w.l6 = hi + c1
}
// Pass 2: double all cross-products (shift left by 1 bit)
var v = w[1]
w[1] = v shl 1
var v = w.l1
w.l1 = v shl 1
var shiftCarry = v ushr 63
v = w[2]
w[2] = (v shl 1) or shiftCarry
v = w.l2
w.l2 = (v shl 1) or shiftCarry
shiftCarry = v ushr 63
v = w[3]
w[3] = (v shl 1) or shiftCarry
v = w.l3
w.l3 = (v shl 1) or shiftCarry
shiftCarry = v ushr 63
v = w[4]
w[4] = (v shl 1) or shiftCarry
v = w.l4
w.l4 = (v shl 1) or shiftCarry
shiftCarry = v ushr 63
v = w[5]
w[5] = (v shl 1) or shiftCarry
v = w.l5
w.l5 = (v shl 1) or shiftCarry
shiftCarry = v ushr 63
v = w[6]
w[6] = (v shl 1) or shiftCarry
v = w.l6
w.l6 = (v shl 1) or shiftCarry
shiftCarry = v ushr 63
w[7] = shiftCarry
w.l7 = shiftCarry
// Pass 3: add diagonal products a[i]²
// Use the original pattern: lo = a*a, hi = umulh(a,a) since the diagonal
@@ -339,50 +339,50 @@ internal fun fieldSqrReduceFused(
dLo = a0 * a0
dHi = umulhFused(a0, a0)
w[0] = dLo
s = w[1] + dHi
c1 = if (uLtInline(s, w[1])) 1L else 0L
w[1] = s
w.l0 = dLo
s = w.l1 + dHi
c1 = if (uLtInline(s, w.l1)) 1L else 0L
w.l1 = s
var dCarry = c1
dLo = a1 * a1
dHi = umulhFused(a1, a1)
s = w[2] + dLo
c1 = if (uLtInline(s, w[2])) 1L else 0L
s = w.l2 + dLo
c1 = if (uLtInline(s, w.l2)) 1L else 0L
s += dCarry
c2 = if (uLtInline(s, dCarry)) 1L else 0L
w[2] = s
prev = w[3] + dHi
val c3a = if (uLtInline(prev, w[3])) 1L else 0L
w.l2 = s
prev = w.l3 + dHi
val c3a = if (uLtInline(prev, w.l3)) 1L else 0L
prev += c1 + c2
val c4a = if (uLtInline(prev, c1 + c2)) 1L else 0L
w[3] = prev
w.l3 = prev
dCarry = c3a + c4a
dLo = a2 * a2
dHi = umulhFused(a2, a2)
s = w[4] + dLo
c1 = if (uLtInline(s, w[4])) 1L else 0L
s = w.l4 + dLo
c1 = if (uLtInline(s, w.l4)) 1L else 0L
s += dCarry
c2 = if (uLtInline(s, dCarry)) 1L else 0L
w[4] = s
prev = w[5] + dHi
val c3b = if (uLtInline(prev, w[5])) 1L else 0L
w.l4 = s
prev = w.l5 + dHi
val c3b = if (uLtInline(prev, w.l5)) 1L else 0L
prev += c1 + c2
val c4b = if (uLtInline(prev, c1 + c2)) 1L else 0L
w[5] = prev
w.l5 = prev
dCarry = c3b + c4b
dLo = a3 * a3
dHi = umulhFused(a3, a3)
s = w[6] + dLo
c1 = if (uLtInline(s, w[6])) 1L else 0L
s = w.l6 + dLo
c1 = if (uLtInline(s, w.l6)) 1L else 0L
s += dCarry
c2 = if (uLtInline(s, dCarry)) 1L else 0L
w[6] = s
prev = w[7] + dHi
w.l6 = s
prev = w.l7 + dHi
prev += c1 + c2
w[7] = prev
w.l7 = prev
// Reduction
reduceWideInline(out, w) { x, y -> umulhFused(x, y) }
@@ -67,10 +67,10 @@ package com.vitorpamplona.quartz.utils.secp256k1
* Platform implementations call fieldMulReduceWith with the best available intrinsic.
*/
internal expect fun fieldMulReduce(
out: LongArray,
a: LongArray,
b: LongArray,
w: LongArray,
out: Fe4,
a: Fe4,
b: Fe4,
w: Wide8,
)
/**
@@ -78,9 +78,9 @@ internal expect fun fieldMulReduce(
* Platform implementations call fieldSqrReduceWith with the best available intrinsic.
*/
internal expect fun fieldSqrReduce(
out: LongArray,
a: LongArray,
w: LongArray,
out: Fe4,
a: Fe4,
w: Wide8,
)
// P[0] constant for reduceSelf (duplicated here because inline functions can't
@@ -101,21 +101,21 @@ private const val FIELD_P0 = -4294968273L // 0xFFFFFFFEFFFFFC2F
*/
@Suppress("LongMethod")
internal inline fun fieldMulReduceWith(
out: LongArray,
a: LongArray,
b: LongArray,
w: LongArray,
out: Fe4,
a: Fe4,
b: Fe4,
w: Wide8,
umulh: (Long, Long) -> Long,
) {
// === Stage 1: 4×4 schoolbook multiplication (16 products) ===
val a0 = a[0]
val a1 = a[1]
val a2 = a[2]
val a3 = a[3]
val b0 = b[0]
val b1 = b[1]
val b2 = b[2]
val b3 = b[3]
val a0 = a.l0
val a1 = a.l1
val a2 = a.l2
val a3 = a.l3
val b0 = b.l0
val b1 = b.l1
val b2 = b.l2
val b3 = b.l3
var lo: Long
var hi: Long
var prev: Long
@@ -126,143 +126,143 @@ internal inline fun fieldMulReduceWith(
// Row 0: a0 × [b0,b1,b2,b3]
lo = a0 * b0
w[0] = lo
w.l0 = lo
carry = umulh(a0, b0)
lo = a0 * b1
s = lo + carry
c1 = if (uLtInline(s, lo)) 1L else 0L
w[1] = s
w.l1 = s
carry = umulh(a0, b1) + c1
lo = a0 * b2
s = lo + carry
c1 = if (uLtInline(s, lo)) 1L else 0L
w[2] = s
w.l2 = s
carry = umulh(a0, b2) + c1
lo = a0 * b3
s = lo + carry
c1 = if (uLtInline(s, lo)) 1L else 0L
w[3] = s
w[4] = umulh(a0, b3) + c1
w.l3 = s
w.l4 = umulh(a0, b3) + c1
// Row 1: a1 × [b0,b1,b2,b3]
lo = a1 * b0
hi = umulh(a1, b0)
prev = w[1]
prev = w.l1
s = prev + lo
c1 = if (uLtInline(s, prev)) 1L else 0L
w[1] = s
w.l1 = s
carry = hi + c1
lo = a1 * b1
hi = umulh(a1, b1)
prev = w[2]
prev = w.l2
s = prev + lo
c1 = if (uLtInline(s, prev)) 1L else 0L
s += carry
c2 = if (uLtInline(s, carry)) 1L else 0L
w[2] = s
w.l2 = s
carry = hi + c1 + c2
lo = a1 * b2
hi = umulh(a1, b2)
prev = w[3]
prev = w.l3
s = prev + lo
c1 = if (uLtInline(s, prev)) 1L else 0L
s += carry
c2 = if (uLtInline(s, carry)) 1L else 0L
w[3] = s
w.l3 = s
carry = hi + c1 + c2
lo = a1 * b3
hi = umulh(a1, b3)
prev = w[4]
prev = w.l4
s = prev + lo
c1 = if (uLtInline(s, prev)) 1L else 0L
s += carry
c2 = if (uLtInline(s, carry)) 1L else 0L
w[4] = s
w[5] = hi + c1 + c2
w.l4 = s
w.l5 = hi + c1 + c2
// Row 2: a2 × [b0,b1,b2,b3]
lo = a2 * b0
hi = umulh(a2, b0)
prev = w[2]
prev = w.l2
s = prev + lo
c1 = if (uLtInline(s, prev)) 1L else 0L
w[2] = s
w.l2 = s
carry = hi + c1
lo = a2 * b1
hi = umulh(a2, b1)
prev = w[3]
prev = w.l3
s = prev + lo
c1 = if (uLtInline(s, prev)) 1L else 0L
s += carry
c2 = if (uLtInline(s, carry)) 1L else 0L
w[3] = s
w.l3 = s
carry = hi + c1 + c2
lo = a2 * b2
hi = umulh(a2, b2)
prev = w[4]
prev = w.l4
s = prev + lo
c1 = if (uLtInline(s, prev)) 1L else 0L
s += carry
c2 = if (uLtInline(s, carry)) 1L else 0L
w[4] = s
w.l4 = s
carry = hi + c1 + c2
lo = a2 * b3
hi = umulh(a2, b3)
prev = w[5]
prev = w.l5
s = prev + lo
c1 = if (uLtInline(s, prev)) 1L else 0L
s += carry
c2 = if (uLtInline(s, carry)) 1L else 0L
w[5] = s
w[6] = hi + c1 + c2
w.l5 = s
w.l6 = hi + c1 + c2
// Row 3: a3 × [b0,b1,b2,b3]
lo = a3 * b0
hi = umulh(a3, b0)
prev = w[3]
prev = w.l3
s = prev + lo
c1 = if (uLtInline(s, prev)) 1L else 0L
w[3] = s
w.l3 = s
carry = hi + c1
lo = a3 * b1
hi = umulh(a3, b1)
prev = w[4]
prev = w.l4
s = prev + lo
c1 = if (uLtInline(s, prev)) 1L else 0L
s += carry
c2 = if (uLtInline(s, carry)) 1L else 0L
w[4] = s
w.l4 = s
carry = hi + c1 + c2
lo = a3 * b2
hi = umulh(a3, b2)
prev = w[5]
prev = w.l5
s = prev + lo
c1 = if (uLtInline(s, prev)) 1L else 0L
s += carry
c2 = if (uLtInline(s, carry)) 1L else 0L
w[5] = s
w.l5 = s
carry = hi + c1 + c2
lo = a3 * b3
hi = umulh(a3, b3)
prev = w[6]
prev = w.l6
s = prev + lo
c1 = if (uLtInline(s, prev)) 1L else 0L
s += carry
c2 = if (uLtInline(s, carry)) 1L else 0L
w[6] = s
w[7] = hi + c1 + c2
w.l6 = s
w.l7 = hi + c1 + c2
// === Stage 2: mod-p reduction (2^256 ≡ 2^32 + 977 mod p) ===
reduceWideInline(out, w, umulh)
@@ -275,15 +275,15 @@ internal inline fun fieldMulReduceWith(
*/
@Suppress("LongMethod")
internal inline fun fieldSqrReduceWith(
out: LongArray,
a: LongArray,
w: LongArray,
out: Fe4,
a: Fe4,
w: Wide8,
umulh: (Long, Long) -> Long,
) {
val a0 = a[0]
val a1 = a[1]
val a2 = a[2]
val a3 = a[3]
val a0 = a.l0
val a1 = a.l1
val a2 = a.l2
val a3 = a.l3
var lo: Long
var hi: Long
var prev: Long
@@ -294,117 +294,117 @@ internal inline fun fieldSqrReduceWith(
var v: Long
// Pass 1: cross-products a[i]*a[j] for i < j
w[0] = 0L
w.l0 = 0L
lo = a0 * a1
w[1] = lo
w.l1 = lo
carry = umulh(a0, a1)
lo = a0 * a2
s = lo + carry
c1 = if (uLtInline(s, lo)) 1L else 0L
w[2] = s
w.l2 = s
carry = umulh(a0, a2) + c1
lo = a0 * a3
s = lo + carry
c1 = if (uLtInline(s, lo)) 1L else 0L
w[3] = s
w[4] = umulh(a0, a3) + c1
w.l3 = s
w.l4 = umulh(a0, a3) + c1
lo = a1 * a2
hi = umulh(a1, a2)
prev = w[3]
prev = w.l3
s = prev + lo
c1 = if (uLtInline(s, prev)) 1L else 0L
w[3] = s
w.l3 = s
carry = hi + c1
lo = a1 * a3
hi = umulh(a1, a3)
prev = w[4]
prev = w.l4
s = prev + lo
c1 = if (uLtInline(s, prev)) 1L else 0L
s += carry
c2 = if (uLtInline(s, carry)) 1L else 0L
w[4] = s
w[5] = hi + c1 + c2
w.l4 = s
w.l5 = hi + c1 + c2
lo = a2 * a3
hi = umulh(a2, a3)
prev = w[5]
prev = w.l5
s = prev + lo
c1 = if (uLtInline(s, prev)) 1L else 0L
w[5] = s
w[6] = hi + c1
w.l5 = s
w.l6 = hi + c1
// Pass 2: double all cross-products (shift left by 1 bit)
v = w[1]
w[1] = v shl 1
v = w.l1
w.l1 = v shl 1
var shiftCarry = v ushr 63
v = w[2]
w[2] = (v shl 1) or shiftCarry
v = w.l2
w.l2 = (v shl 1) or shiftCarry
shiftCarry = v ushr 63
v = w[3]
w[3] = (v shl 1) or shiftCarry
v = w.l3
w.l3 = (v shl 1) or shiftCarry
shiftCarry = v ushr 63
v = w[4]
w[4] = (v shl 1) or shiftCarry
v = w.l4
w.l4 = (v shl 1) or shiftCarry
shiftCarry = v ushr 63
v = w[5]
w[5] = (v shl 1) or shiftCarry
v = w.l5
w.l5 = (v shl 1) or shiftCarry
shiftCarry = v ushr 63
v = w[6]
w[6] = (v shl 1) or shiftCarry
v = w.l6
w.l6 = (v shl 1) or shiftCarry
shiftCarry = v ushr 63
w[7] = shiftCarry
w.l7 = shiftCarry
// Pass 3: add diagonal products a[i]²
lo = a0 * a0
hi = umulh(a0, a0)
w[0] = lo
s = w[1] + hi
c1 = if (uLtInline(s, w[1])) 1L else 0L
w[1] = s
w.l0 = lo
s = w.l1 + hi
c1 = if (uLtInline(s, w.l1)) 1L else 0L
w.l1 = s
var dCarry = c1
lo = a1 * a1
hi = umulh(a1, a1)
s = w[2] + lo
c1 = if (uLtInline(s, w[2])) 1L else 0L
s = w.l2 + lo
c1 = if (uLtInline(s, w.l2)) 1L else 0L
s += dCarry
c2 = if (uLtInline(s, dCarry)) 1L else 0L
w[2] = s
prev = w[3] + hi
val c3a = if (uLtInline(prev, w[3])) 1L else 0L
w.l2 = s
prev = w.l3 + hi
val c3a = if (uLtInline(prev, w.l3)) 1L else 0L
prev += c1 + c2
val c4a = if (uLtInline(prev, c1 + c2)) 1L else 0L
w[3] = prev
w.l3 = prev
dCarry = c3a + c4a
lo = a2 * a2
hi = umulh(a2, a2)
s = w[4] + lo
c1 = if (uLtInline(s, w[4])) 1L else 0L
s = w.l4 + lo
c1 = if (uLtInline(s, w.l4)) 1L else 0L
s += dCarry
c2 = if (uLtInline(s, dCarry)) 1L else 0L
w[4] = s
prev = w[5] + hi
val c3b = if (uLtInline(prev, w[5])) 1L else 0L
w.l4 = s
prev = w.l5 + hi
val c3b = if (uLtInline(prev, w.l5)) 1L else 0L
prev += c1 + c2
val c4b = if (uLtInline(prev, c1 + c2)) 1L else 0L
w[5] = prev
w.l5 = prev
dCarry = c3b + c4b
lo = a3 * a3
hi = umulh(a3, a3)
s = w[6] + lo
c1 = if (uLtInline(s, w[6])) 1L else 0L
s = w.l6 + lo
c1 = if (uLtInline(s, w.l6)) 1L else 0L
s += dCarry
c2 = if (uLtInline(s, dCarry)) 1L else 0L
w[6] = s
prev = w[7] + hi
w.l6 = s
prev = w.l7 + hi
prev += c1 + c2
w[7] = prev
w.l7 = prev
// === Reduction ===
reduceWideInline(out, w, umulh)
@@ -417,8 +417,8 @@ internal inline fun fieldSqrReduceWith(
*/
@Suppress("LongMethod")
internal inline fun reduceWideInline(
out: LongArray,
w: LongArray,
out: Fe4,
w: Wide8,
umulh: (Long, Long) -> Long,
) {
val c = 4294968273L // 2^32 + 977
@@ -430,84 +430,84 @@ internal inline fun reduceWideInline(
var c2: Long
// Round 1: acc = lo + hi × C
hcLo = w[4] * c
hcHi = umulh(w[4], c)
s1 = w[0] + hcLo
c1 = if (uLtInline(s1, w[0])) 1L else 0L
out[0] = s1
hcLo = w.l4 * c
hcHi = umulh(w.l4, c)
s1 = w.l0 + hcLo
c1 = if (uLtInline(s1, w.l0)) 1L else 0L
out.l0 = s1
var carry = hcHi + c1
hcLo = w[5] * c
hcHi = umulh(w[5], c)
s1 = w[1] + hcLo
c1 = if (uLtInline(s1, w[1])) 1L else 0L
hcLo = w.l5 * c
hcHi = umulh(w.l5, c)
s1 = w.l1 + hcLo
c1 = if (uLtInline(s1, w.l1)) 1L else 0L
s2 = s1 + carry
c2 = if (uLtInline(s2, s1)) 1L else 0L
out[1] = s2
out.l1 = s2
carry = hcHi + c1 + c2
hcLo = w[6] * c
hcHi = umulh(w[6], c)
s1 = w[2] + hcLo
c1 = if (uLtInline(s1, w[2])) 1L else 0L
hcLo = w.l6 * c
hcHi = umulh(w.l6, c)
s1 = w.l2 + hcLo
c1 = if (uLtInline(s1, w.l2)) 1L else 0L
s2 = s1 + carry
c2 = if (uLtInline(s2, s1)) 1L else 0L
out[2] = s2
out.l2 = s2
carry = hcHi + c1 + c2
hcLo = w[7] * c
hcHi = umulh(w[7], c)
s1 = w[3] + hcLo
c1 = if (uLtInline(s1, w[3])) 1L else 0L
hcLo = w.l7 * c
hcHi = umulh(w.l7, c)
s1 = w.l3 + hcLo
c1 = if (uLtInline(s1, w.l3)) 1L else 0L
s2 = s1 + carry
c2 = if (uLtInline(s2, s1)) 1L else 0L
out[3] = s2
out.l3 = s2
carry = hcHi + c1 + c2
// Round 2: fold carry × C
if (carry != 0L) {
val ccLo = carry * c
val ccHi = umulh(carry, c)
s1 = out[0] + ccLo
c1 = if (uLtInline(s1, out[0])) 1L else 0L
out[0] = s1
s1 = out.l0 + ccLo
c1 = if (uLtInline(s1, out.l0)) 1L else 0L
out.l0 = s1
var prop = ccHi + c1
if (prop != 0L) {
s1 = out[1] + prop
prop = if (uLtInline(s1, out[1])) 1L else 0L
out[1] = s1
s1 = out.l1 + prop
prop = if (uLtInline(s1, out.l1)) 1L else 0L
out.l1 = s1
if (prop != 0L) {
s1 = out[2] + prop
prop = if (uLtInline(s1, out[2])) 1L else 0L
out[2] = s1
s1 = out.l2 + prop
prop = if (uLtInline(s1, out.l2)) 1L else 0L
out.l2 = s1
if (prop != 0L) {
s1 = out[3] + prop
prop = if (uLtInline(s1, out[3])) 1L else 0L
out[3] = s1
s1 = out.l3 + prop
prop = if (uLtInline(s1, out.l3)) 1L else 0L
out.l3 = s1
}
}
}
if (prop != 0L) {
s1 = out[0] + c
c1 = if (uLtInline(s1, out[0])) 1L else 0L
out[0] = s1
s1 = out.l0 + c
c1 = if (uLtInline(s1, out.l0)) 1L else 0L
out.l0 = s1
if (c1 != 0L) {
out[1]++
if (out[1] == 0L) {
out[2]++
if (out[2] == 0L) out[3]++
out.l1++
if (out.l1 == 0L) {
out.l2++
if (out.l2 == 0L) out.l3++
}
}
}
}
// Final normalization: ensure out < p
if (out[3] == -1L && out[2] == -1L && out[1] == -1L &&
(out[0] xor Long.MIN_VALUE) >= (FIELD_P0 xor Long.MIN_VALUE)
if (out.l3 == -1L && out.l2 == -1L && out.l1 == -1L &&
(out.l0 xor Long.MIN_VALUE) >= (FIELD_P0 xor Long.MIN_VALUE)
) {
out[0] -= FIELD_P0
out[1] = 0L
out[2] = 0L
out[3] = 0L
out.l0 -= FIELD_P0
out.l1 = 0L
out.l2 = 0L
out.l3 = 0L
}
}
@@ -22,9 +22,9 @@ package com.vitorpamplona.quartz.utils.secp256k1
/**
* Arithmetic modulo the secp256k1 field prime: p = 2^256 - 2^32 - 977.
* Uses LongArray(4) limbs (4×64-bit).
* Uses Fe4 limbs (4×64-bit).
*
* Hot-path mul/sqr accept a pre-fetched LongArray(8) wide buffer to avoid
* Hot-path mul/sqr accept a pre-fetched Wide8 wide buffer to avoid
* ThreadLocal.get() overhead (~20-30ns per call, 500+ calls per scalar mul).
*
* Key difference from C libsecp256k1: no lazy reduction / magnitude tracking.
@@ -41,40 +41,40 @@ package com.vitorpamplona.quartz.utils.secp256k1
internal object FieldP {
// p = 0xFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFEFFFFFC2F
val P =
longArrayOf(
Fe4(
-4294968273L, // 0xFFFFFFFEFFFFFC2F
-1L, // 0xFFFFFFFFFFFFFFFF
-1L, // 0xFFFFFFFFFFFFFFFF
-1L, // 0xFFFFFFFFFFFFFFFF
)
private val wide = ScratchLocal { LongArray(8) }
private val wide = ScratchLocal { Wide8() }
// Pre-allocated scratch for inv/sqrt addition chains (11 field elements).
// Avoids 11 LongArray(4) allocations per inv/sqrt call.
private val chainScratch = ScratchLocal { Array(11) { LongArray(4) } }
// Avoids 11 Fe4 allocations per inv/sqrt call.
private val chainScratch = ScratchLocal { Array(11) { Fe4() } }
/** Get a thread-local wide buffer. Call once at the top-level entry point, then pass through. */
fun getWide(): LongArray = wide.get()
fun getWide(): Wide8 = wide.get()
// ==================== Core arithmetic ====================
fun add(
out: LongArray,
a: LongArray,
b: LongArray,
out: Fe4,
a: Fe4,
b: Fe4,
) {
val carry = U256.addTo(out, a, b)
if (carry != 0) {
// Overflow past 2^256: add 2^256 mod p = 2^32 + 977 = 0x1000003D1
val s1 = out[0] + 4294968273L
val c1 = if (uLtInline(s1, out[0])) 1L else 0L
out[0] = s1
val s1 = out.l0 + 4294968273L
val c1 = if (uLtInline(s1, out.l0)) 1L else 0L
out.l0 = s1
if (c1 != 0L) {
out[1]++
if (out[1] == 0L) {
out[2]++
if (out[2] == 0L) out[3]++
out.l1++
if (out.l1 == 0L) {
out.l2++
if (out.l2 == 0L) out.l3++
}
}
}
@@ -87,30 +87,30 @@ internal object FieldP {
* case needs work (subtract 1 with borrow propagation). ~500 calls/verify.
*/
fun sub(
out: LongArray,
a: LongArray,
b: LongArray,
out: Fe4,
a: Fe4,
b: Fe4,
) {
val borrow = U256.subTo(out, a, b)
if (borrow != 0) {
// Add P = [P0, -1, -1, -1].
val s0 = out[0] + P0
val c0 = if (uLtInline(s0, out[0])) 1L else 0L
out[0] = s0
val s0 = out.l0 + P0
val c0 = if (uLtInline(s0, out.l0)) 1L else 0L
out.l0 = s0
// For limbs 1-3: adding P[i]=-1 with carry c:
// c=1 → result unchanged, carry out=1 (identity propagation)
// c=0 → result = out[i]-1, carry out = (out[i] != 0) ? 1 : 0
// So if c0=1, limbs 1-3 are untouched. If c0=0, subtract 1 with borrow:
if (c0 == 0L) {
if (out[1] != 0L) {
out[1]--
if (out.l1 != 0L) {
out.l1--
} else {
out[1] = -1L // 0-1 wraps
if (out[2] != 0L) {
out[2]--
out.l1 = -1L // 0-1 wraps
if (out.l2 != 0L) {
out.l2--
} else {
out[2] = -1L
out[3]--
out.l2 = -1L
out.l3--
}
}
}
@@ -119,9 +119,9 @@ internal object FieldP {
/** Multiply with ThreadLocal wide buffer (convenience for non-hot paths). */
fun mul(
out: LongArray,
a: LongArray,
b: LongArray,
out: Fe4,
a: Fe4,
b: Fe4,
) {
val w = wide.get()
fieldMulReduce(out, a, b, w)
@@ -129,18 +129,18 @@ internal object FieldP {
/** Multiply with caller-provided wide buffer (hot path — no ThreadLocal lookup). */
fun mul(
out: LongArray,
a: LongArray,
b: LongArray,
w: LongArray,
out: Fe4,
a: Fe4,
b: Fe4,
w: Wide8,
) {
fieldMulReduce(out, a, b, w)
}
/** Square with ThreadLocal wide buffer (convenience for non-hot paths). */
fun sqr(
out: LongArray,
a: LongArray,
out: Fe4,
a: Fe4,
) {
val w = wide.get()
fieldSqrReduce(out, a, w)
@@ -148,9 +148,9 @@ internal object FieldP {
/** Square with caller-provided wide buffer (hot path — no ThreadLocal lookup). */
fun sqr(
out: LongArray,
a: LongArray,
w: LongArray,
out: Fe4,
a: Fe4,
w: Wide8,
) {
fieldSqrReduce(out, a, w)
}
@@ -158,28 +158,28 @@ internal object FieldP {
/**
* out = -a mod p = P - a. Specialized for P = [P0, -1, -1, -1]:
* P[i]-a[i] = ~a[i] for i>=1 (bitwise NOT), with borrow from limb 0.
* Avoids generic U256.subTo + P array reads (~260 calls/verify).
* Avoids generic U256.subTo + P field reads (~260 calls/verify).
*/
fun neg(
out: LongArray,
a: LongArray,
out: Fe4,
a: Fe4,
) {
if (U256.isZero(a)) {
out[0] = 0L
out[1] = 0L
out[2] = 0L
out[3] = 0L
if (a.isZero()) {
out.l0 = 0L
out.l1 = 0L
out.l2 = 0L
out.l3 = 0L
return
}
// P - a: limb 0 is P0 - a[0], limbs 1-3 are (-1) - a[i] = ~a[i]
out[0] = P0 - a[0]
val borrow = if (uLtInline(P0, a[0])) 1L else 0L
// P - a: limb 0 is P0 - a.l0, limbs 1-3 are (-1) - a[i] = ~a[i]
out.l0 = P0 - a.l0
val borrow = if (uLtInline(P0, a.l0)) 1L else 0L
// ~a[i] - borrow. New borrow only if ~a[i] == 0 (i.e., a[i] == -1) and borrow == 1
out[1] = a[1].inv() - borrow
val b1 = if (a[1] == -1L && borrow != 0L) 1L else 0L
out[2] = a[2].inv() - b1
val b2 = if (a[2] == -1L && b1 != 0L) 1L else 0L
out[3] = a[3].inv() - b2
out.l1 = a.l1.inv() - borrow
val b1 = if (a.l1 == -1L && borrow != 0L) 1L else 0L
out.l2 = a.l2.inv() - b1
val b2 = if (a.l2 == -1L && b1 != 0L) 1L else 0L
out.l3 = a.l3.inv() - b2
}
/**
@@ -187,10 +187,10 @@ internal object FieldP {
* Unrolled, with P[1..3]=-1 inlined as `mask` (since -1 & mask = mask).
*/
fun half(
out: LongArray,
a: LongArray,
out: Fe4,
a: Fe4,
) {
val mask = -(a[0] and 1L) // all 1s if odd, all 0s if even
val mask = -(a.l0 and 1L) // all 1s if odd, all 0s if even
val p0 = P0 and mask // P[0] masked; P[1..3] are -1, so P[i]&mask = mask
var s1: Long
var s2: Long
@@ -199,46 +199,46 @@ internal object FieldP {
// Conditional add: out = a + (P & mask), unrolled
// Limb 0
s1 = a[0] + p0
c1 = if (uLtInline(s1, a[0])) 1L else 0L
out[0] = s1
s1 = a.l0 + p0
c1 = if (uLtInline(s1, a.l0)) 1L else 0L
out.l0 = s1
var carry = c1
// Limb 1
s1 = a[1] + mask
c1 = if (uLtInline(s1, a[1])) 1L else 0L
s1 = a.l1 + mask
c1 = if (uLtInline(s1, a.l1)) 1L else 0L
s2 = s1 + carry
c2 = if (uLtInline(s2, s1)) 1L else 0L
out[1] = s2
out.l1 = s2
carry = c1 + c2
// Limb 2
s1 = a[2] + mask
c1 = if (uLtInline(s1, a[2])) 1L else 0L
s1 = a.l2 + mask
c1 = if (uLtInline(s1, a.l2)) 1L else 0L
s2 = s1 + carry
c2 = if (uLtInline(s2, s1)) 1L else 0L
out[2] = s2
out.l2 = s2
carry = c1 + c2
// Limb 3
s1 = a[3] + mask
c1 = if (uLtInline(s1, a[3])) 1L else 0L
s1 = a.l3 + mask
c1 = if (uLtInline(s1, a.l3)) 1L else 0L
s2 = s1 + carry
c2 = if (uLtInline(s2, s1)) 1L else 0L
out[3] = s2
out.l3 = s2
carry = c1 + c2
// Right-shift by 1 (unrolled)
out[0] = (out[0] ushr 1) or (out[1] shl 63)
out[1] = (out[1] ushr 1) or (out[2] shl 63)
out[2] = (out[2] ushr 1) or (out[3] shl 63)
out[3] = (out[3] ushr 1) or (carry shl 63)
out.l0 = (out.l0 ushr 1) or (out.l1 shl 63)
out.l1 = (out.l1 ushr 1) or (out.l2 shl 63)
out.l2 = (out.l2 ushr 1) or (out.l3 shl 63)
out.l3 = (out.l3 ushr 1) or (carry shl 63)
}
// ==================== Inversion and square root (optimized addition chains) ====================
fun inv(
out: LongArray,
a: LongArray,
out: Fe4,
a: Fe4,
) {
require(!U256.isZero(a))
require(!a.isZero())
val w = wide.get()
val cs = chainScratch.get()
val x2 = cs[0]
@@ -287,8 +287,8 @@ internal object FieldP {
}
fun sqrt(
out: LongArray,
a: LongArray,
out: Fe4,
a: Fe4,
): Boolean {
val w = wide.get()
val cs = chainScratch.get()
@@ -336,48 +336,48 @@ internal object FieldP {
// Verify: check that out² == a (mod p)
// Reuse cs[0], cs[1] as scratch since we're done with the chain
mul(cs[0], out, out, w) // cs[0] = out²
U256.copyInto(cs[1], a)
cs[1].copyFrom(a)
reduceSelf(cs[1]) // cs[1] = a reduced
return U256.cmp(cs[0], cs[1]) == 0
}
private fun sqrN(
out: LongArray,
a: LongArray,
out: Fe4,
a: Fe4,
n: Int,
w: LongArray,
w: Wide8,
) {
U256.copyInto(out, a)
out.copyFrom(a)
repeat(n) { sqr(out, out, w) }
}
private fun sqrN(
out: LongArray,
a: LongArray,
out: Fe4,
a: Fe4,
n: Int,
) {
U256.copyInto(out, a)
out.copyFrom(a)
repeat(n) { sqr(out, out) }
}
// ==================== Reduction ====================
// P[0] cached as a constant to avoid array load in the hot reduceSelf path.
// P[0] cached as a constant to avoid field load in the hot reduceSelf path.
private const val P0 = -4294968273L // 0xFFFFFFFEFFFFFC2F
fun reduceSelf(a: LongArray) {
fun reduceSelf(a: Fe4) {
// Exploit P's structure: P = [P0, -1, -1, -1] where P[1..3] = 0xFFFFFFFFFFFFFFFF.
// a >= P only if a[3]==a[2]==a[1]==-1 AND a[0] >= P[0]. The first check (a[3]==-1)
// a >= P only if a.l3==a.l2==a.l1==-1 AND a.l0 >= P[0]. The first check (a.l3==-1)
// fails >99.99% of the time for random field elements, making this a single branch.
if (a[3] == -1L && a[2] == -1L && a[1] == -1L &&
(a[0] xor Long.MIN_VALUE) >= (P0 xor Long.MIN_VALUE)
if (a.l3 == -1L && a.l2 == -1L && a.l1 == -1L &&
(a.l0 xor Long.MIN_VALUE) >= (P0 xor Long.MIN_VALUE)
) {
// Inline P subtraction: when a[1..3] = -1 and a[0] >= P0,
// a - P = [a[0] - P0, 0, 0, 0] (no borrows since P[1..3] = -1).
a[0] -= P0
a[1] = 0L
a[2] = 0L
a[3] = 0L
// Inline P subtraction: when a.l1..l3 = -1 and a.l0 >= P0,
// a - P = [a.l0 - P0, 0, 0, 0] (no borrows since P[1..3] = -1).
a.l0 -= P0
a.l1 = 0L
a.l2 = 0L
a.l3 = 0L
}
}
@@ -388,8 +388,8 @@ internal object FieldP {
* Three stages: fold 512→~260 bits, fold carry×C, final reduceSelf.
*/
fun reduceWide(
out: LongArray,
w: LongArray,
out: Fe4,
w: Wide8,
) {
val c = 4294968273L // 2^32 + 977
var hcLo: Long
@@ -402,77 +402,77 @@ internal object FieldP {
// Round 1: acc = lo + hi × C (4 limbs, unrolled)
// Limb 0 (no carry input)
hcLo = w[4] * c
hcHi = unsignedMultiplyHigh(w[4], c)
s1 = w[0] + hcLo
c1 = if (uLtInline(s1, w[0])) 1L else 0L
out[0] = s1
hcLo = w.l4 * c
hcHi = unsignedMultiplyHigh(w.l4, c)
s1 = w.l0 + hcLo
c1 = if (uLtInline(s1, w.l0)) 1L else 0L
out.l0 = s1
var carry = hcHi + c1
// Limb 1
hcLo = w[5] * c
hcHi = unsignedMultiplyHigh(w[5], c)
s1 = w[1] + hcLo
c1 = if (uLtInline(s1, w[1])) 1L else 0L
hcLo = w.l5 * c
hcHi = unsignedMultiplyHigh(w.l5, c)
s1 = w.l1 + hcLo
c1 = if (uLtInline(s1, w.l1)) 1L else 0L
s2 = s1 + carry
c2 = if (uLtInline(s2, s1)) 1L else 0L
out[1] = s2
out.l1 = s2
carry = hcHi + c1 + c2
// Limb 2
hcLo = w[6] * c
hcHi = unsignedMultiplyHigh(w[6], c)
s1 = w[2] + hcLo
c1 = if (uLtInline(s1, w[2])) 1L else 0L
hcLo = w.l6 * c
hcHi = unsignedMultiplyHigh(w.l6, c)
s1 = w.l2 + hcLo
c1 = if (uLtInline(s1, w.l2)) 1L else 0L
s2 = s1 + carry
c2 = if (uLtInline(s2, s1)) 1L else 0L
out[2] = s2
out.l2 = s2
carry = hcHi + c1 + c2
// Limb 3
hcLo = w[7] * c
hcHi = unsignedMultiplyHigh(w[7], c)
s1 = w[3] + hcLo
c1 = if (uLtInline(s1, w[3])) 1L else 0L
hcLo = w.l7 * c
hcHi = unsignedMultiplyHigh(w.l7, c)
s1 = w.l3 + hcLo
c1 = if (uLtInline(s1, w.l3)) 1L else 0L
s2 = s1 + carry
c2 = if (uLtInline(s2, s1)) 1L else 0L
out[3] = s2
out.l3 = s2
carry = hcHi + c1 + c2
// Round 2: if carry > 0, fold carry × C back in
if (carry != 0L) {
val ccLo = carry * c
val ccHi = unsignedMultiplyHigh(carry, c)
s1 = out[0] + ccLo
c1 = if (uLtInline(s1, out[0])) 1L else 0L
out[0] = s1
s1 = out.l0 + ccLo
c1 = if (uLtInline(s1, out.l0)) 1L else 0L
out.l0 = s1
// Propagate carry (unrolled, with early exit)
var prop = ccHi + c1
if (prop != 0L) {
s1 = out[1] + prop
prop = if (uLtInline(s1, out[1])) 1L else 0L
out[1] = s1
s1 = out.l1 + prop
prop = if (uLtInline(s1, out.l1)) 1L else 0L
out.l1 = s1
if (prop != 0L) {
s1 = out[2] + prop
prop = if (uLtInline(s1, out[2])) 1L else 0L
out[2] = s1
s1 = out.l2 + prop
prop = if (uLtInline(s1, out.l2)) 1L else 0L
out.l2 = s1
if (prop != 0L) {
s1 = out[3] + prop
prop = if (uLtInline(s1, out[3])) 1L else 0L
out[3] = s1
s1 = out.l3 + prop
prop = if (uLtInline(s1, out.l3)) 1L else 0L
out.l3 = s1
}
}
}
// Overflow past 256 bits: 2^256 ≡ C (mod p)
if (prop != 0L) {
s1 = out[0] + c
c1 = if (uLtInline(s1, out[0])) 1L else 0L
out[0] = s1
s1 = out.l0 + c
c1 = if (uLtInline(s1, out.l0)) 1L else 0L
out.l0 = s1
if (c1 != 0L) {
out[1]++
if (out[1] == 0L) {
out[2]++
if (out[2] == 0L) out[3]++
out.l1++
if (out.l1 == 0L) {
out.l2++
if (out.l2 == 0L) out.l3++
}
}
}
@@ -484,56 +484,56 @@ internal object FieldP {
// ==================== Convenience wrappers ====================
fun add(
a: LongArray,
b: LongArray,
): LongArray {
val r = LongArray(4)
a: Fe4,
b: Fe4,
): Fe4 {
val r = Fe4()
add(r, a, b)
return r
}
fun sub(
a: LongArray,
b: LongArray,
): LongArray {
val r = LongArray(4)
a: Fe4,
b: Fe4,
): Fe4 {
val r = Fe4()
sub(r, a, b)
return r
}
fun mul(
a: LongArray,
b: LongArray,
): LongArray {
val r = LongArray(4)
a: Fe4,
b: Fe4,
): Fe4 {
val r = Fe4()
mul(r, a, b)
return r
}
fun sqr(a: LongArray): LongArray {
val r = LongArray(4)
fun sqr(a: Fe4): Fe4 {
val r = Fe4()
sqr(r, a)
return r
}
fun neg(a: LongArray): LongArray {
val r = LongArray(4)
fun neg(a: Fe4): Fe4 {
val r = Fe4()
neg(r, a)
return r
}
fun inv(a: LongArray): LongArray {
val r = LongArray(4)
fun inv(a: Fe4): Fe4 {
val r = Fe4()
inv(r, a)
return r
}
fun sqrt(a: LongArray): LongArray? {
val r = LongArray(4)
fun sqrt(a: Fe4): Fe4? {
val r = Fe4()
return if (sqrt(r, a)) r else null
}
fun reduce(a: LongArray): LongArray {
fun reduce(a: Fe4): Fe4 {
val r = a.copyOf()
reduceSelf(r)
return r
@@ -43,7 +43,7 @@ internal object Glv {
/** β: cube root of unity mod p. φ(x,y) = (β·x, y). */
@JvmField
val BETA =
longArrayOf(
Fe4(
-4523465429756870162L,
-7138124642204153451L,
7954561588662645993L,
@@ -53,13 +53,13 @@ internal object Glv {
// ==================== GLV Scalar Decomposition ====================
class Split(
@JvmField val k1: LongArray,
@JvmField val k2: LongArray,
@JvmField val k1: Fe4,
@JvmField val k2: Fe4,
@JvmField val negK1: Boolean,
@JvmField val negK2: Boolean,
)
fun splitScalar(k: LongArray): Split {
fun splitScalar(k: Fe4): Split {
val c1 = mulShift384(k, G1)
val c2 = mulShift384(k, G2)
val r2 = ScalarN.add(ScalarN.mul(c1, MINUS_B1), ScalarN.mul(c2, MINUS_B2))
@@ -79,12 +79,12 @@ internal object Glv {
* Eliminates ~26 LongArray allocations per call (called 2× per verify = ~52 allocs saved).
*/
fun splitScalarInto(
outK1: LongArray,
outK2: LongArray,
k: LongArray,
w: LongArray,
t1: LongArray,
t2: LongArray,
outK1: Fe4,
outK2: Fe4,
k: Fe4,
w: Wide8,
t1: Fe4,
t2: Fe4,
): Split {
// c1 = mulShift384(k, G1)
mulShift384Into(t1, k, G1, w)
@@ -110,19 +110,19 @@ internal object Glv {
/** Allocation-free mulShift384. */
private fun mulShift384Into(
out: LongArray,
k: LongArray,
g: LongArray,
w: LongArray,
out: Fe4,
k: Fe4,
g: Fe4,
w: Wide8,
) {
U256.mulWide(w, k, g)
out[0] = w[6]
out[1] = w[7]
out[2] = 0L
out[3] = 0L
if (w[5] < 0) { // bit 63 of w[5] = bit 383 (rounding)
out[0]++
if (out[0] == 0L) out[1]++
out.l0 = w.l6
out.l1 = w.l7
out.l2 = 0L
out.l3 = 0L
if (w.l5 < 0) { // bit 63 of w.l5 = bit 383 (rounding)
out.l0++
if (out.l0 == 0L) out.l1++
}
}
@@ -135,13 +135,13 @@ internal object Glv {
* The working copy is extended to handle carries past maxBits.
*/
fun wnaf(
scalar: LongArray,
scalar: Fe4,
w: Int,
maxBits: Int,
): IntArray {
val totalBits = maxBits + w
val result = IntArray(totalBits)
val s = LongArray(maxOf((totalBits + 63) / 64, scalar.size))
val s = LongArray(maxOf((totalBits + 63) / 64, 4))
wnafInto(result, s, scalar, w, maxBits)
return result
}
@@ -153,7 +153,7 @@ internal object Glv {
fun wnafInto(
result: IntArray,
sTmp: LongArray,
scalar: LongArray,
scalar: Fe4,
w: Int,
maxBits: Int,
): Int {
@@ -161,7 +161,7 @@ internal object Glv {
// Clear output and copy scalar into scratch
for (i in 0 until totalBits.coerceAtMost(result.size)) result[i] = 0
for (i in sTmp.indices) sTmp[i] = 0
scalar.copyInto(sTmp)
scalar.copyIntoArray(sTmp)
var bit = 0
var highBit = 0
@@ -186,18 +186,18 @@ internal object Glv {
/** Multiply two 256-bit numbers, return result >> 384 (rounded). */
private fun mulShift384(
k: LongArray,
g: LongArray,
): LongArray {
val wide = LongArray(8)
k: Fe4,
g: Fe4,
): Fe4 {
val wide = Wide8()
U256.mulWide(wide, k, g)
val result = LongArray(4)
val result = Fe4()
// 384 bits = 6 Long limbs. Result = wide[6..7], round at bit 383 (wide[5] bit 63)
result[0] = wide[6]
result[1] = wide[7]
if (wide[5] < 0) { // bit 63 of wide[5] = bit 383
result[0]++
if (result[0] == 0L) result[1]++
result.l0 = wide.l6
result.l1 = wide.l7
if (wide.l5 < 0) { // bit 63 of wide.l5 = bit 383
result.l0++
if (result.l0 == 0L) result.l1++
}
return result
}
@@ -235,42 +235,42 @@ internal object Glv {
// ==================== Constants (from libsecp256k1) ====================
private val MINUS_LAMBDA =
longArrayOf(
Fe4(
-2247357714951666737L,
-6304834983940376126L,
6546514211138018212L,
-6008836872998760673L,
)
private val G1 =
longArrayOf(
Fe4(
-1687969588364726223L,
4443515802769476223L,
-1698823648040391915L,
3496713202691238861L,
)
private val G2 =
longArrayOf(
Fe4(
1545214808910233457L,
2455034284347819718L,
8022177200260244676L,
-1998614352016537560L,
)
private val MINUS_B1 =
longArrayOf(
Fe4(
8022177200260244675L,
-1998614352016537560L,
0L,
0L,
)
private val MINUS_B2 =
longArrayOf(
Fe4(
-2925706260434037204L,
-8491525256057179027L,
-2L,
-1L,
)
private val N_HALF =
longArrayOf(
Fe4(
-2312264954237214560L,
6725966010171805725L,
-1L,
@@ -39,47 +39,47 @@ package com.vitorpamplona.quartz.utils.secp256k1
*/
internal object KeyCodec {
/** Curve constant b = 7 in y² = x³ + 7. */
private val B = longArrayOf(7L, 0L, 0L, 0L)
private val B = Fe4(7L, 0L, 0L, 0L)
/**
* Lift an x-coordinate to a curve point with even y (BIP-340 convention).
* Computes y = √(x³ + 7) mod p. Returns false if x is not a valid coordinate.
*/
fun liftX(
outX: LongArray,
outY: LongArray,
x: LongArray,
outX: Fe4,
outY: Fe4,
x: Fe4,
): Boolean {
if (U256.cmp(x, FieldP.P) >= 0) return false
val t = LongArray(4)
val t = Fe4()
FieldP.sqr(t, x)
FieldP.mul(t, t, x)
FieldP.add(t, t, B) // t = x³ + 7
if (!FieldP.sqrt(outY, t)) return false
U256.copyInto(outX, x)
if (outY[0] and 1L != 0L) FieldP.neg(outY, outY) // Ensure even y
outX.copyFrom(x)
if (outY.l0 and 1L != 0L) FieldP.neg(outY, outY) // Ensure even y
return true
}
/** liftX with caller-provided temp buffer (avoids 1 LongArray alloc). */
/** liftX with caller-provided temp buffer (avoids 1 Fe4 alloc). */
fun liftX(
outX: LongArray,
outY: LongArray,
x: LongArray,
tmp: LongArray,
outX: Fe4,
outY: Fe4,
x: Fe4,
tmp: Fe4,
): Boolean {
if (U256.cmp(x, FieldP.P) >= 0) return false
FieldP.sqr(tmp, x)
FieldP.mul(tmp, tmp, x)
FieldP.add(tmp, tmp, B)
if (!FieldP.sqrt(outY, tmp)) return false
U256.copyInto(outX, x)
if (outY[0] and 1L != 0L) FieldP.neg(outY, outY)
outX.copyFrom(x)
if (outY.l0 and 1L != 0L) FieldP.neg(outY, outY)
return true
}
/** Check if y-coordinate is even (LSB = 0). */
fun hasEvenY(y: LongArray): Boolean = y[0] and 1L == 0L
fun hasEvenY(y: Fe4): Boolean = y.l0 and 1L == 0L
/**
* Parse a serialized public key (33 bytes compressed or 65 bytes uncompressed).
@@ -88,20 +88,20 @@ internal object KeyCodec {
*/
fun parsePublicKey(
pubkey: ByteArray,
outX: LongArray,
outY: LongArray,
outX: Fe4,
outY: Fe4,
): Boolean =
when (pubkey.size) {
33 if (pubkey[0] == 0x02.toByte() || pubkey[0] == 0x03.toByte()) -> {
val x = U256.fromBytes(pubkey.copyOfRange(1, 33))
if (U256.cmp(x, FieldP.P) >= 0) return false
val t = LongArray(4)
val t = Fe4()
FieldP.sqr(t, x)
FieldP.mul(t, t, x)
FieldP.add(t, t, B) // y² = x³ + 7
if (!FieldP.sqrt(outY, t)) return false
U256.copyInto(outX, x)
val isOdd = outY[0] and 1L == 1L
outX.copyFrom(x)
val isOdd = outY.l0 and 1L == 1L
if (isOdd != (pubkey[0] == 0x03.toByte())) FieldP.neg(outY, outY)
true
}
@@ -109,16 +109,16 @@ internal object KeyCodec {
65 if pubkey[0] == 0x04.toByte() -> {
val x = U256.fromBytes(pubkey.copyOfRange(1, 33))
val y = U256.fromBytes(pubkey.copyOfRange(33, 65))
val y2 = LongArray(4)
val x3p7 = LongArray(4)
val t = LongArray(4)
val y2 = Fe4()
val x3p7 = Fe4()
val t = Fe4()
FieldP.sqr(y2, y)
FieldP.sqr(t, x)
FieldP.mul(x3p7, t, x)
FieldP.add(x3p7, x3p7, B)
if (U256.cmp(y2, x3p7) != 0) return false
U256.copyInto(outX, x)
U256.copyInto(outY, y)
outX.copyFrom(x)
outY.copyFrom(y)
true
}
@@ -129,8 +129,8 @@ internal object KeyCodec {
/** Serialize as 65-byte uncompressed: 04 || x (32 bytes) || y (32 bytes). */
fun serializeUncompressed(
x: LongArray,
y: LongArray,
x: Fe4,
y: Fe4,
): ByteArray {
val r = ByteArray(65)
r[0] = 0x04
@@ -141,8 +141,8 @@ internal object KeyCodec {
/** Serialize as 33-byte compressed: 02/03 || x (32 bytes). */
fun serializeCompressed(
x: LongArray,
y: LongArray,
x: Fe4,
y: Fe4,
): ByteArray {
val r = ByteArray(33)
r[0] = if (hasEvenY(y)) 0x02 else 0x03
@@ -42,35 +42,37 @@ import kotlin.jvm.JvmField
* is ~3-4ns faster per access on ART. On non-JVM targets, @JvmField is ignored.
*/
internal class MutablePoint(
@JvmField val x: LongArray = LongArray(4),
@JvmField val y: LongArray = LongArray(4),
@JvmField val z: LongArray = LongArray(4),
@JvmField val x: Fe4 = Fe4(),
@JvmField val y: Fe4 = Fe4(),
@JvmField val z: Fe4 = Fe4(),
) {
fun isInfinity(): Boolean = (z[0] or z[1] or z[2] or z[3]) == 0L
fun isInfinity(): Boolean = z.isZero()
fun setInfinity() {
for (i in 0 until 4) {
x[i] = 0L
z[i] = 0L
}
y[0] = 1L
for (i in 1 until 4) y[i] = 0L
x.setZero()
y.l0 = 1L
y.l1 = 0L
y.l2 = 0L
y.l3 = 0L
z.setZero()
}
fun copyFrom(other: MutablePoint) {
other.x.copyInto(x, 0, 0, 4)
other.y.copyInto(y, 0, 0, 4)
other.z.copyInto(z, 0, 0, 4)
x.copyFrom(other.x)
y.copyFrom(other.y)
z.copyFrom(other.z)
}
fun setAffine(
ax: LongArray,
ay: LongArray,
ax: Fe4,
ay: Fe4,
) {
ax.copyInto(x, 0, 0, 4)
ay.copyInto(y, 0, 0, 4)
z[0] = 1L
for (i in 1 until 4) z[i] = 0L
x.copyFrom(ax)
y.copyFrom(ay)
z.l0 = 1L
z.l1 = 0L
z.l2 = 0L
z.l3 = 0L
}
}
@@ -80,8 +82,8 @@ internal class MutablePoint(
* @JvmField: see MutablePoint for rationale (eliminates virtual getter calls).
*/
internal class AffinePoint(
@JvmField val x: LongArray = LongArray(4),
@JvmField val y: LongArray = LongArray(4),
@JvmField val x: Fe4 = Fe4(),
@JvmField val y: Fe4 = Fe4(),
)
/**
@@ -94,7 +96,7 @@ internal class AffinePoint(
* degenerate case, which returns immediately after the recursive call without
* using the temps further.
*
* The wide buffer (LongArray(8)) is pre-fetched once per top-level operation and
* The wide buffer (Wide8) is pre-fetched once per top-level operation and
* passed through to FieldP.mul/sqr, avoiding ~500+ ThreadLocal.get() calls per
* scalar multiplication (~20-30ns each on JVM).
*
@@ -104,11 +106,11 @@ internal class AffinePoint(
* @JvmField compiles these to direct field reads. On non-JVM targets, ignored.
*/
internal class PointScratch {
@JvmField val t = Array(12) { LongArray(4) }
@JvmField val t = Array(12) { Fe4() }
@JvmField val dblCopy = MutablePoint()
@JvmField val w = LongArray(8)
@JvmField val w = Wide8()
@JvmField val wnaf1 = IntArray(145)
@@ -122,7 +124,7 @@ internal class PointScratch {
@JvmField val mixTmp = MutablePoint()
@JvmField val mixNegY = LongArray(4)
@JvmField val mixNegY = Fe4()
@JvmField val pOddJac = Array(8) { MutablePoint() }
@@ -134,43 +136,43 @@ internal class PointScratch {
@JvmField val p2 = MutablePoint()
@JvmField val cumZ = Array(8) { LongArray(4) }
@JvmField val cumZ = Array(8) { Fe4() }
@JvmField val batchInv = LongArray(4)
@JvmField val batchInv = Fe4()
@JvmField val batchZInv = LongArray(4)
@JvmField val batchZInv = Fe4()
@JvmField val batchZInv2 = LongArray(4)
@JvmField val batchZInv2 = Fe4()
@JvmField val batchZInv3 = LongArray(4)
@JvmField val batchZInv3 = Fe4()
@JvmField val splitWide = LongArray(8)
@JvmField val splitWide = Wide8()
@JvmField val splitT1 = LongArray(4)
@JvmField val splitT1 = Fe4()
@JvmField val splitT2 = LongArray(4)
@JvmField val splitT2 = Fe4()
@JvmField val splitK1 = LongArray(4)
@JvmField val splitK1 = Fe4()
@JvmField val splitK2 = LongArray(4)
@JvmField val splitK2 = Fe4()
@JvmField val zInv = LongArray(4)
@JvmField val zInv = Fe4()
@JvmField val zInv2 = LongArray(4)
@JvmField val zInv2 = Fe4()
@JvmField val zInv3 = LongArray(4)
@JvmField val zInv3 = Fe4()
@JvmField val entryPx = LongArray(4)
@JvmField val entryPx = Fe4()
@JvmField val entryPy = LongArray(4)
@JvmField val entryPy = Fe4()
@JvmField val entryPoint = MutablePoint()
@JvmField val entryResult = MutablePoint()
@JvmField val entryTmp = LongArray(4)
@JvmField val entryTmp = Fe4()
@JvmField val entryTmp2 = LongArray(4)
@JvmField val entryTmp2 = Fe4()
// Pre-allocated byte buffers for sign/verify (eliminates ByteArray allocations).
// hashBuf: reusable buffer for BIP-340 tagged hash inputs (prefix(64) + fields).
@@ -183,10 +185,10 @@ internal class PointScratch {
@JvmField val bytesTmp2 = ByteArray(32)
// Scratch LongArray(4) for intermediate scalar results (avoids ScalarN alloc)
@JvmField val scalarTmp1 = LongArray(4)
// Scratch Fe4 for intermediate scalar results (avoids ScalarN alloc)
@JvmField val scalarTmp1 = Fe4()
@JvmField val scalarTmp2 = LongArray(4)
@JvmField val scalarTmp2 = Fe4()
@JvmField val scalarTmp3 = LongArray(4)
@JvmField val scalarTmp3 = Fe4()
}
@@ -21,7 +21,7 @@
package com.vitorpamplona.quartz.utils.secp256k1
/**
* Arithmetic modulo the secp256k1 group order n using LongArray(4) limbs.
* Arithmetic modulo the secp256k1 group order n using Fe4 limbs.
*
* Provides both allocating (convenience) and in-place (hot-path) variants.
* The in-place variants write results to caller-provided output arrays, avoiding
@@ -29,7 +29,7 @@ package com.vitorpamplona.quartz.utils.secp256k1
*/
internal object ScalarN {
val N =
longArrayOf(
Fe4(
-4624529908474429119L,
-4994812053365940165L,
-2L,
@@ -37,7 +37,7 @@ internal object ScalarN {
)
private val N_COMPLEMENT =
longArrayOf(
Fe4(
4624529908474429119L,
4994812053365940164L,
1L,
@@ -45,18 +45,18 @@ internal object ScalarN {
)
private val N_MINUS_2 =
longArrayOf(
Fe4(
-4624529908474429121L,
-4994812053365940165L,
-2L,
-1L,
)
fun isValid(a: LongArray): Boolean = !U256.isZero(a) && U256.cmp(a, N) < 0
fun isValid(a: Fe4): Boolean = !a.isZero() && U256.cmp(a, N) < 0
fun reduce(a: LongArray): LongArray =
fun reduce(a: Fe4): Fe4 =
if (U256.cmp(a, N) >= 0) {
val r = LongArray(4)
val r = Fe4()
U256.subTo(r, a, N)
r
} else {
@@ -65,30 +65,30 @@ internal object ScalarN {
/** Allocation-free reduce: out = a mod n. Safe for out === a. */
fun reduceTo(
out: LongArray,
a: LongArray,
out: Fe4,
a: Fe4,
) {
if (U256.cmp(a, N) >= 0) {
U256.subTo(out, a, N)
} else if (out !== a) {
U256.copyInto(out, a)
out.copyFrom(a)
}
}
fun add(
a: LongArray,
b: LongArray,
): LongArray {
val r = LongArray(4)
a: Fe4,
b: Fe4,
): Fe4 {
val r = Fe4()
addTo(r, a, b)
return r
}
/** In-place add: out = (a + b) mod n. */
fun addTo(
out: LongArray,
a: LongArray,
b: LongArray,
out: Fe4,
a: Fe4,
b: Fe4,
) {
val carry = U256.addTo(out, a, b)
if (carry != 0) U256.addTo(out, out, N_COMPLEMENT)
@@ -96,60 +96,60 @@ internal object ScalarN {
}
fun sub(
a: LongArray,
b: LongArray,
): LongArray {
val r = LongArray(4)
a: Fe4,
b: Fe4,
): Fe4 {
val r = Fe4()
val borrow = U256.subTo(r, a, b)
if (borrow != 0) U256.addTo(r, r, N)
return r
}
fun mul(
a: LongArray,
b: LongArray,
): LongArray {
val w = LongArray(8)
a: Fe4,
b: Fe4,
): Fe4 {
val w = Wide8()
U256.mulWide(w, a, b)
return reduceWide(w)
}
/** In-place multiply: out = (a * b) mod n. Uses caller-provided wide buffer. */
fun mulTo(
out: LongArray,
a: LongArray,
b: LongArray,
w: LongArray,
out: Fe4,
a: Fe4,
b: Fe4,
w: Wide8,
) {
U256.mulWide(w, a, b)
reduceWideTo(out, w)
}
fun neg(a: LongArray): LongArray {
if (U256.isZero(a)) return LongArray(4)
val r = LongArray(4)
fun neg(a: Fe4): Fe4 {
if (a.isZero()) return Fe4()
val r = Fe4()
U256.subTo(r, N, a)
return r
}
/** In-place negate: out = (-a) mod n. Safe for out === a. */
fun negTo(
out: LongArray,
a: LongArray,
out: Fe4,
a: Fe4,
) {
if (U256.isZero(a)) {
for (i in 0 until 4) out[i] = 0L
if (a.isZero()) {
out.setZero()
} else {
U256.subTo(out, N, a)
}
}
fun inv(a: LongArray): LongArray {
require(!U256.isZero(a))
fun inv(a: Fe4): Fe4 {
require(!a.isZero())
return powModN(a, N_MINUS_2)
}
private fun reduceSelf(a: LongArray) {
private fun reduceSelf(a: Fe4) {
if (U256.cmp(a, N) >= 0) U256.subTo(a, a, N)
}
@@ -157,8 +157,8 @@ internal object ScalarN {
* Reduce 512-bit product mod n (allocating version).
* Uses hi × 2^256 ≡ hi × N_COMPLEMENT (mod n). N_COMPLEMENT is ~129 bits.
*/
private fun reduceWide(w: LongArray): LongArray {
val result = LongArray(4)
private fun reduceWide(w: Wide8): Fe4 {
val result = Fe4()
reduceWideTo(result, w)
return result
}
@@ -168,135 +168,210 @@ internal object ScalarN {
* Reuses the wide buffer w as scratch (caller must not need it after this call).
*/
private fun reduceWideTo(
out: LongArray,
w: LongArray,
out: Fe4,
w: Wide8,
) {
// Split into lo (w[0..3]) and hi (w[4..7])
val hasHi = w[4] != 0L || w[5] != 0L || w[6] != 0L || w[7] != 0L
// Split into lo (w.l0..l3) and hi (w.l4..l7)
val hasHi = w.l4 != 0L || w.l5 != 0L || w.l6 != 0L || w.l7 != 0L
if (!hasHi) {
for (i in 0 until 4) out[i] = w[i]
out.l0 = w.l0
out.l1 = w.l1
out.l2 = w.l2
out.l3 = w.l3
reduceSelf(out)
return
}
// Round 1: lo + hi × N_COMPLEMENT
// We reuse w[0..7] as scratch for hiTimesNC by saving lo first
val lo0 = w[0]
val lo1 = w[1]
val lo2 = w[2]
val lo3 = w[3]
// Use `out` as temporary storage for hi limbs (avoids longArrayOf allocation)
out[0] = w[4]
out[1] = w[5]
out[2] = w[6]
out[3] = w[7]
// We reuse w as scratch for hiTimesNC by saving lo first
val lo0 = w.l0
val lo1 = w.l1
val lo2 = w.l2
val lo3 = w.l3
// Use `out` as temporary storage for hi limbs (avoids allocation)
out.l0 = w.l4
out.l1 = w.l5
out.l2 = w.l6
out.l3 = w.l7
val hiTimesNC = w // reuse w as scratch
U256.mulWide(hiTimesNC, out, N_COMPLEMENT)
U256.mulWide(w, out, N_COMPLEMENT)
// sum = hiTimesNC + lo
var carry = 0L
for (i in 0 until 8) {
val loVal =
if (i == 0) {
lo0
} else if (i == 1) {
lo1
} else if (i == 2) {
lo2
} else if (i == 3) {
lo3
} else {
0L
}
val s1 = hiTimesNC[i] + loVal
val c1 = if (uLtInline(s1, hiTimesNC[i])) 1L else 0L
val s2 = s1 + carry
val c2 = if (uLtInline(s2, s1)) 1L else 0L
w[i] = s2
carry = c1 + c2
}
var s1 = w.l0 + lo0
var c1 = if (uLtInline(s1, w.l0)) 1L else 0L
var s2 = s1 + carry
var c2 = if (uLtInline(s2, s1)) 1L else 0L
w.l0 = s2
carry = c1 + c2
s1 = w.l1 + lo1
c1 = if (uLtInline(s1, w.l1)) 1L else 0L
s2 = s1 + carry
c2 = if (uLtInline(s2, s1)) 1L else 0L
w.l1 = s2
carry = c1 + c2
s1 = w.l2 + lo2
c1 = if (uLtInline(s1, w.l2)) 1L else 0L
s2 = s1 + carry
c2 = if (uLtInline(s2, s1)) 1L else 0L
w.l2 = s2
carry = c1 + c2
s1 = w.l3 + lo3
c1 = if (uLtInline(s1, w.l3)) 1L else 0L
s2 = s1 + carry
c2 = if (uLtInline(s2, s1)) 1L else 0L
w.l3 = s2
carry = c1 + c2
s1 = w.l4 + 0L
c1 = 0L
s2 = s1 + carry
c2 = if (uLtInline(s2, s1)) 1L else 0L
w.l4 = s2
carry = c1 + c2
s1 = w.l5 + 0L
c1 = 0L
s2 = s1 + carry
c2 = if (uLtInline(s2, s1)) 1L else 0L
w.l5 = s2
carry = c1 + c2
s1 = w.l6 + 0L
c1 = 0L
s2 = s1 + carry
c2 = if (uLtInline(s2, s1)) 1L else 0L
w.l6 = s2
carry = c1 + c2
s1 = w.l7 + 0L
c1 = 0L
s2 = s1 + carry
c2 = if (uLtInline(s2, s1)) 1L else 0L
w.l7 = s2
carry = c1 + c2
// Check if round 2 needed
val hasHi2 = w[4] != 0L || w[5] != 0L || w[6] != 0L || w[7] != 0L
val hasHi2 = w.l4 != 0L || w.l5 != 0L || w.l6 != 0L || w.l7 != 0L
if (!hasHi2) {
for (i in 0 until 4) out[i] = w[i]
out.l0 = w.l0
out.l1 = w.l1
out.l2 = w.l2
out.l3 = w.l3
reduceSelf(out)
return
}
// Round 2: reuse out for hi2 limbs (avoids longArrayOf allocation)
out[0] = w[4]
out[1] = w[5]
out[2] = w[6]
out[3] = w[7]
val saved0 = w[0]
val saved1 = w[1]
val saved2 = w[2]
val saved3 = w[3]
val hi2NC = w
U256.mulWide(hi2NC, out, N_COMPLEMENT)
// Round 2: reuse out for hi2 limbs (avoids allocation)
out.l0 = w.l4
out.l1 = w.l5
out.l2 = w.l6
out.l3 = w.l7
val saved0 = w.l0
val saved1 = w.l1
val saved2 = w.l2
val saved3 = w.l3
U256.mulWide(w, out, N_COMPLEMENT)
var c2 = 0L
for (i in 0 until 4) {
val loVal =
if (i == 0) {
saved0
} else if (i == 1) {
saved1
} else if (i == 2) {
saved2
} else {
saved3
}
val s1 = loVal + hi2NC[i]
val c1 = if (uLtInline(s1, loVal)) 1L else 0L
val s2 = s1 + c2
val cc = if (uLtInline(s2, s1)) 1L else 0L
out[i] = s2
c2 = c1 + cc
}
var ov = c2 + hi2NC[4]
for (i in 5 until 8) ov += hi2NC[i]
var c2r = 0L
var loVal = saved0
s1 = loVal + w.l0
c1 = if (uLtInline(s1, loVal)) 1L else 0L
s2 = s1 + c2r
var cc = if (uLtInline(s2, s1)) 1L else 0L
out.l0 = s2
c2r = c1 + cc
loVal = saved1
s1 = loVal + w.l1
c1 = if (uLtInline(s1, loVal)) 1L else 0L
s2 = s1 + c2r
cc = if (uLtInline(s2, s1)) 1L else 0L
out.l1 = s2
c2r = c1 + cc
loVal = saved2
s1 = loVal + w.l2
c1 = if (uLtInline(s1, loVal)) 1L else 0L
s2 = s1 + c2r
cc = if (uLtInline(s2, s1)) 1L else 0L
out.l2 = s2
c2r = c1 + cc
loVal = saved3
s1 = loVal + w.l3
c1 = if (uLtInline(s1, loVal)) 1L else 0L
s2 = s1 + c2r
cc = if (uLtInline(s2, s1)) 1L else 0L
out.l3 = s2
c2r = c1 + cc
var ov = c2r + w.l4
ov += w.l5
ov += w.l6
ov += w.l7
if (ov != 0L) {
val c0lo = ov * N_COMPLEMENT[0]
val c0hi = unsignedMultiplyHigh(ov, N_COMPLEMENT[0])
val c1lo = ov * N_COMPLEMENT[1]
val c1hi = unsignedMultiplyHigh(ov, N_COMPLEMENT[1])
val s0 = out[0] + c0lo
val carry0 = if (uLtInline(s0, out[0])) 1L else 0L
out[0] = s0
val s1 = out[1] + c0hi + c1lo + carry0
val carry1 = if (uLtInline(s1, out[1])) 1L else 0L
out[1] = s1
val s2 = out[2] + c1hi + ov + carry1
val carry2 = if (uLtInline(s2, out[2])) 1L else 0L
out[2] = s2
out[3] += carry2
val c0lo = ov * N_COMPLEMENT.l0
val c0hi = unsignedMultiplyHigh(ov, N_COMPLEMENT.l0)
val c1lo = ov * N_COMPLEMENT.l1
val c1hi = unsignedMultiplyHigh(ov, N_COMPLEMENT.l1)
val s0 = out.l0 + c0lo
val carry0 = if (uLtInline(s0, out.l0)) 1L else 0L
out.l0 = s0
val s1r = out.l1 + c0hi + c1lo + carry0
val carry1 = if (uLtInline(s1r, out.l1)) 1L else 0L
out.l1 = s1r
val s2r = out.l2 + c1hi + ov + carry1
val carry2 = if (uLtInline(s2r, out.l2)) 1L else 0L
out.l2 = s2r
out.l3 += carry2
}
while (U256.cmp(out, N) >= 0) U256.subTo(out, out, N)
}
/** Test bit at position pos in an Fe4. */
private fun testBit(
a: Fe4,
pos: Int,
): Boolean {
val limb = pos / 64
val shift = pos % 64
val v =
when (limb) {
0 -> a.l0
1 -> a.l1
2 -> a.l2
3 -> a.l3
else -> 0L
}
return (v ushr shift) and 1L == 1L
}
private fun powModN(
base: LongArray,
exp: LongArray,
): LongArray {
val result = LongArray(4)
base: Fe4,
exp: Fe4,
): Fe4 {
val result = Fe4()
val b = base.copyOf()
var highBit = 255
while (highBit >= 0 && !U256.testBit(exp, highBit)) highBit--
while (highBit >= 0 && !testBit(exp, highBit)) highBit--
if (highBit < 0) {
result[0] = 1L
result.l0 = 1L
return result
}
U256.copyInto(result, b)
result.copyFrom(b)
for (i in highBit - 1 downTo 0) {
val sq = mul(result, result)
U256.copyInto(result, sq)
if (U256.testBit(exp, i)) {
result.copyFrom(sq)
if (testBit(exp, i)) {
val prod = mul(result, b)
U256.copyInto(result, prod)
result.copyFrom(prod)
}
}
return result
@@ -84,8 +84,8 @@ object Secp256k1 {
private class CachedPubkey(
val keyBytes: ByteArray, // 32-byte x-only pubkey (for equality check)
val px: LongArray, // decompressed x (4 limbs)
val py: LongArray, // decompressed y (4 limbs)
val px: Fe4, // decompressed x (4 limbs)
val py: Fe4, // decompressed y (4 limbs)
)
private val pubkeyCache = arrayOfNulls<CachedPubkey>(PUBKEY_CACHE_SIZE)
@@ -96,8 +96,8 @@ object Secp256k1 {
* On cache miss, computes sqrt and stores the result.
*/
private fun liftXCached(
outX: LongArray,
outY: LongArray,
outX: Fe4,
outY: Fe4,
pub: ByteArray,
): Boolean {
// Hash the pubkey bytes to a cache slot (use first 4 bytes as index)
@@ -110,8 +110,8 @@ object Secp256k1 {
val cached = pubkeyCache[slot]
if (cached != null && cached.keyBytes.contentEquals(pub)) {
// Cache hit — copy pre-computed coordinates
cached.px.copyInto(outX, 0, 0, 4)
cached.py.copyInto(outY, 0, 0, 4)
outX.copyFrom(cached.px)
outY.copyFrom(cached.py)
return true
}
@@ -312,14 +312,14 @@ object Secp256k1 {
*/
private fun signSchnorrInternal(
data: ByteArray,
d0: LongArray,
d0: Fe4,
pBytes: ByteArray,
pubKeyHasEvenY: Boolean,
auxrand: ByteArray?,
): ByteArray {
val sc = ECPoint.getScratch()
val d =
val d: Fe4 =
if (pubKeyHasEvenY) {
d0
} else {
@@ -358,7 +358,7 @@ object Secp256k1 {
U256.fromBytesInto(sc.scalarTmp1, sc.bytesTmp2, 0)
ScalarN.reduceTo(sc.scalarTmp1, sc.scalarTmp1)
val k0 = sc.scalarTmp1
require(!U256.isZero(k0))
require(!k0.isZero())
// R = k0·G
ECPoint.mulG(sc.entryResult, k0, sc)
@@ -366,7 +366,7 @@ object Secp256k1 {
val ry = sc.entryPy
check(ECPoint.toAffine(sc.entryResult, rx, ry, sc))
val k =
val k: Fe4 =
if (KeyCodec.hasEvenY(ry)) {
k0
} else {
@@ -525,7 +525,7 @@ object Secp256k1 {
tweak: ByteArray,
): ByteArray {
require(seckey.size == 32 && tweak.size == 32)
// Use thread-local scratch to avoid 2 intermediate LongArray(4) allocations.
// Use thread-local scratch to avoid 2 intermediate Fe4() allocations.
// Old path: fromBytes (alloc) + fromBytes (alloc) + add (alloc) + toBytes (alloc) = 4 allocs
// New path: fromBytesInto (scratch) + fromBytesInto (scratch) + addTo (scratch) + toBytes = 1 alloc
val sc = ECPoint.getScratch()
@@ -535,7 +535,7 @@ object Secp256k1 {
U256.fromBytesInto(a, seckey, 0)
U256.fromBytesInto(b, tweak, 0)
ScalarN.addTo(r, a, b)
require(!U256.isZero(r) && U256.cmp(r, ScalarN.N) < 0)
require(!r.isZero() && U256.cmp(r, ScalarN.N) < 0)
return U256.toBytes(r)
}
@@ -657,8 +657,8 @@ object Secp256k1 {
if (!liftXCached(px, py, pub)) return false
// Accumulators for the scalar sums
val sSum = LongArray(4) // Σ sᵢ mod n
val eSum = LongArray(4) // Σ eᵢ mod n
val sSum = Fe4() // Σ sᵢ mod n
val eSum = Fe4() // Σ eᵢ mod n
// Accumulator for R point sum (Jacobian)
val rSum = MutablePoint()
@@ -24,8 +24,8 @@ package com.vitorpamplona.quartz.utils.secp256k1
// 256-BIT UNSIGNED INTEGER ARITHMETIC FOR secp256k1 (4×64-bit limbs)
// =====================================================================================
//
// Numbers are stored as LongArray(4) in little-endian order. Each Long holds 64 bits
// treated as unsigned. Element [0] is the least significant limb.
// Numbers are stored as Fe4 (4 named Long fields) in little-endian order. Each Long
// holds 64 bits treated as unsigned. Field l0 is the least significant limb.
//
// This representation uses Math.multiplyHigh (JVM 9+) or a pure-Kotlin fallback to
// compute the upper 64 bits of 64×64→128-bit products. On JVM, this maps to a single
@@ -97,26 +97,25 @@ internal inline fun uLtInline(
* Raw 256-bit unsigned integer arithmetic using 4×64-bit limbs.
*/
internal object U256 {
fun isZero(a: LongArray): Boolean = (a[0] or a[1] or a[2] or a[3]) == 0L
fun isZero(a: Fe4): Boolean = a.isZero()
/** Unsigned comparison. Returns -1 if a < b, 0 if equal, 1 if a > b. */
fun cmp(
a: LongArray,
b: LongArray,
a: Fe4,
b: Fe4,
): Int {
for (i in 3 downTo 0) {
if (a[i] != b[i]) {
return if (uLtInline(a[i], b[i])) -1 else 1
}
}
if (a.l3 != b.l3) return if (uLtInline(a.l3, b.l3)) -1 else 1
if (a.l2 != b.l2) return if (uLtInline(a.l2, b.l2)) -1 else 1
if (a.l1 != b.l1) return if (uLtInline(a.l1, b.l1)) -1 else 1
if (a.l0 != b.l0) return if (uLtInline(a.l0, b.l0)) -1 else 1
return 0
}
/** out = a + b. Returns carry (0 or 1). Safe for aliasing. Unrolled for ART JIT. */
fun addTo(
out: LongArray,
a: LongArray,
b: LongArray,
out: Fe4,
a: Fe4,
b: Fe4,
): Int {
var s1: Long
var s2: Long
@@ -124,33 +123,33 @@ internal object U256 {
var c2: Long
// Limb 0 (no carry input)
s1 = a[0] + b[0]
c1 = if (uLtInline(s1, a[0])) 1L else 0L
out[0] = s1
s1 = a.l0 + b.l0
c1 = if (uLtInline(s1, a.l0)) 1L else 0L
out.l0 = s1
var carry = c1
// Limb 1
s1 = a[1] + b[1]
c1 = if (uLtInline(s1, a[1])) 1L else 0L
s1 = a.l1 + b.l1
c1 = if (uLtInline(s1, a.l1)) 1L else 0L
s2 = s1 + carry
c2 = if (uLtInline(s2, s1)) 1L else 0L
out[1] = s2
out.l1 = s2
carry = c1 + c2
// Limb 2
s1 = a[2] + b[2]
c1 = if (uLtInline(s1, a[2])) 1L else 0L
s1 = a.l2 + b.l2
c1 = if (uLtInline(s1, a.l2)) 1L else 0L
s2 = s1 + carry
c2 = if (uLtInline(s2, s1)) 1L else 0L
out[2] = s2
out.l2 = s2
carry = c1 + c2
// Limb 3
s1 = a[3] + b[3]
c1 = if (uLtInline(s1, a[3])) 1L else 0L
s1 = a.l3 + b.l3
c1 = if (uLtInline(s1, a.l3)) 1L else 0L
s2 = s1 + carry
c2 = if (uLtInline(s2, s1)) 1L else 0L
out[3] = s2
out.l3 = s2
carry = c1 + c2
return carry.toInt()
@@ -158,9 +157,9 @@ internal object U256 {
/** out = a - b. Returns borrow (0 or 1). Safe for aliasing. Unrolled for ART JIT. */
fun subTo(
out: LongArray,
a: LongArray,
b: LongArray,
out: Fe4,
a: Fe4,
b: Fe4,
): Int {
var d1: Long
var d2: Long
@@ -168,58 +167,58 @@ internal object U256 {
var c2: Long
// Limb 0 (no borrow input)
d1 = a[0] - b[0]
c1 = if (uLtInline(a[0], b[0])) 1L else 0L
out[0] = d1
d1 = a.l0 - b.l0
c1 = if (uLtInline(a.l0, b.l0)) 1L else 0L
out.l0 = d1
var borrow = c1
// Limb 1
d1 = a[1] - b[1]
c1 = if (uLtInline(a[1], b[1])) 1L else 0L
d1 = a.l1 - b.l1
c1 = if (uLtInline(a.l1, b.l1)) 1L else 0L
d2 = d1 - borrow
c2 = if (uLtInline(d1, borrow)) 1L else 0L
out[1] = d2
out.l1 = d2
borrow = c1 + c2
// Limb 2
d1 = a[2] - b[2]
c1 = if (uLtInline(a[2], b[2])) 1L else 0L
d1 = a.l2 - b.l2
c1 = if (uLtInline(a.l2, b.l2)) 1L else 0L
d2 = d1 - borrow
c2 = if (uLtInline(d1, borrow)) 1L else 0L
out[2] = d2
out.l2 = d2
borrow = c1 + c2
// Limb 3
d1 = a[3] - b[3]
c1 = if (uLtInline(a[3], b[3])) 1L else 0L
d1 = a.l3 - b.l3
c1 = if (uLtInline(a.l3, b.l3)) 1L else 0L
d2 = d1 - borrow
c2 = if (uLtInline(d1, borrow)) 1L else 0L
out[3] = d2
out.l3 = d2
borrow = c1 + c2
return borrow.toInt()
}
/**
* 4×4 schoolbook multiplication: out = a × b (512-bit result in LongArray(8)).
* 4×4 schoolbook multiplication: out = a × b (512-bit result in Wide8).
*
* Fully unrolled: all 16 products are explicit, eliminating loop control overhead
* and array bounds checks. This significantly helps ART JIT on Android, which is
* less aggressive at loop optimization than HotSpot. Called ~1,900× per verify.
*/
fun mulWide(
out: LongArray,
a: LongArray,
b: LongArray,
out: Wide8,
a: Fe4,
b: Fe4,
) {
val a0 = a[0]
val a1 = a[1]
val a2 = a[2]
val a3 = a[3]
val b0 = b[0]
val b1 = b[1]
val b2 = b[2]
val b3 = b[3]
val a0 = a.l0
val a1 = a.l1
val a2 = a.l2
val a3 = a.l3
val b0 = b.l0
val b1 = b.l1
val b2 = b.l2
val b3 = b.l3
var lo: Long
var hi: Long
var prev: Long
@@ -230,158 +229,158 @@ internal object U256 {
// Row 0: a0 × [b0,b1,b2,b3] → out[0..4] (out starts empty, no prev accumulation)
lo = a0 * b0
out[0] = lo
out.l0 = lo
carry = unsignedMultiplyHigh(a0, b0)
lo = a0 * b1
s = lo + carry
c1 = if (uLtInline(s, lo)) 1L else 0L
out[1] = s
out.l1 = s
carry = unsignedMultiplyHigh(a0, b1) + c1
lo = a0 * b2
s = lo + carry
c1 = if (uLtInline(s, lo)) 1L else 0L
out[2] = s
out.l2 = s
carry = unsignedMultiplyHigh(a0, b2) + c1
lo = a0 * b3
s = lo + carry
c1 = if (uLtInline(s, lo)) 1L else 0L
out[3] = s
out[4] = unsignedMultiplyHigh(a0, b3) + c1
out.l3 = s
out.l4 = unsignedMultiplyHigh(a0, b3) + c1
// Row 1: a1 × [b0,b1,b2,b3] accumulated into out[1..5]
lo = a1 * b0
hi = unsignedMultiplyHigh(a1, b0)
prev = out[1]
prev = out.l1
s = prev + lo
c1 = if (uLtInline(s, prev)) 1L else 0L
out[1] = s
out.l1 = s
carry = hi + c1
lo = a1 * b1
hi = unsignedMultiplyHigh(a1, b1)
prev = out[2]
prev = out.l2
s = prev + lo
c1 = if (uLtInline(s, prev)) 1L else 0L
s += carry
c2 = if (uLtInline(s, carry)) 1L else 0L
out[2] = s
out.l2 = s
carry = hi + c1 + c2
lo = a1 * b2
hi = unsignedMultiplyHigh(a1, b2)
prev = out[3]
prev = out.l3
s = prev + lo
c1 = if (uLtInline(s, prev)) 1L else 0L
s += carry
c2 = if (uLtInline(s, carry)) 1L else 0L
out[3] = s
out.l3 = s
carry = hi + c1 + c2
lo = a1 * b3
hi = unsignedMultiplyHigh(a1, b3)
prev = out[4]
prev = out.l4
s = prev + lo
c1 = if (uLtInline(s, prev)) 1L else 0L
s += carry
c2 = if (uLtInline(s, carry)) 1L else 0L
out[4] = s
out[5] = hi + c1 + c2
out.l4 = s
out.l5 = hi + c1 + c2
// Row 2: a2 × [b0,b1,b2,b3] accumulated into out[2..6]
lo = a2 * b0
hi = unsignedMultiplyHigh(a2, b0)
prev = out[2]
prev = out.l2
s = prev + lo
c1 = if (uLtInline(s, prev)) 1L else 0L
out[2] = s
out.l2 = s
carry = hi + c1
lo = a2 * b1
hi = unsignedMultiplyHigh(a2, b1)
prev = out[3]
prev = out.l3
s = prev + lo
c1 = if (uLtInline(s, prev)) 1L else 0L
s += carry
c2 = if (uLtInline(s, carry)) 1L else 0L
out[3] = s
out.l3 = s
carry = hi + c1 + c2
lo = a2 * b2
hi = unsignedMultiplyHigh(a2, b2)
prev = out[4]
prev = out.l4
s = prev + lo
c1 = if (uLtInline(s, prev)) 1L else 0L
s += carry
c2 = if (uLtInline(s, carry)) 1L else 0L
out[4] = s
out.l4 = s
carry = hi + c1 + c2
lo = a2 * b3
hi = unsignedMultiplyHigh(a2, b3)
prev = out[5]
prev = out.l5
s = prev + lo
c1 = if (uLtInline(s, prev)) 1L else 0L
s += carry
c2 = if (uLtInline(s, carry)) 1L else 0L
out[5] = s
out[6] = hi + c1 + c2
out.l5 = s
out.l6 = hi + c1 + c2
// Row 3: a3 × [b0,b1,b2,b3] accumulated into out[3..7]
lo = a3 * b0
hi = unsignedMultiplyHigh(a3, b0)
prev = out[3]
prev = out.l3
s = prev + lo
c1 = if (uLtInline(s, prev)) 1L else 0L
out[3] = s
out.l3 = s
carry = hi + c1
lo = a3 * b1
hi = unsignedMultiplyHigh(a3, b1)
prev = out[4]
prev = out.l4
s = prev + lo
c1 = if (uLtInline(s, prev)) 1L else 0L
s += carry
c2 = if (uLtInline(s, carry)) 1L else 0L
out[4] = s
out.l4 = s
carry = hi + c1 + c2
lo = a3 * b2
hi = unsignedMultiplyHigh(a3, b2)
prev = out[5]
prev = out.l5
s = prev + lo
c1 = if (uLtInline(s, prev)) 1L else 0L
s += carry
c2 = if (uLtInline(s, carry)) 1L else 0L
out[5] = s
out.l5 = s
carry = hi + c1 + c2
lo = a3 * b3
hi = unsignedMultiplyHigh(a3, b3)
prev = out[6]
prev = out.l6
s = prev + lo
c1 = if (uLtInline(s, prev)) 1L else 0L
s += carry
c2 = if (uLtInline(s, carry)) 1L else 0L
out[6] = s
out[7] = hi + c1 + c2
out.l6 = s
out.l7 = hi + c1 + c2
}
/**
* Dedicated squaring: out = a² (512-bit result in LongArray(8)).
* Dedicated squaring: out = a² (512-bit result in Wide8).
* Exploits symmetry: 6 cross-products doubled + 4 diagonal = 10 multiplyHigh calls.
* Fully unrolled for ART JIT optimization.
*/
fun sqrWide(
out: LongArray,
a: LongArray,
out: Wide8,
a: Fe4,
) {
val a0 = a[0]
val a1 = a[1]
val a2 = a[2]
val a3 = a[3]
val a0 = a.l0
val a1 = a.l1
val a2 = a.l2
val a3 = a.l3
var lo: Long
var hi: Long
var prev: Long
@@ -394,214 +393,299 @@ internal object U256 {
// Pass 1: cross-products a[i]*a[j] for i < j (single, before doubling)
// Row i=0: a0 × [a1, a2, a3] → out[1..4]
out[0] = 0L
out.l0 = 0L
lo = a0 * a1
out[1] = lo
out.l1 = lo
carry = unsignedMultiplyHigh(a0, a1)
lo = a0 * a2
s = lo + carry
c1 = if (uLtInline(s, lo)) 1L else 0L
out[2] = s
out.l2 = s
carry = unsignedMultiplyHigh(a0, a2) + c1
lo = a0 * a3
s = lo + carry
c1 = if (uLtInline(s, lo)) 1L else 0L
out[3] = s
out[4] = unsignedMultiplyHigh(a0, a3) + c1
out.l3 = s
out.l4 = unsignedMultiplyHigh(a0, a3) + c1
// Row i=1: a1 × [a2, a3] → accumulated into out[3..5]
lo = a1 * a2
hi = unsignedMultiplyHigh(a1, a2)
prev = out[3]
prev = out.l3
s = prev + lo
c1 = if (uLtInline(s, prev)) 1L else 0L
out[3] = s
out.l3 = s
carry = hi + c1
lo = a1 * a3
hi = unsignedMultiplyHigh(a1, a3)
prev = out[4]
prev = out.l4
s = prev + lo
c1 = if (uLtInline(s, prev)) 1L else 0L
s += carry
c2 = if (uLtInline(s, carry)) 1L else 0L
out[4] = s
out[5] = hi + c1 + c2
out.l4 = s
out.l5 = hi + c1 + c2
// Row i=2: a2 × [a3] → accumulated into out[5..6]
lo = a2 * a3
hi = unsignedMultiplyHigh(a2, a3)
prev = out[5]
prev = out.l5
s = prev + lo
c1 = if (uLtInline(s, prev)) 1L else 0L
out[5] = s
out[6] = hi + c1
out.l5 = s
out.l6 = hi + c1
// Pass 2: double all cross-products (shift left by 1 bit)
v = out[1]
out[1] = v shl 1
v = out.l1
out.l1 = v shl 1
var shiftCarry = v ushr 63
v = out[2]
out[2] = (v shl 1) or shiftCarry
v = out.l2
out.l2 = (v shl 1) or shiftCarry
shiftCarry = v ushr 63
v = out[3]
out[3] = (v shl 1) or shiftCarry
v = out.l3
out.l3 = (v shl 1) or shiftCarry
shiftCarry = v ushr 63
v = out[4]
out[4] = (v shl 1) or shiftCarry
v = out.l4
out.l4 = (v shl 1) or shiftCarry
shiftCarry = v ushr 63
v = out[5]
out[5] = (v shl 1) or shiftCarry
v = out.l5
out.l5 = (v shl 1) or shiftCarry
shiftCarry = v ushr 63
v = out[6]
out[6] = (v shl 1) or shiftCarry
v = out.l6
out.l6 = (v shl 1) or shiftCarry
shiftCarry = v ushr 63
out[7] = shiftCarry
out.l7 = shiftCarry
// Pass 3: add diagonal products a[i]²
// i=0: a0², pos=0
lo = a0 * a0
hi = unsignedMultiplyHigh(a0, a0)
out[0] = lo // out[0] was 0
s = out[1] + hi
c1 = if (uLtInline(s, out[1])) 1L else 0L
out[1] = s
out.l0 = lo // out.l0 was 0
s = out.l1 + hi
c1 = if (uLtInline(s, out.l1)) 1L else 0L
out.l1 = s
var dCarry = c1
// i=1: a1², pos=2
lo = a1 * a1
hi = unsignedMultiplyHigh(a1, a1)
s = out[2] + lo
c1 = if (uLtInline(s, out[2])) 1L else 0L
s = out.l2 + lo
c1 = if (uLtInline(s, out.l2)) 1L else 0L
s += dCarry
c2 = if (uLtInline(s, dCarry)) 1L else 0L
out[2] = s
prev = out[3] + hi
val c3a = if (uLtInline(prev, out[3])) 1L else 0L
out.l2 = s
prev = out.l3 + hi
val c3a = if (uLtInline(prev, out.l3)) 1L else 0L
prev += c1 + c2
val c4a = if (uLtInline(prev, c1 + c2)) 1L else 0L
out[3] = prev
out.l3 = prev
dCarry = c3a + c4a
// i=2: a2², pos=4
lo = a2 * a2
hi = unsignedMultiplyHigh(a2, a2)
s = out[4] + lo
c1 = if (uLtInline(s, out[4])) 1L else 0L
s = out.l4 + lo
c1 = if (uLtInline(s, out.l4)) 1L else 0L
s += dCarry
c2 = if (uLtInline(s, dCarry)) 1L else 0L
out[4] = s
prev = out[5] + hi
val c3b = if (uLtInline(prev, out[5])) 1L else 0L
out.l4 = s
prev = out.l5 + hi
val c3b = if (uLtInline(prev, out.l5)) 1L else 0L
prev += c1 + c2
val c4b = if (uLtInline(prev, c1 + c2)) 1L else 0L
out[5] = prev
out.l5 = prev
dCarry = c3b + c4b
// i=3: a3², pos=6
lo = a3 * a3
hi = unsignedMultiplyHigh(a3, a3)
s = out[6] + lo
c1 = if (uLtInline(s, out[6])) 1L else 0L
s = out.l6 + lo
c1 = if (uLtInline(s, out.l6)) 1L else 0L
s += dCarry
c2 = if (uLtInline(s, dCarry)) 1L else 0L
out[6] = s
prev = out[7] + hi
val c3c = if (uLtInline(prev, out[7])) 1L else 0L
out.l6 = s
prev = out.l7 + hi
val c3c = if (uLtInline(prev, out.l7)) 1L else 0L
prev += c1 + c2
out[7] = prev
out.l7 = prev
}
// ==================== Serialization ====================
/** Decode big-endian 32 bytes into LongArray(4) little-endian limbs. */
fun fromBytes(bytes: ByteArray): LongArray = fromBytes(bytes, 0)
/** Decode big-endian 32 bytes into Fe4 little-endian limbs. */
fun fromBytes(bytes: ByteArray): Fe4 = fromBytes(bytes, 0)
fun fromBytes(
bytes: ByteArray,
offset: Int,
): LongArray {
val r = LongArray(4)
): Fe4 {
val r = Fe4()
fromBytesInto(r, bytes, offset)
return r
}
/** Decode big-endian 32 bytes into a pre-allocated LongArray(4). */
/** Decode big-endian 32 bytes into a pre-allocated Fe4. */
fun fromBytesInto(
out: LongArray,
out: Fe4,
bytes: ByteArray,
offset: Int,
) {
for (i in 0 until 4) {
val o = offset + 24 - i * 8
out[i] = ((bytes[o].toLong() and 0xFF) shl 56) or
((bytes[o + 1].toLong() and 0xFF) shl 48) or
((bytes[o + 2].toLong() and 0xFF) shl 40) or
((bytes[o + 3].toLong() and 0xFF) shl 32) or
((bytes[o + 4].toLong() and 0xFF) shl 24) or
((bytes[o + 5].toLong() and 0xFF) shl 16) or
((bytes[o + 6].toLong() and 0xFF) shl 8) or
(bytes[o + 7].toLong() and 0xFF)
}
var o: Int
o = offset + 24
out.l0 = ((bytes[o].toLong() and 0xFF) shl 56) or
((bytes[o + 1].toLong() and 0xFF) shl 48) or
((bytes[o + 2].toLong() and 0xFF) shl 40) or
((bytes[o + 3].toLong() and 0xFF) shl 32) or
((bytes[o + 4].toLong() and 0xFF) shl 24) or
((bytes[o + 5].toLong() and 0xFF) shl 16) or
((bytes[o + 6].toLong() and 0xFF) shl 8) or
(bytes[o + 7].toLong() and 0xFF)
o = offset + 16
out.l1 = ((bytes[o].toLong() and 0xFF) shl 56) or
((bytes[o + 1].toLong() and 0xFF) shl 48) or
((bytes[o + 2].toLong() and 0xFF) shl 40) or
((bytes[o + 3].toLong() and 0xFF) shl 32) or
((bytes[o + 4].toLong() and 0xFF) shl 24) or
((bytes[o + 5].toLong() and 0xFF) shl 16) or
((bytes[o + 6].toLong() and 0xFF) shl 8) or
(bytes[o + 7].toLong() and 0xFF)
o = offset + 8
out.l2 = ((bytes[o].toLong() and 0xFF) shl 56) or
((bytes[o + 1].toLong() and 0xFF) shl 48) or
((bytes[o + 2].toLong() and 0xFF) shl 40) or
((bytes[o + 3].toLong() and 0xFF) shl 32) or
((bytes[o + 4].toLong() and 0xFF) shl 24) or
((bytes[o + 5].toLong() and 0xFF) shl 16) or
((bytes[o + 6].toLong() and 0xFF) shl 8) or
(bytes[o + 7].toLong() and 0xFF)
o = offset
out.l3 = ((bytes[o].toLong() and 0xFF) shl 56) or
((bytes[o + 1].toLong() and 0xFF) shl 48) or
((bytes[o + 2].toLong() and 0xFF) shl 40) or
((bytes[o + 3].toLong() and 0xFF) shl 32) or
((bytes[o + 4].toLong() and 0xFF) shl 24) or
((bytes[o + 5].toLong() and 0xFF) shl 16) or
((bytes[o + 6].toLong() and 0xFF) shl 8) or
(bytes[o + 7].toLong() and 0xFF)
}
fun toBytes(a: LongArray): ByteArray {
fun toBytes(a: Fe4): ByteArray {
val r = ByteArray(32)
toBytesInto(a, r, 0)
return r
}
fun toBytesInto(
a: LongArray,
a: Fe4,
dest: ByteArray,
offset: Int,
) {
for (i in 0 until 4) {
val o = offset + 24 - i * 8
dest[o] = (a[i] ushr 56).toByte()
dest[o + 1] = (a[i] ushr 48).toByte()
dest[o + 2] = (a[i] ushr 40).toByte()
dest[o + 3] = (a[i] ushr 32).toByte()
dest[o + 4] = (a[i] ushr 24).toByte()
dest[o + 5] = (a[i] ushr 16).toByte()
dest[o + 6] = (a[i] ushr 8).toByte()
dest[o + 7] = a[i].toByte()
}
var o: Int
// Limb 0 (least significant) → bytes at offset+24..offset+31
o = offset + 24
dest[o] = (a.l0 ushr 56).toByte()
dest[o + 1] = (a.l0 ushr 48).toByte()
dest[o + 2] = (a.l0 ushr 40).toByte()
dest[o + 3] = (a.l0 ushr 32).toByte()
dest[o + 4] = (a.l0 ushr 24).toByte()
dest[o + 5] = (a.l0 ushr 16).toByte()
dest[o + 6] = (a.l0 ushr 8).toByte()
dest[o + 7] = a.l0.toByte()
// Limb 1 → bytes at offset+16..offset+23
o = offset + 16
dest[o] = (a.l1 ushr 56).toByte()
dest[o + 1] = (a.l1 ushr 48).toByte()
dest[o + 2] = (a.l1 ushr 40).toByte()
dest[o + 3] = (a.l1 ushr 32).toByte()
dest[o + 4] = (a.l1 ushr 24).toByte()
dest[o + 5] = (a.l1 ushr 16).toByte()
dest[o + 6] = (a.l1 ushr 8).toByte()
dest[o + 7] = a.l1.toByte()
// Limb 2 → bytes at offset+8..offset+15
o = offset + 8
dest[o] = (a.l2 ushr 56).toByte()
dest[o + 1] = (a.l2 ushr 48).toByte()
dest[o + 2] = (a.l2 ushr 40).toByte()
dest[o + 3] = (a.l2 ushr 32).toByte()
dest[o + 4] = (a.l2 ushr 24).toByte()
dest[o + 5] = (a.l2 ushr 16).toByte()
dest[o + 6] = (a.l2 ushr 8).toByte()
dest[o + 7] = a.l2.toByte()
// Limb 3 (most significant) → bytes at offset+0..offset+7
o = offset
dest[o] = (a.l3 ushr 56).toByte()
dest[o + 1] = (a.l3 ushr 48).toByte()
dest[o + 2] = (a.l3 ushr 40).toByte()
dest[o + 3] = (a.l3 ushr 32).toByte()
dest[o + 4] = (a.l3 ushr 24).toByte()
dest[o + 5] = (a.l3 ushr 16).toByte()
dest[o + 6] = (a.l3 ushr 8).toByte()
dest[o + 7] = a.l3.toByte()
}
// ==================== Bit manipulation ====================
/** Extract 4-bit nibble at position pos (0 = lowest nibble). */
fun getNibble(
a: LongArray,
a: Fe4,
pos: Int,
): Int {
val limb = pos / 16
val shift = (pos % 16) * 4
return ((a[limb] ushr shift) and 0xF).toInt()
val v =
when (limb) {
0 -> a.l0
1 -> a.l1
2 -> a.l2
else -> a.l3
}
return ((v ushr shift) and 0xF).toInt()
}
/** Test if bit at position pos is set. Called ~2,800× per mulG (comb table lookup). */
fun testBit(
a: LongArray,
a: Fe4,
pos: Int,
): Boolean = (a[pos / 64] ushr (pos % 64)) and 1L == 1L
): Boolean {
val limb = pos / 64
val shift = pos % 64
val v =
when (limb) {
0 -> a.l0
1 -> a.l1
2 -> a.l2
else -> a.l3
}
return (v ushr shift) and 1L == 1L
}
fun xorTo(
out: LongArray,
a: LongArray,
b: LongArray,
out: Fe4,
a: Fe4,
b: Fe4,
) {
for (i in 0 until 4) out[i] = a[i] xor b[i]
out.l0 = a.l0 xor b.l0
out.l1 = a.l1 xor b.l1
out.l2 = a.l2 xor b.l2
out.l3 = a.l3 xor b.l3
}
fun copyInto(
out: LongArray,
a: LongArray,
out: Fe4,
a: Fe4,
) {
a.copyInto(out, 0, 0, 4)
out.copyFrom(a)
}
}
@@ -31,14 +31,14 @@ import kotlin.test.assertTrue
class FieldPTest {
private fun hex(s: String) = U256.fromBytes(s.hexToByteArray())
private fun toHex(a: LongArray) = U256.toBytes(a).toHexKey()
private fun toHex(a: Fe4) = U256.toBytes(a).toHexKey()
// ==================== Basic identities ====================
@Test
fun addZeroIdentity() {
val a = hex("67e56582298859ddae725f972992a07c6c4fb9f62a8fff58ce3ca926a1063530")
val zero = LongArray(4)
val zero = Fe4()
assertEquals(toHex(a), toHex(FieldP.add(a, zero)))
}
@@ -46,7 +46,7 @@ class FieldPTest {
fun subSelfIsZero() {
val a = hex("67e56582298859ddae725f972992a07c6c4fb9f62a8fff58ce3ca926a1063530")
val result = FieldP.sub(a, a)
assertTrue(U256.isZero(result))
assertTrue(result.isZero())
}
@Test
@@ -61,7 +61,7 @@ class FieldPTest {
@Test
fun mulOneIdentity() {
val a = hex("67e56582298859ddae725f972992a07c6c4fb9f62a8fff58ce3ca926a1063530")
val one = longArrayOf(1L, 0L, 0L, 0L)
val one = Fe4(1L, 0L, 0L, 0L)
assertEquals(toHex(a), toHex(FieldP.mul(a, one)))
}
@@ -71,9 +71,9 @@ class FieldPTest {
fun addNearP() {
// (p - 1) + 1 = p ≡ 0 (mod p)
val pMinus1 = hex("fffffffffffffffffffffffffffffffffffffffffffffffffffffffefffffc2e")
val one = longArrayOf(1L, 0L, 0L, 0L)
val one = Fe4(1L, 0L, 0L, 0L)
val result = FieldP.add(pMinus1, one)
assertTrue(U256.isZero(result))
assertTrue(result.isZero())
}
@Test
@@ -88,8 +88,8 @@ class FieldPTest {
@Test
fun subUnderflow() {
// 0 - 1 ≡ p - 1 (mod p)
val zero = LongArray(4)
val one = longArrayOf(1L, 0L, 0L, 0L)
val zero = Fe4()
val one = Fe4(1L, 0L, 0L, 0L)
val result = FieldP.sub(zero, one)
val expected = hex("fffffffffffffffffffffffffffffffffffffffffffffffffffffffefffffc2e") // p-1
assertEquals(toHex(expected), toHex(result))
@@ -105,14 +105,14 @@ class FieldPTest {
@Test
fun negZeroIsZero() {
assertTrue(U256.isZero(FieldP.neg(LongArray(4))))
assertTrue(U256.isZero(FieldP.neg(Fe4())))
}
@Test
fun addNegIsZero() {
val a = hex("67e56582298859ddae725f972992a07c6c4fb9f62a8fff58ce3ca926a1063530")
val result = FieldP.add(a, FieldP.neg(a))
assertTrue(U256.isZero(result))
assertTrue(result.isZero())
}
// ==================== Multiplication ====================
@@ -148,13 +148,13 @@ class FieldPTest {
val a = hex("67e56582298859ddae725f972992a07c6c4fb9f62a8fff58ce3ca926a1063530")
val aInv = FieldP.inv(a)
val product = FieldP.mul(a, aInv)
val one = longArrayOf(1L, 0L, 0L, 0L)
val one = Fe4(1L, 0L, 0L, 0L)
assertEquals(toHex(one), toHex(product))
}
@Test
fun invOfOne() {
val one = longArrayOf(1L, 0L, 0L, 0L)
val one = Fe4(1L, 0L, 0L, 0L)
assertEquals(toHex(one), toHex(FieldP.inv(one)))
}
@@ -169,29 +169,33 @@ class FieldPTest {
@Test
fun halfOfEven() {
val out = LongArray(4)
val four = longArrayOf(4L, 0L, 0L, 0L)
val out = Fe4()
val four = Fe4(4L, 0L, 0L, 0L)
FieldP.half(out, four)
assertEquals(2L, out[0])
for (i in 1 until 4) assertEquals(0L, out[i])
assertEquals(2L, out.l0)
assertEquals(0L, out.l1)
assertEquals(0L, out.l2)
assertEquals(0L, out.l3)
}
@Test
fun halfOfOdd() {
// half(1) = (1 + p) / 2 = (p + 1) / 2
val out = LongArray(4)
val one = longArrayOf(1L, 0L, 0L, 0L)
val out = Fe4()
val one = Fe4(1L, 0L, 0L, 0L)
FieldP.half(out, one)
// Verify: 2 * half(1) = 1 mod p
val doubled = FieldP.add(out, out)
assertEquals(1L, doubled[0])
for (i in 1 until 4) assertEquals(0L, doubled[i])
assertEquals(1L, doubled.l0)
assertEquals(0L, doubled.l1)
assertEquals(0L, doubled.l2)
assertEquals(0L, doubled.l3)
}
@Test
fun halfThenDoubleRoundTrips() {
val a = hex("67e56582298859ddae725f972992a07c6c4fb9f62a8fff58ce3ca926a1063530")
val out = LongArray(4)
val out = Fe4()
FieldP.half(out, a)
val doubled = FieldP.add(out, out)
assertEquals(toHex(a), toHex(doubled))
@@ -212,7 +216,7 @@ class FieldPTest {
@Test
fun sqrtOfNonResidue() {
// 3 is not a quadratic residue mod p (for secp256k1's p)
val three = longArrayOf(3, 0L, 0L, 0L)
val three = Fe4(3, 0L, 0L, 0L)
assertNull(FieldP.sqrt(three))
}
@@ -222,7 +226,7 @@ class FieldPTest {
val gx = ECPoint.GX
val gy = ECPoint.GY
val x3 = FieldP.mul(FieldP.sqr(gx), gx)
val y2 = FieldP.add(x3, longArrayOf(7, 0L, 0L, 0L))
val y2 = FieldP.add(x3, Fe4(7, 0L, 0L, 0L))
val root = FieldP.sqrt(y2)!!
// root should be gy or -gy
val isGy = U256.cmp(root, gy) == 0
@@ -239,7 +243,7 @@ class FieldPTest {
val result = FieldP.mul(pMinus1, pMinus1)
assertTrue(U256.cmp(result, FieldP.P) < 0, "Result should be < p")
// (p-1)² ≡ 1 (mod p)
val one = longArrayOf(1L, 0L, 0L, 0L)
val one = Fe4(1L, 0L, 0L, 0L)
assertEquals(toHex(one), toHex(result))
}
@@ -249,24 +253,24 @@ class FieldPTest {
fun inPlaceAdd() {
val a = hex("0000000000000000000000000000000000000000000000000000000000000005")
val b = hex("0000000000000000000000000000000000000000000000000000000000000003")
val out = LongArray(4)
val out = Fe4()
FieldP.add(out, a, b)
assertEquals(8L, out[0])
assertEquals(8L, out.l0)
}
@Test
fun inPlaceSqr() {
val a = hex("0000000000000000000000000000000000000000000000000000000000000005")
val out = LongArray(4)
val out = Fe4()
FieldP.sqr(out, a)
assertEquals(25L, out[0]) // 5² = 25
assertEquals(25L, out.l0) // 5² = 25
}
@Test
fun halfOfPMinus1() {
// half(p-1) should equal (p-1)/2
val pMinus1 = hex("fffffffffffffffffffffffffffffffffffffffffffffffffffffffefffffc2e")
val out = LongArray(4)
val out = Fe4()
FieldP.half(out, pMinus1)
// Verify: 2 * half(p-1) = p-1
val doubled = FieldP.add(out, out)
@@ -275,23 +279,23 @@ class FieldPTest {
@Test
fun invOfTwo() {
val two = longArrayOf(2, 0L, 0L, 0L)
val two = Fe4(2, 0L, 0L, 0L)
val inv2 = FieldP.inv(two)
val product = FieldP.mul(two, inv2)
val one = longArrayOf(1L, 0L, 0L, 0L)
val one = Fe4(1L, 0L, 0L, 0L)
assertEquals(toHex(one), toHex(product))
}
@Test
fun sqrtOfZero() {
val zero = LongArray(4)
val zero = Fe4()
val root = FieldP.sqrt(zero)
assertTrue(root != null && U256.isZero(root))
assertTrue(root != null && root.isZero())
}
@Test
fun sqrtOfOne() {
val one = longArrayOf(1L, 0L, 0L, 0L)
val one = Fe4(1L, 0L, 0L, 0L)
val root = FieldP.sqrt(one)!!
assertEquals(toHex(one), toHex(root))
}
@@ -28,7 +28,7 @@ import kotlin.test.assertTrue
/** Comprehensive tests for GLV endomorphism and wNAF encoding. */
class GlvTest {
private fun toHex(a: LongArray) = U256.toBytes(a).toHexKey()
private fun toHex(a: Fe4) = U256.toBytes(a).toHexKey()
private fun hex(s: String) = U256.fromBytes(s.hexToByteArray())
@@ -49,7 +49,7 @@ class GlvTest {
@Test
fun splitScalarZero() {
val split = Glv.splitScalar(LongArray(4))
val split = Glv.splitScalar(Fe4())
assertTrue(U256.isZero(split.k1) && U256.isZero(split.k2))
}
@@ -68,11 +68,11 @@ class GlvTest {
// Both k₁ and k₂ should be ~128 bits (fit in 4 limbs)
val k = hex("67e56582298859ddae725f972992a07c6c4fb9f62a8fff58ce3ca926a1063530")
val split = Glv.splitScalar(k)
// Upper 4 limbs should be zero for a proper 128-bit half-scalar
for (i in 2 until 4) {
assertEquals(0, split.k1[i], "k1 limb $i should be 0")
assertEquals(0, split.k2[i], "k2 limb $i should be 0")
}
// Upper 2 limbs should be zero for a proper 128-bit half-scalar
assertEquals(0L, split.k1.l2, "k1 limb 2 should be 0")
assertEquals(0L, split.k1.l3, "k1 limb 3 should be 0")
assertEquals(0L, split.k2.l2, "k2 limb 2 should be 0")
assertEquals(0L, split.k2.l3, "k2 limb 3 should be 0")
}
@Test
@@ -105,7 +105,7 @@ class GlvTest {
// β³ ≡ 1 (mod p) — the defining property of the cube root of unity
val b2 = FieldP.sqr(Glv.BETA)
val b3 = FieldP.mul(b2, Glv.BETA)
val one = longArrayOf(1L, 0L, 0L, 0L, 0, 0, 0, 0)
val one = Fe4(1L, 0L, 0L, 0L)
assertEquals(toHex(one), toHex(b3))
}
@@ -114,8 +114,8 @@ class GlvTest {
// λ·G should equal (β·Gx, Gy)
val result = MutablePoint()
ECPoint.mulG(result, LAMBDA)
val rx = LongArray(4)
val ry = LongArray(4)
val rx = Fe4()
val ry = Fe4()
ECPoint.toAffine(result, rx, ry)
assertEquals(toHex(FieldP.mul(ECPoint.GX, Glv.BETA)), toHex(rx))
assertEquals(toHex(ECPoint.GY), toHex(ry))
@@ -126,9 +126,10 @@ class GlvTest {
@Test
fun wnafReconstructionSmall() {
// wNAF digits should reconstruct to the original scalar
val k = longArrayOf(17L, 0L, 0L, 0L) // 17 = 10001 in binary
val k = Fe4(17L, 0L, 0L, 0L) // 17 = 10001 in binary
val digits = Glv.wnaf(k, 5, 256)
assertEquals(k[0], reconstructWnaf(digits)[0])
val reconstructed = reconstructWnaf(digits)
assertEquals(0, U256.cmp(k, reconstructed))
}
@Test
@@ -136,7 +137,7 @@ class GlvTest {
val k = hex("e907831f80848d1069a5371b402410364bdf1c5f8307b0084c55f1ce2dca8215")
val digits = Glv.wnaf(k, 5, 256)
val reconstructed = reconstructWnaf(digits)
for (i in 0 until 4) assertEquals(k[i], reconstructed[i], "Limb $i mismatch")
assertEquals(0, U256.cmp(k, reconstructed), "wNAF reconstruction mismatch")
}
@Test
@@ -146,7 +147,7 @@ class GlvTest {
val k = hex("944946c56e0d133f326db0b0645544a04bfcc5a0f447ad6d0227958414d8ba73")
val digits = Glv.wnaf(k, 5, 256)
val reconstructed = reconstructWnaf(digits)
for (i in 0 until 4) assertEquals(k[i], reconstructed[i], "Limb $i mismatch for carry test")
assertEquals(0, U256.cmp(k, reconstructed), "wNAF carry overflow reconstruction mismatch")
}
@Test
@@ -179,10 +180,10 @@ class GlvTest {
@Test
fun wnafSmallMaxBits() {
// wNAF with maxBits=129 (used for GLV half-scalars)
val k = longArrayOf(-7296712173568108936L, 2459565876494606609L, 0L, 0L)
val k = Fe4(-7296712173568108936L, 2459565876494606609L, 0L, 0L)
val digits = Glv.wnaf(k, 5, 129)
val reconstructed = reconstructWnaf(digits)
for (i in 0 until 4) assertEquals(k[i], reconstructed[i], "Limb $i mismatch for 129-bit wNAF")
assertEquals(0, U256.cmp(k, reconstructed), "129-bit wNAF reconstruction mismatch")
}
// ==================== Integration: GLV mulDoubleG ====================
@@ -192,16 +193,16 @@ class GlvTest {
// s·G + 0·P = s·G
val s = hex("67e56582298859ddae725f972992a07c6c4fb9f62a8fff58ce3ca926a1063530")
val p = MutablePoint()
ECPoint.mulG(p, longArrayOf(2L, 0L, 0L, 0L, 0, 0, 0, 0))
ECPoint.mulG(p, Fe4(2L, 0L, 0L, 0L))
val combined = MutablePoint()
ECPoint.mulDoubleG(combined, s, p, LongArray(4))
val cx = LongArray(4)
val cy = LongArray(4)
ECPoint.mulDoubleG(combined, s, p, Fe4())
val cx = Fe4()
val cy = Fe4()
ECPoint.toAffine(combined, cx, cy)
val direct = MutablePoint()
ECPoint.mulG(direct, s)
val dx = LongArray(4)
val dy = LongArray(4)
val dx = Fe4()
val dy = Fe4()
ECPoint.toAffine(direct, dx, dy)
assertEquals(toHex(dx), toHex(cx))
}
@@ -209,37 +210,38 @@ class GlvTest {
// ==================== Helpers ====================
/** Reconstruct a scalar from wNAF digits using Horner's method. */
private fun reconstructWnaf(digits: IntArray): LongArray {
var acc = LongArray(4)
private fun reconstructWnaf(digits: IntArray): Fe4 {
var acc = Fe4()
for (bit in digits.size - 1 downTo 0) {
// Double: acc = acc * 2 (unsigned shift left by 1)
val doubled = LongArray(4)
var shiftCarry = 0L
for (j in 0 until 4) {
doubled[j] = (acc[j] shl 1) or shiftCarry
shiftCarry = acc[j] ushr 63
}
val doubled = Fe4()
doubled.l0 = (acc.l0 shl 1)
doubled.l1 = (acc.l1 shl 1) or (acc.l0 ushr 63)
doubled.l2 = (acc.l2 shl 1) or (acc.l1 ushr 63)
doubled.l3 = (acc.l3 shl 1) or (acc.l2 ushr 63)
acc = doubled
// Add digit
val d = digits[bit]
if (d > 0) {
val s = acc[0] + d.toLong()
val c = if (s.toULong() < acc[0].toULong()) 1L else 0L
acc[0] = s
val s = acc.l0 + d.toLong()
val c = if (s.toULong() < acc.l0.toULong()) 1L else 0L
acc.l0 = s
if (c != 0L) {
for (j in 1 until 4) {
acc[j]++
if (acc[j] != 0L) break
acc.l1++
if (acc.l1 == 0L) {
acc.l2++
if (acc.l2 == 0L) acc.l3++
}
}
} else if (d < 0) {
val s = acc[0] - (-d).toLong()
val b = if (acc[0].toULong() < (-d).toULong()) 1L else 0L
acc[0] = s
val s = acc.l0 - (-d).toLong()
val b = if (acc.l0.toULong() < (-d).toULong()) 1L else 0L
acc.l0 = s
if (b != 0L) {
for (j in 1 until 4) {
acc[j]--
if (acc[j] != -1L) break
acc.l1--
if (acc.l1 == -1L) {
acc.l2--
if (acc.l2 == -1L) acc.l3--
}
}
}
@@ -28,14 +28,14 @@ import kotlin.test.assertTrue
/** Tests for KeyCodec: key parsing, serialization, liftX, hasEvenY. */
class KeyCodecTest {
private fun toHex(a: LongArray) = U256.toBytes(a).toHexKey()
private fun toHex(a: Fe4) = U256.toBytes(a).toHexKey()
// ==================== liftX ====================
@Test
fun liftXGenerator() {
val x = LongArray(4)
val y = LongArray(4)
val x = Fe4()
val y = Fe4()
assertTrue(KeyCodec.liftX(x, y, ECPoint.GX))
assertEquals(toHex(ECPoint.GX), toHex(x))
assertTrue(KeyCodec.hasEvenY(y))
@@ -44,25 +44,25 @@ class KeyCodecTest {
@Test
fun liftXInvalidFieldElement() {
// p itself is not a valid x
val x = LongArray(4)
val y = LongArray(4)
val x = Fe4()
val y = Fe4()
assertFalse(KeyCodec.liftX(x, y, FieldP.P))
}
@Test
fun liftXNotOnCurve() {
// x=2: y² = 8+7 = 15. 15 is not a quadratic residue mod p.
val x = LongArray(4)
val y = LongArray(4)
val two = longArrayOf(2, 0L, 0L, 0L)
val x = Fe4()
val y = Fe4()
val two = Fe4(2, 0L, 0L, 0L)
// This may or may not be on the curve — just check it doesn't crash
KeyCodec.liftX(x, y, two) // result doesn't matter, just no exception
}
@Test
fun liftXAlwaysReturnsEvenY() {
val x = LongArray(4)
val y = LongArray(4)
val x = Fe4()
val y = Fe4()
assertTrue(KeyCodec.liftX(x, y, ECPoint.GX))
assertTrue(KeyCodec.hasEvenY(y), "liftX should always return even y")
}
@@ -71,14 +71,14 @@ class KeyCodecTest {
@Test
fun hasEvenYForEvenValue() {
assertTrue(KeyCodec.hasEvenY(longArrayOf(2, 0L, 0L, 0L)))
assertTrue(KeyCodec.hasEvenY(longArrayOf(0, 0L, 0L, 0L)))
assertTrue(KeyCodec.hasEvenY(Fe4(2, 0L, 0L, 0L)))
assertTrue(KeyCodec.hasEvenY(Fe4(0, 0L, 0L, 0L)))
}
@Test
fun hasEvenYForOddValue() {
assertFalse(KeyCodec.hasEvenY(longArrayOf(1, 0L, 0L, 0L)))
assertFalse(KeyCodec.hasEvenY(longArrayOf(3, 0L, 0L, 0L)))
assertFalse(KeyCodec.hasEvenY(Fe4(1, 0L, 0L, 0L)))
assertFalse(KeyCodec.hasEvenY(Fe4(3, 0L, 0L, 0L)))
}
// ==================== parsePublicKey ====================
@@ -87,8 +87,8 @@ class KeyCodecTest {
fun parseCompressedEvenY() {
val compressed = KeyCodec.serializeCompressed(ECPoint.GX, ECPoint.GY)
assertEquals(0x02.toByte(), compressed[0])
val x = LongArray(4)
val y = LongArray(4)
val x = Fe4()
val y = Fe4()
assertTrue(KeyCodec.parsePublicKey(compressed, x, y))
assertEquals(toHex(ECPoint.GX), toHex(x))
assertEquals(toHex(ECPoint.GY), toHex(y))
@@ -99,8 +99,8 @@ class KeyCodecTest {
val negGy = FieldP.neg(ECPoint.GY)
val compressed = KeyCodec.serializeCompressed(ECPoint.GX, negGy)
assertEquals(0x03.toByte(), compressed[0])
val x = LongArray(4)
val y = LongArray(4)
val x = Fe4()
val y = Fe4()
assertTrue(KeyCodec.parsePublicKey(compressed, x, y))
assertEquals(toHex(ECPoint.GX), toHex(x))
assertEquals(toHex(negGy), toHex(y))
@@ -110,8 +110,8 @@ class KeyCodecTest {
fun parseUncompressed() {
val uncompressed = KeyCodec.serializeUncompressed(ECPoint.GX, ECPoint.GY)
assertEquals(0x04.toByte(), uncompressed[0])
val x = LongArray(4)
val y = LongArray(4)
val x = Fe4()
val y = Fe4()
assertTrue(KeyCodec.parsePublicKey(uncompressed, x, y))
assertEquals(toHex(ECPoint.GX), toHex(x))
assertEquals(toHex(ECPoint.GY), toHex(y))
@@ -119,8 +119,8 @@ class KeyCodecTest {
@Test
fun parseInvalidSizes() {
val x = LongArray(4)
val y = LongArray(4)
val x = Fe4()
val y = Fe4()
assertFalse(KeyCodec.parsePublicKey(ByteArray(0), x, y))
assertFalse(KeyCodec.parsePublicKey(ByteArray(10), x, y))
assertFalse(KeyCodec.parsePublicKey(ByteArray(32), x, y))
@@ -131,8 +131,8 @@ class KeyCodecTest {
@Test
fun parseInvalidPrefix() {
val x = LongArray(4)
val y = LongArray(4)
val x = Fe4()
val y = Fe4()
assertFalse(KeyCodec.parsePublicKey(ByteArray(33), x, y)) // prefix 0x00
assertFalse(KeyCodec.parsePublicKey(ByteArray(65), x, y)) // prefix 0x00
}
@@ -144,8 +144,8 @@ class KeyCodecTest {
fake[0] = 0x04
fake[1] = 0x01 // x = 1 (padded)
fake[33] = 0x01 // y = 1 (padded) — 1² ≠ 1³ + 7
val x = LongArray(4)
val y = LongArray(4)
val x = Fe4()
val y = Fe4()
assertFalse(KeyCodec.parsePublicKey(fake, x, y))
}
@@ -154,8 +154,8 @@ class KeyCodecTest {
@Test
fun compressDecompressRoundTrip() {
val compressed = KeyCodec.serializeCompressed(ECPoint.GX, ECPoint.GY)
val x = LongArray(4)
val y = LongArray(4)
val x = Fe4()
val y = Fe4()
assertTrue(KeyCodec.parsePublicKey(compressed, x, y))
val recompressed = KeyCodec.serializeCompressed(x, y)
assertEquals(compressed.toList(), recompressed.toList())
@@ -164,8 +164,8 @@ class KeyCodecTest {
@Test
fun uncompressedRoundTrip() {
val uncompressed = KeyCodec.serializeUncompressed(ECPoint.GX, ECPoint.GY)
val x = LongArray(4)
val y = LongArray(4)
val x = Fe4()
val y = Fe4()
assertTrue(KeyCodec.parsePublicKey(uncompressed, x, y))
val reser = KeyCodec.serializeUncompressed(x, y)
assertEquals(uncompressed.toList(), reser.toList())
@@ -31,7 +31,7 @@ import kotlin.test.assertTrue
class PointTest {
private fun hex(s: String) = U256.fromBytes(s.hexToByteArray())
private fun toHex(a: LongArray) = U256.toBytes(a).toHexKey()
private fun toHex(a: Fe4) = U256.toBytes(a).toHexKey()
// ==================== Generator point ====================
@@ -39,7 +39,7 @@ class PointTest {
fun generatorIsOnCurve() {
// y² = x³ + 7
val x3 = FieldP.mul(FieldP.sqr(ECPoint.GX), ECPoint.GX)
val y2expected = FieldP.add(x3, longArrayOf(7, 0L, 0L, 0L))
val y2expected = FieldP.add(x3, Fe4(7, 0L, 0L, 0L))
val y2actual = FieldP.sqr(ECPoint.GY)
assertEquals(toHex(y2expected), toHex(y2actual))
}
@@ -53,16 +53,16 @@ class PointTest {
p.setAffine(ECPoint.GX, ECPoint.GY)
val doubled = MutablePoint()
ECPoint.doublePoint(doubled, p)
val dx = LongArray(4)
val dy = LongArray(4)
val dx = Fe4()
val dy = Fe4()
ECPoint.toAffine(doubled, dx, dy)
// 2·G via scalar multiplication
val two = longArrayOf(2, 0L, 0L, 0L)
val two = Fe4(2, 0L, 0L, 0L)
val mulResult = MutablePoint()
ECPoint.mulG(mulResult, two)
val mx = LongArray(4)
val my = LongArray(4)
val mx = Fe4()
val my = Fe4()
ECPoint.toAffine(mulResult, mx, my)
assertEquals(toHex(mx), toHex(dx))
@@ -75,15 +75,15 @@ class PointTest {
val p = MutablePoint()
p.setAffine(ECPoint.GX, ECPoint.GY)
ECPoint.doublePoint(p, p)
val x = LongArray(4)
val y = LongArray(4)
val x = Fe4()
val y = Fe4()
ECPoint.toAffine(p, x, y)
val two = longArrayOf(2, 0L, 0L, 0L)
val two = Fe4(2, 0L, 0L, 0L)
val expected = MutablePoint()
ECPoint.mulG(expected, two)
val ex = LongArray(4)
val ey = LongArray(4)
val ex = Fe4()
val ey = Fe4()
ECPoint.toAffine(expected, ex, ey)
assertEquals(toHex(ex), toHex(x))
@@ -106,14 +106,14 @@ class PointTest {
g.setAffine(ECPoint.GX, ECPoint.GY)
val sum = MutablePoint()
ECPoint.addPoints(sum, g, g)
val sx = LongArray(4)
val sy = LongArray(4)
val sx = Fe4()
val sy = Fe4()
ECPoint.toAffine(sum, sx, sy)
val doubled = MutablePoint()
ECPoint.doublePoint(doubled, g)
val dx = LongArray(4)
val dy = LongArray(4)
val dx = Fe4()
val dy = Fe4()
ECPoint.toAffine(doubled, dx, dy)
assertEquals(toHex(dx), toHex(sx))
@@ -129,15 +129,15 @@ class PointTest {
val result = MutablePoint()
ECPoint.addPoints(result, g, inf)
val rx = LongArray(4)
val ry = LongArray(4)
val rx = Fe4()
val ry = Fe4()
ECPoint.toAffine(result, rx, ry)
assertEquals(toHex(ECPoint.GX), toHex(rx))
val result2 = MutablePoint()
ECPoint.addPoints(result2, inf, g)
val r2x = LongArray(4)
val r2y = LongArray(4)
val r2x = Fe4()
val r2y = Fe4()
ECPoint.toAffine(result2, r2x, r2y)
assertEquals(toHex(ECPoint.GX), toHex(r2x))
}
@@ -160,15 +160,15 @@ class PointTest {
@Test
fun addMixedMatchesFull() {
// addMixed should produce the same result as addPoints when q is affine
val three = longArrayOf(3, 0L, 0L, 0L)
val three = Fe4(3, 0L, 0L, 0L)
val p = MutablePoint()
ECPoint.mulG(p, three) // 3G in Jacobian (z ≠ 1)
// Add G as affine
val mixed = MutablePoint()
ECPoint.addMixed(mixed, p, ECPoint.GX, ECPoint.GY)
val mx = LongArray(4)
val my = LongArray(4)
val mx = Fe4()
val my = Fe4()
ECPoint.toAffine(mixed, mx, my)
// Add G as Jacobian
@@ -176,8 +176,8 @@ class PointTest {
gJac.setAffine(ECPoint.GX, ECPoint.GY)
val full = MutablePoint()
ECPoint.addPoints(full, p, gJac)
val fx = LongArray(4)
val fy = LongArray(4)
val fx = Fe4()
val fy = Fe4()
ECPoint.toAffine(full, fx, fy)
assertEquals(toHex(fx), toHex(mx))
@@ -190,8 +190,8 @@ class PointTest {
inf.setInfinity()
val result = MutablePoint()
ECPoint.addMixed(result, inf, ECPoint.GX, ECPoint.GY)
val rx = LongArray(4)
val ry = LongArray(4)
val rx = Fe4()
val ry = Fe4()
ECPoint.toAffine(result, rx, ry)
assertEquals(toHex(ECPoint.GX), toHex(rx))
}
@@ -200,11 +200,11 @@ class PointTest {
@Test
fun mulGByOne() {
val one = longArrayOf(1, 0L, 0L, 0L)
val one = Fe4(1, 0L, 0L, 0L)
val result = MutablePoint()
ECPoint.mulG(result, one)
val rx = LongArray(4)
val ry = LongArray(4)
val rx = Fe4()
val ry = Fe4()
ECPoint.toAffine(result, rx, ry)
assertEquals(toHex(ECPoint.GX), toHex(rx))
assertEquals(toHex(ECPoint.GY), toHex(ry))
@@ -212,7 +212,7 @@ class PointTest {
@Test
fun mulGByZeroIsInfinity() {
val zero = LongArray(4)
val zero = Fe4()
val result = MutablePoint()
ECPoint.mulG(result, zero)
assertTrue(result.isInfinity())
@@ -232,16 +232,16 @@ class PointTest {
val k = hex("67e56582298859ddae725f972992a07c6c4fb9f62a8fff58ce3ca926a1063530")
val gResult = MutablePoint()
ECPoint.mulG(gResult, k)
val gx = LongArray(4)
val gy = LongArray(4)
val gx = Fe4()
val gy = Fe4()
ECPoint.toAffine(gResult, gx, gy)
val g = MutablePoint()
g.setAffine(ECPoint.GX, ECPoint.GY)
val mResult = MutablePoint()
ECPoint.mul(mResult, g, k)
val mx = LongArray(4)
val my = LongArray(4)
val mx = Fe4()
val my = Fe4()
ECPoint.toAffine(mResult, mx, my)
assertEquals(toHex(mx), toHex(gx))
@@ -255,14 +255,14 @@ class PointTest {
val e = hex("3982f19bef1615bccfbb05e321c10e1d4cba3df0e841c2e41eeb6016347653c3")
val p = MutablePoint()
val two = longArrayOf(2, 0L, 0L, 0L)
val two = Fe4(2, 0L, 0L, 0L)
ECPoint.mulG(p, two) // P = 2·G
// Combined
val combined = MutablePoint()
ECPoint.mulDoubleG(combined, s, p, e)
val cx = LongArray(4)
val cy = LongArray(4)
val cx = Fe4()
val cy = Fe4()
ECPoint.toAffine(combined, cx, cy)
// Separate
@@ -272,8 +272,8 @@ class PointTest {
ECPoint.mul(eP, p, e)
val sep = MutablePoint()
ECPoint.addPoints(sep, sG, eP)
val sx = LongArray(4)
val sy = LongArray(4)
val sx = Fe4()
val sy = Fe4()
ECPoint.toAffine(sep, sx, sy)
assertEquals(toHex(sx), toHex(cx))
@@ -284,8 +284,8 @@ class PointTest {
@Test
fun liftXGenerator() {
val x = LongArray(4)
val y = LongArray(4)
val x = Fe4()
val y = Fe4()
assertTrue(KeyCodec.liftX(x, y, ECPoint.GX))
assertEquals(toHex(ECPoint.GX), toHex(x))
// liftX returns even y
@@ -295,8 +295,8 @@ class PointTest {
@Test
fun liftXInvalidX() {
// p itself is not a valid x coordinate
val x = LongArray(4)
val y = LongArray(4)
val x = Fe4()
val y = Fe4()
assertFalse(KeyCodec.liftX(x, y, FieldP.P))
}
@@ -305,8 +305,8 @@ class PointTest {
@Test
fun compressDecompressRoundTrip() {
val compressed = KeyCodec.serializeCompressed(ECPoint.GX, ECPoint.GY)
val x = LongArray(4)
val y = LongArray(4)
val x = Fe4()
val y = Fe4()
assertTrue(KeyCodec.parsePublicKey(compressed, x, y))
assertEquals(toHex(ECPoint.GX), toHex(x))
assertEquals(toHex(ECPoint.GY), toHex(y))
@@ -315,8 +315,8 @@ class PointTest {
@Test
fun uncompressedRoundTrip() {
val uncompressed = KeyCodec.serializeUncompressed(ECPoint.GX, ECPoint.GY)
val x = LongArray(4)
val y = LongArray(4)
val x = Fe4()
val y = Fe4()
assertTrue(KeyCodec.parsePublicKey(uncompressed, x, y))
assertEquals(toHex(ECPoint.GX), toHex(x))
assertEquals(toHex(ECPoint.GY), toHex(y))
@@ -324,8 +324,8 @@ class PointTest {
@Test
fun parseInvalidKey() {
val x = LongArray(4)
val y = LongArray(4)
val x = Fe4()
val y = Fe4()
assertFalse(KeyCodec.parsePublicKey(ByteArray(10), x, y))
assertFalse(KeyCodec.parsePublicKey(ByteArray(33), x, y)) // wrong prefix (0x00)
}
@@ -337,13 +337,13 @@ class PointTest {
p.setAffine(ECPoint.GX, ECPoint.GY)
val result = MutablePoint()
ECPoint.addMixed(result, p, ECPoint.GX, ECPoint.GY)
val rx = LongArray(4)
val ry = LongArray(4)
val rx = Fe4()
val ry = Fe4()
ECPoint.toAffine(result, rx, ry)
val doubled = MutablePoint()
ECPoint.doublePoint(doubled, p)
val dx = LongArray(4)
val dy = LongArray(4)
val dx = Fe4()
val dy = Fe4()
ECPoint.toAffine(doubled, dx, dy)
assertEquals(toHex(dx), toHex(rx))
}
@@ -364,8 +364,8 @@ class PointTest {
val pubkey = Secp256k1.pubkeyCreate(privKeyBytes)
val compressed = Secp256k1.pubKeyCompress(pubkey)
assertEquals(0x03.toByte(), compressed[0]) // Odd y → 03 prefix
val x = LongArray(4)
val y = LongArray(4)
val x = Fe4()
val y = Fe4()
assertTrue(KeyCodec.parsePublicKey(compressed, x, y))
// Round-trip: compress again should give same result
val recompressed = KeyCodec.serializeCompressed(x, y)
@@ -31,7 +31,7 @@ import kotlin.test.assertTrue
class ScalarNTest {
private fun hex(s: String) = U256.fromBytes(s.hexToByteArray())
private fun toHex(a: LongArray) = U256.toBytes(a).toHexKey()
private fun toHex(a: Fe4) = U256.toBytes(a).toHexKey()
// ==================== isValid ====================
@@ -42,7 +42,7 @@ class ScalarNTest {
@Test
fun isValidZero() {
assertFalse(ScalarN.isValid(LongArray(4)))
assertFalse(ScalarN.isValid(Fe4()))
}
@Test
@@ -62,7 +62,7 @@ class ScalarNTest {
@Test
fun addZeroIdentity() {
val a = hex("67e56582298859ddae725f972992a07c6c4fb9f62a8fff58ce3ca926a1063530")
assertEquals(toHex(a), toHex(ScalarN.add(a, LongArray(4))))
assertEquals(toHex(a), toHex(ScalarN.add(a, Fe4())))
}
@Test
@@ -95,7 +95,7 @@ class ScalarNTest {
@Test
fun mulOneIdentity() {
val a = hex("67e56582298859ddae725f972992a07c6c4fb9f62a8fff58ce3ca926a1063530")
val one = longArrayOf(1, 0L, 0L, 0L)
val one = Fe4(1, 0L, 0L, 0L)
assertEquals(toHex(a), toHex(ScalarN.mul(a, one)))
}
@@ -123,7 +123,7 @@ class ScalarNTest {
val a = hex("67e56582298859ddae725f972992a07c6c4fb9f62a8fff58ce3ca926a1063530")
val aInv = ScalarN.inv(a)
val product = ScalarN.mul(a, aInv)
val one = longArrayOf(1, 0L, 0L, 0L)
val one = Fe4(1, 0L, 0L, 0L)
assertEquals(toHex(one), toHex(product))
}
@@ -133,7 +133,7 @@ class ScalarNTest {
fun addNearN() {
// (n-1) + 1 should wrap to 0
val nMinus1 = hex("fffffffffffffffffffffffffffffffebaaedce6af48a03bbfd25e8cd0364140")
val one = longArrayOf(1, 0L, 0L, 0L)
val one = Fe4(1, 0L, 0L, 0L)
assertTrue(U256.isZero(ScalarN.add(nMinus1, one)))
}
@@ -141,9 +141,9 @@ class ScalarNTest {
fun addNearNWrap() {
// (n-1) + 2 should give 1
val nMinus1 = hex("fffffffffffffffffffffffffffffffebaaedce6af48a03bbfd25e8cd0364140")
val two = longArrayOf(2, 0L, 0L, 0L)
val two = Fe4(2, 0L, 0L, 0L)
val result = ScalarN.add(nMinus1, two)
val one = longArrayOf(1, 0L, 0L, 0L)
val one = Fe4(1, 0L, 0L, 0L)
assertEquals(toHex(one), toHex(result))
}
@@ -152,19 +152,19 @@ class ScalarNTest {
// (n-1) * (n-1) ≡ 1 mod n (since (n-1) ≡ -1 and (-1)² = 1)
val nMinus1 = hex("fffffffffffffffffffffffffffffffebaaedce6af48a03bbfd25e8cd0364140")
val result = ScalarN.mul(nMinus1, nMinus1)
val one = longArrayOf(1, 0L, 0L, 0L)
val one = Fe4(1, 0L, 0L, 0L)
assertEquals(toHex(one), toHex(result))
}
@Test
fun negOfZeroIsZero() {
assertTrue(U256.isZero(ScalarN.neg(LongArray(4))))
assertTrue(U256.isZero(ScalarN.neg(Fe4())))
}
@Test
fun reduceOfN() {
val result = ScalarN.reduce(ScalarN.N.copyOf())
assertTrue(U256.isZero(result))
assertTrue(result.isZero())
}
@Test
@@ -31,18 +31,18 @@ import kotlin.test.assertTrue
class U256Test {
private fun hex(s: String) = U256.fromBytes(s.hexToByteArray())
private fun toHex(a: LongArray) = U256.toBytes(a).toHexKey()
private fun toHex(a: Fe4) = U256.toBytes(a).toHexKey()
// ==================== isZero / cmp ====================
@Test
fun isZeroTrue() = assertTrue(U256.isZero(LongArray(4)))
fun isZeroTrue() = assertTrue(U256.isZero(Fe4()))
@Test
fun isZeroFalse() = assertFalse(U256.isZero(longArrayOf(1, 0L, 0L, 0L)))
fun isZeroFalse() = assertFalse(U256.isZero(Fe4(1, 0L, 0L, 0L)))
@Test
fun isZeroHighBit() = assertFalse(U256.isZero(longArrayOf(0L, 0L, 0L, 1L)))
fun isZeroHighBit() = assertFalse(U256.isZero(Fe4(0L, 0L, 0L, 1L)))
@Test
fun cmpEqual() = assertEquals(0, U256.cmp(hex("0000000000000000000000000000000000000000000000000000000000000001"), hex("0000000000000000000000000000000000000000000000000000000000000001")))
@@ -57,7 +57,7 @@ class U256Test {
@Test
fun addSimple() {
val out = LongArray(4)
val out = Fe4()
val carry = U256.addTo(out, hex("0000000000000000000000000000000000000000000000000000000000000001"), hex("0000000000000000000000000000000000000000000000000000000000000002"))
assertEquals("0000000000000000000000000000000000000000000000000000000000000003", toHex(out))
assertEquals(0, carry)
@@ -65,7 +65,7 @@ class U256Test {
@Test
fun addOverflow() {
val out = LongArray(4)
val out = Fe4()
val carry = U256.addTo(out, hex("ffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff"), hex("0000000000000000000000000000000000000000000000000000000000000001"))
assertEquals("0000000000000000000000000000000000000000000000000000000000000000", toHex(out))
assertEquals(1, carry)
@@ -73,14 +73,14 @@ class U256Test {
@Test
fun addLimbCarry() {
val out = LongArray(4)
val out = Fe4()
U256.addTo(out, hex("00000000000000000000000000000000000000000000000000000000ffffffff"), hex("0000000000000000000000000000000000000000000000000000000000000001"))
assertEquals("0000000000000000000000000000000000000000000000000000000100000000", toHex(out))
}
@Test
fun subSimple() {
val out = LongArray(4)
val out = Fe4()
val borrow = U256.subTo(out, hex("0000000000000000000000000000000000000000000000000000000000000003"), hex("0000000000000000000000000000000000000000000000000000000000000001"))
assertEquals("0000000000000000000000000000000000000000000000000000000000000002", toHex(out))
assertEquals(0, borrow)
@@ -88,7 +88,7 @@ class U256Test {
@Test
fun subUnderflow() {
val out = LongArray(4)
val out = Fe4()
val borrow = U256.subTo(out, hex("0000000000000000000000000000000000000000000000000000000000000000"), hex("0000000000000000000000000000000000000000000000000000000000000001"))
assertEquals("ffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff", toHex(out))
assertEquals(1, borrow)
@@ -98,50 +98,73 @@ class U256Test {
@Test
fun mulWideSmall() {
val out = LongArray(8)
val out = Wide8()
U256.mulWide(out, hex("0000000000000000000000000000000000000000000000000000000000000003"), hex("0000000000000000000000000000000000000000000000000000000000000007"))
// 3 * 7 = 21 = 0x15
assertEquals(0x15L, out[0])
for (i in 1 until 8) assertEquals(0L, out[i])
assertEquals(0x15L, out.l0)
assertEquals(0L, out.l1)
assertEquals(0L, out.l2)
assertEquals(0L, out.l3)
assertEquals(0L, out.l4)
assertEquals(0L, out.l5)
assertEquals(0L, out.l6)
assertEquals(0L, out.l7)
}
@Test
fun mulWideLarge() {
// (2^128 - 1)² consistency: mulWide and sqrWide should match
val out1 = LongArray(8)
val out2 = LongArray(8)
val out1 = Wide8()
val out2 = Wide8()
val maxHalf = hex("00000000000000000000000000000000ffffffffffffffffffffffffffffffff")
U256.mulWide(out1, maxHalf, maxHalf)
U256.sqrWide(out2, maxHalf)
for (i in 0 until 8) assertEquals(out1[i], out2[i], "Limb $i mismatch")
assertEquals(out1.l0, out2.l0, "Limb 0 mismatch")
assertEquals(out1.l1, out2.l1, "Limb 1 mismatch")
assertEquals(out1.l2, out2.l2, "Limb 2 mismatch")
assertEquals(out1.l3, out2.l3, "Limb 3 mismatch")
assertEquals(out1.l4, out2.l4, "Limb 4 mismatch")
assertEquals(out1.l5, out2.l5, "Limb 5 mismatch")
assertEquals(out1.l6, out2.l6, "Limb 6 mismatch")
assertEquals(out1.l7, out2.l7, "Limb 7 mismatch")
// Lowest limb is 1 (from +1 in (2^128-1)² = 2^256 - 2^129 + 1)
assertEquals(1L, out1[0])
assertEquals(1L, out1.l0)
}
@Test
fun sqrWideMatchesMulWide() {
// sqrWide(a) should produce the same result as mulWide(a, a)
val a = hex("67E56582298859DDAE725F972992A07C6C4FB9F62A8FFF58CE3CA926A1063530")
val mulResult = LongArray(8)
val mulResult = Wide8()
U256.mulWide(mulResult, a, a)
val sqrResult = LongArray(8)
val sqrResult = Wide8()
U256.sqrWide(sqrResult, a)
for (i in 0 until 8) {
assertEquals(mulResult[i], sqrResult[i], "Limb $i mismatch")
}
assertEquals(mulResult.l0, sqrResult.l0, "Limb 0 mismatch")
assertEquals(mulResult.l1, sqrResult.l1, "Limb 1 mismatch")
assertEquals(mulResult.l2, sqrResult.l2, "Limb 2 mismatch")
assertEquals(mulResult.l3, sqrResult.l3, "Limb 3 mismatch")
assertEquals(mulResult.l4, sqrResult.l4, "Limb 4 mismatch")
assertEquals(mulResult.l5, sqrResult.l5, "Limb 5 mismatch")
assertEquals(mulResult.l6, sqrResult.l6, "Limb 6 mismatch")
assertEquals(mulResult.l7, sqrResult.l7, "Limb 7 mismatch")
}
@Test
fun sqrWideMaxValue() {
// (2^256 - 1)^2 should match mulWide
val maxVal = hex("ffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff")
val mulResult = LongArray(8)
val mulResult = Wide8()
U256.mulWide(mulResult, maxVal, maxVal)
val sqrResult = LongArray(8)
val sqrResult = Wide8()
U256.sqrWide(sqrResult, maxVal)
for (i in 0 until 8) {
assertEquals(mulResult[i], sqrResult[i], "Limb $i mismatch for max value sqr")
}
assertEquals(mulResult.l0, sqrResult.l0, "Limb 0 mismatch for max value sqr")
assertEquals(mulResult.l1, sqrResult.l1, "Limb 1 mismatch for max value sqr")
assertEquals(mulResult.l2, sqrResult.l2, "Limb 2 mismatch for max value sqr")
assertEquals(mulResult.l3, sqrResult.l3, "Limb 3 mismatch for max value sqr")
assertEquals(mulResult.l4, sqrResult.l4, "Limb 4 mismatch for max value sqr")
assertEquals(mulResult.l5, sqrResult.l5, "Limb 5 mismatch for max value sqr")
assertEquals(mulResult.l6, sqrResult.l6, "Limb 6 mismatch for max value sqr")
assertEquals(mulResult.l7, sqrResult.l7, "Limb 7 mismatch for max value sqr")
}
// ==================== fromBytes / toBytes ====================
@@ -158,7 +181,7 @@ class U256Test {
@Test
fun bytesZero() {
val limbs = U256.fromBytes(ByteArray(32))
assertTrue(U256.isZero(limbs))
assertTrue(limbs.isZero())
}
// ==================== getNibble ====================
@@ -200,7 +223,7 @@ class U256Test {
@Test
fun xorBasic() {
val out = LongArray(4)
val out = Fe4()
U256.xorTo(out, hex("ff00ff00ff00ff00ff00ff00ff00ff00ff00ff00ff00ff00ff00ff00ff00ff00"), hex("0f0f0f0f0f0f0f0f0f0f0f0f0f0f0f0f0f0f0f0f0f0f0f0f0f0f0f0f0f0f0f0f"))
assertEquals("f00ff00ff00ff00ff00ff00ff00ff00ff00ff00ff00ff00ff00ff00ff00ff00f", toHex(out))
}
@@ -220,9 +243,12 @@ class U256Test {
@Test
fun copyIntoTest() {
val src = hex("67e56582298859ddae725f972992a07c6c4fb9f62a8fff58ce3ca926a1063530")
val dst = LongArray(4)
val dst = Fe4()
U256.copyInto(dst, src)
for (i in 0 until 4) assertEquals(src[i], dst[i])
assertEquals(src.l0, dst.l0)
assertEquals(src.l1, dst.l1)
assertEquals(src.l2, dst.l2)
assertEquals(src.l3, dst.l3)
}
@Test
@@ -232,6 +258,9 @@ class U256Test {
val expected = hex("67e56582298859ddae725f972992a07c6c4fb9f62a8fff58ce3ca926a1063530")
U256.toBytesInto(expected, fullArray, 32)
val decoded = U256.fromBytes(fullArray, 32)
for (i in 0 until 4) assertEquals(expected[i], decoded[i])
assertEquals(expected.l0, decoded.l0)
assertEquals(expected.l1, decoded.l1)
assertEquals(expected.l2, decoded.l2)
assertEquals(expected.l3, decoded.l3)
}
}
@@ -38,19 +38,19 @@ package com.vitorpamplona.quartz.utils.secp256k1
* Benchmark confirmed: unfused path is equal or faster on HotSpot JVM 21.
*/
internal actual fun fieldMulReduce(
out: LongArray,
a: LongArray,
b: LongArray,
w: LongArray,
out: Fe4,
a: Fe4,
b: Fe4,
w: Wide8,
) {
U256.mulWide(w, a, b)
FieldP.reduceWide(out, w)
}
internal actual fun fieldSqrReduce(
out: LongArray,
a: LongArray,
w: LongArray,
out: Fe4,
a: Fe4,
w: Wide8,
) {
U256.sqrWide(w, a)
FieldP.reduceWide(out, w)
@@ -24,233 +24,82 @@ import kotlin.test.Test
import kotlin.test.assertTrue
/**
* Benchmark comparing LongArray(4) vs Fe4 (struct with 4 @JvmField Long fields)
* for secp256k1 field element operations.
* Field-level micro-benchmark for the Fe4-based secp256k1 implementation.
*
* The hypothesis: Fe4 eliminates array bounds checks (laload/lastore → getfield/putfield),
* which should measurably improve performance on platforms where the JIT cannot reliably
* eliminate bounds checks for parameter arrays (Android ART, Kotlin/Native LLVM).
* Measures the core field operations using Fe4 struct types (no array bounds checks).
* Compare against the pre-migration LongArray baseline to verify the performance gain.
*
* On JVM HotSpot C2, the difference may be smaller because C2 can profile array sizes
* and eliminate constant-index bounds checks. But even on HotSpot, the Fe4 approach
* avoids the array length field load + comparison that precedes each array access.
* Pre-migration baseline (LongArray, HotSpot C2):
* FieldP.mul: 40 ns/op
* FieldP.sqr: 48 ns/op
* FieldP.add: 9 ns/op
* FieldP.sub: 10 ns/op
*
* Run with: ./gradlew :quartz:jvmTest --tests "*.Fe4Benchmark"
*/
class Fe4Benchmark {
// Test vectors: secp256k1 generator point coordinates
private val aBytes = hexToBytes("79BE667EF9DCBBAC55A06295CE870B07029BFCDB2DCE28D959F2815B16F81798")
private val bBytes = hexToBytes("483ADA7726A3C4655DA4FBFC0E1108A8FD17B448A68554199C47D08FFB10D4B8")
// LongArray representations
private val aArr = U256.fromBytes(aBytes)
private val bArr = U256.fromBytes(bBytes)
private val outArr = LongArray(4)
private val wArr = LongArray(8)
// Fe4 representations (same values)
private val aFe4 = Fe4(aArr[0], aArr[1], aArr[2], aArr[3])
private val bFe4 = Fe4(bArr[0], bArr[1], bArr[2], bArr[3])
private val outFe4 = Fe4()
private val wFe8 = Wide8()
private val a = U256.fromBytes(aBytes)
private val b = U256.fromBytes(bBytes)
private val out = Fe4()
private val w = Wide8()
private data class BenchResult(
val name: String,
val arrayNanos: Long,
val fe4Nanos: Long,
val nanos: Long,
val iterations: Int,
) {
val arrayNsPerOp get() = arrayNanos / iterations
val fe4NsPerOp get() = fe4Nanos / iterations
val speedup get() = arrayNanos.toDouble() / fe4Nanos.toDouble()
override fun toString(): String {
val pct = ((speedup - 1.0) * 100).let { if (it >= 0) "+%.1f%%".format(it) else "%.1f%%".format(it) }
return String.format(
" %-20s LongArray: %,8d ns/op Fe4: %,8d ns/op %s",
name,
arrayNsPerOp,
fe4NsPerOp,
pct,
)
}
val nsPerOp get() = nanos / iterations
}
private inline fun bench(
name: String,
warmup: Int,
iterations: Int,
crossinline arrayOp: () -> Unit,
crossinline fe4Op: () -> Unit,
crossinline op: () -> Unit,
): BenchResult {
// Warmup both generously (C2 compiles at ~10K invocations)
repeat(warmup) { arrayOp() }
repeat(warmup) { fe4Op() }
// Run 3 rounds, alternating order, take best of each to reduce noise
var bestArr = Long.MAX_VALUE
var bestFe4 = Long.MAX_VALUE
repeat(warmup) { op() }
var best = Long.MAX_VALUE
for (round in 0 until 3) {
if (round % 2 == 0) {
// LongArray first
val arrStart = System.nanoTime()
repeat(iterations) { arrayOp() }
bestArr = minOf(bestArr, System.nanoTime() - arrStart)
val fe4Start = System.nanoTime()
repeat(iterations) { fe4Op() }
bestFe4 = minOf(bestFe4, System.nanoTime() - fe4Start)
} else {
// Fe4 first
val fe4Start = System.nanoTime()
repeat(iterations) { fe4Op() }
bestFe4 = minOf(bestFe4, System.nanoTime() - fe4Start)
val arrStart = System.nanoTime()
repeat(iterations) { arrayOp() }
bestArr = minOf(bestArr, System.nanoTime() - arrStart)
}
val start = System.nanoTime()
repeat(iterations) { op() }
best = minOf(best, System.nanoTime() - start)
}
return BenchResult(name, bestArr, bestFe4, iterations)
return BenchResult(name, best, iterations)
}
@Test
fun benchmarkFieldOps() {
// Verify correctness first: both implementations must produce identical results
verifySameResults()
val results = mutableListOf<BenchResult>()
// --- FieldP.mul (the hottest operation: ~1900 calls/verify) ---
// Use out as input to next iteration to create data dependency chain
aArr.copyInto(outArr)
aFe4.copyFrom(Fe4(aArr[0], aArr[1], aArr[2], aArr[3]))
outFe4.copyFrom(aFe4)
results +=
bench(
"FieldP.mul",
10000,
200000,
arrayOp = { FieldP.mul(outArr, outArr, bArr, wArr) },
fe4Op = { Fe4FieldP.mul(outFe4, outFe4, bFe4, wFe8) },
)
out.copyFrom(a)
results += bench("FieldP.mul", 10000, 200000) { FieldP.mul(out, out, b, w) }
// --- FieldP.sqr (second hottest: ~1900 calls/verify) ---
aArr.copyInto(outArr)
outFe4.copyFrom(aFe4)
results +=
bench(
"FieldP.sqr",
10000,
200000,
arrayOp = { FieldP.sqr(outArr, outArr, wArr) },
fe4Op = { Fe4FieldP.sqr(outFe4, outFe4, wFe8) },
)
out.copyFrom(a)
results += bench("FieldP.sqr", 10000, 200000) { FieldP.sqr(out, out, w) }
// --- FieldP.add (~750 calls/verify) ---
aArr.copyInto(outArr)
outFe4.copyFrom(aFe4)
results +=
bench(
"FieldP.add",
10000,
500000,
arrayOp = { FieldP.add(outArr, outArr, bArr) },
fe4Op = { Fe4FieldP.add(outFe4, outFe4, bFe4) },
)
out.copyFrom(a)
results += bench("FieldP.add", 10000, 500000) { FieldP.add(out, out, b) }
// --- FieldP.sub (~500 calls/verify) ---
aArr.copyInto(outArr)
outFe4.copyFrom(aFe4)
results +=
bench(
"FieldP.sub",
10000,
500000,
arrayOp = { FieldP.sub(outArr, outArr, bArr) },
fe4Op = { Fe4FieldP.sub(outFe4, outFe4, bFe4) },
)
out.copyFrom(a)
results += bench("FieldP.sub", 10000, 500000) { FieldP.sub(out, out, b) }
// --- U256.mulWide (raw wide multiply, no reduction) ---
results +=
bench(
"U256.mulWide",
10000,
200000,
arrayOp = { U256.mulWide(wArr, aArr, bArr) },
fe4Op = { Fe4U256.mulWide(wFe8, aFe4, bFe4) },
)
results += bench("U256.mulWide", 10000, 200000) { U256.mulWide(w, a, b) }
// --- U256.sqrWide (raw wide square) ---
results +=
bench(
"U256.sqrWide",
10000,
200000,
arrayOp = { U256.sqrWide(wArr, aArr) },
fe4Op = { Fe4U256.sqrWide(wFe8, aFe4) },
)
results += bench("U256.sqrWide", 10000, 200000) { U256.sqrWide(w, a) }
// Print results
println()
println("=".repeat(80))
println("Fe4 vs LongArray Benchmark: JVM21/HotSpot C2")
println("=".repeat(80))
println(" Hypothesis: Fe4 (named fields) eliminates array bounds checks,")
println(" producing faster code especially on ART/Native where JIT is weaker.")
println("-".repeat(80))
println("=".repeat(70))
println("Fe4 Field Micro-Benchmarks: JVM21/HotSpot C2 (post-migration)")
println("=".repeat(70))
for (r in results) {
println(r)
println(" ${r.name.padEnd(20)} ${r.nsPerOp.toString().padStart(8)} ns/op")
}
println("=".repeat(80))
println()
println(" Bytecode analysis:")
println(" LongArray access: aload + iconst + laload (3 insns + implicit bounds check)")
println(" Fe4 field access: aload + getfield (2 insns, no check)")
println()
println(" U256.class: 150 laload/lastore operations (each bounds-checked)")
println(" FieldP.class: 119 laload/lastore operations")
println(" FieldMulFusedKt: 195 laload/lastore operations")
println(" Total: 464 bounds checks in 3 core files")
println()
println("=".repeat(70))
// Prevent dead code elimination
assertTrue(outArr[0] != Long.MIN_VALUE || outFe4.l0 != Long.MIN_VALUE)
}
private fun verifySameResults() {
// Test mul
FieldP.mul(outArr, aArr, bArr, wArr)
Fe4FieldP.mul(outFe4, aFe4, bFe4, wFe8)
assertFe4EqualsArray("mul", outArr, outFe4)
// Test sqr
FieldP.sqr(outArr, aArr, wArr)
Fe4FieldP.sqr(outFe4, aFe4, wFe8)
assertFe4EqualsArray("sqr", outArr, outFe4)
// Test add
FieldP.add(outArr, aArr, bArr)
Fe4FieldP.add(outFe4, aFe4, bFe4)
assertFe4EqualsArray("add", outArr, outFe4)
// Test sub
FieldP.sub(outArr, aArr, bArr)
Fe4FieldP.sub(outFe4, aFe4, bFe4)
assertFe4EqualsArray("sub", outArr, outFe4)
}
private fun assertFe4EqualsArray(
op: String,
arr: LongArray,
fe: Fe4,
) {
assertTrue(
arr[0] == fe.l0 && arr[1] == fe.l1 && arr[2] == fe.l2 && arr[3] == fe.l3,
"$op: LongArray[${arr[0]},${arr[1]},${arr[2]},${arr[3]}] != " +
"Fe4[${fe.l0},${fe.l1},${fe.l2},${fe.l3}]",
)
assertTrue(out.l0 != Long.MIN_VALUE)
}
private fun hexToBytes(hex: String): ByteArray {
@@ -220,35 +220,35 @@ class Secp256k1NativeBenchmark {
// out of the loop (dead code elimination risk with constant inputs).
val a = U256.fromBytes(hexToBytes("79BE667EF9DCBBAC55A06295CE870B07029BFCDB2DCE28D959F2815B16F81798"))
val b = U256.fromBytes(hexToBytes("483ADA7726A3C4655DA4FBFC0E1108A8FD17B448A68554199C47D08FFB10D4B8"))
val out = LongArray(4)
val w = LongArray(8)
val out = Fe4()
val w = Wide8()
val results = mutableListOf<BenchResult>()
// --- FieldP.mul (the hottest operation) ---
// Uses `out` as both output and next input to create a data dependency chain.
a.copyInto(out)
out.copyFrom(a)
results +=
bench("FieldP.mul", 5000, 100000) {
FieldP.mul(out, out, b, w)
}
// --- FieldP.sqr ---
a.copyInto(out)
out.copyFrom(a)
results +=
bench("FieldP.sqr", 5000, 100000) {
FieldP.sqr(out, out, w)
}
// --- FieldP.add ---
a.copyInto(out)
out.copyFrom(a)
results +=
bench("FieldP.add", 5000, 500000) {
FieldP.add(out, out, b)
}
// --- FieldP.sub ---
a.copyInto(out)
out.copyFrom(a)
results +=
bench("FieldP.sub", 5000, 500000) {
FieldP.sub(out, out, b)
@@ -277,7 +277,7 @@ class Secp256k1NativeBenchmark {
printResults("secp256k1 Field Micro-Benchmarks: Kotlin/Native (LLVM AOT) on linuxX64", results)
// Use sinks to prevent dead code elimination of the entire benchmark
assertTrue(hiSink != Long.MIN_VALUE || ltSink != Long.MIN_VALUE || out[0] != Long.MIN_VALUE)
assertTrue(hiSink != Long.MIN_VALUE || ltSink != Long.MIN_VALUE || out.l0 != Long.MIN_VALUE)
}
private fun hexToBytes(hex: String): ByteArray {
@@ -25,18 +25,18 @@ package com.vitorpamplona.quartz.utils.secp256k1
* Kotlin/Native compiles ahead-of-time, so inline + direct call is optimal.
*/
internal actual fun fieldMulReduce(
out: LongArray,
a: LongArray,
b: LongArray,
w: LongArray,
out: Fe4,
a: Fe4,
b: Fe4,
w: Wide8,
) {
fieldMulReduceFused(out, a, b, w)
}
internal actual fun fieldSqrReduce(
out: LongArray,
a: LongArray,
w: LongArray,
out: Fe4,
a: Fe4,
w: Wide8,
) {
fieldSqrReduceFused(out, a, w)
}