Skip to content

Commit

Permalink
Add an example of swapin transaction that uses musig2 and taproot
Browse files Browse the repository at this point in the history
We add a simple test that uses how to modify the swap-in-potentiam protocol to use musig2 and taproot:
- taproot key path is used for the mutual user key + server key use case, which sends to a single musig2 aggregated key
- tapscript path is used for the refund case (user key + delay)
  • Loading branch information
sstone committed Oct 23, 2023
1 parent 648b7bb commit ff25e72
Show file tree
Hide file tree
Showing 2 changed files with 79 additions and 1 deletion.
2 changes: 1 addition & 1 deletion build.gradle.kts
Original file line number Diff line number Diff line change
Expand Up @@ -33,7 +33,7 @@ kotlin {

val commonMain by sourceSets.getting {
dependencies {
api("fr.acinq.bitcoin:bitcoin-kmp:0.14.0") // when upgrading, keep secp256k1-kmp-jni-jvm in sync below
api("fr.acinq.bitcoin:bitcoin-kmp:0.15.0-MUSIG2-SNAPSHOT") // when upgrading, keep secp256k1-kmp-jni-jvm in sync below
api("org.kodein.log:canard:0.18.0")
api("org.jetbrains.kotlinx:kotlinx-coroutines-core:$coroutineVersion")
api("org.jetbrains.kotlinx:kotlinx-serialization-core:$serializationVersion")
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,10 @@ import fr.acinq.bitcoin.Script.pay2wpkh
import fr.acinq.bitcoin.Script.pay2wsh
import fr.acinq.bitcoin.Script.write
import fr.acinq.bitcoin.crypto.Pack
import fr.acinq.bitcoin.musig2.Musig2
import fr.acinq.bitcoin.musig2.PublicNonce
import fr.acinq.bitcoin.musig2.SecretNonce
import fr.acinq.bitcoin.musig2.SessionCtx
import fr.acinq.lightning.CltvExpiry
import fr.acinq.lightning.CltvExpiryDelta
import fr.acinq.lightning.Lightning.randomBytes
Expand Down Expand Up @@ -480,6 +484,80 @@ class TransactionsTestsCommon : LightningTestSuite() {
}
}

@Test
fun `spend 2-of-2 swap-in taproot-musig2 version`() {
val userPrivateKey = PrivateKey(ByteArray(32) { 1 })
val serverPrivateKey = PrivateKey(ByteArray(32) { 1 })

// the redeem script is just the refund script
val refundDelay = 144
val redeemScript = listOf(OP_PUSHDATA(userPrivateKey.publicKey().xOnly()), OP_CHECKSIGVERIFY, OP_PUSHDATA(Script.encodeNumber(refundDelay)), OP_CHECKSEQUENCEVERIFY)
val scriptTree = ScriptTree.Leaf(ScriptLeaf(0, write(redeemScript).byteVector(), Script.TAPROOT_LEAF_TAPSCRIPT))
val merkleRoot = ScriptTree.hash(scriptTree)

// User and Server exchange public keys and agree on a common aggregated key
val internalPubKey = Musig2.keyAgg(listOf(userPrivateKey.publicKey(), serverPrivateKey.publicKey())).Q.xOnly()
val (commonPubKey, parity) = internalPubKey.outputKey(Crypto.TaprootTweak.ScriptTweak(merkleRoot))
val swapInTx = Transaction(
version = 2,
txIn = listOf(),
txOut = listOf(TxOut(Satoshi(10000), listOf(OP_1, OP_PUSHDATA(commonPubKey)))),
lockTime = 0
)

// The transaction can be spent if the user and the server produce a signature.
run {
val tx = Transaction(
version = 2,
txIn = listOf(TxIn(OutPoint(swapInTx, 0), sequence = TxIn.SEQUENCE_FINAL)),
txOut = listOf(TxOut(Satoshi(10000), pay2wpkh(PrivateKey(randomBytes32()).publicKey()))),
lockTime = 0
)
// this is the beginning of an interactive musig2 signing session. if user and server are disconnected before they have exchanged partial
// signatures they will to start again with fresh nonces
val userNonce = SecretNonce.generate(userPrivateKey, userPrivateKey.publicKey(), commonPubKey, null, null, randomBytes32())
val serverNonce = SecretNonce.generate(serverPrivateKey, serverPrivateKey.publicKey(), commonPubKey, null, null, randomBytes32())

val txHash = Transaction.hashForSigningSchnorr(tx, 0, listOf(swapInTx.txOut[0]), SigHash.SIGHASH_DEFAULT, SigVersion.SIGVERSION_TAPROOT)

val ctx = SessionCtx(
PublicNonce.aggregate(listOf(userNonce.publicNonce(), serverNonce.publicNonce())),
listOf(userPrivateKey.publicKey(), serverPrivateKey.publicKey()),
listOf(Pair(internalPubKey.tweak(Crypto.TaprootTweak.ScriptTweak(merkleRoot)), true)),
txHash
)
val userSig = ctx.sign(userNonce, userPrivateKey)
val severSig = ctx.sign(serverNonce, serverPrivateKey)
val commonSig = ctx.partialSigAgg(listOf(userSig, severSig))
// end of the musig2 signing session

val signedTx = tx.updateWitness(0, ScriptWitness(listOf(commonSig)))
Transaction.correctlySpends(signedTx, swapInTx, ScriptFlags.STANDARD_SCRIPT_VERIFY_FLAGS)
}

// Or it can be spent with only the user's signature, after a delay.
run {
val tx = Transaction(
version = 2,
txIn = listOf(TxIn(OutPoint(swapInTx, 0), sequence = refundDelay.toLong())),
txOut = listOf(TxOut(Satoshi(10000), pay2wpkh(PrivateKey(randomBytes32()).publicKey()))),
lockTime = 0
)
val txHash = Transaction.hashForSigningSchnorr(
tx,
0,
listOf(swapInTx.txOut[0]),
SigHash.SIGHASH_DEFAULT,
SigVersion.SIGVERSION_TAPSCRIPT,
Script.ExecutionData(annex = null, tapleafHash = merkleRoot)
)
val sig = Crypto.signSchnorr(txHash, userPrivateKey, Crypto.SchnorrTweak.NoTweak)
val controlBlock = byteArrayOf((Script.TAPROOT_LEAF_TAPSCRIPT + (if (parity) 1 else 0)).toByte()) + internalPubKey.value.toByteArray()
val signedTx = tx.updateWitness(0, ScriptWitness(listOf(sig, write(redeemScript).byteVector(), controlBlock.byteVector())))
Transaction.correctlySpends(signedTx, swapInTx, ScriptFlags.STANDARD_SCRIPT_VERIFY_FLAGS)
}
}

@Test
fun `swap-in input weight`() {
val pubkey = randomKey().publicKey()
Expand Down

0 comments on commit ff25e72

Please sign in to comment.