Skip to content

Commit

Permalink
Use musig2 helpers to simplify swap-in protocol (#592)
Browse files Browse the repository at this point in the history
We use the musig2 helpers exposed by ACINQ/bitcoin-kmp#114
to simplify the swap-in protocol and hide all of the musig2 internal
details (key aggregation cache, control block, internal taproot key,
opaque session object, nonce aggregation).

The code is simpler to reason about and signing is more similar to
signing normal single-sig inputs.
  • Loading branch information
t-bast authored Feb 5, 2024
1 parent bd7644c commit 23d0c40
Show file tree
Hide file tree
Showing 14 changed files with 330 additions and 399 deletions.
249 changes: 133 additions & 116 deletions src/commonMain/kotlin/fr/acinq/lightning/channel/InteractiveTx.kt

Large diffs are not rendered by default.

Original file line number Diff line number Diff line change
Expand Up @@ -378,6 +378,7 @@ data class Normal(
targetFeerate = cmd.message.feerate
)
val session = InteractiveTxSession(
staticParams.remoteNodeId,
channelKeys(),
keyManager.swapInOnChainWallet,
fundingParams,
Expand Down Expand Up @@ -459,6 +460,7 @@ data class Normal(
is Either.Right -> {
// The splice initiator always sends the first interactive-tx message.
val (interactiveTxSession, interactiveTxAction) = InteractiveTxSession(
staticParams.remoteNodeId,
channelKeys(),
keyManager.swapInOnChainWallet,
fundingParams,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -59,7 +59,7 @@ data class WaitForAcceptChannel(
}
is Either.Right -> {
// The channel initiator always sends the first interactive-tx message.
val (interactiveTxSession, interactiveTxAction) = InteractiveTxSession(channelKeys, keyManager.swapInOnChainWallet, fundingParams, 0.msat, 0.msat, fundingContributions.value).send()
val (interactiveTxSession, interactiveTxAction) = InteractiveTxSession(staticParams.remoteNodeId, channelKeys, keyManager.swapInOnChainWallet, fundingParams, 0.msat, 0.msat, fundingContributions.value).send()
when (interactiveTxAction) {
is InteractiveTxSessionAction.SendMessage -> {
val nextState = WaitForFundingCreated(
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -107,7 +107,7 @@ data class WaitForFundingConfirmed(
addAll(latestFundingTx.sharedTx.tx.localInputs.map { Either.Left(it) })
addAll(latestFundingTx.sharedTx.tx.localOutputs.map { Either.Right(it) })
}
val session = InteractiveTxSession(channelKeys(), keyManager.swapInOnChainWallet, fundingParams, SharedFundingInputBalances(0.msat, 0.msat), toSend, previousFundingTxs.map { it.sharedTx })
val session = InteractiveTxSession(staticParams.remoteNodeId, channelKeys(), keyManager.swapInOnChainWallet, fundingParams, SharedFundingInputBalances(0.msat, 0.msat), toSend, previousFundingTxs.map { it.sharedTx })
val nextState = this@WaitForFundingConfirmed.copy(rbfStatus = RbfStatus.InProgress(session))
Pair(nextState, listOf(ChannelAction.Message.Send(TxAckRbf(channelId, fundingParams.localContribution))))
}
Expand Down Expand Up @@ -142,7 +142,7 @@ data class WaitForFundingConfirmed(
Pair(this@WaitForFundingConfirmed.copy(rbfStatus = RbfStatus.RbfAborted), listOf(ChannelAction.Message.Send(TxAbort(channelId, ChannelFundingError(channelId).message))))
}
is Either.Right -> {
val (session, action) = InteractiveTxSession(channelKeys(), keyManager.swapInOnChainWallet, fundingParams, 0.msat, 0.msat, contributions.value, previousFundingTxs.map { it.sharedTx }).send()
val (session, action) = InteractiveTxSession(staticParams.remoteNodeId, channelKeys(), keyManager.swapInOnChainWallet, fundingParams, 0.msat, 0.msat, contributions.value, previousFundingTxs.map { it.sharedTx }).send()
when (action) {
is InteractiveTxSessionAction.SendMessage -> {
val nextState = this@WaitForFundingConfirmed.copy(rbfStatus = RbfStatus.InProgress(session))
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -86,7 +86,7 @@ data class WaitForOpenChannel(
Pair(Aborted, listOf(ChannelAction.Message.Send(Error(temporaryChannelId, ChannelFundingError(temporaryChannelId).message))))
}
is Either.Right -> {
val interactiveTxSession = InteractiveTxSession(channelKeys, keyManager.swapInOnChainWallet, fundingParams, 0.msat, 0.msat, fundingContributions.value)
val interactiveTxSession = InteractiveTxSession(staticParams.remoteNodeId, channelKeys, keyManager.swapInOnChainWallet, fundingParams, 0.msat, 0.msat, fundingContributions.value)
val nextState = WaitForFundingCreated(
localParams,
remoteParams,
Expand Down
51 changes: 15 additions & 36 deletions src/commonMain/kotlin/fr/acinq/lightning/crypto/KeyManager.kt
Original file line number Diff line number Diff line change
Expand Up @@ -2,16 +2,15 @@ package fr.acinq.lightning.crypto

import fr.acinq.bitcoin.*
import fr.acinq.bitcoin.DeterministicWallet.hardened
import fr.acinq.bitcoin.crypto.musig2.AggregatedNonce
import fr.acinq.bitcoin.crypto.musig2.IndividualNonce
import fr.acinq.bitcoin.crypto.musig2.SecretNonce
import fr.acinq.bitcoin.io.ByteArrayInput
import fr.acinq.bitcoin.utils.Either
import fr.acinq.lightning.DefaultSwapInParams
import fr.acinq.lightning.NodeParams
import fr.acinq.lightning.blockchain.fee.FeeratePerKw
import fr.acinq.lightning.transactions.SwapInProtocolLegacy
import fr.acinq.lightning.transactions.SwapInProtocol
import fr.acinq.lightning.transactions.SwapInProtocolLegacy
import fr.acinq.lightning.transactions.Transactions
import fr.acinq.lightning.utils.sum
import fr.acinq.lightning.utils.toByteVector
Expand Down Expand Up @@ -124,8 +123,6 @@ interface KeyManager {
) {
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()
Expand All @@ -136,32 +133,19 @@ interface KeyManager {
private val localServerExtendedPrivateKey: DeterministicWallet.ExtendedPrivateKey = DeterministicWallet.derivePrivateKey(master, swapInLocalServerKeyPath(chain))
fun localServerPrivateKey(remoteNodeId: PublicKey): PrivateKey = DeterministicWallet.derivePrivateKey(localServerExtendedPrivateKey, perUserPath(remoteNodeId)).privateKey

// legacy p2wsh-based swap-in protocol, with a fixed on-chain address
val legacySwapInProtocol = SwapInProtocolLegacy(userPublicKey, remoteServerPublicKey, refundDelay)

val swapInProtocol = SwapInProtocol(userPublicKey, remoteServerPublicKey, userRefundPublicKey, refundDelay)
val descriptor = SwapInProtocol.descriptor(chain, userPublicKey, remoteServerPublicKey, refundDelay, userRefundExtendedPrivateKey)
val descriptor = swapInProtocol.descriptor(chain, userRefundExtendedPrivateKey)

/**
* The output script descriptor matching our legacy swap-in addresses.
* That descriptor can be imported in bitcoind to recover funds after the refund delay.
*/
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.
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))))"
}
// legacy p2wsh-based swap-in protocol, with a fixed on-chain address
val legacySwapInProtocol = SwapInProtocolLegacy(userPublicKey, remoteServerPublicKey, refundDelay)
val legacyDescriptor = legacySwapInProtocol.descriptor(chain, master, userExtendedPrivateKey)

fun signSwapInputUserLegacy(fundingTx: Transaction, index: Int, parentTxOuts: List<TxOut>): ByteVector64 {
return legacySwapInProtocol.signSwapInputUser(fundingTx, index, parentTxOuts[fundingTx.txIn[index].outPoint.index.toInt()] , userPrivateKey)
return legacySwapInProtocol.signSwapInputUser(fundingTx, index, parentTxOuts[fundingTx.txIn[index].outPoint.index.toInt()], userPrivateKey)
}

fun signSwapInputUser(fundingTx: Transaction, index: Int, parentTxOuts: List<TxOut>, userNonce: Pair<SecretNonce, IndividualNonce>, serverNonce: IndividualNonce): Either<Throwable, ByteVector32> {
return swapInProtocol.signSwapInputUser(fundingTx, index, parentTxOuts, userPrivateKey, userNonce, serverNonce)
fun signSwapInputUser(fundingTx: Transaction, index: Int, parentTxOuts: List<TxOut>, privateNonce: SecretNonce, userNonce: IndividualNonce, serverNonce: IndividualNonce): Either<Throwable, ByteVector32> {
return swapInProtocol.signSwapInputUser(fundingTx, index, parentTxOuts, userPrivateKey, privateNonce, userNonce, serverNonce)
}

/**
Expand All @@ -172,12 +156,11 @@ 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(legacySwapInProtocol.pubkeyScript)) || it.publicKeyScript.contentEquals(Script.write(swapInProtocol.pubkeyScript))}
val utxos = swapInTx.txOut.filter { it.publicKeyScript.contentEquals(Script.write(legacySwapInProtocol.pubkeyScript)) || it.publicKeyScript.contentEquals(Script.write(swapInProtocol.pubkeyScript)) }
return if (utxos.isEmpty()) {
null
} else {
val pubKeyScript = Bitcoin.addressToPublicKeyScript(chain.chainHash, address).right
pubKeyScript?.let { script ->
Bitcoin.addressToPublicKeyScript(chain.chainHash, address).right?.let { script ->
val ourOutput = TxOut(utxos.map { it.amount }.sum(), script)
val unsignedTx = Transaction(
version = 2,
Expand All @@ -187,7 +170,7 @@ interface KeyManager {
)

fun sign(tx: Transaction, index: Int, utxo: TxOut): Transaction {
return if (legacySwapInProtocol.isMine(utxo)) {
return if (Script.isPay2wsh(utxo.publicKeyScript.toByteArray())) {
val sig = legacySwapInProtocol.signSwapInputUser(tx, index, utxo, userPrivateKey)
tx.updateWitness(index, legacySwapInProtocol.witnessRefund(sig))
} else {
Expand All @@ -197,15 +180,11 @@ interface KeyManager {
}

val fees = run {
val recoveryTx = utxos.foldIndexed(unsignedTx) { index, tx, utxo ->
sign(tx, index, utxo)
}
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 ->
sign(tx, index, utxo)
}
val recoveryTx = utxos.foldIndexed(unsignedTx1) { index, tx, utxo -> sign(tx, index, utxo) }
// this tx is signed but cannot be published until swapInTx has `refundDelay` confirmations
recoveryTx
}
Expand All @@ -220,10 +199,10 @@ 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 swapInUserRefundKeyPath(chain: NodeParams.Chain) = swapInKeyBasePath(chain) / hardened(2) / 0L

fun encodedSwapInUserKeyPath(chain: NodeParams.Chain) = when (chain) {
NodeParams.Chain.Regtest, NodeParams.Chain.Testnet -> "51h/0h/0h"
NodeParams.Chain.Mainnet -> "52h/0h/0h"
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -249,7 +249,10 @@ object Deserialization {
previousTx = readTransaction(),
previousTxOutput = readNumber(),
sequence = readNumber().toUInt(),
swapInParams = TxAddInputTlv.SwapInParams.read(this)
userKey = readPublicKey(),
serverKey = readPublicKey(),
userRefundKey = readPublicKey(),
refundDelay = readNumber().toInt(),
)
else -> error("unknown discriminator $discriminator for class ${InteractiveTxInput.Local::class}")
}
Expand All @@ -269,13 +272,16 @@ object Deserialization {
userKey = readPublicKey(),
serverKey = readPublicKey(),
refundDelay = readNumber().toInt()
)
)
0x03 -> InteractiveTxInput.RemoteSwapIn(
serialId = readNumber(),
outPoint = readOutPoint(),
txOut = TxOut.read(readDelimitedByteArray()),
sequence = readNumber().toUInt(),
swapInParams = TxAddInputTlv.SwapInParams.read(this)
userKey = readPublicKey(),
serverKey = readPublicKey(),
userRefundKey = readPublicKey(),
refundDelay = readNumber().toInt()
)
else -> error("unknown discriminator $discriminator for class ${InteractiveTxInput.Remote::class}")
}
Expand Down
Original file line number Diff line number Diff line change
@@ -1,9 +1,9 @@
package fr.acinq.lightning.serialization.v4

import fr.acinq.bitcoin.*
import fr.acinq.bitcoin.crypto.musig2.IndividualNonce
import fr.acinq.bitcoin.io.ByteArrayOutput
import fr.acinq.bitcoin.io.Output
import fr.acinq.bitcoin.crypto.musig2.IndividualNonce
import fr.acinq.lightning.FeatureSupport
import fr.acinq.lightning.Features
import fr.acinq.lightning.channel.*
Expand Down Expand Up @@ -288,7 +288,10 @@ object Serialization {
writeBtcObject(previousTx)
writeNumber(previousTxOutput)
writeNumber(sequence.toLong())
swapInParams.write(this@writeLocalInteractiveTxInput)
writePublicKey(userKey)
writePublicKey(serverKey)
writePublicKey(userRefundKey)
writeNumber(refundDelay)
}
}

Expand Down Expand Up @@ -316,7 +319,10 @@ object Serialization {
writeBtcObject(outPoint)
writeBtcObject(txOut)
writeNumber(sequence.toLong())
swapInParams.write(this@writeRemoteInteractiveTxInput)
writePublicKey(userKey)
writePublicKey(serverKey)
writePublicKey(userRefundKey)
writeNumber(refundDelay)
}
}

Expand Down
Loading

0 comments on commit 23d0c40

Please sign in to comment.