Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion pom.xml
Original file line number Diff line number Diff line change
Expand Up @@ -146,7 +146,7 @@
<dependency>
<groupId>fr.acinq.bitcoin</groupId>
<artifactId>bitcoin-kmp-jvm</artifactId>
<version>0.30.0</version>
<version>0.31.0-SNAPSHOT</version>
</dependency>
<dependency>
<groupId>fr.acinq.secp256k1</groupId>
Expand Down
41 changes: 41 additions & 0 deletions src/main/scala/fr/acinq/bitcoin/scalacompat/Musig2.scala
Original file line number Diff line number Diff line change
Expand Up @@ -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.
*
Expand Down
11 changes: 9 additions & 2 deletions src/main/scala/fr/acinq/bitcoin/scalacompat/Script.scala
Original file line number Diff line number Diff line change
Expand Up @@ -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)

Expand Down
27 changes: 27 additions & 0 deletions src/test/scala/fr/acinq/bitcoin/scalacompat/Musig2Spec.scala
Original file line number Diff line number Diff line change
Expand Up @@ -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)))
}

}
4 changes: 4 additions & 0 deletions src/test/scala/fr/acinq/bitcoin/scalacompat/SegwitSpec.scala
Original file line number Diff line number Diff line change
Expand Up @@ -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 = {
Expand All @@ -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
Expand Down Expand Up @@ -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 = {
Expand All @@ -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
Expand Down
2 changes: 2 additions & 0 deletions src/test/scala/fr/acinq/bitcoin/scalacompat/TaprootSpec.scala
Original file line number Diff line number Diff line change
Expand Up @@ -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(
Expand Down Expand Up @@ -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")
Expand Down
Loading