Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Use musig2 helpers to simplify swap-in protocol #592

Merged
Merged
Show file tree
Hide file tree
Changes from 1 commit
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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 = [email protected](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([email protected](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 = [email protected](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
sstone marked this conversation as resolved.
Show resolved Hide resolved

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
Loading