Skip to content

Commit

Permalink
Add high-level helpers for using Musig2 with Taproot
Browse files Browse the repository at this point in the history
When using Musig2 for a taproot key path, we can provide simpler helper
functions to collaboratively build a shared signature for the spending
transaction.

This hides all of the low-level details of how the musig2 algorithm
works, by exposing a subset of what can be done that is sufficient for
spending taproot inputs.
  • Loading branch information
t-bast committed Jan 17, 2024
1 parent 603efa4 commit 7926ccc
Show file tree
Hide file tree
Showing 2 changed files with 168 additions and 0 deletions.
60 changes: 60 additions & 0 deletions src/main/scala/fr/acinq/bitcoin/scalacompat/Musig2.scala
Original file line number Diff line number Diff line change
@@ -0,0 +1,60 @@
package fr.acinq.bitcoin.scalacompat

import fr.acinq.bitcoin.ScriptTree
import fr.acinq.bitcoin.musig2.{IndividualNonce, SecretNonce}
import fr.acinq.bitcoin.scalacompat.Crypto.{PrivateKey, PublicKey, XonlyPublicKey}
import fr.acinq.bitcoin.scalacompat.KotlinUtils._
import scodec.bits.ByteVector

import scala.jdk.CollectionConverters.SeqHasAsJava
import scala.util.Random

object Musig2 {

/** Aggregate the public keys of a musig2 session into a single public key. */
def aggregateKeys(pubkeys: Seq[PublicKey]): PublicKey = fr.acinq.bitcoin.musig2.Musig2.keyAgg(pubkeys.map(scala2kmp).asJava)

/**
* @param aggregatePublicKey aggregate public key of all participants of the musig2 session.
*/
def generateNonce(privateKey: PrivateKey, aggregatePublicKey: XonlyPublicKey): SecretNonce = {
fr.acinq.bitcoin.musig2.SecretNonce.generate(privateKey, aggregatePublicKey.pub, ByteVector32(ByteVector(Random.nextBytes(32))))
}

/**
* @param pubkeys public keys of all participants of the musig2 session.
*/
def generateNonce(privateKey: PrivateKey, pubkeys: Seq[PublicKey]): SecretNonce = generateNonce(privateKey, aggregateKeys(pubkeys).xOnly)

/**
* Create a partial musig2 signature for the given taproot input key path.
*
* @param privateKey private key of the signing participant.
* @param tx transaction spending the target taproot input.
* @param inputIndex index of the taproot input to spend.
* @param inputs all inputs of the spending transaction.
* @param pubkeys public keys of all participants of the musig2 session.
* @param secretNonce secret nonce of the signing participant.
* @param publicNonces public nonces of all participants of the musig2 session.
* @param scriptTree_opt tapscript tree of the taproot input, if it has script paths.
*/
def signTaprootInput(privateKey: PrivateKey, tx: Transaction, inputIndex: Int, inputs: Seq[TxOut], pubkeys: Seq[PublicKey], secretNonce: SecretNonce, publicNonces: Seq[IndividualNonce], scriptTree_opt: Option[ScriptTree]): Option[ByteVector32] = {
Option(fr.acinq.bitcoin.musig2.Musig2.signTaprootInput(privateKey, tx, inputIndex, inputs.map(scala2kmp).asJava, pubkeys.map(scala2kmp).asJava, secretNonce, publicNonces.asJava, scriptTree_opt.orNull))
}

/**
* Aggregate partial musig2 signatures into a valid schnorr signature for the given taproot input key path.
*
* @param partialSigs partial musig2 signatures of all participants of the musig2 session.
* @param tx transaction spending the target taproot input.
* @param inputIndex index of the taproot input to spend.
* @param inputs all inputs of the spending transaction.
* @param pubkeys public keys of all participants of the musig2 session.
* @param publicNonces public nonces of all participants of the musig2 session.
* @param scriptTree_opt tapscript tree of the taproot input, if it has script paths.
*/
def aggregateTaprootSignatures(partialSigs: Seq[ByteVector32], tx: Transaction, inputIndex: Int, inputs: Seq[TxOut], pubkeys: Seq[PublicKey], publicNonces: Seq[IndividualNonce], scriptTree_opt: Option[ScriptTree]): Option[ByteVector64] = {
Option(fr.acinq.bitcoin.musig2.Musig2.aggregateTaprootSignatures(partialSigs.map(scala2kmp).asJava, tx, inputIndex, inputs.map(scala2kmp).asJava, pubkeys.map(scala2kmp).asJava, publicNonces.asJava, scriptTree_opt.orNull))
}

}
108 changes: 108 additions & 0 deletions src/test/scala/fr/acinq/bitcoin/scalacompat/Musig2Spec.scala
Original file line number Diff line number Diff line change
@@ -0,0 +1,108 @@
package fr.acinq.bitcoin.scalacompat

