feat: add signSchnorrWithXOnlyPubKey for zero-copy cached signing

BIP-340 public keys always have even y, so the y-parity prefix byte
(0x02) is redundant when the caller already has the 32-byte x-only
pubkey. This new overload takes the x-only pubkey directly, avoiding:

1. The expensive G multiplication to derive the pubkey (~20μs on Android)
2. The 33→32 byte array copy that signSchnorrWithPubKey does internally

Added to:
- Secp256k1.signSchnorrWithXOnlyPubKey (core implementation)
- Secp256k1Instance (expect/actual, falls back to C lib's signSchnorr
  since the native C lib always derives pubkey internally)
- Secp256k1InstanceOurs (uses the optimized pure-Kotlin path)
- Nip01Crypto.signWithPubKey (app-level convenience)

The app's KeyPair already stores the 32-byte x-only pubkey. Callers
like EventAssembler.hashAndSign can pass it through to skip the
G multiplication entirely.

https://claude.ai/code/session_01EMY5RnXb9rnsyU2KbXrSaY
This commit is contained in:
Claude
2026-04-09 02:17:25 +00:00
parent 0c27ed89bc
commit 65652d1256
7 changed files with 72 additions and 0 deletions
@@ -41,6 +41,14 @@ actual object Secp256k1Instance {
privKey: ByteArray,
): ByteArray = secp256k1.signSchnorr(data, privKey, null)
// Native C lib always derives pubkey internally — ignore the cached pubkey.
actual fun signSchnorrWithXOnlyPubKey(
data: ByteArray,
privKey: ByteArray,
xOnlyPubKey: ByteArray,
nonce: ByteArray?,
): ByteArray = secp256k1.signSchnorr(data, privKey, nonce)
actual fun verifySchnorr(
signature: ByteArray,
hash: ByteArray,