diff --git a/quartz/src/androidMain/kotlin/com/vitorpamplona/quartz/utils/Secp256k1Instance.android.kt b/quartz/src/androidMain/kotlin/com/vitorpamplona/quartz/utils/Secp256k1Instance.android.kt index 789fe2404..725c8fedf 100644 --- a/quartz/src/androidMain/kotlin/com/vitorpamplona/quartz/utils/Secp256k1Instance.android.kt +++ b/quartz/src/androidMain/kotlin/com/vitorpamplona/quartz/utils/Secp256k1Instance.android.kt @@ -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, diff --git a/quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/nip01Core/crypto/Nip01Crypto.kt b/quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/nip01Core/crypto/Nip01Crypto.kt index 7022d353e..bf47cdff4 100644 --- a/quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/nip01Core/crypto/Nip01Crypto.kt +++ b/quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/nip01Core/crypto/Nip01Crypto.kt @@ -34,6 +34,18 @@ object Nip01Crypto { nonce: ByteArray? = RandomInstance.bytes(32), ): ByteArray = Secp256k1Instance.signSchnorr(data, privKey, nonce) + /** + * Fast sign with pre-computed x-only pubkey (32 bytes). + * Skips the expensive G multiplication to derive the pubkey (~20μs on Android). + * Use when the caller already has the pubkey (e.g., from KeyPair.pubKey). + */ + fun signWithPubKey( + data: ByteArray, + privKey: ByteArray, + xOnlyPubKey: ByteArray, + nonce: ByteArray? = RandomInstance.bytes(32), + ): ByteArray = Secp256k1Instance.signSchnorrWithXOnlyPubKey(data, privKey, xOnlyPubKey, nonce) + fun verify( signature: ByteArray, hash: ByteArray, diff --git a/quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/utils/Secp256k1Instance.kt b/quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/utils/Secp256k1Instance.kt index 594b025f9..49c435f26 100644 --- a/quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/utils/Secp256k1Instance.kt +++ b/quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/utils/Secp256k1Instance.kt @@ -36,6 +36,13 @@ expect object Secp256k1Instance { privKey: ByteArray, ): ByteArray + fun signSchnorrWithXOnlyPubKey( + data: ByteArray, + privKey: ByteArray, + xOnlyPubKey: ByteArray, + nonce: ByteArray? = RandomInstance.bytes(32), + ): ByteArray + fun verifySchnorr( signature: ByteArray, hash: ByteArray, diff --git a/quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/utils/Secp256k1InstanceOurs.kt b/quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/utils/Secp256k1InstanceOurs.kt index 145de4675..324a83443 100644 --- a/quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/utils/Secp256k1InstanceOurs.kt +++ b/quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/utils/Secp256k1InstanceOurs.kt @@ -48,6 +48,14 @@ object Secp256k1InstanceOurs { nonce: ByteArray? = RandomInstance.bytes(32), ): ByteArray = Secp256k1.signSchnorrWithPubKey(data, privKey, compressedPubKey, nonce) + /** Fast signing with pre-computed x-only public key — zero-copy, zero-allocation. */ + fun signSchnorrWithXOnlyPubKey( + data: ByteArray, + privKey: ByteArray, + xOnlyPubKey: ByteArray, + nonce: ByteArray? = RandomInstance.bytes(32), + ): ByteArray = Secp256k1.signSchnorrWithXOnlyPubKey(data, privKey, xOnlyPubKey, nonce) + fun verifySchnorr( signature: ByteArray, hash: ByteArray, diff --git a/quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/utils/secp256k1/Secp256k1.kt b/quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/utils/secp256k1/Secp256k1.kt index e2f08dbf7..5ac2d9552 100644 --- a/quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/utils/secp256k1/Secp256k1.kt +++ b/quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/utils/secp256k1/Secp256k1.kt @@ -282,6 +282,28 @@ object Secp256k1 { return signSchnorrInternal(data, d0, xOnlyPub, hasEvenY, auxrand) } + /** + * Fast signing with a pre-computed x-only public key (32 bytes). + * + * BIP-340 public keys always have even y, so the y-parity is known (even = true). + * This avoids both the expensive G multiplication to derive the pubkey AND the + * 33→32 byte array copy that signSchnorrWithPubKey does internally. + * + * Use when the caller already has the 32-byte x-only pubkey (e.g., from KeyPair.pubKey). + */ + fun signSchnorrWithXOnlyPubKey( + data: ByteArray, + seckey: ByteArray, + xOnlyPub: ByteArray, + auxrand: ByteArray?, + ): ByteArray { + require(seckey.size == 32 && xOnlyPub.size == 32) + val d0 = U256.fromBytes(seckey) + require(ScalarN.isValid(d0)) + // BIP-340: x-only pubkeys always have even y + return signSchnorrInternal(data, d0, xOnlyPub, true, auxrand) + } + /** * Internal signing implementation shared by both public overloads. * Performs: nonce derivation → R = k·G → challenge → s = k + e·d. diff --git a/quartz/src/jvmMain/kotlin/com/vitorpamplona/quartz/utils/Secp256k1Instance.jvm.kt b/quartz/src/jvmMain/kotlin/com/vitorpamplona/quartz/utils/Secp256k1Instance.jvm.kt index 789fe2404..725c8fedf 100644 --- a/quartz/src/jvmMain/kotlin/com/vitorpamplona/quartz/utils/Secp256k1Instance.jvm.kt +++ b/quartz/src/jvmMain/kotlin/com/vitorpamplona/quartz/utils/Secp256k1Instance.jvm.kt @@ -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, diff --git a/quartz/src/nativeMain/kotlin/com/vitorpamplona/quartz/utils/Secp256k1Instance.native.kt b/quartz/src/nativeMain/kotlin/com/vitorpamplona/quartz/utils/Secp256k1Instance.native.kt index 771c35916..eefd59b56 100644 --- a/quartz/src/nativeMain/kotlin/com/vitorpamplona/quartz/utils/Secp256k1Instance.native.kt +++ b/quartz/src/nativeMain/kotlin/com/vitorpamplona/quartz/utils/Secp256k1Instance.native.kt @@ -41,6 +41,13 @@ actual object Secp256k1Instance { privKey: ByteArray, ): ByteArray = secp256k1Ref.signSchnorr(data, privKey, null) + actual fun signSchnorrWithXOnlyPubKey( + data: ByteArray, + privKey: ByteArray, + xOnlyPubKey: ByteArray, + nonce: ByteArray?, + ): ByteArray = secp256k1Ref.signSchnorr(data, privKey, nonce) + actual fun verifySchnorr( signature: ByteArray, hash: ByteArray,