diff --git a/src/commonMain/kotlin/fr/acinq/lightning/channel/InteractiveTx.kt b/src/commonMain/kotlin/fr/acinq/lightning/channel/InteractiveTx.kt index 5ee837ce2..8237a0833 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.* @@ -347,14 +348,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)) } @@ -406,13 +411,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 72dcf951c..ebdc6a556 100644 --- a/src/commonMain/kotlin/fr/acinq/lightning/transactions/Transactions.kt +++ b/src/commonMain/kotlin/fr/acinq/lightning/transactions/Transactions.kt @@ -791,18 +791,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()