Skip to content

Commit

Permalink
Add funding_fee_credit feature
Browse files Browse the repository at this point in the history
We add an optional feature that lets on-the-fly funding clients accept
payments that are too small to pay the fees for an on-the-fly funding.
When that happens, the payment amount is added as "fee credit" without
performing an on-chain operation. Once enough fee credit has been
obtained, we can initiate an on-chain operation to create a channel or
a splice by paying the fees from our fee credit.

This feature makes more efficient use of on-chain transactions by
trusting that our peer will honor our fee credit in the future.
  • Loading branch information
t-bast committed Jun 4, 2024
1 parent 22316a5 commit 11d94a6
Show file tree
Hide file tree
Showing 13 changed files with 372 additions and 46 deletions.
13 changes: 11 additions & 2 deletions src/commonMain/kotlin/fr/acinq/lightning/Features.kt
Original file line number Diff line number Diff line change
Expand Up @@ -263,6 +263,13 @@ sealed class Feature {
override val scopes: Set<FeatureScope> get() = setOf(FeatureScope.Init, FeatureScope.Node)
}

@Serializable
object FundingFeeCredit : Feature() {
override val rfcName get() = "funding_fee_credit"
override val mandatory get() = 562
override val scopes: Set<FeatureScope> get() = setOf(FeatureScope.Init, FeatureScope.Node)
}

}

@Serializable
Expand Down Expand Up @@ -345,7 +352,8 @@ data class Features(val activated: Map<Feature, FeatureSupport>, val unknown: Se
Feature.ChannelBackupClient,
Feature.ChannelBackupProvider,
Feature.ExperimentalSplice,
Feature.OnTheFlyFunding
Feature.OnTheFlyFunding,
Feature.FundingFeeCredit
)

operator fun invoke(bytes: ByteVector): Features = invoke(bytes.toByteArray())
Expand Down Expand Up @@ -378,7 +386,8 @@ data class Features(val activated: Map<Feature, FeatureSupport>, val unknown: Se
Feature.AnchorOutputs to listOf(Feature.StaticRemoteKey),
Feature.TrampolinePayment to listOf(Feature.PaymentSecret),
Feature.ExperimentalTrampolinePayment to listOf(Feature.PaymentSecret),
Feature.OnTheFlyFunding to listOf(Feature.ExperimentalSplice)
Feature.OnTheFlyFunding to listOf(Feature.ExperimentalSplice),
Feature.FundingFeeCredit to listOf(Feature.OnTheFlyFunding)
)

class FeatureException(message: String) : IllegalArgumentException(message)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -1075,6 +1075,8 @@ data class InteractiveTxSigningSession(
// Fees will be paid later, from relayed HTLCs.
is LiquidityAds.PaymentDetails.FromFutureHtlc -> 0.msat
is LiquidityAds.PaymentDetails.FromFutureHtlcWithPreimage -> 0.msat
// Fees are taken from the current fee credit.
is LiquidityAds.PaymentDetails.FromFeeCredit -> 0.msat
}
} ?: 0.msat
return Helpers.Funding.makeCommitTxs(
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -862,6 +862,8 @@ data class Normal(
// Fees don't need to be paid during the splice, they will be deducted from relayed HTLCs.
is LiquidityAds.PaymentDetails.FromFutureHtlc -> true
is LiquidityAds.PaymentDetails.FromFutureHtlcWithPreimage -> true
// Fees don't need to be paid during the splice, they are taken from our fee credit.
is LiquidityAds.PaymentDetails.FromFeeCredit -> true
}
}
}
Expand Down
9 changes: 9 additions & 0 deletions src/commonMain/kotlin/fr/acinq/lightning/db/PaymentsDb.kt
Original file line number Diff line number Diff line change
Expand Up @@ -175,6 +175,15 @@ data class IncomingPayment(val preimage: ByteVector32, val origin: Origin, val r
override val fees: MilliSatoshi = fundingFee?.amount ?: 0.msat
}

/**
* Payment was added to our fee credit for future on-chain operations (see [Feature.FundingFeeCredit]).
* We didn't really receive this amount yet, but we trust our peer to use it for future on-chain operations.
*/
data class AddedToFeeCredit(override val amount: MilliSatoshi) : ReceivedWith() {
// Adding to the fee credit doesn't cost any fees.
override val fees: MilliSatoshi = 0.msat
}

