Skip to content

Commit

Permalink
Move swap-in related methods into their own class
Browse files Browse the repository at this point in the history
  • Loading branch information
sstone committed Oct 30, 2023
1 parent 56ade7e commit 98d9623
Show file tree
Hide file tree
Showing 6 changed files with 78 additions and 60 deletions.
15 changes: 11 additions & 4 deletions src/commonMain/kotlin/fr/acinq/lightning/channel/InteractiveTx.kt
Original file line number Diff line number Diff line change
Expand Up @@ -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.*
Expand Down Expand Up @@ -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<InteractiveTxInput.RemoteSwapIn>()
.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))
}
Expand Down Expand Up @@ -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 }
Expand Down
28 changes: 20 additions & 8 deletions src/commonMain/kotlin/fr/acinq/lightning/crypto/KeyManager.kt
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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<ScriptElt> = Scripts.swapIn2of2(userPublicKey, remoteServerPublicKey, refundDelay)
val pubkeyScript: List<ScriptElt> = Script.pay2wsh(redeemScript)
val address: String = Bitcoin.addressFromPublicKeyScript(chain.chainHash, pubkeyScript).result!!
val swapInProtocol = SwapInProtocol(userPublicKey, remoteServerPublicKey, refundDelay)
val redeemScript: List<ScriptElt> = swapInProtocol.redeemScript
val pubkeyScript: List<ScriptElt> = swapInProtocol.pubkeyScript
val address: String = swapInProtocol.address(chain)

/**
* The output script descriptor matching our swap-in addresses.
Expand All @@ -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
Expand All @@ -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
Expand Down
26 changes: 0 additions & 26 deletions src/commonMain/kotlin/fr/acinq/lightning/transactions/Scripts.kt
Original file line number Diff line number Diff line change
Expand Up @@ -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<ScriptElt> {
// This script was generated with https://bitcoin.sipa.be/miniscript/ using the following miniscript policy:
// and(pk(<user_key>),or(99@pk(<server_key>),older(<delayed_refund>)))
// @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
Expand Down
Original file line number Diff line number Diff line change
@@ -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(<user_key>),or(99@pk(<server_key>),older(<delayed_refund>)))
// @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<ScriptElt> = 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)
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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)
}
Expand All @@ -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)
}
Expand All @@ -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()
Expand Down

0 comments on commit 98d9623

Please sign in to comment.