Merge pull request #2387 from vitorpamplona/claude/secp256k1-implementation-comparison-ThHC3
Fix secp256k1 field arithmetic carry-fold bugs and optimize squaring
This commit is contained in:
@@ -171,6 +171,10 @@ 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 field reads (~260 calls/verify).
|
||||
*
|
||||
* Branch-free for a == 0: P - 0 = P, which is then reduced to 0 by the
|
||||
* trailing reduceSelf. Removing the early return matches the C fe_negate
|
||||
* path and eliminates a data-dependent branch that the JIT can't optimize.
|
||||
*/
|
||||
fun neg(
|
||||
out: Fe4,
|
||||
@@ -178,13 +182,6 @@ internal object FieldP {
|
||||
) {
|
||||
// Normalize input: P - a underflows if a > P (from lazy add)
|
||||
reduceSelf(a)
|
||||
if (a.isZero()) {
|
||||
out.l0 = 0L
|
||||
out.l1 = 0L
|
||||
out.l2 = 0L
|
||||
out.l3 = 0L
|
||||
return
|
||||
}
|
||||
// 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
|
||||
@@ -194,6 +191,8 @@ internal object FieldP {
|
||||
out.l2 = a.l2.inv() - b1
|
||||
val b2 = if (a.l2 == -1L && b1 != 0L) 1L else 0L
|
||||
out.l3 = a.l3.inv() - b2
|
||||
// a == 0 ⇒ out == P here; reduceSelf folds P back to 0.
|
||||
reduceSelf(out)
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -358,7 +358,11 @@ object Secp256k1 {
|
||||
U256.fromBytesInto(sc.scalarTmp1, sc.bytesTmp2, 0)
|
||||
ScalarN.reduceTo(sc.scalarTmp1, sc.scalarTmp1)
|
||||
val k0 = sc.scalarTmp1
|
||||
require(!k0.isZero())
|
||||
// BIP-340 step "Fail if k' = 0". Probability ~2^-256, unreachable in
|
||||
// practice but the spec requires refusal. `check` here (vs the prior
|
||||
// `require`) communicates it as an internal invariant rather than an
|
||||
// argument error, matching the semantics of the C side returning 0.
|
||||
check(!k0.isZero()) { "BIP-340 nonce derived to 0 — refuse to sign" }
|
||||
|
||||
// R = k0·G
|
||||
ECPoint.mulG(sc.entryResult, k0, sc)
|
||||
@@ -521,7 +525,14 @@ object Secp256k1 {
|
||||
|
||||
// ==================== Tweak operations ====================
|
||||
|
||||
/** Add a tweak to a private key: result = (seckey + tweak) mod n. Used by BIP-32. */
|
||||
/**
|
||||
* Add a tweak to a private key: result = (seckey + tweak) mod n. Used by BIP-32.
|
||||
*
|
||||
* Throws `IllegalArgumentException` for wrong-size inputs and
|
||||
* `IllegalStateException` when the result is 0 or >= n (matching the
|
||||
* C side which returns 0). The latter is a ~2^-128 cryptographic edge
|
||||
* case that indicates an adversarial tweak choice.
|
||||
*/
|
||||
fun privKeyTweakAdd(
|
||||
seckey: ByteArray,
|
||||
tweak: ByteArray,
|
||||
@@ -537,7 +548,10 @@ object Secp256k1 {
|
||||
U256.fromBytesInto(a, seckey, 0)
|
||||
U256.fromBytesInto(b, tweak, 0)
|
||||
ScalarN.addTo(r, a, b)
|
||||
require(!r.isZero() && U256.cmp(r, ScalarN.N) < 0)
|
||||
// ScalarN.addTo already reduces mod n, so r < n is guaranteed.
|
||||
// The only remaining invalid state is r == 0, which matches the
|
||||
// C side's `scalar_is_zero(&r)` failure.
|
||||
check(!r.isZero()) { "Tweaked private key is zero — invalid tweak" }
|
||||
return U256.toBytes(r)
|
||||
}
|
||||
|
||||
|
||||
+92
-3
@@ -70,9 +70,12 @@ class FieldPTest {
|
||||
@Test
|
||||
fun addNearP() {
|
||||
// (p - 1) + 1 = p ≡ 0 (mod p)
|
||||
// FieldP.add is lazy: the result here is the literal limb pattern for p
|
||||
// rather than the canonical 0. Compare after an explicit reduceSelf.
|
||||
val pMinus1 = hex("fffffffffffffffffffffffffffffffffffffffffffffffffffffffefffffc2e")
|
||||
val one = Fe4(1L, 0L, 0L, 0L)
|
||||
val result = FieldP.add(pMinus1, one)
|
||||
FieldP.reduceSelf(result)
|
||||
assertTrue(result.isZero())
|
||||
}
|
||||
|
||||
@@ -112,6 +115,9 @@ class FieldPTest {
|
||||
fun addNegIsZero() {
|
||||
val a = hex("67e56582298859ddae725f972992a07c6c4fb9f62a8fff58ce3ca926a1063530")
|
||||
val result = FieldP.add(a, FieldP.neg(a))
|
||||
// a + (-a) = a + (p - a) = p, which the lazy adder stores as the raw
|
||||
// limb pattern for p rather than 0. Reduce before checking.
|
||||
FieldP.reduceSelf(result)
|
||||
assertTrue(result.isZero())
|
||||
}
|
||||
|
||||
@@ -149,6 +155,7 @@ class FieldPTest {
|
||||
val aInv = FieldP.inv(a)
|
||||
val product = FieldP.mul(a, aInv)
|
||||
val one = Fe4(1L, 0L, 0L, 0L)
|
||||
FieldP.reduceSelf(product)
|
||||
assertEquals(toHex(one), toHex(product))
|
||||
}
|
||||
|
||||
@@ -180,12 +187,14 @@ class FieldPTest {
|
||||
|
||||
@Test
|
||||
fun halfOfOdd() {
|
||||
// half(1) = (1 + p) / 2 = (p + 1) / 2
|
||||
// half(1) = (1 + p) / 2. Then 2 * half(1) ≡ 1 (mod p).
|
||||
// The lazy adder may leave `doubled` as a representation of 1 + p
|
||||
// rather than the canonical 1; reduce before comparing.
|
||||
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)
|
||||
FieldP.reduceSelf(doubled)
|
||||
assertEquals(1L, doubled.l0)
|
||||
assertEquals(0L, doubled.l1)
|
||||
assertEquals(0L, doubled.l2)
|
||||
@@ -238,9 +247,12 @@ class FieldPTest {
|
||||
|
||||
@Test
|
||||
fun reduceWideWithMaxValues() {
|
||||
// Multiply two values near p and verify result is < p
|
||||
// Multiply two values near p and verify result is mathematically < p.
|
||||
// fe_mul is lazy, so the raw limbs can temporarily be in [P, 2^256);
|
||||
// we explicitly normalize, then compare both bounds and value.
|
||||
val pMinus1 = hex("fffffffffffffffffffffffffffffffffffffffffffffffffffffffefffffc2e")
|
||||
val result = FieldP.mul(pMinus1, pMinus1)
|
||||
FieldP.reduceSelf(result)
|
||||
assertTrue(U256.cmp(result, FieldP.P) < 0, "Result should be < p")
|
||||
// (p-1)² ≡ 1 (mod p)
|
||||
val one = Fe4(1L, 0L, 0L, 0L)
|
||||
@@ -283,6 +295,7 @@ class FieldPTest {
|
||||
val inv2 = FieldP.inv(two)
|
||||
val product = FieldP.mul(two, inv2)
|
||||
val one = Fe4(1L, 0L, 0L, 0L)
|
||||
FieldP.reduceSelf(product)
|
||||
assertEquals(toHex(one), toHex(product))
|
||||
}
|
||||
|
||||
@@ -310,4 +323,80 @@ class FieldPTest {
|
||||
FieldP.mul(aCopy, aCopy, b) // out == a
|
||||
assertEquals(toHex(expected), toHex(aCopy))
|
||||
}
|
||||
|
||||
// ==================== Adversarial carry-fold regression tests ====================
|
||||
//
|
||||
// These tests exercise the code path where the reduction's final fold has
|
||||
// to loop more than once. A naïve single-pass fold (the shape the C and
|
||||
// Kotlin reducers had historically) silently drops a carry out of the top
|
||||
// limb when the result grows past 2^256. The while-loop fold makes the
|
||||
// invariant "output in [0, 2^256)" robust for any reachable input,
|
||||
// including pathological lazy-reduced chains like these.
|
||||
//
|
||||
// The Kotlin field arithmetic uses lazy reduction, so results up to
|
||||
// ~2^256 can be representationally in [P, 2^256) rather than in [0, P).
|
||||
// These tests compare after an explicit `reduceSelf` to compare the
|
||||
// mathematical value rather than the raw limb bytes.
|
||||
|
||||
private fun reduced(a: Fe4): Fe4 {
|
||||
val r = a.copyOf()
|
||||
FieldP.reduceSelf(r)
|
||||
return r
|
||||
}
|
||||
|
||||
@Test
|
||||
fun chainedLazyAddThenMulReducesCorrectly() {
|
||||
// Build a value close to 2^256 via chained lazy adds so the
|
||||
// subsequent multiplication feeds the reducer an input near its
|
||||
// upper bound. Verify: 4*(p-1) ≡ p-4 (mod p).
|
||||
val pMinus1 = hex("fffffffffffffffffffffffffffffffffffffffffffffffffffffffefffffc2e")
|
||||
val two = FieldP.add(pMinus1, pMinus1)
|
||||
val four = FieldP.add(two, two)
|
||||
val expected = hex("fffffffffffffffffffffffffffffffffffffffffffffffffffffffefffffc2b")
|
||||
val one = Fe4(1L, 0L, 0L, 0L)
|
||||
val product = FieldP.mul(four, one)
|
||||
assertEquals(toHex(expected), toHex(reduced(product)))
|
||||
}
|
||||
|
||||
@Test
|
||||
fun mulOfLargeValuesStaysReduced() {
|
||||
// ((p-1)^2)^2 ≡ 1 (mod p). Feed the squared result back into another
|
||||
// multiplication to flush any hidden lazy state.
|
||||
val pMinus1 = hex("fffffffffffffffffffffffffffffffffffffffffffffffffffffffefffffc2e")
|
||||
val sq = FieldP.sqr(pMinus1)
|
||||
val sqAgain = FieldP.mul(sq, sq)
|
||||
val one = Fe4(1L, 0L, 0L, 0L)
|
||||
assertEquals(toHex(one), toHex(reduced(sqAgain)))
|
||||
}
|
||||
|
||||
@Test
|
||||
fun repeatedSquaringConvergesAcrossMulAndSqrPaths() {
|
||||
// Squaring chain: a, a², a⁴, …, a^(2^10). Any latent carry-drop
|
||||
// would compound across 10 iterations. Run via both fe_mul(x, x)
|
||||
// and the dedicated fe_sqr path and compare results.
|
||||
val a = hex("fffffffffffffffffffffffffffffffffffffffffffffffffffffffefffffc2d") // p-2
|
||||
var byMul = a
|
||||
repeat(10) { byMul = FieldP.mul(byMul, byMul) }
|
||||
var bySqr = a
|
||||
val out = Fe4()
|
||||
repeat(10) {
|
||||
FieldP.sqr(out, bySqr)
|
||||
bySqr = out.copyOf()
|
||||
}
|
||||
assertEquals(toHex(reduced(byMul)), toHex(reduced(bySqr)))
|
||||
}
|
||||
|
||||
@Test
|
||||
fun mulRoundTripViaInvStressesLazyInput() {
|
||||
// Build a large, possibly-unreduced value and check the round trip
|
||||
// (x * b) * inv(b) ≡ x (mod p). Exercises the full pipeline:
|
||||
// unreduced input → mul → reduce → inv → mul → reduce.
|
||||
val pMinus1 = hex("fffffffffffffffffffffffffffffffffffffffffffffffffffffffefffffc2e")
|
||||
val lazyBig = FieldP.add(pMinus1, pMinus1) // may be lazy
|
||||
val b = hex("0000000000000000000000000000000000000000000000000000000000000042")
|
||||
val ref = FieldP.mul(lazyBig, b)
|
||||
val bInv = FieldP.inv(b)
|
||||
val back = FieldP.mul(ref, bInv)
|
||||
assertEquals(toHex(reduced(lazyBig)), toHex(reduced(back)))
|
||||
}
|
||||
}
|
||||
|
||||
@@ -102,9 +102,13 @@ class GlvTest {
|
||||
|
||||
@Test
|
||||
fun betaCubedIsOne() {
|
||||
// β³ ≡ 1 (mod p) — the defining property of the cube root of unity
|
||||
// β³ ≡ 1 (mod p) — the defining property of the cube root of unity.
|
||||
// FieldP.mul is lazy, so the result may be the raw limb pattern for
|
||||
// 1 + p (mathematically 1 mod p, but not bit-equal to the canonical
|
||||
// representation). Reduce explicitly before comparing.
|
||||
val b2 = FieldP.sqr(Glv.BETA)
|
||||
val b3 = FieldP.mul(b2, Glv.BETA)
|
||||
FieldP.reduceSelf(b3)
|
||||
val one = Fe4(1L, 0L, 0L, 0L)
|
||||
assertEquals(toHex(one), toHex(b3))
|
||||
}
|
||||
|
||||
+49
@@ -35,6 +35,26 @@ import fr.acinq.secp256k1.Secp256k1 as NativeSecp256k1
|
||||
* benefit from the pubkey decompression cache and P-table cache. This is realistic
|
||||
* for Nostr (feed from one author) but not representative of first-time verification.
|
||||
*
|
||||
* ## Benchmark Fairness Notes
|
||||
*
|
||||
* The comparisons below are apples-to-apples wherever possible, but a few
|
||||
* intentional asymmetries remain because the public APIs don't line up 1:1:
|
||||
*
|
||||
* - **signSchnorr (XPubKey)**: the Kotlin side uses `signSchnorrWithPubKey`,
|
||||
* which skips the internal pubkey derivation when the caller already has
|
||||
* the compressed pubkey cached. The ACINQ side always derives the pubkey
|
||||
* inside `signSchnorr`. A separate `signSchnorr` row below exercises the
|
||||
* same derivation path on both sides for a fair comparison.
|
||||
*
|
||||
* - **ecdhXOnly (NIP-44)**: ACINQ has no x-only ECDH API, so the native
|
||||
* baseline uses `pubKeyTweakMul` + x-coordinate extraction. Both paths
|
||||
* compute the same shared secret but the native side pays an extra
|
||||
* (de)serialization step that the Kotlin side skips.
|
||||
*
|
||||
* - **privKeyTweakAdd**: ACINQ mutates its first argument, so the native
|
||||
* side does `privKey.copyOf()` before each call, adding one allocation
|
||||
* to the native measurement that the Kotlin side doesn't incur.
|
||||
*
|
||||
* Run with: ./gradlew :quartz:jvmTest --tests "*.Secp256k1Benchmark"
|
||||
*/
|
||||
class Secp256k1Benchmark {
|
||||
@@ -270,6 +290,35 @@ class Secp256k1Benchmark {
|
||||
},
|
||||
)
|
||||
|
||||
// --- taggedHash (BIP-340, heavily used by NIP-44 key derivation) ---
|
||||
// The cached BIP-340 tag prefixes in Secp256k1.kt make each call
|
||||
// exactly 1 SHA-256 over (64-byte prefix || message). ACINQ has no
|
||||
// dedicated tagged-hash API, so we compose sha256(sha256(tag) ||
|
||||
// sha256(tag) || msg) by hand on the native side using the same
|
||||
// challenge prefix bytes. This measures raw SHA-256 throughput
|
||||
// through the different SHA implementations.
|
||||
val challengeTag = "BIP0340/challenge".toByteArray()
|
||||
val tagHash =
|
||||
java.security.MessageDigest
|
||||
.getInstance("SHA-256")
|
||||
.digest(challengeTag)
|
||||
val nativeTaggedInput = tagHash + tagHash + msg32
|
||||
val nativeDigest = java.security.MessageDigest.getInstance("SHA-256")
|
||||
results +=
|
||||
bench(
|
||||
name = "taggedHash (NIP-44/BIP340)",
|
||||
warmup = 5000,
|
||||
iterations = 100000,
|
||||
nativeOp = {
|
||||
nativeDigest.reset()
|
||||
nativeDigest.digest(nativeTaggedInput)
|
||||
},
|
||||
kotlinOp = {
|
||||
com.vitorpamplona.quartz.utils.secp256k1.Secp256k1
|
||||
.taggedHash("BIP0340/challenge", msg32)
|
||||
},
|
||||
)
|
||||
|
||||
// Print results
|
||||
println()
|
||||
println("=".repeat(76))
|
||||
|
||||
+227
@@ -0,0 +1,227 @@
|
||||
/*
|
||||
* 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
|
||||
|
||||
import com.vitorpamplona.quartz.utils.Secp256k1InstanceC
|
||||
import java.util.Random
|
||||
import kotlin.test.Test
|
||||
import kotlin.test.assertContentEquals
|
||||
import kotlin.test.assertTrue
|
||||
import fr.acinq.secp256k1.Secp256k1 as NativeSecp256k1
|
||||
|
||||
/**
|
||||
* Byte-for-byte cross-validation between the three secp256k1 implementations
|
||||
* available on JVM:
|
||||
* 1. ACINQ / libsecp256k1-kmp (reference implementation)
|
||||
* 2. Pure Kotlin ([Secp256k1])
|
||||
* 3. Custom C library via JNI ([Secp256k1InstanceC]) — when loadable
|
||||
*
|
||||
* BIP-340 signatures are deterministic given (privkey, message, aux_rand),
|
||||
* so two correct implementations MUST produce identical signature bytes.
|
||||
* This catches subtle reduction/limb-representation bugs that wouldn't show
|
||||
* up in a roundtrip verify test (where a wrong-but-internally-consistent
|
||||
* implementation can still verify its own output).
|
||||
*/
|
||||
class Secp256k1CrossValidationTest {
|
||||
private val acinq = NativeSecp256k1.get()
|
||||
|
||||
// Deterministic RNG for reproducible test failures. java.util.Random has
|
||||
// a documented LCG and is fine for non-cryptographic test data.
|
||||
private val rng = Random(0xAFEE5EED1234L)
|
||||
|
||||
private fun randomSeckey(): ByteArray {
|
||||
while (true) {
|
||||
val b = ByteArray(32).also { rng.nextBytes(it) }
|
||||
if (Secp256k1.secKeyVerify(b)) return b
|
||||
}
|
||||
}
|
||||
|
||||
private fun randomMessage(size: Int): ByteArray = ByteArray(size).also { rng.nextBytes(it) }
|
||||
|
||||
private fun randomAuxRand(): ByteArray = ByteArray(32).also { rng.nextBytes(it) }
|
||||
|
||||
@Test
|
||||
fun pubkeyCreateMatchesAcinq() {
|
||||
// 50 distinct private keys must produce identical uncompressed pubkeys.
|
||||
repeat(50) {
|
||||
val seckey = randomSeckey()
|
||||
val kotlinPub = Secp256k1.pubkeyCreate(seckey)
|
||||
val acinqPub = acinq.pubkeyCreate(seckey)
|
||||
assertContentEquals(
|
||||
acinqPub,
|
||||
kotlinPub,
|
||||
"pubkeyCreate must be byte-identical to ACINQ for seckey=${seckey.toHex()}",
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
fun signSchnorrMatchesAcinq() {
|
||||
// 50 random (seckey, message, auxrand) triples must yield byte-identical
|
||||
// BIP-340 signatures. Any divergence here points at a reduction or
|
||||
// limb-representation bug in one implementation.
|
||||
repeat(50) {
|
||||
val seckey = randomSeckey()
|
||||
val msg = randomMessage(32) // Nostr event IDs are 32 bytes
|
||||
val aux = randomAuxRand()
|
||||
val kotlinSig = Secp256k1.signSchnorr(msg, seckey, aux)
|
||||
val acinqSig = acinq.signSchnorr(msg, seckey, aux)
|
||||
assertContentEquals(
|
||||
acinqSig,
|
||||
kotlinSig,
|
||||
"signSchnorr must be byte-identical to ACINQ for seckey=${seckey.toHex()} msg=${msg.toHex()}",
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
fun signSchnorrVariableMessageLengthSelfConsistent() {
|
||||
// ACINQ's signSchnorr API hard-codes the BIP-340 convention that the
|
||||
// message is exactly 32 bytes. Our Kotlin implementation accepts
|
||||
// messages of any length (NIP-01 event bodies can hash to something
|
||||
// other than 32 bytes in custom protocols). For non-32 inputs we
|
||||
// verify that the Kotlin signer and verifier at least self-agree:
|
||||
// any (msg, sig, pub) tuple produced by the signer must verify.
|
||||
val lengths = intArrayOf(0, 1, 31, 33, 55, 56, 64, 65, 127, 128, 511, 512, 1024)
|
||||
val seckey = randomSeckey()
|
||||
val xOnly = Secp256k1.pubKeyCompress(Secp256k1.pubkeyCreate(seckey)).copyOfRange(1, 33)
|
||||
for (len in lengths) {
|
||||
val msg = randomMessage(len)
|
||||
val aux = randomAuxRand()
|
||||
val sig = Secp256k1.signSchnorr(msg, seckey, aux)
|
||||
assertTrue(
|
||||
Secp256k1.verifySchnorr(sig, msg, xOnly),
|
||||
"Kotlin self-verify failed at message length $len",
|
||||
)
|
||||
}
|
||||
|
||||
// For the 32-byte case we can cross-check against ACINQ for
|
||||
// byte-identical signatures, since that's what BIP-340 covers.
|
||||
val msg32 = randomMessage(32)
|
||||
val aux = randomAuxRand()
|
||||
val kotlinSig = Secp256k1.signSchnorr(msg32, seckey, aux)
|
||||
val acinqSig = acinq.signSchnorr(msg32, seckey, aux)
|
||||
assertContentEquals(acinqSig, kotlinSig)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun customCImplementationMatchesAcinqAndKotlin() {
|
||||
val cAvailable =
|
||||
try {
|
||||
Secp256k1InstanceC.init()
|
||||
true
|
||||
} catch (e: UnsatisfiedLinkError) {
|
||||
println("SKIP: custom C library not loadable (${e.message})")
|
||||
false
|
||||
} catch (e: Throwable) {
|
||||
println("SKIP: custom C library init failed (${e.message})")
|
||||
false
|
||||
}
|
||||
if (!cAvailable) return
|
||||
|
||||
// 25 triples — fewer than the pure tests because each round exercises
|
||||
// three implementations and the test is slower.
|
||||
repeat(25) {
|
||||
val seckey = randomSeckey()
|
||||
val msg = randomMessage(32)
|
||||
val aux = randomAuxRand()
|
||||
|
||||
val kotlinPubCompressed = Secp256k1.pubKeyCompress(Secp256k1.pubkeyCreate(seckey))
|
||||
val acinqPubCompressed = acinq.pubKeyCompress(acinq.pubkeyCreate(seckey))
|
||||
val cPubCompressed = Secp256k1InstanceC.compressedPubKeyFor(seckey)
|
||||
assertContentEquals(acinqPubCompressed, kotlinPubCompressed, "pubkey: Kotlin vs ACINQ")
|
||||
assertContentEquals(acinqPubCompressed, cPubCompressed, "pubkey: C vs ACINQ")
|
||||
|
||||
val kotlinSig = Secp256k1.signSchnorr(msg, seckey, aux)
|
||||
val acinqSig = acinq.signSchnorr(msg, seckey, aux)
|
||||
val cSig = Secp256k1InstanceC.signSchnorr(msg, seckey, aux)
|
||||
assertContentEquals(acinqSig, kotlinSig, "sig: Kotlin vs ACINQ")
|
||||
assertContentEquals(acinqSig, cSig, "sig: C vs ACINQ")
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
fun privKeyTweakAddMatchesAcinq() {
|
||||
repeat(30) {
|
||||
val seckey = randomSeckey()
|
||||
val tweak = randomSeckey()
|
||||
val kotlinResult = Secp256k1.privKeyTweakAdd(seckey, tweak)
|
||||
val acinqResult = acinq.privKeyTweakAdd(seckey.copyOf(), tweak)
|
||||
assertContentEquals(
|
||||
acinqResult,
|
||||
kotlinResult,
|
||||
"privKeyTweakAdd mismatch for seckey=${seckey.toHex()} tweak=${tweak.toHex()}",
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
fun batchOfSignaturesRoundTripAcrossImpls() {
|
||||
// Sign with ACINQ, verify with Kotlin and C. Sign with Kotlin, verify
|
||||
// with ACINQ and C. This is the weakest form of interoperability check,
|
||||
// but covers the case where a signing bug produces a valid-looking
|
||||
// signature that only the producing implementation accepts.
|
||||
val seckey = randomSeckey()
|
||||
val kotlinPubCompressed = Secp256k1.pubKeyCompress(Secp256k1.pubkeyCreate(seckey))
|
||||
val xOnly = kotlinPubCompressed.copyOfRange(1, 33)
|
||||
|
||||
repeat(20) {
|
||||
val msg = randomMessage(32)
|
||||
val aux = randomAuxRand()
|
||||
val kotlinSig = Secp256k1.signSchnorr(msg, seckey, aux)
|
||||
val acinqSig = acinq.signSchnorr(msg, seckey, aux)
|
||||
|
||||
// Same inputs → same signature (BIP-340 determinism).
|
||||
assertContentEquals(acinqSig, kotlinSig)
|
||||
|
||||
assertTrue(acinq.verifySchnorr(kotlinSig, msg, xOnly), "ACINQ must verify Kotlin sig")
|
||||
assertTrue(
|
||||
Secp256k1.verifySchnorr(acinqSig, msg, xOnly),
|
||||
"Kotlin must verify ACINQ sig",
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
fun ecdhXOnlySymmetric() {
|
||||
// Kotlin ecdhXOnly shared secret should equal what ACINQ's
|
||||
// pubKeyTweakMul produces (after extracting the x coordinate). The
|
||||
// two sides of an ECDH must also see the same shared secret.
|
||||
repeat(20) {
|
||||
val skA = randomSeckey()
|
||||
val skB = randomSeckey()
|
||||
val pubA = Secp256k1.pubKeyCompress(Secp256k1.pubkeyCreate(skA))
|
||||
val pubB = Secp256k1.pubKeyCompress(Secp256k1.pubkeyCreate(skB))
|
||||
val xA = pubA.copyOfRange(1, 33)
|
||||
val xB = pubB.copyOfRange(1, 33)
|
||||
|
||||
val kotlinAB = Secp256k1.ecdhXOnly(xB, skA)
|
||||
val kotlinBA = Secp256k1.ecdhXOnly(xA, skB)
|
||||
assertContentEquals(kotlinAB, kotlinBA, "ECDH must be symmetric")
|
||||
|
||||
// Compare against ACINQ's pubKeyTweakMul + extract x
|
||||
val acinqAB = acinq.pubKeyTweakMul(pubB, skA).copyOfRange(1, 33)
|
||||
assertContentEquals(acinqAB, kotlinAB, "Kotlin ECDH must match ACINQ")
|
||||
}
|
||||
}
|
||||
|
||||
private fun ByteArray.toHex(): String = joinToString("") { b -> ((b.toInt() and 0xFF) or 0x100).toString(16).substring(1) }
|
||||
}
|
||||
@@ -3,6 +3,22 @@ project(secp256k1_amethyst C)
|
||||
|
||||
set(CMAKE_C_STANDARD 11)
|
||||
|
||||
# ==================== Link-Time Optimization ====================
|
||||
#
|
||||
# LTO lets the linker inline fe_mul / fe_sqr across translation units
|
||||
# (field.c ↔ point.c ↔ scalar.c ↔ schnorr.c), recovering ~5-15% on the
|
||||
# hot Jacobian formulas where fe_mul would otherwise be a call boundary.
|
||||
# Disabled automatically when the toolchain doesn't support it.
|
||||
|
||||
include(CheckIPOSupported)
|
||||
check_ipo_supported(RESULT LTO_SUPPORTED OUTPUT LTO_ERROR)
|
||||
if(LTO_SUPPORTED)
|
||||
message(STATUS "LTO supported - enabling interprocedural optimization")
|
||||
set(CMAKE_INTERPROCEDURAL_OPTIMIZATION TRUE)
|
||||
else()
|
||||
message(STATUS "LTO not supported: ${LTO_ERROR}")
|
||||
endif()
|
||||
|
||||
# ==================== Platform-specific optimizations ====================
|
||||
|
||||
if(CMAKE_SYSTEM_PROCESSOR MATCHES "aarch64|arm64|ARM64")
|
||||
|
||||
@@ -32,7 +32,6 @@ void mul_wide(uint64_t out[8], const uint64_t a[4], const uint64_t b[4]) {
|
||||
* This avoids the column-based carry overflow problem.
|
||||
*/
|
||||
uint128_t acc;
|
||||
uint64_t carry;
|
||||
|
||||
/* Row 0: out += a[0] * b */
|
||||
acc = (uint128_t)a[0] * b[0];
|
||||
@@ -106,18 +105,29 @@ void reduce_wide(secp256k1_fe *r, const uint64_t w[8]) {
|
||||
r->d[3] = (uint64_t)acc;
|
||||
uint64_t carry = (uint64_t)(acc >> 64);
|
||||
|
||||
/* Round 2: fold remaining carry */
|
||||
if (carry) {
|
||||
/* Round 2+: fold remaining carry through C. Looped for correctness
|
||||
* against any reachable input (see fe_mul for the detailed bound). */
|
||||
while (carry) {
|
||||
acc = (uint128_t)r->d[0] + (uint128_t)carry * FIELD_C;
|
||||
r->d[0] = (uint64_t)acc;
|
||||
carry = (uint64_t)(acc >> 64);
|
||||
if (carry) {
|
||||
r->d[1] += carry;
|
||||
if (r->d[1] < carry) {
|
||||
r->d[2]++;
|
||||
if (r->d[2] == 0) r->d[3]++;
|
||||
uint64_t c1 = (uint64_t)(acc >> 64);
|
||||
if (c1) {
|
||||
uint64_t s = r->d[1] + c1;
|
||||
uint64_t cc = (s < c1) ? 1 : 0;
|
||||
r->d[1] = s;
|
||||
if (cc) {
|
||||
s = r->d[2] + 1;
|
||||
cc = (s == 0) ? 1 : 0;
|
||||
r->d[2] = s;
|
||||
if (cc) {
|
||||
s = r->d[3] + 1;
|
||||
r->d[3] = s;
|
||||
carry = (s == 0) ? 1 : 0;
|
||||
continue;
|
||||
}
|
||||
}
|
||||
}
|
||||
carry = 0;
|
||||
}
|
||||
|
||||
/* No fe_normalize — lazy. Output is in [0, 2^256), possibly in [P, P+C).
|
||||
@@ -186,10 +196,32 @@ void fe_mul(secp256k1_fe *r, const secp256k1_fe *a, const secp256k1_fe *b) {
|
||||
acc += (uint128_t)lo3 + (uint128_t)hi3 * FIELD_C;
|
||||
r->d[3] = (uint64_t)acc;
|
||||
uint64_t carry = (uint64_t)(acc >> 64);
|
||||
if (carry) {
|
||||
/* Final fold: carry * C back into the low limbs. In theory two rounds
|
||||
* suffice because carry ≤ ~2^34 on the first pass and carry*C < 2^67
|
||||
* produces at most a 3-bit secondary carry. The loop is cheap and makes
|
||||
* the invariant "output < 2^256" hold for any reachable inputs — not
|
||||
* only from fe_mul itself but also from chained lazy fe_add results. */
|
||||
while (carry) {
|
||||
acc = (uint128_t)r->d[0] + (uint128_t)carry * FIELD_C;
|
||||
r->d[0] = (uint64_t)acc; carry = (uint64_t)(acc >> 64);
|
||||
if (carry) { r->d[1] += carry; if (r->d[1] < carry) { r->d[2]++; if (!r->d[2]) r->d[3]++; } }
|
||||
r->d[0] = (uint64_t)acc;
|
||||
uint64_t c1 = (uint64_t)(acc >> 64);
|
||||
if (c1) {
|
||||
uint64_t s = r->d[1] + c1;
|
||||
uint64_t cc = (s < c1) ? 1 : 0;
|
||||
r->d[1] = s;
|
||||
if (cc) {
|
||||
s = r->d[2] + 1;
|
||||
cc = (s == 0) ? 1 : 0;
|
||||
r->d[2] = s;
|
||||
if (cc) {
|
||||
s = r->d[3] + 1;
|
||||
r->d[3] = s;
|
||||
carry = (s == 0) ? 1 : 0;
|
||||
continue;
|
||||
}
|
||||
}
|
||||
}
|
||||
carry = 0;
|
||||
}
|
||||
/* No fe_normalize — lazy. Output is in [0, 2^256), possibly in [P, P+C).
|
||||
* This is safe: mul/add/sub all handle unreduced inputs.
|
||||
@@ -201,16 +233,9 @@ void fe_mul(secp256k1_fe *r, const secp256k1_fe *a, const secp256k1_fe *b) {
|
||||
#endif
|
||||
}
|
||||
|
||||
/*
|
||||
* Squaring: just call fe_mul(r, a, a).
|
||||
* With 4x64 limbs, a dedicated sqr doesn't help because:
|
||||
* - Cross-product doubling overflows uint128 (64+64+1 > 128 bits)
|
||||
* - fe_mul is already inlined with optimal instruction scheduling
|
||||
* - Saves 0 products (still 16 MUL instructions either way)
|
||||
*/
|
||||
void fe_sqr(secp256k1_fe *r, const secp256k1_fe *a) {
|
||||
fe_mul(r, a, a);
|
||||
}
|
||||
/* fe_sqr is now defined as static inline in field.h (10-mul dedicated
|
||||
* squaring using __int128). This routes through fe_sqr_inline to save 6
|
||||
* multiplications per squaring compared to fe_mul(a, a). */
|
||||
#endif /* !FE_MUL_ASM */
|
||||
|
||||
#else /* Portable fallback (no HAVE_INT128) */
|
||||
@@ -246,17 +271,34 @@ void fe_mul(secp256k1_fe *r, const secp256k1_fe *a, const secp256k1_fe *b) {
|
||||
carry2 = c_hi + (sum < w[i] ? 1 : 0);
|
||||
r->d[i] = sum;
|
||||
}
|
||||
if (carry2) {
|
||||
while (carry2) {
|
||||
mul64(&c_hi, &c_lo, carry2, FIELD_C);
|
||||
uint64_t sum = r->d[0] + c_lo;
|
||||
uint64_t new_carry = (sum < c_lo) ? 1 : 0;
|
||||
r->d[0] = sum;
|
||||
if (sum < c_lo) { r->d[1]++; if (!r->d[1]) { r->d[2]++; if (!r->d[2]) r->d[3]++; } }
|
||||
uint64_t s1 = r->d[1] + c_hi + new_carry;
|
||||
uint64_t nc1 = (s1 < c_hi) || (new_carry && s1 == c_hi) ? 1 : 0;
|
||||
r->d[1] = s1;
|
||||
if (nc1) {
|
||||
uint64_t s2 = r->d[2] + 1;
|
||||
uint64_t nc2 = (s2 == 0) ? 1 : 0;
|
||||
r->d[2] = s2;
|
||||
if (nc2) {
|
||||
uint64_t s3 = r->d[3] + 1;
|
||||
r->d[3] = s3;
|
||||
carry2 = (s3 == 0) ? 1 : 0;
|
||||
continue;
|
||||
}
|
||||
}
|
||||
carry2 = 0;
|
||||
}
|
||||
/* No fe_normalize — lazy. Output is in [0, 2^256), possibly in [P, P+C).
|
||||
* This is safe: mul/add/sub all handle unreduced inputs.
|
||||
* Only neg/half/isZero/cmp/toBytes need explicit normalize. */
|
||||
}
|
||||
|
||||
/* Portable fallback squaring: use fe_mul(a, a) because the 10-mul inline path
|
||||
* requires __int128 and this branch is for compilers without it. */
|
||||
void fe_sqr(secp256k1_fe *r, const secp256k1_fe *a) { fe_mul(r, a, a); }
|
||||
|
||||
#endif /* HAVE_INT128 */
|
||||
|
||||
@@ -128,26 +128,23 @@ static inline void fe_add_assign(secp256k1_fe *r, const secp256k1_fe *a) {
|
||||
}
|
||||
|
||||
/* r = -a mod p.
|
||||
* Uses 2P - a instead of P - a to handle unnormalized inputs in [0, 2P).
|
||||
* Result is in [0, 2P). */
|
||||
*
|
||||
* Normalizes a first (fast — usually a no-op), then computes P - a. When
|
||||
* a == 0, P - 0 = P, which a final fe_normalize collapses back to 0. This
|
||||
* branch-free form matches the Kotlin FieldP.neg path and avoids the
|
||||
* data-dependent early-return the previous version had. */
|
||||
static inline void fe_negate(secp256k1_fe *r, const secp256k1_fe *a, int m) {
|
||||
(void)m;
|
||||
/* 2P = [2*P0, MAX, MAX, MAX-1] + carry handling.
|
||||
* Since P = [P0, MAX, MAX, MAX], 2P = [2*P0, MAX+carry, ...].
|
||||
* Actually 2P mod 2^256 = 2*P0 with carries. Let's just do P + (P - a). */
|
||||
/* Simpler: normalize a first, then P - a. The normalize is fast (usually no-op). */
|
||||
secp256k1_fe t = *a;
|
||||
fe_normalize(&t);
|
||||
if (t.d[0] == 0 && t.d[1] == 0 && t.d[2] == 0 && t.d[3] == 0) {
|
||||
*r = FE_ZERO;
|
||||
return;
|
||||
}
|
||||
uint64_t borrow = 0;
|
||||
for (int i = 0; i < 4; i++) {
|
||||
uint64_t diff = FE_P.d[i] - t.d[i] - borrow;
|
||||
borrow = (FE_P.d[i] < t.d[i] + borrow) || (borrow && t.d[i] == UINT64_MAX) ? 1 : 0;
|
||||
r->d[i] = diff;
|
||||
}
|
||||
/* If a was 0, r is now P; fold back to 0. Normal inputs leave r < P. */
|
||||
fe_normalize(r);
|
||||
}
|
||||
|
||||
/* ==================== Function declarations ==================== */
|
||||
@@ -159,8 +156,156 @@ static inline void fe_negate(secp256k1_fe *r, const secp256k1_fe *a, int m) {
|
||||
* calling fe_mul), we rely on LTO or the static inline below. */
|
||||
#include "field_asm.h"
|
||||
|
||||
#if HAVE_INT128
|
||||
/*
|
||||
* Dedicated field squaring using 10 multiplications instead of the 16 that
|
||||
* an unspecialized 4x4 schoolbook mul would use. Exploits symmetry
|
||||
* a[i]*a[j] == a[j]*a[i] with 4 diagonal + 6 doubled cross products.
|
||||
*
|
||||
* Three-pass structure (same as the Kotlin U256.sqrWide):
|
||||
* Pass 1: compute un-doubled cross-product sum in 6 limbs
|
||||
* Pass 2: shift left by 1 (doubles every cross product simultaneously)
|
||||
* Pass 3: add the 4 diagonal products a[i]^2
|
||||
*
|
||||
* NOTE: this inline is only wired up for the non-FE_MUL_ASM build path.
|
||||
* On x86_64 GCC with BMI2/ADX and on ARM64 GCC, fe_mul_asm's hand-tuned
|
||||
* MULX + ADCX/ADOX (or ARM64 MUL/UMULH) carry-chain scheduling beats the
|
||||
* __int128 path despite fe_mul_asm using 16 muls. Benchmark measurement
|
||||
* (bench_vs_acinq) showed fe_sqr_inline regressed verify/batch-verify by
|
||||
* ~10-25% on x86_64 when used in place of fe_mul_asm(r, a, a), so on
|
||||
* ASM platforms we keep fe_mul_asm(r, a, a) for fe_sqr. The 10-mul
|
||||
* version still helps portable builds (clang, MSVC, non-GCC toolchains).
|
||||
*/
|
||||
static inline void fe_sqr_inline(secp256k1_fe *r, const secp256k1_fe *a) {
|
||||
uint64_t a0 = a->d[0], a1 = a->d[1], a2 = a->d[2], a3 = a->d[3];
|
||||
uint128_t acc;
|
||||
|
||||
/* ===== Pass 1: un-doubled cross-product sum =====
|
||||
* Cross products and their column positions:
|
||||
* col 1: a0*a1
|
||||
* col 2: a0*a2
|
||||
* col 3: a0*a3 + a1*a2 (two products in one column)
|
||||
* col 4: a1*a3
|
||||
* col 5: a2*a3
|
||||
*
|
||||
* Cols 1,2,4,5 each have a single product, so the 128-bit accumulator
|
||||
* trick (`acc += new_product; extract low 64`) stays within bounds:
|
||||
* after the extract, acc < 2^64, and adding another full 128-bit product
|
||||
* gives at most (2^64-1)+(2^128-2^65+1) = 2^128-2^64 which still fits.
|
||||
*
|
||||
* Col 3 is the only column with two products, so the straightforward
|
||||
* `acc += p1; acc += p2` sum can overflow 128 bits. We instead extract
|
||||
* the low limb after the first product, then manually fold the second
|
||||
* product's low/high halves into the running accumulator with an
|
||||
* explicit carry flag.
|
||||
*/
|
||||
acc = (uint128_t)a0 * a1;
|
||||
uint64_t x1 = (uint64_t)acc; acc >>= 64;
|
||||
acc += (uint128_t)a0 * a2;
|
||||
uint64_t x2 = (uint64_t)acc; acc >>= 64;
|
||||
acc += (uint128_t)a0 * a3;
|
||||
uint64_t x3_partial = (uint64_t)acc; acc >>= 64;
|
||||
/* Fold the second col-3 product (a1*a2) into x3 and the running carry. */
|
||||
uint128_t p12 = (uint128_t)a1 * a2;
|
||||
uint64_t p12_lo = (uint64_t)p12;
|
||||
uint64_t p12_hi = (uint64_t)(p12 >> 64);
|
||||
uint64_t x3 = x3_partial + p12_lo;
|
||||
uint64_t x3_carry = (x3 < x3_partial) ? 1ULL : 0ULL;
|
||||
acc += (uint128_t)p12_hi + x3_carry;
|
||||
acc += (uint128_t)a1 * a3;
|
||||
uint64_t x4 = (uint64_t)acc; acc >>= 64;
|
||||
acc += (uint128_t)a2 * a3;
|
||||
uint64_t x5 = (uint64_t)acc;
|
||||
uint64_t x6 = (uint64_t)(acc >> 64);
|
||||
|
||||
/* ===== Pass 2: shift the cross-product sum left by one bit ===== */
|
||||
uint64_t x7 = x6 >> 63;
|
||||
x6 = (x6 << 1) | (x5 >> 63);
|
||||
x5 = (x5 << 1) | (x4 >> 63);
|
||||
x4 = (x4 << 1) | (x3 >> 63);
|
||||
x3 = (x3 << 1) | (x2 >> 63);
|
||||
x2 = (x2 << 1) | (x1 >> 63);
|
||||
x1 = x1 << 1;
|
||||
|
||||
/* ===== Pass 3: add the 4 diagonal products a[i]^2 ===== */
|
||||
uint64_t d0lo, d0hi, d1lo, d1hi, d2lo, d2hi, d3lo, d3hi;
|
||||
acc = (uint128_t)a0 * a0;
|
||||
d0lo = (uint64_t)acc; d0hi = (uint64_t)(acc >> 64);
|
||||
acc = (uint128_t)a1 * a1;
|
||||
d1lo = (uint64_t)acc; d1hi = (uint64_t)(acc >> 64);
|
||||
acc = (uint128_t)a2 * a2;
|
||||
d2lo = (uint64_t)acc; d2hi = (uint64_t)(acc >> 64);
|
||||
acc = (uint128_t)a3 * a3;
|
||||
d3lo = (uint64_t)acc; d3hi = (uint64_t)(acc >> 64);
|
||||
|
||||
/* w[] = full 8-limb product, cross-sum already doubled. */
|
||||
uint64_t w0 = d0lo;
|
||||
acc = (uint128_t)x1 + d0hi;
|
||||
uint64_t w1 = (uint64_t)acc; acc >>= 64;
|
||||
acc += (uint128_t)x2 + d1lo;
|
||||
uint64_t w2 = (uint64_t)acc; acc >>= 64;
|
||||
acc += (uint128_t)x3 + d1hi;
|
||||
uint64_t w3 = (uint64_t)acc; acc >>= 64;
|
||||
acc += (uint128_t)x4 + d2lo;
|
||||
uint64_t w4 = (uint64_t)acc; acc >>= 64;
|
||||
acc += (uint128_t)x5 + d2hi;
|
||||
uint64_t w5 = (uint64_t)acc; acc >>= 64;
|
||||
acc += (uint128_t)x6 + d3lo;
|
||||
uint64_t w6 = (uint64_t)acc; acc >>= 64;
|
||||
acc += (uint128_t)x7 + d3hi;
|
||||
uint64_t w7 = (uint64_t)acc;
|
||||
/* Any carry out of w7 is impossible: the full product a^2 < 2^512,
|
||||
* and we've accounted for every bit. */
|
||||
|
||||
/* ===== Reduce: r = w[0..3] + w[4..7] * C ===== */
|
||||
const uint64_t FC = 0x1000003D1ULL;
|
||||
acc = (uint128_t)w0 + (uint128_t)w4 * FC;
|
||||
r->d[0] = (uint64_t)acc; acc >>= 64;
|
||||
acc += (uint128_t)w1 + (uint128_t)w5 * FC;
|
||||
r->d[1] = (uint64_t)acc; acc >>= 64;
|
||||
acc += (uint128_t)w2 + (uint128_t)w6 * FC;
|
||||
r->d[2] = (uint64_t)acc; acc >>= 64;
|
||||
acc += (uint128_t)w3 + (uint128_t)w7 * FC;
|
||||
r->d[3] = (uint64_t)acc;
|
||||
uint64_t carry = (uint64_t)(acc >> 64);
|
||||
|
||||
/* Final fold loop (same shape as fe_mul). */
|
||||
while (carry) {
|
||||
acc = (uint128_t)r->d[0] + (uint128_t)carry * FC;
|
||||
r->d[0] = (uint64_t)acc;
|
||||
uint64_t c1 = (uint64_t)(acc >> 64);
|
||||
if (c1) {
|
||||
uint64_t s = r->d[1] + c1;
|
||||
uint64_t cc = (s < c1) ? 1 : 0;
|
||||
r->d[1] = s;
|
||||
if (cc) {
|
||||
s = r->d[2] + 1;
|
||||
cc = (s == 0) ? 1 : 0;
|
||||
r->d[2] = s;
|
||||
if (cc) {
|
||||
s = r->d[3] + 1;
|
||||
r->d[3] = s;
|
||||
carry = (s == 0) ? 1 : 0;
|
||||
continue;
|
||||
}
|
||||
}
|
||||
}
|
||||
carry = 0;
|
||||
}
|
||||
}
|
||||
#endif /* HAVE_INT128 */
|
||||
|
||||
#if FE_MUL_ASM
|
||||
/* Use the ASM version directly as static inline so point.c can inline it */
|
||||
/* Use the ASM version directly as static inline so point.c can inline it.
|
||||
*
|
||||
* fe_sqr: on FE_MUL_ASM platforms (x86_64 GCC with BMI2/ADX, ARM64 GCC) the
|
||||
* hand-tuned fe_mul_asm uses MULX + dual ADCX/ADOX carry chains that the
|
||||
* __int128-based fe_sqr_inline cannot match in practice. Measurement showed
|
||||
* that saving 6 multiplications via symmetry doesn't compensate for losing
|
||||
* the ASM's optimized carry-chain scheduling and register usage. So on ASM
|
||||
* platforms we just square via fe_mul_asm(r, a, a), which is faster in
|
||||
* wall-clock terms. The 10-mul fe_sqr_inline is still used for compilers
|
||||
* without the ASM path (clang, MSVC, portable fallback). */
|
||||
static inline void fe_mul(secp256k1_fe *r, const secp256k1_fe *a, const secp256k1_fe *b) {
|
||||
fe_mul_asm(r, a, b);
|
||||
}
|
||||
@@ -169,8 +314,14 @@ static inline void fe_sqr(secp256k1_fe *r, const secp256k1_fe *a) {
|
||||
}
|
||||
#else
|
||||
void fe_mul(secp256k1_fe *r, const secp256k1_fe *a, const secp256k1_fe *b);
|
||||
#if HAVE_INT128
|
||||
static inline void fe_sqr(secp256k1_fe *r, const secp256k1_fe *a) {
|
||||
fe_sqr_inline(r, a);
|
||||
}
|
||||
#else
|
||||
void fe_sqr(secp256k1_fe *r, const secp256k1_fe *a);
|
||||
#endif
|
||||
#endif
|
||||
void fe_inv(secp256k1_fe *r, const secp256k1_fe *a);
|
||||
int fe_sqrt(secp256k1_fe *r, const secp256k1_fe *a);
|
||||
void fe_half(secp256k1_fe *r, const secp256k1_fe *a);
|
||||
|
||||
@@ -31,6 +31,38 @@ static jbyteArray make_bytes(JNIEnv *env, const uint8_t *data, int len) {
|
||||
return arr;
|
||||
}
|
||||
|
||||
/*
|
||||
* Copy a variable-length Java byte[] into a native buffer without pinning.
|
||||
*
|
||||
* GetByteArrayElements pins the underlying Java array, blocking GC compaction
|
||||
* for the duration of the call. For short-lived crypto operations (signing
|
||||
* or verifying a Nostr event, almost always a 32-byte message digest), copying
|
||||
* via GetByteArrayRegion into a stack or heap buffer is both cheaper and
|
||||
* friendlier to the GC. Small messages (<=512 B) go on the stack; larger
|
||||
* ones allocate via malloc. Caller frees `*out_buf` only if `*needs_free`.
|
||||
*/
|
||||
static int copy_msg_bytes(JNIEnv *env, jbyteArray arr,
|
||||
uint8_t *stack_buf, size_t stack_cap,
|
||||
uint8_t **out_buf, size_t *out_len,
|
||||
int *needs_free) {
|
||||
if (!arr) return 0;
|
||||
jint len = (*env)->GetArrayLength(env, arr);
|
||||
if (len < 0) return 0;
|
||||
*out_len = (size_t)len;
|
||||
*needs_free = 0;
|
||||
if ((size_t)len <= stack_cap) {
|
||||
*out_buf = stack_buf;
|
||||
} else {
|
||||
*out_buf = (uint8_t *)malloc((size_t)len);
|
||||
if (!*out_buf) return 0;
|
||||
*needs_free = 1;
|
||||
}
|
||||
if (len > 0) {
|
||||
(*env)->GetByteArrayRegion(env, arr, 0, len, (jbyte *)*out_buf);
|
||||
}
|
||||
return 1;
|
||||
}
|
||||
|
||||
/* ==================== Library Init ==================== */
|
||||
|
||||
JNIEXPORT void JNICALL
|
||||
@@ -83,9 +115,14 @@ Java_com_vitorpamplona_quartz_utils_Secp256k1C_nativeSchnorrSign(
|
||||
uint8_t sk[32], aux[32], sig[64];
|
||||
if (!get_bytes(env, seckey, sk, 32)) return NULL;
|
||||
|
||||
jint msg_len = (*env)->GetArrayLength(env, msg);
|
||||
uint8_t *msg_buf = (uint8_t *)(*env)->GetByteArrayElements(env, msg, NULL);
|
||||
if (!msg_buf) return NULL;
|
||||
uint8_t stack_msg[512];
|
||||
uint8_t *msg_buf = NULL;
|
||||
size_t msg_len = 0;
|
||||
int needs_free = 0;
|
||||
if (!copy_msg_bytes(env, msg, stack_msg, sizeof(stack_msg),
|
||||
&msg_buf, &msg_len, &needs_free)) {
|
||||
return NULL;
|
||||
}
|
||||
|
||||
uint8_t *aux_ptr = NULL;
|
||||
if (auxrand) {
|
||||
@@ -94,8 +131,8 @@ Java_com_vitorpamplona_quartz_utils_Secp256k1C_nativeSchnorrSign(
|
||||
}
|
||||
}
|
||||
|
||||
int ok = secp256k1c_schnorr_sign(sig, msg_buf, (size_t)msg_len, sk, aux_ptr);
|
||||
(*env)->ReleaseByteArrayElements(env, msg, (jbyte *)msg_buf, JNI_ABORT);
|
||||
int ok = secp256k1c_schnorr_sign(sig, msg_buf, msg_len, sk, aux_ptr);
|
||||
if (needs_free) free(msg_buf);
|
||||
|
||||
return ok ? make_bytes(env, sig, 64) : NULL;
|
||||
}
|
||||
@@ -110,9 +147,14 @@ Java_com_vitorpamplona_quartz_utils_Secp256k1C_nativeSchnorrSignXOnly(
|
||||
if (!get_bytes(env, seckey, sk, 32)) return NULL;
|
||||
if (!get_bytes(env, xonlyPub, xonly, 32)) return NULL;
|
||||
|
||||
jint msg_len = (*env)->GetArrayLength(env, msg);
|
||||
uint8_t *msg_buf = (uint8_t *)(*env)->GetByteArrayElements(env, msg, NULL);
|
||||
if (!msg_buf) return NULL;
|
||||
uint8_t stack_msg[512];
|
||||
uint8_t *msg_buf = NULL;
|
||||
size_t msg_len = 0;
|
||||
int needs_free = 0;
|
||||
if (!copy_msg_bytes(env, msg, stack_msg, sizeof(stack_msg),
|
||||
&msg_buf, &msg_len, &needs_free)) {
|
||||
return NULL;
|
||||
}
|
||||
|
||||
uint8_t *aux_ptr = NULL;
|
||||
if (auxrand) {
|
||||
@@ -121,8 +163,8 @@ Java_com_vitorpamplona_quartz_utils_Secp256k1C_nativeSchnorrSignXOnly(
|
||||
}
|
||||
}
|
||||
|
||||
int ok = secp256k1c_schnorr_sign_xonly(sig, msg_buf, (size_t)msg_len, sk, xonly, aux_ptr);
|
||||
(*env)->ReleaseByteArrayElements(env, msg, (jbyte *)msg_buf, JNI_ABORT);
|
||||
int ok = secp256k1c_schnorr_sign_xonly(sig, msg_buf, msg_len, sk, xonly, aux_ptr);
|
||||
if (needs_free) free(msg_buf);
|
||||
|
||||
return ok ? make_bytes(env, sig, 64) : NULL;
|
||||
}
|
||||
@@ -138,12 +180,17 @@ Java_com_vitorpamplona_quartz_utils_Secp256k1C_nativeSchnorrVerify(
|
||||
if (!get_bytes(env, sig, s, 64)) return JNI_FALSE;
|
||||
if (!get_bytes(env, pub, p, 32)) return JNI_FALSE;
|
||||
|
||||
jint msg_len = (*env)->GetArrayLength(env, msg);
|
||||
uint8_t *msg_buf = (uint8_t *)(*env)->GetByteArrayElements(env, msg, NULL);
|
||||
if (!msg_buf) return JNI_FALSE;
|
||||
uint8_t stack_msg[512];
|
||||
uint8_t *msg_buf = NULL;
|
||||
size_t msg_len = 0;
|
||||
int needs_free = 0;
|
||||
if (!copy_msg_bytes(env, msg, stack_msg, sizeof(stack_msg),
|
||||
&msg_buf, &msg_len, &needs_free)) {
|
||||
return JNI_FALSE;
|
||||
}
|
||||
|
||||
int ok = secp256k1c_schnorr_verify(s, msg_buf, (size_t)msg_len, p);
|
||||
(*env)->ReleaseByteArrayElements(env, msg, (jbyte *)msg_buf, JNI_ABORT);
|
||||
int ok = secp256k1c_schnorr_verify(s, msg_buf, msg_len, p);
|
||||
if (needs_free) free(msg_buf);
|
||||
return ok ? JNI_TRUE : JNI_FALSE;
|
||||
}
|
||||
|
||||
@@ -156,12 +203,17 @@ Java_com_vitorpamplona_quartz_utils_Secp256k1C_nativeSchnorrVerifyFast(
|
||||
if (!get_bytes(env, sig, s, 64)) return JNI_FALSE;
|
||||
if (!get_bytes(env, pub, p, 32)) return JNI_FALSE;
|
||||
|
||||
jint msg_len = (*env)->GetArrayLength(env, msg);
|
||||
uint8_t *msg_buf = (uint8_t *)(*env)->GetByteArrayElements(env, msg, NULL);
|
||||
if (!msg_buf) return JNI_FALSE;
|
||||
uint8_t stack_msg[512];
|
||||
uint8_t *msg_buf = NULL;
|
||||
size_t msg_len = 0;
|
||||
int needs_free = 0;
|
||||
if (!copy_msg_bytes(env, msg, stack_msg, sizeof(stack_msg),
|
||||
&msg_buf, &msg_len, &needs_free)) {
|
||||
return JNI_FALSE;
|
||||
}
|
||||
|
||||
int ok = secp256k1c_schnorr_verify_fast(s, msg_buf, (size_t)msg_len, p);
|
||||
(*env)->ReleaseByteArrayElements(env, msg, (jbyte *)msg_buf, JNI_ABORT);
|
||||
int ok = secp256k1c_schnorr_verify_fast(s, msg_buf, msg_len, p);
|
||||
if (needs_free) free(msg_buf);
|
||||
return ok ? JNI_TRUE : JNI_FALSE;
|
||||
}
|
||||
|
||||
@@ -282,12 +334,17 @@ Java_com_vitorpamplona_quartz_utils_Secp256k1C_nativeSha256(
|
||||
JNIEnv *env, jclass cls, jbyteArray data
|
||||
) {
|
||||
(void)cls;
|
||||
jint len = (*env)->GetArrayLength(env, data);
|
||||
uint8_t *buf = (uint8_t *)(*env)->GetByteArrayElements(env, data, NULL);
|
||||
if (!buf) return NULL;
|
||||
uint8_t stack_buf[1024];
|
||||
uint8_t *buf = NULL;
|
||||
size_t len = 0;
|
||||
int needs_free = 0;
|
||||
if (!copy_msg_bytes(env, data, stack_buf, sizeof(stack_buf),
|
||||
&buf, &len, &needs_free)) {
|
||||
return NULL;
|
||||
}
|
||||
|
||||
uint8_t out[32];
|
||||
secp256k1_sha256_hash(out, buf, (size_t)len);
|
||||
(*env)->ReleaseByteArrayElements(env, data, (jbyte *)buf, JNI_ABORT);
|
||||
secp256k1_sha256_hash(out, buf, len);
|
||||
if (needs_free) free(buf);
|
||||
return make_bytes(env, out, 32);
|
||||
}
|
||||
|
||||
@@ -5,6 +5,11 @@
|
||||
#include "scalar.h"
|
||||
#include <string.h>
|
||||
|
||||
/* 2^256 mod n — a ~129-bit constant. Used by scalar_add and scalar_mul. */
|
||||
static const uint64_t SCALAR_NC[4] = {
|
||||
0x402DA1732FC9BEBFULL, 0x4551231950B75FC4ULL, 1, 0
|
||||
};
|
||||
|
||||
int scalar_is_zero(const secp256k1_scalar *a) {
|
||||
return (a->d[0] | a->d[1] | a->d[2] | a->d[3]) == 0;
|
||||
}
|
||||
@@ -52,14 +57,9 @@ void scalar_reduce(secp256k1_scalar *r) {
|
||||
void scalar_add(secp256k1_scalar *r, const secp256k1_scalar *a, const secp256k1_scalar *b) {
|
||||
int carry = add256(r->d, a->d, b->d);
|
||||
if (carry) {
|
||||
/* Overflow: subtract n. n_complement = 2^256 - n */
|
||||
uint64_t nc[4] = {
|
||||
~SCALAR_N.d[0] + 1,
|
||||
~SCALAR_N.d[1] + (!SCALAR_N.d[0] ? 1ULL : 0ULL),
|
||||
~SCALAR_N.d[2] + (!(SCALAR_N.d[0] | SCALAR_N.d[1]) ? 1ULL : 0ULL),
|
||||
~SCALAR_N.d[3]
|
||||
};
|
||||
add256(r->d, r->d, nc);
|
||||
/* Overflow past 2^256: r_true = (a+b) - 2^256 + 2^256 = r + 2^256.
|
||||
* Since 2^256 ≡ NC (mod n), adding NC undoes the wraparound mod n. */
|
||||
add256(r->d, r->d, SCALAR_NC);
|
||||
}
|
||||
scalar_reduce(r);
|
||||
}
|
||||
@@ -81,62 +81,50 @@ void scalar_sub(secp256k1_scalar *r, const secp256k1_scalar *a, const secp256k1_
|
||||
/* Use the field module's proven mul_wide for 4x4 → 8-limb product */
|
||||
extern void mul_wide(uint64_t out[8], const uint64_t a[4], const uint64_t b[4]);
|
||||
|
||||
/* Multiply mod n: r = (a * b) mod n */
|
||||
/*
|
||||
* Multiply mod n: r = (a * b) mod n.
|
||||
*
|
||||
* Fully reduces the 512-bit product via repeated folding: 2^256 ≡ NC (mod n).
|
||||
* Because NC < 2^130, each round strictly shrinks the high half, so the fold
|
||||
* converges in at most 3 iterations before hi is entirely zero. Loop-driven
|
||||
* to avoid the subtle third-fold carry-drop bug the previous unrolled version
|
||||
* had, and to give correct results in the portable (!HAVE_INT128) path too.
|
||||
*/
|
||||
void scalar_mul(secp256k1_scalar *r, const secp256k1_scalar *a, const secp256k1_scalar *b) {
|
||||
/* 2^256 mod n */
|
||||
static const uint64_t NC[4] = {
|
||||
0x402DA1732FC9BEBFULL, 0x4551231950B75FC4ULL, 1, 0
|
||||
};
|
||||
|
||||
uint64_t t[8];
|
||||
mul_wide(t, a->d, b->d);
|
||||
|
||||
/* Reduce: r = t[0..3] + t[4..7]*NC. Use mul_wide for the hi*NC product. */
|
||||
uint64_t hc[8];
|
||||
mul_wide(hc, &t[4], NC);
|
||||
uint64_t lo[4] = { t[0], t[1], t[2], t[3] };
|
||||
uint64_t hi[4] = { t[4], t[5], t[6], t[7] };
|
||||
|
||||
/* Add lo + hc */
|
||||
#if HAVE_INT128
|
||||
uint128_t acc = 0;
|
||||
uint64_t sum[8];
|
||||
for (int i = 0; i < 8; i++) {
|
||||
acc += (uint128_t)(i < 4 ? t[i] : 0) + hc[i];
|
||||
sum[i] = (uint64_t)acc;
|
||||
acc >>= 64;
|
||||
}
|
||||
/* Second fold: sum[4..7] * NC + sum[0..3] */
|
||||
if (sum[4] | sum[5] | sum[6] | sum[7]) {
|
||||
uint64_t hc2[8];
|
||||
mul_wide(hc2, &sum[4], NC);
|
||||
acc = 0;
|
||||
uint64_t sum2[8];
|
||||
for (int i = 0; i < 8; i++) {
|
||||
acc += (uint128_t)(i < 4 ? sum[i] : 0) + hc2[i];
|
||||
sum2[i] = (uint64_t)acc;
|
||||
acc >>= 64;
|
||||
while ((hi[0] | hi[1] | hi[2] | hi[3]) != 0) {
|
||||
/* wide = hi * NC (an 8-limb product), then wide += lo. */
|
||||
uint64_t wide[8];
|
||||
mul_wide(wide, hi, SCALAR_NC);
|
||||
uint64_t carry = 0;
|
||||
for (int i = 0; i < 4; i++) {
|
||||
uint64_t s1 = lo[i] + wide[i];
|
||||
uint64_t c1 = (s1 < lo[i]) ? 1ULL : 0ULL;
|
||||
uint64_t s2 = s1 + carry;
|
||||
uint64_t c2 = (s2 < s1) ? 1ULL : 0ULL;
|
||||
wide[i] = s2;
|
||||
carry = c1 + c2;
|
||||
}
|
||||
/* Third fold if still > 256 bits (sum2 is at most ~130 bits above 256) */
|
||||
if (sum2[4] | sum2[5] | sum2[6] | sum2[7]) {
|
||||
/* sum2[4..7] is tiny (~2 limbs at most). Use reduce_wide pattern. */
|
||||
acc = (uint128_t)sum2[0] + (uint128_t)sum2[4] * NC[0];
|
||||
r->d[0] = (uint64_t)acc; acc >>= 64;
|
||||
acc += (uint128_t)sum2[1] + (uint128_t)sum2[4] * NC[1] + (uint128_t)sum2[5] * NC[0];
|
||||
r->d[1] = (uint64_t)acc; acc >>= 64;
|
||||
acc += (uint128_t)sum2[2] + (uint128_t)sum2[4] * NC[2] + (uint128_t)sum2[5] * NC[1] + (uint128_t)sum2[6] * NC[0];
|
||||
r->d[2] = (uint64_t)acc; acc >>= 64;
|
||||
acc += (uint128_t)sum2[3] + (uint128_t)sum2[5] * NC[2] + (uint128_t)sum2[6] * NC[1] + (uint128_t)sum2[7] * NC[0];
|
||||
r->d[3] = (uint64_t)acc;
|
||||
/* Any remaining carry is negligible — handled by while loop below */
|
||||
} else {
|
||||
r->d[0] = sum2[0]; r->d[1] = sum2[1]; r->d[2] = sum2[2]; r->d[3] = sum2[3];
|
||||
for (int i = 4; i < 8 && carry != 0; i++) {
|
||||
uint64_t s = wide[i] + carry;
|
||||
carry = (s < wide[i]) ? 1ULL : 0ULL;
|
||||
wide[i] = s;
|
||||
}
|
||||
} else {
|
||||
r->d[0] = sum[0]; r->d[1] = sum[1]; r->d[2] = sum[2]; r->d[3] = sum[3];
|
||||
/* carry out of wide[7] is impossible: NC < 2^130, so hi * NC is
|
||||
* bounded by 2^256 * 2^130 = 2^386, fitting comfortably in wide[].
|
||||
* Adding lo (< 2^256) and a tiny running carry stays inside wide[]. */
|
||||
|
||||
lo[0] = wide[0]; lo[1] = wide[1]; lo[2] = wide[2]; lo[3] = wide[3];
|
||||
hi[0] = wide[4]; hi[1] = wide[5]; hi[2] = wide[6]; hi[3] = wide[7];
|
||||
}
|
||||
#else
|
||||
/* Portable: just take low 4 limbs and subtract n repeatedly */
|
||||
r->d[0] = t[0]; r->d[1] = t[1]; r->d[2] = t[2]; r->d[3] = t[3];
|
||||
#endif
|
||||
|
||||
r->d[0] = lo[0]; r->d[1] = lo[1]; r->d[2] = lo[2]; r->d[3] = lo[3];
|
||||
/* At this point r < n + small-epsilon; a single subtract is enough. */
|
||||
while (scalar_cmp(r, &SCALAR_N) >= 0) {
|
||||
sub256(r->d, r->d, SCALAR_N.d);
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user