Merge pull request #2170 from vitorpamplona/claude/improve-secp256k1-performance-nFGT8

Optimize secp256k1 with unrolled arithmetic and caching
This commit is contained in:
Vitor Pamplona
2026-04-08 08:41:06 -04:00
committed by GitHub
12 changed files with 1509 additions and 615 deletions
@@ -286,10 +286,10 @@ class PointTest {
fun liftXGenerator() {
val x = LongArray(4)
val y = LongArray(4)
assertTrue(ECPoint.liftX(x, y, ECPoint.GX))
assertTrue(KeyCodec.liftX(x, y, ECPoint.GX))
assertEquals(toHex(ECPoint.GX), toHex(x))
// liftX returns even y
assertTrue(ECPoint.hasEvenY(y))
assertTrue(KeyCodec.hasEvenY(y))
}
@Test
@@ -297,27 +297,27 @@ class PointTest {
// p itself is not a valid x coordinate
val x = LongArray(4)
val y = LongArray(4)
assertFalse(ECPoint.liftX(x, y, FieldP.P))
assertFalse(KeyCodec.liftX(x, y, FieldP.P))
}
// ==================== Serialization round-trips ====================
@Test
fun compressDecompressRoundTrip() {
val compressed = ECPoint.serializeCompressed(ECPoint.GX, ECPoint.GY)
val compressed = KeyCodec.serializeCompressed(ECPoint.GX, ECPoint.GY)
val x = LongArray(4)
val y = LongArray(4)
assertTrue(ECPoint.parsePublicKey(compressed, x, y))
assertTrue(KeyCodec.parsePublicKey(compressed, x, y))
assertEquals(toHex(ECPoint.GX), toHex(x))
assertEquals(toHex(ECPoint.GY), toHex(y))
}
@Test
fun uncompressedRoundTrip() {
val uncompressed = ECPoint.serializeUncompressed(ECPoint.GX, ECPoint.GY)
val uncompressed = KeyCodec.serializeUncompressed(ECPoint.GX, ECPoint.GY)
val x = LongArray(4)
val y = LongArray(4)
assertTrue(ECPoint.parsePublicKey(uncompressed, x, y))
assertTrue(KeyCodec.parsePublicKey(uncompressed, x, y))
assertEquals(toHex(ECPoint.GX), toHex(x))
assertEquals(toHex(ECPoint.GY), toHex(y))
}
@@ -326,8 +326,8 @@ class PointTest {
fun parseInvalidKey() {
val x = LongArray(4)
val y = LongArray(4)
assertFalse(ECPoint.parsePublicKey(ByteArray(10), x, y))
assertFalse(ECPoint.parsePublicKey(ByteArray(33), x, y)) // wrong prefix (0x00)
assertFalse(KeyCodec.parsePublicKey(ByteArray(10), x, y))
assertFalse(KeyCodec.parsePublicKey(ByteArray(33), x, y)) // wrong prefix (0x00)
}
@Test
@@ -366,9 +366,9 @@ class PointTest {
assertEquals(0x03.toByte(), compressed[0]) // Odd y → 03 prefix
val x = LongArray(4)
val y = LongArray(4)
assertTrue(ECPoint.parsePublicKey(compressed, x, y))
assertTrue(KeyCodec.parsePublicKey(compressed, x, y))
// Round-trip: compress again should give same result
val recompressed = ECPoint.serializeCompressed(x, y)
val recompressed = KeyCodec.serializeCompressed(x, y)
assertEquals(compressed.toList(), recompressed.toList())
}
}
@@ -336,4 +336,67 @@ class Secp256k1Test {
.sha256(tagHash + tagHash + msg)
assertEquals(expected.toHexKey(), result.toHexKey())
}
// ============================================================
// Same-pubkey batch verification
// ============================================================
@Test
fun batchSamePubkeyAllValid() {
val seckey = "67E56582298859DDAE725F972992A07C6C4FB9F62A8FFF58CE3CA926A1063530".hexToByteArray()
val pub = Secp256k1.pubKeyCompress(Secp256k1.pubkeyCreate(seckey)).copyOfRange(1, 33)
val sigs = mutableListOf<ByteArray>()
val msgs = mutableListOf<ByteArray>()
for (i in 0 until 10) {
val msg = ByteArray(32) { (i * 7 + it).toByte() }
val sig = Secp256k1.signSchnorr(msg, seckey, null)
assertTrue(Secp256k1.verifySchnorr(sig, msg, pub), "Individual verify failed for event $i")
sigs.add(sig)
msgs.add(msg)
}
assertTrue(Secp256k1.verifySchnorrBatch(pub, sigs, msgs))
}
@Test
fun batchSamePubkeyWithInvalid() {
val seckey = "67E56582298859DDAE725F972992A07C6C4FB9F62A8FFF58CE3CA926A1063530".hexToByteArray()
val pub = Secp256k1.pubKeyCompress(Secp256k1.pubkeyCreate(seckey)).copyOfRange(1, 33)
val msg1 = ByteArray(32) { 0x01 }
val msg2 = ByteArray(32) { 0x02 }
val sig1 = Secp256k1.signSchnorr(msg1, seckey, null)
val sig2 = Secp256k1.signSchnorr(msg2, seckey, null)
// Corrupt sig2
val badSig2 = sig2.copyOf()
badSig2[63] = (badSig2[63].toInt() xor 0x01).toByte()
assertFalse(Secp256k1.verifySchnorrBatch(pub, listOf(sig1, badSig2), listOf(msg1, msg2)))
}
@Test
fun batchSamePubkeyEmpty() {
val pub = "67E56582298859DDAE725F972992A07C6C4FB9F62A8FFF58CE3CA926A1063530".hexToByteArray()
assertTrue(Secp256k1.verifySchnorrBatch(pub, emptyList(), emptyList()))
}
@Test
fun batchSamePubkeySingleFallback() {
val seckey = "67E56582298859DDAE725F972992A07C6C4FB9F62A8FFF58CE3CA926A1063530".hexToByteArray()
val pub = Secp256k1.pubKeyCompress(Secp256k1.pubkeyCreate(seckey)).copyOfRange(1, 33)
val msg = ByteArray(32) { 0x42 }
val sig = Secp256k1.signSchnorr(msg, seckey, null)
assertTrue(Secp256k1.verifySchnorrBatch(pub, listOf(sig), listOf(msg)))
}
@Test
fun batchSamePubkeyLargeBatch() {
val seckey = "3982F19BEF1615BCCFBB05E321C10E1D4CBA3DF0E841C2E41EEB6016347653C3".hexToByteArray()
val pub = Secp256k1.pubKeyCompress(Secp256k1.pubkeyCreate(seckey)).copyOfRange(1, 33)
val sigs = mutableListOf<ByteArray>()
val msgs = mutableListOf<ByteArray>()
for (i in 0 until 32) {
val msg = ByteArray(64) { (i * 13 + it).toByte() }
sigs.add(Secp256k1.signSchnorr(msg, seckey, null))
msgs.add(msg)
}
assertTrue(Secp256k1.verifySchnorrBatch(pub, sigs, msgs))
}
}