import fr.acinq.bitcoin.scalacompat.Crypto.PrivateKey
import fr.acinq.bitcoin.{ScriptFlags, ScriptTree, SigHash}
import org.scalatest.FunSuite
import scodec.bits.{ByteVector, HexStringSyntax}

import scala.jdk.CollectionConverters.SeqHasAsJava

class Musig2Spec extends FunSuite {

test("use musig2 to replace multisig 2-of-2") {
val alicePrivKey = PrivateKey(hex"0101010101010101010101010101010101010101010101010101010101010101")
val alicePubKey = alicePrivKey.publicKey
val bobPrivKey = PrivateKey(hex"0202020202020202020202020202020202020202020202020202020202020202")
val bobPubKey = bobPrivKey.publicKey

// Alice and Bob exchange public keys and agree on a common aggregated key.
val aggregatedKey = Musig2.aggregateKeys(Seq(alicePubKey, bobPubKey)).xOnly
// This tx sends to a taproot script that doesn't contain any script path.
val tx = Transaction(2, Nil, Seq(TxOut(10_000 sat, Script.pay2tr(aggregatedKey, scripts_opt = None))), 0)

// this is how Alice and Bob would spend that tx
val spendingTx = Transaction(2, Seq(TxIn(OutPoint(tx, 0), ByteVector.empty, 0)), Seq(TxOut(10_000 sat, Script.pay2wpkh(alicePubKey))), 0)
val sig = {
// 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 aliceNonce = Musig2.generateNonce(alicePrivKey, aggregatedKey)
val bobNonce = Musig2.generateNonce(bobPrivKey, Seq(alicePubKey, bobPubKey))

// Once they have each other's public nonce, they can produce partial signatures.
val publicNonces = Seq(aliceNonce.publicNonce(), bobNonce.publicNonce())
val aliceSig = Musig2.signTaprootInput(alicePrivKey, spendingTx, 0, tx.txOut, Seq(alicePubKey, bobPubKey), aliceNonce, publicNonces, scriptTree_opt = None).get
val bobSig = Musig2.signTaprootInput(bobPrivKey, spendingTx, 0, tx.txOut, Seq(alicePubKey, bobPubKey), bobNonce, publicNonces, scriptTree_opt = None).get

// Once they have each other's partial signature, they can aggregate them into a valid signature.
Musig2.aggregateTaprootSignatures(Seq(aliceSig, bobSig), spendingTx, 0, tx.txOut, Seq(alicePubKey, bobPubKey), publicNonces, scriptTree_opt = None).get
}

// This tx looks like any other tx that spends a p2tr output, with a single signature.
val signedSpendingTx = spendingTx.updateWitness(0, Script.witnessKeyPathPay2tr(sig))
Transaction.correctlySpends(signedSpendingTx, Seq(tx), ScriptFlags.STANDARD_SCRIPT_VERIFY_FLAGS)
}

test("swap-in-potentiam example with musig2 and taproot") {
val userPrivateKey = PrivateKey(hex"0101010101010101010101010101010101010101010101010101010101010101")
val userPublicKey = userPrivateKey.publicKey
val serverPrivateKey = PrivateKey(hex"0202020202020202020202020202020202020202020202020202020202020202")
val serverPublicKey = serverPrivateKey.publicKey
val userRefundPrivateKey = PrivateKey(hex"0303030303030303030303030303030303030303030303030303030303030303")
val refundDelay = 25920

// The redeem script is just the refund script. it is generated from this policy: and_v(v:pk(user),older(refundDelay)).
// It does not depend upon the user's or server's key, just the user's refund key and the refund delay.
val redeemScript = Seq(OP_PUSHDATA(userRefundPrivateKey.xOnlyPublicKey()), OP_CHECKSIGVERIFY, OP_PUSHDATA(Script.encodeNumber(refundDelay)), OP_CHECKSEQUENCEVERIFY)
val scriptTree = new ScriptTree.Leaf(0, redeemScript.map(KotlinUtils.scala2kmp).asJava)

// The internal pubkey is the musig2 aggregation of the user's and server's public keys: it does not depend upon the user's refund's key.
val aggregatedKey = Musig2.aggregateKeys(Seq(userPublicKey, serverPublicKey)).xOnly
val pubkeyScript = Script.pay2tr(aggregatedKey, Some(scriptTree))

val swapInTx = Transaction(
version = 2,
txIn = Nil,
txOut = Seq(TxOut(10_000 sat, pubkeyScript)),
lockTime = 0
)

// The transaction can be spent if the user and the server produce a signature.
{
val tx = Transaction(
version = 2,
txIn = Seq(TxIn(OutPoint(swapInTx, 0), ByteVector.empty, 0xFFFFFFFD)),
txOut = Seq(TxOut(10_000 sat, Script.pay2wpkh(userPublicKey))),
lockTime = 0
)
// 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 userNonce = Musig2.generateNonce(userPrivateKey, Seq(userPublicKey, serverPublicKey))
val serverNonce = Musig2.generateNonce(serverPrivateKey, Seq(userPublicKey, serverPublicKey))

// Once they have each other's public nonce, they can produce partial signatures.
val publicNonces = Seq(userNonce.publicNonce(), serverNonce.publicNonce())
val userSig = Musig2.signTaprootInput(userPrivateKey, tx, 0, swapInTx.txOut, Seq(userPublicKey, serverPublicKey), userNonce, publicNonces, Some(scriptTree)).get
val serverSig = Musig2.signTaprootInput(serverPrivateKey, tx, 0, swapInTx.txOut, Seq(userPublicKey, serverPublicKey), serverNonce, publicNonces, Some(scriptTree)).get

// Once they have each other's partial signature, they can aggregate them into a valid signature.
val sig = Musig2.aggregateTaprootSignatures(Seq(userSig, serverSig), tx, 0, swapInTx.txOut, Seq(userPublicKey, serverPublicKey), publicNonces, Some(scriptTree)).get
val signedTx = tx.updateWitness(0, Script.witnessKeyPathPay2tr(sig))
Transaction.correctlySpends(signedTx, Seq(swapInTx), ScriptFlags.STANDARD_SCRIPT_VERIFY_FLAGS)
}

// Or it can be spent with only the user's signature, after a delay.
{
val tx = Transaction(
version = 2,
txIn = Seq(TxIn(OutPoint(swapInTx, 0), ByteVector.empty, refundDelay)),
txOut = Seq(TxOut(10_000 sat, Script.pay2wpkh(userPublicKey))),
lockTime = 0
)
val sig = Crypto.signTaprootScriptPath(userRefundPrivateKey, tx, 0, swapInTx.txOut, SigHash.SIGHASH_DEFAULT, KotlinUtils.kmp2scala(scriptTree.hash()))
val witness = Script.witnessScriptPathPay2tr(aggregatedKey, scriptTree, ScriptWitness(Seq(sig)), scriptTree)
val signedTx = tx.updateWitness(0, witness)
Transaction.correctlySpends(signedTx, Seq(swapInTx), ScriptFlags.STANDARD_SCRIPT_VERIFY_FLAGS)
}
}

}

0 comments on commit 7926ccc

Please sign in to comment.