From 8220451a46365efa5b6ba71d6e5ba81fe68003ca Mon Sep 17 00:00:00 2001 From: sstone Date: Wed, 4 Jun 2025 11:35:35 +0200 Subject: [PATCH 1/2] Update to bitcoin-kmp 0.24.0 and export counter-based nonce generation method --- pom.xml | 2 +- .../fr/acinq/bitcoin/scalacompat/Musig2.scala | 21 ++++++++++++++++--- .../bitcoin/scalacompat/Musig2Spec.scala | 14 +++++++++---- 3 files changed, 29 insertions(+), 8 deletions(-) diff --git a/pom.xml b/pom.xml index 4d0cdbb9..0ff8465a 100644 --- a/pom.xml +++ b/pom.xml @@ -177,7 +177,7 @@ fr.acinq.bitcoin bitcoin-kmp-jvm - 0.23.0 + 0.24.0 fr.acinq.secp256k1 diff --git a/src/main/scala/fr/acinq/bitcoin/scalacompat/Musig2.scala b/src/main/scala/fr/acinq/bitcoin/scalacompat/Musig2.scala index dc9578b4..715492e5 100644 --- a/src/main/scala/fr/acinq/bitcoin/scalacompat/Musig2.scala +++ b/src/main/scala/fr/acinq/bitcoin/scalacompat/Musig2.scala @@ -20,11 +20,26 @@ object Musig2 { /** * @param sessionId a random, unique session ID. - * @param privateKey signer's private key. + * @param privateKey signer's private key + * @param publicKey signer's public key * @param publicKeys public keys of all participants: callers must verify that all public keys are valid. + * @param message (optional) message that will be signed, if already known. + * @param extraInput (optional) additional random data. */ - def generateNonce(sessionId: ByteVector32, privateKey: PrivateKey, publicKeys: Seq[PublicKey]): (SecretNonce, IndividualNonce) = { - val nonce = fr.acinq.bitcoin.crypto.musig2.Musig2.generateNonce(sessionId, privateKey, publicKeys.map(scala2kmp).asJava) + def generateNonce(sessionId: ByteVector32, privateKey: Option[PrivateKey], publicKey: PublicKey, publicKeys: Seq[PublicKey], message: Option[ByteVector32], extraInput: Option[ByteVector32]): (SecretNonce, IndividualNonce) = { + val nonce = fr.acinq.bitcoin.crypto.musig2.Musig2.generateNonce(sessionId, privateKey.map(scala2kmp).orNull, publicKey, publicKeys.map(scala2kmp).asJava, message.map(scala2kmp).orNull, extraInput.map(scala2kmp).orNull) + (nonce.getFirst, nonce.getSecond) + } + + /** + * @param nonRepeatingCounter non-repeating counter that must never be reused with the same private key. + * @param privateKey signer's private key. + * @param publicKeys public keys of all participants: callers must verify that all public keys are valid. + * @param message (optional) message that will be signed, if already known. + * @param extraInput (optional) additional random data. + */ + def generateNonceWithCounter(nonRepeatingCounter: Long, privateKey: PrivateKey, publicKeys: Seq[PublicKey], message: Option[ByteVector32], extraInput: Option[ByteVector32]): (SecretNonce, IndividualNonce) = { + val nonce = fr.acinq.bitcoin.crypto.musig2.Musig2.generateNonceWithCounter(nonRepeatingCounter, privateKey, publicKeys.map(scala2kmp).asJava, message.map(scala2kmp).orNull, extraInput.map(scala2kmp).orNull) (nonce.getFirst, nonce.getSecond) } diff --git a/src/test/scala/fr/acinq/bitcoin/scalacompat/Musig2Spec.scala b/src/test/scala/fr/acinq/bitcoin/scalacompat/Musig2Spec.scala index 278eb0cd..9c62af85 100644 --- a/src/test/scala/fr/acinq/bitcoin/scalacompat/Musig2Spec.scala +++ b/src/test/scala/fr/acinq/bitcoin/scalacompat/Musig2Spec.scala @@ -2,6 +2,7 @@ package fr.acinq.bitcoin.scalacompat import fr.acinq.bitcoin.scalacompat.Crypto.PrivateKey import fr.acinq.bitcoin.{ScriptFlags, ScriptTree, SigHash} +import fr.acinq.secp256k1.Hex import org.scalatest.FunSuite import scodec.bits.{ByteVector, HexStringSyntax} @@ -26,8 +27,8 @@ class Musig2Spec extends FunSuite { // The first step of a musig2 signing session is to exchange nonces. // If participants are disconnected before the end of the signing session, they must start again with fresh nonces. - val (aliceSecretNonce, alicePublicNonce) = Musig2.generateNonce(ByteVector32(ByteVector(Random.nextBytes(32))), alicePrivKey, Seq(alicePubKey, bobPubKey)) - val (bobSecretNonce, bobPublicNonce) = Musig2.generateNonce(ByteVector32(ByteVector(Random.nextBytes(32))), bobPrivKey, Seq(alicePubKey, bobPubKey)) + val (aliceSecretNonce, alicePublicNonce) = Musig2.generateNonce(ByteVector32(ByteVector(Random.nextBytes(32))), Some(alicePrivKey), alicePrivKey.publicKey, Seq(alicePubKey, bobPubKey), None, None) + val (bobSecretNonce, bobPublicNonce) = Musig2.generateNonce(ByteVector32(ByteVector(Random.nextBytes(32))), Some(bobPrivKey), bobPrivKey.publicKey, Seq(alicePubKey, bobPubKey), None, None) // Once they have each other's public nonce, they can produce partial signatures. val publicNonces = Seq(alicePublicNonce, bobPublicNonce) @@ -79,8 +80,8 @@ class Musig2Spec extends FunSuite { ) // The first step of a musig2 signing session is to exchange nonces. // If participants are disconnected before the end of the signing session, they must start again with fresh nonces. - val (userSecretNonce, userPublicNonce) = Musig2.generateNonce(ByteVector32(ByteVector(Random.nextBytes(32))), userPrivateKey, Seq(userPublicKey, serverPublicKey)) - val (serverSecretNonce, serverPublicNonce) = Musig2.generateNonce(ByteVector32(ByteVector(Random.nextBytes(32))), serverPrivateKey, Seq(userPublicKey, serverPublicKey)) + val (userSecretNonce, userPublicNonce) = Musig2.generateNonce(ByteVector32(ByteVector(Random.nextBytes(32))), Some(userPrivateKey), userPrivateKey.publicKey, Seq(userPublicKey, serverPublicKey), None, None) + val (serverSecretNonce, serverPublicNonce) = Musig2.generateNonce(ByteVector32(ByteVector(Random.nextBytes(32))), Some(serverPrivateKey), serverPrivateKey.publicKey, Seq(userPublicKey, serverPublicKey), None, None) // Once they have each other's public nonce, they can produce partial signatures. val publicNonces = Seq(userPublicNonce, serverPublicNonce) @@ -110,4 +111,9 @@ class Musig2Spec extends FunSuite { } } + test("generate nonce with counter") { + val sk = PrivateKey(ByteVector.fromValidHex("EEC1CB7D1B7254C5CAB0D9C61AB02E643D464A59FE6C96A7EFE871F07C5AEF54")) + val (_, pubnonce) = Musig2.generateNonceWithCounter(0, sk, Seq(sk.publicKey), None, None) + assert(pubnonce.getData.contentEquals(Hex.decode("0271efb262c0535e921efacacd30146fa93f193689e4974d5348fa9d909d90000702a049680ef3f6acfb12320297df31d3a634214491cbeebacef5acdf13f8f61cc2"))) + } } From bd9fefc06f0208fa15a54c42fef5209b5dd9dff4 Mon Sep 17 00:00:00 2001 From: sstone Date: Wed, 4 Jun 2025 12:02:35 +0200 Subject: [PATCH 2/2] Fixup: make it explicit that input is either a private or public key --- .../fr/acinq/bitcoin/scalacompat/Musig2.scala | 23 +++++++++++-------- .../bitcoin/scalacompat/Musig2Spec.scala | 8 +++---- 2 files changed, 17 insertions(+), 14 deletions(-) diff --git a/src/main/scala/fr/acinq/bitcoin/scalacompat/Musig2.scala b/src/main/scala/fr/acinq/bitcoin/scalacompat/Musig2.scala index 715492e5..3f7ffb59 100644 --- a/src/main/scala/fr/acinq/bitcoin/scalacompat/Musig2.scala +++ b/src/main/scala/fr/acinq/bitcoin/scalacompat/Musig2.scala @@ -20,14 +20,17 @@ object Musig2 { /** * @param sessionId a random, unique session ID. - * @param privateKey signer's private key - * @param publicKey signer's public key + * @param signingKey either the signer's private key or public key * @param publicKeys public keys of all participants: callers must verify that all public keys are valid. - * @param message (optional) message that will be signed, if already known. - * @param extraInput (optional) additional random data. + * @param message_opt (optional) message that will be signed, if already known. + * @param extraInput_opt (optional) additional random data. */ - def generateNonce(sessionId: ByteVector32, privateKey: Option[PrivateKey], publicKey: PublicKey, publicKeys: Seq[PublicKey], message: Option[ByteVector32], extraInput: Option[ByteVector32]): (SecretNonce, IndividualNonce) = { - val nonce = fr.acinq.bitcoin.crypto.musig2.Musig2.generateNonce(sessionId, privateKey.map(scala2kmp).orNull, publicKey, publicKeys.map(scala2kmp).asJava, message.map(scala2kmp).orNull, extraInput.map(scala2kmp).orNull) + def generateNonce(sessionId: ByteVector32, signingKey: Either[PrivateKey, PublicKey], publicKeys: Seq[PublicKey], message_opt: Option[ByteVector32], extraInput_opt: Option[ByteVector32]): (SecretNonce, IndividualNonce) = { + val (privateKey, publicKey) = signingKey match { + case Left(priv) => (scala2kmp(priv), priv.publicKey) + case Right(pub) => (null, pub) + } + val nonce = fr.acinq.bitcoin.crypto.musig2.Musig2.generateNonce(sessionId, privateKey, publicKey, publicKeys.map(scala2kmp).asJava, message_opt.map(scala2kmp).orNull, extraInput_opt.map(scala2kmp).orNull) (nonce.getFirst, nonce.getSecond) } @@ -35,11 +38,11 @@ object Musig2 { * @param nonRepeatingCounter non-repeating counter that must never be reused with the same private key. * @param privateKey signer's private key. * @param publicKeys public keys of all participants: callers must verify that all public keys are valid. - * @param message (optional) message that will be signed, if already known. - * @param extraInput (optional) additional random data. + * @param message_opt (optional) message that will be signed, if already known. + * @param extraInput_opt (optional) additional random data. */ - def generateNonceWithCounter(nonRepeatingCounter: Long, privateKey: PrivateKey, publicKeys: Seq[PublicKey], message: Option[ByteVector32], extraInput: Option[ByteVector32]): (SecretNonce, IndividualNonce) = { - val nonce = fr.acinq.bitcoin.crypto.musig2.Musig2.generateNonceWithCounter(nonRepeatingCounter, privateKey, publicKeys.map(scala2kmp).asJava, message.map(scala2kmp).orNull, extraInput.map(scala2kmp).orNull) + def generateNonceWithCounter(nonRepeatingCounter: Long, privateKey: PrivateKey, publicKeys: Seq[PublicKey], message_opt: Option[ByteVector32], extraInput_opt: Option[ByteVector32]): (SecretNonce, IndividualNonce) = { + val nonce = fr.acinq.bitcoin.crypto.musig2.Musig2.generateNonceWithCounter(nonRepeatingCounter, privateKey, publicKeys.map(scala2kmp).asJava, message_opt.map(scala2kmp).orNull, extraInput_opt.map(scala2kmp).orNull) (nonce.getFirst, nonce.getSecond) } diff --git a/src/test/scala/fr/acinq/bitcoin/scalacompat/Musig2Spec.scala b/src/test/scala/fr/acinq/bitcoin/scalacompat/Musig2Spec.scala index 9c62af85..9311bd19 100644 --- a/src/test/scala/fr/acinq/bitcoin/scalacompat/Musig2Spec.scala +++ b/src/test/scala/fr/acinq/bitcoin/scalacompat/Musig2Spec.scala @@ -27,8 +27,8 @@ class Musig2Spec extends FunSuite { // The first step of a musig2 signing session is to exchange nonces. // If participants are disconnected before the end of the signing session, they must start again with fresh nonces. - val (aliceSecretNonce, alicePublicNonce) = Musig2.generateNonce(ByteVector32(ByteVector(Random.nextBytes(32))), Some(alicePrivKey), alicePrivKey.publicKey, Seq(alicePubKey, bobPubKey), None, None) - val (bobSecretNonce, bobPublicNonce) = Musig2.generateNonce(ByteVector32(ByteVector(Random.nextBytes(32))), Some(bobPrivKey), bobPrivKey.publicKey, Seq(alicePubKey, bobPubKey), None, None) + val (aliceSecretNonce, alicePublicNonce) = Musig2.generateNonce(ByteVector32(ByteVector(Random.nextBytes(32))), Left(alicePrivKey), Seq(alicePubKey, bobPubKey), None, None) + val (bobSecretNonce, bobPublicNonce) = Musig2.generateNonce(ByteVector32(ByteVector(Random.nextBytes(32))), Right(bobPrivKey.publicKey), Seq(alicePubKey, bobPubKey), None, None) // Once they have each other's public nonce, they can produce partial signatures. val publicNonces = Seq(alicePublicNonce, bobPublicNonce) @@ -80,8 +80,8 @@ class Musig2Spec extends FunSuite { ) // The first step of a musig2 signing session is to exchange nonces. // If participants are disconnected before the end of the signing session, they must start again with fresh nonces. - val (userSecretNonce, userPublicNonce) = Musig2.generateNonce(ByteVector32(ByteVector(Random.nextBytes(32))), Some(userPrivateKey), userPrivateKey.publicKey, Seq(userPublicKey, serverPublicKey), None, None) - val (serverSecretNonce, serverPublicNonce) = Musig2.generateNonce(ByteVector32(ByteVector(Random.nextBytes(32))), Some(serverPrivateKey), serverPrivateKey.publicKey, Seq(userPublicKey, serverPublicKey), None, None) + val (userSecretNonce, userPublicNonce) = Musig2.generateNonce(ByteVector32(ByteVector(Random.nextBytes(32))), Left(userPrivateKey), Seq(userPublicKey, serverPublicKey), None, None) + val (serverSecretNonce, serverPublicNonce) = Musig2.generateNonce(ByteVector32(ByteVector(Random.nextBytes(32))), Right(serverPrivateKey.publicKey), Seq(userPublicKey, serverPublicKey), None, None) // Once they have each other's public nonce, they can produce partial signatures. val publicNonces = Seq(userPublicNonce, serverPublicNonce)