From ff25e72aa483c74ce92615e07b6f267fb2c605c1 Mon Sep 17 00:00:00 2001 From: sstone Date: Mon, 23 Oct 2023 18:33:38 +0200 Subject: [PATCH] Add an example of swapin transaction that uses musig2 and taproot 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) --- build.gradle.kts | 2 +- .../transactions/TransactionsTestsCommon.kt | 78 +++++++++++++++++++ 2 files changed, 79 insertions(+), 1 deletion(-) diff --git a/build.gradle.kts b/build.gradle.kts index a2a394a02..64bacf6da 100644 --- a/build.gradle.kts +++ b/build.gradle.kts @@ -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") diff --git a/src/commonTest/kotlin/fr/acinq/lightning/transactions/TransactionsTestsCommon.kt b/src/commonTest/kotlin/fr/acinq/lightning/transactions/TransactionsTestsCommon.kt index d5324e6db..9cd1dddd4 100644 --- a/src/commonTest/kotlin/fr/acinq/lightning/transactions/TransactionsTestsCommon.kt +++ b/src/commonTest/kotlin/fr/acinq/lightning/transactions/TransactionsTestsCommon.kt @@ -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 @@ -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()