From 6c8988e6404d0826f7e980624feb8b9349cb2b33 Mon Sep 17 00:00:00 2001 From: sstone Date: Mon, 30 Oct 2023 19:15:30 +0100 Subject: [PATCH 1/8] Move swap-in related methods into their own class --- .../acinq/lightning/channel/InteractiveTx.kt | 15 ++++++-- .../fr/acinq/lightning/crypto/KeyManager.kt | 28 ++++++++++---- .../acinq/lightning/transactions/Scripts.kt | 26 ------------- .../lightning/transactions/SwapInProtocol.kt | 38 +++++++++++++++++++ .../lightning/transactions/Transactions.kt | 12 ------ .../transactions/TransactionsTestsCommon.kt | 19 +++++----- 6 files changed, 78 insertions(+), 60 deletions(-) create mode 100644 src/commonMain/kotlin/fr/acinq/lightning/transactions/SwapInProtocol.kt diff --git a/src/commonMain/kotlin/fr/acinq/lightning/channel/InteractiveTx.kt b/src/commonMain/kotlin/fr/acinq/lightning/channel/InteractiveTx.kt index af65d8a4b..1d28d923d 100644 --- a/src/commonMain/kotlin/fr/acinq/lightning/channel/InteractiveTx.kt +++ b/src/commonMain/kotlin/fr/acinq/lightning/channel/InteractiveTx.kt @@ -8,6 +8,7 @@ import fr.acinq.lightning.blockchain.fee.FeeratePerKw import fr.acinq.lightning.crypto.KeyManager import fr.acinq.lightning.transactions.CommitmentSpec import fr.acinq.lightning.transactions.Scripts +import fr.acinq.lightning.transactions.SwapInProtocol import fr.acinq.lightning.transactions.Transactions import fr.acinq.lightning.utils.* import fr.acinq.lightning.wire.* @@ -348,14 +349,18 @@ data class SharedTransaction( val swapUserSigs = unsignedTx.txIn.mapIndexed { i, txIn -> localInputs .find { txIn.outPoint == it.outPoint } - ?.let { input -> Transactions.signSwapInputUser(unsignedTx, i, input.txOut, keyManager.swapInOnChainWallet.userPrivateKey, keyManager.swapInOnChainWallet.remoteServerPublicKey, keyManager.swapInOnChainWallet.refundDelay) } + ?.let { input -> keyManager.swapInOnChainWallet.signSwapInputUser(unsignedTx, i, input.txOut) } }.filterNotNull() // If the remote is swapping funds in, they'll need our partial signatures to finalize their witness. val swapServerSigs = unsignedTx.txIn.mapIndexed { i, txIn -> remoteInputs .filterIsInstance() .find { txIn.outPoint == it.outPoint } - ?.let { input -> Transactions.signSwapInputServer(unsignedTx, i, input.txOut, input.userKey, keyManager.swapInOnChainWallet.localServerPrivateKey(remoteNodeId), keyManager.swapInOnChainWallet.refundDelay) } + ?.let { input -> + val serverKey = keyManager.swapInOnChainWallet.localServerPrivateKey(remoteNodeId) + val swapInProtocol = SwapInProtocol(input.userKey, serverKey.publicKey(), input.refundDelay) + swapInProtocol.signSwapInputServer(unsignedTx, i, input.txOut, serverKey) + } }.filterNotNull() return PartiallySignedSharedTransaction(this, TxSignatures(fundingParams.channelId, unsignedTx, listOf(), sharedSig, swapUserSigs, swapServerSigs)) } @@ -407,13 +412,15 @@ data class FullySignedSharedTransaction(override val tx: SharedTransaction, over val localOnlyTxIn = tx.localOnlyInputs().sortedBy { i -> i.serialId }.zip(localSigs.witnesses).map { (i, w) -> Pair(i.serialId, TxIn(OutPoint(i.previousTx, i.previousTxOutput), ByteVector.empty, i.sequence.toLong(), w)) } val localSwapTxIn = tx.localSwapInputs().sortedBy { i -> i.serialId }.zip(localSigs.swapInUserSigs.zip(remoteSigs.swapInServerSigs)).map { (i, sigs) -> val (userSig, serverSig) = sigs - val witness = Scripts.witnessSwapIn2of2(userSig, i.userKey, serverSig, i.serverKey, i.refundDelay) + val swapInProtocol = SwapInProtocol(i.userKey, i.serverKey, i.refundDelay) + val witness = swapInProtocol.witness(userSig, serverSig) Pair(i.serialId, TxIn(OutPoint(i.previousTx, i.previousTxOutput), ByteVector.empty, i.sequence.toLong(), witness)) } val remoteOnlyTxIn = tx.remoteOnlyInputs().sortedBy { i -> i.serialId }.zip(remoteSigs.witnesses).map { (i, w) -> Pair(i.serialId, TxIn(i.outPoint, ByteVector.empty, i.sequence.toLong(), w)) } val remoteSwapTxIn = tx.remoteSwapInputs().sortedBy { i -> i.serialId }.zip(remoteSigs.swapInUserSigs.zip(localSigs.swapInServerSigs)).map { (i, sigs) -> val (userSig, serverSig) = sigs - val witness = Scripts.witnessSwapIn2of2(userSig, i.userKey, serverSig, i.serverKey, i.refundDelay) + val swapInProtocol = SwapInProtocol(i.userKey, i.serverKey, i.refundDelay) + val witness = swapInProtocol.witness(userSig, serverSig) Pair(i.serialId, TxIn(i.outPoint, ByteVector.empty, i.sequence.toLong(), witness)) } val inputs = (sharedTxIn + localOnlyTxIn + localSwapTxIn + remoteOnlyTxIn + remoteSwapTxIn).sortedBy { (serialId, _) -> serialId }.map { (_, i) -> i } diff --git a/src/commonMain/kotlin/fr/acinq/lightning/crypto/KeyManager.kt b/src/commonMain/kotlin/fr/acinq/lightning/crypto/KeyManager.kt index ab0dab9fe..1ac72e6f6 100644 --- a/src/commonMain/kotlin/fr/acinq/lightning/crypto/KeyManager.kt +++ b/src/commonMain/kotlin/fr/acinq/lightning/crypto/KeyManager.kt @@ -6,7 +6,7 @@ import fr.acinq.bitcoin.io.ByteArrayInput import fr.acinq.lightning.DefaultSwapInParams import fr.acinq.lightning.NodeParams import fr.acinq.lightning.blockchain.fee.FeeratePerKw -import fr.acinq.lightning.transactions.Scripts +import fr.acinq.lightning.transactions.SwapInProtocol import fr.acinq.lightning.transactions.Transactions import fr.acinq.lightning.utils.sum import fr.acinq.lightning.utils.toByteVector @@ -118,15 +118,19 @@ interface KeyManager { val refundDelay: Int = DefaultSwapInParams.RefundDelay ) { private val userExtendedPrivateKey: DeterministicWallet.ExtendedPrivateKey = DeterministicWallet.derivePrivateKey(master, swapInUserKeyPath(chain)) + private val swapExtendedPublicKey = DeterministicWallet.publicKey(DeterministicWallet.derivePrivateKey(master, swapInLocalServerKeyPath(chain))) + private val xpub = DeterministicWallet.encode(swapExtendedPublicKey, DeterministicWallet.tpub) + val userPrivateKey: PrivateKey = userExtendedPrivateKey.privateKey val userPublicKey: PublicKey = userPrivateKey.publicKey() private val localServerExtendedPrivateKey: DeterministicWallet.ExtendedPrivateKey = DeterministicWallet.derivePrivateKey(master, swapInLocalServerKeyPath(chain)) fun localServerPrivateKey(remoteNodeId: PublicKey): PrivateKey = DeterministicWallet.derivePrivateKey(localServerExtendedPrivateKey, perUserPath(remoteNodeId)).privateKey - val redeemScript: List = Scripts.swapIn2of2(userPublicKey, remoteServerPublicKey, refundDelay) - val pubkeyScript: List = Script.pay2wsh(redeemScript) - val address: String = Bitcoin.addressFromPublicKeyScript(chain.chainHash, pubkeyScript).result!! + val swapInProtocol = SwapInProtocol(userPublicKey, remoteServerPublicKey, refundDelay) + val redeemScript: List = swapInProtocol.redeemScript + val pubkeyScript: List = swapInProtocol.pubkeyScript + val address: String = swapInProtocol.address(chain) /** * The output script descriptor matching our swap-in addresses. @@ -142,6 +146,14 @@ interface KeyManager { "wsh(and_v(v:pk($userKey),or_d(pk(${remoteServerPublicKey.toHex()}),older($refundDelay))))" } + fun signSwapInputUser(fundingTx: Transaction, index: Int, parentTxOut: TxOut): ByteVector64 { + return swapInProtocol.signSwapInputUser(fundingTx, index, parentTxOut, userPrivateKey) + } + + fun signSwapInputServer(fundingTx: Transaction, index: Int, parentTxOut: TxOut, remoteNodeId: PublicKey): ByteVector64 { + return swapInProtocol.signSwapInputServer(fundingTx, index, parentTxOut, localServerPrivateKey(remoteNodeId)) + } + /** * Create a recovery transaction that spends a swap-in transaction after the refund delay has passed * @param swapInTx swap-in transaction @@ -165,15 +177,15 @@ interface KeyManager { ) val fees = run { val recoveryTx = utxos.foldIndexed(unsignedTx) { index, tx, utxo -> - val sig = Transactions.signSwapInputUser(tx, index, utxo, userPrivateKey, remoteServerPublicKey, refundDelay) - tx.updateWitness(index, Scripts.witnessSwapIn2of2Refund(sig, userPublicKey, remoteServerPublicKey, refundDelay)) + val sig = swapInProtocol.signSwapInputUser(tx, index, utxo, userPrivateKey) + tx.updateWitness(index, swapInProtocol.witnessRefund(sig)) } Transactions.weight2fee(feeRate, recoveryTx.weight()) } val unsignedTx1 = unsignedTx.copy(txOut = listOf(ourOutput.copy(amount = ourOutput.amount - fees))) val recoveryTx = utxos.foldIndexed(unsignedTx1) { index, tx, utxo -> - val sig = Transactions.signSwapInputUser(tx, index, utxo, userPrivateKey, remoteServerPublicKey, refundDelay) - tx.updateWitness(index, Scripts.witnessSwapIn2of2Refund(sig, userPublicKey, remoteServerPublicKey, refundDelay)) + val sig = swapInProtocol.signSwapInputUser(tx, index, utxo, userPrivateKey) + tx.updateWitness(index, swapInProtocol.witnessRefund(sig)) } // this tx is signed but cannot be published until swapInTx has `refundDelay` confirmations recoveryTx diff --git a/src/commonMain/kotlin/fr/acinq/lightning/transactions/Scripts.kt b/src/commonMain/kotlin/fr/acinq/lightning/transactions/Scripts.kt index d36431ae1..985d09dec 100644 --- a/src/commonMain/kotlin/fr/acinq/lightning/transactions/Scripts.kt +++ b/src/commonMain/kotlin/fr/acinq/lightning/transactions/Scripts.kt @@ -30,32 +30,6 @@ object Scripts { ScriptWitness(listOf(ByteVector.empty, der(sig2, SigHash.SIGHASH_ALL), der(sig1, SigHash.SIGHASH_ALL), ByteVector(Script.write(multiSig2of2(pubkey1, pubkey2))))) } - /** - * @return the script used for a 2-of-2 swap-in as used in Phoenix. - */ - fun swapIn2of2(userKey: PublicKey, serverKey: PublicKey, delayedRefund: Int): List { - // This script was generated with https://bitcoin.sipa.be/miniscript/ using the following miniscript policy: - // and(pk(),or(99@pk(),older())) - // @formatter:off - return listOf( - OP_PUSHDATA(userKey), OP_CHECKSIGVERIFY, OP_PUSHDATA(serverKey), OP_CHECKSIG, OP_IFDUP, - OP_NOTIF, - OP_PUSHDATA(Script.encodeNumber(delayedRefund)), OP_CHECKSEQUENCEVERIFY, - OP_ENDIF - ) - // @formatter:on - } - - fun witnessSwapIn2of2(userSig: ByteVector64, userKey: PublicKey, serverSig: ByteVector64, serverKey: PublicKey, delayedRefund: Int): ScriptWitness { - val redeemScript = swapIn2of2(userKey, serverKey, delayedRefund) - return ScriptWitness(listOf(der(serverSig, SigHash.SIGHASH_ALL), der(userSig, SigHash.SIGHASH_ALL), Script.write(redeemScript).byteVector())) - } - - fun witnessSwapIn2of2Refund(userSig: ByteVector64, userKey: PublicKey, serverKey: PublicKey, delayedRefund: Int): ScriptWitness { - val redeemScript = swapIn2of2(userKey, serverKey, delayedRefund) - return ScriptWitness(listOf(ByteVector.empty, der(userSig, SigHash.SIGHASH_ALL), Script.write(redeemScript).byteVector())) - } - /** * minimal encoding of a number into a script element: * - OP_0 to OP_16 if 0 <= n <= 16 diff --git a/src/commonMain/kotlin/fr/acinq/lightning/transactions/SwapInProtocol.kt b/src/commonMain/kotlin/fr/acinq/lightning/transactions/SwapInProtocol.kt new file mode 100644 index 000000000..a87e0860a --- /dev/null +++ b/src/commonMain/kotlin/fr/acinq/lightning/transactions/SwapInProtocol.kt @@ -0,0 +1,38 @@ +package fr.acinq.lightning.transactions + +import fr.acinq.bitcoin.* +import fr.acinq.lightning.NodeParams + +class SwapInProtocol(val userPublicKey: PublicKey, val serverPublicKey: PublicKey, val refundDelay: Int) { + // This script was generated with https://bitcoin.sipa.be/miniscript/ using the following miniscript policy: + // and(pk(),or(99@pk(),older())) + // @formatter:off + val redeemScript = listOf( + OP_PUSHDATA(userPublicKey), OP_CHECKSIGVERIFY, OP_PUSHDATA(serverPublicKey), OP_CHECKSIG, OP_IFDUP, + OP_NOTIF, + OP_PUSHDATA(Script.encodeNumber(refundDelay)), OP_CHECKSEQUENCEVERIFY, + OP_ENDIF + ) + // @formatter:on + + val pubkeyScript: List = Script.pay2wsh(redeemScript) + + fun address(chain: NodeParams.Chain): String = Bitcoin.addressFromPublicKeyScript(chain.chainHash, pubkeyScript).result!! + + fun witness(userSig: ByteVector64, serverSig: ByteVector64): ScriptWitness { + return ScriptWitness(listOf(Scripts.der(serverSig, SigHash.SIGHASH_ALL), Scripts.der(userSig, SigHash.SIGHASH_ALL), Script.write(redeemScript).byteVector())) + } + + fun witnessRefund(userSig: ByteVector64): ScriptWitness { + return ScriptWitness(listOf(ByteVector.empty, Scripts.der(userSig, SigHash.SIGHASH_ALL), Script.write(redeemScript).byteVector())) + } + + fun signSwapInputUser(fundingTx: Transaction, index: Int, parentTxOut: TxOut, userKey: PrivateKey): ByteVector64 { + require(userKey.publicKey() == userPublicKey) + return Transactions.sign(fundingTx, index, Script.write(redeemScript), parentTxOut.amount, userKey) + } + + fun signSwapInputServer(fundingTx: Transaction, index: Int, parentTxOut: TxOut, serverKey: PrivateKey): ByteVector64 { + return Transactions.sign(fundingTx, index, Script.write(redeemScript), parentTxOut.amount, serverKey) + } +} \ No newline at end of file diff --git a/src/commonMain/kotlin/fr/acinq/lightning/transactions/Transactions.kt b/src/commonMain/kotlin/fr/acinq/lightning/transactions/Transactions.kt index 016d8a42b..0085ee6c6 100644 --- a/src/commonMain/kotlin/fr/acinq/lightning/transactions/Transactions.kt +++ b/src/commonMain/kotlin/fr/acinq/lightning/transactions/Transactions.kt @@ -806,18 +806,6 @@ object Transactions { return sign(txInfo.tx, inputIndex, txInfo.input.redeemScript.toByteArray(), txInfo.input.txOut.amount, key, sigHash) } - /** Sign an input from a 2-of-2 swap-in address with the swap user's key. */ - fun signSwapInputUser(fundingTx: Transaction, index: Int, parentTxOut: TxOut, userKey: PrivateKey, serverKey: PublicKey, refundDelay: Int): ByteVector64 { - val redeemScript = Scripts.swapIn2of2(userKey.publicKey(), serverKey, refundDelay) - return sign(fundingTx, index, Script.write(redeemScript), parentTxOut.amount, userKey) - } - - /** Sign an input from a 2-of-2 swap-in address with the swap server's key. */ - fun signSwapInputServer(fundingTx: Transaction, index: Int, parentTxOut: TxOut, userKey: PublicKey, serverKey: PrivateKey, refundDelay: Int): ByteVector64 { - val redeemScript = Scripts.swapIn2of2(userKey, serverKey.publicKey(), refundDelay) - return sign(fundingTx, index, Script.write(redeemScript), parentTxOut.amount, serverKey) - } - fun addSigs( commitTx: TransactionWithInputInfo.CommitTx, localFundingPubkey: PublicKey, diff --git a/src/commonTest/kotlin/fr/acinq/lightning/transactions/TransactionsTestsCommon.kt b/src/commonTest/kotlin/fr/acinq/lightning/transactions/TransactionsTestsCommon.kt index 38f61c079..fb506e9a7 100644 --- a/src/commonTest/kotlin/fr/acinq/lightning/transactions/TransactionsTestsCommon.kt +++ b/src/commonTest/kotlin/fr/acinq/lightning/transactions/TransactionsTestsCommon.kt @@ -9,7 +9,6 @@ import fr.acinq.bitcoin.Script.write import fr.acinq.bitcoin.crypto.Pack import fr.acinq.lightning.CltvExpiry import fr.acinq.lightning.CltvExpiryDelta -import fr.acinq.lightning.Lightning.randomBytes import fr.acinq.lightning.Lightning.randomBytes32 import fr.acinq.lightning.Lightning.randomKey import fr.acinq.lightning.blockchain.fee.FeeratePerKw @@ -458,10 +457,10 @@ class TransactionsTestsCommon : LightningTestSuite() { txOut = listOf(TxOut(90_000.sat, pay2wpkh(randomKey().publicKey()))), lockTime = 0 ) - val userSig = Transactions.signSwapInputUser(fundingTx, 0, swapInTx.txOut.first(), userWallet.userPrivateKey, userWallet.remoteServerPublicKey, userWallet.refundDelay) - val serverWallet = TestConstants.Bob.keyManager.swapInOnChainWallet - val serverSig = Transactions.signSwapInputServer(fundingTx, 0, swapInTx.txOut.first(), userWallet.userPublicKey, serverWallet.localServerPrivateKey(TestConstants.Alice.nodeParams.nodeId), serverWallet.refundDelay) - val witness = Scripts.witnessSwapIn2of2(userSig, userWallet.userPublicKey, serverSig, userWallet.remoteServerPublicKey, userWallet.refundDelay) + val userSig = userWallet.signSwapInputUser(fundingTx, 0, swapInTx.txOut.first()) + val serverKey = TestConstants.Bob.keyManager.swapInOnChainWallet.localServerPrivateKey(TestConstants.Alice.nodeParams.nodeId) + val serverSig = userWallet.swapInProtocol.signSwapInputServer(fundingTx, 0, swapInTx.txOut.first(), serverKey) + val witness = userWallet.swapInProtocol.witness(userSig, serverSig) val signedTx = fundingTx.updateWitness(0, witness) Transaction.correctlySpends(signedTx, listOf(swapInTx), ScriptFlags.STANDARD_SCRIPT_VERIFY_FLAGS) } @@ -473,8 +472,8 @@ class TransactionsTestsCommon : LightningTestSuite() { txOut = listOf(TxOut(90_000.sat, pay2wpkh(randomKey().publicKey()))), lockTime = 0 ) - val userSig = Transactions.signSwapInputUser(fundingTx, 0, swapInTx.txOut.first(), userWallet.userPrivateKey, userWallet.remoteServerPublicKey, userWallet.refundDelay) - val witness = Scripts.witnessSwapIn2of2Refund(userSig, userWallet.userPublicKey, userWallet.remoteServerPublicKey, userWallet.refundDelay) + val userSig = userWallet.signSwapInputUser(fundingTx, 0, swapInTx.txOut.first()) + val witness = userWallet.swapInProtocol.witnessRefund(userSig) val signedTx = fundingTx.updateWitness(0, witness) Transaction.correctlySpends(signedTx, listOf(swapInTx), ScriptFlags.STANDARD_SCRIPT_VERIFY_FLAGS) } @@ -484,10 +483,10 @@ class TransactionsTestsCommon : LightningTestSuite() { fun `swap-in input weight`() { val pubkey = randomKey().publicKey() // DER-encoded ECDSA signatures usually take up to 72 bytes. - val sig = randomBytes(72).toByteVector() + val sig = ByteVector64.fromValidHex("90b658d172a51f1b3f1a2becd30942397f5df97da8cd2c026854607e955ad815ccfd87d366e348acc32aaf15ff45263aebbb7ecc913a0e5999133f447aee828c") val tx = Transaction(2, listOf(TxIn(OutPoint(ByteVector32.Zeroes, 2), 0)), listOf(TxOut(50_000.sat, pay2wpkh(pubkey))), 0) - val redeemScript = Scripts.swapIn2of2(pubkey, pubkey, 144) - val witness = ScriptWitness(listOf(sig, sig, write(redeemScript).byteVector())) + val swapInProtocol = SwapInProtocol(pubkey, pubkey, 144) + val witness = swapInProtocol.witness(sig, sig) val swapInput = TxIn(OutPoint(ByteVector32.Zeroes, 3), ByteVector.empty, 0, witness) val txWithAdditionalInput = tx.copy(txIn = tx.txIn + listOf(swapInput)) val inputWeight = txWithAdditionalInput.weight() - tx.weight() From b96cb7fd3323fdc9ebff7de6f808df0a85564a34 Mon Sep 17 00:00:00 2001 From: sstone Date: Mon, 23 Oct 2023 18:33:38 +0200 Subject: [PATCH 2/8] Add an example of swapin transaction that uses musig2 and taproot 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) Add another example with taproot but not musig2 that uses 2 differents scripts (mutual case and refund case) --- build.gradle.kts | 6 +- .../lightning/transactions/SwapInProtocol.kt | 65 ++++++++++ .../transactions/TransactionsTestsCommon.kt | 121 ++++++++++++++++++ 3 files changed, 189 insertions(+), 3 deletions(-) diff --git a/build.gradle.kts b/build.gradle.kts index 84ca9809d..bd4eb6df6 100644 --- a/build.gradle.kts +++ b/build.gradle.kts @@ -11,7 +11,7 @@ plugins { allprojects { group = "fr.acinq.lightning" - version = "1.5.12-SNAPSHOT" + version = "1.5.12-SWAPIN2-SNAPSHOT" repositories { // using the local maven repository with Kotlin Multi Platform can lead to build errors that are hard to diagnose. @@ -33,7 +33,7 @@ kotlin { val commonMain by sourceSets.getting { dependencies { - api("fr.acinq.bitcoin:bitcoin-kmp:0.13.0") // when upgrading, keep secp256k1-kmp-jni-jvm in sync below + api("fr.acinq.bitcoin:bitcoin-kmp:0.14.1-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") @@ -63,7 +63,7 @@ kotlin { api(ktor("client-okhttp")) api(ktor("network")) api(ktor("network-tls")) - implementation("fr.acinq.secp256k1:secp256k1-kmp-jni-jvm:0.10.1") + implementation("fr.acinq.secp256k1:secp256k1-kmp-jni-jvm:0.11.0") implementation("org.slf4j:slf4j-api:1.7.36") api("org.xerial:sqlite-jdbc:3.32.3.2") } diff --git a/src/commonMain/kotlin/fr/acinq/lightning/transactions/SwapInProtocol.kt b/src/commonMain/kotlin/fr/acinq/lightning/transactions/SwapInProtocol.kt index a87e0860a..297923770 100644 --- a/src/commonMain/kotlin/fr/acinq/lightning/transactions/SwapInProtocol.kt +++ b/src/commonMain/kotlin/fr/acinq/lightning/transactions/SwapInProtocol.kt @@ -1,6 +1,11 @@ package fr.acinq.lightning.transactions import fr.acinq.bitcoin.* +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.Lightning import fr.acinq.lightning.NodeParams class SwapInProtocol(val userPublicKey: PublicKey, val serverPublicKey: PublicKey, val refundDelay: Int) { @@ -35,4 +40,64 @@ class SwapInProtocol(val userPublicKey: PublicKey, val serverPublicKey: PublicKe fun signSwapInputServer(fundingTx: Transaction, index: Int, parentTxOut: TxOut, serverKey: PrivateKey): ByteVector64 { return Transactions.sign(fundingTx, index, Script.write(redeemScript), parentTxOut.amount, serverKey) } +} + +class SwapInProtocolMusig2(val userPublicKey: PublicKey, val serverPublicKey: PublicKey, val refundDelay: Int) { + // the redeem script is just the refund script. it is generated from this policy: and_v(v:pk(user),older(refundDelay)) + val redeemScript = listOf(OP_PUSHDATA(userPublicKey.xOnly()), OP_CHECKSIGVERIFY, OP_PUSHDATA(Script.encodeNumber(refundDelay)), OP_CHECKSEQUENCEVERIFY) + private val scriptTree = ScriptTree.Leaf(ScriptLeaf(0, Script.write(redeemScript).byteVector(), Script.TAPROOT_LEAF_TAPSCRIPT)) + private val merkleRoot = ScriptTree.hash(scriptTree) + private val internalPubKey = Musig2.keyAgg(listOf(userPublicKey, serverPublicKey)).Q.xOnly() + private val commonPubKeyAndParity = internalPubKey.outputKey(Crypto.TaprootTweak.ScriptTweak(merkleRoot)) + val commonPubKey = commonPubKeyAndParity.first + private val parity = commonPubKeyAndParity.second + val pubkeyScript: List = Script.pay2tr(commonPubKey) + private val executionData = Script.ExecutionData(annex = null, tapleafHash = merkleRoot) + private val controlBlock = byteArrayOf((Script.TAPROOT_LEAF_TAPSCRIPT + (if (parity) 1 else 0)).toByte()) + internalPubKey.value.toByteArray() + + fun address(chain: NodeParams.Chain): String = Bitcoin.addressFromPublicKeyScript(chain.chainHash, pubkeyScript).result!! + + fun witness(commonSig: ByteVector64): ScriptWitness = ScriptWitness(listOf(commonSig)) + + fun witnessRefund(userSig: ByteVector64): ScriptWitness = ScriptWitness.empty.push(userSig).push(redeemScript).push(controlBlock) + + fun signSwapInputUser(fundingTx: Transaction, index: Int, parentTxOuts: List, userPrivateKey: PrivateKey, userNonce: SecretNonce, serverNonce: PublicNonce): ByteVector32 { + require(userPrivateKey.publicKey() == userPublicKey) + val txHash = Transaction.hashForSigningSchnorr(fundingTx, index, parentTxOuts, SigHash.SIGHASH_DEFAULT, SigVersion.SIGVERSION_TAPROOT) + + val ctx = SessionCtx( + PublicNonce.aggregate(listOf(userNonce.publicNonce(), serverNonce)), + listOf(userPrivateKey.publicKey(), serverPublicKey), + listOf(Pair(internalPubKey.tweak(Crypto.TaprootTweak.ScriptTweak(merkleRoot)), true)), + txHash + ) + return ctx.sign(userNonce, userPrivateKey) + } + + fun signSwapInputRefund(fundingTx: Transaction, index: Int, parentTxOuts: List, userPrivateKey: PrivateKey): ByteVector64 { + val txHash = Transaction.hashForSigningSchnorr(fundingTx, index, parentTxOuts, SigHash.SIGHASH_DEFAULT, SigVersion.SIGVERSION_TAPSCRIPT, executionData) + return Crypto.signSchnorr(txHash, userPrivateKey, Crypto.SchnorrTweak.NoTweak) + } + + fun signSwapInputServer(fundingTx: Transaction, index: Int, parentTxOuts: List, userNonce: PublicNonce, serverPrivateKey: PrivateKey, serverNonce: SecretNonce): ByteVector32 { + val txHash = Transaction.hashForSigningSchnorr(fundingTx, index, parentTxOuts, SigHash.SIGHASH_DEFAULT, SigVersion.SIGVERSION_TAPROOT) + + val ctx = SessionCtx( + PublicNonce.aggregate(listOf(userNonce, serverNonce.publicNonce())), + listOf(userPublicKey, serverPrivateKey.publicKey()), + listOf(Pair(internalPubKey.tweak(Crypto.TaprootTweak.ScriptTweak(merkleRoot)), true)), + txHash + ) + return ctx.sign(serverNonce, serverPrivateKey) + } + + fun signingCtx(fundingTx: Transaction, index: Int, parentTxOuts: List, commonNonce: PublicNonce): SessionCtx { + val txHash = Transaction.hashForSigningSchnorr(fundingTx, index, parentTxOuts, SigHash.SIGHASH_DEFAULT, SigVersion.SIGVERSION_TAPROOT) + return SessionCtx( + commonNonce, + listOf(userPublicKey, serverPublicKey), + listOf(Pair(internalPubKey.tweak(Crypto.TaprootTweak.ScriptTweak(merkleRoot)), true)), + txHash + ) + } } \ No newline at end of file diff --git a/src/commonTest/kotlin/fr/acinq/lightning/transactions/TransactionsTestsCommon.kt b/src/commonTest/kotlin/fr/acinq/lightning/transactions/TransactionsTestsCommon.kt index fb506e9a7..824e4ba11 100644 --- a/src/commonTest/kotlin/fr/acinq/lightning/transactions/TransactionsTestsCommon.kt +++ b/src/commonTest/kotlin/fr/acinq/lightning/transactions/TransactionsTestsCommon.kt @@ -7,6 +7,9 @@ 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.lightning.CltvExpiry import fr.acinq.lightning.CltvExpiryDelta import fr.acinq.lightning.Lightning.randomBytes32 @@ -479,6 +482,124 @@ class TransactionsTestsCommon : LightningTestSuite() { } } + @Test + fun `spend 2-of-2 swap-in taproot without musig2 version`() { + val userPrivateKey = PrivateKey(ByteArray(32) { 1 }) + val serverPrivateKey = PrivateKey(ByteArray(32) { 2 }) + + // mutual agreement script is generated from this policy: and_v(v:pk(A),pk(B)) + val mutualScript = listOf(OP_PUSHDATA(userPrivateKey.xOnlyPublicKey()), OP_CHECKSIGVERIFY, OP_PUSHDATA(serverPrivateKey.xOnlyPublicKey()), OP_CHECKSIG) + + // the refund script is generated from this policy: and_v(v:pk(user),older(refundDelay)) + val refundDelay = 144 + val refundScript = listOf(OP_PUSHDATA(userPrivateKey.xOnlyPublicKey()), OP_CHECKSIGVERIFY, OP_PUSHDATA(Script.encodeNumber(refundDelay)), OP_CHECKSEQUENCEVERIFY) + + // we have a simple script tree with 2 leaves + val scriptTree = ScriptTree.Branch( + ScriptTree.Leaf(ScriptLeaf(0, write(mutualScript).byteVector(), Script.TAPROOT_LEAF_TAPSCRIPT)), + ScriptTree.Leaf(ScriptLeaf(1, write(refundScript).byteVector(), Script.TAPROOT_LEAF_TAPSCRIPT)) + ) + val merkleRoot = ScriptTree.hash(scriptTree) + + // we choose a pubkey that does not have a corresponding private key: our swap-in tx can only be spent through the script path, not the key path + val internalPubkey = XonlyPublicKey(PublicKey.fromHex("0250929b74c1a04954b78b4b6035e97a5e078a5a0f28ec96d547bfee9ace803ac0")) + val (tweakedKey, parity) = internalPubkey.outputKey(Crypto.TaprootTweak.ScriptTweak(merkleRoot)) + + val swapInTx = Transaction( + version = 2, + txIn = listOf(), + txOut = listOf(TxOut(Satoshi(10000), listOf(OP_1, OP_PUSHDATA(tweakedKey)))), + 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 + ) + // we want to spend the left leave of the tree, so we provide the hash of the right leave (to be able to recompute the merkle root of the tree) + val controlBlock = byteArrayOf((Script.TAPROOT_LEAF_TAPSCRIPT + (if (parity) 1 else 0)).toByte()) + + internalPubkey.value.toByteArray() + + ScriptTree.hash(scriptTree.right).toByteArray() + + val txHash = Transaction.hashForSigningSchnorr(tx, 0, swapInTx.txOut, SigHash.SIGHASH_DEFAULT, SigVersion.SIGVERSION_TAPSCRIPT, Script.ExecutionData(null, ScriptTree.hash(scriptTree.left))) + val userSig = Crypto.signSchnorr(txHash, userPrivateKey, Crypto.SchnorrTweak.NoTweak) + val serverSig = Crypto.signSchnorr(txHash, serverPrivateKey, Crypto.SchnorrTweak.NoTweak) + + val signedTx = tx.updateWitness(0, ScriptWitness.empty.push(serverSig).push(userSig).push(mutualScript).push(controlBlock)) + 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 controlBlock = byteArrayOf((Script.TAPROOT_LEAF_TAPSCRIPT + (if (parity) 1 else 0)).toByte()) + + internalPubkey.value.toByteArray() + + ScriptTree.hash(scriptTree.left).toByteArray() + val txHash = Transaction.hashForSigningSchnorr(tx, 0, swapInTx.txOut, SigHash.SIGHASH_DEFAULT, SigVersion.SIGVERSION_TAPSCRIPT, Script.ExecutionData(null, ScriptTree.hash(scriptTree.right))) + val userSig = Crypto.signSchnorr(txHash, userPrivateKey, Crypto.SchnorrTweak.NoTweak) + val signedTx = tx.updateWitness(0, ScriptWitness.empty.push(userSig).push(refundScript).push(controlBlock)) + Transaction.correctlySpends(signedTx, swapInTx, ScriptFlags.STANDARD_SCRIPT_VERIFY_FLAGS) + } + } + + @Test + fun `spend 2-of-2 swap-in taproot-musig2 version`() { + val userPrivateKey = PrivateKey(ByteArray(32) { 1 }) + val serverPrivateKey = PrivateKey(ByteArray(32) { 2 }) + + val swapInProtocolMusig2 = SwapInProtocolMusig2(userPrivateKey.publicKey(), serverPrivateKey.publicKey(), 144) + val swapInTx = Transaction( + version = 2, + txIn = listOf(), + txOut = listOf(TxOut(Satoshi(10000), swapInProtocolMusig2.pubkeyScript)), + 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 have to start again with fresh nonces + val commonPubKey = Musig2.keyAgg(listOf(userPrivateKey.publicKey(), serverPrivateKey.publicKey())).Q.xOnly() + val userNonce = SecretNonce.generate(userPrivateKey, userPrivateKey.publicKey(), commonPubKey, null, null, randomBytes32()) + val serverNonce = SecretNonce.generate(serverPrivateKey, serverPrivateKey.publicKey(), commonPubKey, null, null, randomBytes32()) + + val userSig = swapInProtocolMusig2.signSwapInputUser(tx, 0, swapInTx.txOut, userPrivateKey, userNonce, serverNonce.publicNonce()) + val serverSig = swapInProtocolMusig2.signSwapInputServer(tx, 0, swapInTx.txOut, userNonce.publicNonce(), serverPrivateKey, serverNonce) + val ctx = swapInProtocolMusig2.signingCtx(tx, 0, swapInTx.txOut, PublicNonce.aggregate(listOf(userNonce.publicNonce(), serverNonce.publicNonce()))) + val commonSig = ctx.partialSigAgg(listOf(userSig, serverSig)) + 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 = 144)), + txOut = listOf(TxOut(Satoshi(10000), pay2wpkh(PrivateKey(randomBytes32()).publicKey()))), + lockTime = 0 + ) + val sig = swapInProtocolMusig2.signSwapInputRefund(tx, 0, swapInTx.txOut, userPrivateKey) + val signedTx = tx.updateWitness(0, swapInProtocolMusig2.witnessRefund(sig)) + Transaction.correctlySpends(signedTx, swapInTx, ScriptFlags.STANDARD_SCRIPT_VERIFY_FLAGS) + } + } + @Test fun `swap-in input weight`() { val pubkey = randomKey().publicKey() From c6687de2c599bd92838a2476158ea0d8705d841f Mon Sep 17 00:00:00 2001 From: sstone Date: Sat, 4 Nov 2023 13:35:52 +0100 Subject: [PATCH 3/8] Add a RemoteSwapInV2 message This message includes all outputs from the remote tx and not just the one that is included in the swap. This is needed for Schnorr signatures. --- .../kotlin/fr/acinq/lightning/channel/InteractiveTx.kt | 4 ++++ .../lightning/serialization/v4/Deserialization.kt | 9 +++++++++ .../acinq/lightning/serialization/v4/Serialization.kt | 10 ++++++++++ 3 files changed, 23 insertions(+) diff --git a/src/commonMain/kotlin/fr/acinq/lightning/channel/InteractiveTx.kt b/src/commonMain/kotlin/fr/acinq/lightning/channel/InteractiveTx.kt index 1d28d923d..d45eafa51 100644 --- a/src/commonMain/kotlin/fr/acinq/lightning/channel/InteractiveTx.kt +++ b/src/commonMain/kotlin/fr/acinq/lightning/channel/InteractiveTx.kt @@ -131,6 +131,10 @@ sealed class InteractiveTxInput { /** A remote input from a swap-in: our peer needs our signature to build a witness for that input. */ data class RemoteSwapIn(override val serialId: Long, override val outPoint: OutPoint, override val txOut: TxOut, override val sequence: UInt, val userKey: PublicKey, val serverKey: PublicKey, val refundDelay: Int) : Remote() + data class RemoteSwapInV2(override val serialId: Long, override val outPoint: OutPoint, val txOuts: List, override val sequence: UInt, val userKey: PublicKey, val serverKey: PublicKey, val refundDelay: Int) : Remote() { + override val txOut: TxOut get() = txOuts[outPoint.index.toInt()] + } + /** The shared input can be added by us or by our peer, depending on who initiated the protocol. */ data class Shared(override val serialId: Long, override val outPoint: OutPoint, override val sequence: UInt, val localAmount: MilliSatoshi, val remoteAmount: MilliSatoshi) : InteractiveTxInput(), Incoming, Outgoing } diff --git a/src/commonMain/kotlin/fr/acinq/lightning/serialization/v4/Deserialization.kt b/src/commonMain/kotlin/fr/acinq/lightning/serialization/v4/Deserialization.kt index 0edf0c81d..56766eb2f 100644 --- a/src/commonMain/kotlin/fr/acinq/lightning/serialization/v4/Deserialization.kt +++ b/src/commonMain/kotlin/fr/acinq/lightning/serialization/v4/Deserialization.kt @@ -245,6 +245,15 @@ object Deserialization { serverKey = readPublicKey(), refundDelay = readNumber().toInt() ) + 0x03 -> InteractiveTxInput.RemoteSwapInV2( + serialId = readNumber(), + outPoint = readOutPoint(), + txOuts = readCollection { TxOut.read(readDelimitedByteArray()) }.toList(), + sequence = readNumber().toUInt(), + userKey = readPublicKey(), + serverKey = readPublicKey(), + refundDelay = readNumber().toInt() + ) else -> error("unknown discriminator $discriminator for class ${InteractiveTxInput.Remote::class}") } diff --git a/src/commonMain/kotlin/fr/acinq/lightning/serialization/v4/Serialization.kt b/src/commonMain/kotlin/fr/acinq/lightning/serialization/v4/Serialization.kt index f3948de37..30011ff30 100644 --- a/src/commonMain/kotlin/fr/acinq/lightning/serialization/v4/Serialization.kt +++ b/src/commonMain/kotlin/fr/acinq/lightning/serialization/v4/Serialization.kt @@ -297,6 +297,16 @@ object Serialization { writePublicKey(serverKey) writeNumber(refundDelay) } + is InteractiveTxInput.RemoteSwapInV2 -> i.run { + write(0x03) + writeNumber(serialId) + writeBtcObject(outPoint) + writeCollection(i.txOuts) { o -> writeBtcObject(o) } + writeNumber(sequence.toLong()) + writePublicKey(userKey) + writePublicKey(serverKey) + writeNumber(refundDelay) + } } private fun Output.writeSharedInteractiveTxOutput(o: InteractiveTxOutput.Shared) = o.run { From 84ca1431c24301092516fb6edcf297ee511bf1dd Mon Sep 17 00:00:00 2001 From: sstone Date: Sun, 5 Nov 2023 21:26:58 +0100 Subject: [PATCH 4/8] Add musig2-based swap-in protocol --- .../acinq/lightning/channel/InteractiveTx.kt | 267 ++++++++++++++---- .../acinq/lightning/channel/states/Normal.kt | 1 + .../channel/states/WaitForFundingConfirmed.kt | 1 + .../channel/states/WaitForFundingCreated.kt | 1 + .../fr/acinq/lightning/crypto/KeyManager.kt | 36 ++- .../kotlin/fr/acinq/lightning/io/Peer.kt | 7 +- .../serialization/v4/Deserialization.kt | 31 +- .../serialization/v4/Serialization.kt | 24 +- .../lightning/transactions/SwapInProtocol.kt | 19 +- .../lightning/transactions/Transactions.kt | 3 + .../acinq/lightning/wire/InteractiveTxTlv.kt | 76 ++++- .../acinq/lightning/wire/LightningMessages.kt | 26 +- .../channel/InteractiveTxTestsCommon.kt | 54 ++-- .../fr/acinq/lightning/channel/TestsHelper.kt | 6 +- .../channel/states/SpliceTestsCommon.kt | 2 +- .../WaitForFundingConfirmedTestsCommon.kt | 2 +- .../crypto/LocalKeyManagerTestsCommon.kt | 11 +- .../StateSerializationTestsCommon.kt | 1 + .../transactions/TransactionsTestsCommon.kt | 23 +- .../wire/LightningCodecsTestsCommon.kt | 31 +- 20 files changed, 461 insertions(+), 161 deletions(-) diff --git a/src/commonMain/kotlin/fr/acinq/lightning/channel/InteractiveTx.kt b/src/commonMain/kotlin/fr/acinq/lightning/channel/InteractiveTx.kt index d45eafa51..8b174fb02 100644 --- a/src/commonMain/kotlin/fr/acinq/lightning/channel/InteractiveTx.kt +++ b/src/commonMain/kotlin/fr/acinq/lightning/channel/InteractiveTx.kt @@ -2,14 +2,14 @@ package fr.acinq.lightning.channel import fr.acinq.bitcoin.* import fr.acinq.bitcoin.Script.tail +import fr.acinq.bitcoin.musig2.PublicNonce +import fr.acinq.bitcoin.musig2.SecretNonce +import fr.acinq.lightning.Lightning.randomBytes32 import fr.acinq.lightning.MilliSatoshi import fr.acinq.lightning.blockchain.electrum.WalletState import fr.acinq.lightning.blockchain.fee.FeeratePerKw import fr.acinq.lightning.crypto.KeyManager -import fr.acinq.lightning.transactions.CommitmentSpec -import fr.acinq.lightning.transactions.Scripts -import fr.acinq.lightning.transactions.SwapInProtocol -import fr.acinq.lightning.transactions.Transactions +import fr.acinq.lightning.transactions.* import fr.acinq.lightning.utils.* import fr.acinq.lightning.wire.* import kotlinx.coroutines.CompletableDeferred @@ -80,8 +80,10 @@ data class InteractiveTxParams( /** Amount of the new funding output, which is the sum of the shared input, if any, and both sides' contributions. */ val fundingAmount: Satoshi = (sharedInput?.info?.txOut?.amount ?: 0.sat) + localContribution + remoteContribution + // BOLT 2: MUST set `feerate` greater than or equal to 25/24 times the `feerate` of the previously constructed transaction, rounded down. val minNextFeerate: FeeratePerKw = targetFeerate * 25 / 24 + // BOLT 2: the initiator's serial IDs MUST use even values and the non-initiator odd values. val serialIdParity = if (isInitiator) 0 else 1 @@ -95,6 +97,7 @@ sealed class InteractiveTxInput { abstract val serialId: Long abstract val outPoint: OutPoint abstract val sequence: UInt + abstract val txOut: TxOut sealed interface Outgoing sealed interface Incoming @@ -102,41 +105,44 @@ sealed class InteractiveTxInput { sealed class Local : InteractiveTxInput(), Outgoing { abstract val previousTx: Transaction abstract val previousTxOutput: Long - abstract val txOut: TxOut + override val txOut: TxOut + get() = previousTx.txOut[previousTxOutput.toInt()] } /** A local-only input that funds the interactive transaction. */ data class LocalOnly(override val serialId: Long, override val previousTx: Transaction, override val previousTxOutput: Long, override val sequence: UInt) : Local() { override val outPoint: OutPoint = OutPoint(previousTx, previousTxOutput) - override val txOut: TxOut = previousTx.txOut[previousTxOutput.toInt()] } /** A local input that funds the interactive transaction, coming from a 2-of-2 swap-in transaction. */ - data class LocalSwapIn(override val serialId: Long, override val previousTx: Transaction, override val previousTxOutput: Long, override val sequence: UInt, val userKey: PublicKey, val serverKey: PublicKey, val refundDelay: Int) : Local() { + data class LocalSwapIn( + override val serialId: Long, + override val previousTx: Transaction, + override val previousTxOutput: Long, + override val sequence: UInt, + val swapInParams: TxAddInputTlv.SwapInParams) : Local() { override val outPoint: OutPoint = OutPoint(previousTx, previousTxOutput) - override val txOut: TxOut = previousTx.txOut[previousTxOutput.toInt()] } /** * A remote input that funds the interactive transaction. * We only keep the data we need from our peer's TxAddInput to avoid storing potentially large messages in our DB. */ - sealed class Remote : InteractiveTxInput(), Incoming { - abstract val txOut: TxOut - } + sealed class Remote : InteractiveTxInput(), Incoming /** A remote-only input that funds the interactive transaction. */ data class RemoteOnly(override val serialId: Long, override val outPoint: OutPoint, override val txOut: TxOut, override val sequence: UInt) : Remote() /** A remote input from a swap-in: our peer needs our signature to build a witness for that input. */ - data class RemoteSwapIn(override val serialId: Long, override val outPoint: OutPoint, override val txOut: TxOut, override val sequence: UInt, val userKey: PublicKey, val serverKey: PublicKey, val refundDelay: Int) : Remote() - - data class RemoteSwapInV2(override val serialId: Long, override val outPoint: OutPoint, val txOuts: List, override val sequence: UInt, val userKey: PublicKey, val serverKey: PublicKey, val refundDelay: Int) : Remote() { - override val txOut: TxOut get() = txOuts[outPoint.index.toInt()] - } + data class RemoteSwapIn( + override val serialId: Long, + override val outPoint: OutPoint, + override val txOut: TxOut, + override val sequence: UInt, + val swapInParams: TxAddInputTlv.SwapInParams) : Remote() /** The shared input can be added by us or by our peer, depending on who initiated the protocol. */ - data class Shared(override val serialId: Long, override val outPoint: OutPoint, override val sequence: UInt, val localAmount: MilliSatoshi, val remoteAmount: MilliSatoshi) : InteractiveTxInput(), Incoming, Outgoing + data class Shared(override val serialId: Long, override val outPoint: OutPoint, override val txOut: TxOut, override val sequence: UInt, val localAmount: MilliSatoshi, val remoteAmount: MilliSatoshi) : InteractiveTxInput(), Incoming, Outgoing } sealed class InteractiveTxOutput { @@ -250,8 +256,17 @@ data class FundingContributions(val inputs: List, v } } } - val sharedInput = sharedUtxo?.let { (i, balances) -> listOf(InteractiveTxInput.Shared(0, i.info.outPoint, 0xfffffffdU, balances.toLocal, balances.toRemote)) } ?: listOf() - val localInputs = walletInputs.map { i -> InteractiveTxInput.LocalSwapIn(0, i.previousTx.stripInputWitnesses(), i.outputIndex.toLong(), 0xfffffffdU, swapInKeys.userPublicKey, swapInKeys.remoteServerPublicKey, swapInKeys.refundDelay) } + val sharedInput = sharedUtxo?.let { (i, balances) -> listOf(InteractiveTxInput.Shared(0, i.info.outPoint, i.info.txOut, 0xfffffffdU, balances.toLocal, balances.toRemote)) } ?: listOf() + val localInputs = walletInputs.map { i -> + val version = if (Script.isPay2wsh(i.previousTx.txOut[i.outputIndex].publicKeyScript.toByteArray())) 1 else 2 + InteractiveTxInput.LocalSwapIn( + 0, + i.previousTx.stripInputWitnesses(), + i.outputIndex.toLong(), + 0xfffffffdU, + TxAddInputTlv.SwapInParams(swapInKeys.userPublicKey, swapInKeys.remoteServerPublicKey, swapInKeys.refundDelay, version) + ) + } return if (params.isInitiator) { Either.Right(sortFundingContributions(params, sharedInput + localInputs, sharedOutput + nonChangeOutputs + changeOutput)) } else { @@ -264,7 +279,7 @@ data class FundingContributions(val inputs: List, v /** Compute the weight we need to pay on-chain fees for. */ private fun computeWeightPaid(isInitiator: Boolean, sharedInput: SharedFundingInput?, sharedOutputScript: ByteVector, walletInputs: List, localOutputs: List): Int { - val walletInputsWeight = walletInputs.size * Transactions.swapInputWeight + val walletInputsWeight = weight(walletInputs) val localOutputsWeight = localOutputs.sumOf { it.weight() } return if (isInitiator) { // The initiator must add the shared input, the shared output and pay for the fees of the common transaction fields. @@ -286,6 +301,13 @@ data class FundingContributions(val inputs: List, v localOutputs ) + fun weight(walletInputs: List): Int = walletInputs.sumOf { + when { + Script.isPay2wsh(it.previousTx.txOut[it.outputIndex].publicKeyScript.toByteArray()) -> Transactions.swapInputWeight + else -> Transactions.swapInputWeightMusig2 + } + } + /** We always randomize the order of inputs and outputs. */ private fun sortFundingContributions(params: InteractiveTxParams, inputs: List, outputs: List): FundingContributions { val sortedInputs = inputs.shuffled().mapIndexed { i, input -> @@ -325,6 +347,13 @@ data class SharedTransaction( val localFees: MilliSatoshi = localAmountIn - localAmountOut val remoteFees: MilliSatoshi = remoteAmountIn - remoteAmountOut val fees: Satoshi = (localFees + remoteFees).truncateToSatoshi() + // tx outputs spent by this transaction + val spentOutputs: Map = run { + val sharedOutput = sharedInput?.let { i -> mapOf(i.outPoint to i.txOut) } ?: mapOf() + val localOutputs = localInputs.associate { i -> i.outPoint to i.txOut } + val remoteOutputs = remoteInputs.associate { i -> i.outPoint to i.txOut } + sharedOutput + localOutputs + remoteOutputs + } fun localOnlyInputs(): List = localInputs.filterIsInstance() @@ -346,27 +375,72 @@ data class SharedTransaction( return Transaction(2, inputs, outputs, lockTime) } - fun sign(keyManager: KeyManager, fundingParams: InteractiveTxParams, localParams: LocalParams, remoteNodeId: PublicKey): PartiallySignedSharedTransaction { + fun sign(session: InteractiveTxSession, keyManager: KeyManager, fundingParams: InteractiveTxParams, localParams: LocalParams, remoteNodeId: PublicKey): PartiallySignedSharedTransaction { val unsignedTx = buildUnsignedTx() val sharedSig = fundingParams.sharedInput?.sign(keyManager.channelKeys(localParams.fundingKeyPath), unsignedTx) + val sharedOutput = fundingParams.sharedInput?.let { i -> mapOf(i.info.outPoint to i.info.txOut) } ?: mapOf() + val localOutputs = localInputs.associate { i -> i.outPoint to i.txOut } + val remoteOutputs = remoteInputs.associate { i -> i.outPoint to i.txOut } + val previousOutputsMap = sharedOutput + localOutputs + remoteOutputs + val previousOutputs = unsignedTx.txIn.map { previousOutputsMap[it.outPoint]!! }.toList() + // If we are swapping funds in, we provide our partial signatures to the corresponding inputs. val swapUserSigs = unsignedTx.txIn.mapIndexed { i, txIn -> localInputs + .filterIsInstance() + .filter { it.swapInParams.version == 1 } .find { txIn.outPoint == it.outPoint } - ?.let { input -> keyManager.swapInOnChainWallet.signSwapInputUser(unsignedTx, i, input.txOut) } + ?.let { input -> keyManager.swapInOnChainWallet.signSwapInputUser(unsignedTx, i, input.previousTx.txOut) } }.filterNotNull() + + val swapUserPartialSigs = unsignedTx.txIn.mapIndexed { i, txIn -> + localInputs + .filterIsInstance() + .filter { it.swapInParams.version == 2 } + .find { txIn.outPoint == it.outPoint } + ?.let { input -> + val userNonce = session.secretNonces[input.serialId] + require(userNonce != null) + require(session.txCompleteReceived != null) + val serverNonce = session.txCompleteReceived.publicNonces[input.serialId] + require(serverNonce != null) + val commonNonce = PublicNonce.aggregate(listOf(userNonce.publicNonce(), serverNonce)) + TxSignatures.Companion.PartialSignature(keyManager.swapInOnChainWallet.signSwapInputUserMusig2(unsignedTx, i, previousOutputs, userNonce, serverNonce), commonNonce) + } + }.filterNotNull() + // If the remote is swapping funds in, they'll need our partial signatures to finalize their witness. val swapServerSigs = unsignedTx.txIn.mapIndexed { i, txIn -> remoteInputs .filterIsInstance() + .filter { it.swapInParams.version == 1 } .find { txIn.outPoint == it.outPoint } ?.let { input -> val serverKey = keyManager.swapInOnChainWallet.localServerPrivateKey(remoteNodeId) - val swapInProtocol = SwapInProtocol(input.userKey, serverKey.publicKey(), input.refundDelay) + val swapInProtocol = SwapInProtocol(input.swapInParams.userKey, serverKey.publicKey(), input.swapInParams.refundDelay) swapInProtocol.signSwapInputServer(unsignedTx, i, input.txOut, serverKey) } }.filterNotNull() - return PartiallySignedSharedTransaction(this, TxSignatures(fundingParams.channelId, unsignedTx, listOf(), sharedSig, swapUserSigs, swapServerSigs)) + + val swapServerPartialSigs = unsignedTx.txIn.mapIndexed { i, txIn -> + remoteInputs + .filterIsInstance() + .filter { it.swapInParams.version == 2 } + .find { txIn.outPoint == it.outPoint } + ?.let { input -> + val serverKey = keyManager.swapInOnChainWallet.localServerPrivateKey(remoteNodeId) + val userNonce = session.secretNonces[input.serialId] + require(userNonce != null) + require(session.txCompleteReceived != null) + val serverNonce = session.txCompleteReceived.publicNonces[input.serialId] + require(serverNonce != null) + val commonNonce = PublicNonce.aggregate(listOf(userNonce.publicNonce(), serverNonce)) + val swapInProtocol = SwapInProtocolMusig2(input.swapInParams.userKey, serverKey.publicKey(), input.swapInParams.refundDelay) + TxSignatures.Companion.PartialSignature(swapInProtocol.signSwapInputServer(unsignedTx, i, previousOutputs, serverNonce, serverKey, userNonce), commonNonce) + } + }.filterNotNull() + + return PartiallySignedSharedTransaction(this, TxSignatures(fundingParams.channelId, unsignedTx, listOf(), sharedSig, swapUserSigs, swapServerSigs, swapUserPartialSigs, swapServerPartialSigs)) } } @@ -383,10 +457,13 @@ data class PartiallySignedSharedTransaction(override val tx: SharedTransaction, override val signedTx = null fun addRemoteSigs(channelKeys: KeyManager.ChannelKeys, fundingParams: InteractiveTxParams, remoteSigs: TxSignatures): FullySignedSharedTransaction? { - if (localSigs.swapInUserSigs.size != tx.localInputs.size) return null + if (localSigs.swapInUserSigs.size != tx.localInputs.filterIsInstance().filter { it.swapInParams.version == 1 }.size) return null + if (localSigs.swapInUserPartialSigs.size != tx.localInputs.filterIsInstance().filter { it.swapInParams.version == 2 }.size) return null + if (remoteSigs.swapInUserSigs.size != tx.remoteSwapInputs().filter { it.swapInParams.version ==1 }.size) return null + if (remoteSigs.swapInUserPartialSigs.size != tx.remoteSwapInputs().filter { it.swapInParams.version ==2 }.size) return null + if (remoteSigs.swapInServerSigs.size != tx.localInputs.filter { it is InteractiveTxInput.LocalSwapIn && it.swapInParams.version == 1}.size) return null + if (remoteSigs.swapInServerPartialSigs.size != tx.localInputs.filter { it is InteractiveTxInput.LocalSwapIn && it.swapInParams.version == 2}.size) return null if (remoteSigs.witnesses.size != tx.remoteOnlyInputs().size) return null - if (remoteSigs.swapInUserSigs.size != tx.remoteSwapInputs().size) return null - if (remoteSigs.swapInServerSigs.size != tx.localInputs.size) return null if (remoteSigs.txId != localSigs.txId) return null val sharedSigs = fundingParams.sharedInput?.let { when (it) { @@ -399,11 +476,7 @@ data class PartiallySignedSharedTransaction(override val tx: SharedTransaction, } } val fullySignedTx = FullySignedSharedTransaction(tx, localSigs, remoteSigs, sharedSigs) - val sharedOutput = fundingParams.sharedInput?.let { i -> mapOf(i.info.outPoint to i.info.txOut) } ?: mapOf() - val localOutputs = tx.localInputs.associate { i -> i.outPoint to i.txOut } - val remoteOutputs = tx.remoteInputs.associate { i -> i.outPoint to i.txOut } - val previousOutputs = sharedOutput + localOutputs + remoteOutputs - return when (runTrying { Transaction.correctlySpends(fullySignedTx.signedTx, previousOutputs, ScriptFlags.STANDARD_SCRIPT_VERIFY_FLAGS) }) { + return when (runTrying { Transaction.correctlySpends(fullySignedTx.signedTx, tx.spentOutputs, ScriptFlags.STANDARD_SCRIPT_VERIFY_FLAGS) }) { is Try.Success -> fullySignedTx is Try.Failure -> null } @@ -414,20 +487,43 @@ data class FullySignedSharedTransaction(override val tx: SharedTransaction, over override val signedTx = run { val sharedTxIn = tx.sharedInput?.let { i -> listOf(Pair(i.serialId, TxIn(i.outPoint, ByteVector.empty, i.sequence.toLong(), sharedSigs ?: ScriptWitness.empty))) } ?: listOf() val localOnlyTxIn = tx.localOnlyInputs().sortedBy { i -> i.serialId }.zip(localSigs.witnesses).map { (i, w) -> Pair(i.serialId, TxIn(OutPoint(i.previousTx, i.previousTxOutput), ByteVector.empty, i.sequence.toLong(), w)) } - val localSwapTxIn = tx.localSwapInputs().sortedBy { i -> i.serialId }.zip(localSigs.swapInUserSigs.zip(remoteSigs.swapInServerSigs)).map { (i, sigs) -> + val localSwapTxIn = tx.localSwapInputs().filter { it.swapInParams.version == 1 }.sortedBy { i -> i.serialId }.zip(localSigs.swapInUserSigs.zip(remoteSigs.swapInServerSigs)).map { (i, sigs) -> val (userSig, serverSig) = sigs - val swapInProtocol = SwapInProtocol(i.userKey, i.serverKey, i.refundDelay) + val swapInProtocol = SwapInProtocol(i.swapInParams) val witness = swapInProtocol.witness(userSig, serverSig) Pair(i.serialId, TxIn(OutPoint(i.previousTx, i.previousTxOutput), ByteVector.empty, i.sequence.toLong(), witness)) } + val localSwapTxInMusig2 = tx.localSwapInputs().filter { it.swapInParams.version == 2 }.sortedBy { i -> i.serialId }.zip(localSigs.swapInUserPartialSigs.zip(remoteSigs.swapInServerPartialSigs)).map { (i, sigs) -> + val (userSig, serverSig) = sigs + val swapInProtocol = SwapInProtocolMusig2(i.swapInParams) + require(userSig.nonce == serverSig.nonce){ "user and server public nonces mismatch for local input ${i.serialId}"} + val commonNonce = userSig.nonce + val unsignedTx = tx.buildUnsignedTx() + val ctx = swapInProtocol.signingCtx(unsignedTx, unsignedTx.txIn.indexOfFirst { it.outPoint == i.outPoint }, unsignedTx.txIn.map { tx.spentOutputs[it.outPoint]!! }, commonNonce) + val commonSig = ctx.partialSigAgg(listOf(userSig.sig, serverSig.sig)) + val witness = swapInProtocol.witness(commonSig) + Pair(i.serialId, TxIn(OutPoint(i.previousTx, i.previousTxOutput), ByteVector.empty, i.sequence.toLong(), witness)) + } + val remoteOnlyTxIn = tx.remoteOnlyInputs().sortedBy { i -> i.serialId }.zip(remoteSigs.witnesses).map { (i, w) -> Pair(i.serialId, TxIn(i.outPoint, ByteVector.empty, i.sequence.toLong(), w)) } - val remoteSwapTxIn = tx.remoteSwapInputs().sortedBy { i -> i.serialId }.zip(remoteSigs.swapInUserSigs.zip(localSigs.swapInServerSigs)).map { (i, sigs) -> + val remoteSwapTxIn = tx.remoteSwapInputs().filter { it.swapInParams.version == 1 }.sortedBy { i -> i.serialId }.zip(remoteSigs.swapInUserSigs.zip(localSigs.swapInServerSigs)).map { (i, sigs) -> val (userSig, serverSig) = sigs - val swapInProtocol = SwapInProtocol(i.userKey, i.serverKey, i.refundDelay) + val swapInProtocol = SwapInProtocol(i.swapInParams.userKey, i.swapInParams.serverKey, i.swapInParams.refundDelay) val witness = swapInProtocol.witness(userSig, serverSig) Pair(i.serialId, TxIn(i.outPoint, ByteVector.empty, i.sequence.toLong(), witness)) } - val inputs = (sharedTxIn + localOnlyTxIn + localSwapTxIn + remoteOnlyTxIn + remoteSwapTxIn).sortedBy { (serialId, _) -> serialId }.map { (_, i) -> i } + val remoteSwapTxInMusig2 = tx.remoteSwapInputs().filter { it.swapInParams.version == 2 }.sortedBy { i -> i.serialId }.zip(remoteSigs.swapInUserPartialSigs.zip(localSigs.swapInServerPartialSigs)).map { (i, sigs) -> + val (userSig, serverSig) = sigs + val swapInProtocol = SwapInProtocolMusig2(i.swapInParams) + require(userSig.nonce == serverSig.nonce){ "user and server public nonces mismatch for remote input ${i.serialId}"} + val commonNonce = userSig.nonce + val unsignedTx = tx.buildUnsignedTx() + val ctx = swapInProtocol.signingCtx(unsignedTx, unsignedTx.txIn.indexOfFirst { it.outPoint == i.outPoint }, unsignedTx.txIn.map { tx.spentOutputs[it.outPoint]!! }, commonNonce) + val commonSig = ctx.partialSigAgg(listOf(userSig.sig, serverSig.sig)) + val witness = swapInProtocol.witness(commonSig) + Pair(i.serialId, TxIn(i.outPoint, ByteVector.empty, i.sequence.toLong(), witness)) + } + val inputs = (sharedTxIn + localOnlyTxIn + localSwapTxIn + localSwapTxInMusig2 + remoteOnlyTxIn + remoteSwapTxIn + remoteSwapTxInMusig2).sortedBy { (serialId, _) -> serialId }.map { (_, i) -> i } val sharedTxOut = listOf(Pair(tx.sharedOutput.serialId, TxOut(tx.sharedOutput.amount, tx.sharedOutput.pubkeyScript))) val localTxOut = tx.localOutputs.map { o -> Pair(o.serialId, TxOut(o.amount, o.pubkeyScript)) } val remoteTxOut = tx.remoteOutputs.map { o -> Pair(o.serialId, TxOut(o.amount, o.pubkeyScript)) } @@ -463,6 +559,7 @@ sealed class InteractiveTxSessionAction { data class InvalidTxWeight(val channelId: ByteVector32, val txId: ByteVector32) : RemoteFailure() { override fun toString(): String = "transaction weight is too big for standardness rules (txId=$txId)" } data class InvalidTxFeerate(val channelId: ByteVector32, val txId: ByteVector32, val targetFeerate: FeeratePerKw, val actualFeerate: FeeratePerKw) : RemoteFailure() { override fun toString(): String = "transaction feerate too low (txId=$txId, targetFeerate=$targetFeerate, actualFeerate=$actualFeerate" } data class InvalidTxDoesNotDoubleSpendPreviousTx(val channelId: ByteVector32, val txId: ByteVector32, val previousTxId: ByteVector32) : RemoteFailure() { override fun toString(): String = "transaction replacement with txId=$txId doesn't double-spend previous attempt (txId=$previousTxId)" } + data class MissingNonce(val channelId: ByteVector32, val serialId: Long): RemoteFailure() { override fun toString(): String = "missing musig2 nonce for input serial_id=$serialId)" } // @formatter:on } @@ -477,10 +574,11 @@ data class InteractiveTxSession( val remoteInputs: List = listOf(), val localOutputs: List = listOf(), val remoteOutputs: List = listOf(), - val txCompleteSent: Boolean = false, - val txCompleteReceived: Boolean = false, + val txCompleteSent: TxComplete? = null, + val txCompleteReceived: TxComplete? = null, val inputsReceivedCount: Int = 0, val outputsReceivedCount: Int = 0, + val secretNonces: Map = mapOf() ) { // Example flow: @@ -514,31 +612,45 @@ data class InteractiveTxSession( previousTxs ) - val isComplete: Boolean = txCompleteSent && txCompleteReceived + val isComplete: Boolean = txCompleteSent != null && txCompleteReceived != null fun send(): Pair { return when (val msg = toSend.firstOrNull()) { null -> { - val txComplete = TxComplete(fundingParams.channelId) - val next = copy(txCompleteSent = true) + // generate a new secret nonce for each musig2 new swapin every time we send TxComplete + val currentNonces = secretNonces + fun userNonce(serialId: Long) = currentNonces.getOrElse(serialId) { SecretNonce.generate(swapInKeys.userPrivateKey, swapInKeys.userPublicKey, null, null, null, randomBytes32()) } + fun serverNonce(serialId: Long, serverKey: PublicKey) = currentNonces.getOrElse(serialId) { SecretNonce.generate(null, serverKey, null, null, null, randomBytes32()) } + val localMusig2SwapIns = localInputs.filterIsInstance().filter { swapInKeys.swapInProtocolMusig2.isMine(it.txOut) } + val localNonces = localMusig2SwapIns.map { it.serialId to userNonce(it.serialId) }.toMap() + val remoteMusig2SwapIns = remoteInputs.filterIsInstance().filter { it.swapInParams.version == 2 } + val remoteNonces = remoteMusig2SwapIns.map { it.serialId to serverNonce(it.serialId, it.swapInParams.serverKey) }.toMap() + val txComplete = TxComplete(fundingParams.channelId, (localNonces + remoteNonces).mapValues { it.value.publicNonce() }) + val next = copy(txCompleteSent = txComplete, secretNonces = localNonces + remoteNonces) if (next.isComplete) { Pair(next, next.validateTx(txComplete)) } else { Pair(next, InteractiveTxSessionAction.SendMessage(txComplete)) } } + is Either.Left -> { - val next = copy(toSend = toSend.tail(), localInputs = localInputs + msg.value, txCompleteSent = false) - val swapInParams = TxAddInputTlv.SwapInParams(swapInKeys.userPublicKey, swapInKeys.remoteServerPublicKey, swapInKeys.refundDelay) + val next = copy(toSend = toSend.tail(), localInputs = localInputs + msg.value, txCompleteSent = null) val txAddInput = when (msg.value) { is InteractiveTxInput.LocalOnly -> TxAddInput(fundingParams.channelId, msg.value.serialId, msg.value.previousTx, msg.value.previousTxOutput, msg.value.sequence) - is InteractiveTxInput.LocalSwapIn -> TxAddInput(fundingParams.channelId, msg.value.serialId, msg.value.previousTx, msg.value.previousTxOutput, msg.value.sequence, TlvStream(swapInParams)) + is InteractiveTxInput.LocalSwapIn -> { + val version = if (swapInKeys.swapInProtocolMusig2.isMine(msg.value.txOut)) 2 else 1 + val swapInParams = TxAddInputTlv.SwapInParams(swapInKeys.userPublicKey, swapInKeys.remoteServerPublicKey, swapInKeys.refundDelay, version) + TxAddInput(fundingParams.channelId, msg.value.serialId, msg.value.previousTx, msg.value.previousTxOutput, msg.value.sequence, TlvStream(swapInParams)) + } + is InteractiveTxInput.Shared -> TxAddInput(fundingParams.channelId, msg.value.serialId, msg.value.outPoint, msg.value.sequence) } Pair(next, InteractiveTxSessionAction.SendMessage(txAddInput)) } + is Either.Right -> { - val next = copy(toSend = toSend.tail(), localOutputs = localOutputs + msg.value, txCompleteSent = false) + val next = copy(toSend = toSend.tail(), localOutputs = localOutputs + msg.value, txCompleteSent = null) val txAddOutput = when (msg.value) { is InteractiveTxOutput.Local -> TxAddOutput(fundingParams.channelId, msg.value.serialId, msg.value.amount, msg.value.pubkeyScript) is InteractiveTxOutput.Shared -> TxAddOutput(fundingParams.channelId, msg.value.serialId, msg.value.amount, msg.value.pubkeyScript) @@ -561,8 +673,9 @@ data class InteractiveTxSession( val expectedSharedOutpoint = fundingParams.sharedInput?.info?.outPoint ?: return Either.Left(InteractiveTxSessionAction.PreviousTxMissing(message.channelId, message.serialId)) val receivedSharedOutpoint = message.sharedInput ?: return Either.Left(InteractiveTxSessionAction.PreviousTxMissing(message.channelId, message.serialId)) if (expectedSharedOutpoint != receivedSharedOutpoint) return Either.Left(InteractiveTxSessionAction.PreviousTxMissing(message.channelId, message.serialId)) - InteractiveTxInput.Shared(message.serialId, receivedSharedOutpoint, message.sequence, previousFunding.toLocal, previousFunding.toRemote) + InteractiveTxInput.Shared(message.serialId, receivedSharedOutpoint, fundingParams.sharedInput.info.txOut, message.sequence, previousFunding.toLocal, previousFunding.toRemote) } + else -> { if (message.previousTx.txOut.size <= message.previousTxOutput) { return Either.Left(InteractiveTxSessionAction.InputOutOfBounds(message.channelId, message.serialId, message.previousTx.txid, message.previousTxOutput)) @@ -579,7 +692,7 @@ data class InteractiveTxSession( val txOut = message.previousTx.txOut[message.previousTxOutput.toInt()] when (message.swapInParams) { null -> InteractiveTxInput.RemoteOnly(message.serialId, outpoint, txOut, message.sequence) - else -> InteractiveTxInput.RemoteSwapIn(message.serialId, outpoint, txOut, message.sequence, message.swapInParams.userKey, message.swapInParams.serverKey, message.swapInParams.refundDelay) + else -> InteractiveTxInput.RemoteSwapIn(message.serialId, outpoint, txOut, message.sequence, message.swapInParams) } } } @@ -618,35 +731,39 @@ data class InteractiveTxSession( is TxAddInput -> { receiveInput(message).fold( { f -> Pair(this, f) }, - { input -> copy(remoteInputs = remoteInputs + input, inputsReceivedCount = inputsReceivedCount + 1, txCompleteReceived = false).send() } + { input -> copy(remoteInputs = remoteInputs + input, inputsReceivedCount = inputsReceivedCount + 1, txCompleteReceived = null).send() } ) } + is TxAddOutput -> { receiveOutput(message).fold( { f -> Pair(this, f) }, - { output -> copy(remoteOutputs = remoteOutputs + output, outputsReceivedCount = outputsReceivedCount + 1, txCompleteReceived = false).send() } + { output -> copy(remoteOutputs = remoteOutputs + output, outputsReceivedCount = outputsReceivedCount + 1, txCompleteReceived = null).send() } ) } + is TxRemoveInput -> { val remoteInputs1 = remoteInputs.filterNot { i -> (i as InteractiveTxInput).serialId == message.serialId } if (remoteInputs.size != remoteInputs1.size) { - val next = copy(remoteInputs = remoteInputs1, txCompleteReceived = false) + val next = copy(remoteInputs = remoteInputs1, txCompleteReceived = null) next.send() } else { Pair(this, InteractiveTxSessionAction.UnknownSerialId(message.channelId, message.serialId)) } } + is TxRemoveOutput -> { val remoteOutputs1 = remoteOutputs.filterNot { o -> (o as InteractiveTxOutput).serialId == message.serialId } if (remoteOutputs.size != remoteOutputs1.size) { - val next = copy(remoteOutputs = remoteOutputs1, txCompleteReceived = false) + val next = copy(remoteOutputs = remoteOutputs1, txCompleteReceived = null) next.send() } else { Pair(this, InteractiveTxSessionAction.UnknownSerialId(message.channelId, message.serialId)) } } + is TxComplete -> { - val next = copy(txCompleteReceived = true) + val next = copy(txCompleteReceived = message) if (next.isComplete) { Pair(next, next.validateTx(null)) } else { @@ -657,6 +774,10 @@ data class InteractiveTxSession( } private fun validateTx(txComplete: TxComplete?): InteractiveTxSessionAction { + // tx_complete MUST have been sent and received for us to reach this state, require is used here to tell the compiler that txCompleteSent and txCompleteReceived are not null + require(txCompleteSent != null) + require(txCompleteReceived != null) + if (localInputs.size + remoteInputs.size > 252 || localOutputs.size + remoteOutputs.size > 252) { return InteractiveTxSessionAction.InvalidTxInputOutputCount(fundingParams.channelId, localInputs.size + remoteInputs.size, localOutputs.size + remoteOutputs.size) } @@ -691,8 +812,32 @@ data class InteractiveTxSession( } sharedInputs.first() } + val localOnlyInputsWithNonces = localOnlyInputs.map { + when { + it is InteractiveTxInput.LocalSwapIn && swapInKeys.swapInProtocolMusig2.isMine(it.txOut) -> { + val userNonce = secretNonces[it.serialId] + val serverNonce = txCompleteReceived.publicNonces[it.serialId] + if (userNonce == null || serverNonce == null) return InteractiveTxSessionAction.MissingNonce(fundingParams.channelId, it.serialId) + it + } + + else -> it + } + } + val remoteOnlyInputsWithNonces = remoteOnlyInputs.map { + when { + it is InteractiveTxInput.RemoteSwapIn && it.swapInParams.version == 2 -> { + val userNonce = secretNonces[it.serialId] + val serverNonce = txCompleteReceived.publicNonces[it.serialId] + if (userNonce == null || serverNonce == null) return InteractiveTxSessionAction.MissingNonce(fundingParams.channelId, it.serialId) + it + } + + else -> it + } + } - val sharedTx = SharedTransaction(sharedInput, sharedOutput, localOnlyInputs, remoteOnlyInputs, localOnlyOutputs, remoteOnlyOutputs, fundingParams.lockTime) + val sharedTx = SharedTransaction(sharedInput, sharedOutput, localOnlyInputsWithNonces, remoteOnlyInputsWithNonces, localOnlyOutputs, remoteOnlyOutputs, fundingParams.lockTime) val tx = sharedTx.buildUnsignedTx() if (sharedTx.localAmountIn < sharedTx.localAmountOut || sharedTx.remoteAmountIn < sharedTx.remoteAmountOut) { return InteractiveTxSessionAction.InvalidTxChangeAmount(fundingParams.channelId, tx.txid) @@ -794,6 +939,7 @@ data class InteractiveTxSigningSession( logger.info { "signedLocalCommitTx=$signedLocalCommitTx" } Pair(this, InteractiveTxSigningSessionAction.AbortFundingAttempt(InvalidCommitmentSignature(fundingParams.channelId, signedLocalCommitTx.tx.txid))) } + is Try.Success -> { val signedLocalCommit = LocalCommit(localCommit.value.index, localCommit.value.spec, PublishableTxs(signedLocalCommitTx, listOf())) if (shouldSignFirst(channelParams, fundingTx.tx)) { @@ -807,6 +953,7 @@ data class InteractiveTxSigningSession( } } } + is Either.Right -> Pair(this, InteractiveTxSigningSessionAction.WaitForTxSigs) } } @@ -830,6 +977,7 @@ data class InteractiveTxSigningSession( data class UnsignedLocalCommit(val index: Long, val spec: CommitmentSpec, val commitTx: Transactions.TransactionWithInputInfo.CommitTx, val htlcTxs: List) fun create( + session: InteractiveTxSession, keyManager: KeyManager, channelParams: ChannelParams, fundingParams: InteractiveTxParams, @@ -879,7 +1027,7 @@ data class InteractiveTxSigningSession( val commitSig = CommitSig(channelParams.channelId, localSigOfRemoteTx, listOf(), TlvStream(CommitSigTlv.AlternativeFeerateSigs(alternativeSigs))) val unsignedLocalCommit = UnsignedLocalCommit(localCommitmentIndex, firstCommitTx.localSpec, firstCommitTx.localCommitTx, listOf()) val remoteCommit = RemoteCommit(remoteCommitmentIndex, firstCommitTx.remoteSpec, firstCommitTx.remoteCommitTx.tx.txid, remotePerCommitmentPoint) - val signedFundingTx = sharedTx.sign(keyManager, fundingParams, channelParams.localParams, channelParams.remoteParams.nodeId) + val signedFundingTx = sharedTx.sign(session, keyManager, fundingParams, channelParams.localParams, channelParams.remoteParams.nodeId) Pair(InteractiveTxSigningSession(fundingParams, fundingTxIndex, signedFundingTx, Either.Left(unsignedLocalCommit), remoteCommit), commitSig) } } @@ -908,7 +1056,14 @@ sealed class RbfStatus { sealed class SpliceStatus { object None : SpliceStatus() data class Requested(val command: ChannelCommand.Commitment.Splice.Request, val spliceInit: SpliceInit) : SpliceStatus() - data class InProgress(val replyTo: CompletableDeferred?, val spliceSession: InteractiveTxSession, val localPushAmount: MilliSatoshi, val remotePushAmount: MilliSatoshi, val origins: List) : SpliceStatus() + data class InProgress( + val replyTo: CompletableDeferred?, + val spliceSession: InteractiveTxSession, + val localPushAmount: MilliSatoshi, + val remotePushAmount: MilliSatoshi, + val origins: List + ) : SpliceStatus() + data class WaitingForSigs(val session: InteractiveTxSigningSession, val origins: List) : SpliceStatus() object Aborted : SpliceStatus() } diff --git a/src/commonMain/kotlin/fr/acinq/lightning/channel/states/Normal.kt b/src/commonMain/kotlin/fr/acinq/lightning/channel/states/Normal.kt index e29abd09c..53ef0d2ad 100644 --- a/src/commonMain/kotlin/fr/acinq/lightning/channel/states/Normal.kt +++ b/src/commonMain/kotlin/fr/acinq/lightning/channel/states/Normal.kt @@ -468,6 +468,7 @@ data class Normal( is InteractiveTxSessionAction.SignSharedTx -> { val parentCommitment = commitments.active.first() val signingSession = InteractiveTxSigningSession.create( + interactiveTxSession, keyManager, commitments.params, spliceStatus.spliceSession.fundingParams, diff --git a/src/commonMain/kotlin/fr/acinq/lightning/channel/states/WaitForFundingConfirmed.kt b/src/commonMain/kotlin/fr/acinq/lightning/channel/states/WaitForFundingConfirmed.kt index 06e9c8f35..c1daa806f 100644 --- a/src/commonMain/kotlin/fr/acinq/lightning/channel/states/WaitForFundingConfirmed.kt +++ b/src/commonMain/kotlin/fr/acinq/lightning/channel/states/WaitForFundingConfirmed.kt @@ -169,6 +169,7 @@ data class WaitForFundingConfirmed( is InteractiveTxSessionAction.SignSharedTx -> { val replacedCommitment = commitments.latest val signingSession = InteractiveTxSigningSession.create( + rbfSession1, keyManager, commitments.params, rbfSession1.fundingParams, diff --git a/src/commonMain/kotlin/fr/acinq/lightning/channel/states/WaitForFundingCreated.kt b/src/commonMain/kotlin/fr/acinq/lightning/channel/states/WaitForFundingCreated.kt index 209848c21..be6da8f1a 100644 --- a/src/commonMain/kotlin/fr/acinq/lightning/channel/states/WaitForFundingCreated.kt +++ b/src/commonMain/kotlin/fr/acinq/lightning/channel/states/WaitForFundingCreated.kt @@ -55,6 +55,7 @@ data class WaitForFundingCreated( is InteractiveTxSessionAction.SignSharedTx -> { val channelParams = ChannelParams(channelId, channelConfig, channelFeatures, localParams, remoteParams, channelFlags) val signingSession = InteractiveTxSigningSession.create( + interactiveTxSession1, keyManager, channelParams, interactiveTxSession.fundingParams, diff --git a/src/commonMain/kotlin/fr/acinq/lightning/crypto/KeyManager.kt b/src/commonMain/kotlin/fr/acinq/lightning/crypto/KeyManager.kt index 1ac72e6f6..7c598cb67 100644 --- a/src/commonMain/kotlin/fr/acinq/lightning/crypto/KeyManager.kt +++ b/src/commonMain/kotlin/fr/acinq/lightning/crypto/KeyManager.kt @@ -3,10 +3,13 @@ package fr.acinq.lightning.crypto import fr.acinq.bitcoin.* import fr.acinq.bitcoin.DeterministicWallet.hardened import fr.acinq.bitcoin.io.ByteArrayInput +import fr.acinq.bitcoin.musig2.PublicNonce +import fr.acinq.bitcoin.musig2.SecretNonce import fr.acinq.lightning.DefaultSwapInParams import fr.acinq.lightning.NodeParams import fr.acinq.lightning.blockchain.fee.FeeratePerKw import fr.acinq.lightning.transactions.SwapInProtocol +import fr.acinq.lightning.transactions.SwapInProtocolMusig2 import fr.acinq.lightning.transactions.Transactions import fr.acinq.lightning.utils.sum import fr.acinq.lightning.utils.toByteVector @@ -128,9 +131,7 @@ interface KeyManager { fun localServerPrivateKey(remoteNodeId: PublicKey): PrivateKey = DeterministicWallet.derivePrivateKey(localServerExtendedPrivateKey, perUserPath(remoteNodeId)).privateKey val swapInProtocol = SwapInProtocol(userPublicKey, remoteServerPublicKey, refundDelay) - val redeemScript: List = swapInProtocol.redeemScript - val pubkeyScript: List = swapInProtocol.pubkeyScript - val address: String = swapInProtocol.address(chain) + val swapInProtocolMusig2 = SwapInProtocolMusig2(userPublicKey, remoteServerPublicKey, refundDelay) /** * The output script descriptor matching our swap-in addresses. @@ -146,13 +147,13 @@ interface KeyManager { "wsh(and_v(v:pk($userKey),or_d(pk(${remoteServerPublicKey.toHex()}),older($refundDelay))))" } - fun signSwapInputUser(fundingTx: Transaction, index: Int, parentTxOut: TxOut): ByteVector64 { - return swapInProtocol.signSwapInputUser(fundingTx, index, parentTxOut, userPrivateKey) + fun signSwapInputUser(fundingTx: Transaction, index: Int, parentTxOuts: List): ByteVector64 { + return swapInProtocol.signSwapInputUser(fundingTx, index, parentTxOuts[fundingTx.txIn[index].outPoint.index.toInt()] , userPrivateKey) } - fun signSwapInputServer(fundingTx: Transaction, index: Int, parentTxOut: TxOut, remoteNodeId: PublicKey): ByteVector64 { - return swapInProtocol.signSwapInputServer(fundingTx, index, parentTxOut, localServerPrivateKey(remoteNodeId)) - } + fun signSwapInputUserMusig2(fundingTx: Transaction, index: Int, parentTxOuts: List, userNonce: SecretNonce, serverNonce: PublicNonce): ByteVector32 { + return swapInProtocolMusig2.signSwapInputUser(fundingTx, index, parentTxOuts, userPrivateKey, userNonce, serverNonce) + } /** * Create a recovery transaction that spends a swap-in transaction after the refund delay has passed @@ -162,7 +163,7 @@ interface KeyManager { * @return a signed transaction that spends our swap-in transaction. It cannot be published until `swapInTx` has enough confirmations */ fun createRecoveryTransaction(swapInTx: Transaction, address: String, feeRate: FeeratePerKw): Transaction? { - val utxos = swapInTx.txOut.filter { it.publicKeyScript.contentEquals(Script.write(pubkeyScript)) } + val utxos = swapInTx.txOut.filter { it.publicKeyScript.contentEquals(Script.write(swapInProtocol.pubkeyScript)) || it.publicKeyScript.contentEquals(Script.write(swapInProtocolMusig2.pubkeyScript))} return if (utxos.isEmpty()) { null } else { @@ -175,17 +176,26 @@ interface KeyManager { txOut = listOf(ourOutput), lockTime = 0 ) - val fees = run { - val recoveryTx = utxos.foldIndexed(unsignedTx) { index, tx, utxo -> + + fun sign(tx: Transaction, index: Int, utxo: TxOut): Transaction { + return if (swapInProtocol.isMine(utxo)) { val sig = swapInProtocol.signSwapInputUser(tx, index, utxo, userPrivateKey) tx.updateWitness(index, swapInProtocol.witnessRefund(sig)) + } else { + val sig = swapInProtocolMusig2.signSwapInputRefund(tx, index, utxos, userPrivateKey) + tx.updateWitness(index, swapInProtocolMusig2.witnessRefund(sig)) + } + } + + val fees = run { + val recoveryTx = utxos.foldIndexed(unsignedTx) { index, tx, utxo -> + sign(tx, index, utxo) } Transactions.weight2fee(feeRate, recoveryTx.weight()) } val unsignedTx1 = unsignedTx.copy(txOut = listOf(ourOutput.copy(amount = ourOutput.amount - fees))) val recoveryTx = utxos.foldIndexed(unsignedTx1) { index, tx, utxo -> - val sig = swapInProtocol.signSwapInputUser(tx, index, utxo, userPrivateKey) - tx.updateWitness(index, swapInProtocol.witnessRefund(sig)) + sign(tx, index, utxo) } // this tx is signed but cannot be published until swapInTx has `refundDelay` confirmations recoveryTx diff --git a/src/commonMain/kotlin/fr/acinq/lightning/io/Peer.kt b/src/commonMain/kotlin/fr/acinq/lightning/io/Peer.kt index 6047e1409..acde94e13 100644 --- a/src/commonMain/kotlin/fr/acinq/lightning/io/Peer.kt +++ b/src/commonMain/kotlin/fr/acinq/lightning/io/Peer.kt @@ -188,7 +188,8 @@ class Peer( val finalAddress: String = nodeParams.keyManager.finalOnChainWallet.address(addressIndex = 0L).also { finalWallet.addAddress(it) } val swapInWallet = ElectrumMiniWallet(nodeParams.chainHash, watcher.client, scope, nodeParams.loggerFactory, name = "swap-in") - val swapInAddress: String = nodeParams.keyManager.swapInOnChainWallet.address.also { swapInWallet.addAddress(it) } + val swapInAddress: String = nodeParams.keyManager.swapInOnChainWallet.swapInProtocol.address(nodeParams.chain).also { swapInWallet.addAddress(it) } + val swapInAddressMusig2: String = nodeParams.keyManager.swapInOnChainWallet.swapInProtocolMusig2.address(nodeParams.chain).also { swapInWallet.addAddress(it) } private var swapInJob: Job? = null @@ -861,7 +862,7 @@ class Peer( peerConnection?.send(Error(msg.temporaryChannelId, "cancelling open due to local liquidity policy")) return } - val fundingFee = Transactions.weight2fee(msg.fundingFeerate, request.walletInputs.size * Transactions.swapInputWeight) + val fundingFee = Transactions.weight2fee(msg.fundingFeerate, FundingContributions.weight(request.walletInputs)) // We have to pay the fees for our inputs, so we deduce them from our funding amount. val fundingAmount = request.walletInputs.balance - fundingFee // We pay the other fees by pushing the corresponding amount @@ -1066,7 +1067,7 @@ class Peer( cmd.requestId, cmd.walletInputs.balance, cmd.walletInputs.size, - cmd.walletInputs.size * Transactions.swapInputWeight, + FundingContributions.weight(cmd.walletInputs), TlvStream(PleaseOpenChannelTlv.GrandParents(grandParents)) ) logger.info { "sending please_open_channel with ${cmd.walletInputs.size} utxos (amount = ${cmd.walletInputs.balance})" } diff --git a/src/commonMain/kotlin/fr/acinq/lightning/serialization/v4/Deserialization.kt b/src/commonMain/kotlin/fr/acinq/lightning/serialization/v4/Deserialization.kt index 56766eb2f..421199718 100644 --- a/src/commonMain/kotlin/fr/acinq/lightning/serialization/v4/Deserialization.kt +++ b/src/commonMain/kotlin/fr/acinq/lightning/serialization/v4/Deserialization.kt @@ -3,6 +3,7 @@ package fr.acinq.lightning.serialization.v4 import fr.acinq.bitcoin.* import fr.acinq.bitcoin.io.ByteArrayInput import fr.acinq.bitcoin.io.Input +import fr.acinq.bitcoin.musig2.PublicNonce import fr.acinq.lightning.CltvExpiryDelta import fr.acinq.lightning.Features import fr.acinq.lightning.ShortChannelId @@ -203,6 +204,15 @@ object Deserialization { 0x01 -> InteractiveTxInput.Shared( serialId = readNumber(), outPoint = readOutPoint(), + txOut = TxOut(Satoshi(0), ByteVector.empty), + sequence = readNumber().toUInt(), + localAmount = readNumber().msat, + remoteAmount = readNumber().msat, + ) + 0x02 -> InteractiveTxInput.Shared( + serialId = readNumber(), + outPoint = readOutPoint(), + txOut = readTxOut(), sequence = readNumber().toUInt(), localAmount = readNumber().msat, remoteAmount = readNumber().msat, @@ -222,9 +232,7 @@ object Deserialization { previousTx = readTransaction(), previousTxOutput = readNumber(), sequence = readNumber().toUInt(), - userKey = readPublicKey(), - serverKey = readPublicKey(), - refundDelay = readNumber().toInt(), + swapInParams = TxAddInputTlv.SwapInParams.read(this), ) else -> error("unknown discriminator $discriminator for class ${InteractiveTxInput.Local::class}") } @@ -241,18 +249,7 @@ object Deserialization { outPoint = readOutPoint(), txOut = TxOut.read(readDelimitedByteArray()), sequence = readNumber().toUInt(), - userKey = readPublicKey(), - serverKey = readPublicKey(), - refundDelay = readNumber().toInt() - ) - 0x03 -> InteractiveTxInput.RemoteSwapInV2( - serialId = readNumber(), - outPoint = readOutPoint(), - txOuts = readCollection { TxOut.read(readDelimitedByteArray()) }.toList(), - sequence = readNumber().toUInt(), - userKey = readPublicKey(), - serverKey = readPublicKey(), - refundDelay = readNumber().toInt() + swapInParams = TxAddInputTlv.SwapInParams.read(this) ) else -> error("unknown discriminator $discriminator for class ${InteractiveTxInput.Remote::class}") } @@ -544,6 +541,8 @@ object Deserialization { private fun Input.readOutPoint(): OutPoint = OutPoint.read(readDelimitedByteArray()) + private fun Input.readTxOut(): TxOut = TxOut.read(readDelimitedByteArray()) + private fun Input.readTransaction(): Transaction = Transaction.read(readDelimitedByteArray()) private fun Input.readTransactionWithInputInfo(): Transactions.TransactionWithInputInfo = when (val discriminator = read()) { @@ -583,6 +582,8 @@ object Deserialization { private fun Input.readPublicKey() = PublicKey(ByteArray(33).also { read(it, 0, it.size) }) + private fun Input.readPublicNonce() = PublicNonce.fromBin(ByteArray(66).also { read(it, 0, it.size) }) + private fun Input.readDelimitedByteArray(): ByteArray { val size = readNumber().toInt() return ByteArray(size).also { read(it, 0, size) } diff --git a/src/commonMain/kotlin/fr/acinq/lightning/serialization/v4/Serialization.kt b/src/commonMain/kotlin/fr/acinq/lightning/serialization/v4/Serialization.kt index 30011ff30..31ef72c6e 100644 --- a/src/commonMain/kotlin/fr/acinq/lightning/serialization/v4/Serialization.kt +++ b/src/commonMain/kotlin/fr/acinq/lightning/serialization/v4/Serialization.kt @@ -3,6 +3,7 @@ package fr.acinq.lightning.serialization.v4 import fr.acinq.bitcoin.* import fr.acinq.bitcoin.io.ByteArrayOutput import fr.acinq.bitcoin.io.Output +import fr.acinq.bitcoin.musig2.PublicNonce import fr.acinq.lightning.FeatureSupport import fr.acinq.lightning.Features import fr.acinq.lightning.channel.* @@ -251,9 +252,10 @@ object Serialization { } private fun Output.writeSharedInteractiveTxInput(i: InteractiveTxInput.Shared) = i.run { - write(0x01) + write(0x02) writeNumber(serialId) writeBtcObject(outPoint) + writeBtcObject(txOut) writeNumber(sequence.toLong()) writeNumber(localAmount.toLong()) writeNumber(remoteAmount.toLong()) @@ -273,9 +275,7 @@ object Serialization { writeBtcObject(previousTx) writeNumber(previousTxOutput) writeNumber(sequence.toLong()) - writePublicKey(userKey) - writePublicKey(serverKey) - writeNumber(refundDelay) + swapInParams.write(this@writeLocalInteractiveTxInput) } } @@ -293,19 +293,7 @@ object Serialization { writeBtcObject(outPoint) writeBtcObject(txOut) writeNumber(sequence.toLong()) - writePublicKey(userKey) - writePublicKey(serverKey) - writeNumber(refundDelay) - } - is InteractiveTxInput.RemoteSwapInV2 -> i.run { - write(0x03) - writeNumber(serialId) - writeBtcObject(outPoint) - writeCollection(i.txOuts) { o -> writeBtcObject(o) } - writeNumber(sequence.toLong()) - writePublicKey(userKey) - writePublicKey(serverKey) - writeNumber(refundDelay) + swapInParams.write(this@writeRemoteInteractiveTxInput) } } @@ -649,6 +637,8 @@ object Serialization { private fun Output.writePublicKey(o: PublicKey) = write(o.value.toByteArray()) + private fun Output.writePublicNonce(o: PublicNonce) = write(o.toByteArray()) + private fun Output.writeDelimited(o: ByteArray) { writeNumber(o.size) write(o) diff --git a/src/commonMain/kotlin/fr/acinq/lightning/transactions/SwapInProtocol.kt b/src/commonMain/kotlin/fr/acinq/lightning/transactions/SwapInProtocol.kt index 297923770..11cd27772 100644 --- a/src/commonMain/kotlin/fr/acinq/lightning/transactions/SwapInProtocol.kt +++ b/src/commonMain/kotlin/fr/acinq/lightning/transactions/SwapInProtocol.kt @@ -7,8 +7,13 @@ import fr.acinq.bitcoin.musig2.SecretNonce import fr.acinq.bitcoin.musig2.SessionCtx import fr.acinq.lightning.Lightning import fr.acinq.lightning.NodeParams +import fr.acinq.lightning.wire.TxAddInputTlv +import org.kodein.log.newLogger class SwapInProtocol(val userPublicKey: PublicKey, val serverPublicKey: PublicKey, val refundDelay: Int) { + + constructor(swapInParams: TxAddInputTlv.SwapInParams) : this(swapInParams.userKey, swapInParams.serverKey, swapInParams.refundDelay) + // This script was generated with https://bitcoin.sipa.be/miniscript/ using the following miniscript policy: // and(pk(),or(99@pk(),older())) // @formatter:off @@ -22,6 +27,8 @@ class SwapInProtocol(val userPublicKey: PublicKey, val serverPublicKey: PublicKe val pubkeyScript: List = Script.pay2wsh(redeemScript) + fun isMine(txOut: TxOut): Boolean = txOut.publicKeyScript.contentEquals(Script.write(pubkeyScript)) + fun address(chain: NodeParams.Chain): String = Bitcoin.addressFromPublicKeyScript(chain.chainHash, pubkeyScript).result!! fun witness(userSig: ByteVector64, serverSig: ByteVector64): ScriptWitness { @@ -43,6 +50,8 @@ class SwapInProtocol(val userPublicKey: PublicKey, val serverPublicKey: PublicKe } class SwapInProtocolMusig2(val userPublicKey: PublicKey, val serverPublicKey: PublicKey, val refundDelay: Int) { + constructor(swapInParams: TxAddInputTlv.SwapInParams) : this(swapInParams.userKey, swapInParams.serverKey, swapInParams.refundDelay) + // the redeem script is just the refund script. it is generated from this policy: and_v(v:pk(user),older(refundDelay)) val redeemScript = listOf(OP_PUSHDATA(userPublicKey.xOnly()), OP_CHECKSIGVERIFY, OP_PUSHDATA(Script.encodeNumber(refundDelay)), OP_CHECKSEQUENCEVERIFY) private val scriptTree = ScriptTree.Leaf(ScriptLeaf(0, Script.write(redeemScript).byteVector(), Script.TAPROOT_LEAF_TAPSCRIPT)) @@ -55,6 +64,8 @@ class SwapInProtocolMusig2(val userPublicKey: PublicKey, val serverPublicKey: Pu private val executionData = Script.ExecutionData(annex = null, tapleafHash = merkleRoot) private val controlBlock = byteArrayOf((Script.TAPROOT_LEAF_TAPSCRIPT + (if (parity) 1 else 0)).toByte()) + internalPubKey.value.toByteArray() + fun isMine(txOut: TxOut): Boolean = txOut.publicKeyScript.contentEquals(Script.write(pubkeyScript)) + fun address(chain: NodeParams.Chain): String = Bitcoin.addressFromPublicKeyScript(chain.chainHash, pubkeyScript).result!! fun witness(commonSig: ByteVector64): ScriptWitness = ScriptWitness(listOf(commonSig)) @@ -64,9 +75,9 @@ class SwapInProtocolMusig2(val userPublicKey: PublicKey, val serverPublicKey: Pu fun signSwapInputUser(fundingTx: Transaction, index: Int, parentTxOuts: List, userPrivateKey: PrivateKey, userNonce: SecretNonce, serverNonce: PublicNonce): ByteVector32 { require(userPrivateKey.publicKey() == userPublicKey) val txHash = Transaction.hashForSigningSchnorr(fundingTx, index, parentTxOuts, SigHash.SIGHASH_DEFAULT, SigVersion.SIGVERSION_TAPROOT) - + val commonNonce = PublicNonce.aggregate(listOf(userNonce.publicNonce(), serverNonce)) val ctx = SessionCtx( - PublicNonce.aggregate(listOf(userNonce.publicNonce(), serverNonce)), + commonNonce, listOf(userPrivateKey.publicKey(), serverPublicKey), listOf(Pair(internalPubKey.tweak(Crypto.TaprootTweak.ScriptTweak(merkleRoot)), true)), txHash @@ -81,9 +92,9 @@ class SwapInProtocolMusig2(val userPublicKey: PublicKey, val serverPublicKey: Pu fun signSwapInputServer(fundingTx: Transaction, index: Int, parentTxOuts: List, userNonce: PublicNonce, serverPrivateKey: PrivateKey, serverNonce: SecretNonce): ByteVector32 { val txHash = Transaction.hashForSigningSchnorr(fundingTx, index, parentTxOuts, SigHash.SIGHASH_DEFAULT, SigVersion.SIGVERSION_TAPROOT) - + val commonNonce = PublicNonce.aggregate(listOf(userNonce, serverNonce.publicNonce())) val ctx = SessionCtx( - PublicNonce.aggregate(listOf(userNonce, serverNonce.publicNonce())), + commonNonce, listOf(userPublicKey, serverPrivateKey.publicKey()), listOf(Pair(internalPubKey.tweak(Crypto.TaprootTweak.ScriptTweak(merkleRoot)), true)), txHash diff --git a/src/commonMain/kotlin/fr/acinq/lightning/transactions/Transactions.kt b/src/commonMain/kotlin/fr/acinq/lightning/transactions/Transactions.kt index 0085ee6c6..65e89c300 100644 --- a/src/commonMain/kotlin/fr/acinq/lightning/transactions/Transactions.kt +++ b/src/commonMain/kotlin/fr/acinq/lightning/transactions/Transactions.kt @@ -159,7 +159,10 @@ object Transactions { * - [[ClaimDelayedOutputPenaltyTx]] spends [[HtlcTimeoutTx]] using the revocation secret (published by local) * - [[HtlcPenaltyTx]] spends competes with [[HtlcSuccessTx]] and [[HtlcTimeoutTx]] for the same outputs (published by local) */ + // legacy swap-in. witness is 2 signatures (73 bytes) + redeem script (77 bytes)) const val swapInputWeight = 392 + // musig2 swap-in. witness is a single Schnorr signature (64 bytes) + const val swapInputWeightMusig2 = 233 // The following values are specific to lightning and used to estimate fees. const val claimP2WPKHOutputWeight = 438 diff --git a/src/commonMain/kotlin/fr/acinq/lightning/wire/InteractiveTxTlv.kt b/src/commonMain/kotlin/fr/acinq/lightning/wire/InteractiveTxTlv.kt index da9906de0..04a91586b 100644 --- a/src/commonMain/kotlin/fr/acinq/lightning/wire/InteractiveTxTlv.kt +++ b/src/commonMain/kotlin/fr/acinq/lightning/wire/InteractiveTxTlv.kt @@ -3,6 +3,7 @@ package fr.acinq.lightning.wire import fr.acinq.bitcoin.* import fr.acinq.bitcoin.io.Input import fr.acinq.bitcoin.io.Output +import fr.acinq.bitcoin.musig2.PublicNonce import fr.acinq.lightning.utils.sat import fr.acinq.lightning.utils.toByteVector import fr.acinq.lightning.utils.toByteVector32 @@ -24,12 +25,13 @@ sealed class TxAddInputTlv : Tlv { } /** When adding a swap-in input to an interactive-tx, the user needs to provide the corresponding script parameters. */ - data class SwapInParams(val userKey: PublicKey, val serverKey: PublicKey, val refundDelay: Int) : TxAddInputTlv() { + data class SwapInParams(val userKey: PublicKey, val serverKey: PublicKey, val refundDelay: Int, val version: Int) : TxAddInputTlv() { override val tag: Long get() = SwapInParams.tag override fun write(out: Output) { LightningCodecs.writeBytes(userKey.value, out) LightningCodecs.writeBytes(serverKey.value, out) LightningCodecs.writeU32(refundDelay, out) + LightningCodecs.writeU32(version, out) } companion object : TlvValueReader { @@ -37,7 +39,8 @@ sealed class TxAddInputTlv : Tlv { override fun read(input: Input): SwapInParams = SwapInParams( PublicKey(LightningCodecs.bytes(input, 33)), PublicKey(LightningCodecs.bytes(input, 33)), - LightningCodecs.u32(input) + LightningCodecs.u32(input), + if (input.availableBytes >= 4) LightningCodecs.u32(input) else 1 ) } } @@ -49,7 +52,32 @@ sealed class TxRemoveInputTlv : Tlv sealed class TxRemoveOutputTlv : Tlv -sealed class TxCompleteTlv : Tlv +sealed class TxCompleteTlv : Tlv { + data class Nonces(val nonces: Map): TxCompleteTlv() { + override val tag: Long get() = Nonces.tag + + override fun write(out: Output) { + LightningCodecs.writeU16(nonces.size, out) + nonces.forEach { (serialId, nonce) -> + LightningCodecs.writeBigSize(serialId, out) + LightningCodecs.writeBytes(nonce.toByteArray(), out) + } + } + + companion object : TlvValueReader { + const val tag: Long = 101 + override fun read(input: Input): Nonces { + val noncesCount = LightningCodecs.u16(input) + val nonces = (1..noncesCount).map { + val serialId = LightningCodecs.bigSize(input) + val nonce = PublicNonce.fromBin(LightningCodecs.bytes(input, 66)) + serialId to nonce + } + return Nonces(nonces.toMap()) + } + } + } +} sealed class TxSignaturesTlv : Tlv { /** When doing a splice, each peer must provide their signature for the previous 2-of-2 funding output. */ @@ -93,6 +121,48 @@ sealed class TxSignaturesTlv : Tlv { } } + data class SwapInUserPartialSigs(val psigs: List) : TxSignaturesTlv() { + override val tag: Long get() = SwapInUserPartialSigs.tag + override fun write(out: Output) = psigs.forEach { psig -> + LightningCodecs.writeBytes(psig.sig, out) + LightningCodecs.writeBytes(psig.nonce.toByteArray(), out) + } + + companion object : TlvValueReader { + const val tag: Long = 607 + override fun read(input: Input): SwapInUserPartialSigs { + val count = input.availableBytes / (32 + 66) + val psigs = (0 until count).map { + val sig = LightningCodecs.bytes(input, 32).byteVector32() + val nonce = PublicNonce.fromBin(LightningCodecs.bytes(input, 66)) + TxSignatures.Companion.PartialSignature(sig, nonce) + } + return SwapInUserPartialSigs(psigs) + } + } + } + + data class SwapInServerPartialSigs(val psigs: List) : TxSignaturesTlv() { + override val tag: Long get() = SwapInServerPartialSigs.tag + override fun write(out: Output) = psigs.forEach { psig -> + LightningCodecs.writeBytes(psig.sig, out) + LightningCodecs.writeBytes(psig.nonce.toByteArray(), out) + } + + companion object : TlvValueReader { + const val tag: Long = 609 + override fun read(input: Input): SwapInServerPartialSigs { + val count = input.availableBytes / (32 + 66) + val psigs = (0 until count).map { + val sig = LightningCodecs.bytes(input, 32).byteVector32() + val nonce = PublicNonce.fromBin(LightningCodecs.bytes(input, 66)) + TxSignatures.Companion.PartialSignature(sig, nonce) + } + return SwapInServerPartialSigs(psigs) + } + } + } + data class ChannelData(val ecb: EncryptedChannelData) : TxSignaturesTlv() { override val tag: Long get() = ChannelData.tag override fun write(out: Output) = LightningCodecs.writeBytes(ecb.data, out) diff --git a/src/commonMain/kotlin/fr/acinq/lightning/wire/LightningMessages.kt b/src/commonMain/kotlin/fr/acinq/lightning/wire/LightningMessages.kt index 5b69146bb..4624a45ce 100644 --- a/src/commonMain/kotlin/fr/acinq/lightning/wire/LightningMessages.kt +++ b/src/commonMain/kotlin/fr/acinq/lightning/wire/LightningMessages.kt @@ -5,6 +5,8 @@ import fr.acinq.bitcoin.io.ByteArrayInput import fr.acinq.bitcoin.io.ByteArrayOutput import fr.acinq.bitcoin.io.Input import fr.acinq.bitcoin.io.Output +import fr.acinq.bitcoin.musig2.PublicNonce +import fr.acinq.bitcoin.musig2.SecretNonce import fr.acinq.lightning.* import fr.acinq.lightning.blockchain.fee.FeeratePerKw import fr.acinq.lightning.channel.ChannelType @@ -448,12 +450,22 @@ data class TxComplete( ) : InteractiveTxConstructionMessage(), HasChannelId { override val type: Long get() = TxComplete.type - override fun write(out: Output) = LightningCodecs.writeBytes(channelId.toByteArray(), out) + val publicNonces: Map = tlvs.get()?.nonces?.toMap() ?: mapOf() + + constructor(channelId: ByteVector32, publicNonces: Map) : this(channelId, TlvStream(TxCompleteTlv.Nonces(publicNonces))) + + override fun write(out: Output) { + LightningCodecs.writeBytes(channelId.toByteArray(), out) + TlvStreamSerializer(false, readers).write(tlvs, out) + } companion object : LightningMessageReader { const val type: Long = 70 - override fun read(input: Input): TxComplete = TxComplete(LightningCodecs.bytes(input, 32).byteVector32()) + @Suppress("UNCHECKED_CAST") + val readers = mapOf(TxCompleteTlv.Nonces.tag to TxCompleteTlv.Nonces.Companion as TlvValueReader) + + override fun read(input: Input): TxComplete = TxComplete(LightningCodecs.bytes(input, 32).byteVector32(), TlvStreamSerializer(false, readers).read(input)) } } @@ -463,7 +475,7 @@ data class TxSignatures( val witnesses: List, val tlvs: TlvStream = TlvStream.empty() ) : InteractiveTxMessage(), HasChannelId, HasEncryptedChannelData { - constructor(channelId: ByteVector32, tx: Transaction, witnesses: List, previousFundingSig: ByteVector64?, swapInUserSigs: List, swapInServerSigs: List) : this( + constructor(channelId: ByteVector32, tx: Transaction, witnesses: List, previousFundingSig: ByteVector64?, swapInUserSigs: List, swapInServerSigs: List, swapInUserPartialSigs: List, swapInServerPartialSigs: List) : this( channelId, tx.hash, witnesses, @@ -472,6 +484,8 @@ data class TxSignatures( previousFundingSig?.let { TxSignaturesTlv.PreviousFundingTxSig(it) }, if (swapInUserSigs.isNotEmpty()) TxSignaturesTlv.SwapInUserSigs(swapInUserSigs) else null, if (swapInServerSigs.isNotEmpty()) TxSignaturesTlv.SwapInServerSigs(swapInServerSigs) else null, + if (swapInUserPartialSigs.isNotEmpty()) TxSignaturesTlv.SwapInUserPartialSigs(swapInUserPartialSigs) else null, + if (swapInServerPartialSigs.isNotEmpty()) TxSignaturesTlv.SwapInServerPartialSigs(swapInServerPartialSigs) else null, ).toSet() ), ) @@ -482,6 +496,8 @@ data class TxSignatures( val previousFundingTxSig: ByteVector64? = tlvs.get()?.sig val swapInUserSigs: List = tlvs.get()?.sigs ?: listOf() val swapInServerSigs: List = tlvs.get()?.sigs ?: listOf() + val swapInUserPartialSigs: List = tlvs.get()?.psigs ?: listOf() + val swapInServerPartialSigs: List = tlvs.get()?.psigs ?: listOf() override val channelData: EncryptedChannelData get() = tlvs.get()?.ecb ?: EncryptedChannelData.empty override fun withNonEmptyChannelData(ecd: EncryptedChannelData): TxSignatures = copy(tlvs = tlvs.addOrUpdate(TxSignaturesTlv.ChannelData(ecd))) @@ -501,11 +517,15 @@ data class TxSignatures( companion object : LightningMessageReader { const val type: Long = 71 + data class PartialSignature(val sig: ByteVector32, val nonce: PublicNonce) + @Suppress("UNCHECKED_CAST") val readers = mapOf( TxSignaturesTlv.PreviousFundingTxSig.tag to TxSignaturesTlv.PreviousFundingTxSig.Companion as TlvValueReader, TxSignaturesTlv.SwapInUserSigs.tag to TxSignaturesTlv.SwapInUserSigs.Companion as TlvValueReader, TxSignaturesTlv.SwapInServerSigs.tag to TxSignaturesTlv.SwapInServerSigs.Companion as TlvValueReader, + TxSignaturesTlv.SwapInUserPartialSigs.tag to TxSignaturesTlv.SwapInUserPartialSigs.Companion as TlvValueReader, + TxSignaturesTlv.SwapInServerPartialSigs.tag to TxSignaturesTlv.SwapInServerPartialSigs.Companion as TlvValueReader, TxSignaturesTlv.ChannelData.tag to TxSignaturesTlv.ChannelData.Companion as TlvValueReader, ) diff --git a/src/commonTest/kotlin/fr/acinq/lightning/channel/InteractiveTxTestsCommon.kt b/src/commonTest/kotlin/fr/acinq/lightning/channel/InteractiveTxTestsCommon.kt index 345f379d5..1ab4b42ec 100644 --- a/src/commonTest/kotlin/fr/acinq/lightning/channel/InteractiveTxTestsCommon.kt +++ b/src/commonTest/kotlin/fr/acinq/lightning/channel/InteractiveTxTestsCommon.kt @@ -74,24 +74,24 @@ class InteractiveTxTestsCommon : LightningTestSuite() { assertTrue(sharedTxB.sharedTx.localFees < sharedTxA.sharedTx.localFees) // Bob sends signatures first as he contributed less than Alice. - val signedTxB = sharedTxB.sharedTx.sign(f.keyManagerB, f.fundingParamsB, f.localParamsB, f.localParamsA.nodeId) + val signedTxB = sharedTxB.sharedTx.sign(bob5, f.keyManagerB, f.fundingParamsB, f.localParamsB, f.localParamsA.nodeId) assertEquals(signedTxB.localSigs.swapInUserSigs.size, 1) assertEquals(signedTxB.localSigs.swapInServerSigs.size, 3) // Alice detects invalid signatures from Bob. val sigsInvalidTxId = signedTxB.localSigs.copy(txHash = randomBytes32()) - assertNull(sharedTxA.sharedTx.sign(f.keyManagerA, f.fundingParamsA, f.localParamsA, f.localParamsB.nodeId).addRemoteSigs(f.channelKeysA, f.fundingParamsA, sigsInvalidTxId)) + assertNull(sharedTxA.sharedTx.sign(alice5, f.keyManagerA, f.fundingParamsA, f.localParamsA, f.localParamsB.nodeId).addRemoteSigs(f.channelKeysA, f.fundingParamsA, sigsInvalidTxId)) val sigsMissingUserSigs = signedTxB.localSigs.copy(tlvs = TlvStream(TxSignaturesTlv.SwapInUserSigs(listOf()), TxSignaturesTlv.SwapInServerSigs(signedTxB.localSigs.swapInServerSigs))) - assertNull(sharedTxA.sharedTx.sign(f.keyManagerA, f.fundingParamsA, f.localParamsA, f.localParamsB.nodeId).addRemoteSigs(f.channelKeysA, f.fundingParamsA, sigsMissingUserSigs)) + assertNull(sharedTxA.sharedTx.sign(alice5, f.keyManagerA, f.fundingParamsA, f.localParamsA, f.localParamsB.nodeId).addRemoteSigs(f.channelKeysA, f.fundingParamsA, sigsMissingUserSigs)) val sigsMissingServerSigs = signedTxB.localSigs.copy(tlvs = TlvStream(TxSignaturesTlv.SwapInUserSigs(signedTxB.localSigs.swapInUserSigs), TxSignaturesTlv.SwapInServerSigs(listOf()))) - assertNull(sharedTxA.sharedTx.sign(f.keyManagerA, f.fundingParamsA, f.localParamsA, f.localParamsB.nodeId).addRemoteSigs(f.channelKeysA, f.fundingParamsA, sigsMissingServerSigs)) + assertNull(sharedTxA.sharedTx.sign(alice5, f.keyManagerA, f.fundingParamsA, f.localParamsA, f.localParamsB.nodeId).addRemoteSigs(f.channelKeysA, f.fundingParamsA, sigsMissingServerSigs)) val sigsInvalidUserSig = signedTxB.localSigs.copy(tlvs = TlvStream(TxSignaturesTlv.SwapInUserSigs(listOf(randomBytes64())), TxSignaturesTlv.SwapInServerSigs(signedTxB.localSigs.swapInServerSigs))) - assertNull(sharedTxA.sharedTx.sign(f.keyManagerA, f.fundingParamsA, f.localParamsA, f.localParamsB.nodeId).addRemoteSigs(f.channelKeysA, f.fundingParamsA, sigsInvalidUserSig)) + assertNull(sharedTxA.sharedTx.sign(alice5, f.keyManagerA, f.fundingParamsA, f.localParamsA, f.localParamsB.nodeId).addRemoteSigs(f.channelKeysA, f.fundingParamsA, sigsInvalidUserSig)) val sigsInvalidServerSig = signedTxB.localSigs.copy(tlvs = TlvStream(TxSignaturesTlv.SwapInUserSigs(signedTxB.localSigs.swapInUserSigs), TxSignaturesTlv.SwapInServerSigs(signedTxB.localSigs.swapInServerSigs.reversed()))) - assertNull(sharedTxA.sharedTx.sign(f.keyManagerA, f.fundingParamsA, f.localParamsA, f.localParamsB.nodeId).addRemoteSigs(f.channelKeysA, f.fundingParamsA, sigsInvalidServerSig)) + assertNull(sharedTxA.sharedTx.sign(alice5, f.keyManagerA, f.fundingParamsA, f.localParamsA, f.localParamsB.nodeId).addRemoteSigs(f.channelKeysA, f.fundingParamsA, sigsInvalidServerSig)) // The resulting transaction is valid and has the right feerate. - val signedTxA = sharedTxA.sharedTx.sign(f.keyManagerA, f.fundingParamsA, f.localParamsA, f.localParamsB.nodeId).addRemoteSigs(f.channelKeysA, f.fundingParamsA, signedTxB.localSigs) + val signedTxA = sharedTxA.sharedTx.sign(alice5, f.keyManagerA, f.fundingParamsA, f.localParamsA, f.localParamsB.nodeId).addRemoteSigs(f.channelKeysA, f.fundingParamsA, signedTxB.localSigs) assertNotNull(signedTxA) assertEquals(signedTxA.localSigs.swapInUserSigs.size, 3) assertEquals(signedTxA.localSigs.swapInServerSigs.size, 1) @@ -146,13 +146,13 @@ class InteractiveTxTestsCommon : LightningTestSuite() { assertTrue(sharedTxB.sharedTx.localFees < sharedTxA.sharedTx.localFees) // Alice sends signatures first as she contributed less than Bob. - val signedTxA = sharedTxA.sharedTx.sign(f.keyManagerA, f.fundingParamsA, f.localParamsA, f.localParamsB.nodeId) + val signedTxA = sharedTxA.sharedTx.sign(alice3, f.keyManagerA, f.fundingParamsA, f.localParamsA, f.localParamsB.nodeId) assertNotNull(signedTxA) assertEquals(signedTxA.localSigs.swapInUserSigs.size, 1) assertEquals(signedTxA.localSigs.swapInServerSigs.size, 1) // The resulting transaction is valid and has the right feerate. - val signedTxB = sharedTxB.sharedTx.sign(f.keyManagerB, f.fundingParamsB, f.localParamsB, f.localParamsA.nodeId).addRemoteSigs(f.channelKeysB, f.fundingParamsB, signedTxA.localSigs) + val signedTxB = sharedTxB.sharedTx.sign(bob3, f.keyManagerB, f.fundingParamsB, f.localParamsB, f.localParamsA.nodeId).addRemoteSigs(f.channelKeysB, f.fundingParamsB, signedTxA.localSigs) assertNotNull(signedTxB) assertEquals(signedTxB.localSigs.swapInUserSigs.size, 1) assertEquals(signedTxB.localSigs.swapInServerSigs.size, 1) @@ -206,13 +206,13 @@ class InteractiveTxTestsCommon : LightningTestSuite() { assertTrue(sharedTxA.sharedTx.remoteFees < sharedTxA.sharedTx.localFees) // Alice contributes more than Bob to the funding output, but Bob's inputs are bigger than Alice's, so Alice must sign first. - val signedTxA = sharedTxA.sharedTx.sign(f.keyManagerA, f.fundingParamsA, f.localParamsA, f.localParamsB.nodeId) + val signedTxA = sharedTxA.sharedTx.sign(alice3, f.keyManagerA, f.fundingParamsA, f.localParamsA, f.localParamsB.nodeId) assertNotNull(signedTxA) assertEquals(signedTxA.localSigs.swapInUserSigs.size, 1) assertEquals(signedTxA.localSigs.swapInServerSigs.size, 1) // The resulting transaction is valid and has the right feerate. - val signedTxB = sharedTxB.sharedTx.sign(f.keyManagerB, f.fundingParamsB, f.localParamsB, f.localParamsA.nodeId).addRemoteSigs(f.channelKeysB, f.fundingParamsB, signedTxA.localSigs) + val signedTxB = sharedTxB.sharedTx.sign(bob3, f.keyManagerB, f.fundingParamsB, f.localParamsB, f.localParamsA.nodeId).addRemoteSigs(f.channelKeysB, f.fundingParamsB, signedTxA.localSigs) assertNotNull(signedTxB) Transaction.correctlySpends(signedTxB.signedTx, (sharedTxA.sharedTx.localInputs + sharedTxB.sharedTx.localInputs).map { it.previousTx }, ScriptFlags.STANDARD_SCRIPT_VERIFY_FLAGS) val feerate = Transactions.fee2rate(signedTxB.tx.fees, signedTxB.signedTx.weight()) @@ -263,13 +263,13 @@ class InteractiveTxTestsCommon : LightningTestSuite() { assertEquals(sharedTxB.sharedTx.remoteFees, 2_800_000.msat) // Bob sends signatures first as he did not contribute at all. - val signedTxB = sharedTxB.sharedTx.sign(f.keyManagerB, f.fundingParamsB, f.localParamsB, f.localParamsA.nodeId) + val signedTxB = sharedTxB.sharedTx.sign(bob4, f.keyManagerB, f.fundingParamsB, f.localParamsB, f.localParamsA.nodeId) assertNotNull(signedTxB) assertEquals(signedTxB.localSigs.swapInUserSigs.size, 0) assertEquals(signedTxB.localSigs.swapInServerSigs.size, 2) // The resulting transaction is valid and has the right feerate. - val signedTxA = sharedTxA.sharedTx.sign(f.keyManagerA, f.fundingParamsA, f.localParamsA, f.localParamsB.nodeId).addRemoteSigs(f.channelKeysA, f.fundingParamsA, signedTxB.localSigs) + val signedTxA = sharedTxA.sharedTx.sign(alice4, f.keyManagerA, f.fundingParamsA, f.localParamsA, f.localParamsB.nodeId).addRemoteSigs(f.channelKeysA, f.fundingParamsA, signedTxB.localSigs) assertNotNull(signedTxA) assertEquals(signedTxA.localSigs.swapInUserSigs.size, 2) assertEquals(signedTxA.localSigs.swapInServerSigs.size, 0) @@ -338,14 +338,14 @@ class InteractiveTxTestsCommon : LightningTestSuite() { assertEquals(sharedTxB.sharedTx.remoteFees, 1_116_000.msat) // Bob sends signatures first as he contributed less than Alice. - val signedTxB = sharedTxB.sharedTx.sign(f.keyManagerB, f.fundingParamsB, f.localParamsB, f.localParamsA.nodeId) + val signedTxB = sharedTxB.sharedTx.sign(bob4, f.keyManagerB, f.fundingParamsB, f.localParamsB, f.localParamsA.nodeId) assertNotNull(signedTxB) assertEquals(signedTxB.localSigs.swapInUserSigs.size, 1) assertEquals(signedTxB.localSigs.swapInServerSigs.size, 1) assertNotNull(signedTxB.localSigs.previousFundingTxSig) // The resulting transaction is valid and has the right feerate. - val signedTxA = sharedTxA.sharedTx.sign(f.keyManagerA, f.fundingParamsA, f.localParamsA, f.localParamsB.nodeId).addRemoteSigs(f.channelKeysA, f.fundingParamsA, signedTxB.localSigs) + val signedTxA = sharedTxA.sharedTx.sign(alice4, f.keyManagerA, f.fundingParamsA, f.localParamsA, f.localParamsB.nodeId).addRemoteSigs(f.channelKeysA, f.fundingParamsA, signedTxB.localSigs) assertNotNull(signedTxA) assertEquals(signedTxA.localSigs.swapInUserSigs.size, 1) assertEquals(signedTxA.localSigs.swapInServerSigs.size, 1) @@ -412,7 +412,7 @@ class InteractiveTxTestsCommon : LightningTestSuite() { assertEquals(sharedTxB.sharedTx.remoteFees, 1_000_000.msat) // Bob sends signatures first as he did not contribute. - val signedTxB = sharedTxB.sharedTx.sign(f.keyManagerB, f.fundingParamsB, f.localParamsB, f.localParamsA.nodeId) + val signedTxB = sharedTxB.sharedTx.sign(bob3, f.keyManagerB, f.fundingParamsB, f.localParamsB, f.localParamsA.nodeId) assertNotNull(signedTxB) assertTrue(signedTxB.localSigs.witnesses.isEmpty()) assertTrue(signedTxB.localSigs.swapInUserSigs.isEmpty()) @@ -420,7 +420,7 @@ class InteractiveTxTestsCommon : LightningTestSuite() { assertNotNull(signedTxB.localSigs.previousFundingTxSig) // The resulting transaction is valid. - val signedTxA = sharedTxA.sharedTx.sign(f.keyManagerA, f.fundingParamsA, f.localParamsA, f.localParamsB.nodeId).addRemoteSigs(f.channelKeysA, f.fundingParamsA, signedTxB.localSigs) + val signedTxA = sharedTxA.sharedTx.sign(alice3, f.keyManagerA, f.fundingParamsA, f.localParamsA, f.localParamsB.nodeId).addRemoteSigs(f.channelKeysA, f.fundingParamsA, signedTxB.localSigs) assertNotNull(signedTxA) assertTrue(signedTxA.localSigs.witnesses.isEmpty()) assertTrue(signedTxA.localSigs.swapInUserSigs.isEmpty()) @@ -492,13 +492,13 @@ class InteractiveTxTestsCommon : LightningTestSuite() { assertEquals(sharedTxB.sharedTx.remoteFees, 1_000_000.msat) // Bob sends signatures first as he did not contribute. - val signedTxB = sharedTxB.sharedTx.sign(f.keyManagerB, f.fundingParamsB, f.localParamsB, f.localParamsA.nodeId) + val signedTxB = sharedTxB.sharedTx.sign(bob5, f.keyManagerB, f.fundingParamsB, f.localParamsB, f.localParamsA.nodeId) assertNotNull(signedTxB) assertTrue(signedTxB.localSigs.swapInUserSigs.isEmpty()) assertNotNull(signedTxB.localSigs.previousFundingTxSig) // The resulting transaction is valid. - val signedTxA = sharedTxA.sharedTx.sign(f.keyManagerA, f.fundingParamsA, f.localParamsA, f.localParamsB.nodeId).addRemoteSigs(f.channelKeysA, f.fundingParamsA, signedTxB.localSigs) + val signedTxA = sharedTxA.sharedTx.sign(alice5, f.keyManagerA, f.fundingParamsA, f.localParamsA, f.localParamsB.nodeId).addRemoteSigs(f.channelKeysA, f.fundingParamsA, signedTxB.localSigs) assertNotNull(signedTxA) assertTrue(signedTxA.localSigs.swapInUserSigs.isEmpty()) assertNotNull(signedTxA.localSigs.previousFundingTxSig) @@ -569,14 +569,14 @@ class InteractiveTxTestsCommon : LightningTestSuite() { assertEquals(sharedTxB.sharedTx.remoteFees, 1_240_000.msat) // Bob sends signatures first as he did not contribute. - val signedTxB = sharedTxB.sharedTx.sign(f.keyManagerB, f.fundingParamsB, f.localParamsB, f.localParamsA.nodeId) + val signedTxB = sharedTxB.sharedTx.sign(bob5, f.keyManagerB, f.fundingParamsB, f.localParamsB, f.localParamsA.nodeId) assertNotNull(signedTxB) assertEquals(signedTxB.localSigs.swapInUserSigs.size, 1) assertEquals(signedTxB.localSigs.swapInServerSigs.size, 1) assertNotNull(signedTxB.localSigs.previousFundingTxSig) // The resulting transaction is valid. - val signedTxA = sharedTxA.sharedTx.sign(f.keyManagerA, f.fundingParamsA, f.localParamsA, f.localParamsB.nodeId).addRemoteSigs(f.channelKeysA, f.fundingParamsA, signedTxB.localSigs) + val signedTxA = sharedTxA.sharedTx.sign(alice5, f.keyManagerA, f.fundingParamsA, f.localParamsA, f.localParamsB.nodeId).addRemoteSigs(f.channelKeysA, f.fundingParamsA, signedTxB.localSigs) assertNotNull(signedTxA) assertEquals(signedTxA.localSigs.swapInUserSigs.size, 1) assertEquals(signedTxA.localSigs.swapInServerSigs.size, 1) @@ -869,10 +869,10 @@ class InteractiveTxTestsCommon : LightningTestSuite() { assertNull(sharedTxB.txComplete) // Alice didn't send her user key, so Bob thinks there aren't any swap inputs - val signedTxB = sharedTxB.sharedTx.sign(f.keyManagerB, f.fundingParamsB, f.localParamsB, f.localParamsA.nodeId) + val signedTxB = sharedTxB.sharedTx.sign(bob3, f.keyManagerB, f.fundingParamsB, f.localParamsB, f.localParamsA.nodeId) assertTrue(signedTxB.localSigs.swapInServerSigs.isEmpty()) // Alice is unable to sign her input since Bob didn't provide his signature. - assertNull(sharedTxA.sharedTx.sign(f.keyManagerA, f.fundingParamsA, f.localParamsA, f.localParamsB.nodeId).addRemoteSigs(f.channelKeysA, f.fundingParamsA, signedTxB.localSigs)) + assertNull(sharedTxA.sharedTx.sign(alice3, f.keyManagerA, f.fundingParamsA, f.localParamsA, f.localParamsB.nodeId).addRemoteSigs(f.channelKeysA, f.fundingParamsA, signedTxB.localSigs)) } @Test @@ -983,7 +983,7 @@ class InteractiveTxTestsCommon : LightningTestSuite() { val f = createFixture(100_000.sat, listOf(120_000.sat), 0.sat, listOf(), FeeratePerKw(5000.sat), 330.sat, 0) val sharedOutput = InteractiveTxOutput.Shared(0, f.fundingParamsA.fundingPubkeyScript(f.channelKeysA), 100_000_000.msat, 0.msat) val previousTx1 = Transaction(2, listOf(), listOf(TxOut(150_000.sat, Script.pay2wpkh(randomKey().publicKey()))), 0) - val previousTx2 = Transaction(2, listOf(), listOf(TxOut(160_000.sat, Script.pay2wpkh(randomKey().publicKey())), TxOut(175_000.sat, Script.pay2wpkh(randomKey().publicKey()))), 0) + val previousTx2 = Transaction(2, listOf(), listOf(TxOut(160_000.sat, Script.pay2wpkh(randomKey().publicKey())), TxOut(200_000.sat, Script.pay2wpkh(randomKey().publicKey()))), 0) val validScript = Script.write(Script.pay2wpkh(randomKey().publicKey())).byteVector() val firstAttempt = FullySignedSharedTransaction( SharedTransaction(null, sharedOutput, listOf(), listOf(InteractiveTxInput.RemoteOnly(2, OutPoint(previousTx1, 0), TxOut(125_000.sat, validScript), 0u)), listOf(), listOf(), 0), @@ -1049,14 +1049,14 @@ class InteractiveTxTestsCommon : LightningTestSuite() { ByteVector("82012088a820add57dfe5277079d069ca4ad4893c96de91f88ffb981fdc6a2a34d5336c66aff87") ) ) - val initiatorSigs = TxSignatures(channelId, unsignedTx, listOf(initiatorWitness), null, listOf(), listOf()) + val initiatorSigs = TxSignatures(channelId, unsignedTx, listOf(initiatorWitness), null, listOf(), listOf(), listOf(), listOf()) val nonInitiatorWitness = ScriptWitness( listOf( ByteVector("304402207de9ba56bb9f641372e805782575ee840a899e61021c8b1572b3ec1d5b5950e9022069e9ba998915dae193d3c25cb89b5e64370e6a3a7755e7f31cf6d7cbc2a49f6d01"), ByteVector("034695f5b7864c580bf11f9f8cb1a94eb336f2ce9ef872d2ae1a90ee276c772484") ) ) - val nonInitiatorSigs = TxSignatures(channelId, unsignedTx, listOf(nonInitiatorWitness), null, listOf(), listOf()) + val nonInitiatorSigs = TxSignatures(channelId, unsignedTx, listOf(nonInitiatorWitness), null, listOf(), listOf(), listOf(), listOf()) val initiatorSignedTx = FullySignedSharedTransaction(initiatorTx, initiatorSigs, nonInitiatorSigs, null) assertEquals(initiatorSignedTx.feerate, FeeratePerKw(262.sat)) val nonInitiatorSignedTx = FullySignedSharedTransaction(nonInitiatorTx, nonInitiatorSigs, initiatorSigs, null) @@ -1216,7 +1216,7 @@ class InteractiveTxTestsCommon : LightningTestSuite() { private fun createWallet(onChainKeys: KeyManager.SwapInOnChainKeys, amounts: List): List { return amounts.map { amount -> val txIn = listOf(TxIn(OutPoint(randomBytes32(), 2), 0)) - val txOut = listOf(TxOut(amount, onChainKeys.pubkeyScript), TxOut(150.sat, Script.pay2wpkh(randomKey().publicKey()))) + val txOut = listOf(TxOut(amount, onChainKeys.swapInProtocol.pubkeyScript), TxOut(150.sat, Script.pay2wpkh(randomKey().publicKey()))) val parentTx = Transaction(2, txIn, txOut, 0) WalletState.Utxo(parentTx, 0, 0) } diff --git a/src/commonTest/kotlin/fr/acinq/lightning/channel/TestsHelper.kt b/src/commonTest/kotlin/fr/acinq/lightning/channel/TestsHelper.kt index 2b012dc39..596e380ec 100644 --- a/src/commonTest/kotlin/fr/acinq/lightning/channel/TestsHelper.kt +++ b/src/commonTest/kotlin/fr/acinq/lightning/channel/TestsHelper.kt @@ -18,6 +18,7 @@ import fr.acinq.lightning.tests.TestConstants import fr.acinq.lightning.transactions.Transactions import fr.acinq.lightning.utils.* import fr.acinq.lightning.wire.* +import io.ktor.util.reflect.* import kotlinx.serialization.encodeToString import org.kodein.log.LoggerFactory import org.kodein.log.newLogger @@ -120,6 +121,9 @@ data class LNChannel( val serialized = Serialization.serialize(state) val deserialized = Serialization.deserialize(serialized).value + if (deserialized != state) { + error("serialization error") + } assertEquals(removeRbfAttempt(state), deserialized, "serialization error") } @@ -406,7 +410,7 @@ object TestsHelper { } fun createWallet(keyManager: KeyManager, amount: Satoshi): Pair> { - val (privateKey, script) = keyManager.swapInOnChainWallet.run { Pair(userPrivateKey, pubkeyScript) } + val (privateKey, script) = keyManager.swapInOnChainWallet.run { Pair(userPrivateKey, swapInProtocolMusig2.pubkeyScript) } val parentTx = Transaction(2, listOf(TxIn(OutPoint(randomBytes32(), 3), 0)), listOf(TxOut(amount, script)), 0) return privateKey to listOf(WalletState.Utxo(parentTx, 0, 42)) } diff --git a/src/commonTest/kotlin/fr/acinq/lightning/channel/states/SpliceTestsCommon.kt b/src/commonTest/kotlin/fr/acinq/lightning/channel/states/SpliceTestsCommon.kt index 3b6110b4e..deb84283b 100644 --- a/src/commonTest/kotlin/fr/acinq/lightning/channel/states/SpliceTestsCommon.kt +++ b/src/commonTest/kotlin/fr/acinq/lightning/channel/states/SpliceTestsCommon.kt @@ -1158,7 +1158,7 @@ class SpliceTestsCommon : LightningTestSuite() { } private fun createWalletWithFunds(keyManager: KeyManager, amounts: List): List { - val script = keyManager.swapInOnChainWallet.pubkeyScript + val script = keyManager.swapInOnChainWallet.swapInProtocol.pubkeyScript return amounts.map { amount -> val txIn = listOf(TxIn(OutPoint(Lightning.randomBytes32(), 2), 0)) val txOut = listOf(TxOut(amount, script), TxOut(150.sat, Script.pay2wpkh(randomKey().publicKey()))) diff --git a/src/commonTest/kotlin/fr/acinq/lightning/channel/states/WaitForFundingConfirmedTestsCommon.kt b/src/commonTest/kotlin/fr/acinq/lightning/channel/states/WaitForFundingConfirmedTestsCommon.kt index ca278662e..0fac7b37c 100644 --- a/src/commonTest/kotlin/fr/acinq/lightning/channel/states/WaitForFundingConfirmedTestsCommon.kt +++ b/src/commonTest/kotlin/fr/acinq/lightning/channel/states/WaitForFundingConfirmedTestsCommon.kt @@ -443,7 +443,7 @@ class WaitForFundingConfirmedTestsCommon : LightningTestSuite() { val previousFundingTx = alice.state.latestFundingTx.sharedTx assertIs(previousFundingTx) // Alice adds a new input that increases her contribution and covers the additional fees. - val script = alice.staticParams.nodeParams.keyManager.swapInOnChainWallet.pubkeyScript + val script = alice.staticParams.nodeParams.keyManager.swapInOnChainWallet.swapInProtocol.pubkeyScript val parentTx = Transaction(2, listOf(TxIn(OutPoint(randomBytes32(), 1), 0)), listOf(TxOut(30_000.sat, script)), 0) val wallet1 = wallet + listOf(WalletState.Utxo(parentTx, 0, 42)) return ChannelCommand.Funding.BumpFundingFee(previousFundingTx.feerate * 1.1, previousFundingParams.localContribution + 20_000.sat, wallet1, previousFundingTx.tx.lockTime + 1) diff --git a/src/commonTest/kotlin/fr/acinq/lightning/crypto/LocalKeyManagerTestsCommon.kt b/src/commonTest/kotlin/fr/acinq/lightning/crypto/LocalKeyManagerTestsCommon.kt index 9bd0811a5..b3108165d 100644 --- a/src/commonTest/kotlin/fr/acinq/lightning/crypto/LocalKeyManagerTestsCommon.kt +++ b/src/commonTest/kotlin/fr/acinq/lightning/crypto/LocalKeyManagerTestsCommon.kt @@ -2,6 +2,7 @@ package fr.acinq.lightning.crypto import fr.acinq.bitcoin.* import fr.acinq.bitcoin.crypto.Pack +import fr.acinq.lightning.Lightning.randomKey import fr.acinq.lightning.NodeParams import fr.acinq.lightning.blockchain.fee.FeeratePerByte import fr.acinq.lightning.blockchain.fee.FeeratePerKw @@ -195,12 +196,16 @@ class LocalKeyManagerTestsCommon : LightningTestSuite() { val swapInTx = Transaction(version = 2, txIn = listOf(), txOut = listOf( - TxOut(Satoshi(100000), Bitcoin.addressToPublicKeyScript(Block.RegtestGenesisBlock.hash, TestConstants.Alice.keyManager.swapInOnChainWallet.address).result!!), - TxOut(Satoshi(150000), Bitcoin.addressToPublicKeyScript(Block.RegtestGenesisBlock.hash, TestConstants.Alice.keyManager.swapInOnChainWallet.address).result!!) + TxOut(Satoshi(100000), TestConstants.Alice.keyManager.swapInOnChainWallet.swapInProtocol.pubkeyScript), + TxOut(Satoshi(150000), TestConstants.Alice.keyManager.swapInOnChainWallet.swapInProtocol.pubkeyScript), + TxOut(Satoshi(150000), Script.pay2wpkh(randomKey().publicKey())), + TxOut(Satoshi(100000), TestConstants.Alice.keyManager.swapInOnChainWallet.swapInProtocolMusig2.pubkeyScript), + TxOut(Satoshi(150000), TestConstants.Alice.keyManager.swapInOnChainWallet.swapInProtocolMusig2.pubkeyScript), + TxOut(Satoshi(150000), Script.pay2wpkh(randomKey().publicKey())) ), lockTime = 0) val recoveryTx = TestConstants.Alice.keyManager.swapInOnChainWallet.createRecoveryTransaction(swapInTx, TestConstants.Alice.keyManager.finalOnChainWallet.address(0), FeeratePerKw(FeeratePerByte(Satoshi(5))))!! - assertEquals(swapInTx.txOut.size, recoveryTx.txIn.size) + assertEquals(4, recoveryTx.txIn.size) Transaction.correctlySpends(recoveryTx, swapInTx, ScriptFlags.STANDARD_SCRIPT_VERIFY_FLAGS) } diff --git a/src/commonTest/kotlin/fr/acinq/lightning/serialization/StateSerializationTestsCommon.kt b/src/commonTest/kotlin/fr/acinq/lightning/serialization/StateSerializationTestsCommon.kt index 0f7117262..95b271179 100644 --- a/src/commonTest/kotlin/fr/acinq/lightning/serialization/StateSerializationTestsCommon.kt +++ b/src/commonTest/kotlin/fr/acinq/lightning/serialization/StateSerializationTestsCommon.kt @@ -13,6 +13,7 @@ import fr.acinq.lightning.utils.value import fr.acinq.lightning.wire.CommitSig import fr.acinq.lightning.wire.EncryptedChannelData import fr.acinq.lightning.wire.LightningMessage +import fr.acinq.lightning.wire.TxSignatures import fr.acinq.secp256k1.Hex import kotlin.math.max import kotlin.test.* diff --git a/src/commonTest/kotlin/fr/acinq/lightning/transactions/TransactionsTestsCommon.kt b/src/commonTest/kotlin/fr/acinq/lightning/transactions/TransactionsTestsCommon.kt index 824e4ba11..498096f82 100644 --- a/src/commonTest/kotlin/fr/acinq/lightning/transactions/TransactionsTestsCommon.kt +++ b/src/commonTest/kotlin/fr/acinq/lightning/transactions/TransactionsTestsCommon.kt @@ -53,6 +53,7 @@ import fr.acinq.lightning.transactions.Transactions.makeHtlcTxs import fr.acinq.lightning.transactions.Transactions.makeMainPenaltyTx import fr.acinq.lightning.transactions.Transactions.sign import fr.acinq.lightning.transactions.Transactions.swapInputWeight +import fr.acinq.lightning.transactions.Transactions.swapInputWeightMusig2 import fr.acinq.lightning.transactions.Transactions.weight2fee import fr.acinq.lightning.utils.* import fr.acinq.lightning.wire.UpdateAddHtlc @@ -449,7 +450,7 @@ class TransactionsTestsCommon : LightningTestSuite() { val swapInTx = Transaction( version = 2, txIn = listOf(TxIn(OutPoint(randomBytes32(), 2), 0)), - txOut = listOf(TxOut(100_000.sat, userWallet.pubkeyScript)), + txOut = listOf(TxOut(100_000.sat, userWallet.swapInProtocol.pubkeyScript)), lockTime = 0 ) // The transaction can be spent if the user and the server produce a signature. @@ -460,7 +461,7 @@ class TransactionsTestsCommon : LightningTestSuite() { txOut = listOf(TxOut(90_000.sat, pay2wpkh(randomKey().publicKey()))), lockTime = 0 ) - val userSig = userWallet.signSwapInputUser(fundingTx, 0, swapInTx.txOut.first()) + val userSig = userWallet.signSwapInputUser(fundingTx, 0, swapInTx.txOut) val serverKey = TestConstants.Bob.keyManager.swapInOnChainWallet.localServerPrivateKey(TestConstants.Alice.nodeParams.nodeId) val serverSig = userWallet.swapInProtocol.signSwapInputServer(fundingTx, 0, swapInTx.txOut.first(), serverKey) val witness = userWallet.swapInProtocol.witness(userSig, serverSig) @@ -475,7 +476,7 @@ class TransactionsTestsCommon : LightningTestSuite() { txOut = listOf(TxOut(90_000.sat, pay2wpkh(randomKey().publicKey()))), lockTime = 0 ) - val userSig = userWallet.signSwapInputUser(fundingTx, 0, swapInTx.txOut.first()) + val userSig = userWallet.signSwapInputUser(fundingTx, 0, swapInTx.txOut) val witness = userWallet.swapInProtocol.witnessRefund(userSig) val signedTx = fundingTx.updateWitness(0, witness) Transaction.correctlySpends(signedTx, listOf(swapInTx), ScriptFlags.STANDARD_SCRIPT_VERIFY_FLAGS) @@ -582,7 +583,7 @@ class TransactionsTestsCommon : LightningTestSuite() { val serverSig = swapInProtocolMusig2.signSwapInputServer(tx, 0, swapInTx.txOut, userNonce.publicNonce(), serverPrivateKey, serverNonce) val ctx = swapInProtocolMusig2.signingCtx(tx, 0, swapInTx.txOut, PublicNonce.aggregate(listOf(userNonce.publicNonce(), serverNonce.publicNonce()))) val commonSig = ctx.partialSigAgg(listOf(userSig, serverSig)) - val signedTx = tx.updateWitness(0, ScriptWitness(listOf(commonSig))) + val signedTx = tx.updateWitness(0, swapInProtocolMusig2.witness(commonSig)) Transaction.correctlySpends(signedTx, swapInTx, ScriptFlags.STANDARD_SCRIPT_VERIFY_FLAGS) } @@ -614,6 +615,20 @@ class TransactionsTestsCommon : LightningTestSuite() { assertEquals(inputWeight, swapInputWeight) } + @Test + fun `swap-in input weight -- musig2 version`() { + val pubkey = randomKey().publicKey() + // DER-encoded ECDSA signatures usually take up to 72 bytes. + val sig = ByteVector64.fromValidHex("90b658d172a51f1b3f1a2becd30942397f5df97da8cd2c026854607e955ad815ccfd87d366e348acc32aaf15ff45263aebbb7ecc913a0e5999133f447aee828c") + val tx = Transaction(2, listOf(TxIn(OutPoint(ByteVector32.Zeroes, 2), 0)), listOf(TxOut(50_000.sat, pay2wpkh(pubkey))), 0) + val swapInProtocol = SwapInProtocolMusig2(pubkey, pubkey, 144) + val witness = swapInProtocol.witness(sig) + val swapInput = TxIn(OutPoint(ByteVector32.Zeroes, 3), ByteVector.empty, 0, witness) + val txWithAdditionalInput = tx.copy(txIn = tx.txIn + listOf(swapInput)) + val inputWeight = txWithAdditionalInput.weight() - tx.weight() + assertEquals(inputWeight, swapInputWeightMusig2) + } + @Test fun `sort the htlc outputs using BIP69 and cltv expiry`() { val localFundingPriv = PrivateKey.fromHex("a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1") diff --git a/src/commonTest/kotlin/fr/acinq/lightning/wire/LightningCodecsTestsCommon.kt b/src/commonTest/kotlin/fr/acinq/lightning/wire/LightningCodecsTestsCommon.kt index 7753c6df7..ca15ad23a 100644 --- a/src/commonTest/kotlin/fr/acinq/lightning/wire/LightningCodecsTestsCommon.kt +++ b/src/commonTest/kotlin/fr/acinq/lightning/wire/LightningCodecsTestsCommon.kt @@ -3,6 +3,8 @@ package fr.acinq.lightning.wire import fr.acinq.bitcoin.* import fr.acinq.bitcoin.io.ByteArrayInput import fr.acinq.bitcoin.io.ByteArrayOutput +import fr.acinq.bitcoin.musig2.PublicNonce +import fr.acinq.bitcoin.musig2.SecretNonce import fr.acinq.lightning.* import fr.acinq.lightning.Lightning.randomBytes import fr.acinq.lightning.Lightning.randomBytes32 @@ -374,6 +376,12 @@ class LightningCodecsTestsCommon : LightningTestSuite() { ByteVector64("c49269a9baa73a5ec44b63bdcaabf9c7c6477f72866b822f8502e5c989aa3562fe69d72bec62025d3474b9c2d947ec6d68f9f577be5fab8ee80503cefd8846c3"), ByteVector64("2dadacd65b585e4061421b5265ff543e2a7bdc4d4a7fea932727426bdc53db252a2f914ea1fcbd580b80cdea60226f63288cd44bd84a8850c9189a24f08c7cc5"), ) + val pubKey1 = PrivateKey.fromHex("aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa").publicKey() + val pubKey2 = PrivateKey.fromHex("bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb").publicKey() + val swapInPartialSignatures = listOf( + TxSignatures.Companion.PartialSignature(ByteVector32("cccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccc"), PublicNonce(pubKey1, pubKey2)), + TxSignatures.Companion.PartialSignature(ByteVector32("dddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddd"), PublicNonce(pubKey1, pubKey2)) + ) val signature = ByteVector64("aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaabbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb") // This is a random mainnet transaction. val tx1 = Transaction.read( @@ -383,26 +391,29 @@ class LightningCodecsTestsCommon : LightningTestSuite() { val tx2 = Transaction.read( "0200000000010142180a8812fc79a3da7fb2471eff3e22d7faee990604c2ba7f2fc8dfb15b550a0200000000feffffff030f241800000000001976a9146774040642a78ca3b8b395e70f8391b21ec026fc88ac4a155801000000001600148d2e0b57adcb8869e603fd35b5179caf053361253b1d010000000000160014e032f4f4b9f8611df0d30a20648c190c263bbc33024730440220506005aa347f5b698542cafcb4f1a10250aeb52a609d6fd67ef68f9c1a5d954302206b9bb844343f4012bccd9d08a0f5430afb9549555a3252e499be7df97aae477a012103976d6b3eea3de4b056cd88cdfd50a22daf121e0fb5c6e45ba0f40e1effbd275a00000000" ) + val testCases = listOf( // @formatter:off TxAddInput(channelId1, 561, tx1, 1, 5u) to ByteVector("0042 aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa 0000000000000231 00f7 020000000001014ade359c5deb7c1cde2e94f401854658f97d7fa31c17ce9a831db253120a0a410100000017160014eb9a5bd79194a23d19d6ec473c768fb74f9ed32cffffffff021ca408000000000017a914946118f24bb7b37d5e9e39579e4a411e70f5b6a08763e703000000000017a9143638b2602d11f934c04abc6adb1494f69d1f14af8702473044022059ddd943b399211e4266a349f26b3289979e29f9b067792c6cfa8cc5ae25f44602204d627a5a5b603d0562e7969011fb3d64908af90a3ec7c876eaa9baf61e1958af012102f5188df1da92ed818581c29778047800ed6635788aa09d9469f7d17628f7323300000000 00000001 00000005"), TxAddInput(channelId2, 0, tx2, 2, 0u) to ByteVector("0042 bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb 0000000000000000 0100 0200000000010142180a8812fc79a3da7fb2471eff3e22d7faee990604c2ba7f2fc8dfb15b550a0200000000feffffff030f241800000000001976a9146774040642a78ca3b8b395e70f8391b21ec026fc88ac4a155801000000001600148d2e0b57adcb8869e603fd35b5179caf053361253b1d010000000000160014e032f4f4b9f8611df0d30a20648c190c263bbc33024730440220506005aa347f5b698542cafcb4f1a10250aeb52a609d6fd67ef68f9c1a5d954302206b9bb844343f4012bccd9d08a0f5430afb9549555a3252e499be7df97aae477a012103976d6b3eea3de4b056cd88cdfd50a22daf121e0fb5c6e45ba0f40e1effbd275a00000000 00000002 00000000"), TxAddInput(channelId1, 561, tx1, 0, 0xfffffffdu) to ByteVector("0042 aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa 0000000000000231 00f7 020000000001014ade359c5deb7c1cde2e94f401854658f97d7fa31c17ce9a831db253120a0a410100000017160014eb9a5bd79194a23d19d6ec473c768fb74f9ed32cffffffff021ca408000000000017a914946118f24bb7b37d5e9e39579e4a411e70f5b6a08763e703000000000017a9143638b2602d11f934c04abc6adb1494f69d1f14af8702473044022059ddd943b399211e4266a349f26b3289979e29f9b067792c6cfa8cc5ae25f44602204d627a5a5b603d0562e7969011fb3d64908af90a3ec7c876eaa9baf61e1958af012102f5188df1da92ed818581c29778047800ed6635788aa09d9469f7d17628f7323300000000 00000000 fffffffd"), TxAddInput(channelId1, 561, OutPoint(tx1, 1), 5u) to ByteVector("0042 aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa 0000000000000231 0000 00000001 00000005 fd0451201f2ec025a33e39ef8e177afcdc1adc855bf128dc906182255aeb64efa825f106"), - TxAddInput(channelId1, 561, tx1, 1, 5u, TlvStream(TxAddInputTlv.SwapInParams(swapInUserKey, swapInServerKey, swapInRefundDelay))) to ByteVector("0042 aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa 0000000000000231 00f7 020000000001014ade359c5deb7c1cde2e94f401854658f97d7fa31c17ce9a831db253120a0a410100000017160014eb9a5bd79194a23d19d6ec473c768fb74f9ed32cffffffff021ca408000000000017a914946118f24bb7b37d5e9e39579e4a411e70f5b6a08763e703000000000017a9143638b2602d11f934c04abc6adb1494f69d1f14af8702473044022059ddd943b399211e4266a349f26b3289979e29f9b067792c6cfa8cc5ae25f44602204d627a5a5b603d0562e7969011fb3d64908af90a3ec7c876eaa9baf61e1958af012102f5188df1da92ed818581c29778047800ed6635788aa09d9469f7d17628f7323300000000 00000001 00000005 fd04534603462779ad4aad39514614751a71085f2f10e1c7a593e4e030efb5b8721ce55b0b03864ef025fde8fb587d989186ce6a4a186895ee44a926bfc370e2c366597a3f8f00000090"), + TxAddInput(channelId1, 561, tx1, 1, 5u, TlvStream(TxAddInputTlv.SwapInParams(swapInUserKey, swapInServerKey, swapInRefundDelay, 1))) to ByteVector("0042 aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa 0000000000000231 00f7 020000000001014ade359c5deb7c1cde2e94f401854658f97d7fa31c17ce9a831db253120a0a410100000017160014eb9a5bd79194a23d19d6ec473c768fb74f9ed32cffffffff021ca408000000000017a914946118f24bb7b37d5e9e39579e4a411e70f5b6a08763e703000000000017a9143638b2602d11f934c04abc6adb1494f69d1f14af8702473044022059ddd943b399211e4266a349f26b3289979e29f9b067792c6cfa8cc5ae25f44602204d627a5a5b603d0562e7969011fb3d64908af90a3ec7c876eaa9baf61e1958af012102f5188df1da92ed818581c29778047800ed6635788aa09d9469f7d17628f7323300000000 00000001 00000005 fd04534a03462779ad4aad39514614751a71085f2f10e1c7a593e4e030efb5b8721ce55b0b03864ef025fde8fb587d989186ce6a4a186895ee44a926bfc370e2c366597a3f8f0000009000000001"), TxAddOutput(channelId1, 1105, 2047.sat, ByteVector("00149357014afd0ccd265658c9ae81efa995e771f472")) to ByteVector("0043 aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa 0000000000000451 00000000000007ff 0016 00149357014afd0ccd265658c9ae81efa995e771f472"), TxRemoveInput(channelId2, 561) to ByteVector("0044 bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb 0000000000000231"), TxRemoveOutput(channelId1, 1) to ByteVector("0045 aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa 0000000000000001"), TxComplete(channelId1) to ByteVector("0046 aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa"), - TxSignatures(channelId1, tx2, listOf(ScriptWitness(listOf(ByteVector("68656c6c6f2074686572652c2074686973206973206120626974636f6e212121"), ByteVector("82012088a820add57dfe5277079d069ca4ad4893c96de91f88ffb981fdc6a2a34d5336c66aff87"))), ScriptWitness(listOf(ByteVector("304402207de9ba56bb9f641372e805782575ee840a899e61021c8b1572b3ec1d5b5950e9022069e9ba998915dae193d3c25cb89b5e64370e6a3a7755e7f31cf6d7cbc2a49f6d01"), ByteVector("034695f5b7864c580bf11f9f8cb1a94eb336f2ce9ef872d2ae1a90ee276c772484")))), null, listOf(), listOf()) to ByteVector("0047 aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa fc7aa8845f192959202c1b7ff704e7cbddded463c05e844676a94ccb4bed69f1 0002 004a 022068656c6c6f2074686572652c2074686973206973206120626974636f6e2121212782012088a820add57dfe5277079d069ca4ad4893c96de91f88ffb981fdc6a2a34d5336c66aff87 006b 0247304402207de9ba56bb9f641372e805782575ee840a899e61021c8b1572b3ec1d5b5950e9022069e9ba998915dae193d3c25cb89b5e64370e6a3a7755e7f31cf6d7cbc2a49f6d0121034695f5b7864c580bf11f9f8cb1a94eb336f2ce9ef872d2ae1a90ee276c772484"), - TxSignatures(channelId2, tx1, listOf(), null, listOf(), listOf()) to ByteVector("0047 bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb 1f2ec025a33e39ef8e177afcdc1adc855bf128dc906182255aeb64efa825f106 0000"), - TxSignatures(channelId2, tx1, listOf(), null, swapInSignatures, listOf()) to ByteVector("0047 bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb 1f2ec025a33e39ef8e177afcdc1adc855bf128dc906182255aeb64efa825f106 0000 fd025b 80 c49269a9baa73a5ec44b63bdcaabf9c7c6477f72866b822f8502e5c989aa3562fe69d72bec62025d3474b9c2d947ec6d68f9f577be5fab8ee80503cefd8846c3 2dadacd65b585e4061421b5265ff543e2a7bdc4d4a7fea932727426bdc53db252a2f914ea1fcbd580b80cdea60226f63288cd44bd84a8850c9189a24f08c7cc5"), - TxSignatures(channelId2, tx1, listOf(), null, listOf(), swapInSignatures) to ByteVector("0047 bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb 1f2ec025a33e39ef8e177afcdc1adc855bf128dc906182255aeb64efa825f106 0000 fd025d 80 c49269a9baa73a5ec44b63bdcaabf9c7c6477f72866b822f8502e5c989aa3562fe69d72bec62025d3474b9c2d947ec6d68f9f577be5fab8ee80503cefd8846c3 2dadacd65b585e4061421b5265ff543e2a7bdc4d4a7fea932727426bdc53db252a2f914ea1fcbd580b80cdea60226f63288cd44bd84a8850c9189a24f08c7cc5"), - TxSignatures(channelId2, tx1, listOf(), null, swapInSignatures.take(1), swapInSignatures.drop(1)) to ByteVector("0047 bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb 1f2ec025a33e39ef8e177afcdc1adc855bf128dc906182255aeb64efa825f106 0000 fd025b 40 c49269a9baa73a5ec44b63bdcaabf9c7c6477f72866b822f8502e5c989aa3562fe69d72bec62025d3474b9c2d947ec6d68f9f577be5fab8ee80503cefd8846c3 fd025d 40 2dadacd65b585e4061421b5265ff543e2a7bdc4d4a7fea932727426bdc53db252a2f914ea1fcbd580b80cdea60226f63288cd44bd84a8850c9189a24f08c7cc5"), - TxSignatures(channelId2, tx1, listOf(), signature, listOf(), listOf()) to ByteVector("0047 bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb 1f2ec025a33e39ef8e177afcdc1adc855bf128dc906182255aeb64efa825f106 0000 fd0259 40 aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaabbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb"), - TxSignatures(channelId2, tx1, listOf(), signature, swapInSignatures, listOf()) to ByteVector("0047 bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb 1f2ec025a33e39ef8e177afcdc1adc855bf128dc906182255aeb64efa825f106 0000 fd0259 40 aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaabbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb fd025b 80 c49269a9baa73a5ec44b63bdcaabf9c7c6477f72866b822f8502e5c989aa3562fe69d72bec62025d3474b9c2d947ec6d68f9f577be5fab8ee80503cefd8846c3 2dadacd65b585e4061421b5265ff543e2a7bdc4d4a7fea932727426bdc53db252a2f914ea1fcbd580b80cdea60226f63288cd44bd84a8850c9189a24f08c7cc5"), - TxSignatures(channelId2, tx1, listOf(), signature, listOf(), swapInSignatures) to ByteVector("0047 bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb 1f2ec025a33e39ef8e177afcdc1adc855bf128dc906182255aeb64efa825f106 0000 fd0259 40 aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaabbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb fd025d 80 c49269a9baa73a5ec44b63bdcaabf9c7c6477f72866b822f8502e5c989aa3562fe69d72bec62025d3474b9c2d947ec6d68f9f577be5fab8ee80503cefd8846c3 2dadacd65b585e4061421b5265ff543e2a7bdc4d4a7fea932727426bdc53db252a2f914ea1fcbd580b80cdea60226f63288cd44bd84a8850c9189a24f08c7cc5"), - TxSignatures(channelId2, tx1, listOf(), signature, swapInSignatures.take(1), swapInSignatures.drop(1)) to ByteVector("0047 bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb 1f2ec025a33e39ef8e177afcdc1adc855bf128dc906182255aeb64efa825f106 0000 fd0259 40 aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaabbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb fd025b 40 c49269a9baa73a5ec44b63bdcaabf9c7c6477f72866b822f8502e5c989aa3562fe69d72bec62025d3474b9c2d947ec6d68f9f577be5fab8ee80503cefd8846c3 fd025d 40 2dadacd65b585e4061421b5265ff543e2a7bdc4d4a7fea932727426bdc53db252a2f914ea1fcbd580b80cdea60226f63288cd44bd84a8850c9189a24f08c7cc5"), + TxSignatures(channelId1, tx2, listOf(ScriptWitness(listOf(ByteVector("68656c6c6f2074686572652c2074686973206973206120626974636f6e212121"), ByteVector("82012088a820add57dfe5277079d069ca4ad4893c96de91f88ffb981fdc6a2a34d5336c66aff87"))), ScriptWitness(listOf(ByteVector("304402207de9ba56bb9f641372e805782575ee840a899e61021c8b1572b3ec1d5b5950e9022069e9ba998915dae193d3c25cb89b5e64370e6a3a7755e7f31cf6d7cbc2a49f6d01"), ByteVector("034695f5b7864c580bf11f9f8cb1a94eb336f2ce9ef872d2ae1a90ee276c772484")))), null, listOf(), listOf(), listOf(), listOf()) to ByteVector("0047 aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa fc7aa8845f192959202c1b7ff704e7cbddded463c05e844676a94ccb4bed69f1 0002 004a 022068656c6c6f2074686572652c2074686973206973206120626974636f6e2121212782012088a820add57dfe5277079d069ca4ad4893c96de91f88ffb981fdc6a2a34d5336c66aff87 006b 0247304402207de9ba56bb9f641372e805782575ee840a899e61021c8b1572b3ec1d5b5950e9022069e9ba998915dae193d3c25cb89b5e64370e6a3a7755e7f31cf6d7cbc2a49f6d0121034695f5b7864c580bf11f9f8cb1a94eb336f2ce9ef872d2ae1a90ee276c772484"), + TxSignatures(channelId2, tx1, listOf(), null, listOf(), listOf(), listOf(), listOf()) to ByteVector("0047 bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb 1f2ec025a33e39ef8e177afcdc1adc855bf128dc906182255aeb64efa825f106 0000"), + TxSignatures(channelId2, tx1, listOf(), null, swapInSignatures, listOf(), listOf(), listOf()) to ByteVector("0047 bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb 1f2ec025a33e39ef8e177afcdc1adc855bf128dc906182255aeb64efa825f106 0000 fd025b 80 c49269a9baa73a5ec44b63bdcaabf9c7c6477f72866b822f8502e5c989aa3562fe69d72bec62025d3474b9c2d947ec6d68f9f577be5fab8ee80503cefd8846c3 2dadacd65b585e4061421b5265ff543e2a7bdc4d4a7fea932727426bdc53db252a2f914ea1fcbd580b80cdea60226f63288cd44bd84a8850c9189a24f08c7cc5"), + TxSignatures(channelId2, tx1, listOf(), null, listOf(), swapInSignatures, listOf(), listOf()) to ByteVector("0047 bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb 1f2ec025a33e39ef8e177afcdc1adc855bf128dc906182255aeb64efa825f106 0000 fd025d 80 c49269a9baa73a5ec44b63bdcaabf9c7c6477f72866b822f8502e5c989aa3562fe69d72bec62025d3474b9c2d947ec6d68f9f577be5fab8ee80503cefd8846c3 2dadacd65b585e4061421b5265ff543e2a7bdc4d4a7fea932727426bdc53db252a2f914ea1fcbd580b80cdea60226f63288cd44bd84a8850c9189a24f08c7cc5"), + TxSignatures(channelId2, tx1, listOf(), null, swapInSignatures.take(1), swapInSignatures.drop(1), listOf(), listOf()) to ByteVector("0047 bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb 1f2ec025a33e39ef8e177afcdc1adc855bf128dc906182255aeb64efa825f106 0000 fd025b 40 c49269a9baa73a5ec44b63bdcaabf9c7c6477f72866b822f8502e5c989aa3562fe69d72bec62025d3474b9c2d947ec6d68f9f577be5fab8ee80503cefd8846c3 fd025d 40 2dadacd65b585e4061421b5265ff543e2a7bdc4d4a7fea932727426bdc53db252a2f914ea1fcbd580b80cdea60226f63288cd44bd84a8850c9189a24f08c7cc5"), + TxSignatures(channelId2, tx1, listOf(), signature, listOf(), listOf(), listOf(), listOf()) to ByteVector("0047 bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb 1f2ec025a33e39ef8e177afcdc1adc855bf128dc906182255aeb64efa825f106 0000 fd0259 40 aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaabbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb"), + TxSignatures(channelId2, tx1, listOf(), signature, swapInSignatures, listOf(), listOf(), listOf()) to ByteVector("0047 bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb 1f2ec025a33e39ef8e177afcdc1adc855bf128dc906182255aeb64efa825f106 0000 fd0259 40 aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaabbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb fd025b 80 c49269a9baa73a5ec44b63bdcaabf9c7c6477f72866b822f8502e5c989aa3562fe69d72bec62025d3474b9c2d947ec6d68f9f577be5fab8ee80503cefd8846c3 2dadacd65b585e4061421b5265ff543e2a7bdc4d4a7fea932727426bdc53db252a2f914ea1fcbd580b80cdea60226f63288cd44bd84a8850c9189a24f08c7cc5"), + TxSignatures(channelId2, tx1, listOf(), signature, listOf(), swapInSignatures, listOf(), listOf()) to ByteVector("0047 bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb 1f2ec025a33e39ef8e177afcdc1adc855bf128dc906182255aeb64efa825f106 0000 fd0259 40 aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaabbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb fd025d 80 c49269a9baa73a5ec44b63bdcaabf9c7c6477f72866b822f8502e5c989aa3562fe69d72bec62025d3474b9c2d947ec6d68f9f577be5fab8ee80503cefd8846c3 2dadacd65b585e4061421b5265ff543e2a7bdc4d4a7fea932727426bdc53db252a2f914ea1fcbd580b80cdea60226f63288cd44bd84a8850c9189a24f08c7cc5"), + TxSignatures(channelId2, tx1, listOf(), signature, listOf(), listOf(), swapInPartialSignatures, listOf()) to ByteVector("0047 bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb 1f2ec025a33e39ef8e177afcdc1adc855bf128dc906182255aeb64efa825f106 000 0fd0259 40 aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaabbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb fd025f c4 cccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccc026a04ab98d9e4774ad806e302dddeb63bea16b5cb5f223ee77478e861bb583eb30268680737c76dabb801cb2204f57dbe4e4579e4f710cd67dc1b4227592c81e9b5dddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddd026a04ab98d9e4774ad806e302dddeb63bea16b5cb5f223ee77478e861bb583eb30268680737c76dabb801cb2204f57dbe4e4579e4f710cd67dc1b4227592c81e9b5"), + TxSignatures(channelId2, tx1, listOf(), signature, listOf(), listOf(), listOf(), swapInPartialSignatures) to ByteVector("0047 bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb 1f2ec025a33e39ef8e177afcdc1adc855bf128dc906182255aeb64efa825f106 000 0fd0259 40 aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaabbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb fd0261 c4 cccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccc026a04ab98d9e4774ad806e302dddeb63bea16b5cb5f223ee77478e861bb583eb30268680737c76dabb801cb2204f57dbe4e4579e4f710cd67dc1b4227592c81e9b5dddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddd026a04ab98d9e4774ad806e302dddeb63bea16b5cb5f223ee77478e861bb583eb30268680737c76dabb801cb2204f57dbe4e4579e4f710cd67dc1b4227592c81e9b5"), + TxSignatures(channelId2, tx1, listOf(), signature, swapInSignatures.take(1), swapInSignatures.drop(1), listOf(), listOf()) to ByteVector("0047 bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb 1f2ec025a33e39ef8e177afcdc1adc855bf128dc906182255aeb64efa825f106 0000 fd0259 40 aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaabbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb fd025b 40 c49269a9baa73a5ec44b63bdcaabf9c7c6477f72866b822f8502e5c989aa3562fe69d72bec62025d3474b9c2d947ec6d68f9f577be5fab8ee80503cefd8846c3 fd025d 40 2dadacd65b585e4061421b5265ff543e2a7bdc4d4a7fea932727426bdc53db252a2f914ea1fcbd580b80cdea60226f63288cd44bd84a8850c9189a24f08c7cc5"), TxInitRbf(channelId1, 8388607, FeeratePerKw(4000.sat)) to ByteVector("0048 aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa 007fffff 00000fa0"), TxInitRbf(channelId1, 0, FeeratePerKw(4000.sat), TlvStream(TxInitRbfTlv.SharedOutputContributionTlv(1_500_000.sat))) to ByteVector("0048 aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa 00000000 00000fa0 0008000000000016e360"), TxInitRbf(channelId1, 0, FeeratePerKw(4000.sat), TlvStream(TxInitRbfTlv.SharedOutputContributionTlv(0.sat))) to ByteVector("0048 aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa 00000000 00000fa0 00080000000000000000"), From 9722067857db0e55abf241b476f67b2edc4a8f5a Mon Sep 17 00:00:00 2001 From: sstone Date: Tue, 21 Nov 2023 19:02:49 +0100 Subject: [PATCH 5/8] Use different user keys for the common and refund paths This allows us to easily rotate swap-in addresses and generate a single generic taproot descriptor (for bitcoin core 26 and newer) that can be used to recover swap-in funds once the refund delay has passed, assuming that: - user and server keys are static - user refund keys follow BIP derivation --- RECOVERY.md | 101 ++++++++---------- .../acinq/lightning/channel/InteractiveTx.kt | 92 ++++++++++------ .../fr/acinq/lightning/crypto/KeyManager.kt | 8 +- .../serialization/v4/Deserialization.kt | 14 +++ .../serialization/v4/Serialization.kt | 16 +++ .../lightning/transactions/SwapInProtocol.kt | 62 +++++++++-- .../acinq/lightning/wire/InteractiveTxTlv.kt | 27 ++++- .../acinq/lightning/wire/LightningMessages.kt | 2 + .../transactions/TransactionsTestsCommon.kt | 15 ++- .../wire/LightningCodecsTestsCommon.kt | 3 +- 10 files changed, 230 insertions(+), 110 deletions(-) diff --git a/RECOVERY.md b/RECOVERY.md index 081a05fe0..e414446fe 100644 --- a/RECOVERY.md +++ b/RECOVERY.md @@ -19,24 +19,29 @@ For example, when using [electrum](https://electrum.org/): When swapping funds to a `lightning-kmp` wallet, the following steps are performed: -- funds are sent to a swap-in address via a swap transaction +- funds are sent to a swap-in address via a swap transaction. - we wait for that transaction to have enough confirmations - then, if the fees don't exceed the user's liquidity policy, these funds are moved into a lightning channel +We use musig2 to aggregate user keys (user being the wallet) and server keys (server being the LSP: the ACINQ node): swap-in addresses are standard p2tr addresses, and +swap-in transactions to your wallet are indistinguishable from other p2tr transactions. + The swap transaction's output can be spent using either: -1. A signature from the user's wallet and a signature from the remote node +1. A aggregated musig2 signature built from a partial signature from the user's wallet and a partial signature from the remote node 2. A signature from the user's wallet after a refund delay Funds can be recovered using the second option and [Bitcoin Core](https://github.com/bitcoin/bitcoin). -This process needs at least Bitcoin Core 25.0. +This process needs at least Bitcoin Core 26.0. This process will become simpler once popular on-chain wallets (such as [electrum](https://electrum.org/)) add supports for output script descriptors. -### Extract master keys +### Get your wallet descriptor -We don't directly export your extended master private key for security reasons, so you will need to manually insert it in the descriptor. -You can obtain your extended master private key in [electrum](https://electrum.org/). After restoring your seed, type `wallet.keystore.xprv` in the console to obtain your master `xprv`. +lighting-kmp provides both a public descriptor and private descriptor for your swap-in wallet. +The public descriptor can be used to create a watch-only wallet for your swap-in funds. +The private descriptor can be used to recover your swap-in funds, after the refund delay has passed. +:warning: Do not share this private descriptor with anyone ! ### Create recovery wallet @@ -48,43 +53,28 @@ bitcoin-cli createwallet recovery ### Import descriptor into the recovery wallet -`lightning-kmp` provides the public descriptor for your swap-in address, which uses the following template: +`lightning-kmp` provides a public and private descriptor for your swap-in wallet, which both use the following template: ```txt -wsh(and_v(v:pk([/]),or_d(pk(),older()))) +tr(,and_v(v:pk(/),older())) ``` -For example, it will look like this: +For example, your public descriptor will look like this: ```txt -wsh(and_v(v:pk([14620948/51h/0h/0h]tpubDCvYeHUZisCMV3h1zPevPWQmNPfA3g3vnu7gDqskXVCbJB1VKk2F7LApV6TTdm1sCyGout8ma27CCHvYTuMZxpwrcHnLwL4kaXW8z2KfFcW),or_d(pk(0256e948180f33f067246710a41656084fc245b97eda081efe1e488b21577d60fd),older(25920)))) +tr(1fc559d9c96c5953895d3150e64ebf3dd696a0b08e758650b48ff6251d7e60d1,and_v(v:pk(tprv8h9x3k1njDX6to9q2G3aEvcic81MJk64SUVMXFc2Eo2YQqPGCBpQa8uJDkTz3DMHVXEmvhuwf4ShjLQ7YaVr34x9DFT3y43cPzVKGB94r1n/*),older(25920)))#7dne06j5 ``` -Replace the `extended_public_key` and the `derivation_path` with the extended private key obtained in the [first step](#extract-master-keys). -In our example, the extended private key matching our seed is `tprv8ZgxMBicQKsPdKRFLVct6VDpfmCxk6aC7iAF8tb6roQ7hv1zFCyGwDLBUUxMVJ95dTiQS5VvCbQ6J7CcGqguw5SbnDpNjbjpfVwcMwUtmjS`, so we create the following private descriptor: +And your private descriptor will look like this: -```txt -wsh(and_v(v:pk(tprv8ZgxMBicQKsPdKRFLVct6VDpfmCxk6aC7iAF8tb6roQ7hv1zFCyGwDLBUUxMVJ95dTiQS5VvCbQ6J7CcGqguw5SbnDpNjbjpfVwcMwUtmjS/51h/0h/0h),or_d(pk(0256e948180f33f067246710a41656084fc245b97eda081efe1e488b21577d60fd),older(25920)))) ``` - -We need to obtain a checksum for this descriptor, which is provided by Bitcoin Core: - -```sh -bitcoin-cli getdescriptorinfo "wsh(and_v(v:pk(tprv8ZgxMBicQKsPdKRFLVct6VDpfmCxk6aC7iAF8tb6roQ7hv1zFCyGwDLBUUxMVJ95dTiQS5VvCbQ6J7CcGqguw5SbnDpNjbjpfVwcMwUtmjS/51h/0h/0h),or_d(pk(0256e948180f33f067246710a41656084fc245b97eda081efe1e488b21577d60fd),older(25920))))" - -{ - "descriptor": "wsh(and_v(v:pk(tpubD6NzVbkrYhZ4WnT3E9HUVtswEnituRm6h1m2RQdQH5CWYQGksbns7hx3ediWHpFEkEQC4vPssnQN2gQpzkodRDuMA7nQtWiQ5EDzkGpGVNw/51'/0'/0'),or_d(pk(0256e948180f33f067246710a41656084fc245b97eda081efe1e488b21577d60fd),older(25920))))#m8v4e6vu", - "checksum": "dlcgkrnc", - "isrange": false, - "issolvable": true, - "hasprivatekeys": true -} +tr(1fc559d9c96c5953895d3150e64ebf3dd696a0b08e758650b48ff6251d7e60d1,and_v(v:pk(tpubDDqzCA42sbCmnGBcuuiAeLGqB9XHU5Gy1n68omeKf4pwFKe2padzkdXAPsDMWMdee879oPYrGrTS8sioqyjv8b6TztunE526eo4Au9kTef3/*),older(25920)))#z6mq2a3u ``` -We can the append this checksum to our private descriptor and import it into our recovery wallet: +We can import our private descriptor into our recovery wallet: ```sh -bitcoin-cli -rpcwallet=recovery importdescriptors '[{ "desc": "wsh(and_v(v:pk(tprv8ZgxMBicQKsPdKRFLVct6VDpfmCxk6aC7iAF8tb6roQ7hv1zFCyGwDLBUUxMVJ95dTiQS5VvCbQ6J7CcGqguw5SbnDpNjbjpfVwcMwUtmjS/51h/0h/0h),or_d(pk(0256e948180f33f067246710a41656084fc245b97eda081efe1e488b21577d60fd),older(25920))))#dlcgkrnc", "timestamp": 0 }]' +bitcoin-cli -rpcwallet=recovery importdescriptors '[{ "desc": "tr(1fc559d9c96c5953895d3150e64ebf3dd696a0b08e758650b48ff6251d7e60d1,and_v(v:pk(tprv8ZgxMBicQKsPdKRFLVct6VDpfmCxk6aC7iAF8tb6roQ7hv1zFCyGwDLBUUxMVJ95dTiQS5VvCbQ6J7CcGqguw5SbnDpNjbjpfVwcMwUtmjS/51h/0h/0h/*),older(25920)))#rn7cy7yr", "timestamp": 0 }]' [ { @@ -130,25 +120,25 @@ bitcoin-cli -rpcwallet=recovery listtransactions [ { - "address": "bcrt1qw78cdcsn55vwsvmwe9qgwnx0fwffzqej7keuqfjnwj5xm0f5u6js2hp66f", + "address": "bcrt1pzz7rudhpqyy6zdnuwrg3dpnethckfzncma2urxghuc62dz49zenqv0p0q6", "parent_descs": [ - "wsh(and_v(v:pk(tpubD6NzVbkrYhZ4WnT3E9HUVtswEnituRm6h1m2RQdQH5CWYQGksbns7hx3ediWHpFEkEQC4vPssnQN2gQpzkodRDuMA7nQtWiQ5EDzkGpGVNw/51'/0'/0'),or_d(pk(0256e948180f33f067246710a41656084fc245b97eda081efe1e488b21577d60fd),older(25920))))#m8v4e6vu" + "tr(1fc559d9c96c5953895d3150e64ebf3dd696a0b08e758650b48ff6251d7e60d1,and_v(v:pk(tpubDDqzCA42sbCmnGBcuuiAeLGqB9XHU5Gy1n68omeKf4pwFKe2padzkdXAPsDMWMdee879oPYrGrTS8sioqyjv8b6TztunE526eo4Au9kTef3/*),older(144)))#zqam8e56" ], "category": "receive", - "amount": 1.50000000, - "label": "", - "vout": 1, - "confirmations": 5, - "blockhash": "6e1048a8d7829d36a766188b499ddcc2e497193427678d115fd341b2b452c0bd", - "blockheight": 151, + "amount": 0.10000000, + "vout": 0, + "abandoned": false, + "confirmations": 1, + "blockhash": "06361beb06e7d24bea80fc6800f4b5f374f09542a07fae77a7f8c26a9f7544b2", + "blockheight": 146, "blockindex": 1, - "blocktime": 1687759025, - "txid": "d9940b7eb709ff8eaec307bdd6d20633e30a6eb1627d9ef8c8e03dfd28298c75", - "wtxid": "261492e5f930b82f65f269bb3006db9c3ef14423e5f52f2a185ace18704bb7b0", + "blocktime": 1700670588, + "txid": "4c3236b1fa1f3ed124ab83b1667be95f855952e68729eae54a9f511c8c8cb993", + "wtxid": "16ab0b31f680e5bd4f149527148b542e16de96ce2d14db9c41552752f3d8e655", "walletconflicts": [ ], - "time": 1687759025, - "timereceived": 1687759181, + "time": 1700670571, + "timereceived": 1700670571, "bip125-replaceable": "no" } ] @@ -160,29 +150,22 @@ Once those funds have been recovered and the refund delay has expired (the `conf Compute the total amount received (in our example, 1.5 BTC), choose the address to send to (for example, `bcrt1q9ez7rt33wynwpah582lnqlj3u0tpzsrkj2flas`) and create a transaction using all of the received funds: ```sh -bitcoin-cli -rpcwallet=recovery walletcreatefundedpsbt '[{"txid":"d9940b7eb709ff8eaec307bdd6d20633e30a6eb1627d9ef8c8e03dfd28298c75","vout":1,"sequence":25920}]' '[{"bcrt1q9ez7rt33wynwpah582lnqlj3u0tpzsrkj2flas":1.5}]' 0 '{"subtractFeeFromOutputs":[0]}' - +bitcoin-cli -rpcwallet=recovery walletcreatefundedpsbt '[{"txid":"4c3236b1fa1f3ed124ab83b1667be95f855952e68729eae54a9f511c8c8cb993", "vout":0, "sequence":144}]' '[{"bcrt1qzy4h8dux6pjl8ys979632uynqffd53vjkzffjl":0.09}]' { - "psbt": "cHNidP8BAFICAAAAAXWMKSj9PeDI+J59YrFuCuMzBtLWvQfDro7/Cbd+C5TZAQAAAABAZQAAAQzI8AgAAAAAFgAULkXhrjFxJuD29Dq/MH5R49YRQHYAAAAAAAEAiQIAAAABsDXoV21bcbM8ii+Nyo4r8ZWmMEJIiaYqYg6pKaXiOiMAAAAAAP3///8CnBMVIQEAAAAiUSA520UqAgN8jz9APGIBbNHksiweuAEnMZvjgpMKiRUKkoDR8AgAAAAAIgAgd4+G4hOlGOgzbslAh0zPS5KRAzL1s8AmU3Sobb005qWWAAAAAQErgNHwCAAAAAAiACB3j4biE6UY6DNuyUCHTM9LkpEDMvWzwCZTdKhtvTTmpQEFTSED1ZiIPcIwgcWSbaso29B11ULE+6VERxkh27lqMde8SRmtIQJW6UgYDzPwZyRnEKQWVghPwkW5ftoIHv4eSIshV31g/axzZAJAZbJoIgYCVulIGA8z8GckZxCkFlYIT8JFuX7aCB7+HkiLIVd9YP0EsxuziiIGA9WYiD3CMIHFkm2rKNvQddVCxPulREcZIdu5ajHXvEkZEBRiCUgzAACAAAAAgAAAAIAAAA==", - "fee": 0.00002420, - "changepos": -1 + "psbt": "cHNidP8BAHECAAAAAZO5jIwcUZ9K5eoph+ZSWYVf6XtmsYOrJNE+H/qxNjJMAAAAAACQAAAAAkBUiQAAAAAAFgAUEStzt4bQZfOSBfF1FXCTAlLaRZIEOA8AAAAAABYAFEx1yJgBL6kfpf2sybIL0WajM0rXAAAAAAABASuAlpgAAAAAACJRIBC8PjbhAQmhNnxw0RaGeV3xZIp431XBmRfmNKaKpRZmIhXBH8VZ2clsWVOJXTFQ5k6/PdaWoLCOdYZQtI/2JR1+YNEnIG0cAcgg4JAO3Y5ZtLOD5zp/WFAHJFWAT5z/6Z+k+FQbrQKQALLAIRYfxVnZyWxZU4ldMVDmTr891pagsI51hlC0j/YlHX5g0QUA0EkObyEWbRwByCDgkA7djlm0s4PnOn9YUAckVYBPnP/pn6T4VBspAWR9Jdbf5zHI25Gs69RTMJILBCLUX82cmJj59Bk4SZKgTHUGgQAAAAABFyAfxVnZyWxZU4ldMVDmTr891pagsI51hlC0j/YlHX5g0QEYIGR9Jdbf5zHI25Gs69RTMJILBCLUX82cmJj59Bk4SZKgAAAiAgMhzD3XSvW4p+oRyBAvB6rUHaOCIyjVxJV9tEin3sUiqxjpcZ0vVAAAgAEAAIAAAACAAQAAAAEAAAAA", + "fee": 0.00002620, + "changepos": 1 } -bitcoin-cli -rpcwallet=recovery walletprocesspsbt "cHNidP8BAFICAAAAAXWMKSj9PeDI+J59YrFuCuMzBtLWvQfDro7/Cbd+C5TZAQAAAABAZQAAAQzI8AgAAAAAFgAULkXhrjFxJuD29Dq/MH5R49YRQHYAAAAAAAEAiQIAAAABsDXoV21bcbM8ii+Nyo4r8ZWmMEJIiaYqYg6pKaXiOiMAAAAAAP3///8CnBMVIQEAAAAiUSA520UqAgN8jz9APGIBbNHksiweuAEnMZvjgpMKiRUKkoDR8AgAAAAAIgAgd4+G4hOlGOgzbslAh0zPS5KRAzL1s8AmU3Sobb005qWWAAAAAQErgNHwCAAAAAAiACB3j4biE6UY6DNuyUCHTM9LkpEDMvWzwCZTdKhtvTTmpQEFTSED1ZiIPcIwgcWSbaso29B11ULE+6VERxkh27lqMde8SRmtIQJW6UgYDzPwZyRnEKQWVghPwkW5ftoIHv4eSIshV31g/axzZAJAZbJoIgYCVulIGA8z8GckZxCkFlYIT8JFuX7aCB7+HkiLIVd9YP0EsxuziiIGA9WYiD3CMIHFkm2rKNvQddVCxPulREcZIdu5ajHXvEkZEBRiCUgzAACAAAAAgAAAAIAAAA==" - -{ - "psbt": "cHNidP8BAFICAAAAAXWMKSj9PeDI+J59YrFuCuMzBtLWvQfDro7/Cbd+C5TZAQAAAABAZQAAAQzI8AgAAAAAFgAULkXhrjFxJuD29Dq/MH5R49YRQHYAAAAAAAEAiQIAAAABsDXoV21bcbM8ii+Nyo4r8ZWmMEJIiaYqYg6pKaXiOiMAAAAAAP3///8CnBMVIQEAAAAiUSA520UqAgN8jz9APGIBbNHksiweuAEnMZvjgpMKiRUKkoDR8AgAAAAAIgAgd4+G4hOlGOgzbslAh0zPS5KRAzL1s8AmU3Sobb005qWWAAAAAQErgNHwCAAAAAAiACB3j4biE6UY6DNuyUCHTM9LkpEDMvWzwCZTdKhtvTTmpQEImAMARzBEAiBNe5Y/fGWNfCIh2oBoZZHh5Em1kR3GFumpa0bgn9WRCQIgTDKGl/F59wpGRhdJ/jLlOTHqszmHonQTD4qgVNNJIc4BTSED1ZiIPcIwgcWSbaso29B11ULE+6VERxkh27lqMde8SRmtIQJW6UgYDzPwZyRnEKQWVghPwkW5ftoIHv4eSIshV31g/axzZAJAZbJoAAA=", - "complete": true -} - -bitcoin-cli -rpcwallet=recovery finalizepsbt "cHNidP8BAFICAAAAAXWMKSj9PeDI+J59YrFuCuMzBtLWvQfDro7/Cbd+C5TZAQAAAABAZQAAAQzI8AgAAAAAFgAULkXhrjFxJuD29Dq/MH5R49YRQHYAAAAAAAEAiQIAAAABsDXoV21bcbM8ii+Nyo4r8ZWmMEJIiaYqYg6pKaXiOiMAAAAAAP3///8CnBMVIQEAAAAiUSA520UqAgN8jz9APGIBbNHksiweuAEnMZvjgpMKiRUKkoDR8AgAAAAAIgAgd4+G4hOlGOgzbslAh0zPS5KRAzL1s8AmU3Sobb005qWWAAAAAQErgNHwCAAAAAAiACB3j4biE6UY6DNuyUCHTM9LkpEDMvWzwCZTdKhtvTTmpQEImAMARzBEAiBNe5Y/fGWNfCIh2oBoZZHh5Em1kR3GFumpa0bgn9WRCQIgTDKGl/F59wpGRhdJ/jLlOTHqszmHonQTD4qgVNNJIc4BTSED1ZiIPcIwgcWSbaso29B11ULE+6VERxkh27lqMde8SRmtIQJW6UgYDzPwZyRnEKQWVghPwkW5ftoIHv4eSIshV31g/axzZAJAZbJoAAA=" - +bitcoin-cli -rpcwallet=recovery walletprocesspsbt "cHNidP8BAHECAAAAAZO5jIwcUZ9K5eoph+ZSWYVf6XtmsYOrJNE+H/qxNjJMAAAAAACQAAAAAkBUiQAAAAAAFgAUEStzt4bQZfOSBfF1FXCTAlLaRZIEOA8AAAAAABYAFEx1yJgBL6kfpf2sybIL0WajM0rXAAAAAAABASuAlpgAAAAAACJRIBC8PjbhAQmhNnxw0RaGeV3xZIp431XBmRfmNKaKpRZmIhXBH8VZ2clsWVOJXTFQ5k6/PdaWoLCOdYZQtI/2JR1+YNEnIG0cAcgg4JAO3Y5ZtLOD5zp/WFAHJFWAT5z/6Z+k+FQbrQKQALLAIRYfxVnZyWxZU4ldMVDmTr891pagsI51hlC0j/YlHX5g0QUA0EkObyEWbRwByCDgkA7djlm0s4PnOn9YUAckVYBPnP/pn6T4VBspAWR9Jdbf5zHI25Gs69RTMJILBCLUX82cmJj59Bk4SZKgTHUGgQAAAAABFyAfxVnZyWxZU4ldMVDmTr891pagsI51hlC0j/YlHX5g0QEYIGR9Jdbf5zHI25Gs69RTMJILBCLUX82cmJj59Bk4SZKgAAAiAgMhzD3XSvW4p+oRyBAvB6rUHaOCIyjVxJV9tEin3sUiqxjpcZ0vVAAAgAEAAIAAAACAAQAAAAEAAAAA" { - "hex": "02000000000101758c2928fd3de0c8f89e7d62b16e0ae33306d2d6bd07c3ae8eff09b77e0b94d9010000000040650000010cc8f008000000001600142e45e1ae317126e0f6f43abf307e51e3d6114076030047304402204d7b963f7c658d7c2221da80686591e1e449b5911dc616e9a96b46e09fd5910902204c328697f179f70a46461749fe32e53931eab33987a274130f8aa054d34921ce014d2103d598883dc23081c5926dab28dbd075d542c4fba544471921dbb96a31d7bc4919ad210256e948180f33f067246710a41656084fc245b97eda081efe1e488b21577d60fdac7364024065b26800000000", - "complete": true + "psbt": "cHNidP8BAHECAAAAAZO5jIwcUZ9K5eoph+ZSWYVf6XtmsYOrJNE+H/qxNjJMAAAAAACQAAAAAkBUiQAAAAAAFgAUEStzt4bQZfOSBfF1FXCTAlLaRZIEOA8AAAAAABYAFEx1yJgBL6kfpf2sybIL0WajM0rXAAAAAAABASuAlpgAAAAAACJRIBC8PjbhAQmhNnxw0RaGeV3xZIp431XBmRfmNKaKpRZmAQiLA0D59zl6TLlwXk2oCio3Ffff8dpRQmpYWs7MaY+cUk1Zfl03hzxj1vwIAHBQQbyh33PCX7JoDrlXxlo/Le86jMjQJiBtHAHIIOCQDt2OWbSzg+c6f1hQByRVgE+c/+mfpPhUG60CkACyIcEfxVnZyWxZU4ldMVDmTr891pagsI51hlC0j/YlHX5g0QAAIgIDIcw910r1uKfqEcgQLweq1B2jgiMo1cSVfbRIp97FIqsY6XGdL1QAAIABAACAAAAAgAEAAAABAAAAAA==", + "complete": true, + "hex": "0200000000010193b98c8c1c519f4ae5ea2987e65259855fe97b66b183ab24d13e1ffab136324c000000000090000000024054890000000000160014112b73b786d065f39205f1751570930252da459204380f00000000001600144c75c898012fa91fa5fdacc9b20bd166a3334ad70340f9f7397a4cb9705e4da80a2a3715f7dff1da51426a585acecc698f9c524d597e5d37873c63d6fc0800705041bca1df73c25fb2680eb957c65a3f2def3a8cc8d026206d1c01c820e0900edd8e59b4b383e73a7f5850072455804f9cffe99fa4f8541bad029000b221c11fc559d9c96c5953895d3150e64ebf3dd696a0b08e758650b48ff6251d7e60d100000000" } -bitcoin-cli -rpcwallet=recovery sendrawtransaction 02000000000101758c2928fd3de0c8f89e7d62b16e0ae33306d2d6bd07c3ae8eff09b77e0b94d9010000000040650000010cc8f008000000001600142e45e1ae317126e0f6f43abf307e51e3d6114076030047304402204d7b963f7c658d7c2221da80686591e1e449b5911dc616e9a96b46e09fd5910902204c328697f179f70a46461749fe32e53931eab33987a274130f8aa054d34921ce014d2103d598883dc23081c5926dab28dbd075d542c4fba544471921dbb96a31d7bc4919ad210256e948180f33f067246710a41656084fc245b97eda081efe1e488b21577d60fdac7364024065b26800000000 +bitcoin-cli sendrawtransaction 0200000000010193b98c8c1c519f4ae5ea2987e65259855fe97b66b183ab24d13e1ffab136324c000000000090000000024054890000000000160014112b73b786d065f39205f1751570930252da459204380f00000000001600144c75c898012fa91fa5fdacc9b20bd166a3334ad70340f9f7397a4cb9705e4da80a2a3715f7dff1da51426a585acecc698f9c524d597e5d37873c63d6fc0800705041bca1df73c25fb2680eb957c65a3f2def3a8cc8d026206d1c01c820e0900edd8e59b4b383e73a7f5850072455804f9cffe99fa4f8541bad029000b221c11fc559d9c96c5953895d3150e64ebf3dd696a0b08e758650b48ff6251d7e60d100000000 +09efe025805b2db8ae845a94639e5ad415756fb0d010aad54bf3f74ae71e015d ``` Wait for that transaction to confirm, and your funds will have been successfully recovered! diff --git a/src/commonMain/kotlin/fr/acinq/lightning/channel/InteractiveTx.kt b/src/commonMain/kotlin/fr/acinq/lightning/channel/InteractiveTx.kt index 8b174fb02..d344d983e 100644 --- a/src/commonMain/kotlin/fr/acinq/lightning/channel/InteractiveTx.kt +++ b/src/commonMain/kotlin/fr/acinq/lightning/channel/InteractiveTx.kt @@ -124,6 +124,14 @@ sealed class InteractiveTxInput { override val outPoint: OutPoint = OutPoint(previousTx, previousTxOutput) } + data class LocalMusig2SwapIn( + override val serialId: Long, + override val previousTx: Transaction, + override val previousTxOutput: Long, + override val sequence: UInt, + val swapInParams: TxAddInputTlv.SwapInParamsMusig2) : Local() { + override val outPoint: OutPoint = OutPoint(previousTx, previousTxOutput) + } /** * A remote input that funds the interactive transaction. * We only keep the data we need from our peer's TxAddInput to avoid storing potentially large messages in our DB. @@ -141,6 +149,13 @@ sealed class InteractiveTxInput { override val sequence: UInt, val swapInParams: TxAddInputTlv.SwapInParams) : Remote() + data class RemoteSwapInMusig2( + override val serialId: Long, + override val outPoint: OutPoint, + override val txOut: TxOut, + override val sequence: UInt, + val swapInParams: TxAddInputTlv.SwapInParamsMusig2) : Remote() + /** The shared input can be added by us or by our peer, depending on who initiated the protocol. */ data class Shared(override val serialId: Long, override val outPoint: OutPoint, override val txOut: TxOut, override val sequence: UInt, val localAmount: MilliSatoshi, val remoteAmount: MilliSatoshi) : InteractiveTxInput(), Incoming, Outgoing } @@ -258,14 +273,23 @@ data class FundingContributions(val inputs: List, v } val sharedInput = sharedUtxo?.let { (i, balances) -> listOf(InteractiveTxInput.Shared(0, i.info.outPoint, i.info.txOut, 0xfffffffdU, balances.toLocal, balances.toRemote)) } ?: listOf() val localInputs = walletInputs.map { i -> - val version = if (Script.isPay2wsh(i.previousTx.txOut[i.outputIndex].publicKeyScript.toByteArray())) 1 else 2 - InteractiveTxInput.LocalSwapIn( - 0, - i.previousTx.stripInputWitnesses(), - i.outputIndex.toLong(), - 0xfffffffdU, - TxAddInputTlv.SwapInParams(swapInKeys.userPublicKey, swapInKeys.remoteServerPublicKey, swapInKeys.refundDelay, version) - ) + when { + Script.isPay2wsh(i.previousTx.txOut[i.outputIndex].publicKeyScript.toByteArray()) -> + InteractiveTxInput.LocalSwapIn( + 0, + i.previousTx.stripInputWitnesses(), + i.outputIndex.toLong(), + 0xfffffffdU, + TxAddInputTlv.SwapInParams(swapInKeys.userPublicKey, swapInKeys.remoteServerPublicKey, swapInKeys.refundDelay) + ) + else -> InteractiveTxInput.LocalMusig2SwapIn( + 0, + i.previousTx.stripInputWitnesses(), + i.outputIndex.toLong(), + 0xfffffffdU, + TxAddInputTlv.SwapInParamsMusig2(swapInKeys.userPublicKey, swapInKeys.remoteServerPublicKey, swapInKeys.userRefundPublicKey, swapInKeys.refundDelay) + ) + } } return if (params.isInitiator) { Either.Right(sortFundingContributions(params, sharedInput + localInputs, sharedOutput + nonChangeOutputs + changeOutput)) @@ -315,6 +339,7 @@ data class FundingContributions(val inputs: List, v when (input) { is InteractiveTxInput.LocalOnly -> input.copy(serialId = serialId) is InteractiveTxInput.LocalSwapIn -> input.copy(serialId = serialId) + is InteractiveTxInput.LocalMusig2SwapIn-> input.copy(serialId = serialId) is InteractiveTxInput.Shared -> input.copy(serialId = serialId) } } @@ -388,15 +413,13 @@ data class SharedTransaction( val swapUserSigs = unsignedTx.txIn.mapIndexed { i, txIn -> localInputs .filterIsInstance() - .filter { it.swapInParams.version == 1 } .find { txIn.outPoint == it.outPoint } ?.let { input -> keyManager.swapInOnChainWallet.signSwapInputUser(unsignedTx, i, input.previousTx.txOut) } }.filterNotNull() val swapUserPartialSigs = unsignedTx.txIn.mapIndexed { i, txIn -> localInputs - .filterIsInstance() - .filter { it.swapInParams.version == 2 } + .filterIsInstance() .find { txIn.outPoint == it.outPoint } ?.let { input -> val userNonce = session.secretNonces[input.serialId] @@ -413,7 +436,6 @@ data class SharedTransaction( val swapServerSigs = unsignedTx.txIn.mapIndexed { i, txIn -> remoteInputs .filterIsInstance() - .filter { it.swapInParams.version == 1 } .find { txIn.outPoint == it.outPoint } ?.let { input -> val serverKey = keyManager.swapInOnChainWallet.localServerPrivateKey(remoteNodeId) @@ -424,8 +446,7 @@ data class SharedTransaction( val swapServerPartialSigs = unsignedTx.txIn.mapIndexed { i, txIn -> remoteInputs - .filterIsInstance() - .filter { it.swapInParams.version == 2 } + .filterIsInstance() .find { txIn.outPoint == it.outPoint } ?.let { input -> val serverKey = keyManager.swapInOnChainWallet.localServerPrivateKey(remoteNodeId) @@ -435,7 +456,7 @@ data class SharedTransaction( val serverNonce = session.txCompleteReceived.publicNonces[input.serialId] require(serverNonce != null) val commonNonce = PublicNonce.aggregate(listOf(userNonce.publicNonce(), serverNonce)) - val swapInProtocol = SwapInProtocolMusig2(input.swapInParams.userKey, serverKey.publicKey(), input.swapInParams.refundDelay) + val swapInProtocol = SwapInProtocolMusig2(input.swapInParams.userKey, serverKey.publicKey(), input.swapInParams.userRefundKey, input.swapInParams.refundDelay) TxSignatures.Companion.PartialSignature(swapInProtocol.signSwapInputServer(unsignedTx, i, previousOutputs, serverNonce, serverKey, userNonce), commonNonce) } }.filterNotNull() @@ -457,12 +478,12 @@ data class PartiallySignedSharedTransaction(override val tx: SharedTransaction, override val signedTx = null fun addRemoteSigs(channelKeys: KeyManager.ChannelKeys, fundingParams: InteractiveTxParams, remoteSigs: TxSignatures): FullySignedSharedTransaction? { - if (localSigs.swapInUserSigs.size != tx.localInputs.filterIsInstance().filter { it.swapInParams.version == 1 }.size) return null - if (localSigs.swapInUserPartialSigs.size != tx.localInputs.filterIsInstance().filter { it.swapInParams.version == 2 }.size) return null - if (remoteSigs.swapInUserSigs.size != tx.remoteSwapInputs().filter { it.swapInParams.version ==1 }.size) return null - if (remoteSigs.swapInUserPartialSigs.size != tx.remoteSwapInputs().filter { it.swapInParams.version ==2 }.size) return null - if (remoteSigs.swapInServerSigs.size != tx.localInputs.filter { it is InteractiveTxInput.LocalSwapIn && it.swapInParams.version == 1}.size) return null - if (remoteSigs.swapInServerPartialSigs.size != tx.localInputs.filter { it is InteractiveTxInput.LocalSwapIn && it.swapInParams.version == 2}.size) return null + if (localSigs.swapInUserSigs.size != tx.localInputs.filterIsInstance().size) return null + if (localSigs.swapInUserPartialSigs.size != tx.localInputs.filterIsInstance().size) return null + if (remoteSigs.swapInUserSigs.size != tx.remoteInputs.filterIsInstance().size) return null + if (remoteSigs.swapInUserPartialSigs.size != tx.remoteInputs.filterIsInstance().size) return null + if (remoteSigs.swapInServerSigs.size != tx.localInputs.filterIsInstance().size) return null + if (remoteSigs.swapInServerPartialSigs.size != tx.localInputs.filterIsInstance().size) return null if (remoteSigs.witnesses.size != tx.remoteOnlyInputs().size) return null if (remoteSigs.txId != localSigs.txId) return null val sharedSigs = fundingParams.sharedInput?.let { @@ -487,13 +508,13 @@ data class FullySignedSharedTransaction(override val tx: SharedTransaction, over override val signedTx = run { val sharedTxIn = tx.sharedInput?.let { i -> listOf(Pair(i.serialId, TxIn(i.outPoint, ByteVector.empty, i.sequence.toLong(), sharedSigs ?: ScriptWitness.empty))) } ?: listOf() val localOnlyTxIn = tx.localOnlyInputs().sortedBy { i -> i.serialId }.zip(localSigs.witnesses).map { (i, w) -> Pair(i.serialId, TxIn(OutPoint(i.previousTx, i.previousTxOutput), ByteVector.empty, i.sequence.toLong(), w)) } - val localSwapTxIn = tx.localSwapInputs().filter { it.swapInParams.version == 1 }.sortedBy { i -> i.serialId }.zip(localSigs.swapInUserSigs.zip(remoteSigs.swapInServerSigs)).map { (i, sigs) -> + val localSwapTxIn = tx.localInputs.filterIsInstance().sortedBy { i -> i.serialId }.zip(localSigs.swapInUserSigs.zip(remoteSigs.swapInServerSigs)).map { (i, sigs) -> val (userSig, serverSig) = sigs val swapInProtocol = SwapInProtocol(i.swapInParams) val witness = swapInProtocol.witness(userSig, serverSig) Pair(i.serialId, TxIn(OutPoint(i.previousTx, i.previousTxOutput), ByteVector.empty, i.sequence.toLong(), witness)) } - val localSwapTxInMusig2 = tx.localSwapInputs().filter { it.swapInParams.version == 2 }.sortedBy { i -> i.serialId }.zip(localSigs.swapInUserPartialSigs.zip(remoteSigs.swapInServerPartialSigs)).map { (i, sigs) -> + val localSwapTxInMusig2 = tx.localInputs.filterIsInstance().sortedBy { i -> i.serialId }.zip(localSigs.swapInUserPartialSigs.zip(remoteSigs.swapInServerPartialSigs)).map { (i, sigs) -> val (userSig, serverSig) = sigs val swapInProtocol = SwapInProtocolMusig2(i.swapInParams) require(userSig.nonce == serverSig.nonce){ "user and server public nonces mismatch for local input ${i.serialId}"} @@ -506,13 +527,13 @@ data class FullySignedSharedTransaction(override val tx: SharedTransaction, over } val remoteOnlyTxIn = tx.remoteOnlyInputs().sortedBy { i -> i.serialId }.zip(remoteSigs.witnesses).map { (i, w) -> Pair(i.serialId, TxIn(i.outPoint, ByteVector.empty, i.sequence.toLong(), w)) } - val remoteSwapTxIn = tx.remoteSwapInputs().filter { it.swapInParams.version == 1 }.sortedBy { i -> i.serialId }.zip(remoteSigs.swapInUserSigs.zip(localSigs.swapInServerSigs)).map { (i, sigs) -> + val remoteSwapTxIn = tx.remoteInputs.filterIsInstance().sortedBy { i -> i.serialId }.zip(remoteSigs.swapInUserSigs.zip(localSigs.swapInServerSigs)).map { (i, sigs) -> val (userSig, serverSig) = sigs val swapInProtocol = SwapInProtocol(i.swapInParams.userKey, i.swapInParams.serverKey, i.swapInParams.refundDelay) val witness = swapInProtocol.witness(userSig, serverSig) Pair(i.serialId, TxIn(i.outPoint, ByteVector.empty, i.sequence.toLong(), witness)) } - val remoteSwapTxInMusig2 = tx.remoteSwapInputs().filter { it.swapInParams.version == 2 }.sortedBy { i -> i.serialId }.zip(remoteSigs.swapInUserPartialSigs.zip(localSigs.swapInServerPartialSigs)).map { (i, sigs) -> + val remoteSwapTxInMusig2 = tx.remoteInputs.filterIsInstance().sortedBy { i -> i.serialId }.zip(remoteSigs.swapInUserPartialSigs.zip(localSigs.swapInServerPartialSigs)).map { (i, sigs) -> val (userSig, serverSig) = sigs val swapInProtocol = SwapInProtocolMusig2(i.swapInParams) require(userSig.nonce == serverSig.nonce){ "user and server public nonces mismatch for remote input ${i.serialId}"} @@ -621,9 +642,9 @@ data class InteractiveTxSession( val currentNonces = secretNonces fun userNonce(serialId: Long) = currentNonces.getOrElse(serialId) { SecretNonce.generate(swapInKeys.userPrivateKey, swapInKeys.userPublicKey, null, null, null, randomBytes32()) } fun serverNonce(serialId: Long, serverKey: PublicKey) = currentNonces.getOrElse(serialId) { SecretNonce.generate(null, serverKey, null, null, null, randomBytes32()) } - val localMusig2SwapIns = localInputs.filterIsInstance().filter { swapInKeys.swapInProtocolMusig2.isMine(it.txOut) } + val localMusig2SwapIns = localInputs.filterIsInstance() val localNonces = localMusig2SwapIns.map { it.serialId to userNonce(it.serialId) }.toMap() - val remoteMusig2SwapIns = remoteInputs.filterIsInstance().filter { it.swapInParams.version == 2 } + val remoteMusig2SwapIns = remoteInputs.filterIsInstance() val remoteNonces = remoteMusig2SwapIns.map { it.serialId to serverNonce(it.serialId, it.swapInParams.serverKey) }.toMap() val txComplete = TxComplete(fundingParams.channelId, (localNonces + remoteNonces).mapValues { it.value.publicNonce() }) val next = copy(txCompleteSent = txComplete, secretNonces = localNonces + remoteNonces) @@ -639,11 +660,13 @@ data class InteractiveTxSession( val txAddInput = when (msg.value) { is InteractiveTxInput.LocalOnly -> TxAddInput(fundingParams.channelId, msg.value.serialId, msg.value.previousTx, msg.value.previousTxOutput, msg.value.sequence) is InteractiveTxInput.LocalSwapIn -> { - val version = if (swapInKeys.swapInProtocolMusig2.isMine(msg.value.txOut)) 2 else 1 - val swapInParams = TxAddInputTlv.SwapInParams(swapInKeys.userPublicKey, swapInKeys.remoteServerPublicKey, swapInKeys.refundDelay, version) + val swapInParams = TxAddInputTlv.SwapInParams(swapInKeys.userPublicKey, swapInKeys.remoteServerPublicKey, swapInKeys.refundDelay) + TxAddInput(fundingParams.channelId, msg.value.serialId, msg.value.previousTx, msg.value.previousTxOutput, msg.value.sequence, TlvStream(swapInParams)) + } + is InteractiveTxInput.LocalMusig2SwapIn -> { + val swapInParams = TxAddInputTlv.SwapInParamsMusig2(swapInKeys.userPublicKey, swapInKeys.remoteServerPublicKey, swapInKeys.userRefundPublicKey, swapInKeys.refundDelay) TxAddInput(fundingParams.channelId, msg.value.serialId, msg.value.previousTx, msg.value.previousTxOutput, msg.value.sequence, TlvStream(swapInParams)) } - is InteractiveTxInput.Shared -> TxAddInput(fundingParams.channelId, msg.value.serialId, msg.value.outPoint, msg.value.sequence) } Pair(next, InteractiveTxSessionAction.SendMessage(txAddInput)) @@ -690,9 +713,10 @@ data class InteractiveTxSession( } val outpoint = OutPoint(message.previousTx, message.previousTxOutput) val txOut = message.previousTx.txOut[message.previousTxOutput.toInt()] - when (message.swapInParams) { - null -> InteractiveTxInput.RemoteOnly(message.serialId, outpoint, txOut, message.sequence) - else -> InteractiveTxInput.RemoteSwapIn(message.serialId, outpoint, txOut, message.sequence, message.swapInParams) + when { + message.swapInParamsMusig2 != null -> InteractiveTxInput.RemoteSwapInMusig2(message.serialId, outpoint, txOut, message.sequence, message.swapInParamsMusig2) + message.swapInParams != null -> InteractiveTxInput.RemoteSwapIn(message.serialId, outpoint, txOut, message.sequence, message.swapInParams) + else -> InteractiveTxInput.RemoteOnly(message.serialId, outpoint, txOut, message.sequence) } } } @@ -826,7 +850,7 @@ data class InteractiveTxSession( } val remoteOnlyInputsWithNonces = remoteOnlyInputs.map { when { - it is InteractiveTxInput.RemoteSwapIn && it.swapInParams.version == 2 -> { + it is InteractiveTxInput.RemoteSwapInMusig2 -> { val userNonce = secretNonces[it.serialId] val serverNonce = txCompleteReceived.publicNonces[it.serialId] if (userNonce == null || serverNonce == null) return InteractiveTxSessionAction.MissingNonce(fundingParams.channelId, it.serialId) diff --git a/src/commonMain/kotlin/fr/acinq/lightning/crypto/KeyManager.kt b/src/commonMain/kotlin/fr/acinq/lightning/crypto/KeyManager.kt index 7c598cb67..386d21000 100644 --- a/src/commonMain/kotlin/fr/acinq/lightning/crypto/KeyManager.kt +++ b/src/commonMain/kotlin/fr/acinq/lightning/crypto/KeyManager.kt @@ -121,17 +121,21 @@ interface KeyManager { val refundDelay: Int = DefaultSwapInParams.RefundDelay ) { private val userExtendedPrivateKey: DeterministicWallet.ExtendedPrivateKey = DeterministicWallet.derivePrivateKey(master, swapInUserKeyPath(chain)) + private val userRefundExtendedPrivateKey: DeterministicWallet.ExtendedPrivateKey = DeterministicWallet.derivePrivateKey(master, swapInUserRefundKeyPath(chain)) private val swapExtendedPublicKey = DeterministicWallet.publicKey(DeterministicWallet.derivePrivateKey(master, swapInLocalServerKeyPath(chain))) private val xpub = DeterministicWallet.encode(swapExtendedPublicKey, DeterministicWallet.tpub) val userPrivateKey: PrivateKey = userExtendedPrivateKey.privateKey val userPublicKey: PublicKey = userPrivateKey.publicKey() + val userRefundPrivateKey: PrivateKey = userRefundExtendedPrivateKey.privateKey + val userRefundPublicKey: PublicKey = userPrivateKey.publicKey() + private val localServerExtendedPrivateKey: DeterministicWallet.ExtendedPrivateKey = DeterministicWallet.derivePrivateKey(master, swapInLocalServerKeyPath(chain)) fun localServerPrivateKey(remoteNodeId: PublicKey): PrivateKey = DeterministicWallet.derivePrivateKey(localServerExtendedPrivateKey, perUserPath(remoteNodeId)).privateKey val swapInProtocol = SwapInProtocol(userPublicKey, remoteServerPublicKey, refundDelay) - val swapInProtocolMusig2 = SwapInProtocolMusig2(userPublicKey, remoteServerPublicKey, refundDelay) + val swapInProtocolMusig2 = SwapInProtocolMusig2(userPublicKey, remoteServerPublicKey, userRefundPublicKey, refundDelay) /** * The output script descriptor matching our swap-in addresses. @@ -211,6 +215,8 @@ interface KeyManager { fun swapInUserKeyPath(chain: NodeParams.Chain) = swapInKeyBasePath(chain) / hardened(0) + fun swapInUserRefundKeyPath(chain: NodeParams.Chain) = swapInKeyBasePath(chain) / hardened(0) / 0L + fun swapInLocalServerKeyPath(chain: NodeParams.Chain) = swapInKeyBasePath(chain) / hardened(1) fun encodedSwapInUserKeyPath(chain: NodeParams.Chain) = when (chain) { diff --git a/src/commonMain/kotlin/fr/acinq/lightning/serialization/v4/Deserialization.kt b/src/commonMain/kotlin/fr/acinq/lightning/serialization/v4/Deserialization.kt index 421199718..37b8a4c12 100644 --- a/src/commonMain/kotlin/fr/acinq/lightning/serialization/v4/Deserialization.kt +++ b/src/commonMain/kotlin/fr/acinq/lightning/serialization/v4/Deserialization.kt @@ -234,6 +234,13 @@ object Deserialization { sequence = readNumber().toUInt(), swapInParams = TxAddInputTlv.SwapInParams.read(this), ) + 0x03 -> InteractiveTxInput.LocalMusig2SwapIn( + serialId = readNumber(), + previousTx = readTransaction(), + previousTxOutput = readNumber(), + sequence = readNumber().toUInt(), + swapInParams = TxAddInputTlv.SwapInParamsMusig2.read(this), + ) else -> error("unknown discriminator $discriminator for class ${InteractiveTxInput.Local::class}") } @@ -251,6 +258,13 @@ object Deserialization { sequence = readNumber().toUInt(), swapInParams = TxAddInputTlv.SwapInParams.read(this) ) + 0x03 -> InteractiveTxInput.RemoteSwapInMusig2( + serialId = readNumber(), + outPoint = readOutPoint(), + txOut = TxOut.read(readDelimitedByteArray()), + sequence = readNumber().toUInt(), + swapInParams = TxAddInputTlv.SwapInParamsMusig2.read(this) + ) else -> error("unknown discriminator $discriminator for class ${InteractiveTxInput.Remote::class}") } diff --git a/src/commonMain/kotlin/fr/acinq/lightning/serialization/v4/Serialization.kt b/src/commonMain/kotlin/fr/acinq/lightning/serialization/v4/Serialization.kt index 31ef72c6e..af1ae716b 100644 --- a/src/commonMain/kotlin/fr/acinq/lightning/serialization/v4/Serialization.kt +++ b/src/commonMain/kotlin/fr/acinq/lightning/serialization/v4/Serialization.kt @@ -277,6 +277,14 @@ object Serialization { writeNumber(sequence.toLong()) swapInParams.write(this@writeLocalInteractiveTxInput) } + is InteractiveTxInput.LocalMusig2SwapIn -> i.run { + write(0x03) + writeNumber(serialId) + writeBtcObject(previousTx) + writeNumber(previousTxOutput) + writeNumber(sequence.toLong()) + swapInParams.write(this@writeLocalInteractiveTxInput) + } } private fun Output.writeRemoteInteractiveTxInput(i: InteractiveTxInput.Remote) = when (i) { @@ -295,6 +303,14 @@ object Serialization { writeNumber(sequence.toLong()) swapInParams.write(this@writeRemoteInteractiveTxInput) } + is InteractiveTxInput.RemoteSwapInMusig2 -> i.run { + write(0x03) + writeNumber(serialId) + writeBtcObject(outPoint) + writeBtcObject(txOut) + writeNumber(sequence.toLong()) + swapInParams.write(this@writeRemoteInteractiveTxInput) + } } private fun Output.writeSharedInteractiveTxOutput(o: InteractiveTxOutput.Shared) = o.run { diff --git a/src/commonMain/kotlin/fr/acinq/lightning/transactions/SwapInProtocol.kt b/src/commonMain/kotlin/fr/acinq/lightning/transactions/SwapInProtocol.kt index 11cd27772..9974255ab 100644 --- a/src/commonMain/kotlin/fr/acinq/lightning/transactions/SwapInProtocol.kt +++ b/src/commonMain/kotlin/fr/acinq/lightning/transactions/SwapInProtocol.kt @@ -5,14 +5,15 @@ 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.Lightning import fr.acinq.lightning.NodeParams import fr.acinq.lightning.wire.TxAddInputTlv -import org.kodein.log.newLogger +/** + * legacy swap-in protocol, that uses p2wsh and a single "user + server OR user + delay" script + */ class SwapInProtocol(val userPublicKey: PublicKey, val serverPublicKey: PublicKey, val refundDelay: Int) { - constructor(swapInParams: TxAddInputTlv.SwapInParams) : this(swapInParams.userKey, swapInParams.serverKey, swapInParams.refundDelay) + constructor(swapInParams: TxAddInputTlv.SwapInParams) : this(swapInParams.userKey, swapInParams.serverKey, swapInParams.refundDelay) // This script was generated with https://bitcoin.sipa.be/miniscript/ using the following miniscript policy: // and(pk(),or(99@pk(),older())) @@ -49,18 +50,31 @@ class SwapInProtocol(val userPublicKey: PublicKey, val serverPublicKey: PublicKe } } -class SwapInProtocolMusig2(val userPublicKey: PublicKey, val serverPublicKey: PublicKey, val refundDelay: Int) { - constructor(swapInParams: TxAddInputTlv.SwapInParams) : this(swapInParams.userKey, swapInParams.serverKey, swapInParams.refundDelay) +/** + * new swap-in protocol based on musig2 and taproot: (user key + server key) OR (user refund key + delay) + * for the common case, we use the musig2 aggregate of the user and server keys, spent through the key-spend path + * for the refund case, we use the refund script, spent through the script-spend path + * we use a different user key for the refund case: this allows us to generate generic descriptor for all swap-in addresses + * (see the descriptor() method below) + */ +class SwapInProtocolMusig2(val userPublicKey: PublicKey, val serverPublicKey: PublicKey, val userRefundKey: PublicKey, val refundDelay: Int) { + constructor(swapInParams: TxAddInputTlv.SwapInParamsMusig2) : this(swapInParams.userKey, swapInParams.serverKey, swapInParams.userRefundKey, swapInParams.refundDelay) // the redeem script is just the refund script. it is generated from this policy: and_v(v:pk(user),older(refundDelay)) - val redeemScript = listOf(OP_PUSHDATA(userPublicKey.xOnly()), OP_CHECKSIGVERIFY, OP_PUSHDATA(Script.encodeNumber(refundDelay)), OP_CHECKSEQUENCEVERIFY) + // it does not depend upon the user's or server's key, just the user's refund key and the refund delay + val redeemScript = listOf(OP_PUSHDATA(userRefundKey.xOnly()), OP_CHECKSIGVERIFY, OP_PUSHDATA(Script.encodeNumber(refundDelay)), OP_CHECKSEQUENCEVERIFY) private val scriptTree = ScriptTree.Leaf(ScriptLeaf(0, Script.write(redeemScript).byteVector(), Script.TAPROOT_LEAF_TAPSCRIPT)) private val merkleRoot = ScriptTree.hash(scriptTree) + + // 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 private val internalPubKey = Musig2.keyAgg(listOf(userPublicKey, serverPublicKey)).Q.xOnly() + + // it is tweaked with the script's merkle root to get the pubkey that will be exposed private val commonPubKeyAndParity = internalPubKey.outputKey(Crypto.TaprootTweak.ScriptTweak(merkleRoot)) val commonPubKey = commonPubKeyAndParity.first private val parity = commonPubKeyAndParity.second val pubkeyScript: List = Script.pay2tr(commonPubKey) + private val executionData = Script.ExecutionData(annex = null, tapleafHash = merkleRoot) private val controlBlock = byteArrayOf((Script.TAPROOT_LEAF_TAPSCRIPT + (if (parity) 1 else 0)).toByte()) + internalPubKey.value.toByteArray() @@ -111,4 +125,40 @@ class SwapInProtocolMusig2(val userPublicKey: PublicKey, val serverPublicKey: Pu txHash ) } + + /** + * + * @param chain chain we're on + * @param masterRefundKey master private key for the refund keys. we assume that there is a single level of derivation to compute the refund keys + * @return a taproot descriptor that can be imported in bitcoin core (from version 26 on) to recover user funds once the funding delay has passed + */ + fun descriptor(chain: NodeParams.Chain, masterRefundKey: DeterministicWallet.ExtendedPrivateKey): String { + val prefix = when (chain) { + NodeParams.Chain.Mainnet -> DeterministicWallet.xprv + else -> DeterministicWallet.tprv + } + val xpriv = DeterministicWallet.encode(masterRefundKey, prefix) + val path = masterRefundKey.path.toString().replace('\'', 'h').removePrefix("m") + val desc = "tr(${internalPubKey.value},and_v(v:pk($xpriv$path/*),older($refundDelay)))" + val checksum = Descriptor.checksum(desc) + return "$desc#$checksum" + } + + /** + * + * @param chain chain we're on + * @param masterRefundKey master public key for the refund keys. we assume that there is a single level of derivation to compute the refund keys + * @return a taproot descriptor that can be imported in bitcoin core (from version 26 on) to create a watch-only wallet for your swap-in transactions + */ + fun descriptor(chain: NodeParams.Chain, masterRefundKey: DeterministicWallet.ExtendedPublicKey): String { + val prefix = when (chain) { + NodeParams.Chain.Mainnet -> DeterministicWallet.xpub + else -> DeterministicWallet.tpub + } + val xpub = DeterministicWallet.encode(masterRefundKey, prefix) + val path = masterRefundKey.path.toString().replace('\'', 'h').removePrefix("m") + val desc = "tr(${internalPubKey.value},and_v(v:pk($xpub$path/*),older($refundDelay)))" + val checksum = Descriptor.checksum(desc) + return "$desc#$checksum" + } } \ No newline at end of file diff --git a/src/commonMain/kotlin/fr/acinq/lightning/wire/InteractiveTxTlv.kt b/src/commonMain/kotlin/fr/acinq/lightning/wire/InteractiveTxTlv.kt index 04a91586b..207d52f95 100644 --- a/src/commonMain/kotlin/fr/acinq/lightning/wire/InteractiveTxTlv.kt +++ b/src/commonMain/kotlin/fr/acinq/lightning/wire/InteractiveTxTlv.kt @@ -25,13 +25,12 @@ sealed class TxAddInputTlv : Tlv { } /** When adding a swap-in input to an interactive-tx, the user needs to provide the corresponding script parameters. */ - data class SwapInParams(val userKey: PublicKey, val serverKey: PublicKey, val refundDelay: Int, val version: Int) : TxAddInputTlv() { + data class SwapInParams(val userKey: PublicKey, val serverKey: PublicKey, val refundDelay: Int) : TxAddInputTlv() { override val tag: Long get() = SwapInParams.tag override fun write(out: Output) { LightningCodecs.writeBytes(userKey.value, out) LightningCodecs.writeBytes(serverKey.value, out) LightningCodecs.writeU32(refundDelay, out) - LightningCodecs.writeU32(version, out) } companion object : TlvValueReader { @@ -39,8 +38,28 @@ sealed class TxAddInputTlv : Tlv { override fun read(input: Input): SwapInParams = SwapInParams( PublicKey(LightningCodecs.bytes(input, 33)), PublicKey(LightningCodecs.bytes(input, 33)), - LightningCodecs.u32(input), - if (input.availableBytes >= 4) LightningCodecs.u32(input) else 1 + LightningCodecs.u32(input) + ) + } + } + + /** When adding a swap-in input to an interactive-tx, the user needs to provide the corresponding script parameters. */ + data class SwapInParamsMusig2(val userKey: PublicKey, val serverKey: PublicKey, val userRefundKey: PublicKey, val refundDelay: Int) : TxAddInputTlv() { + override val tag: Long get() = SwapInParamsMusig2.tag + override fun write(out: Output) { + LightningCodecs.writeBytes(userKey.value, out) + LightningCodecs.writeBytes(serverKey.value, out) + LightningCodecs.writeBytes(userRefundKey.value, out) + LightningCodecs.writeU32(refundDelay, out) + } + + companion object : TlvValueReader { + const val tag: Long = 1109 + override fun read(input: Input): SwapInParamsMusig2 = SwapInParamsMusig2( + PublicKey(LightningCodecs.bytes(input, 33)), + PublicKey(LightningCodecs.bytes(input, 33)), + PublicKey(LightningCodecs.bytes(input, 33)), + LightningCodecs.u32(input) ) } } diff --git a/src/commonMain/kotlin/fr/acinq/lightning/wire/LightningMessages.kt b/src/commonMain/kotlin/fr/acinq/lightning/wire/LightningMessages.kt index 4624a45ce..8140a94d6 100644 --- a/src/commonMain/kotlin/fr/acinq/lightning/wire/LightningMessages.kt +++ b/src/commonMain/kotlin/fr/acinq/lightning/wire/LightningMessages.kt @@ -331,6 +331,7 @@ data class TxAddInput( override val type: Long get() = TxAddInput.type val sharedInput: OutPoint? = tlvs.get()?.let { OutPoint(it.txId.reversed(), previousTxOutput) } val swapInParams = tlvs.get() + val swapInParamsMusig2 = tlvs.get() override fun write(out: Output) { LightningCodecs.writeBytes(channelId.toByteArray(), out) @@ -355,6 +356,7 @@ data class TxAddInput( val readers = mapOf( TxAddInputTlv.SharedInputTxId.tag to TxAddInputTlv.SharedInputTxId.Companion as TlvValueReader, TxAddInputTlv.SwapInParams.tag to TxAddInputTlv.SwapInParams.Companion as TlvValueReader, + TxAddInputTlv.SwapInParamsMusig2.tag to TxAddInputTlv.SwapInParamsMusig2.Companion as TlvValueReader, ) override fun read(input: Input): TxAddInput = TxAddInput( diff --git a/src/commonTest/kotlin/fr/acinq/lightning/transactions/TransactionsTestsCommon.kt b/src/commonTest/kotlin/fr/acinq/lightning/transactions/TransactionsTestsCommon.kt index 498096f82..4a1e7b701 100644 --- a/src/commonTest/kotlin/fr/acinq/lightning/transactions/TransactionsTestsCommon.kt +++ b/src/commonTest/kotlin/fr/acinq/lightning/transactions/TransactionsTestsCommon.kt @@ -14,6 +14,7 @@ import fr.acinq.lightning.CltvExpiry import fr.acinq.lightning.CltvExpiryDelta import fr.acinq.lightning.Lightning.randomBytes32 import fr.acinq.lightning.Lightning.randomKey +import fr.acinq.lightning.NodeParams import fr.acinq.lightning.blockchain.fee.FeeratePerKw import fr.acinq.lightning.channel.Commitments import fr.acinq.lightning.channel.Helpers.Funding @@ -556,8 +557,14 @@ class TransactionsTestsCommon : LightningTestSuite() { fun `spend 2-of-2 swap-in taproot-musig2 version`() { val userPrivateKey = PrivateKey(ByteArray(32) { 1 }) val serverPrivateKey = PrivateKey(ByteArray(32) { 2 }) + val refundDelay = 25920 + + val mnemonics = "abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon about".split(" ") + val seed = MnemonicCode.toSeed(mnemonics, "") + val masterPrivateKey = DeterministicWallet.derivePrivateKey(DeterministicWallet.generate(seed), "/51'/0'/0'").copy(path = KeyPath.empty) + val userRefundPrivateKey = DeterministicWallet.derivePrivateKey(masterPrivateKey, "0").privateKey + val swapInProtocolMusig2 = SwapInProtocolMusig2(userPrivateKey.publicKey(), serverPrivateKey.publicKey(), userRefundPrivateKey.publicKey(), refundDelay) - val swapInProtocolMusig2 = SwapInProtocolMusig2(userPrivateKey.publicKey(), serverPrivateKey.publicKey(), 144) val swapInTx = Transaction( version = 2, txIn = listOf(), @@ -591,11 +598,11 @@ class TransactionsTestsCommon : LightningTestSuite() { run { val tx = Transaction( version = 2, - txIn = listOf(TxIn(OutPoint(swapInTx, 0), sequence = 144)), + txIn = listOf(TxIn(OutPoint(swapInTx, 0), sequence = refundDelay.toLong())), txOut = listOf(TxOut(Satoshi(10000), pay2wpkh(PrivateKey(randomBytes32()).publicKey()))), lockTime = 0 ) - val sig = swapInProtocolMusig2.signSwapInputRefund(tx, 0, swapInTx.txOut, userPrivateKey) + val sig = swapInProtocolMusig2.signSwapInputRefund(tx, 0, swapInTx.txOut, userRefundPrivateKey) val signedTx = tx.updateWitness(0, swapInProtocolMusig2.witnessRefund(sig)) Transaction.correctlySpends(signedTx, swapInTx, ScriptFlags.STANDARD_SCRIPT_VERIFY_FLAGS) } @@ -621,7 +628,7 @@ class TransactionsTestsCommon : LightningTestSuite() { // DER-encoded ECDSA signatures usually take up to 72 bytes. val sig = ByteVector64.fromValidHex("90b658d172a51f1b3f1a2becd30942397f5df97da8cd2c026854607e955ad815ccfd87d366e348acc32aaf15ff45263aebbb7ecc913a0e5999133f447aee828c") val tx = Transaction(2, listOf(TxIn(OutPoint(ByteVector32.Zeroes, 2), 0)), listOf(TxOut(50_000.sat, pay2wpkh(pubkey))), 0) - val swapInProtocol = SwapInProtocolMusig2(pubkey, pubkey, 144) + val swapInProtocol = SwapInProtocolMusig2(pubkey, pubkey, pubkey, 144) val witness = swapInProtocol.witness(sig) val swapInput = TxIn(OutPoint(ByteVector32.Zeroes, 3), ByteVector.empty, 0, witness) val txWithAdditionalInput = tx.copy(txIn = tx.txIn + listOf(swapInput)) diff --git a/src/commonTest/kotlin/fr/acinq/lightning/wire/LightningCodecsTestsCommon.kt b/src/commonTest/kotlin/fr/acinq/lightning/wire/LightningCodecsTestsCommon.kt index ca15ad23a..2fe8e61b9 100644 --- a/src/commonTest/kotlin/fr/acinq/lightning/wire/LightningCodecsTestsCommon.kt +++ b/src/commonTest/kotlin/fr/acinq/lightning/wire/LightningCodecsTestsCommon.kt @@ -4,7 +4,6 @@ import fr.acinq.bitcoin.* import fr.acinq.bitcoin.io.ByteArrayInput import fr.acinq.bitcoin.io.ByteArrayOutput import fr.acinq.bitcoin.musig2.PublicNonce -import fr.acinq.bitcoin.musig2.SecretNonce import fr.acinq.lightning.* import fr.acinq.lightning.Lightning.randomBytes import fr.acinq.lightning.Lightning.randomBytes32 @@ -398,7 +397,7 @@ class LightningCodecsTestsCommon : LightningTestSuite() { TxAddInput(channelId2, 0, tx2, 2, 0u) to ByteVector("0042 bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb 0000000000000000 0100 0200000000010142180a8812fc79a3da7fb2471eff3e22d7faee990604c2ba7f2fc8dfb15b550a0200000000feffffff030f241800000000001976a9146774040642a78ca3b8b395e70f8391b21ec026fc88ac4a155801000000001600148d2e0b57adcb8869e603fd35b5179caf053361253b1d010000000000160014e032f4f4b9f8611df0d30a20648c190c263bbc33024730440220506005aa347f5b698542cafcb4f1a10250aeb52a609d6fd67ef68f9c1a5d954302206b9bb844343f4012bccd9d08a0f5430afb9549555a3252e499be7df97aae477a012103976d6b3eea3de4b056cd88cdfd50a22daf121e0fb5c6e45ba0f40e1effbd275a00000000 00000002 00000000"), TxAddInput(channelId1, 561, tx1, 0, 0xfffffffdu) to ByteVector("0042 aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa 0000000000000231 00f7 020000000001014ade359c5deb7c1cde2e94f401854658f97d7fa31c17ce9a831db253120a0a410100000017160014eb9a5bd79194a23d19d6ec473c768fb74f9ed32cffffffff021ca408000000000017a914946118f24bb7b37d5e9e39579e4a411e70f5b6a08763e703000000000017a9143638b2602d11f934c04abc6adb1494f69d1f14af8702473044022059ddd943b399211e4266a349f26b3289979e29f9b067792c6cfa8cc5ae25f44602204d627a5a5b603d0562e7969011fb3d64908af90a3ec7c876eaa9baf61e1958af012102f5188df1da92ed818581c29778047800ed6635788aa09d9469f7d17628f7323300000000 00000000 fffffffd"), TxAddInput(channelId1, 561, OutPoint(tx1, 1), 5u) to ByteVector("0042 aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa 0000000000000231 0000 00000001 00000005 fd0451201f2ec025a33e39ef8e177afcdc1adc855bf128dc906182255aeb64efa825f106"), - TxAddInput(channelId1, 561, tx1, 1, 5u, TlvStream(TxAddInputTlv.SwapInParams(swapInUserKey, swapInServerKey, swapInRefundDelay, 1))) to ByteVector("0042 aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa 0000000000000231 00f7 020000000001014ade359c5deb7c1cde2e94f401854658f97d7fa31c17ce9a831db253120a0a410100000017160014eb9a5bd79194a23d19d6ec473c768fb74f9ed32cffffffff021ca408000000000017a914946118f24bb7b37d5e9e39579e4a411e70f5b6a08763e703000000000017a9143638b2602d11f934c04abc6adb1494f69d1f14af8702473044022059ddd943b399211e4266a349f26b3289979e29f9b067792c6cfa8cc5ae25f44602204d627a5a5b603d0562e7969011fb3d64908af90a3ec7c876eaa9baf61e1958af012102f5188df1da92ed818581c29778047800ed6635788aa09d9469f7d17628f7323300000000 00000001 00000005 fd04534a03462779ad4aad39514614751a71085f2f10e1c7a593e4e030efb5b8721ce55b0b03864ef025fde8fb587d989186ce6a4a186895ee44a926bfc370e2c366597a3f8f0000009000000001"), + TxAddInput(channelId1, 561, tx1, 1, 5u, TlvStream(TxAddInputTlv.SwapInParams(swapInUserKey, swapInServerKey, swapInRefundDelay))) to ByteVector("0042 aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa 0000000000000231 00f7 020000000001014ade359c5deb7c1cde2e94f401854658f97d7fa31c17ce9a831db253120a0a410100000017160014eb9a5bd79194a23d19d6ec473c768fb74f9ed32cffffffff021ca408000000000017a914946118f24bb7b37d5e9e39579e4a411e70f5b6a08763e703000000000017a9143638b2602d11f934c04abc6adb1494f69d1f14af8702473044022059ddd943b399211e4266a349f26b3289979e29f9b067792c6cfa8cc5ae25f44602204d627a5a5b603d0562e7969011fb3d64908af90a3ec7c876eaa9baf61e1958af012102f5188df1da92ed818581c29778047800ed6635788aa09d9469f7d17628f7323300000000 00000001 00000005 fd04534603462779ad4aad39514614751a71085f2f10e1c7a593e4e030efb5b8721ce55b0b03864ef025fde8fb587d989186ce6a4a186895ee44a926bfc370e2c366597a3f8f00000090"), TxAddOutput(channelId1, 1105, 2047.sat, ByteVector("00149357014afd0ccd265658c9ae81efa995e771f472")) to ByteVector("0043 aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa 0000000000000451 00000000000007ff 0016 00149357014afd0ccd265658c9ae81efa995e771f472"), TxRemoveInput(channelId2, 561) to ByteVector("0044 bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb 0000000000000231"), TxRemoveOutput(channelId1, 1) to ByteVector("0045 aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa 0000000000000001"), From 4c044fbb3d440d56197dfb911a208850ff71c6dd Mon Sep 17 00:00:00 2001 From: sstone Date: Mon, 27 Nov 2023 16:30:38 +0100 Subject: [PATCH 6/8] Rename PartialSignature.nonce to aggregatedPublicNonce --- .../kotlin/fr/acinq/lightning/channel/InteractiveTx.kt | 8 ++++---- .../kotlin/fr/acinq/lightning/wire/InteractiveTxTlv.kt | 4 ++-- .../kotlin/fr/acinq/lightning/wire/LightningMessages.kt | 3 +-- 3 files changed, 7 insertions(+), 8 deletions(-) diff --git a/src/commonMain/kotlin/fr/acinq/lightning/channel/InteractiveTx.kt b/src/commonMain/kotlin/fr/acinq/lightning/channel/InteractiveTx.kt index d344d983e..b51981f8a 100644 --- a/src/commonMain/kotlin/fr/acinq/lightning/channel/InteractiveTx.kt +++ b/src/commonMain/kotlin/fr/acinq/lightning/channel/InteractiveTx.kt @@ -517,8 +517,8 @@ data class FullySignedSharedTransaction(override val tx: SharedTransaction, over val localSwapTxInMusig2 = tx.localInputs.filterIsInstance().sortedBy { i -> i.serialId }.zip(localSigs.swapInUserPartialSigs.zip(remoteSigs.swapInServerPartialSigs)).map { (i, sigs) -> val (userSig, serverSig) = sigs val swapInProtocol = SwapInProtocolMusig2(i.swapInParams) - require(userSig.nonce == serverSig.nonce){ "user and server public nonces mismatch for local input ${i.serialId}"} - val commonNonce = userSig.nonce + require(userSig.aggregatedPublicNonce == serverSig.aggregatedPublicNonce){ "aggregated public nonces mismatch for local input ${i.serialId}"} + val commonNonce = userSig.aggregatedPublicNonce val unsignedTx = tx.buildUnsignedTx() val ctx = swapInProtocol.signingCtx(unsignedTx, unsignedTx.txIn.indexOfFirst { it.outPoint == i.outPoint }, unsignedTx.txIn.map { tx.spentOutputs[it.outPoint]!! }, commonNonce) val commonSig = ctx.partialSigAgg(listOf(userSig.sig, serverSig.sig)) @@ -536,8 +536,8 @@ data class FullySignedSharedTransaction(override val tx: SharedTransaction, over val remoteSwapTxInMusig2 = tx.remoteInputs.filterIsInstance().sortedBy { i -> i.serialId }.zip(remoteSigs.swapInUserPartialSigs.zip(localSigs.swapInServerPartialSigs)).map { (i, sigs) -> val (userSig, serverSig) = sigs val swapInProtocol = SwapInProtocolMusig2(i.swapInParams) - require(userSig.nonce == serverSig.nonce){ "user and server public nonces mismatch for remote input ${i.serialId}"} - val commonNonce = userSig.nonce + require(userSig.aggregatedPublicNonce == serverSig.aggregatedPublicNonce){ "aggregated public nonces mismatch for remote input ${i.serialId}"} + val commonNonce = userSig.aggregatedPublicNonce val unsignedTx = tx.buildUnsignedTx() val ctx = swapInProtocol.signingCtx(unsignedTx, unsignedTx.txIn.indexOfFirst { it.outPoint == i.outPoint }, unsignedTx.txIn.map { tx.spentOutputs[it.outPoint]!! }, commonNonce) val commonSig = ctx.partialSigAgg(listOf(userSig.sig, serverSig.sig)) diff --git a/src/commonMain/kotlin/fr/acinq/lightning/wire/InteractiveTxTlv.kt b/src/commonMain/kotlin/fr/acinq/lightning/wire/InteractiveTxTlv.kt index 207d52f95..0419609ac 100644 --- a/src/commonMain/kotlin/fr/acinq/lightning/wire/InteractiveTxTlv.kt +++ b/src/commonMain/kotlin/fr/acinq/lightning/wire/InteractiveTxTlv.kt @@ -144,7 +144,7 @@ sealed class TxSignaturesTlv : Tlv { override val tag: Long get() = SwapInUserPartialSigs.tag override fun write(out: Output) = psigs.forEach { psig -> LightningCodecs.writeBytes(psig.sig, out) - LightningCodecs.writeBytes(psig.nonce.toByteArray(), out) + LightningCodecs.writeBytes(psig.aggregatedPublicNonce.toByteArray(), out) } companion object : TlvValueReader { @@ -165,7 +165,7 @@ sealed class TxSignaturesTlv : Tlv { override val tag: Long get() = SwapInServerPartialSigs.tag override fun write(out: Output) = psigs.forEach { psig -> LightningCodecs.writeBytes(psig.sig, out) - LightningCodecs.writeBytes(psig.nonce.toByteArray(), out) + LightningCodecs.writeBytes(psig.aggregatedPublicNonce.toByteArray(), out) } companion object : TlvValueReader { diff --git a/src/commonMain/kotlin/fr/acinq/lightning/wire/LightningMessages.kt b/src/commonMain/kotlin/fr/acinq/lightning/wire/LightningMessages.kt index 8140a94d6..f2986ec14 100644 --- a/src/commonMain/kotlin/fr/acinq/lightning/wire/LightningMessages.kt +++ b/src/commonMain/kotlin/fr/acinq/lightning/wire/LightningMessages.kt @@ -6,7 +6,6 @@ import fr.acinq.bitcoin.io.ByteArrayOutput import fr.acinq.bitcoin.io.Input import fr.acinq.bitcoin.io.Output import fr.acinq.bitcoin.musig2.PublicNonce -import fr.acinq.bitcoin.musig2.SecretNonce import fr.acinq.lightning.* import fr.acinq.lightning.blockchain.fee.FeeratePerKw import fr.acinq.lightning.channel.ChannelType @@ -519,7 +518,7 @@ data class TxSignatures( companion object : LightningMessageReader { const val type: Long = 71 - data class PartialSignature(val sig: ByteVector32, val nonce: PublicNonce) + data class PartialSignature(val sig: ByteVector32, val aggregatedPublicNonce: PublicNonce) @Suppress("UNCHECKED_CAST") val readers = mapOf( From ce7529951ade4585751a5393fd1e3f56e674bd99 Mon Sep 17 00:00:00 2001 From: sstone Date: Mon, 27 Nov 2023 17:49:48 +0100 Subject: [PATCH 7/8] Add a musig2 secret nonce field to local/remote musing2 swap-in classes It makes the code cleaner and we get rid of the secret nonces map. These nonces are replaced with dummy values whenever this classes are serialized, which is safe since they're never reused for signing txs. --- .../acinq/lightning/channel/InteractiveTx.kt | 114 +++++++++++------- .../serialization/v4/Deserialization.kt | 5 +- 2 files changed, 76 insertions(+), 43 deletions(-) diff --git a/src/commonMain/kotlin/fr/acinq/lightning/channel/InteractiveTx.kt b/src/commonMain/kotlin/fr/acinq/lightning/channel/InteractiveTx.kt index b51981f8a..40bcbc3be 100644 --- a/src/commonMain/kotlin/fr/acinq/lightning/channel/InteractiveTx.kt +++ b/src/commonMain/kotlin/fr/acinq/lightning/channel/InteractiveTx.kt @@ -129,8 +129,35 @@ sealed class InteractiveTxInput { override val previousTx: Transaction, override val previousTxOutput: Long, override val sequence: UInt, - val swapInParams: TxAddInputTlv.SwapInParamsMusig2) : Local() { + val swapInParams: TxAddInputTlv.SwapInParamsMusig2, + val secretNonce: SecretNonce) : Local() { override val outPoint: OutPoint = OutPoint(previousTx, previousTxOutput) + override fun equals(other: Any?): Boolean { + if (this === other) return true + if (other == null || this::class != other::class) return false + + other as LocalMusig2SwapIn + + if (serialId != other.serialId) return false + if (previousTx != other.previousTx) return false + if (previousTxOutput != other.previousTxOutput) return false + if (sequence != other.sequence) return false + if (swapInParams != other.swapInParams) return false + if (outPoint != other.outPoint) return false + + return true + } + + override fun hashCode(): Int { + var result = serialId.hashCode() + result = 31 * result + previousTx.hashCode() + result = 31 * result + previousTxOutput.hashCode() + result = 31 * result + sequence.hashCode() + result = 31 * result + swapInParams.hashCode() + result = 31 * result + outPoint.hashCode() + return result + } + } /** * A remote input that funds the interactive transaction. @@ -154,7 +181,32 @@ sealed class InteractiveTxInput { override val outPoint: OutPoint, override val txOut: TxOut, override val sequence: UInt, - val swapInParams: TxAddInputTlv.SwapInParamsMusig2) : Remote() + val swapInParams: TxAddInputTlv.SwapInParamsMusig2, + val secretNonce: SecretNonce) : Remote() { + override fun equals(other: Any?): Boolean { + if (this === other) return true + if (other == null || this::class != other::class) return false + + other as RemoteSwapInMusig2 + + if (serialId != other.serialId) return false + if (outPoint != other.outPoint) return false + if (txOut != other.txOut) return false + if (sequence != other.sequence) return false + if (swapInParams != other.swapInParams) return false + + return true + } + + override fun hashCode(): Int { + var result = serialId.hashCode() + result = 31 * result + outPoint.hashCode() + result = 31 * result + txOut.hashCode() + result = 31 * result + sequence.hashCode() + result = 31 * result + swapInParams.hashCode() + return result + } + } /** The shared input can be added by us or by our peer, depending on who initiated the protocol. */ data class Shared(override val serialId: Long, override val outPoint: OutPoint, override val txOut: TxOut, override val sequence: UInt, val localAmount: MilliSatoshi, val remoteAmount: MilliSatoshi) : InteractiveTxInput(), Incoming, Outgoing @@ -287,7 +339,8 @@ data class FundingContributions(val inputs: List, v i.previousTx.stripInputWitnesses(), i.outputIndex.toLong(), 0xfffffffdU, - TxAddInputTlv.SwapInParamsMusig2(swapInKeys.userPublicKey, swapInKeys.remoteServerPublicKey, swapInKeys.userRefundPublicKey, swapInKeys.refundDelay) + TxAddInputTlv.SwapInParamsMusig2(swapInKeys.userPublicKey, swapInKeys.remoteServerPublicKey, swapInKeys.userRefundPublicKey, swapInKeys.refundDelay), + SecretNonce.generate(swapInKeys.userPrivateKey, swapInKeys.userPrivateKey.publicKey(), null, null, null, randomBytes32()), ) } } @@ -422,8 +475,7 @@ data class SharedTransaction( .filterIsInstance() .find { txIn.outPoint == it.outPoint } ?.let { input -> - val userNonce = session.secretNonces[input.serialId] - require(userNonce != null) + val userNonce = input.secretNonce require(session.txCompleteReceived != null) val serverNonce = session.txCompleteReceived.publicNonces[input.serialId] require(serverNonce != null) @@ -450,8 +502,7 @@ data class SharedTransaction( .find { txIn.outPoint == it.outPoint } ?.let { input -> val serverKey = keyManager.swapInOnChainWallet.localServerPrivateKey(remoteNodeId) - val userNonce = session.secretNonces[input.serialId] - require(userNonce != null) + val userNonce = input.secretNonce require(session.txCompleteReceived != null) val serverNonce = session.txCompleteReceived.publicNonces[input.serialId] require(serverNonce != null) @@ -598,9 +649,7 @@ data class InteractiveTxSession( val txCompleteSent: TxComplete? = null, val txCompleteReceived: TxComplete? = null, val inputsReceivedCount: Int = 0, - val outputsReceivedCount: Int = 0, - val secretNonces: Map = mapOf() -) { + val outputsReceivedCount: Int = 0) { // Example flow: // +-------+ +-------+ @@ -639,15 +688,12 @@ data class InteractiveTxSession( return when (val msg = toSend.firstOrNull()) { null -> { // generate a new secret nonce for each musig2 new swapin every time we send TxComplete - val currentNonces = secretNonces - fun userNonce(serialId: Long) = currentNonces.getOrElse(serialId) { SecretNonce.generate(swapInKeys.userPrivateKey, swapInKeys.userPublicKey, null, null, null, randomBytes32()) } - fun serverNonce(serialId: Long, serverKey: PublicKey) = currentNonces.getOrElse(serialId) { SecretNonce.generate(null, serverKey, null, null, null, randomBytes32()) } val localMusig2SwapIns = localInputs.filterIsInstance() - val localNonces = localMusig2SwapIns.map { it.serialId to userNonce(it.serialId) }.toMap() + val localNonces = localMusig2SwapIns.map { it.serialId to it.secretNonce.publicNonce() }.toMap() val remoteMusig2SwapIns = remoteInputs.filterIsInstance() - val remoteNonces = remoteMusig2SwapIns.map { it.serialId to serverNonce(it.serialId, it.swapInParams.serverKey) }.toMap() - val txComplete = TxComplete(fundingParams.channelId, (localNonces + remoteNonces).mapValues { it.value.publicNonce() }) - val next = copy(txCompleteSent = txComplete, secretNonces = localNonces + remoteNonces) + val remoteNonces = remoteMusig2SwapIns.map { it.serialId to it.secretNonce.publicNonce() }.toMap() + val txComplete = TxComplete(fundingParams.channelId, (localNonces + remoteNonces)) + val next = copy(txCompleteSent = txComplete) if (next.isComplete) { Pair(next, next.validateTx(txComplete)) } else { @@ -714,7 +760,10 @@ data class InteractiveTxSession( val outpoint = OutPoint(message.previousTx, message.previousTxOutput) val txOut = message.previousTx.txOut[message.previousTxOutput.toInt()] when { - message.swapInParamsMusig2 != null -> InteractiveTxInput.RemoteSwapInMusig2(message.serialId, outpoint, txOut, message.sequence, message.swapInParamsMusig2) + message.swapInParamsMusig2 != null -> { + val secretNonce = SecretNonce.generate(null, message.swapInParamsMusig2.serverKey, null, null, null, randomBytes32()) + InteractiveTxInput.RemoteSwapInMusig2(message.serialId, outpoint, txOut, message.sequence, message.swapInParamsMusig2, secretNonce) + } message.swapInParams != null -> InteractiveTxInput.RemoteSwapIn(message.serialId, outpoint, txOut, message.sequence, message.swapInParams) else -> InteractiveTxInput.RemoteOnly(message.serialId, outpoint, txOut, message.sequence) } @@ -836,32 +885,13 @@ data class InteractiveTxSession( } sharedInputs.first() } - val localOnlyInputsWithNonces = localOnlyInputs.map { - when { - it is InteractiveTxInput.LocalSwapIn && swapInKeys.swapInProtocolMusig2.isMine(it.txOut) -> { - val userNonce = secretNonces[it.serialId] - val serverNonce = txCompleteReceived.publicNonces[it.serialId] - if (userNonce == null || serverNonce == null) return InteractiveTxSessionAction.MissingNonce(fundingParams.channelId, it.serialId) - it - } - - else -> it - } + localOnlyInputs.filterIsInstance().forEach { + txCompleteReceived.publicNonces[it.serialId] ?: return InteractiveTxSessionAction.MissingNonce(fundingParams.channelId, it.serialId) } - val remoteOnlyInputsWithNonces = remoteOnlyInputs.map { - when { - it is InteractiveTxInput.RemoteSwapInMusig2 -> { - val userNonce = secretNonces[it.serialId] - val serverNonce = txCompleteReceived.publicNonces[it.serialId] - if (userNonce == null || serverNonce == null) return InteractiveTxSessionAction.MissingNonce(fundingParams.channelId, it.serialId) - it - } - - else -> it - } + remoteOnlyInputs.filterIsInstance().forEach { + txCompleteReceived.publicNonces[it.serialId] ?: return InteractiveTxSessionAction.MissingNonce(fundingParams.channelId, it.serialId) } - - val sharedTx = SharedTransaction(sharedInput, sharedOutput, localOnlyInputsWithNonces, remoteOnlyInputsWithNonces, localOnlyOutputs, remoteOnlyOutputs, fundingParams.lockTime) + val sharedTx = SharedTransaction(sharedInput, sharedOutput, localOnlyInputs, remoteOnlyInputs, localOnlyOutputs, remoteOnlyOutputs, fundingParams.lockTime) val tx = sharedTx.buildUnsignedTx() if (sharedTx.localAmountIn < sharedTx.localAmountOut || sharedTx.remoteAmountIn < sharedTx.remoteAmountOut) { return InteractiveTxSessionAction.InvalidTxChangeAmount(fundingParams.channelId, tx.txid) diff --git a/src/commonMain/kotlin/fr/acinq/lightning/serialization/v4/Deserialization.kt b/src/commonMain/kotlin/fr/acinq/lightning/serialization/v4/Deserialization.kt index 37b8a4c12..4c9c56be0 100644 --- a/src/commonMain/kotlin/fr/acinq/lightning/serialization/v4/Deserialization.kt +++ b/src/commonMain/kotlin/fr/acinq/lightning/serialization/v4/Deserialization.kt @@ -4,6 +4,7 @@ import fr.acinq.bitcoin.* import fr.acinq.bitcoin.io.ByteArrayInput import fr.acinq.bitcoin.io.Input import fr.acinq.bitcoin.musig2.PublicNonce +import fr.acinq.bitcoin.musig2.SecretNonce import fr.acinq.lightning.CltvExpiryDelta import fr.acinq.lightning.Features import fr.acinq.lightning.ShortChannelId @@ -240,6 +241,7 @@ object Deserialization { previousTxOutput = readNumber(), sequence = readNumber().toUInt(), swapInParams = TxAddInputTlv.SwapInParamsMusig2.read(this), + secretNonce = SecretNonce(PrivateKey(ByteVector32.One), PrivateKey(ByteVector32.One), PrivateKey(ByteVector32.One).publicKey()) ) else -> error("unknown discriminator $discriminator for class ${InteractiveTxInput.Local::class}") } @@ -263,7 +265,8 @@ object Deserialization { outPoint = readOutPoint(), txOut = TxOut.read(readDelimitedByteArray()), sequence = readNumber().toUInt(), - swapInParams = TxAddInputTlv.SwapInParamsMusig2.read(this) + swapInParams = TxAddInputTlv.SwapInParamsMusig2.read(this), + secretNonce = SecretNonce(PrivateKey(ByteVector32.One), PrivateKey(ByteVector32.One), PrivateKey(ByteVector32.One).publicKey()) ) else -> error("unknown discriminator $discriminator for class ${InteractiveTxInput.Remote::class}") } From 504c49da527dff7da237732ebd2cc96aa55db5e9 Mon Sep 17 00:00:00 2001 From: sstone Date: Mon, 27 Nov 2023 18:41:21 +0100 Subject: [PATCH 8/8] Rework TxComplete to use implicit ordering for musig2 nonces Instead of sending an explicit serialId -> nonce map, we send a list of public nonces ordered by serial id. This matches how signatures are sent in TxSignatures. --- .../acinq/lightning/channel/InteractiveTx.kt | 47 ++++++++++++------- .../acinq/lightning/wire/InteractiveTxTlv.kt | 19 +++----- .../acinq/lightning/wire/LightningMessages.kt | 4 +- 3 files changed, 39 insertions(+), 31 deletions(-) diff --git a/src/commonMain/kotlin/fr/acinq/lightning/channel/InteractiveTx.kt b/src/commonMain/kotlin/fr/acinq/lightning/channel/InteractiveTx.kt index 40bcbc3be..3f96ef5f8 100644 --- a/src/commonMain/kotlin/fr/acinq/lightning/channel/InteractiveTx.kt +++ b/src/commonMain/kotlin/fr/acinq/lightning/channel/InteractiveTx.kt @@ -379,11 +379,11 @@ data class FundingContributions(val inputs: List, v ) fun weight(walletInputs: List): Int = walletInputs.sumOf { - when { - Script.isPay2wsh(it.previousTx.txOut[it.outputIndex].publicKeyScript.toByteArray()) -> Transactions.swapInputWeight - else -> Transactions.swapInputWeightMusig2 - } + when { + Script.isPay2wsh(it.previousTx.txOut[it.outputIndex].publicKeyScript.toByteArray()) -> Transactions.swapInputWeight + else -> Transactions.swapInputWeightMusig2 } + } /** We always randomize the order of inputs and outputs. */ private fun sortFundingContributions(params: InteractiveTxParams, inputs: List, outputs: List): FundingContributions { @@ -392,7 +392,7 @@ data class FundingContributions(val inputs: List, v when (input) { is InteractiveTxInput.LocalOnly -> input.copy(serialId = serialId) is InteractiveTxInput.LocalSwapIn -> input.copy(serialId = serialId) - is InteractiveTxInput.LocalMusig2SwapIn-> input.copy(serialId = serialId) + is InteractiveTxInput.LocalMusig2SwapIn -> input.copy(serialId = serialId) is InteractiveTxInput.Shared -> input.copy(serialId = serialId) } } @@ -462,6 +462,16 @@ data class SharedTransaction( val previousOutputsMap = sharedOutput + localOutputs + remoteOutputs val previousOutputs = unsignedTx.txIn.map { previousOutputsMap[it.outPoint]!! }.toList() + // nonces that we've received for all musig2 swap-in + val receivedNonces: Map = when (session.txCompleteReceived) { + null -> mapOf() + else -> (localInputs.filterIsInstance() + remoteInputs.filterIsInstance()) + .sortedBy { it.serialId } + .zip(session.txCompleteReceived.publicNonces) + .associate { it.first.serialId to it.second } + } + + // If we are swapping funds in, we provide our partial signatures to the corresponding inputs. val swapUserSigs = unsignedTx.txIn.mapIndexed { i, txIn -> localInputs @@ -477,8 +487,8 @@ data class SharedTransaction( ?.let { input -> val userNonce = input.secretNonce require(session.txCompleteReceived != null) - val serverNonce = session.txCompleteReceived.publicNonces[input.serialId] - require(serverNonce != null) + val serverNonce = receivedNonces[input.serialId] + require(serverNonce != null) { "missing server nonce for input ${input.serialId}" } val commonNonce = PublicNonce.aggregate(listOf(userNonce.publicNonce(), serverNonce)) TxSignatures.Companion.PartialSignature(keyManager.swapInOnChainWallet.signSwapInputUserMusig2(unsignedTx, i, previousOutputs, userNonce, serverNonce), commonNonce) } @@ -504,8 +514,8 @@ data class SharedTransaction( val serverKey = keyManager.swapInOnChainWallet.localServerPrivateKey(remoteNodeId) val userNonce = input.secretNonce require(session.txCompleteReceived != null) - val serverNonce = session.txCompleteReceived.publicNonces[input.serialId] - require(serverNonce != null) + val serverNonce = receivedNonces[input.serialId] + require(serverNonce != null) { "missing server nonce for input ${input.serialId}" } val commonNonce = PublicNonce.aggregate(listOf(userNonce.publicNonce(), serverNonce)) val swapInProtocol = SwapInProtocolMusig2(input.swapInParams.userKey, serverKey.publicKey(), input.swapInParams.userRefundKey, input.swapInParams.refundDelay) TxSignatures.Companion.PartialSignature(swapInProtocol.signSwapInputServer(unsignedTx, i, previousOutputs, serverNonce, serverKey, userNonce), commonNonce) @@ -568,7 +578,7 @@ data class FullySignedSharedTransaction(override val tx: SharedTransaction, over val localSwapTxInMusig2 = tx.localInputs.filterIsInstance().sortedBy { i -> i.serialId }.zip(localSigs.swapInUserPartialSigs.zip(remoteSigs.swapInServerPartialSigs)).map { (i, sigs) -> val (userSig, serverSig) = sigs val swapInProtocol = SwapInProtocolMusig2(i.swapInParams) - require(userSig.aggregatedPublicNonce == serverSig.aggregatedPublicNonce){ "aggregated public nonces mismatch for local input ${i.serialId}"} + require(userSig.aggregatedPublicNonce == serverSig.aggregatedPublicNonce) { "aggregated public nonces mismatch for local input ${i.serialId}" } val commonNonce = userSig.aggregatedPublicNonce val unsignedTx = tx.buildUnsignedTx() val ctx = swapInProtocol.signingCtx(unsignedTx, unsignedTx.txIn.indexOfFirst { it.outPoint == i.outPoint }, unsignedTx.txIn.map { tx.spentOutputs[it.outPoint]!! }, commonNonce) @@ -587,7 +597,7 @@ data class FullySignedSharedTransaction(override val tx: SharedTransaction, over val remoteSwapTxInMusig2 = tx.remoteInputs.filterIsInstance().sortedBy { i -> i.serialId }.zip(remoteSigs.swapInUserPartialSigs.zip(localSigs.swapInServerPartialSigs)).map { (i, sigs) -> val (userSig, serverSig) = sigs val swapInProtocol = SwapInProtocolMusig2(i.swapInParams) - require(userSig.aggregatedPublicNonce == serverSig.aggregatedPublicNonce){ "aggregated public nonces mismatch for remote input ${i.serialId}"} + require(userSig.aggregatedPublicNonce == serverSig.aggregatedPublicNonce) { "aggregated public nonces mismatch for remote input ${i.serialId}" } val commonNonce = userSig.aggregatedPublicNonce val unsignedTx = tx.buildUnsignedTx() val ctx = swapInProtocol.signingCtx(unsignedTx, unsignedTx.txIn.indexOfFirst { it.outPoint == i.outPoint }, unsignedTx.txIn.map { tx.spentOutputs[it.outPoint]!! }, commonNonce) @@ -689,10 +699,10 @@ data class InteractiveTxSession( null -> { // generate a new secret nonce for each musig2 new swapin every time we send TxComplete val localMusig2SwapIns = localInputs.filterIsInstance() - val localNonces = localMusig2SwapIns.map { it.serialId to it.secretNonce.publicNonce() }.toMap() + val localNonces = localMusig2SwapIns.map { it.serialId to it.secretNonce.publicNonce() } val remoteMusig2SwapIns = remoteInputs.filterIsInstance() - val remoteNonces = remoteMusig2SwapIns.map { it.serialId to it.secretNonce.publicNonce() }.toMap() - val txComplete = TxComplete(fundingParams.channelId, (localNonces + remoteNonces)) + val remoteNonces = remoteMusig2SwapIns.map { it.serialId to it.secretNonce.publicNonce() } + val txComplete = TxComplete(fundingParams.channelId, (localNonces + remoteNonces).sortedBy { it.first }.map { it.second }) val next = copy(txCompleteSent = txComplete) if (next.isComplete) { Pair(next, next.validateTx(txComplete)) @@ -885,11 +895,16 @@ data class InteractiveTxSession( } sharedInputs.first() } + val receivedNonces = (localInputs.filterIsInstance() + remoteInputs.filterIsInstance()) + .sortedBy { it.serialId } + .zip(txCompleteReceived.publicNonces) + .associate { it.first.serialId to it.second } + localOnlyInputs.filterIsInstance().forEach { - txCompleteReceived.publicNonces[it.serialId] ?: return InteractiveTxSessionAction.MissingNonce(fundingParams.channelId, it.serialId) + receivedNonces[it.serialId] ?: return InteractiveTxSessionAction.MissingNonce(fundingParams.channelId, it.serialId) } remoteOnlyInputs.filterIsInstance().forEach { - txCompleteReceived.publicNonces[it.serialId] ?: return InteractiveTxSessionAction.MissingNonce(fundingParams.channelId, it.serialId) + receivedNonces[it.serialId] ?: return InteractiveTxSessionAction.MissingNonce(fundingParams.channelId, it.serialId) } val sharedTx = SharedTransaction(sharedInput, sharedOutput, localOnlyInputs, remoteOnlyInputs, localOnlyOutputs, remoteOnlyOutputs, fundingParams.lockTime) val tx = sharedTx.buildUnsignedTx() diff --git a/src/commonMain/kotlin/fr/acinq/lightning/wire/InteractiveTxTlv.kt b/src/commonMain/kotlin/fr/acinq/lightning/wire/InteractiveTxTlv.kt index 0419609ac..54684306c 100644 --- a/src/commonMain/kotlin/fr/acinq/lightning/wire/InteractiveTxTlv.kt +++ b/src/commonMain/kotlin/fr/acinq/lightning/wire/InteractiveTxTlv.kt @@ -72,27 +72,20 @@ sealed class TxRemoveInputTlv : Tlv sealed class TxRemoveOutputTlv : Tlv sealed class TxCompleteTlv : Tlv { - data class Nonces(val nonces: Map): TxCompleteTlv() { + /** nonces for all Musig2 swap-in inputs, ordered by serial id */ + data class Nonces(val nonces: List): TxCompleteTlv() { override val tag: Long get() = Nonces.tag override fun write(out: Output) { - LightningCodecs.writeU16(nonces.size, out) - nonces.forEach { (serialId, nonce) -> - LightningCodecs.writeBigSize(serialId, out) - LightningCodecs.writeBytes(nonce.toByteArray(), out) - } + nonces.forEach { LightningCodecs.writeBytes(it.toByteArray(), out) } } companion object : TlvValueReader { const val tag: Long = 101 override fun read(input: Input): Nonces { - val noncesCount = LightningCodecs.u16(input) - val nonces = (1..noncesCount).map { - val serialId = LightningCodecs.bigSize(input) - val nonce = PublicNonce.fromBin(LightningCodecs.bytes(input, 66)) - serialId to nonce - } - return Nonces(nonces.toMap()) + val count = input.availableBytes / 66 + val nonces = (0 until count).map { PublicNonce.fromBin(LightningCodecs.bytes(input, 66)) } + return Nonces(nonces) } } } diff --git a/src/commonMain/kotlin/fr/acinq/lightning/wire/LightningMessages.kt b/src/commonMain/kotlin/fr/acinq/lightning/wire/LightningMessages.kt index f2986ec14..eec440ff7 100644 --- a/src/commonMain/kotlin/fr/acinq/lightning/wire/LightningMessages.kt +++ b/src/commonMain/kotlin/fr/acinq/lightning/wire/LightningMessages.kt @@ -451,9 +451,9 @@ data class TxComplete( ) : InteractiveTxConstructionMessage(), HasChannelId { override val type: Long get() = TxComplete.type - val publicNonces: Map = tlvs.get()?.nonces?.toMap() ?: mapOf() + val publicNonces: List = tlvs.get()?.nonces ?: listOf() - constructor(channelId: ByteVector32, publicNonces: Map) : this(channelId, TlvStream(TxCompleteTlv.Nonces(publicNonces))) + constructor(channelId: ByteVector32, publicNonces: List) : this(channelId, TlvStream(TxCompleteTlv.Nonces(publicNonces))) override fun write(out: Output) { LightningCodecs.writeBytes(channelId.toByteArray(), out)