sealed class OnChainIncomingPayment : ReceivedWith() {
abstract val serviceFee: MilliSatoshi
abstract val miningFee: Satoshi
Expand Down
27 changes: 21 additions & 6 deletions src/commonMain/kotlin/fr/acinq/lightning/io/Peer.kt
Original file line number Diff line number Diff line change
Expand Up @@ -211,6 +211,7 @@ class Peer(
val currentTipFlow = MutableStateFlow<Int?>(null)
val onChainFeeratesFlow = MutableStateFlow<OnChainFeerates?>(null)
val peerFeeratesFlow = MutableStateFlow<RecommendedFeerates?>(null)
val feeCreditFlow = MutableStateFlow<MilliSatoshi>(0.msat)

private val _channelLogger = nodeParams.loggerFactory.newLogger(ChannelState::class)
private suspend fun ChannelState.process(cmd: ChannelCommand): Pair<ChannelState, List<ChannelAction>> {
Expand Down Expand Up @@ -720,9 +721,13 @@ class Peer(
peerConnection?.send(message)
}

/** Return true if we are currently funding a channel. */
/**
* Return true if we are currently funding a channel.
* Note that we also return true if we haven't yet received the remote [TxSignatures] for the latest splice transaction.
* Since our peer sends [CurrentFeeCredit] before [TxSignatures], this ensures that we never over-estimate our fee credit when initiating a funding flow.
*/
private fun channelFundingIsInProgress(): Boolean = when (val channel = _channels.values.firstOrNull { it is Normal }) {
is Normal -> channel.spliceStatus != SpliceStatus.None
is Normal -> channel.spliceStatus != SpliceStatus.None || channel.commitments.latest.localFundingStatus.signedTx == null
else -> _channels.values.any { it is WaitForAcceptChannel || it is WaitForFundingCreated || it is WaitForFundingSigned || it is WaitForFundingConfirmed || it is WaitForChannelReady }
}

Expand Down Expand Up @@ -865,9 +870,10 @@ class Peer(
private suspend fun processIncomingPayment(item: Either<WillAddHtlc, UpdateAddHtlc>) {
val currentBlockHeight = currentTipFlow.filterNotNull().first()
val currentFeerate = peerFeeratesFlow.filterNotNull().first().fundingFeerate
val currentFeeCredit = feeCreditFlow.first()
val result = when (item) {
is Either.Right -> incomingPaymentHandler.process(item.value, currentBlockHeight, currentFeerate)
is Either.Left -> incomingPaymentHandler.process(item.value, currentBlockHeight, currentFeerate)
is Either.Right -> incomingPaymentHandler.process(item.value, currentBlockHeight, currentFeerate, currentFeeCredit)
is Either.Left -> incomingPaymentHandler.process(item.value, currentBlockHeight, currentFeerate, currentFeeCredit)
}
when (result) {
is IncomingPaymentHandler.ProcessAddResult.Accepted -> {
Expand Down Expand Up @@ -973,6 +979,12 @@ class Peer(
}
}
}
is CurrentFeeCredit -> {
when {
nodeParams.features.hasFeature(Feature.FundingFeeCredit) -> feeCreditFlow.value = msg.amount
else -> {}
}
}
is Ping -> {
val pong = Pong(ByteVector(ByteArray(msg.pongLength)))
peerConnection?.send(pong)
Expand Down Expand Up @@ -1276,6 +1288,7 @@ class Peer(
is OpenOrSplicePayment -> {
val channel = channels.values.firstOrNull { it is Normal }
val currentFeerates = peerFeeratesFlow.filterNotNull().first()
val currentFeeCredit = feeCreditFlow.first().truncateToSatoshi()
when {
channelFundingIsInProgress() -> {
// Once the channel funding is complete, we may have enough inbound liquidity to receive the payment without an on-chain operation
Expand Down Expand Up @@ -1303,8 +1316,9 @@ class Peer(
// We must cover the shared input and the shared output, which is a lot of weight, so we add 50%.
else -> fundingFeerate * 1.5
}
// We cannot pay the liquidity fees from our channel balance, so we fall back to future HTLCs.
// We cannot pay the liquidity fees from our channel balance, so we fall back to future HTLCs or fee credit.
val paymentDetails = when {
remoteFundingRates.paymentTypes.contains(LiquidityAds.PaymentType.FromFeeCredit) && cmd.leaseFees(targetFeerate).total <= currentFeeCredit -> LiquidityAds.PaymentDetails.FromFeeCredit
remoteFundingRates.paymentTypes.contains(LiquidityAds.PaymentType.FromFutureHtlc) -> LiquidityAds.PaymentDetails.FromFutureHtlc(listOf(cmd.paymentHash))
remoteFundingRates.paymentTypes.contains(LiquidityAds.PaymentType.FromFutureHtlcWithPreimage) -> LiquidityAds.PaymentDetails.FromFutureHtlcWithPreimage(listOf(cmd.preimage))
else -> null
Expand Down Expand Up @@ -1347,8 +1361,9 @@ class Peer(
// We don't pay any local on-chain fees, our fee is only for the liquidity lease.
val leaseFees = cmd.leaseFees(fundingFeerate)
val totalFees = TransactionFees(miningFee = leaseFees.miningFee, serviceFee = leaseFees.serviceFee)
// We cannot pay the liquidity fees from our channel balance, so we fall back to future HTLCs.
// We cannot pay the liquidity fees from our channel balance, so we fall back to future HTLCs or fee credit.
val paymentDetails = when {
remoteFundingRates.paymentTypes.contains(LiquidityAds.PaymentType.FromFeeCredit) && leaseFees.total <= currentFeeCredit -> LiquidityAds.PaymentDetails.FromFeeCredit
remoteFundingRates.paymentTypes.contains(LiquidityAds.PaymentType.FromFutureHtlc) -> LiquidityAds.PaymentDetails.FromFutureHtlc(listOf(cmd.paymentHash))
remoteFundingRates.paymentTypes.contains(LiquidityAds.PaymentType.FromFutureHtlcWithPreimage) -> LiquidityAds.PaymentDetails.FromFutureHtlcWithPreimage(listOf(cmd.preimage))
else -> null
Expand Down
Loading

0 comments on commit 11d94a6

Please sign in to comment.