Skip to content

Commit

Permalink
Add liquidity ads to the 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.

We also update liquidity ads to use the latest version of the spec from
lightning/bolts#1153. This introduces more ways
of paying the liquidity fees, to support on-the-fly funding without
existing channel balance (not implemented in this commit).

Note that we need some backwards-compatibility with the previous
liquidity ads types in our state serialization code: when we're in the
middle of signing a splice transaction, we may have a legacy liquidity
lease in our splice status. We ignore it when finalizing the splice: the
only consequence is that we won't store an entry in our DB for that
lease, but the channel will otherwise work correctly.
  • Loading branch information
t-bast committed Sep 16, 2024
1 parent ab14c59 commit 526a528
Show file tree
Hide file tree
Showing 53 changed files with 1,055 additions and 516 deletions.
5 changes: 3 additions & 2 deletions src/commonMain/kotlin/fr/acinq/lightning/NodeParams.kt
Original file line number Diff line number Diff line change
Expand Up @@ -70,14 +70,15 @@ object DefaultSwapInParams {
* @param trampolineFees ordered list of trampoline fees to try when making an outgoing payment.
* @param invoiceDefaultRoutingFees default routing fees set in invoices when we don't have any channel.
* @param swapInParams parameters for swap-in transactions.
* @param leaseRate rate at which our peer sells their liquidity.
* @param remoteFundingRates rates at which our peer sells their liquidity.
*/
data class WalletParams(
val trampolineNode: NodeUri,
val trampolineFees: List<TrampolineFees>,
val invoiceDefaultRoutingFees: InvoiceDefaultRoutingFees,
val swapInParams: SwapInParams,
val leaseRate: LiquidityAds.LeaseRate,
// TODO: once standardized, we should get this data from our peer's init message.
val remoteFundingRates: LiquidityAds.WillFundRates,
)

/**
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -87,7 +87,7 @@ sealed class ChannelAction {
abstract val txId: TxId
data class ViaSpliceOut(val amount: Satoshi, override val miningFees: Satoshi, val address: String, override val txId: TxId) : StoreOutgoingPayment()
data class ViaSpliceCpfp(override val miningFees: Satoshi, override val txId: TxId) : StoreOutgoingPayment()
data class ViaInboundLiquidityRequest(override val txId: TxId, override val miningFees: Satoshi, val lease: LiquidityAds.Lease) : StoreOutgoingPayment()
data class ViaInboundLiquidityRequest(override val txId: TxId, override val miningFees: Satoshi, val purchase: LiquidityAds.Purchase) : StoreOutgoingPayment()
data class ViaClose(val amount: Satoshi, override val miningFees: Satoshi, val address: String, override val txId: TxId, val isSentToDefaultAddress: Boolean, val closingType: ChannelClosingType) : StoreOutgoingPayment()
}
data class SetLocked(val txId: TxId) : Storage()
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -35,7 +35,7 @@ sealed class ChannelCommand {
val channelFlags: ChannelFlags,
val channelConfig: ChannelConfig,
val channelType: ChannelType.SupportedChannelType,
val requestRemoteFunding: LiquidityAds.RequestRemoteFunding?,
val requestRemoteFunding: LiquidityAds.RequestFunding?,
val channelOrigin: Origin?,
) : Init() {
fun temporaryChannelId(keyManager: KeyManager): ByteVector32 = keyManager.channelKeys(localParams.fundingKeyPath).temporaryChannelId
Expand All @@ -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 fundingRates: LiquidityAds.WillFundRates?
) : Init()

data class Restore(val state: PersistedChannelState) : Init()
Expand Down Expand Up @@ -86,7 +87,7 @@ sealed class ChannelCommand {
data class UpdateFee(val feerate: FeeratePerKw, val commit: Boolean = false) : Commitment(), ForbiddenDuringSplice, ForbiddenDuringQuiescence
data object CheckHtlcTimeout : Commitment()
sealed class Splice : Commitment() {
data class Request(val replyTo: CompletableDeferred<Response>, val spliceIn: SpliceIn?, val spliceOut: SpliceOut?, val requestRemoteFunding: LiquidityAds.RequestRemoteFunding?, val feerate: FeeratePerKw, val origins: List<Origin>) : Splice() {
data class Request(val replyTo: CompletableDeferred<Response>, val spliceIn: SpliceIn?, val spliceOut: SpliceOut?, val requestRemoteFunding: LiquidityAds.RequestFunding?, val feerate: FeeratePerKw, val origins: List<Origin>) : Splice() {
val pushAmount: MilliSatoshi = spliceIn?.pushAmount ?: 0.msat
val spliceOutputs: List<TxOut> = spliceOut?.let { listOf(TxOut(it.amount, it.scriptPubKey)) } ?: emptyList()

Expand All @@ -105,7 +106,7 @@ sealed class ChannelCommand {
val fundingTxId: TxId,
val capacity: Satoshi,
val balance: MilliSatoshi,
val liquidityLease: LiquidityAds.Lease?,
val liquidityPurchase: LiquidityAds.Purchase?,
) : Response()

sealed class Failure : Response() {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -28,7 +28,9 @@ data class ToSelfDelayTooHigh (override val channelId: Byte
data class MissingLiquidityAds (override val channelId: ByteVector32) : ChannelException(channelId, "liquidity ads field is missing")
data class InvalidLiquidityAdsSig (override val channelId: ByteVector32) : ChannelException(channelId, "liquidity ads signature is invalid")
data class InvalidLiquidityAdsAmount (override val channelId: ByteVector32, val proposed: Satoshi, val min: Satoshi) : ChannelException(channelId, "liquidity ads funding amount is too low (expected at least $min, got $proposed)")
data class InvalidLiquidityRates (override val channelId: ByteVector32) : ChannelException(channelId, "rejecting liquidity ads proposed rates")
data class InvalidLiquidityAdsRate (override val channelId: ByteVector32) : ChannelException(channelId, "liquidity ads funding rate does not match the rate we selected")
data class UnexpectedLiquidityAdsFundingFee (override val channelId: ByteVector32, val fundingTxId: TxId) : ChannelException(channelId, "unexpected liquidity ads funding fee for txId=$fundingTxId (transaction not found)")
data class InvalidLiquidityAdsFundingFee (override val channelId: ByteVector32, val fundingTxId: TxId, val paymentHash: ByteVector32, val expected: Satoshi, val proposed: MilliSatoshi) : ChannelException(channelId, "invalid liquidity ads funding fee for txId=$fundingTxId and paymentHash=$paymentHash (expected $expected, got $proposed)")
data class ChannelFundingError (override val channelId: ByteVector32) : ChannelException(channelId, "channel funding error")
data class RbfAttemptAborted (override val channelId: ByteVector32) : ChannelException(channelId, "rbf attempt aborted")
data class SpliceAborted (override val channelId: ByteVector32) : ChannelException(channelId, "splice aborted")
Expand Down
25 changes: 16 additions & 9 deletions src/commonMain/kotlin/fr/acinq/lightning/channel/InteractiveTx.kt
Original file line number Diff line number Diff line change
Expand Up @@ -673,8 +673,7 @@ data class InteractiveTxSession(
val isComplete: Boolean = txCompleteSent != null && txCompleteReceived != null

fun send(): Pair<InteractiveTxSession, InteractiveTxSessionAction> {
val msg = toSend.firstOrNull()
return when (msg) {
return when (val msg = toSend.firstOrNull()) {
null -> {
val localSwapIns = localInputs.filterIsInstance<InteractiveTxInput.LocalSwapIn>()
val remoteSwapIns = remoteInputs.filterIsInstance<InteractiveTxInput.RemoteSwapIn>()
Expand Down Expand Up @@ -987,7 +986,6 @@ data class InteractiveTxSigningSession(
val fundingParams: InteractiveTxParams,
val fundingTxIndex: Long,
val fundingTx: PartiallySignedSharedTransaction,
val liquidityLease: LiquidityAds.Lease?,
val localCommit: Either<UnsignedLocalCommit, LocalCommit>,
val remoteCommit: RemoteCommit,
) {
Expand Down Expand Up @@ -1065,7 +1063,7 @@ data class InteractiveTxSigningSession(
sharedTx: SharedTransaction,
localPushAmount: MilliSatoshi,
remotePushAmount: MilliSatoshi,
liquidityLease: LiquidityAds.Lease?,
liquidityPurchase: LiquidityAds.Purchase?,
localCommitmentIndex: Long,
remoteCommitmentIndex: Long,
commitTxFeerate: FeeratePerKw,
Expand All @@ -1075,7 +1073,16 @@ 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 = liquidityPurchase?.let { l ->
val fees = l.fees.total.toMilliSatoshi()
when (l.paymentDetails) {
is LiquidityAds.PaymentDetails.FromChannelBalance -> if (fundingParams.isInitiator) fees else -fees
is LiquidityAds.PaymentDetails.FromChannelBalanceForFutureHtlc -> if (fundingParams.isInitiator) fees else -fees
// Fees will be paid later, from relayed HTLCs.
is LiquidityAds.PaymentDetails.FromFutureHtlc -> 0.msat
is LiquidityAds.PaymentDetails.FromFutureHtlcWithPreimage -> 0.msat
}
} ?: 0.msat
return Helpers.Funding.makeCommitTxs(
channelKeys,
channelParams.channelId,
Expand Down Expand Up @@ -1120,7 +1127,7 @@ data class InteractiveTxSigningSession(
val unsignedLocalCommit = UnsignedLocalCommit(localCommitmentIndex, firstCommitTx.localSpec, firstCommitTx.localCommitTx, listOf())
val remoteCommit = RemoteCommit(remoteCommitmentIndex, firstCommitTx.remoteSpec, firstCommitTx.remoteCommitTx.tx.txid, remotePerCommitmentPoint)
val signedFundingTx = sharedTx.sign(session, keyManager, fundingParams, channelParams.localParams, channelParams.remoteParams.nodeId)
Pair(InteractiveTxSigningSession(fundingParams, fundingTxIndex, signedFundingTx, liquidityLease, Either.Left(unsignedLocalCommit), remoteCommit), commitSig)
Pair(InteractiveTxSigningSession(fundingParams, fundingTxIndex, signedFundingTx, Either.Left(unsignedLocalCommit), remoteCommit), commitSig)
}
}

Expand Down Expand Up @@ -1168,7 +1175,7 @@ sealed class SpliceStatus {
/** Our peer has asked us to stop sending new updates and wait for our updates to be added to the local and remote commitments. */
data class ReceivedStfu(val stfu: Stfu) : QuiescenceNegotiation.NonInitiator()
/** Our updates have been added to the local and remote commitments, we wait for our peer to use the now quiescent channel. */
object NonInitiatorQuiescent : QuiescentSpliceStatus()
data object NonInitiatorQuiescent : QuiescentSpliceStatus()
/** We told our peer we want to splice funds in the channel. */
data class Requested(val command: ChannelCommand.Commitment.Splice.Request, val spliceInit: SpliceInit) : QuiescentSpliceStatus()
/** We both agreed to splice and are building the splice transaction. */
Expand All @@ -1177,11 +1184,11 @@ sealed class SpliceStatus {
val spliceSession: InteractiveTxSession,
val localPushAmount: MilliSatoshi,
val remotePushAmount: MilliSatoshi,
val liquidityLease: LiquidityAds.Lease?,
val liquidityPurchase: LiquidityAds.Purchase?,
val origins: List<Origin>
) : QuiescentSpliceStatus()
/** The splice transaction has been negotiated, we're exchanging signatures. */
data class WaitingForSigs(val session: InteractiveTxSigningSession, val origins: List<Origin>) : QuiescentSpliceStatus()
data class WaitingForSigs(val session: InteractiveTxSigningSession, val liquidityPurchase: LiquidityAds.Purchase?, val origins: List<Origin>) : QuiescentSpliceStatus()
/** The splice attempt was aborted by us, we're waiting for our peer to ack. */
data object Aborted : QuiescentSpliceStatus()
}
Original file line number Diff line number Diff line change
Expand Up @@ -48,7 +48,6 @@ data class LegacyWaitForFundingLocked(
null,
null,
SpliceStatus.None,
listOf(),
)
val actions = listOf(
ChannelAction.Storage.StoreState(nextState),
Expand Down
Loading

0 comments on commit 526a528

Please sign in to comment.