-
Notifications
You must be signed in to change notification settings - Fork 50
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Add high-level helpers for using Musig2 with Taproot
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
Showing
2 changed files
with
168 additions
and
0 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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
108
src/test/scala/fr/acinq/bitcoin/scalacompat/Musig2Spec.scala
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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) | ||
} | ||
} | ||
|
||
} |