diff --git a/src/commonMain/kotlin/fr/acinq/lightning/crypto/KeyManager.kt b/src/commonMain/kotlin/fr/acinq/lightning/crypto/KeyManager.kt index d47827c35..65ef0de9e 100644 --- a/src/commonMain/kotlin/fr/acinq/lightning/crypto/KeyManager.kt +++ b/src/commonMain/kotlin/fr/acinq/lightning/crypto/KeyManager.kt @@ -34,8 +34,6 @@ interface KeyManager { val swapInOnChainWallet: SwapInOnChainKeys - val swapInOnChainWalletV2: SwapInOnChainKeysV2 - /** * Keys used for the node. They are used to generate the node id, to secure communication with other peers, and * to sign network-wide public announcements. @@ -108,11 +106,15 @@ interface KeyManager { } /** - * We use a specific kind of swap-in where users send funds to a 2-of-2 multisig with a timelock refund. + * We use a specific kind of swap-in where users send funds to a shared address with a timelock refund. * Once confirmed, the swap-in utxos can be spent by one of two paths: * - with a signature from both [userPublicKey] and [remoteServerPublicKey] * - with a signature from [userPublicKey] after the [refundDelay] * The keys used are static across swaps to make recovery easier. + * + * There are 2 implementations for this protocol: + * - a legacy implementation that uses a script(multisig user + server || user + delay), which has a very specific onchain footprint + * - a implementation based on taproot and musig2 that looks like any p2tr transaction onchain */ data class SwapInOnChainKeys( private val chain: NodeParams.Chain, @@ -127,116 +129,20 @@ interface KeyManager { 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!! - - /** - * The output script descriptor matching our swap-in addresses. - * That descriptor can be imported in bitcoind to recover funds after the refund delay. - */ - val descriptor = run { - // Since child public keys cannot be derived from a master xpub when hardened derivation is used, - // we need to provide the fingerprint of the master xpub and the hardened derivation path. - // This lets wallets that have access to the master xpriv derive the corresponding private and public keys. - val masterFingerprint = ByteVector(Crypto.hash160(DeterministicWallet.publicKey(master).publickeybytes).take(4).toByteArray()) - val encodedChildKey = DeterministicWallet.encode(DeterministicWallet.publicKey(userExtendedPrivateKey), testnet = chain != NodeParams.Chain.Mainnet) - val userKey = "[${masterFingerprint.toHex()}/${encodedSwapInUserKeyPath(chain)}]$encodedChildKey" - "wsh(and_v(v:pk($userKey),or_d(pk(${remoteServerPublicKey.toHex()}),older($refundDelay))))" - } - - /** - * Create a recovery transaction that spends a swap-in transaction after the refund delay has passed - * @param swapInTx swap-in transaction - * @param address address to send funds to - * @param feeRate fee rate for the refund transaction - * @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)) } - return if (utxos.isEmpty()) { - null - } else { - val pubKeyScript = Bitcoin.addressToPublicKeyScript(chain.chainHash, address).result - pubKeyScript?.let { script -> - val ourOutput = TxOut(utxos.map { it.amount }.sum(), script) - val unsignedTx = Transaction( - version = 2, - txIn = utxos.map { TxIn(OutPoint(swapInTx, swapInTx.txOut.indexOf(it).toLong()), sequence = refundDelay.toLong()) }, - txOut = listOf(ourOutput), - lockTime = 0 - ) - 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)) - } - 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)) - } - // this tx is signed but cannot be published until swapInTx has `refundDelay` confirmations - recoveryTx - } - } - } - - companion object { - private fun swapInKeyBasePath(chain: NodeParams.Chain) = when (chain) { - NodeParams.Chain.Regtest, NodeParams.Chain.Testnet -> KeyPath.empty / hardened(51) / hardened(0) - NodeParams.Chain.Mainnet -> KeyPath.empty / hardened(52) / hardened(0) - } - - fun swapInUserKeyPath(chain: NodeParams.Chain) = swapInKeyBasePath(chain) / hardened(0) - - fun swapInLocalServerKeyPath(chain: NodeParams.Chain) = swapInKeyBasePath(chain) / hardened(1) - - fun encodedSwapInUserKeyPath(chain: NodeParams.Chain) = when (chain) { - NodeParams.Chain.Regtest, NodeParams.Chain.Testnet -> "51h/0h/0h" - NodeParams.Chain.Mainnet -> "52h/0h/0h" - } - - /** Swap-in servers use a different swap-in key for different users. */ - fun perUserPath(remoteNodeId: PublicKey): KeyPath { - // We hash the remote node_id and break it into 2-byte values to get non-hardened path indices. - val h = ByteArrayInput(Crypto.sha256(remoteNodeId.value)) - return KeyPath((0 until 16).map { _ -> LightningCodecs.u16(h).toLong() }) - } - } - } - - /** - * We use a specific kind of swap-in where users send funds to a 2-of-2 multisig with a timelock refund. - * Once confirmed, the swap-in utxos can be spent by one of two paths: - * - with a signature from both [userPublicKey] and [remoteServerPublicKey] - * - with a signature from [userPublicKey] after the [refundDelay] - * The keys used are static across swaps to make recovery easier. - */ - data class SwapInOnChainKeysV2( - private val chain: NodeParams.Chain, - private val master: DeterministicWallet.ExtendedPrivateKey, - val remoteServerPublicKey: PublicKey, - val refundDelay: Int = DefaultSwapInParams.RefundDelay - ) { - private val userExtendedPrivateKey: DeterministicWallet.ExtendedPrivateKey = DeterministicWallet.derivePrivateKey(master, swapInUserKeyPath(chain)) - 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 + // legacy swap-in-potentiam that uses standard multisig 2-of-2 + private val legacyRedeemScript: List = Scripts.swapIn2of2(userPublicKey, remoteServerPublicKey, refundDelay) + val legacyPubkeyScript: List = Script.pay2wsh(legacyRedeemScript) + val legacyAddress: String = Bitcoin.addressFromPublicKeyScript(chain.chainHash, legacyPubkeyScript).result!! + // swap-in-potentiam uses musig2 and taproot // 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) - val scriptTree = ScriptTree.Leaf(ScriptLeaf(0, Script.write(redeemScript).byteVector(), Script.TAPROOT_LEAF_TAPSCRIPT)) - val merkleRoot = ScriptTree.hash(scriptTree) - - // User and Server exchange public keys and agree on a common aggregated key - val internalPubKey = Musig2.keyAgg(listOf(userPublicKey, remoteServerPublicKey)).Q.xOnly() - val commonPubKeyAndParity = internalPubKey.outputKey(Crypto.TaprootTweak.ScriptTweak(merkleRoot)) - + private 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) + // User and Server exchange public keys and agree on a common musig2 aggregated key, the swapin address is the p2tr address for that musig2 public key tweaked + // with our script tree merkle root which has only one leaf: the refund script + private val internalPubKey = Musig2.keyAgg(listOf(userPublicKey, remoteServerPublicKey)).Q.xOnly() + private val commonPubKeyAndParity = internalPubKey.outputKey(Crypto.TaprootTweak.ScriptTweak(merkleRoot)) val pubkeyScript: List = Script.pay2tr(commonPubKeyAndParity.first) val address: String = Bitcoin.addressFromPublicKeyScript(chain.chainHash, pubkeyScript).result!! @@ -244,7 +150,7 @@ interface KeyManager { * The output script descriptor matching our swap-in addresses. * That descriptor can be imported in bitcoind to recover funds after the refund delay. */ - val descriptor = run { + val legacyDescriptor = run { // Since child public keys cannot be derived from a master xpub when hardened derivation is used, // we need to provide the fingerprint of the master xpub and the hardened derivation path. // This lets wallets that have access to the master xpriv derive the corresponding private and public keys. @@ -254,6 +160,9 @@ interface KeyManager { "wsh(and_v(v:pk($userKey),or_d(pk(${remoteServerPublicKey.toHex()}),older($refundDelay))))" } + // TODO: this is wrong. what should we use here ? + val descriptor = legacyDescriptor + /** * Create a recovery transaction that spends a swap-in transaction after the refund delay has passed * @param swapInTx swap-in transaction @@ -262,7 +171,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(legacyPubkeyScript)) || it.publicKeyScript.contentEquals(Script.write(pubkeyScript)) } return if (utxos.isEmpty()) { null } else { @@ -275,21 +184,31 @@ interface KeyManager { txOut = listOf(ourOutput), lockTime = 0 ) + val controlBlock = byteArrayOf((Script.TAPROOT_LEAF_TAPSCRIPT + (if (commonPubKeyAndParity.second) 1 else 0)).toByte()) + internalPubKey.value.toByteArray() val execData = Script.ExecutionData(annex = null, tapleafHash = merkleRoot) - fun signTx(unsignedTx: Transaction): Transaction = utxos.foldIndexed(unsignedTx) { index, tx, _ -> - val txHash = Transaction.hashForSigningSchnorr(tx, index, utxos, SigHash.SIGHASH_DEFAULT, SigVersion.SIGVERSION_TAPSCRIPT, execData) - val sig = Crypto.signSchnorr(txHash, userPrivateKey, Crypto.SchnorrTweak.NoTweak) - tx.updateWitness(index, ScriptWitness.empty.push(sig).push(redeemScript).push(controlBlock)) + fun signInput(tx: Transaction, index: Int, utxo: TxOut): Transaction { + return if (utxo.publicKeyScript.contentEquals(Script.write(legacyPubkeyScript))) { + val sig = Transactions.signSwapInputUser(tx, index, utxo, userPrivateKey, remoteServerPublicKey, refundDelay) + tx.updateWitness(index, Scripts.witnessSwapIn2of2Refund(sig, userPublicKey, remoteServerPublicKey, refundDelay)) + } else { + val txHash = Transaction.hashForSigningSchnorr(tx, index, utxos, SigHash.SIGHASH_DEFAULT, SigVersion.SIGVERSION_TAPSCRIPT, execData) + val sig = Crypto.signSchnorr(txHash, userPrivateKey, Crypto.SchnorrTweak.NoTweak) + tx.updateWitness(index, ScriptWitness.empty.push(sig).push(redeemScript).push(controlBlock)) + } } val fees = run { - val recoveryTx = signTx(unsignedTx) + val recoveryTx = utxos.foldIndexed(unsignedTx) { index, tx, utxo -> + signInput(tx, index, utxo) + } Transactions.weight2fee(feeRate, recoveryTx.weight()) } val unsignedTx1 = unsignedTx.copy(txOut = listOf(ourOutput.copy(amount = ourOutput.amount - fees))) - val recoveryTx = signTx(unsignedTx1) + val recoveryTx = utxos.foldIndexed(unsignedTx1) { index, tx, utxo -> + signInput(tx, index, utxo) + } // this tx is signed but cannot be published until swapInTx has `refundDelay` confirmations recoveryTx } @@ -319,5 +238,4 @@ interface KeyManager { } } } - } diff --git a/src/commonMain/kotlin/fr/acinq/lightning/crypto/LocalKeyManager.kt b/src/commonMain/kotlin/fr/acinq/lightning/crypto/LocalKeyManager.kt index 29c177363..a9811c618 100644 --- a/src/commonMain/kotlin/fr/acinq/lightning/crypto/LocalKeyManager.kt +++ b/src/commonMain/kotlin/fr/acinq/lightning/crypto/LocalKeyManager.kt @@ -54,16 +54,6 @@ data class LocalKeyManager(val seed: ByteVector, val chain: Chain, val remoteSwa val remoteSwapInPublicKey = DeterministicWallet.derivePublicKey(xpub, KeyManager.SwapInOnChainKeys.perUserPath(nodeKeys.nodeKey.publicKey)).publicKey KeyManager.SwapInOnChainKeys(chain, master, remoteSwapInPublicKey) } - override val swapInOnChainWalletV2: KeyManager.SwapInOnChainKeysV2 = run { - val (prefix, xpub) = DeterministicWallet.ExtendedPublicKey.decode(remoteSwapInExtendedPublicKey) - val expectedPrefix = when (chain) { - Chain.Mainnet -> DeterministicWallet.xpub - else -> DeterministicWallet.tpub - } - require(prefix == expectedPrefix) { "unexpected swap-in xpub prefix $prefix (expected $expectedPrefix)" } - val remoteSwapInPublicKey = DeterministicWallet.derivePublicKey(xpub, KeyManager.SwapInOnChainKeys.perUserPath(nodeKeys.nodeKey.publicKey)).publicKey - KeyManager.SwapInOnChainKeysV2(chain, master, remoteSwapInPublicKey) - } private val channelKeyBasePath: KeyPath = channelKeyBasePath(chain) /** diff --git a/src/commonMain/kotlin/fr/acinq/lightning/io/Peer.kt b/src/commonMain/kotlin/fr/acinq/lightning/io/Peer.kt index 05eec50b1..c6b510021 100644 --- a/src/commonMain/kotlin/fr/acinq/lightning/io/Peer.kt +++ b/src/commonMain/kotlin/fr/acinq/lightning/io/Peer.kt @@ -188,6 +188,7 @@ class Peer( 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 legacySwapInAddress: String = nodeParams.keyManager.swapInOnChainWallet.legacyAddress.also { swapInWallet.addAddress(it) } private var swapInJob: Job? = null diff --git a/src/commonMain/kotlin/fr/acinq/lightning/transactions/Scripts.kt b/src/commonMain/kotlin/fr/acinq/lightning/transactions/Scripts.kt index d36431ae1..1a0f76441 100644 --- a/src/commonMain/kotlin/fr/acinq/lightning/transactions/Scripts.kt +++ b/src/commonMain/kotlin/fr/acinq/lightning/transactions/Scripts.kt @@ -2,6 +2,7 @@ package fr.acinq.lightning.transactions import fr.acinq.bitcoin.* import fr.acinq.bitcoin.ScriptEltMapping.code2elt +import fr.acinq.bitcoin.musig2.Musig2 import fr.acinq.lightning.CltvExpiry import fr.acinq.lightning.CltvExpiryDelta import fr.acinq.lightning.utils.sat @@ -31,9 +32,21 @@ object Scripts { } /** - * @return the script used for a 2-of-2 swap-in as used in Phoenix. + * @return the script used for a musig2 2-of-2 swap-in as used in Phoenix. */ fun swapIn2of2(userKey: PublicKey, serverKey: PublicKey, delayedRefund: Int): List { + val redeemScript = listOf(OP_PUSHDATA(userKey.xOnly()), OP_CHECKSIGVERIFY, OP_PUSHDATA(Script.encodeNumber(delayedRefund)), OP_CHECKSEQUENCEVERIFY) + val scriptTree = ScriptTree.Leaf(ScriptLeaf(0, Script.write(redeemScript).byteVector(), Script.TAPROOT_LEAF_TAPSCRIPT)) + val merkleRoot = ScriptTree.hash(scriptTree) + val internalPubKey = Musig2.keyAgg(listOf(userKey, serverKey)).Q.xOnly() + val (commonPubKey, _) = internalPubKey.outputKey(Crypto.TaprootTweak.ScriptTweak(merkleRoot)) + return Script.pay2tr(commonPubKey) + } + + /** + * @return the script used for a legacy 2-of-2 swap-in as used in Phoenix. + */ + fun swapIn2of2Legacy(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 diff --git a/src/commonMain/kotlin/fr/acinq/lightning/transactions/Transactions.kt b/src/commonMain/kotlin/fr/acinq/lightning/transactions/Transactions.kt index 72dcf951c..a43650de2 100644 --- a/src/commonMain/kotlin/fr/acinq/lightning/transactions/Transactions.kt +++ b/src/commonMain/kotlin/fr/acinq/lightning/transactions/Transactions.kt @@ -18,6 +18,10 @@ package fr.acinq.lightning.transactions import fr.acinq.bitcoin.* import fr.acinq.bitcoin.crypto.Pack +import fr.acinq.bitcoin.musig2.Musig2 +import fr.acinq.bitcoin.musig2.PublicNonce +import fr.acinq.bitcoin.musig2.SecretNonce +import fr.acinq.bitcoin.musig2.SessionCtx import fr.acinq.lightning.CltvExpiryDelta import fr.acinq.lightning.MilliSatoshi import fr.acinq.lightning.blockchain.fee.FeeratePerKw @@ -803,6 +807,55 @@ object Transactions { return sign(fundingTx, index, Script.write(redeemScript), parentTxOut.amount, serverKey) } + fun signSwapInputUser(fundingTx: Transaction, index: Int, parentTxOuts: List, userKey: PrivateKey, userNonce: SecretNonce, serverKey: PublicKey, serverNonce: PublicNonce, refundDelay: Int): Pair { + val redeemScript = listOf(OP_PUSHDATA(userKey.publicKey().xOnly()), OP_CHECKSIGVERIFY, OP_PUSHDATA(Script.encodeNumber(refundDelay)), OP_CHECKSEQUENCEVERIFY) + val scriptTree = ScriptTree.Leaf(ScriptLeaf(0, Script.write(redeemScript).byteVector(), Script.TAPROOT_LEAF_TAPSCRIPT)) + val merkleRoot = ScriptTree.hash(scriptTree) + val internalPubKey = Musig2.keyAgg(listOf(userKey.publicKey(), serverKey)).Q.xOnly() + val txHash = Transaction.hashForSigningSchnorr(fundingTx, index, parentTxOuts, SigHash.SIGHASH_DEFAULT, SigVersion.SIGVERSION_TAPROOT) + val commonNonce = PublicNonce.aggregate(listOf(userNonce.publicNonce(), serverNonce)) + val ctx = SessionCtx( + commonNonce, + listOf(userKey.publicKey(), serverKey), + listOf(Pair(internalPubKey.tweak(Crypto.TaprootTweak.ScriptTweak(merkleRoot)), true)), + txHash + ) + val userSig = ctx.sign(userNonce, userKey) + return Pair(userSig, commonNonce) + } + + fun signSwapInputServer(fundingTx: Transaction, index: Int, parentTxOuts: List, userKey: PublicKey, userNonce: PublicNonce, serverKey: PrivateKey, serverNonce: SecretNonce, refundDelay: Int): Pair { + val redeemScript = listOf(OP_PUSHDATA(userKey.xOnly()), OP_CHECKSIGVERIFY, OP_PUSHDATA(Script.encodeNumber(refundDelay)), OP_CHECKSEQUENCEVERIFY) + val scriptTree = ScriptTree.Leaf(ScriptLeaf(0, Script.write(redeemScript).byteVector(), Script.TAPROOT_LEAF_TAPSCRIPT)) + val merkleRoot = ScriptTree.hash(scriptTree) + val internalPubKey = Musig2.keyAgg(listOf(userKey, serverKey.publicKey())).Q.xOnly() + val txHash = Transaction.hashForSigningSchnorr(fundingTx, index, parentTxOuts, SigHash.SIGHASH_DEFAULT, SigVersion.SIGVERSION_TAPROOT) + val commonNonce = PublicNonce.aggregate(listOf(userNonce, serverNonce.publicNonce())) + val ctx = SessionCtx( + commonNonce, + listOf(userKey, serverKey.publicKey()), + listOf(Pair(internalPubKey.tweak(Crypto.TaprootTweak.ScriptTweak(merkleRoot)), true)), + txHash + ) + val serverSig = ctx.sign(serverNonce, serverKey) + return Pair(serverSig, commonNonce) + } + + fun aggregateSwapInputSignatures(fundingTx: Transaction, index: Int, parentTxOuts: List, userKey: PublicKey, serverKey: PublicKey, userSig: ByteVector32, serverSig: ByteVector32, commonNonce: PublicNonce, refundDelay: Int) : ByteVector64 { + val txHash = Transaction.hashForSigningSchnorr(fundingTx, index, parentTxOuts, SigHash.SIGHASH_DEFAULT, SigVersion.SIGVERSION_TAPROOT) + val redeemScript = listOf(OP_PUSHDATA(userKey.xOnly()), OP_CHECKSIGVERIFY, OP_PUSHDATA(Script.encodeNumber(refundDelay)), OP_CHECKSEQUENCEVERIFY) + val scriptTree = ScriptTree.Leaf(ScriptLeaf(0, Script.write(redeemScript).byteVector(), Script.TAPROOT_LEAF_TAPSCRIPT)) + val merkleRoot = ScriptTree.hash(scriptTree) + val internalPubKey = Musig2.keyAgg(listOf(userKey, serverKey)).Q.xOnly() + val ctx = SessionCtx( + commonNonce, + listOf(userKey, serverKey), + listOf(Pair(internalPubKey.tweak(Crypto.TaprootTweak.ScriptTweak(merkleRoot)), true)), + txHash + ) + return ctx.partialSigAgg(listOf(userSig, serverSig)) + } + fun addSigs( commitTx: TransactionWithInputInfo.CommitTx, localFundingPubkey: PublicKey, diff --git a/src/commonTest/kotlin/fr/acinq/lightning/crypto/LocalKeyManagerTestsCommon.kt b/src/commonTest/kotlin/fr/acinq/lightning/crypto/LocalKeyManagerTestsCommon.kt index 0e5ad9fdb..ff366033e 100644 --- a/src/commonTest/kotlin/fr/acinq/lightning/crypto/LocalKeyManagerTestsCommon.kt +++ b/src/commonTest/kotlin/fr/acinq/lightning/crypto/LocalKeyManagerTestsCommon.kt @@ -195,6 +195,8 @@ class LocalKeyManagerTestsCommon : LightningTestSuite() { val swapInTx = Transaction(version = 2, txIn = listOf(), txOut = listOf( + TxOut(Satoshi(100000), Bitcoin.addressToPublicKeyScript(Block.RegtestGenesisBlock.hash, TestConstants.Alice.keyManager.swapInOnChainWallet.legacyAddress).result!!), + TxOut(Satoshi(150000), Bitcoin.addressToPublicKeyScript(Block.RegtestGenesisBlock.hash, TestConstants.Alice.keyManager.swapInOnChainWallet.legacyAddress).result!!), 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!!) ), @@ -204,20 +206,6 @@ class LocalKeyManagerTestsCommon : LightningTestSuite() { Transaction.correctlySpends(recoveryTx, swapInTx, ScriptFlags.STANDARD_SCRIPT_VERIFY_FLAGS) } - @Test - fun `spend swap-in v2 transactions`() { - val swapInTx = Transaction(version = 2, - txIn = listOf(), - txOut = listOf( - TxOut(Satoshi(100000), Bitcoin.addressToPublicKeyScript(Block.RegtestGenesisBlock.hash, TestConstants.Alice.keyManager.swapInOnChainWalletV2.address).result!!), - TxOut(Satoshi(150000), Bitcoin.addressToPublicKeyScript(Block.RegtestGenesisBlock.hash, TestConstants.Alice.keyManager.swapInOnChainWalletV2.address).result!!) - ), - lockTime = 0) - val recoveryTx = TestConstants.Alice.keyManager.swapInOnChainWalletV2.createRecoveryTransaction(swapInTx, TestConstants.Alice.keyManager.finalOnChainWallet.address(0), FeeratePerKw(FeeratePerByte(Satoshi(5))))!! - assertEquals(swapInTx.txOut.size, recoveryTx.txIn.size) - Transaction.correctlySpends(recoveryTx, swapInTx, ScriptFlags.STANDARD_SCRIPT_VERIFY_FLAGS) - } - companion object { val dummyExtendedPubkey = DeterministicWallet.publicKey(DeterministicWallet.generate(ByteVector("deadbeef"))) } diff --git a/src/commonTest/kotlin/fr/acinq/lightning/transactions/TransactionsTestsCommon.kt b/src/commonTest/kotlin/fr/acinq/lightning/transactions/TransactionsTestsCommon.kt index f29267399..25c8af70e 100644 --- a/src/commonTest/kotlin/fr/acinq/lightning/transactions/TransactionsTestsCommon.kt +++ b/src/commonTest/kotlin/fr/acinq/lightning/transactions/TransactionsTestsCommon.kt @@ -570,7 +570,7 @@ class TransactionsTestsCommon : LightningTestSuite() { val swapInTx = Transaction( version = 2, txIn = listOf(), - txOut = listOf(TxOut(Satoshi(10000), listOf(OP_1, OP_PUSHDATA(commonPubKey)))), + txOut = listOf(TxOut(Satoshi(10000), Scripts.swapIn2of2(userPrivateKey.publicKey(), serverPrivateKey.publicKey(), refundDelay))), lockTime = 0 ) @@ -586,20 +586,9 @@ class TransactionsTestsCommon : LightningTestSuite() { // signatures they will have to start again with fresh nonces val userNonce = SecretNonce.generate(userPrivateKey, userPrivateKey.publicKey(), commonPubKey, null, null, randomBytes32()) val serverNonce = SecretNonce.generate(serverPrivateKey, serverPrivateKey.publicKey(), commonPubKey, null, null, randomBytes32()) - - val txHash = Transaction.hashForSigningSchnorr(tx, 0, swapInTx.txOut, SigHash.SIGHASH_DEFAULT, SigVersion.SIGVERSION_TAPROOT) - - val ctx = SessionCtx( - PublicNonce.aggregate(listOf(userNonce.publicNonce(), serverNonce.publicNonce())), - listOf(userPrivateKey.publicKey(), serverPrivateKey.publicKey()), - listOf(Pair(internalPubKey.tweak(Crypto.TaprootTweak.ScriptTweak(merkleRoot)), true)), - txHash - ) - val userSig = ctx.sign(userNonce, userPrivateKey) - val severSig = ctx.sign(serverNonce, serverPrivateKey) - val commonSig = ctx.partialSigAgg(listOf(userSig, severSig)) - // end of the musig2 signing session - + val (userSig, commonNonce) = Transactions.signSwapInputUser(tx, 0, swapInTx.txOut, userPrivateKey, userNonce, serverPrivateKey.publicKey(), serverNonce.publicNonce(), refundDelay) + val (serverSig, _) = Transactions.signSwapInputServer(tx, 0, swapInTx.txOut, userPrivateKey.publicKey(), userNonce.publicNonce(), serverPrivateKey, serverNonce, refundDelay) + val commonSig = Transactions.aggregateSwapInputSignatures(tx, 0, swapInTx.txOut, userPrivateKey.publicKey(), serverPrivateKey.publicKey(), userSig, serverSig, commonNonce, refundDelay) val signedTx = tx.updateWitness(0, ScriptWitness(listOf(commonSig))) Transaction.correctlySpends(signedTx, swapInTx, ScriptFlags.STANDARD_SCRIPT_VERIFY_FLAGS) }