feat: migrate secp256k1 from LongArray to Fe4/Wide8 structs

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

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

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

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

All secp256k1 unit tests pass on JVM.

https://claude.ai/code/session_01Sxi6Gpxbstuj3Y8TBY7XrU
This commit is contained in:
Claude
2026-04-10 03:30:53 +00:00
parent ea8b693785
commit a2ba64baba
23 changed files with 1391 additions and 1925 deletions
@@ -31,14 +31,14 @@ import kotlin.test.assertTrue
class FieldPTest {
private fun hex(s: String) = U256.fromBytes(s.hexToByteArray())
private fun toHex(a: LongArray) = U256.toBytes(a).toHexKey()
private fun toHex(a: Fe4) = U256.toBytes(a).toHexKey()
// ==================== Basic identities ====================
@Test
fun addZeroIdentity() {
val a = hex("67e56582298859ddae725f972992a07c6c4fb9f62a8fff58ce3ca926a1063530")
val zero = LongArray(4)
val zero = Fe4()
assertEquals(toHex(a), toHex(FieldP.add(a, zero)))
}
@@ -46,7 +46,7 @@ class FieldPTest {
fun subSelfIsZero() {
val a = hex("67e56582298859ddae725f972992a07c6c4fb9f62a8fff58ce3ca926a1063530")
val result = FieldP.sub(a, a)
assertTrue(U256.isZero(result))
assertTrue(result.isZero())
}
@Test
@@ -61,7 +61,7 @@ class FieldPTest {
@Test
fun mulOneIdentity() {
val a = hex("67e56582298859ddae725f972992a07c6c4fb9f62a8fff58ce3ca926a1063530")
val one = longArrayOf(1L, 0L, 0L, 0L)
val one = Fe4(1L, 0L, 0L, 0L)
assertEquals(toHex(a), toHex(FieldP.mul(a, one)))
}
@@ -71,9 +71,9 @@ class FieldPTest {
fun addNearP() {
// (p - 1) + 1 = p ≡ 0 (mod p)
val pMinus1 = hex("fffffffffffffffffffffffffffffffffffffffffffffffffffffffefffffc2e")
val one = longArrayOf(1L, 0L, 0L, 0L)
val one = Fe4(1L, 0L, 0L, 0L)
val result = FieldP.add(pMinus1, one)
assertTrue(U256.isZero(result))
assertTrue(result.isZero())
}
@Test
@@ -88,8 +88,8 @@ class FieldPTest {
@Test
fun subUnderflow() {
// 0 - 1 ≡ p - 1 (mod p)
val zero = LongArray(4)
val one = longArrayOf(1L, 0L, 0L, 0L)
val zero = Fe4()
val one = Fe4(1L, 0L, 0L, 0L)
val result = FieldP.sub(zero, one)
val expected = hex("fffffffffffffffffffffffffffffffffffffffffffffffffffffffefffffc2e") // p-1
assertEquals(toHex(expected), toHex(result))
@@ -105,14 +105,14 @@ class FieldPTest {
@Test
fun negZeroIsZero() {
assertTrue(U256.isZero(FieldP.neg(LongArray(4))))
assertTrue(U256.isZero(FieldP.neg(Fe4())))
}
@Test
fun addNegIsZero() {
val a = hex("67e56582298859ddae725f972992a07c6c4fb9f62a8fff58ce3ca926a1063530")
val result = FieldP.add(a, FieldP.neg(a))
assertTrue(U256.isZero(result))
assertTrue(result.isZero())
}
// ==================== Multiplication ====================
@@ -148,13 +148,13 @@ class FieldPTest {
val a = hex("67e56582298859ddae725f972992a07c6c4fb9f62a8fff58ce3ca926a1063530")
val aInv = FieldP.inv(a)
val product = FieldP.mul(a, aInv)
val one = longArrayOf(1L, 0L, 0L, 0L)
val one = Fe4(1L, 0L, 0L, 0L)
assertEquals(toHex(one), toHex(product))
}
@Test
fun invOfOne() {
val one = longArrayOf(1L, 0L, 0L, 0L)
val one = Fe4(1L, 0L, 0L, 0L)
assertEquals(toHex(one), toHex(FieldP.inv(one)))
}
@@ -169,29 +169,33 @@ class FieldPTest {
@Test
fun halfOfEven() {
val out = LongArray(4)
val four = longArrayOf(4L, 0L, 0L, 0L)
val out = Fe4()
val four = Fe4(4L, 0L, 0L, 0L)
FieldP.half(out, four)
assertEquals(2L, out[0])
for (i in 1 until 4) assertEquals(0L, out[i])
assertEquals(2L, out.l0)
assertEquals(0L, out.l1)
assertEquals(0L, out.l2)
assertEquals(0L, out.l3)
}
@Test
fun halfOfOdd() {
// half(1) = (1 + p) / 2 = (p + 1) / 2
val out = LongArray(4)
val one = longArrayOf(1L, 0L, 0L, 0L)
val out = Fe4()
val one = Fe4(1L, 0L, 0L, 0L)
FieldP.half(out, one)
// Verify: 2 * half(1) = 1 mod p
val doubled = FieldP.add(out, out)
assertEquals(1L, doubled[0])
for (i in 1 until 4) assertEquals(0L, doubled[i])
assertEquals(1L, doubled.l0)
assertEquals(0L, doubled.l1)
assertEquals(0L, doubled.l2)
assertEquals(0L, doubled.l3)
}
@Test
fun halfThenDoubleRoundTrips() {
val a = hex("67e56582298859ddae725f972992a07c6c4fb9f62a8fff58ce3ca926a1063530")
val out = LongArray(4)
val out = Fe4()
FieldP.half(out, a)
val doubled = FieldP.add(out, out)
assertEquals(toHex(a), toHex(doubled))
@@ -212,7 +216,7 @@ class FieldPTest {
@Test
fun sqrtOfNonResidue() {
// 3 is not a quadratic residue mod p (for secp256k1's p)
val three = longArrayOf(3, 0L, 0L, 0L)
val three = Fe4(3, 0L, 0L, 0L)
assertNull(FieldP.sqrt(three))
}
@@ -222,7 +226,7 @@ class FieldPTest {
val gx = ECPoint.GX
val gy = ECPoint.GY
val x3 = FieldP.mul(FieldP.sqr(gx), gx)
val y2 = FieldP.add(x3, longArrayOf(7, 0L, 0L, 0L))
val y2 = FieldP.add(x3, Fe4(7, 0L, 0L, 0L))
val root = FieldP.sqrt(y2)!!
// root should be gy or -gy
val isGy = U256.cmp(root, gy) == 0
@@ -239,7 +243,7 @@ class FieldPTest {
val result = FieldP.mul(pMinus1, pMinus1)
assertTrue(U256.cmp(result, FieldP.P) < 0, "Result should be < p")
// (p-1)² ≡ 1 (mod p)
val one = longArrayOf(1L, 0L, 0L, 0L)
val one = Fe4(1L, 0L, 0L, 0L)
assertEquals(toHex(one), toHex(result))
}
@@ -249,24 +253,24 @@ class FieldPTest {
fun inPlaceAdd() {
val a = hex("0000000000000000000000000000000000000000000000000000000000000005")
val b = hex("0000000000000000000000000000000000000000000000000000000000000003")
val out = LongArray(4)
val out = Fe4()
FieldP.add(out, a, b)
assertEquals(8L, out[0])
assertEquals(8L, out.l0)
}
@Test
fun inPlaceSqr() {
val a = hex("0000000000000000000000000000000000000000000000000000000000000005")
val out = LongArray(4)
val out = Fe4()
FieldP.sqr(out, a)
assertEquals(25L, out[0]) // 5² = 25
assertEquals(25L, out.l0) // 5² = 25
}
@Test
fun halfOfPMinus1() {
// half(p-1) should equal (p-1)/2
val pMinus1 = hex("fffffffffffffffffffffffffffffffffffffffffffffffffffffffefffffc2e")
val out = LongArray(4)
val out = Fe4()
FieldP.half(out, pMinus1)
// Verify: 2 * half(p-1) = p-1
val doubled = FieldP.add(out, out)
@@ -275,23 +279,23 @@ class FieldPTest {
@Test
fun invOfTwo() {
val two = longArrayOf(2, 0L, 0L, 0L)
val two = Fe4(2, 0L, 0L, 0L)
val inv2 = FieldP.inv(two)
val product = FieldP.mul(two, inv2)
val one = longArrayOf(1L, 0L, 0L, 0L)
val one = Fe4(1L, 0L, 0L, 0L)
assertEquals(toHex(one), toHex(product))
}
@Test
fun sqrtOfZero() {
val zero = LongArray(4)
val zero = Fe4()
val root = FieldP.sqrt(zero)
assertTrue(root != null && U256.isZero(root))
assertTrue(root != null && root.isZero())
}
@Test
fun sqrtOfOne() {
val one = longArrayOf(1L, 0L, 0L, 0L)
val one = Fe4(1L, 0L, 0L, 0L)
val root = FieldP.sqrt(one)!!
assertEquals(toHex(one), toHex(root))
}
@@ -28,7 +28,7 @@ import kotlin.test.assertTrue
/** Comprehensive tests for GLV endomorphism and wNAF encoding. */
class GlvTest {
private fun toHex(a: LongArray) = U256.toBytes(a).toHexKey()
private fun toHex(a: Fe4) = U256.toBytes(a).toHexKey()
private fun hex(s: String) = U256.fromBytes(s.hexToByteArray())
@@ -49,7 +49,7 @@ class GlvTest {
@Test
fun splitScalarZero() {
val split = Glv.splitScalar(LongArray(4))
val split = Glv.splitScalar(Fe4())
assertTrue(U256.isZero(split.k1) && U256.isZero(split.k2))
}
@@ -68,11 +68,11 @@ class GlvTest {
// Both k₁ and k₂ should be ~128 bits (fit in 4 limbs)
val k = hex("67e56582298859ddae725f972992a07c6c4fb9f62a8fff58ce3ca926a1063530")
val split = Glv.splitScalar(k)
// Upper 4 limbs should be zero for a proper 128-bit half-scalar
for (i in 2 until 4) {
assertEquals(0, split.k1[i], "k1 limb $i should be 0")
assertEquals(0, split.k2[i], "k2 limb $i should be 0")
}
// Upper 2 limbs should be zero for a proper 128-bit half-scalar
assertEquals(0L, split.k1.l2, "k1 limb 2 should be 0")
assertEquals(0L, split.k1.l3, "k1 limb 3 should be 0")
assertEquals(0L, split.k2.l2, "k2 limb 2 should be 0")
assertEquals(0L, split.k2.l3, "k2 limb 3 should be 0")
}
@Test
@@ -105,7 +105,7 @@ class GlvTest {
// β³ ≡ 1 (mod p) — the defining property of the cube root of unity
val b2 = FieldP.sqr(Glv.BETA)
val b3 = FieldP.mul(b2, Glv.BETA)
val one = longArrayOf(1L, 0L, 0L, 0L, 0, 0, 0, 0)
val one = Fe4(1L, 0L, 0L, 0L)
assertEquals(toHex(one), toHex(b3))
}
@@ -114,8 +114,8 @@ class GlvTest {
// λ·G should equal (β·Gx, Gy)
val result = MutablePoint()
ECPoint.mulG(result, LAMBDA)
val rx = LongArray(4)
val ry = LongArray(4)
val rx = Fe4()
val ry = Fe4()
ECPoint.toAffine(result, rx, ry)
assertEquals(toHex(FieldP.mul(ECPoint.GX, Glv.BETA)), toHex(rx))
assertEquals(toHex(ECPoint.GY), toHex(ry))
@@ -126,9 +126,10 @@ class GlvTest {
@Test
fun wnafReconstructionSmall() {
// wNAF digits should reconstruct to the original scalar
val k = longArrayOf(17L, 0L, 0L, 0L) // 17 = 10001 in binary
val k = Fe4(17L, 0L, 0L, 0L) // 17 = 10001 in binary
val digits = Glv.wnaf(k, 5, 256)
assertEquals(k[0], reconstructWnaf(digits)[0])
val reconstructed = reconstructWnaf(digits)
assertEquals(0, U256.cmp(k, reconstructed))
}
@Test
@@ -136,7 +137,7 @@ class GlvTest {
val k = hex("e907831f80848d1069a5371b402410364bdf1c5f8307b0084c55f1ce2dca8215")
val digits = Glv.wnaf(k, 5, 256)
val reconstructed = reconstructWnaf(digits)
for (i in 0 until 4) assertEquals(k[i], reconstructed[i], "Limb $i mismatch")
assertEquals(0, U256.cmp(k, reconstructed), "wNAF reconstruction mismatch")
}
@Test
@@ -146,7 +147,7 @@ class GlvTest {
val k = hex("944946c56e0d133f326db0b0645544a04bfcc5a0f447ad6d0227958414d8ba73")
val digits = Glv.wnaf(k, 5, 256)
val reconstructed = reconstructWnaf(digits)
for (i in 0 until 4) assertEquals(k[i], reconstructed[i], "Limb $i mismatch for carry test")
assertEquals(0, U256.cmp(k, reconstructed), "wNAF carry overflow reconstruction mismatch")
}
@Test
@@ -179,10 +180,10 @@ class GlvTest {
@Test
fun wnafSmallMaxBits() {
// wNAF with maxBits=129 (used for GLV half-scalars)
val k = longArrayOf(-7296712173568108936L, 2459565876494606609L, 0L, 0L)
val k = Fe4(-7296712173568108936L, 2459565876494606609L, 0L, 0L)
val digits = Glv.wnaf(k, 5, 129)
val reconstructed = reconstructWnaf(digits)
for (i in 0 until 4) assertEquals(k[i], reconstructed[i], "Limb $i mismatch for 129-bit wNAF")
assertEquals(0, U256.cmp(k, reconstructed), "129-bit wNAF reconstruction mismatch")
}
// ==================== Integration: GLV mulDoubleG ====================
@@ -192,16 +193,16 @@ class GlvTest {
// s·G + 0·P = s·G
val s = hex("67e56582298859ddae725f972992a07c6c4fb9f62a8fff58ce3ca926a1063530")
val p = MutablePoint()
ECPoint.mulG(p, longArrayOf(2L, 0L, 0L, 0L, 0, 0, 0, 0))
ECPoint.mulG(p, Fe4(2L, 0L, 0L, 0L))
val combined = MutablePoint()
ECPoint.mulDoubleG(combined, s, p, LongArray(4))
val cx = LongArray(4)
val cy = LongArray(4)
ECPoint.mulDoubleG(combined, s, p, Fe4())
val cx = Fe4()
val cy = Fe4()
ECPoint.toAffine(combined, cx, cy)
val direct = MutablePoint()
ECPoint.mulG(direct, s)
val dx = LongArray(4)
val dy = LongArray(4)
val dx = Fe4()
val dy = Fe4()
ECPoint.toAffine(direct, dx, dy)
assertEquals(toHex(dx), toHex(cx))
}
@@ -209,37 +210,38 @@ class GlvTest {
// ==================== Helpers ====================
/** Reconstruct a scalar from wNAF digits using Horner's method. */
private fun reconstructWnaf(digits: IntArray): LongArray {
var acc = LongArray(4)
private fun reconstructWnaf(digits: IntArray): Fe4 {
var acc = Fe4()
for (bit in digits.size - 1 downTo 0) {
// Double: acc = acc * 2 (unsigned shift left by 1)
val doubled = LongArray(4)
var shiftCarry = 0L
for (j in 0 until 4) {
doubled[j] = (acc[j] shl 1) or shiftCarry
shiftCarry = acc[j] ushr 63
}
val doubled = Fe4()
doubled.l0 = (acc.l0 shl 1)
doubled.l1 = (acc.l1 shl 1) or (acc.l0 ushr 63)
doubled.l2 = (acc.l2 shl 1) or (acc.l1 ushr 63)
doubled.l3 = (acc.l3 shl 1) or (acc.l2 ushr 63)
acc = doubled
// Add digit
val d = digits[bit]
if (d > 0) {
val s = acc[0] + d.toLong()
val c = if (s.toULong() < acc[0].toULong()) 1L else 0L
acc[0] = s
val s = acc.l0 + d.toLong()
val c = if (s.toULong() < acc.l0.toULong()) 1L else 0L
acc.l0 = s
if (c != 0L) {
for (j in 1 until 4) {
acc[j]++
if (acc[j] != 0L) break
acc.l1++
if (acc.l1 == 0L) {
acc.l2++
if (acc.l2 == 0L) acc.l3++
}
}
} else if (d < 0) {
val s = acc[0] - (-d).toLong()
val b = if (acc[0].toULong() < (-d).toULong()) 1L else 0L
acc[0] = s
val s = acc.l0 - (-d).toLong()
val b = if (acc.l0.toULong() < (-d).toULong()) 1L else 0L
acc.l0 = s
if (b != 0L) {
for (j in 1 until 4) {
acc[j]--
if (acc[j] != -1L) break
acc.l1--
if (acc.l1 == -1L) {
acc.l2--
if (acc.l2 == -1L) acc.l3--
}
}
}
@@ -28,14 +28,14 @@ import kotlin.test.assertTrue
/** Tests for KeyCodec: key parsing, serialization, liftX, hasEvenY. */
class KeyCodecTest {
private fun toHex(a: LongArray) = U256.toBytes(a).toHexKey()
private fun toHex(a: Fe4) = U256.toBytes(a).toHexKey()
// ==================== liftX ====================
@Test
fun liftXGenerator() {
val x = LongArray(4)
val y = LongArray(4)
val x = Fe4()
val y = Fe4()
assertTrue(KeyCodec.liftX(x, y, ECPoint.GX))
assertEquals(toHex(ECPoint.GX), toHex(x))
assertTrue(KeyCodec.hasEvenY(y))
@@ -44,25 +44,25 @@ class KeyCodecTest {
@Test
fun liftXInvalidFieldElement() {
// p itself is not a valid x
val x = LongArray(4)
val y = LongArray(4)
val x = Fe4()
val y = Fe4()
assertFalse(KeyCodec.liftX(x, y, FieldP.P))
}
@Test
fun liftXNotOnCurve() {
// x=2: y² = 8+7 = 15. 15 is not a quadratic residue mod p.
val x = LongArray(4)
val y = LongArray(4)
val two = longArrayOf(2, 0L, 0L, 0L)
val x = Fe4()
val y = Fe4()
val two = Fe4(2, 0L, 0L, 0L)
// This may or may not be on the curve — just check it doesn't crash
KeyCodec.liftX(x, y, two) // result doesn't matter, just no exception
}
@Test
fun liftXAlwaysReturnsEvenY() {
val x = LongArray(4)
val y = LongArray(4)
val x = Fe4()
val y = Fe4()
assertTrue(KeyCodec.liftX(x, y, ECPoint.GX))
assertTrue(KeyCodec.hasEvenY(y), "liftX should always return even y")
}
@@ -71,14 +71,14 @@ class KeyCodecTest {
@Test
fun hasEvenYForEvenValue() {
assertTrue(KeyCodec.hasEvenY(longArrayOf(2, 0L, 0L, 0L)))
assertTrue(KeyCodec.hasEvenY(longArrayOf(0, 0L, 0L, 0L)))
assertTrue(KeyCodec.hasEvenY(Fe4(2, 0L, 0L, 0L)))
assertTrue(KeyCodec.hasEvenY(Fe4(0, 0L, 0L, 0L)))
}
@Test
fun hasEvenYForOddValue() {
assertFalse(KeyCodec.hasEvenY(longArrayOf(1, 0L, 0L, 0L)))
assertFalse(KeyCodec.hasEvenY(longArrayOf(3, 0L, 0L, 0L)))
assertFalse(KeyCodec.hasEvenY(Fe4(1, 0L, 0L, 0L)))
assertFalse(KeyCodec.hasEvenY(Fe4(3, 0L, 0L, 0L)))
}
// ==================== parsePublicKey ====================
@@ -87,8 +87,8 @@ class KeyCodecTest {
fun parseCompressedEvenY() {
val compressed = KeyCodec.serializeCompressed(ECPoint.GX, ECPoint.GY)
assertEquals(0x02.toByte(), compressed[0])
val x = LongArray(4)
val y = LongArray(4)
val x = Fe4()
val y = Fe4()
assertTrue(KeyCodec.parsePublicKey(compressed, x, y))
assertEquals(toHex(ECPoint.GX), toHex(x))
assertEquals(toHex(ECPoint.GY), toHex(y))
@@ -99,8 +99,8 @@ class KeyCodecTest {
val negGy = FieldP.neg(ECPoint.GY)
val compressed = KeyCodec.serializeCompressed(ECPoint.GX, negGy)
assertEquals(0x03.toByte(), compressed[0])
val x = LongArray(4)
val y = LongArray(4)
val x = Fe4()
val y = Fe4()
assertTrue(KeyCodec.parsePublicKey(compressed, x, y))
assertEquals(toHex(ECPoint.GX), toHex(x))
assertEquals(toHex(negGy), toHex(y))
@@ -110,8 +110,8 @@ class KeyCodecTest {
fun parseUncompressed() {
val uncompressed = KeyCodec.serializeUncompressed(ECPoint.GX, ECPoint.GY)
assertEquals(0x04.toByte(), uncompressed[0])
val x = LongArray(4)
val y = LongArray(4)
val x = Fe4()
val y = Fe4()
assertTrue(KeyCodec.parsePublicKey(uncompressed, x, y))
assertEquals(toHex(ECPoint.GX), toHex(x))
assertEquals(toHex(ECPoint.GY), toHex(y))
@@ -119,8 +119,8 @@ class KeyCodecTest {
@Test
fun parseInvalidSizes() {
val x = LongArray(4)
val y = LongArray(4)
val x = Fe4()
val y = Fe4()
assertFalse(KeyCodec.parsePublicKey(ByteArray(0), x, y))
assertFalse(KeyCodec.parsePublicKey(ByteArray(10), x, y))
assertFalse(KeyCodec.parsePublicKey(ByteArray(32), x, y))
@@ -131,8 +131,8 @@ class KeyCodecTest {
@Test
fun parseInvalidPrefix() {
val x = LongArray(4)
val y = LongArray(4)
val x = Fe4()
val y = Fe4()
assertFalse(KeyCodec.parsePublicKey(ByteArray(33), x, y)) // prefix 0x00
assertFalse(KeyCodec.parsePublicKey(ByteArray(65), x, y)) // prefix 0x00
}
@@ -144,8 +144,8 @@ class KeyCodecTest {
fake[0] = 0x04
fake[1] = 0x01 // x = 1 (padded)
fake[33] = 0x01 // y = 1 (padded) — 1² ≠ 1³ + 7
val x = LongArray(4)
val y = LongArray(4)
val x = Fe4()
val y = Fe4()
assertFalse(KeyCodec.parsePublicKey(fake, x, y))
}
@@ -154,8 +154,8 @@ class KeyCodecTest {
@Test
fun compressDecompressRoundTrip() {
val compressed = KeyCodec.serializeCompressed(ECPoint.GX, ECPoint.GY)
val x = LongArray(4)
val y = LongArray(4)
val x = Fe4()
val y = Fe4()
assertTrue(KeyCodec.parsePublicKey(compressed, x, y))
val recompressed = KeyCodec.serializeCompressed(x, y)
assertEquals(compressed.toList(), recompressed.toList())
@@ -164,8 +164,8 @@ class KeyCodecTest {
@Test
fun uncompressedRoundTrip() {
val uncompressed = KeyCodec.serializeUncompressed(ECPoint.GX, ECPoint.GY)
val x = LongArray(4)
val y = LongArray(4)
val x = Fe4()
val y = Fe4()
assertTrue(KeyCodec.parsePublicKey(uncompressed, x, y))
val reser = KeyCodec.serializeUncompressed(x, y)
assertEquals(uncompressed.toList(), reser.toList())
@@ -31,7 +31,7 @@ import kotlin.test.assertTrue
class PointTest {
private fun hex(s: String) = U256.fromBytes(s.hexToByteArray())
private fun toHex(a: LongArray) = U256.toBytes(a).toHexKey()
private fun toHex(a: Fe4) = U256.toBytes(a).toHexKey()
// ==================== Generator point ====================
@@ -39,7 +39,7 @@ class PointTest {
fun generatorIsOnCurve() {
// y² = x³ + 7
val x3 = FieldP.mul(FieldP.sqr(ECPoint.GX), ECPoint.GX)
val y2expected = FieldP.add(x3, longArrayOf(7, 0L, 0L, 0L))
val y2expected = FieldP.add(x3, Fe4(7, 0L, 0L, 0L))
val y2actual = FieldP.sqr(ECPoint.GY)
assertEquals(toHex(y2expected), toHex(y2actual))
}
@@ -53,16 +53,16 @@ class PointTest {
p.setAffine(ECPoint.GX, ECPoint.GY)
val doubled = MutablePoint()
ECPoint.doublePoint(doubled, p)
val dx = LongArray(4)
val dy = LongArray(4)
val dx = Fe4()
val dy = Fe4()
ECPoint.toAffine(doubled, dx, dy)
// 2·G via scalar multiplication
val two = longArrayOf(2, 0L, 0L, 0L)
val two = Fe4(2, 0L, 0L, 0L)
val mulResult = MutablePoint()
ECPoint.mulG(mulResult, two)
val mx = LongArray(4)
val my = LongArray(4)
val mx = Fe4()
val my = Fe4()
ECPoint.toAffine(mulResult, mx, my)
assertEquals(toHex(mx), toHex(dx))
@@ -75,15 +75,15 @@ class PointTest {
val p = MutablePoint()
p.setAffine(ECPoint.GX, ECPoint.GY)
ECPoint.doublePoint(p, p)
val x = LongArray(4)
val y = LongArray(4)
val x = Fe4()
val y = Fe4()
ECPoint.toAffine(p, x, y)
val two = longArrayOf(2, 0L, 0L, 0L)
val two = Fe4(2, 0L, 0L, 0L)
val expected = MutablePoint()
ECPoint.mulG(expected, two)
val ex = LongArray(4)
val ey = LongArray(4)
val ex = Fe4()
val ey = Fe4()
ECPoint.toAffine(expected, ex, ey)
assertEquals(toHex(ex), toHex(x))
@@ -106,14 +106,14 @@ class PointTest {
g.setAffine(ECPoint.GX, ECPoint.GY)
val sum = MutablePoint()
ECPoint.addPoints(sum, g, g)
val sx = LongArray(4)
val sy = LongArray(4)
val sx = Fe4()
val sy = Fe4()
ECPoint.toAffine(sum, sx, sy)
val doubled = MutablePoint()
ECPoint.doublePoint(doubled, g)
val dx = LongArray(4)
val dy = LongArray(4)
val dx = Fe4()
val dy = Fe4()
ECPoint.toAffine(doubled, dx, dy)
assertEquals(toHex(dx), toHex(sx))
@@ -129,15 +129,15 @@ class PointTest {
val result = MutablePoint()
ECPoint.addPoints(result, g, inf)
val rx = LongArray(4)
val ry = LongArray(4)
val rx = Fe4()
val ry = Fe4()
ECPoint.toAffine(result, rx, ry)
assertEquals(toHex(ECPoint.GX), toHex(rx))
val result2 = MutablePoint()
ECPoint.addPoints(result2, inf, g)
val r2x = LongArray(4)
val r2y = LongArray(4)
val r2x = Fe4()
val r2y = Fe4()
ECPoint.toAffine(result2, r2x, r2y)
assertEquals(toHex(ECPoint.GX), toHex(r2x))
}
@@ -160,15 +160,15 @@ class PointTest {
@Test
fun addMixedMatchesFull() {
// addMixed should produce the same result as addPoints when q is affine
val three = longArrayOf(3, 0L, 0L, 0L)
val three = Fe4(3, 0L, 0L, 0L)
val p = MutablePoint()
ECPoint.mulG(p, three) // 3G in Jacobian (z ≠ 1)
// Add G as affine
val mixed = MutablePoint()
ECPoint.addMixed(mixed, p, ECPoint.GX, ECPoint.GY)
val mx = LongArray(4)
val my = LongArray(4)
val mx = Fe4()
val my = Fe4()
ECPoint.toAffine(mixed, mx, my)
// Add G as Jacobian
@@ -176,8 +176,8 @@ class PointTest {
gJac.setAffine(ECPoint.GX, ECPoint.GY)
val full = MutablePoint()
ECPoint.addPoints(full, p, gJac)
val fx = LongArray(4)
val fy = LongArray(4)
val fx = Fe4()
val fy = Fe4()
ECPoint.toAffine(full, fx, fy)
assertEquals(toHex(fx), toHex(mx))
@@ -190,8 +190,8 @@ class PointTest {
inf.setInfinity()
val result = MutablePoint()
ECPoint.addMixed(result, inf, ECPoint.GX, ECPoint.GY)
val rx = LongArray(4)
val ry = LongArray(4)
val rx = Fe4()
val ry = Fe4()
ECPoint.toAffine(result, rx, ry)
assertEquals(toHex(ECPoint.GX), toHex(rx))
}
@@ -200,11 +200,11 @@ class PointTest {
@Test
fun mulGByOne() {
val one = longArrayOf(1, 0L, 0L, 0L)
val one = Fe4(1, 0L, 0L, 0L)
val result = MutablePoint()
ECPoint.mulG(result, one)
val rx = LongArray(4)
val ry = LongArray(4)
val rx = Fe4()
val ry = Fe4()
ECPoint.toAffine(result, rx, ry)
assertEquals(toHex(ECPoint.GX), toHex(rx))
assertEquals(toHex(ECPoint.GY), toHex(ry))
@@ -212,7 +212,7 @@ class PointTest {
@Test
fun mulGByZeroIsInfinity() {
val zero = LongArray(4)
val zero = Fe4()
val result = MutablePoint()
ECPoint.mulG(result, zero)
assertTrue(result.isInfinity())
@@ -232,16 +232,16 @@ class PointTest {
val k = hex("67e56582298859ddae725f972992a07c6c4fb9f62a8fff58ce3ca926a1063530")
val gResult = MutablePoint()
ECPoint.mulG(gResult, k)
val gx = LongArray(4)
val gy = LongArray(4)
val gx = Fe4()
val gy = Fe4()
ECPoint.toAffine(gResult, gx, gy)
val g = MutablePoint()
g.setAffine(ECPoint.GX, ECPoint.GY)
val mResult = MutablePoint()
ECPoint.mul(mResult, g, k)
val mx = LongArray(4)
val my = LongArray(4)
val mx = Fe4()
val my = Fe4()
ECPoint.toAffine(mResult, mx, my)
assertEquals(toHex(mx), toHex(gx))
@@ -255,14 +255,14 @@ class PointTest {
val e = hex("3982f19bef1615bccfbb05e321c10e1d4cba3df0e841c2e41eeb6016347653c3")
val p = MutablePoint()
val two = longArrayOf(2, 0L, 0L, 0L)
val two = Fe4(2, 0L, 0L, 0L)
ECPoint.mulG(p, two) // P = 2·G
// Combined
val combined = MutablePoint()
ECPoint.mulDoubleG(combined, s, p, e)
val cx = LongArray(4)
val cy = LongArray(4)
val cx = Fe4()
val cy = Fe4()
ECPoint.toAffine(combined, cx, cy)
// Separate
@@ -272,8 +272,8 @@ class PointTest {
ECPoint.mul(eP, p, e)
val sep = MutablePoint()
ECPoint.addPoints(sep, sG, eP)
val sx = LongArray(4)
val sy = LongArray(4)
val sx = Fe4()
val sy = Fe4()
ECPoint.toAffine(sep, sx, sy)
assertEquals(toHex(sx), toHex(cx))
@@ -284,8 +284,8 @@ class PointTest {
@Test
fun liftXGenerator() {
val x = LongArray(4)
val y = LongArray(4)
val x = Fe4()
val y = Fe4()
assertTrue(KeyCodec.liftX(x, y, ECPoint.GX))
assertEquals(toHex(ECPoint.GX), toHex(x))
// liftX returns even y
@@ -295,8 +295,8 @@ class PointTest {
@Test
fun liftXInvalidX() {
// p itself is not a valid x coordinate
val x = LongArray(4)
val y = LongArray(4)
val x = Fe4()
val y = Fe4()
assertFalse(KeyCodec.liftX(x, y, FieldP.P))
}
@@ -305,8 +305,8 @@ class PointTest {
@Test
fun compressDecompressRoundTrip() {
val compressed = KeyCodec.serializeCompressed(ECPoint.GX, ECPoint.GY)
val x = LongArray(4)
val y = LongArray(4)
val x = Fe4()
val y = Fe4()
assertTrue(KeyCodec.parsePublicKey(compressed, x, y))
assertEquals(toHex(ECPoint.GX), toHex(x))
assertEquals(toHex(ECPoint.GY), toHex(y))
@@ -315,8 +315,8 @@ class PointTest {
@Test
fun uncompressedRoundTrip() {
val uncompressed = KeyCodec.serializeUncompressed(ECPoint.GX, ECPoint.GY)
val x = LongArray(4)
val y = LongArray(4)
val x = Fe4()
val y = Fe4()
assertTrue(KeyCodec.parsePublicKey(uncompressed, x, y))
assertEquals(toHex(ECPoint.GX), toHex(x))
assertEquals(toHex(ECPoint.GY), toHex(y))
@@ -324,8 +324,8 @@ class PointTest {
@Test
fun parseInvalidKey() {
val x = LongArray(4)
val y = LongArray(4)
val x = Fe4()
val y = Fe4()
assertFalse(KeyCodec.parsePublicKey(ByteArray(10), x, y))
assertFalse(KeyCodec.parsePublicKey(ByteArray(33), x, y)) // wrong prefix (0x00)
}
@@ -337,13 +337,13 @@ class PointTest {
p.setAffine(ECPoint.GX, ECPoint.GY)
val result = MutablePoint()
ECPoint.addMixed(result, p, ECPoint.GX, ECPoint.GY)
val rx = LongArray(4)
val ry = LongArray(4)
val rx = Fe4()
val ry = Fe4()
ECPoint.toAffine(result, rx, ry)
val doubled = MutablePoint()
ECPoint.doublePoint(doubled, p)
val dx = LongArray(4)
val dy = LongArray(4)
val dx = Fe4()
val dy = Fe4()
ECPoint.toAffine(doubled, dx, dy)
assertEquals(toHex(dx), toHex(rx))
}
@@ -364,8 +364,8 @@ class PointTest {
val pubkey = Secp256k1.pubkeyCreate(privKeyBytes)
val compressed = Secp256k1.pubKeyCompress(pubkey)
assertEquals(0x03.toByte(), compressed[0]) // Odd y → 03 prefix
val x = LongArray(4)
val y = LongArray(4)
val x = Fe4()
val y = Fe4()
assertTrue(KeyCodec.parsePublicKey(compressed, x, y))
// Round-trip: compress again should give same result
val recompressed = KeyCodec.serializeCompressed(x, y)
@@ -31,7 +31,7 @@ import kotlin.test.assertTrue
class ScalarNTest {
private fun hex(s: String) = U256.fromBytes(s.hexToByteArray())
private fun toHex(a: LongArray) = U256.toBytes(a).toHexKey()
private fun toHex(a: Fe4) = U256.toBytes(a).toHexKey()
// ==================== isValid ====================
@@ -42,7 +42,7 @@ class ScalarNTest {
@Test
fun isValidZero() {
assertFalse(ScalarN.isValid(LongArray(4)))
assertFalse(ScalarN.isValid(Fe4()))
}
@Test
@@ -62,7 +62,7 @@ class ScalarNTest {
@Test
fun addZeroIdentity() {
val a = hex("67e56582298859ddae725f972992a07c6c4fb9f62a8fff58ce3ca926a1063530")
assertEquals(toHex(a), toHex(ScalarN.add(a, LongArray(4))))
assertEquals(toHex(a), toHex(ScalarN.add(a, Fe4())))
}
@Test
@@ -95,7 +95,7 @@ class ScalarNTest {
@Test
fun mulOneIdentity() {
val a = hex("67e56582298859ddae725f972992a07c6c4fb9f62a8fff58ce3ca926a1063530")
val one = longArrayOf(1, 0L, 0L, 0L)
val one = Fe4(1, 0L, 0L, 0L)
assertEquals(toHex(a), toHex(ScalarN.mul(a, one)))
}
@@ -123,7 +123,7 @@ class ScalarNTest {
val a = hex("67e56582298859ddae725f972992a07c6c4fb9f62a8fff58ce3ca926a1063530")
val aInv = ScalarN.inv(a)
val product = ScalarN.mul(a, aInv)
val one = longArrayOf(1, 0L, 0L, 0L)
val one = Fe4(1, 0L, 0L, 0L)
assertEquals(toHex(one), toHex(product))
}
@@ -133,7 +133,7 @@ class ScalarNTest {
fun addNearN() {
// (n-1) + 1 should wrap to 0
val nMinus1 = hex("fffffffffffffffffffffffffffffffebaaedce6af48a03bbfd25e8cd0364140")
val one = longArrayOf(1, 0L, 0L, 0L)
val one = Fe4(1, 0L, 0L, 0L)
assertTrue(U256.isZero(ScalarN.add(nMinus1, one)))
}
@@ -141,9 +141,9 @@ class ScalarNTest {
fun addNearNWrap() {
// (n-1) + 2 should give 1
val nMinus1 = hex("fffffffffffffffffffffffffffffffebaaedce6af48a03bbfd25e8cd0364140")
val two = longArrayOf(2, 0L, 0L, 0L)
val two = Fe4(2, 0L, 0L, 0L)
val result = ScalarN.add(nMinus1, two)
val one = longArrayOf(1, 0L, 0L, 0L)
val one = Fe4(1, 0L, 0L, 0L)
assertEquals(toHex(one), toHex(result))
}
@@ -152,19 +152,19 @@ class ScalarNTest {
// (n-1) * (n-1) ≡ 1 mod n (since (n-1) ≡ -1 and (-1)² = 1)
val nMinus1 = hex("fffffffffffffffffffffffffffffffebaaedce6af48a03bbfd25e8cd0364140")
val result = ScalarN.mul(nMinus1, nMinus1)
val one = longArrayOf(1, 0L, 0L, 0L)
val one = Fe4(1, 0L, 0L, 0L)
assertEquals(toHex(one), toHex(result))
}
@Test
fun negOfZeroIsZero() {
assertTrue(U256.isZero(ScalarN.neg(LongArray(4))))
assertTrue(U256.isZero(ScalarN.neg(Fe4())))
}
@Test
fun reduceOfN() {
val result = ScalarN.reduce(ScalarN.N.copyOf())
assertTrue(U256.isZero(result))
assertTrue(result.isZero())
}
@Test
@@ -31,18 +31,18 @@ import kotlin.test.assertTrue
class U256Test {
private fun hex(s: String) = U256.fromBytes(s.hexToByteArray())
private fun toHex(a: LongArray) = U256.toBytes(a).toHexKey()
private fun toHex(a: Fe4) = U256.toBytes(a).toHexKey()
// ==================== isZero / cmp ====================
@Test
fun isZeroTrue() = assertTrue(U256.isZero(LongArray(4)))
fun isZeroTrue() = assertTrue(U256.isZero(Fe4()))
@Test
fun isZeroFalse() = assertFalse(U256.isZero(longArrayOf(1, 0L, 0L, 0L)))
fun isZeroFalse() = assertFalse(U256.isZero(Fe4(1, 0L, 0L, 0L)))
@Test
fun isZeroHighBit() = assertFalse(U256.isZero(longArrayOf(0L, 0L, 0L, 1L)))
fun isZeroHighBit() = assertFalse(U256.isZero(Fe4(0L, 0L, 0L, 1L)))
@Test
fun cmpEqual() = assertEquals(0, U256.cmp(hex("0000000000000000000000000000000000000000000000000000000000000001"), hex("0000000000000000000000000000000000000000000000000000000000000001")))
@@ -57,7 +57,7 @@ class U256Test {
@Test
fun addSimple() {
val out = LongArray(4)
val out = Fe4()
val carry = U256.addTo(out, hex("0000000000000000000000000000000000000000000000000000000000000001"), hex("0000000000000000000000000000000000000000000000000000000000000002"))
assertEquals("0000000000000000000000000000000000000000000000000000000000000003", toHex(out))
assertEquals(0, carry)
@@ -65,7 +65,7 @@ class U256Test {
@Test
fun addOverflow() {
val out = LongArray(4)
val out = Fe4()
val carry = U256.addTo(out, hex("ffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff"), hex("0000000000000000000000000000000000000000000000000000000000000001"))
assertEquals("0000000000000000000000000000000000000000000000000000000000000000", toHex(out))
assertEquals(1, carry)
@@ -73,14 +73,14 @@ class U256Test {
@Test
fun addLimbCarry() {
val out = LongArray(4)
val out = Fe4()
U256.addTo(out, hex("00000000000000000000000000000000000000000000000000000000ffffffff"), hex("0000000000000000000000000000000000000000000000000000000000000001"))
assertEquals("0000000000000000000000000000000000000000000000000000000100000000", toHex(out))
}
@Test
fun subSimple() {
val out = LongArray(4)
val out = Fe4()
val borrow = U256.subTo(out, hex("0000000000000000000000000000000000000000000000000000000000000003"), hex("0000000000000000000000000000000000000000000000000000000000000001"))
assertEquals("0000000000000000000000000000000000000000000000000000000000000002", toHex(out))
assertEquals(0, borrow)
@@ -88,7 +88,7 @@ class U256Test {
@Test
fun subUnderflow() {
val out = LongArray(4)
val out = Fe4()
val borrow = U256.subTo(out, hex("0000000000000000000000000000000000000000000000000000000000000000"), hex("0000000000000000000000000000000000000000000000000000000000000001"))
assertEquals("ffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff", toHex(out))
assertEquals(1, borrow)
@@ -98,50 +98,73 @@ class U256Test {
@Test
fun mulWideSmall() {
val out = LongArray(8)
val out = Wide8()
U256.mulWide(out, hex("0000000000000000000000000000000000000000000000000000000000000003"), hex("0000000000000000000000000000000000000000000000000000000000000007"))
// 3 * 7 = 21 = 0x15
assertEquals(0x15L, out[0])
for (i in 1 until 8) assertEquals(0L, out[i])
assertEquals(0x15L, out.l0)
assertEquals(0L, out.l1)
assertEquals(0L, out.l2)
assertEquals(0L, out.l3)
assertEquals(0L, out.l4)
assertEquals(0L, out.l5)
assertEquals(0L, out.l6)
assertEquals(0L, out.l7)
}
@Test
fun mulWideLarge() {
// (2^128 - 1)² consistency: mulWide and sqrWide should match
val out1 = LongArray(8)
val out2 = LongArray(8)
val out1 = Wide8()
val out2 = Wide8()
val maxHalf = hex("00000000000000000000000000000000ffffffffffffffffffffffffffffffff")
U256.mulWide(out1, maxHalf, maxHalf)
U256.sqrWide(out2, maxHalf)
for (i in 0 until 8) assertEquals(out1[i], out2[i], "Limb $i mismatch")
assertEquals(out1.l0, out2.l0, "Limb 0 mismatch")
assertEquals(out1.l1, out2.l1, "Limb 1 mismatch")
assertEquals(out1.l2, out2.l2, "Limb 2 mismatch")
assertEquals(out1.l3, out2.l3, "Limb 3 mismatch")
assertEquals(out1.l4, out2.l4, "Limb 4 mismatch")
assertEquals(out1.l5, out2.l5, "Limb 5 mismatch")
assertEquals(out1.l6, out2.l6, "Limb 6 mismatch")
assertEquals(out1.l7, out2.l7, "Limb 7 mismatch")
// Lowest limb is 1 (from +1 in (2^128-1)² = 2^256 - 2^129 + 1)
assertEquals(1L, out1[0])
assertEquals(1L, out1.l0)
}
@Test
fun sqrWideMatchesMulWide() {
// sqrWide(a) should produce the same result as mulWide(a, a)
val a = hex("67E56582298859DDAE725F972992A07C6C4FB9F62A8FFF58CE3CA926A1063530")
val mulResult = LongArray(8)
val mulResult = Wide8()
U256.mulWide(mulResult, a, a)
val sqrResult = LongArray(8)
val sqrResult = Wide8()
U256.sqrWide(sqrResult, a)
for (i in 0 until 8) {
assertEquals(mulResult[i], sqrResult[i], "Limb $i mismatch")
}
assertEquals(mulResult.l0, sqrResult.l0, "Limb 0 mismatch")
assertEquals(mulResult.l1, sqrResult.l1, "Limb 1 mismatch")
assertEquals(mulResult.l2, sqrResult.l2, "Limb 2 mismatch")
assertEquals(mulResult.l3, sqrResult.l3, "Limb 3 mismatch")
assertEquals(mulResult.l4, sqrResult.l4, "Limb 4 mismatch")
assertEquals(mulResult.l5, sqrResult.l5, "Limb 5 mismatch")
assertEquals(mulResult.l6, sqrResult.l6, "Limb 6 mismatch")
assertEquals(mulResult.l7, sqrResult.l7, "Limb 7 mismatch")
}
@Test
fun sqrWideMaxValue() {
// (2^256 - 1)^2 should match mulWide
val maxVal = hex("ffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff")
val mulResult = LongArray(8)
val mulResult = Wide8()
U256.mulWide(mulResult, maxVal, maxVal)
val sqrResult = LongArray(8)
val sqrResult = Wide8()
U256.sqrWide(sqrResult, maxVal)
for (i in 0 until 8) {
assertEquals(mulResult[i], sqrResult[i], "Limb $i mismatch for max value sqr")
}
assertEquals(mulResult.l0, sqrResult.l0, "Limb 0 mismatch for max value sqr")
assertEquals(mulResult.l1, sqrResult.l1, "Limb 1 mismatch for max value sqr")
assertEquals(mulResult.l2, sqrResult.l2, "Limb 2 mismatch for max value sqr")
assertEquals(mulResult.l3, sqrResult.l3, "Limb 3 mismatch for max value sqr")
assertEquals(mulResult.l4, sqrResult.l4, "Limb 4 mismatch for max value sqr")
assertEquals(mulResult.l5, sqrResult.l5, "Limb 5 mismatch for max value sqr")
assertEquals(mulResult.l6, sqrResult.l6, "Limb 6 mismatch for max value sqr")
assertEquals(mulResult.l7, sqrResult.l7, "Limb 7 mismatch for max value sqr")
}
// ==================== fromBytes / toBytes ====================
@@ -158,7 +181,7 @@ class U256Test {
@Test
fun bytesZero() {
val limbs = U256.fromBytes(ByteArray(32))
assertTrue(U256.isZero(limbs))
assertTrue(limbs.isZero())
}
// ==================== getNibble ====================
@@ -200,7 +223,7 @@ class U256Test {
@Test
fun xorBasic() {
val out = LongArray(4)
val out = Fe4()
U256.xorTo(out, hex("ff00ff00ff00ff00ff00ff00ff00ff00ff00ff00ff00ff00ff00ff00ff00ff00"), hex("0f0f0f0f0f0f0f0f0f0f0f0f0f0f0f0f0f0f0f0f0f0f0f0f0f0f0f0f0f0f0f0f"))
assertEquals("f00ff00ff00ff00ff00ff00ff00ff00ff00ff00ff00ff00ff00ff00ff00ff00f", toHex(out))
}
@@ -220,9 +243,12 @@ class U256Test {
@Test
fun copyIntoTest() {
val src = hex("67e56582298859ddae725f972992a07c6c4fb9f62a8fff58ce3ca926a1063530")
val dst = LongArray(4)
val dst = Fe4()
U256.copyInto(dst, src)
for (i in 0 until 4) assertEquals(src[i], dst[i])
assertEquals(src.l0, dst.l0)
assertEquals(src.l1, dst.l1)
assertEquals(src.l2, dst.l2)
assertEquals(src.l3, dst.l3)
}
@Test
@@ -232,6 +258,9 @@ class U256Test {
val expected = hex("67e56582298859ddae725f972992a07c6c4fb9f62a8fff58ce3ca926a1063530")
U256.toBytesInto(expected, fullArray, 32)
val decoded = U256.fromBytes(fullArray, 32)
for (i in 0 until 4) assertEquals(expected[i], decoded[i])
assertEquals(expected.l0, decoded.l0)
assertEquals(expected.l1, decoded.l1)
assertEquals(expected.l2, decoded.l2)
assertEquals(expected.l3, decoded.l3)
}
}