Skip to content

Commit

Permalink
WIP Add musig2-based swap-in protocol
Browse files Browse the repository at this point in the history
  • Loading branch information
sstone committed Nov 13, 2023
1 parent 3282d1b commit 3fec17f
Show file tree
Hide file tree
Showing 16 changed files with 444 additions and 118 deletions.
280 changes: 241 additions & 39 deletions src/commonMain/kotlin/fr/acinq/lightning/channel/InteractiveTx.kt

Large diffs are not rendered by default.

36 changes: 23 additions & 13 deletions src/commonMain/kotlin/fr/acinq/lightning/crypto/KeyManager.kt
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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<ScriptElt> = swapInProtocol.redeemScript
val pubkeyScript: List<ScriptElt> = swapInProtocol.pubkeyScript
val address: String = swapInProtocol.address(chain)
val swapInProtocolMusig2 = SwapInProtocolMusig2(userPublicKey, remoteServerPublicKey, refundDelay)

/**
* The output script descriptor matching our swap-in addresses.
Expand All @@ -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<TxOut>): 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<TxOut>, 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
Expand All @@ -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 {
Expand All @@ -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
Expand Down
3 changes: 2 additions & 1 deletion src/commonMain/kotlin/fr/acinq/lightning/io/Peer.kt
Original file line number Diff line number Diff line change
Expand Up @@ -187,7 +187,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

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -222,9 +223,8 @@ object Deserialization {
previousTx = readTransaction(),
previousTxOutput = readNumber(),
sequence = readNumber().toUInt(),
userKey = readPublicKey(),
serverKey = readPublicKey(),
refundDelay = readNumber().toInt(),
swapInParams = TxAddInputTlv.SwapInParams.read(this),
commonNonce = readNullable { readPublicNonce() }
)
else -> error("unknown discriminator $discriminator for class ${InteractiveTxInput.Local::class}")
}
Expand All @@ -241,18 +241,8 @@ 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),
commonNonce = readNullable { readPublicNonce() }
)
else -> error("unknown discriminator $discriminator for class ${InteractiveTxInput.Remote::class}")
}
Expand Down Expand Up @@ -312,6 +302,7 @@ object Deserialization {
localSigs = readLightningMessage() as TxSignatures,
remoteSigs = readLightningMessage() as TxSignatures,
sharedSigs = readNullable { readScriptWitness() },
spentOutputs = readCollection { readOutPoint() to readTxOut() }.toMap()
)
else -> error("unknown discriminator $discriminator for class ${SignedSharedTransaction::class}")
}
Expand Down Expand Up @@ -544,6 +535,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()) {
Expand Down Expand Up @@ -583,6 +576,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) }
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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.*
Expand All @@ -12,6 +13,7 @@ import fr.acinq.lightning.transactions.Transactions.TransactionWithInputInfo.*
import fr.acinq.lightning.utils.Either
import fr.acinq.lightning.wire.LightningCodecs
import fr.acinq.lightning.wire.LightningMessage
import fr.acinq.lightning.wire.TxAddInputTlv

/**
* Serialization for [ChannelStateWithCommitments].
Expand Down Expand Up @@ -273,9 +275,8 @@ object Serialization {
writeBtcObject(previousTx)
writeNumber(previousTxOutput)
writeNumber(sequence.toLong())
writePublicKey(userKey)
writePublicKey(serverKey)
writeNumber(refundDelay)
swapInParams.write(this@writeLocalInteractiveTxInput)
writeNullable(commonNonce){ writePublicNonce(it)}
}
}

Expand All @@ -293,19 +294,8 @@ 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)
writeNullable(commonNonce){ writePublicNonce(it)}
}
}

Expand Down Expand Up @@ -363,6 +353,10 @@ object Serialization {
writeLightningMessage(localSigs)
writeLightningMessage(remoteSigs)
writeNullable(sharedSigs) { writeScriptWitness(it) }
writeCollection(spentOutputs.toList()){ x ->
writeBtcObject(x.first)
writeBtcObject(x.second)
}
}
}

Expand Down Expand Up @@ -649,6 +643,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)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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(<user_key>),or(99@pk(<server_key>),older(<delayed_refund>)))
// @formatter:off
Expand All @@ -22,6 +27,8 @@ class SwapInProtocol(val userPublicKey: PublicKey, val serverPublicKey: PublicKe

val pubkeyScript: List<ScriptElt> = 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 {
Expand All @@ -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))
Expand All @@ -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))
Expand All @@ -64,9 +75,9 @@ class SwapInProtocolMusig2(val userPublicKey: PublicKey, val serverPublicKey: Pu
fun signSwapInputUser(fundingTx: Transaction, index: Int, parentTxOuts: List<TxOut>, 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
Expand All @@ -81,9 +92,9 @@ class SwapInProtocolMusig2(val userPublicKey: PublicKey, val serverPublicKey: Pu

fun signSwapInputServer(fundingTx: Transaction, index: Int, parentTxOuts: List<TxOut>, 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
Expand Down
Loading

0 comments on commit 3fec17f

Please sign in to comment.