diff --git a/quartz/src/androidMain/kotlin/com/vitorpamplona/quartz/utils/secp256k1/FieldMulPlatform.android.kt b/quartz/src/androidMain/kotlin/com/vitorpamplona/quartz/utils/secp256k1/FieldMulPlatform.android.kt index 14f9bda718..1d042d12ff 100644 --- a/quartz/src/androidMain/kotlin/com/vitorpamplona/quartz/utils/secp256k1/FieldMulPlatform.android.kt +++ b/quartz/src/androidMain/kotlin/com/vitorpamplona/quartz/utils/secp256k1/FieldMulPlatform.android.kt @@ -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) } } diff --git a/quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/utils/secp256k1/ECPoint.kt b/quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/utils/secp256k1/ECPoint.kt index c9279fd1c0..2edaee779e 100644 --- a/quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/utils/secp256k1/ECPoint.kt +++ b/quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/utils/secp256k1/ECPoint.kt @@ -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, // 8 affine odd-multiples of P val pLamOdd: Array, // 8 affine odd-multiples of λ(P) ) @@ -127,7 +127,7 @@ internal object ECPoint { private val pTableCache = arrayOfNulls(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 { 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, @@ -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 diff --git a/quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/utils/secp256k1/Fe4.kt b/quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/utils/secp256k1/Fe4.kt index 0e4714bd4b..3d7baeac9a 100644 --- a/quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/utils/secp256k1/Fe4.kt +++ b/quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/utils/secp256k1/Fe4.kt @@ -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] + } } /** diff --git a/quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/utils/secp256k1/Fe4Ops.kt b/quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/utils/secp256k1/Fe4Ops.kt deleted file mode 100644 index 9f662f8868..0000000000 --- a/quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/utils/secp256k1/Fe4Ops.kt +++ /dev/null @@ -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) - } -} diff --git a/quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/utils/secp256k1/FieldMulFused.kt b/quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/utils/secp256k1/FieldMulFused.kt index 4d3362e2a2..0d20007bcf 100644 --- a/quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/utils/secp256k1/FieldMulFused.kt +++ b/quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/utils/secp256k1/FieldMulFused.kt @@ -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) } diff --git a/quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/utils/secp256k1/FieldMulPlatform.kt b/quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/utils/secp256k1/FieldMulPlatform.kt index ac2ec36c5b..12bf12369a 100644 --- a/quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/utils/secp256k1/FieldMulPlatform.kt +++ b/quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/utils/secp256k1/FieldMulPlatform.kt @@ -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 } } diff --git a/quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/utils/secp256k1/FieldP.kt b/quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/utils/secp256k1/FieldP.kt index af5456b140..ff66bc1eff 100644 --- a/quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/utils/secp256k1/FieldP.kt +++ b/quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/utils/secp256k1/FieldP.kt @@ -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 diff --git a/quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/utils/secp256k1/Glv.kt b/quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/utils/secp256k1/Glv.kt index bfda6d1d45..f58b89ab7e 100644 --- a/quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/utils/secp256k1/Glv.kt +++ b/quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/utils/secp256k1/Glv.kt @@ -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, diff --git a/quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/utils/secp256k1/KeyCodec.kt b/quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/utils/secp256k1/KeyCodec.kt index 59b8867003..24c29e5bcf 100644 --- a/quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/utils/secp256k1/KeyCodec.kt +++ b/quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/utils/secp256k1/KeyCodec.kt @@ -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 diff --git a/quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/utils/secp256k1/PointTypes.kt b/quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/utils/secp256k1/PointTypes.kt index 40f1e22ec9..ac8f437281 100644 --- a/quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/utils/secp256k1/PointTypes.kt +++ b/quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/utils/secp256k1/PointTypes.kt @@ -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() } diff --git a/quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/utils/secp256k1/ScalarN.kt b/quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/utils/secp256k1/ScalarN.kt index cbbdb20008..a8034cd66e 100644 --- a/quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/utils/secp256k1/ScalarN.kt +++ b/quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/utils/secp256k1/ScalarN.kt @@ -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 diff --git a/quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/utils/secp256k1/Secp256k1.kt b/quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/utils/secp256k1/Secp256k1.kt index 9eea4728c3..6291c1d6f0 100644 --- a/quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/utils/secp256k1/Secp256k1.kt +++ b/quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/utils/secp256k1/Secp256k1.kt @@ -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(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() diff --git a/quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/utils/secp256k1/U256.kt b/quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/utils/secp256k1/U256.kt index 8c5486dac0..490ec8ef57 100644 --- a/quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/utils/secp256k1/U256.kt +++ b/quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/utils/secp256k1/U256.kt @@ -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) } } diff --git a/quartz/src/commonTest/kotlin/com/vitorpamplona/quartz/utils/secp256k1/FieldPTest.kt b/quartz/src/commonTest/kotlin/com/vitorpamplona/quartz/utils/secp256k1/FieldPTest.kt index dbee6eda05..e532d17612 100644 --- a/quartz/src/commonTest/kotlin/com/vitorpamplona/quartz/utils/secp256k1/FieldPTest.kt +++ b/quartz/src/commonTest/kotlin/com/vitorpamplona/quartz/utils/secp256k1/FieldPTest.kt @@ -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)) } diff --git a/quartz/src/commonTest/kotlin/com/vitorpamplona/quartz/utils/secp256k1/GlvTest.kt b/quartz/src/commonTest/kotlin/com/vitorpamplona/quartz/utils/secp256k1/GlvTest.kt index 02f64310b0..fd58b3f205 100644 --- a/quartz/src/commonTest/kotlin/com/vitorpamplona/quartz/utils/secp256k1/GlvTest.kt +++ b/quartz/src/commonTest/kotlin/com/vitorpamplona/quartz/utils/secp256k1/GlvTest.kt @@ -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-- } } } diff --git a/quartz/src/commonTest/kotlin/com/vitorpamplona/quartz/utils/secp256k1/KeyCodecTest.kt b/quartz/src/commonTest/kotlin/com/vitorpamplona/quartz/utils/secp256k1/KeyCodecTest.kt index ce93125bb6..c431bf3f69 100644 --- a/quartz/src/commonTest/kotlin/com/vitorpamplona/quartz/utils/secp256k1/KeyCodecTest.kt +++ b/quartz/src/commonTest/kotlin/com/vitorpamplona/quartz/utils/secp256k1/KeyCodecTest.kt @@ -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()) diff --git a/quartz/src/commonTest/kotlin/com/vitorpamplona/quartz/utils/secp256k1/PointTest.kt b/quartz/src/commonTest/kotlin/com/vitorpamplona/quartz/utils/secp256k1/PointTest.kt index d7d8252cb6..08a119e282 100644 --- a/quartz/src/commonTest/kotlin/com/vitorpamplona/quartz/utils/secp256k1/PointTest.kt +++ b/quartz/src/commonTest/kotlin/com/vitorpamplona/quartz/utils/secp256k1/PointTest.kt @@ -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) diff --git a/quartz/src/commonTest/kotlin/com/vitorpamplona/quartz/utils/secp256k1/ScalarNTest.kt b/quartz/src/commonTest/kotlin/com/vitorpamplona/quartz/utils/secp256k1/ScalarNTest.kt index 888d99595c..b1497751b8 100644 --- a/quartz/src/commonTest/kotlin/com/vitorpamplona/quartz/utils/secp256k1/ScalarNTest.kt +++ b/quartz/src/commonTest/kotlin/com/vitorpamplona/quartz/utils/secp256k1/ScalarNTest.kt @@ -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 diff --git a/quartz/src/commonTest/kotlin/com/vitorpamplona/quartz/utils/secp256k1/U256Test.kt b/quartz/src/commonTest/kotlin/com/vitorpamplona/quartz/utils/secp256k1/U256Test.kt index 390e714ed3..2fa050acef 100644 --- a/quartz/src/commonTest/kotlin/com/vitorpamplona/quartz/utils/secp256k1/U256Test.kt +++ b/quartz/src/commonTest/kotlin/com/vitorpamplona/quartz/utils/secp256k1/U256Test.kt @@ -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) } } diff --git a/quartz/src/jvmMain/kotlin/com/vitorpamplona/quartz/utils/secp256k1/FieldMulPlatform.jvm.kt b/quartz/src/jvmMain/kotlin/com/vitorpamplona/quartz/utils/secp256k1/FieldMulPlatform.jvm.kt index b4ec7a4b59..59cbd0c09a 100644 --- a/quartz/src/jvmMain/kotlin/com/vitorpamplona/quartz/utils/secp256k1/FieldMulPlatform.jvm.kt +++ b/quartz/src/jvmMain/kotlin/com/vitorpamplona/quartz/utils/secp256k1/FieldMulPlatform.jvm.kt @@ -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) diff --git a/quartz/src/jvmTest/kotlin/com/vitorpamplona/quartz/utils/secp256k1/Fe4Benchmark.kt b/quartz/src/jvmTest/kotlin/com/vitorpamplona/quartz/utils/secp256k1/Fe4Benchmark.kt index efae1e5c94..3559795757 100644 --- a/quartz/src/jvmTest/kotlin/com/vitorpamplona/quartz/utils/secp256k1/Fe4Benchmark.kt +++ b/quartz/src/jvmTest/kotlin/com/vitorpamplona/quartz/utils/secp256k1/Fe4Benchmark.kt @@ -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() - // --- 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 { diff --git a/quartz/src/linuxX64Test/kotlin/com/vitorpamplona/quartz/utils/secp256k1/Secp256k1NativeBenchmark.kt b/quartz/src/linuxX64Test/kotlin/com/vitorpamplona/quartz/utils/secp256k1/Secp256k1NativeBenchmark.kt index 74570ee2da..896bb825b6 100644 --- a/quartz/src/linuxX64Test/kotlin/com/vitorpamplona/quartz/utils/secp256k1/Secp256k1NativeBenchmark.kt +++ b/quartz/src/linuxX64Test/kotlin/com/vitorpamplona/quartz/utils/secp256k1/Secp256k1NativeBenchmark.kt @@ -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() // --- 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 { diff --git a/quartz/src/nativeMain/kotlin/com/vitorpamplona/quartz/utils/secp256k1/FieldMulPlatform.native.kt b/quartz/src/nativeMain/kotlin/com/vitorpamplona/quartz/utils/secp256k1/FieldMulPlatform.native.kt index 720688720a..f98da5602a 100644 --- a/quartz/src/nativeMain/kotlin/com/vitorpamplona/quartz/utils/secp256k1/FieldMulPlatform.native.kt +++ b/quartz/src/nativeMain/kotlin/com/vitorpamplona/quartz/utils/secp256k1/FieldMulPlatform.native.kt @@ -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) }