Skip to content

Commit

Permalink
Add liquidity ads to channel opening flow
Browse files Browse the repository at this point in the history
We previously only used liquidity ads with splicing: we now support it
during the initial channel opening flow as well. This lets us add more
unit tests, including tests for the case where the node receiving the
`open_channel` message is responsible for paying the commitment fees.
  • Loading branch information
t-bast committed Mar 12, 2024
1 parent 8a9007a commit ff2191f
Show file tree
Hide file tree
Showing 22 changed files with 415 additions and 109 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -48,7 +48,8 @@ sealed class ChannelCommand {
val walletInputs: List<WalletState.Utxo>,
val localParams: LocalParams,
val channelConfig: ChannelConfig,
val remoteInit: InitMessage
val remoteInit: InitMessage,
val leaseRate: LiquidityAds.LeaseRate?,
) : Init()

data class Restore(val state: PersistedChannelState) : Init()
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -5,22 +5,17 @@ import fr.acinq.bitcoin.Script.tail
import fr.acinq.bitcoin.crypto.musig2.IndividualNonce
import fr.acinq.bitcoin.crypto.musig2.Musig2
import fr.acinq.bitcoin.crypto.musig2.SecretNonce
import fr.acinq.bitcoin.utils.getOrDefault
import fr.acinq.lightning.Lightning.randomBytes32
import fr.acinq.bitcoin.utils.Either
import fr.acinq.bitcoin.utils.Try
import fr.acinq.bitcoin.utils.getOrDefault
import fr.acinq.bitcoin.utils.runTrying
import fr.acinq.lightning.Lightning.randomBytes32
import fr.acinq.lightning.MilliSatoshi
import fr.acinq.lightning.blockchain.electrum.WalletState
import fr.acinq.lightning.blockchain.fee.FeeratePerKw
import fr.acinq.lightning.crypto.Bolt3Derivation.deriveForCommitment
import fr.acinq.lightning.crypto.KeyManager
import fr.acinq.lightning.logging.*
import fr.acinq.lightning.transactions.CommitmentSpec
import fr.acinq.lightning.transactions.DirectedHtlc
import fr.acinq.lightning.transactions.Scripts
import fr.acinq.lightning.transactions.SwapInProtocol
import fr.acinq.lightning.transactions.Transactions
import fr.acinq.lightning.logging.MDCLogger
import fr.acinq.lightning.transactions.*
import fr.acinq.lightning.utils.*
import fr.acinq.lightning.wire.*
Expand Down Expand Up @@ -1075,7 +1070,7 @@ data class InteractiveTxSigningSession(
val channelKeys = channelParams.localParams.channelKeys(keyManager)
val unsignedTx = sharedTx.buildUnsignedTx()
val sharedOutputIndex = unsignedTx.txOut.indexOfFirst { it.publicKeyScript == fundingParams.fundingPubkeyScript(channelKeys) }
val liquidityFees = liquidityLease?.fees?.total?.toMilliSatoshi() ?: 0.msat
val liquidityFees = liquidityLease?.fees?.total?.toMilliSatoshi()?.let { if (fundingParams.isInitiator) it else -it } ?: 0.msat
return Helpers.Funding.makeCommitTxs(
channelKeys,
channelParams.channelId,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ import fr.acinq.lightning.channel.Helpers.Funding.computeChannelId
import fr.acinq.lightning.utils.msat
import fr.acinq.lightning.wire.AcceptDualFundedChannel
import fr.acinq.lightning.wire.Error
import fr.acinq.lightning.wire.LiquidityAds
import fr.acinq.lightning.wire.OpenDualFundedChannel

/*
Expand Down Expand Up @@ -53,41 +54,67 @@ data class WaitForAcceptChannel(
val remoteFundingPubkey = accept.fundingPubkey
val dustLimit = accept.dustLimit.max(init.localParams.dustLimit)
val fundingParams = InteractiveTxParams(channelId, true, init.fundingAmount, accept.fundingAmount, remoteFundingPubkey, lastSent.lockTime, dustLimit, lastSent.fundingFeerate)
when (val fundingContributions = FundingContributions.create(channelKeys, keyManager.swapInOnChainWallet, fundingParams, init.walletInputs)) {
is Either.Left -> {
logger.error { "could not fund channel: ${fundingContributions.value}" }
Pair(Aborted, listOf(ChannelAction.Message.Send(Error(channelId, ChannelFundingError(channelId).message))))
when (val liquidityLease = LiquidityAds.validateLease(
init.requestRemoteFunding,
staticParams.remoteNodeId,
channelId,
fundingParams.fundingPubkeyScript(channelKeys),
accept.fundingAmount,
lastSent.fundingFeerate,
accept.willFund
)) {
is Either.Left<ChannelException> -> {
logger.error { "rejecting liquidity proposal: ${liquidityLease.value.message}" }
Pair(Aborted, listOf(ChannelAction.Message.Send(Error(cmd.message.temporaryChannelId, liquidityLease.value.message))))
}
is Either.Right -> {
// The channel initiator always sends the first interactive-tx message.
val (interactiveTxSession, interactiveTxAction) = InteractiveTxSession(staticParams.remoteNodeId, channelKeys, keyManager.swapInOnChainWallet, fundingParams, 0.msat, 0.msat, emptySet(), fundingContributions.value).send()
when (interactiveTxAction) {
is InteractiveTxSessionAction.SendMessage -> {
val nextState = WaitForFundingCreated(
init.localParams,
remoteParams,
interactiveTxSession,
lastSent.pushAmount,
accept.pushAmount,
lastSent.commitmentFeerate,
accept.firstPerCommitmentPoint,
accept.secondPerCommitmentPoint,
lastSent.channelFlags,
init.channelConfig,
channelFeatures,
channelOrigin
)
val actions = listOf(
ChannelAction.ChannelId.IdAssigned(staticParams.remoteNodeId, temporaryChannelId, channelId),
ChannelAction.Message.Send(interactiveTxAction.msg),
ChannelAction.EmitEvent(ChannelEvents.Creating(nextState))
)
Pair(nextState, actions)
}
else -> {
logger.error { "could not start interactive-tx session: $interactiveTxAction" }
is Either.Right<LiquidityAds.Lease?> -> {
when (val fundingContributions = FundingContributions.create(channelKeys, keyManager.swapInOnChainWallet, fundingParams, init.walletInputs)) {
is Either.Left -> {
logger.error { "could not fund channel: ${fundingContributions.value}" }
Pair(Aborted, listOf(ChannelAction.Message.Send(Error(channelId, ChannelFundingError(channelId).message))))
}
is Either.Right -> {
// The channel initiator always sends the first interactive-tx message.
val (interactiveTxSession, interactiveTxAction) = InteractiveTxSession(
staticParams.remoteNodeId,
channelKeys,
keyManager.swapInOnChainWallet,
fundingParams,
0.msat,
0.msat,
emptySet(),
fundingContributions.value
).send()
when (interactiveTxAction) {
is InteractiveTxSessionAction.SendMessage -> {
val nextState = WaitForFundingCreated(
init.localParams,
remoteParams,
interactiveTxSession,
lastSent.pushAmount,
accept.pushAmount,
lastSent.commitmentFeerate,
accept.firstPerCommitmentPoint,
accept.secondPerCommitmentPoint,
lastSent.channelFlags,
init.channelConfig,
channelFeatures,
liquidityLease.value,
channelOrigin
)
val actions = listOf(
ChannelAction.ChannelId.IdAssigned(staticParams.remoteNodeId, temporaryChannelId, channelId),
ChannelAction.Message.Send(interactiveTxAction.msg),
ChannelAction.EmitEvent(ChannelEvents.Creating(nextState))
)
Pair(nextState, actions)
}
else -> {
logger.error { "could not start interactive-tx session: $interactiveTxAction" }
Pair(Aborted, listOf(ChannelAction.Message.Send(Error(channelId, ChannelFundingError(channelId).message))))
}
}
}
}
}
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -41,6 +41,7 @@ data class WaitForFundingCreated(
val channelFlags: ChannelFlags,
val channelConfig: ChannelConfig,
val channelFeatures: ChannelFeatures,
val liquidityLease: LiquidityAds.Lease?,
val channelOrigin: Origin?
) : ChannelState() {
val channelId: ByteVector32 = interactiveTxSession.fundingParams.channelId
Expand All @@ -63,7 +64,7 @@ data class WaitForFundingCreated(
interactiveTxAction.sharedTx,
localPushAmount,
remotePushAmount,
liquidityLease = null,
liquidityLease,
localCommitmentIndex = 0,
remoteCommitmentIndex = 0,
commitTxFeerate,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,8 @@ data object WaitForInit : ChannelState() {
cmd.walletInputs,
cmd.localParams,
cmd.channelConfig,
cmd.remoteInit
cmd.remoteInit,
cmd.leaseRate,
)
Pair(nextState, listOf())
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -27,7 +27,8 @@ data class WaitForOpenChannel(
val walletInputs: List<WalletState.Utxo>,
val localParams: LocalParams,
val channelConfig: ChannelConfig,
val remoteInit: Init
val remoteInit: Init,
val leaseRate: LiquidityAds.LeaseRate?,
) : ChannelState() {
override fun ChannelContext.processInternal(cmd: ChannelCommand): Pair<ChannelState, List<ChannelAction>> {
return when (cmd) {
Expand All @@ -40,6 +41,17 @@ data class WaitForOpenChannel(
val channelFeatures = ChannelFeatures(channelType, localFeatures = localParams.features, remoteFeatures = remoteInit.features)
val minimumDepth = if (staticParams.useZeroConf) 0 else Helpers.minDepthForFunding(staticParams.nodeParams, open.fundingAmount)
val channelKeys = keyManager.channelKeys(localParams.fundingKeyPath)
val localFundingPubkey = channelKeys.fundingPubKey(0)
val willFundLease = leaseRate?.let { rate ->
open.requestFunds?.let { request ->
if (request.amount <= fundingAmount) {
val fundingScript = Helpers.Funding.makeFundingPubKeyScript(localFundingPubkey, open.fundingPubkey)
rate.signLease(staticParams.nodeParams.nodePrivateKey, fundingAmount, fundingScript, open.fundingFeerate, request)
} else {
null
}
}
}
val accept = AcceptDualFundedChannel(
temporaryChannelId = open.temporaryChannelId,
fundingAmount = fundingAmount,
Expand All @@ -49,7 +61,7 @@ data class WaitForOpenChannel(
minimumDepth = minimumDepth.toLong(),
toSelfDelay = localParams.toSelfDelay,
maxAcceptedHtlcs = localParams.maxAcceptedHtlcs,
fundingPubkey = channelKeys.fundingPubKey(0),
fundingPubkey = localFundingPubkey,
revocationBasepoint = channelKeys.revocationBasepoint,
paymentBasepoint = channelKeys.paymentBasepoint,
delayedPaymentBasepoint = channelKeys.delayedPaymentBasepoint,
Expand All @@ -59,6 +71,7 @@ data class WaitForOpenChannel(
tlvStream = TlvStream(
buildSet {
add(ChannelTlv.ChannelTypeTlv(channelType))
willFundLease?.let { add(it.willFund) }
if (pushAmount > 0.msat) add(ChannelTlv.PushAmountTlv(pushAmount))
}
),
Expand Down Expand Up @@ -88,7 +101,8 @@ data class WaitForOpenChannel(
is Either.Right -> {
val interactiveTxSession = InteractiveTxSession(staticParams.remoteNodeId, channelKeys, keyManager.swapInOnChainWallet, fundingParams, 0.msat, 0.msat, emptySet(), fundingContributions.value)
val nextState = WaitForFundingCreated(
localParams,
// If our peer asks us to pay the commit tx fees, we accept (only used in tests, as we're otherwise always the channel opener).
localParams.copy(payCommitTxFees = open.channelFlags.nonInitiatorPaysCommitFees),
remoteParams,
interactiveTxSession,
pushAmount,
Expand All @@ -99,6 +113,7 @@ data class WaitForOpenChannel(
open.channelFlags,
channelConfig,
channelFeatures,
willFundLease?.lease,
channelOrigin = null,
)
val actions = listOf(
Expand Down
2 changes: 1 addition & 1 deletion src/commonMain/kotlin/fr/acinq/lightning/io/Peer.kt
Original file line number Diff line number Diff line change
Expand Up @@ -929,7 +929,7 @@ class Peer(
val localParams = LocalParams(nodeParams, isChannelOpener = false, payCommitTxFees = msg.channelFlags.nonInitiatorPaysCommitFees)
val state = WaitForInit
val channelConfig = ChannelConfig.standard
val (state1, actions1) = state.process(ChannelCommand.Init.NonInitiator(msg.temporaryChannelId, 0.sat, 0.msat, listOf(), localParams, channelConfig, theirInit!!))
val (state1, actions1) = state.process(ChannelCommand.Init.NonInitiator(msg.temporaryChannelId, 0.sat, 0.msat, listOf(), localParams, channelConfig, theirInit!!, leaseRate = null))
val (state2, actions2) = state1.process(ChannelCommand.MessageReceived(msg))
_channels = _channels + (msg.temporaryChannelId to state2)
processActions(msg.temporaryChannelId, peerConnection, actions1 + actions2)
Expand Down
10 changes: 8 additions & 2 deletions src/commonMain/kotlin/fr/acinq/lightning/wire/LiquidityAds.kt
Original file line number Diff line number Diff line change
Expand Up @@ -51,10 +51,14 @@ object LiquidityAds {
return LeaseFees(onChainFees, leaseFeeBase + proportionalFee)
}

fun signLease(nodeKey: PrivateKey, fundingScript: ByteVector, requestFunds: ChannelTlv.RequestFunds): ChannelTlv.WillFund {
fun signLease(nodeKey: PrivateKey, fundingAmount: Satoshi, fundingScript: ByteVector, fundingFeerate: FeeratePerKw, requestFunds: ChannelTlv.RequestFunds): WillFundLease {
require(fundingAmount >= requestFunds.amount) { "funding amount is smaller than requested by our peer ($fundingAmount < ${requestFunds.amount})" }
val witness = LeaseWitness(fundingScript, requestFunds.leaseDuration, requestFunds.leaseExpiry, maxRelayFeeProportional, maxRelayFeeBase)
val sig = witness.sign(nodeKey)
return ChannelTlv.WillFund(sig, fundingWeight, leaseFeeProportional, leaseFeeBase, maxRelayFeeProportional, maxRelayFeeBase)
val leaseFees = fees(fundingFeerate, requestFunds.amount, fundingAmount)
val lease = Lease(requestFunds.amount, leaseFees, sig, witness)
val willFund = ChannelTlv.WillFund(sig, fundingWeight, leaseFeeProportional, leaseFeeBase, maxRelayFeeProportional, maxRelayFeeBase)
return WillFundLease(willFund, lease)
}

fun write(out: Output) {
Expand Down Expand Up @@ -137,6 +141,8 @@ object LiquidityAds {
val expiry: Int = witness.leaseEnd
}

data class WillFundLease(val willFund: ChannelTlv.WillFund, val lease: Lease)

/** The seller signs the lease parameters: if they cheat, the buyer can use that signature to prove they cheated. */
data class LeaseWitness(val fundingScript: ByteVector, val leaseDuration: Int, val leaseEnd: Int, val maxRelayFeeProportional: Int, val maxRelayFeeBase: MilliSatoshi) {
fun sign(nodeKey: PrivateKey): ByteVector64 = Crypto.sign(Crypto.sha256(encode()), nodeKey)
Expand Down
Loading

0 comments on commit ff2191f

Please sign in to comment.