Skip to content

Commit b5c187d

Browse files
committed
Add liquidity ads to the channel opening flow
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.
1 parent e307660 commit b5c187d

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

49 files changed

+1051
-472
lines changed

src/commonMain/kotlin/fr/acinq/lightning/channel/ChannelCommand.kt

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -35,7 +35,7 @@ sealed class ChannelCommand {
3535
val channelFlags: ChannelFlags,
3636
val channelConfig: ChannelConfig,
3737
val channelType: ChannelType.SupportedChannelType,
38-
val requestRemoteFunding: LiquidityAds.RequestRemoteFunding?,
38+
val requestRemoteFunding: LiquidityAds.RequestFunds?,
3939
val channelOrigin: Origin?,
4040
) : Init() {
4141
fun temporaryChannelId(keyManager: KeyManager): ByteVector32 = keyManager.channelKeys(localParams.fundingKeyPath).temporaryChannelId
@@ -48,7 +48,8 @@ sealed class ChannelCommand {
4848
val walletInputs: List<WalletState.Utxo>,
4949
val localParams: LocalParams,
5050
val channelConfig: ChannelConfig,
51-
val remoteInit: InitMessage
51+
val remoteInit: InitMessage,
52+
val fundingRates: LiquidityAds.WillFundRates?
5253
) : Init()
5354

5455
data class Restore(val state: PersistedChannelState) : Init()
@@ -86,7 +87,7 @@ sealed class ChannelCommand {
8687
data class UpdateFee(val feerate: FeeratePerKw, val commit: Boolean = false) : Commitment(), ForbiddenDuringSplice, ForbiddenDuringQuiescence
8788
data object CheckHtlcTimeout : Commitment()
8889
sealed class Splice : Commitment() {
89-
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() {
90+
data class Request(val replyTo: CompletableDeferred<Response>, val spliceIn: SpliceIn?, val spliceOut: SpliceOut?, val requestRemoteFunding: LiquidityAds.RequestFunds?, val feerate: FeeratePerKw, val origins: List<Origin>) : Splice() {
9091
val pushAmount: MilliSatoshi = spliceIn?.pushAmount ?: 0.msat
9192
val spliceOutputs: List<TxOut> = spliceOut?.let { listOf(TxOut(it.amount, it.scriptPubKey)) } ?: emptyList()
9293

src/commonMain/kotlin/fr/acinq/lightning/channel/ChannelException.kt

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -28,7 +28,6 @@ data class ToSelfDelayTooHigh (override val channelId: Byte
2828
data class MissingLiquidityAds (override val channelId: ByteVector32) : ChannelException(channelId, "liquidity ads field is missing")
2929
data class InvalidLiquidityAdsSig (override val channelId: ByteVector32) : ChannelException(channelId, "liquidity ads signature is invalid")
3030
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)")
31-
data class InvalidLiquidityRates (override val channelId: ByteVector32) : ChannelException(channelId, "rejecting liquidity ads proposed rates")
3231
data class ChannelFundingError (override val channelId: ByteVector32) : ChannelException(channelId, "channel funding error")
3332
data class RbfAttemptAborted (override val channelId: ByteVector32) : ChannelException(channelId, "rbf attempt aborted")
3433
data class SpliceAborted (override val channelId: ByteVector32) : ChannelException(channelId, "splice aborted")

src/commonMain/kotlin/fr/acinq/lightning/channel/InteractiveTx.kt

Lines changed: 13 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -673,8 +673,7 @@ data class InteractiveTxSession(
673673
val isComplete: Boolean = txCompleteSent != null && txCompleteReceived != null
674674

675675
fun send(): Pair<InteractiveTxSession, InteractiveTxSessionAction> {
676-
val msg = toSend.firstOrNull()
677-
return when (msg) {
676+
return when (val msg = toSend.firstOrNull()) {
678677
null -> {
679678
val localSwapIns = localInputs.filterIsInstance<InteractiveTxInput.LocalSwapIn>()
680679
val remoteSwapIns = remoteInputs.filterIsInstance<InteractiveTxInput.RemoteSwapIn>()
@@ -987,7 +986,6 @@ data class InteractiveTxSigningSession(
987986
val fundingParams: InteractiveTxParams,
988987
val fundingTxIndex: Long,
989988
val fundingTx: PartiallySignedSharedTransaction,
990-
val liquidityLease: LiquidityAds.Lease?,
991989
val localCommit: Either<UnsignedLocalCommit, LocalCommit>,
992990
val remoteCommit: RemoteCommit,
993991
) {
@@ -1075,7 +1073,15 @@ data class InteractiveTxSigningSession(
10751073
val channelKeys = channelParams.localParams.channelKeys(keyManager)
10761074
val unsignedTx = sharedTx.buildUnsignedTx()
10771075
val sharedOutputIndex = unsignedTx.txOut.indexOfFirst { it.publicKeyScript == fundingParams.fundingPubkeyScript(channelKeys) }
1078-
val liquidityFees = liquidityLease?.fees?.total?.toMilliSatoshi() ?: 0.msat
1076+
val liquidityFees = liquidityLease?.let { l ->
1077+
val fees = l.fees.total.toMilliSatoshi()
1078+
when (l.paymentDetails) {
1079+
is LiquidityAds.PaymentDetails.FromChannelBalance -> if (fundingParams.isInitiator) fees else -fees
1080+
// Fees will be paid later, from relayed HTLCs.
1081+
is LiquidityAds.PaymentDetails.FromFutureHtlc -> 0.msat
1082+
is LiquidityAds.PaymentDetails.FromFutureHtlcWithPreimage -> 0.msat
1083+
}
1084+
} ?: 0.msat
10791085
return Helpers.Funding.makeCommitTxs(
10801086
channelKeys,
10811087
channelParams.channelId,
@@ -1120,7 +1126,7 @@ data class InteractiveTxSigningSession(
11201126
val unsignedLocalCommit = UnsignedLocalCommit(localCommitmentIndex, firstCommitTx.localSpec, firstCommitTx.localCommitTx, listOf())
11211127
val remoteCommit = RemoteCommit(remoteCommitmentIndex, firstCommitTx.remoteSpec, firstCommitTx.remoteCommitTx.tx.txid, remotePerCommitmentPoint)
11221128
val signedFundingTx = sharedTx.sign(session, keyManager, fundingParams, channelParams.localParams, channelParams.remoteParams.nodeId)
1123-
Pair(InteractiveTxSigningSession(fundingParams, fundingTxIndex, signedFundingTx, liquidityLease, Either.Left(unsignedLocalCommit), remoteCommit), commitSig)
1129+
Pair(InteractiveTxSigningSession(fundingParams, fundingTxIndex, signedFundingTx, Either.Left(unsignedLocalCommit), remoteCommit), commitSig)
11241130
}
11251131
}
11261132

@@ -1168,7 +1174,7 @@ sealed class SpliceStatus {
11681174
/** Our peer has asked us to stop sending new updates and wait for our updates to be added to the local and remote commitments. */
11691175
data class ReceivedStfu(val stfu: Stfu) : QuiescenceNegotiation.NonInitiator()
11701176
/** Our updates have been added to the local and remote commitments, we wait for our peer to use the now quiescent channel. */
1171-
object NonInitiatorQuiescent : QuiescentSpliceStatus()
1177+
data object NonInitiatorQuiescent : QuiescentSpliceStatus()
11721178
/** We told our peer we want to splice funds in the channel. */
11731179
data class Requested(val command: ChannelCommand.Commitment.Splice.Request, val spliceInit: SpliceInit) : QuiescentSpliceStatus()
11741180
/** We both agreed to splice and are building the splice transaction. */
@@ -1181,7 +1187,7 @@ sealed class SpliceStatus {
11811187
val origins: List<Origin>
11821188
) : QuiescentSpliceStatus()
11831189
/** The splice transaction has been negotiated, we're exchanging signatures. */
1184-
data class WaitingForSigs(val session: InteractiveTxSigningSession, val origins: List<Origin>) : QuiescentSpliceStatus()
1190+
data class WaitingForSigs(val session: InteractiveTxSigningSession, val liquidityLease: LiquidityAds.Lease?, val origins: List<Origin>) : QuiescentSpliceStatus()
11851191
/** The splice attempt was aborted by us, we're waiting for our peer to ack. */
11861192
data object Aborted : QuiescentSpliceStatus()
11871193
}

src/commonMain/kotlin/fr/acinq/lightning/channel/states/LegacyWaitForFundingLocked.kt

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -48,7 +48,6 @@ data class LegacyWaitForFundingLocked(
4848
null,
4949
null,
5050
SpliceStatus.None,
51-
listOf(),
5251
)
5352
val actions = listOf(
5453
ChannelAction.Storage.StoreState(nextState),

src/commonMain/kotlin/fr/acinq/lightning/channel/states/Normal.kt

Lines changed: 19 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -24,7 +24,6 @@ data class Normal(
2424
val remoteShutdown: Shutdown?,
2525
val closingFeerates: ClosingFeerates?,
2626
val spliceStatus: SpliceStatus,
27-
val liquidityLeases: List<LiquidityAds.Lease>,
2827
) : ChannelStateWithCommitments() {
2928

3029
override fun updateCommitments(input: Commitments): ChannelStateWithCommitments = this.copy(commitments = input)
@@ -179,7 +178,7 @@ data class Normal(
179178
logger.info { "waiting for tx_sigs" }
180179
Pair(this@Normal.copy(spliceStatus = spliceStatus.copy(session = signingSession1)), listOf())
181180
}
182-
is InteractiveTxSigningSessionAction.SendTxSigs -> sendSpliceTxSigs(spliceStatus.origins, action, spliceStatus.session.liquidityLease, cmd.message.channelData)
181+
is InteractiveTxSigningSessionAction.SendTxSigs -> sendSpliceTxSigs(spliceStatus.origins, action, spliceStatus.liquidityLease, cmd.message.channelData)
183182
}
184183
}
185184
ignoreRetransmittedCommitSig(cmd.message) -> {
@@ -406,8 +405,8 @@ data class Normal(
406405
add(ChannelAction.Disconnect)
407406
}
408407
Pair(this@Normal.copy(spliceStatus = SpliceStatus.None), actions)
409-
} else if (spliceStatus.command.requestRemoteFunding?.let { r -> r.rate.fees(spliceStatus.command.feerate, r.fundingAmount, r.fundingAmount).total <= parentCommitment.localCommit.spec.toLocal.truncateToSatoshi() } == false) {
410-
val missing = spliceStatus.command.requestRemoteFunding.let { r -> r.rate.fees(spliceStatus.command.feerate, r.fundingAmount, r.fundingAmount).total - parentCommitment.localCommit.spec.toLocal.truncateToSatoshi() }
408+
} else if (!canAffordSpliceLiquidityFees(spliceStatus.command, parentCommitment)) {
409+
val missing = spliceStatus.command.requestRemoteFunding?.let { r -> r.fees(spliceStatus.command.feerate).total - parentCommitment.localCommit.spec.toLocal.truncateToSatoshi() }
411410
logger.warning { "cannot do splice: balance is too low to pay for inbound liquidity (missing=$missing)" }
412411
spliceStatus.command.replyTo.complete(ChannelCommand.Commitment.Splice.Response.Failure.InsufficientFunds)
413412
Pair(this@Normal, emptyList())
@@ -419,7 +418,7 @@ data class Normal(
419418
feerate = spliceStatus.command.feerate,
420419
fundingPubkey = channelKeys().fundingPubKey(parentCommitment.fundingTxIndex + 1),
421420
pushAmount = spliceStatus.command.pushAmount,
422-
requestFunds = spliceStatus.command.requestRemoteFunding?.requestFunds,
421+
requestFunds = spliceStatus.command.requestRemoteFunding,
423422
)
424423
logger.info { "initiating splice with local.amount=${spliceInit.fundingContribution} local.push=${spliceInit.pushAmount}" }
425424
Pair(this@Normal.copy(spliceStatus = SpliceStatus.Requested(spliceStatus.command, spliceInit)), listOf(ChannelAction.Message.Send(spliceInit)))
@@ -642,7 +641,7 @@ data class Normal(
642641
liquidityLease = spliceStatus.liquidityLease,
643642
)
644643
)
645-
val nextState = this@Normal.copy(spliceStatus = SpliceStatus.WaitingForSigs(session, spliceStatus.origins))
644+
val nextState = this@Normal.copy(spliceStatus = SpliceStatus.WaitingForSigs(session, spliceStatus.liquidityLease, spliceStatus.origins))
646645
val actions = buildList {
647646
interactiveTxAction.txComplete?.let { add(ChannelAction.Message.Send(it)) }
648647
add(ChannelAction.Storage.StoreState(nextState))
@@ -674,7 +673,7 @@ data class Normal(
674673
}
675674
is Either.Right -> {
676675
val action: InteractiveTxSigningSessionAction.SendTxSigs = res.value
677-
sendSpliceTxSigs(spliceStatus.origins, action, spliceStatus.session.liquidityLease, cmd.message.channelData)
676+
sendSpliceTxSigs(spliceStatus.origins, action, spliceStatus.liquidityLease, cmd.message.channelData)
678677
}
679678
}
680679
}
@@ -840,6 +839,18 @@ data class Normal(
840839
}
841840
}
842841

842+
private fun canAffordSpliceLiquidityFees(splice: ChannelCommand.Commitment.Splice.Request, parentCommitment: Commitment): Boolean {
843+
return when (val request = splice.requestRemoteFunding) {
844+
null -> true
845+
else -> when (request.paymentDetails) {
846+
is LiquidityAds.PaymentDetails.FromChannelBalance -> request.fees(splice.feerate).total <= parentCommitment.localCommit.spec.toLocal.truncateToSatoshi()
847+
// Fees don't need to be paid during the splice, they will be deducted from relayed HTLCs.
848+
is LiquidityAds.PaymentDetails.FromFutureHtlc -> true
849+
is LiquidityAds.PaymentDetails.FromFutureHtlcWithPreimage -> true
850+
}
851+
}
852+
}
853+
843854
private fun ChannelContext.sendSpliceTxSigs(
844855
origins: List<Origin>,
845856
action: InteractiveTxSigningSessionAction.SendTxSigs,
@@ -851,7 +862,7 @@ data class Normal(
851862
val fundingMinDepth = Helpers.minDepthForFunding(staticParams.nodeParams, action.fundingTx.fundingParams.fundingAmount)
852863
val watchConfirmed = WatchConfirmed(channelId, action.commitment.fundingTxId, action.commitment.commitInput.txOut.publicKeyScript, fundingMinDepth.toLong(), BITCOIN_FUNDING_DEPTHOK)
853864
val commitments = commitments.add(action.commitment).copy(remoteChannelData = remoteChannelData)
854-
val nextState = this@Normal.copy(commitments = commitments, spliceStatus = SpliceStatus.None, liquidityLeases = liquidityLeases + listOfNotNull(liquidityLease))
865+
val nextState = this@Normal.copy(commitments = commitments, spliceStatus = SpliceStatus.None)
855866
val actions = buildList {
856867
add(ChannelAction.Storage.StoreState(nextState))
857868
action.fundingTx.signedTx?.let { add(ChannelAction.Blockchain.PublishTx(it, ChannelAction.Blockchain.PublishTx.Type.FundingTx)) }

0 commit comments

Comments
 (0)