perf: Jacobian x-check + inline wNAF zero-checks in verify
Two optimizations for verifySchnorr: 1. Jacobian x-coordinate check before toAffine: Instead of converting to affine first (1 inversion = ~270 field ops), check X == r·Z² in Jacobian coordinates (1 sqr + 1 mul = 2 ops). Invalid signatures (x mismatch) are rejected immediately without paying the inversion cost. Valid signatures still need inversion for the y-parity check. For Nostr, ~all sigs are valid so this mainly helps adversarial/spam rejection. 2. Inline wNAF zero-checks in mulDoubleG inner loop: ~70% of wNAF digits are zero. Previously, each still called addWnafMixedPP (function call overhead: null checks, frame setup). Now the zero-check is inlined before the call, avoiding ~364 function calls per verify. On ART (5-8ns per call), this saves ~2-3μs. On HotSpot, the JIT already optimized this — no change. https://claude.ai/code/session_01EMY5RnXb9rnsyU2KbXrSaY
This commit is contained in:
@@ -677,29 +677,80 @@ internal object ECPoint {
|
|||||||
cur.setInfinity()
|
cur.setInfinity()
|
||||||
val negY = sc.mixNegY
|
val negY = sc.mixNegY
|
||||||
|
|
||||||
|
// Inner loop: double + conditionally add from 4 wNAF streams.
|
||||||
|
//
|
||||||
|
// The wNAF zero-check is INLINED here rather than delegated to
|
||||||
|
// addWnafMixedPP. ~70% of digits are zero, so this avoids ~364
|
||||||
|
// function calls per verify. Each function call on ART has ~5-8ns
|
||||||
|
// overhead (parameter null checks, frame setup), saving ~2-3μs total.
|
||||||
|
val negK1s = sSplit.negK1
|
||||||
|
val negK2s = sSplit.negK2
|
||||||
|
val negK1e = eSplit.negK1
|
||||||
|
val negK2e = eSplit.negK2
|
||||||
|
|
||||||
for (i in bits - 1 downTo 0) {
|
for (i in bits - 1 downTo 0) {
|
||||||
doublePoint(alt, cur, sc)
|
doublePoint(alt, cur, sc)
|
||||||
var t = cur
|
var t = cur
|
||||||
cur = alt
|
cur = alt
|
||||||
alt = t
|
alt = t
|
||||||
// Streams 1-2: G-side (affine tables, mixed addition)
|
|
||||||
if (addWnafMixedPP(cur, alt, negY, wnafS1, i, gOdd, sSplit.negK1, sc)) {
|
var d: Int
|
||||||
|
|
||||||
|
// Stream 1: s₁ (G-side)
|
||||||
|
d = wnafS1[i]
|
||||||
|
if (d != 0) {
|
||||||
|
val idx = (if (d > 0) d else -d) / 2
|
||||||
|
if ((d < 0) xor negK1s) {
|
||||||
|
FieldP.neg(negY, gOdd[idx].y)
|
||||||
|
addMixed(alt, cur, gOdd[idx].x, negY, sc)
|
||||||
|
} else {
|
||||||
|
addMixed(alt, cur, gOdd[idx].x, gOdd[idx].y, sc)
|
||||||
|
}
|
||||||
t = cur
|
t = cur
|
||||||
cur = alt
|
cur = alt
|
||||||
alt = t
|
alt = t
|
||||||
}
|
}
|
||||||
if (addWnafMixedPP(cur, alt, negY, wnafS2, i, gLam, sSplit.negK2, sc)) {
|
|
||||||
|
// Stream 2: s₂ (λ(G)-side)
|
||||||
|
d = wnafS2[i]
|
||||||
|
if (d != 0) {
|
||||||
|
val idx = (if (d > 0) d else -d) / 2
|
||||||
|
if ((d < 0) xor negK2s) {
|
||||||
|
FieldP.neg(negY, gLam[idx].y)
|
||||||
|
addMixed(alt, cur, gLam[idx].x, negY, sc)
|
||||||
|
} else {
|
||||||
|
addMixed(alt, cur, gLam[idx].x, gLam[idx].y, sc)
|
||||||
|
}
|
||||||
t = cur
|
t = cur
|
||||||
cur = alt
|
cur = alt
|
||||||
alt = t
|
alt = t
|
||||||
}
|
}
|
||||||
// Streams 3-4: P-side (affine tables via effective-affine, mixed addition)
|
|
||||||
if (addWnafMixedPP(cur, alt, negY, wnafE1, i, pOdd, eSplit.negK1, sc)) {
|
// Stream 3: e₁ (P-side)
|
||||||
|
d = wnafE1[i]
|
||||||
|
if (d != 0) {
|
||||||
|
val idx = (if (d > 0) d else -d) / 2
|
||||||
|
if ((d < 0) xor negK1e) {
|
||||||
|
FieldP.neg(negY, pOdd[idx].y)
|
||||||
|
addMixed(alt, cur, pOdd[idx].x, negY, sc)
|
||||||
|
} else {
|
||||||
|
addMixed(alt, cur, pOdd[idx].x, pOdd[idx].y, sc)
|
||||||
|
}
|
||||||
t = cur
|
t = cur
|
||||||
cur = alt
|
cur = alt
|
||||||
alt = t
|
alt = t
|
||||||
}
|
}
|
||||||
if (addWnafMixedPP(cur, alt, negY, wnafE2, i, pLamOdd, eSplit.negK2, sc)) {
|
|
||||||
|
// Stream 4: e₂ (λ(P)-side)
|
||||||
|
d = wnafE2[i]
|
||||||
|
if (d != 0) {
|
||||||
|
val idx = (if (d > 0) d else -d) / 2
|
||||||
|
if ((d < 0) xor negK2e) {
|
||||||
|
FieldP.neg(negY, pLamOdd[idx].y)
|
||||||
|
addMixed(alt, cur, pLamOdd[idx].x, negY, sc)
|
||||||
|
} else {
|
||||||
|
addMixed(alt, cur, pLamOdd[idx].x, pLamOdd[idx].y, sc)
|
||||||
|
}
|
||||||
t = cur
|
t = cur
|
||||||
cur = alt
|
cur = alt
|
||||||
alt = t
|
alt = t
|
||||||
|
|||||||
@@ -391,20 +391,35 @@ object Secp256k1 {
|
|||||||
pub.copyInto(hashInput, 96)
|
pub.copyInto(hashInput, 96)
|
||||||
data.copyInto(hashInput, 128)
|
data.copyInto(hashInput, 128)
|
||||||
val eHash = sha256(hashInput)
|
val eHash = sha256(hashInput)
|
||||||
// Reuse entryPx for e (liftX result already copied into pPoint below)
|
// Reuse zInv for e (safe: zInv not used until toAffine, which we skip here)
|
||||||
val e = sc.zInv // safe: zInv not used until toAffine after mulDoubleG
|
val e = sc.zInv
|
||||||
U256.fromBytesInto(e, eHash, 0)
|
U256.fromBytesInto(e, eHash, 0)
|
||||||
if (U256.cmp(e, ScalarN.N) >= 0) U256.subTo(e, e, ScalarN.N) // inline reduce
|
if (U256.cmp(e, ScalarN.N) >= 0) U256.subTo(e, e, ScalarN.N) // inline reduce
|
||||||
|
|
||||||
// R = s·G + (-e)·P via Shamir's trick
|
// Q = s·G + (-e)·P via Shamir's trick
|
||||||
ScalarN.negTo(e, e) // negate in-place
|
ScalarN.negTo(e, e) // negate in-place
|
||||||
sc.entryPoint.setAffine(sc.entryPx, sc.entryPy) // copies px/py, so entryPx is free
|
sc.entryPoint.setAffine(sc.entryPx, sc.entryPy) // copies px/py, so entryPx is free
|
||||||
ECPoint.mulDoubleG(sc.entryResult, s, sc.entryPoint, e)
|
ECPoint.mulDoubleG(sc.entryResult, s, sc.entryPoint, e)
|
||||||
|
|
||||||
if (sc.entryResult.isInfinity()) return false
|
if (sc.entryResult.isInfinity()) return false
|
||||||
if (!ECPoint.toAffine(sc.entryResult, sc.entryPx, sc.entryPy, sc)) return false
|
|
||||||
if (!KeyCodec.hasEvenY(sc.entryPy)) return false
|
// Check x-coordinate in Jacobian FIRST (2 field ops, no inversion).
|
||||||
return U256.cmp(sc.entryPx, r) == 0
|
// This is what C libsecp256k1 does: X/Z² == r → X == r·Z².
|
||||||
|
// For invalid signatures (~any non-matching x), this saves the expensive
|
||||||
|
// toAffine inversion (~270 field ops). For valid signatures (Nostr's
|
||||||
|
// common case), we still need toAffine for the y-parity check.
|
||||||
|
val w = sc.w
|
||||||
|
FieldP.sqr(sc.zInv2, sc.entryResult.z, w) // Z²
|
||||||
|
FieldP.mul(sc.zInv3, r, sc.zInv2, w) // r·Z²
|
||||||
|
if (U256.cmp(sc.entryResult.x, sc.zInv3) != 0) return false // x mismatch → reject fast
|
||||||
|
|
||||||
|
// x matches — now check y-parity (requires inversion)
|
||||||
|
FieldP.inv(sc.zInv, sc.entryResult.z)
|
||||||
|
// We already have Z² from above; compute Z^(-2) = inv(Z)², Z^(-3) = Z^(-2) · Z^(-1)
|
||||||
|
FieldP.sqr(sc.zInv2, sc.zInv)
|
||||||
|
FieldP.mul(sc.zInv3, sc.zInv2, sc.zInv)
|
||||||
|
FieldP.mul(sc.entryPy, sc.entryResult.y, sc.zInv3)
|
||||||
|
return KeyCodec.hasEvenY(sc.entryPy)
|
||||||
}
|
}
|
||||||
|
|
||||||
// ==================== Tweak operations ====================
|
// ==================== Tweak operations ====================
|
||||||
|
|||||||
Reference in New Issue
Block a user