diff --git a/pom.xml b/pom.xml index 7b014367..18330a6f 100644 --- a/pom.xml +++ b/pom.xml @@ -146,7 +146,7 @@ fr.acinq.bitcoin bitcoin-kmp-jvm - 0.30.0 + 0.31.0-SNAPSHOT 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 2c601435..020e2f48 100644 --- a/src/main/scala/fr/acinq/bitcoin/scalacompat/Musig2.scala +++ b/src/main/scala/fr/acinq/bitcoin/scalacompat/Musig2.scala @@ -60,6 +60,47 @@ object Musig2 { LocalNonce(SecretNonce(nonce.getFirst), IndividualNonce(nonce.getSecond.getData)) } + /** + * Create a partial musig2 signature for the given arbitrary message. + * + * @param privateKey private key of the signing participant. + * @param secretNonce secret nonce of the signing participant. + * @param msg message that should be signed. + * @param publicKeys public keys of all participants of the musig2 session: callers must verify that all public keys are valid. + * @param publicNonces public nonces of all participants of the musig2 session. + * @return a partial signature, or an error if the nonce has already been used or session creation/signing fails. + */ + def signMessage(privateKey: PrivateKey, secretNonce: SecretNonce, msg: ByteVector32, publicKeys: Seq[PublicKey], publicNonces: Seq[IndividualNonce]): Either[Throwable, ByteVector32] = { + musig2.Musig2.signMessage(privateKey, secretNonce.inner, msg, publicKeys.map(scala2kmp).asJava, publicNonces.map(n => new musig2.IndividualNonce(n.data.toArray)).asJava).map(kmp2scala) + } + + /** + * Verify a partial musig2 signature of an arbitrary message. + * + * @param partialSig partial musig2 signature. + * @param nonce public nonce matching the secret nonce used to generate the signature. + * @param publicKey public key for the private key used to generate the signature. + * @param msg message signed. + * @param publicKeys public keys of all participants of the musig2 session: callers must verify that all public keys are valid. + * @param publicNonces public nonces of all participants of the musig2 session. + * @return true if the partial signature is valid. + */ + def verifyPartialSignature(partialSig: ByteVector32, nonce: IndividualNonce, publicKey: PublicKey, msg: ByteVector32, publicKeys: Seq[PublicKey], publicNonces: Seq[IndividualNonce]): Boolean = { + musig2.Musig2.verify(partialSig, new musig2.IndividualNonce(nonce.data.toArray), publicKey, msg, publicKeys.map(scala2kmp).asJava, publicNonces.map(n => new musig2.IndividualNonce(n.data.toArray)).asJava) + } + + /** + * Aggregate partial musig2 signatures into a valid schnorr signature for the given arbitrary message. + * + * @param partialSigs partial musig2 signatures of all participants of the musig2 session. + * @param msg message signed. + * @param publicKeys public keys of all participants of the musig2 session: callers must verify that all public keys are valid. + * @param publicNonces public nonces of all participants of the musig2 session. + */ + def aggregatePartialSignatures(partialSigs: Seq[ByteVector32], msg: ByteVector32, publicKeys: Seq[PublicKey], publicNonces: Seq[IndividualNonce]): Either[Throwable, ByteVector64] = { + musig2.Musig2.aggregatePartialSignatures(partialSigs.map(scala2kmp).asJava, msg, publicKeys.map(scala2kmp).asJava, publicNonces.map(n => new musig2.IndividualNonce(n.data.toArray)).asJava).map(kmp2scala) + } + /** * Create a partial musig2 signature for the given taproot input key path. * diff --git a/src/main/scala/fr/acinq/bitcoin/scalacompat/Script.scala b/src/main/scala/fr/acinq/bitcoin/scalacompat/Script.scala index 41b08bc3..05e928db 100644 --- a/src/main/scala/fr/acinq/bitcoin/scalacompat/Script.scala +++ b/src/main/scala/fr/acinq/bitcoin/scalacompat/Script.scala @@ -173,18 +173,25 @@ object Script { /** * @param internalKey internal public key that will be tweaked with the [scripts] provided. - * @param scripts spending scripts that can be used instead of key-path spending. + * @param scripts spending scripts that can be used instead of key-path spending. */ def pay2tr(internalKey: XonlyPublicKey, scripts: ScriptTree): Seq[ScriptElt] = pay2tr(internalKey, scripts.hash()) /** - * @param internalKey internal public key that will be tweaked with the provided [taprootTweak]. + * @param internalKey internal public key that will be tweaked with the provided [taprootTweak]. * @param taprootTweak tweak to apply to [internalKey]. */ def pay2tr(internalKey: XonlyPublicKey, taprootTweak: Crypto.TaprootTweak): Seq[ScriptElt] = bitcoin.Script.pay2tr(internalKey.pub, taprootTweak).asScala.map(kmp2scala).toList def isPay2tr(script: Seq[ScriptElt]): Boolean = bitcoin.Script.isPay2tr(script.map(scala2kmp).asJava) + def pay2trOutputKey(script: Seq[ScriptElt]): Option[XonlyPublicKey] = bitcoin.Script.pay2trOutputKey(script.map(scala2kmp).asJava) match { + case null => None + case outputKey => Some(kmp2scala(outputKey)) + } + + def pay2trOutputKey(script: ByteVector): Option[XonlyPublicKey] = pay2trOutputKey(parse(script)) + /** NB: callers must ensure that they use the correct taproot tweak when generating their signature. */ def witnessKeyPathPay2tr(sig: ByteVector64, sighash: Int = bitcoin.SigHash.SIGHASH_DEFAULT): ScriptWitness = bitcoin.Script.witnessKeyPathPay2tr(sig, sighash) diff --git a/src/test/scala/fr/acinq/bitcoin/scalacompat/Musig2Spec.scala b/src/test/scala/fr/acinq/bitcoin/scalacompat/Musig2Spec.scala index ec7e4a9b..a46ef696 100644 --- a/src/test/scala/fr/acinq/bitcoin/scalacompat/Musig2Spec.scala +++ b/src/test/scala/fr/acinq/bitcoin/scalacompat/Musig2Spec.scala @@ -116,4 +116,31 @@ class Musig2Spec extends FunSuite { assert(nonce.publicNonce.data == hex"0271efb262c0535e921efacacd30146fa93f193689e4974d5348fa9d909d90000702a049680ef3f6acfb12320297df31d3a634214491cbeebacef5acdf13f8f61cc2") } + test("sign arbitrary messages with musig2") { + val priv1 = PrivateKey(ByteVector(Random.nextBytes(32))) + val priv2 = PrivateKey(ByteVector(Random.nextBytes(32))) + val priv3 = PrivateKey(ByteVector(Random.nextBytes(32))) + val msg = ByteVector32(ByteVector(Random.nextBytes(32))) + val publicKeys = Seq(priv1.publicKey, priv2.publicKey, priv3.publicKey) + val nonce1 = Musig2.generateNonce(ByteVector32(ByteVector(Random.nextBytes(32))), Left(priv1), publicKeys, None, None) + val nonce2 = Musig2.generateNonce(ByteVector32(ByteVector(Random.nextBytes(32))), Left(priv2), publicKeys, None, None) + val nonce3 = Musig2.generateNonce(ByteVector32(ByteVector(Random.nextBytes(32))), Left(priv3), publicKeys, None, None) + val publicNonces = Seq(nonce1, nonce2, nonce3).map(_.publicNonce) + val Some(sig1) = Musig2.signMessage(priv1, nonce1.secretNonce, msg, publicKeys, publicNonces).toOption + assert(Musig2.verifyPartialSignature(sig1, nonce1.publicNonce, priv1.publicKey, msg, publicKeys, publicNonces)) + val Some(sig2) = Musig2.signMessage(priv2, nonce2.secretNonce, msg, publicKeys, publicNonces).toOption + assert(Musig2.verifyPartialSignature(sig2, nonce2.publicNonce, priv2.publicKey, msg, publicKeys, publicNonces)) + val Some(sig3) = Musig2.signMessage(priv3, nonce3.secretNonce, msg, publicKeys, publicNonces).toOption + assert(Musig2.verifyPartialSignature(sig3, nonce3.publicNonce, priv3.publicKey, msg, publicKeys, publicNonces)) + assert(!Musig2.verifyPartialSignature(sig3, nonce2.publicNonce, priv3.publicKey, msg, publicKeys, publicNonces)) + // We can partially aggregate signatures, but it doesn't create a valid schnorr signature for the aggregated public key. + val Some(incompleteSig) = Musig2.aggregatePartialSignatures(Seq(sig1, sig2), msg, publicKeys, publicNonces).toOption + assert(!Crypto.verifySignatureSchnorr(msg, incompleteSig, Musig2.aggregateKeys(publicKeys))) + // Including redundant partial signatures doesn't yield a valid signature. + val Some(redundantSig) = Musig2.aggregatePartialSignatures(Seq(sig1, sig2, sig1), msg, publicKeys, publicNonces).toOption + assert(!Crypto.verifySignatureSchnorr(msg, redundantSig, Musig2.aggregateKeys(publicKeys))) + val Some(sig) = Musig2.aggregatePartialSignatures(Seq(sig1, sig2, sig3), msg, publicKeys, publicNonces).toOption + assert(Crypto.verifySignatureSchnorr(msg, sig, Musig2.aggregateKeys(publicKeys))) + } + } diff --git a/src/test/scala/fr/acinq/bitcoin/scalacompat/SegwitSpec.scala b/src/test/scala/fr/acinq/bitcoin/scalacompat/SegwitSpec.scala index 9110f20d..4b47a288 100644 --- a/src/test/scala/fr/acinq/bitcoin/scalacompat/SegwitSpec.scala +++ b/src/test/scala/fr/acinq/bitcoin/scalacompat/SegwitSpec.scala @@ -73,6 +73,7 @@ class SegwitSpec extends FunSuite { // this is a standard tx that sends 0.04 BTC to mp4eLFx7CpifAJxnvCZ3FmqKsh9dQmi5dA val tx1 = Transaction.read("02000000000101516508384a3e006340f1ea700eb3635330beed5d94c7b460b6b495eb1593d55c0100000023220020a5fdf5b5f2c592362b78a50997821964b39dd90476c6e1f3e97e79acb134ca3bfdffffff0200093d00000000001976a9145dbf52b8d7af4fb5f9b75b808f0a8284493531b388aca005071d0000000017a914d77e5f7ca4d9f05dc4f25dc0aa1391f0e901bdfc87040047304402207bfb18327be173512f38bd4120b8f02545321ecc6105a852cbc25b1de687ba570220705a1225d8a8e0fbd4b35f3bc38a2840706f8524e8dc6f0151746aeff14033ce014730440220486925fb0495442e4ccb1b711692af7057d4db24f8775b5dfa3f8c74992081f102203beae7d96423e0c66b7b5f8919a5f3ad89a42dc4303f37201e4e596909478357014752210245119449d07c16992c148e3b33f1395ee05c936fc510d9fae83417f8e1901f922103eb03f67b56c88bccff90b76182c08556eac9ebc5a0efee8669bef69ae6d4ea5752ae75bb2300", pversion) + assert(Script.pay2trOutputKey(tx1.txOut.head.publicKeyScript).isEmpty) // now let's create a simple tx that spends tx1 and send 0.039 BTC to P2WPK output val tx2 = { @@ -86,6 +87,7 @@ class SegwitSpec extends FunSuite { } Transaction.correctlySpends(tx2, Seq(tx1), ScriptFlags.STANDARD_SCRIPT_VERIFY_FLAGS) assert(tx2.txid == TxId.fromValidHex("f25b3fecc9652466926237d96e4bc7ee2c984051fe48e61417aba218af5570c3")) + assert(Script.pay2trOutputKey(tx2.txOut.head.publicKeyScript).isEmpty) // this tx was published on testnet as f25b3fecc9652466926237d96e4bc7ee2c984051fe48e61417aba218af5570c3 // and now we create a testnet tx that spends the P2WPK output @@ -123,6 +125,7 @@ class SegwitSpec extends FunSuite { // this is a standard tx that sends 0.05 BTC to mkNdbutRYE3me7wvbwvvJ8XQwbzi56sneZ val tx1 = Transaction.read("020000000001016ecc08b535a0c774234419dee508867ace1535a0d256d6b2aa19942441777336000000002322002073bb471aa121fbdd95942eabb5e665d66e71542e6e075c8392cd0df72a075b72fdffffff02803823030000000017a914d3c15be7951c9de644bdf9e22dcbcb77550c4ae487404b4c00000000001976a9143545b2a6659dbe5bdf841d1158135be184d81d3688ac0400473044022041cac92405e4e3215c2f9c27a67ff0792c8fb76e4182023fed081f541f4563e002203bd04d4d810ef8074aeb26a19e01e1ee1a40ad83e4d0ac2c614b8cb22825d2ae0147304402204c947b46ea480419c04098a56a5219bb1f491b07e12926fb6f304132a1f1e29e022078cc9f004c74d6c3c2b2dfcca6385d2fabe44d4eadb027a0d764e1ab9d7f09190147522102be608bf8904326b4d0ec9346aa348773fe51ee70338849acd2dd710b73bf611a2103627c19e40f67c5ee8b44df85ee911b7e978869fa5a3de1d972a461f47ea349e452ae90bb2300", pversion) + assert(Script.pay2trOutputKey(tx1.txOut.head.publicKeyScript).isEmpty) // now let's create a simple tx that spends tx1 and send 0.5 BTC to a P2WSH output val tx2 = { @@ -139,6 +142,7 @@ class SegwitSpec extends FunSuite { } Transaction.correctlySpends(tx2, Seq(tx1), ScriptFlags.STANDARD_SCRIPT_VERIFY_FLAGS) assert(tx2.txid == TxId.fromValidHex("2f8360a06a31ca642d717b1857aa86b3306fc554fa9c437d88b4bc61b7f2b3e9")) + assert(Script.pay2trOutputKey(tx2.txOut.head.publicKeyScript).isEmpty) // this tx was published on testnet as 2f8360a06a31ca642d717b1857aa86b3306fc554fa9c437d88b4bc61b7f2b3e9 // and now we create a testnet tx that spends the P2WSH output diff --git a/src/test/scala/fr/acinq/bitcoin/scalacompat/TaprootSpec.scala b/src/test/scala/fr/acinq/bitcoin/scalacompat/TaprootSpec.scala index 79d6e3a2..682ca838 100644 --- a/src/test/scala/fr/acinq/bitcoin/scalacompat/TaprootSpec.scala +++ b/src/test/scala/fr/acinq/bitcoin/scalacompat/TaprootSpec.scala @@ -26,6 +26,7 @@ class TaprootSpec extends FunSuite { ) assert(Script.isPay2tr(Script.parse(tx.txOut.head.publicKeyScript))) assert(script == Script.parse(tx.txOut.head.publicKeyScript)) + assert(Script.pay2trOutputKey(script).contains(outputKey)) // tx1 spends tx using key path spending i.e its witness just includes a single signature that is valid for outputKey val tx1 = Transaction.read( @@ -67,6 +68,7 @@ class TaprootSpec extends FunSuite { "02000000000101bf77ef36f2c0f32e0822cef0514948254997495a34bfba7dd4a73aabfcbb87900000000000fdffffff02c2c2000000000000160014b5c3dbfeb8e7d0c809c3ba3f815fd430777ef4be50c30000000000002251208c5db7f797196d6edc4dd7df6048f4ea6b883a6af6af032342088f436543790f0140583f758bea307216e03c1f54c3c6088e8923c8e1c89d96679fb00de9e808a79d0fba1cc3f9521cb686e8f43fb37cc6429f2e1480c70cc25ecb4ac0dde8921a01f1f70000" ) assert(Script.pay2tr(internalKey, KeyPathTweak) == Script.parse(tx.txOut(1).publicKeyScript)) + assert(Script.pay2trOutputKey(tx.txOut(1).publicKeyScript).contains(outputKey)) // we want to spend val Right(outputScript) = addressToPublicKeyScript(Block.Testnet3GenesisBlock.hash, "tb1pn3g330w4n5eut7d4vxq0pp303267qc6vg8d2e0ctjuqre06gs3yqnc5yx0")