feat: fix constants and types in ScalarN, KeyCodec, partial Glv fix (WIP — still broken)

Progress on the LongArray(4) migration:
- ScalarN: constants converted to 4×64-bit, loop bounds fixed
- KeyCodec: B constant fixed
- Glv: constants partially converted but regex left residual old values
- Point: types fixed, GX/GY constants converted
- Secp256k1: parameter types updated

Still needs: manual cleanup of Glv constants, ScalarN reduceWide internals,
test hex() helpers, test constant arrays, wNAF bit manipulation for 64-bit limbs.

https://claude.ai/code/session_01BhU63WUe9AhikZxRdw3Lpg
This commit is contained in:
Claude
2026-04-06 01:18:40 +00:00
parent 69f222c6d5
commit 689c52ed6f
11 changed files with 137 additions and 123 deletions
@@ -51,7 +51,8 @@ internal object Glv {
/** β: cube root of unity mod p. The endomorphism is φ(x,y) = (β·x, y). */
val BETA =
longArrayOf(
0x719501EE.toInt(),
-4523465429756870162L, -7138124642204153451L, 7954561588662645993L, 8856726876819556112L,
),
0xC1396C28.toInt(),
0x12F58995.toInt(),
0x9CF04975.toInt(),
@@ -65,8 +66,8 @@ internal object Glv {
/** Result of splitting a 256-bit scalar into two ~128-bit halves via GLV. */
data class Split(
val k1: IntArray,
val k2: IntArray,
val k1: LongArray,
val k2: LongArray,
val negK1: Boolean,
val negK2: Boolean,
)
@@ -76,7 +77,7 @@ internal object Glv {
* with |k₁|, |k₂| ≈ 128 bits. Both are made positive for wNAF encoding;
* the negation flags indicate whether the corresponding point should be negated.
*/
fun splitScalar(k: IntArray): Split {
fun splitScalar(k: LongArray): 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))
@@ -101,10 +102,10 @@ internal object Glv {
* bits — a fix for a bug where carries past bit 255 were silently dropped.
*/
fun wnaf(
scalar: IntArray,
scalar: LongArray,
w: Int,
maxBits: Int,
): IntArray {
): LongArray {
val totalBits = maxBits + w
val sLimbs = maxOf((totalBits + 31) / 32, scalar.size)
val result = IntArray(totalBits)
@@ -112,7 +113,7 @@ internal object Glv {
scalar.copyInto(s)
var bit = 0
while (bit < totalBits) {
if (s[bit / 32] ushr (bit % 32) and 1 == 0) {
if (s[bit / 64] ushr (bit % 64) and 1 == 0) {
bit++
continue
}
@@ -131,48 +132,50 @@ internal object Glv {
/** Multiply two 256-bit numbers, return the result shifted right by 384 bits (rounded). */
private fun mulShift384(
k: IntArray,
g: IntArray,
): IntArray {
k: LongArray,
g: LongArray,
): LongArray {
val wide = LongArray(8)
U256.mulWide(wide, k, g)
val result = LongArray(4)
for (i in 0 until 4) result[i] = wide[i + 12]
if (wide[11] < 0) { // Round based on bit 383
for (i in 0 until 2) result[i] = wide[i + 6]
if (wide[5] < 0) { // Round based on bit 383
var c = 1L
for (i in 0 until 8) {
c += (result[i].toLong() and 0xFFFFFFFFL)
result[i] = c.toInt()
c = c ushr 32
for (i in 0 until 4) {
val s = result[i] + c
val ov = if (s.toULong() < result[i].toULong()) 1L else 0L
result[i] = s
c = ov
}
}
return result
}
private fun getBitsVar(
s: IntArray,
s: LongArray,
bitPos: Int,
count: Int,
): Int {
if (count == 0) return 0
val limb = bitPos / 32
val limb = bitPos / 64
val shift = bitPos % 32
var r = (s[limb] ushr shift)
if (shift + count > 32 && limb + 1 < s.size) r = r or (s[limb + 1] shl (32 - shift))
return r and ((1 shl count) - 1)
if (shift + count > 64 && limb + 1 < s.size) r = r or (s[limb + 1] shl (64 - shift))
return (r and ((1L shl count) - 1L)).toInt()
}
private fun addBitTo(
s: IntArray,
s: LongArray,
bitPos: Int,
) {
val limb = bitPos / 32
val limb = bitPos / 64
if (limb >= s.size) return
var carry = (1L shl (bitPos % 32))
var carry = (1L shl (bitPos % 64))
for (i in limb until s.size) {
carry += (s[i].toLong() and 0xFFFFFFFFL)
s[i] = carry.toInt()
carry = carry ushr 32
val sum = s[i] + carry
val ov = if (sum.toULong() < s[i].toULong()) 1L else 0L
s[i] = sum
carry = ov
if (carry == 0L) break
}
}
@@ -183,7 +186,8 @@ internal object Glv {
/** -λ mod n */
private val MINUS_LAMBDA =
longArrayOf(
0xB51283CF.toInt(),
-2247357714951666737L, -6304834983940376126L, 6546514211138018212L, -6008836872998760673L,
),
0xE0CFC810.toInt(),
0x8EC739C2.toInt(),
0xA880B9FC.toInt(),
@@ -196,7 +200,8 @@ internal object Glv {
/** Babai rounding constant g1 = round(2^384 · |b2| / n) */
private val G1 =
longArrayOf(
0x45DBB031.toInt(),
-1687969588364726223L, 4443515802769476223L, -1698823648040391915L, 3496713202691238861L,
),
0xE893209A.toInt(),
0x71E8CA7F.toInt(),
0x3DAA8A14.toInt(),
@@ -209,7 +214,8 @@ internal object Glv {
/** Babai rounding constant g2 = round(2^384 · |b1| / n) */
private val G2 =
longArrayOf(
0x8AC47F71.toInt(),
1545214808910233457L, 2455034284347819718L, 8022177200260244676L, -1998614352016537560L,
),
0x1571B4AE.toInt(),
0x9DF506C6.toInt(),
0x221208AC.toInt(),
@@ -222,7 +228,8 @@ internal object Glv {
/** -b1 mod n (lattice basis vector) */
private val MINUS_B1 =
longArrayOf(
0x0ABFE4C3.toInt(),
8022177200260244675L, -1998614352016537560L, 0L, 0L,
),
0x6F547FA9.toInt(),
0x010E8828.toInt(),
0xE4437ED6.toInt(),
@@ -235,7 +242,8 @@ internal object Glv {
/** -b2 mod n (lattice basis vector) */
private val MINUS_B2 =
longArrayOf(
0x3DB1562C.toInt(),
-2925706260434037204L, -8491525256057179027L, -2L, -1L,
),
0xD765CDA8.toInt(),
0x0774346D.toInt(),
0x8A280AC5.toInt(),
@@ -248,7 +256,8 @@ internal object Glv {
/** n / 2, used to determine if a half-scalar needs negation */
private val N_HALF =
longArrayOf(
0x681B20A0.toInt(),
-2312264954237214560L, 6725966010171805725L, -1L, 9223372036854775807L,
),
0xDFE92F46.toInt(),
0x57A4501D.toInt(),
0x5D576E73.toInt(),
@@ -39,16 +39,16 @@ package com.vitorpamplona.quartz.utils.secp256k1
*/
internal object KeyCodec {
/** Curve constant b = 7 in y² = x³ + 7. */
private val B = longArrayOf(7, 0, 0, 0, 0, 0, 0, 0)
private val B = longArrayOf(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: IntArray,
outY: IntArray,
x: IntArray,
outX: LongArray,
outY: LongArray,
x: LongArray,
): Boolean {
if (U256.cmp(x, FieldP.P) >= 0) return false
val t = LongArray(4)
@@ -62,7 +62,7 @@ internal object KeyCodec {
}
/** Check if y-coordinate is even (LSB = 0). */
fun hasEvenY(y: IntArray): Boolean = y[0] and 1 == 0
fun hasEvenY(y: LongArray): Boolean = y[0] and 1 == 0
/**
* Parse a serialized public key (33 bytes compressed or 65 bytes uncompressed).
@@ -71,8 +71,8 @@ internal object KeyCodec {
*/
fun parsePublicKey(
pubkey: ByteArray,
outX: IntArray,
outY: IntArray,
outX: LongArray,
outY: LongArray,
): Boolean =
when {
pubkey.size == 33 && (pubkey[0] == 0x02.toByte() || pubkey[0] == 0x03.toByte()) -> {
@@ -112,8 +112,8 @@ internal object KeyCodec {
/** Serialize as 65-byte uncompressed: 04 || x (32 bytes) || y (32 bytes). */
fun serializeUncompressed(
x: IntArray,
y: IntArray,
x: LongArray,
y: LongArray,
): ByteArray {
val r = ByteArray(65)
r[0] = 0x04
@@ -124,8 +124,8 @@ internal object KeyCodec {
/** Serialize as 33-byte compressed: 02/03 || x (32 bytes). */
fun serializeCompressed(
x: IntArray,
y: IntArray,
x: LongArray,
y: LongArray,
): ByteArray {
val r = ByteArray(33)
r[0] = if (hasEvenY(y)) 0x02 else 0x03
@@ -71,19 +71,19 @@ package com.vitorpamplona.quartz.utils.secp256k1
* multiplication, which performs thousands of doublings and additions per operation.
*/
internal class MutablePoint(
val x: IntArray = LongArray(4),
val y: IntArray = LongArray(4),
val z: IntArray = LongArray(4),
val x: LongArray = LongArray(4),
val y: LongArray = LongArray(4),
val z: LongArray = LongArray(4),
) {
fun isInfinity(): Boolean = U256.isZero(z)
fun setInfinity() {
for (i in 0 until 8) {
x[i] = 0
z[i] = 0
for (i in 0 until 4) {
x[i] = 0L
z[i] = 0L
}
y[0] = 1
for (i in 1 until 8) y[i] = 0
y[0] = 1L
for (i in 1 until 4) y[i] = 0L
}
fun copyFrom(other: MutablePoint) {
@@ -93,13 +93,13 @@ internal class MutablePoint(
}
fun setAffine(
ax: IntArray,
ay: IntArray,
ax: LongArray,
ay: LongArray,
) {
ax.copyInto(x)
ay.copyInto(y)
z[0] = 1
for (i in 1 until 8) z[i] = 0
z[0] = 1L
for (i in 1 until 4) z[i] = 0L
}
}
@@ -108,8 +108,8 @@ internal class MutablePoint(
* Used for precomputed tables where we want compact storage and mixed addition.
*/
internal class AffinePoint(
val x: IntArray = LongArray(4),
val y: IntArray = LongArray(4),
val x: LongArray = LongArray(4),
val y: LongArray = LongArray(4),
)
internal object ECPoint {
@@ -117,7 +117,8 @@ internal object ECPoint {
val GX =
longArrayOf(
0x16F81798.toInt(),
6481385041966929816L, 188021827762530521L, 6170039885052185351L, 8772561819708210092L,
),
0x59F2815B.toInt(),
0x2DCE28D9.toInt(),
0x029BFCDB.toInt(),
@@ -128,7 +129,8 @@ internal object ECPoint {
)
val GY =
longArrayOf(
0xFB10D4B8.toInt(),
-7185545363635252040L, -209500633525038055L, 6747795201694173352L, 5204712524664259685L,
),
0x9C47D08F.toInt(),
0xA6855419.toInt(),
0xFD17B448.toInt(),
@@ -139,7 +141,7 @@ internal object ECPoint {
)
/** Curve constant b = 7 in y² = x³ + 7. */
private val B = longArrayOf(7, 0, 0, 0, 0, 0, 0, 0)
private val B = longArrayOf(7L, 0L, 0L, 0L)
/**
* wNAF window width for the G-side of scalar multiplication.
@@ -329,8 +331,8 @@ internal object ECPoint {
fun addMixed(
out: MutablePoint,
p: MutablePoint,
qx: IntArray,
qy: IntArray,
qx: LongArray,
qy: LongArray,
) {
if (p.isInfinity()) {
out.setAffine(qx, qy)
@@ -450,7 +452,7 @@ internal object ECPoint {
fun mul(
out: MutablePoint,
p: MutablePoint,
scalar: IntArray,
scalar: LongArray,
) {
if (U256.isZero(scalar) || p.isInfinity()) {
out.setInfinity()
@@ -513,7 +515,7 @@ internal object ECPoint {
*/
fun mulG(
out: MutablePoint,
scalar: IntArray,
scalar: LongArray,
) {
if (U256.isZero(scalar)) {
out.setInfinity()
@@ -559,9 +561,9 @@ internal object ECPoint {
*/
fun mulDoubleG(
out: MutablePoint,
s: IntArray,
s: LongArray,
p: MutablePoint,
e: IntArray,
e: LongArray,
) {
val wP = 5 // Window for P-side (table built per-call, keep small)
val pTableSize = 1 shl (wP - 2) // 8 entries for P
@@ -629,8 +631,8 @@ internal object ECPoint {
private fun addWnafMixed(
out: MutablePoint,
tmp: MutablePoint,
negY: IntArray,
wnafDigits: IntArray,
negY: LongArray,
wnafDigits: LongArray,
bitIndex: Int,
table: Array<AffinePoint>,
glvNeg: Boolean,
@@ -654,7 +656,7 @@ internal object ECPoint {
out: MutablePoint,
tmp: MutablePoint,
negScratch: MutablePoint,
wnafDigits: IntArray,
wnafDigits: LongArray,
bitIndex: Int,
table: Array<MutablePoint>,
glvNeg: Boolean,
@@ -685,8 +687,8 @@ internal object ECPoint {
*/
fun toAffine(
p: MutablePoint,
outX: IntArray,
outY: IntArray,
outX: LongArray,
outY: LongArray,
): Boolean {
if (p.isInfinity()) return false
val zInv = LongArray(4)
@@ -703,26 +705,26 @@ internal object ECPoint {
// ==================== Key Encoding (delegates to KeyCodec) ====================
fun liftX(
outX: IntArray,
outY: IntArray,
x: IntArray,
outX: LongArray,
outY: LongArray,
x: LongArray,
) = KeyCodec.liftX(outX, outY, x)
fun hasEvenY(y: IntArray) = KeyCodec.hasEvenY(y)
fun hasEvenY(y: LongArray) = KeyCodec.hasEvenY(y)
fun parsePublicKey(
pubkey: ByteArray,
outX: IntArray,
outY: IntArray,
outX: LongArray,
outY: LongArray,
) = KeyCodec.parsePublicKey(pubkey, outX, outY)
fun serializeUncompressed(
x: IntArray,
y: IntArray,
x: LongArray,
y: LongArray,
) = KeyCodec.serializeUncompressed(x, y)
fun serializeCompressed(
x: IntArray,
y: IntArray,
x: LongArray,
y: LongArray,
) = KeyCodec.serializeCompressed(x, y)
}
@@ -35,7 +35,8 @@ package com.vitorpamplona.quartz.utils.secp256k1
internal object ScalarN {
val N =
longArrayOf(
0xD0364141.toInt(),
-4624529908474429119L, -4994812053365940165L, -2L, -1L,
),
0xBFD25E8C.toInt(),
0xAF48A03B.toInt(),
0xBAAEDCE6.toInt(),
@@ -48,7 +49,8 @@ internal object ScalarN {
/** 2^256 - n: the small constant used for reduction (≈129 bits) */
private val N_COMPLEMENT =
longArrayOf(
0x2FC9BEBF.toInt(),
4624529908474429119L, 4994812053365940164L, 1L, 0L,
),
0x402DA173.toInt(),
0x50B75FC4.toInt(),
0x45512319.toInt(),
@@ -61,7 +63,8 @@ internal object ScalarN {
/** n - 2: exponent for Fermat inversion */
private val N_MINUS_2 =
longArrayOf(
0xD036413F.toInt(),
-4624529908474429121L, -4994812053365940165L, -2L, -1L,
),
0xBFD25E8C.toInt(),
0xAF48A03B.toInt(),
0xBAAEDCE6.toInt(),
@@ -72,10 +75,10 @@ internal object ScalarN {
)
/** Check if 0 < a < n (valid non-zero scalar). */
fun isValid(a: IntArray): Boolean = !U256.isZero(a) && U256.cmp(a, N) < 0
fun isValid(a: LongArray): Boolean = !U256.isZero(a) && U256.cmp(a, N) < 0
/** If a >= n, return a - n. Otherwise return a unchanged. */
fun reduce(a: IntArray): IntArray =
fun reduce(a: LongArray): LongArray =
if (U256.cmp(a, N) >= 0) {
val r = LongArray(4)
U256.subTo(r, a, N)
@@ -85,9 +88,9 @@ internal object ScalarN {
}
fun add(
a: IntArray,
b: IntArray,
): IntArray {
a: LongArray,
b: LongArray,
): LongArray {
val r = LongArray(4)
val carry = U256.addTo(r, a, b)
if (carry != 0) U256.addTo(r, r, N_COMPLEMENT)
@@ -96,9 +99,9 @@ internal object ScalarN {
}
fun sub(
a: IntArray,
b: IntArray,
): IntArray {
a: LongArray,
b: LongArray,
): LongArray {
val r = LongArray(4)
val borrow = U256.subTo(r, a, b)
if (borrow != 0) U256.addTo(r, r, N)
@@ -106,15 +109,15 @@ internal object ScalarN {
}
fun mul(
a: IntArray,
b: IntArray,
): IntArray {
a: LongArray,
b: LongArray,
): LongArray {
val w = LongArray(8)
U256.mulWide(w, a, b)
return reduceWide(w)
}
fun neg(a: IntArray): IntArray {
fun neg(a: LongArray): LongArray {
if (U256.isZero(a)) return LongArray(4)
val r = LongArray(4)
U256.subTo(r, N, a)
@@ -122,12 +125,12 @@ internal object ScalarN {
}
/** a^(-1) mod n via Fermat's little theorem. */
fun inv(a: IntArray): IntArray {
fun inv(a: LongArray): LongArray {
require(!U256.isZero(a))
return powModN(a, N_MINUS_2)
}
private fun reduceSelf(a: IntArray) {
private fun reduceSelf(a: LongArray) {
if (U256.cmp(a, N) >= 0) U256.subTo(a, a, N)
}
@@ -138,10 +141,10 @@ internal object ScalarN {
* Since N_COMPLEMENT is ~129 bits, hi × N_COMPLEMENT is ~385 bits. We repeat the
* reduction until the result fits in 256 bits, then do a final conditional subtraction.
*/
private fun reduceWide(w: IntArray): IntArray {
private fun reduceWide(w: LongArray): LongArray {
val lo = LongArray(4)
val hi = LongArray(4)
for (i in 0 until 8) {
for (i in 0 until 4) {
lo[i] = w[i]
hi[i] = w[i + 8]
}
@@ -155,9 +158,9 @@ internal object ScalarN {
U256.mulWide(hiTimesNC, hi, N_COMPLEMENT)
val sum = LongArray(8)
var carry = 0L
for (i in 0 until 16) {
carry += (hiTimesNC[i].toLong() and 0xFFFFFFFFL) +
if (i < 8) (lo[i].toLong() and 0xFFFFFFFFL) else 0L
for (i in 0 until 8) {
carry += (hiTimesNC[i]) +
if (i < 4) (lo[i]) else 0L
sum[i] = carry.toInt()
carry = carry ushr 32
}
@@ -165,7 +168,7 @@ internal object ScalarN {
// Round 2 if still > 256 bits
val lo2 = LongArray(4)
val hi2 = LongArray(4)
for (i in 0 until 8) {
for (i in 0 until 4) {
lo2[i] = sum[i]
hi2[i] = sum[i + 8]
}
@@ -178,24 +181,24 @@ internal object ScalarN {
U256.mulWide(hi2NC, hi2, N_COMPLEMENT)
var c2 = 0L
val result = LongArray(4)
for (i in 0 until 8) {
c2 += (lo2[i].toLong() and 0xFFFFFFFFL) + (hi2NC[i].toLong() and 0xFFFFFFFFL)
for (i in 0 until 4) {
c2 += (lo2[i]) + (hi2NC[i])
result[i] = c2.toInt()
c2 = c2 ushr 32
}
var overflow = c2
for (i in 8 until 16) overflow += (hi2NC[i].toLong() and 0xFFFFFFFFL)
for (i in 4 until 8) overflow += (hi2NC[i])
if (overflow > 0) {
val corr = IntArray(9)
val corr = LongArray(5)
var cc = 0L
for (i in 0 until 8) {
cc += (N_COMPLEMENT[i].toLong() and 0xFFFFFFFFL) * overflow
for (i in 0 until 4) {
cc += (N_COMPLEMENT[i]) * overflow
corr[i] = cc.toInt()
cc = cc ushr 32
}
var c3 = 0L
for (i in 0 until 8) {
c3 += (result[i].toLong() and 0xFFFFFFFFL) + (corr[i].toLong() and 0xFFFFFFFFL)
for (i in 0 until 4) {
c3 += (result[i]) + (corr[i])
result[i] = c3.toInt()
c3 = c3 ushr 32
}
@@ -205,9 +208,9 @@ internal object ScalarN {
}
private fun powModN(
base: IntArray,
exp: IntArray,
): IntArray {
base: LongArray,
exp: LongArray,
): LongArray {
val result = LongArray(4)
val b = base.copyOf()
var highBit = 255
@@ -227,7 +227,7 @@ object Secp256k1 {
*/
private fun signSchnorrInternal(
data: ByteArray,
d0: IntArray,
d0: LongArray,
pBytes: ByteArray,
pubKeyHasEvenY: Boolean,
auxrand: ByteArray?,
@@ -32,7 +32,7 @@ class FieldPTest {
s.chunked(2).map { it.toInt(16).toByte() }.toByteArray(),
)
private fun toHex(a: IntArray) = U256.toBytes(a).joinToString("") { "%02x".format(it) }
private fun toHex(a: LongArray) = U256.toBytes(a).joinToString("") { "%02x".format(it) }
// ==================== Basic identities ====================
@@ -26,7 +26,7 @@ import kotlin.test.assertTrue
/** Comprehensive tests for GLV endomorphism and wNAF encoding. */
class GlvTest {
private fun toHex(a: IntArray) = U256.toBytes(a).joinToString("") { "%02x".format(it) }
private fun toHex(a: LongArray) = U256.toBytes(a).joinToString("") { "%02x".format(it) }
private fun hex(s: String) =
U256.fromBytes(
@@ -210,7 +210,7 @@ class GlvTest {
// ==================== Helpers ====================
/** Reconstruct a scalar from wNAF digits using Horner's method. */
private fun reconstructWnaf(digits: IntArray): IntArray {
private fun reconstructWnaf(digits: LongArray): LongArray {
var acc = LongArray(4)
for (bit in digits.size - 1 downTo 0) {
val doubled = LongArray(4)
@@ -27,7 +27,7 @@ import kotlin.test.assertTrue
/** Tests for KeyCodec: key parsing, serialization, liftX, hasEvenY. */
class KeyCodecTest {
private fun toHex(a: IntArray) = U256.toBytes(a).joinToString("") { "%02x".format(it) }
private fun toHex(a: LongArray) = U256.toBytes(a).joinToString("") { "%02x".format(it) }
// ==================== liftX ====================
@@ -32,7 +32,7 @@ class PointTest {
s.chunked(2).map { it.toInt(16).toByte() }.toByteArray(),
)
private fun toHex(a: IntArray) = U256.toBytes(a).joinToString("") { "%02x".format(it) }
private fun toHex(a: LongArray) = U256.toBytes(a).joinToString("") { "%02x".format(it) }
// ==================== Generator point ====================
@@ -32,7 +32,7 @@ class ScalarNTest {
s.chunked(2).map { it.toInt(16).toByte() }.toByteArray(),
)
private fun toHex(a: IntArray) = U256.toBytes(a).joinToString("") { "%02x".format(it) }
private fun toHex(a: LongArray) = U256.toBytes(a).joinToString("") { "%02x".format(it) }
// ==================== isValid ====================
@@ -32,7 +32,7 @@ class U256Test {
s.chunked(2).map { it.toInt(16).toByte() }.toByteArray(),
)
private fun toHex(a: IntArray) = U256.toBytes(a).joinToString("") { "%02x".format(it) }
private fun toHex(a: LongArray) = U256.toBytes(a).joinToString("") { "%02x".format(it) }
// ==================== isZero / cmp ====================