Skip to content

Commit

Permalink
use ChannelManagement fees in LiquidityPolicy
Browse files Browse the repository at this point in the history
Instead of aggregating mining and service fees. We still use the `total`
for the fee check.

No functional changes.
  • Loading branch information
pm47 committed Oct 10, 2024
1 parent e99c9af commit 78f6449
Show file tree
Hide file tree
Showing 4 changed files with 21 additions and 17 deletions.
4 changes: 2 additions & 2 deletions src/commonMain/kotlin/fr/acinq/lightning/io/Peer.kt
Original file line number Diff line number Diff line change
Expand Up @@ -1289,7 +1289,7 @@ class Peer(
val weight = FundingContributions.computeWeightPaid(isInitiator = true, commitment = available.channel.commitments.active.first(), walletInputs = cmd.walletInputs, localOutputs = emptyList())
val (feerate, fee) = client.computeSpliceCpfpFeerate(available.channel.commitments, targetFeerate, spliceWeight = weight, logger)
logger.info { "requesting splice-in using balance=${cmd.walletInputs.balance} feerate=$feerate fee=$fee" }
when (val rejected = nodeParams.liquidityPolicy.value.maybeReject(cmd.walletInputs.balance.toMilliSatoshi(), fee.toMilliSatoshi(), LiquidityEvents.Source.OnChainWallet, logger)) {
when (val rejected = nodeParams.liquidityPolicy.value.maybeReject(cmd.walletInputs.balance.toMilliSatoshi(), ChannelManagementFees(miningFee = fee, serviceFee = 0.sat), LiquidityEvents.Source.OnChainWallet, logger)) {
is LiquidityEvents.Rejected -> {
logger.info { "rejecting splice: reason=${rejected.reason}" }
nodeParams._nodeEvents.emit(rejected)
Expand Down Expand Up @@ -1356,7 +1356,7 @@ class Peer(
swapInCommands.trySend(SwapInCommand.UnlockWalletInputs(cmd.walletInputs.map { it.outPoint }.toSet()))
} else {
val totalAmount = cmd.walletInputs.balance.toMilliSatoshi() + requestRemoteFunding.requestedAmount.toMilliSatoshi()
when (val rejected = nodeParams.liquidityPolicy.first().maybeReject(totalAmount, fees.total.toMilliSatoshi(), LiquidityEvents.Source.OnChainWallet, logger)) {
when (val rejected = nodeParams.liquidityPolicy.first().maybeReject(totalAmount, fees, LiquidityEvents.Source.OnChainWallet, logger)) {
is LiquidityEvents.Rejected -> {
logger.info { "rejecting channel open: reason=${rejected.reason}" }
nodeParams._nodeEvents.emit(rejected)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -275,7 +275,8 @@ class IncomingPaymentHandler(val nodeParams: NodeParams, val db: PaymentsDb) {
else -> {
// We're not adding to our fee credit, so we need to check our liquidity policy.
// Even if we have enough fee credit to pay the fees, we may want to wait for a lower feerate.
val fees = fundingRate.fees(currentFeerate, requestedAmount, requestedAmount, isChannelCreation = true).total.toMilliSatoshi()
val fees = fundingRate.fees(currentFeerate, requestedAmount, requestedAmount, isChannelCreation = true)
.let { ChannelManagementFees(miningFee = it.miningFee, serviceFee = it.serviceFee) }
when (val rejected = nodeParams.liquidityPolicy.value.maybeReject(requestedAmount.toMilliSatoshi(), fees, LiquidityEvents.Source.OffChainPayment, logger)) {
is LiquidityEvents.Rejected -> {
nodeParams._nodeEvents.emit(rejected)
Expand Down Expand Up @@ -368,21 +369,22 @@ class IncomingPaymentHandler(val nodeParams: NodeParams, val db: PaymentsDb) {
else -> {
// We don't know at that point if we'll need a channel or if we already have one.
// We must use the worst case fees that applies to channel creation.
val fees = fundingRate.fees(currentFeerate, requestedAmount, requestedAmount, isChannelCreation = true).total
val fees = fundingRate.fees(currentFeerate, requestedAmount, requestedAmount, isChannelCreation = true)
.let { ChannelManagementFees(miningFee = it.miningFee, serviceFee = it.serviceFee) }
val canAddToFeeCredit = Features.canUseFeature(nodeParams.features, remoteFeatures, Feature.FundingFeeCredit) && (willAddHtlcAmount + currentFeeCredit) <= liquidityPolicy.maxAllowedFeeCredit
logger.info { "on-the-fly assessment: amount=$requestedAmount feerate=$currentFeerate fees=$fees" }
val rejected = when {
// We never reject if we can add payments to our fee credit until making an on-chain operation becomes acceptable.
canAddToFeeCredit -> null
// We only initiate on-the-fly funding if the missing amount is greater than the fees paid.
// Otherwise our peer may not be able to claim the funding fees from the relayed HTLCs.
(willAddHtlcAmount + currentFeeCredit) < fees * 2 -> LiquidityEvents.Rejected(
(willAddHtlcAmount + currentFeeCredit) < fees.total * 2 -> LiquidityEvents.Rejected(
requestedAmount.toMilliSatoshi(),
fees.toMilliSatoshi(),
fees.total.toMilliSatoshi(),
LiquidityEvents.Source.OffChainPayment,
LiquidityEvents.Rejected.Reason.MissingOffChainAmountTooLow(willAddHtlcAmount, currentFeeCredit)
)
else -> liquidityPolicy.maybeReject(requestedAmount.toMilliSatoshi(), fees.toMilliSatoshi(), LiquidityEvents.Source.OffChainPayment, logger)
else -> liquidityPolicy.maybeReject(requestedAmount.toMilliSatoshi(), fees, LiquidityEvents.Source.OffChainPayment, logger)
}
when (rejected) {
null -> Either.Right(Pair(requestedAmount, fundingRate))
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ package fr.acinq.lightning.payment
import fr.acinq.bitcoin.Satoshi
import fr.acinq.lightning.LiquidityEvents
import fr.acinq.lightning.MilliSatoshi
import fr.acinq.lightning.channel.ChannelManagementFees
import fr.acinq.lightning.logging.MDCLogger
import fr.acinq.lightning.utils.msat
import fr.acinq.lightning.utils.sat
Expand All @@ -24,19 +25,19 @@ sealed class LiquidityPolicy {
data class Auto(val inboundLiquidityTarget: Satoshi?, val maxAbsoluteFee: Satoshi, val maxRelativeFeeBasisPoints: Int, val skipAbsoluteFeeCheck: Boolean, val maxAllowedFeeCredit: MilliSatoshi) : LiquidityPolicy()

/** Make a decision for a particular liquidity event. */
fun maybeReject(amount: MilliSatoshi, fee: MilliSatoshi, source: LiquidityEvents.Source, logger: MDCLogger): LiquidityEvents.Rejected? {
fun maybeReject(amount: MilliSatoshi, fee: ChannelManagementFees, source: LiquidityEvents.Source, logger: MDCLogger): LiquidityEvents.Rejected? {
return when (this) {
is Disable -> LiquidityEvents.Rejected.Reason.PolicySetToDisabled
is Auto -> {
val maxAbsoluteFee = if (skipAbsoluteFeeCheck && source == LiquidityEvents.Source.OffChainPayment) Long.MAX_VALUE.msat else this.maxAbsoluteFee.toMilliSatoshi()
val maxRelativeFee = amount * maxRelativeFeeBasisPoints / 10_000
logger.info { "liquidity policy check: amount=$amount liquidityTarget=${inboundLiquidityTarget ?: 0.sat} fee=$fee maxAbsoluteFee=$maxAbsoluteFee maxRelativeFee=$maxRelativeFee policy=$this" }
when {
fee > maxRelativeFee -> LiquidityEvents.Rejected.Reason.TooExpensive.OverRelativeFee(maxRelativeFeeBasisPoints)
fee > maxAbsoluteFee -> LiquidityEvents.Rejected.Reason.TooExpensive.OverAbsoluteFee(this.maxAbsoluteFee)
fee.total.toMilliSatoshi() > maxRelativeFee -> LiquidityEvents.Rejected.Reason.TooExpensive.OverRelativeFee(maxRelativeFeeBasisPoints)
fee.total.toMilliSatoshi() > maxAbsoluteFee -> LiquidityEvents.Rejected.Reason.TooExpensive.OverAbsoluteFee(this.maxAbsoluteFee)
else -> null // accept
}
}
}?.let { reason -> LiquidityEvents.Rejected(amount, fee, source, reason) }
}?.let { reason -> LiquidityEvents.Rejected(amount, fee.total.toMilliSatoshi(), source, reason) }
}
}
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
package fr.acinq.lightning.payment

import fr.acinq.lightning.LiquidityEvents
import fr.acinq.lightning.channel.ChannelManagementFees
import fr.acinq.lightning.logging.MDCLogger
import fr.acinq.lightning.tests.utils.LightningTestSuite
import fr.acinq.lightning.utils.msat
Expand All @@ -19,30 +20,30 @@ class LiquidityPolicyTestsCommon : LightningTestSuite() {
// fee over both absolute and relative
assertEquals(
expected = LiquidityEvents.Rejected.Reason.TooExpensive.OverRelativeFee(policy.maxRelativeFeeBasisPoints),
actual = policy.maybeReject(amount = 4_000_000.msat, fee = 3_000_000.msat, source = LiquidityEvents.Source.OffChainPayment, logger)?.reason
actual = policy.maybeReject(amount = 4_000_000.msat, fee = ChannelManagementFees(miningFee = 3_000.sat, serviceFee = 0.sat), source = LiquidityEvents.Source.OffChainPayment, logger)?.reason
)
// fee over absolute
assertEquals(
expected = LiquidityEvents.Rejected.Reason.TooExpensive.OverAbsoluteFee(policy.maxAbsoluteFee),
actual = policy.maybeReject(amount = 15_000_000.msat, fee = 3_000_000.msat, source = LiquidityEvents.Source.OffChainPayment, logger)?.reason
actual = policy.maybeReject(amount = 15_000_000.msat, fee = ChannelManagementFees(miningFee = 3_000.sat, serviceFee = 0.sat), source = LiquidityEvents.Source.OffChainPayment, logger)?.reason
)
// fee over relative
assertEquals(
expected = LiquidityEvents.Rejected.Reason.TooExpensive.OverRelativeFee(policy.maxRelativeFeeBasisPoints),
actual = policy.maybeReject(amount = 4_000_000.msat, fee = 2_000_000.msat, source = LiquidityEvents.Source.OffChainPayment, logger)?.reason
actual = policy.maybeReject(amount = 4_000_000.msat, fee = ChannelManagementFees(miningFee = 2_000.sat, serviceFee = 0.sat), source = LiquidityEvents.Source.OffChainPayment, logger)?.reason
)
assertNull(policy.maybeReject(amount = 10_000_000.msat, fee = 2_000_000.msat, source = LiquidityEvents.Source.OffChainPayment, logger))
assertNull(policy.maybeReject(amount = 10_000_000.msat, fee = ChannelManagementFees(miningFee = 2_000.sat, serviceFee = 0.sat), source = LiquidityEvents.Source.OffChainPayment, logger))
}

@Test
fun `policy rejection skip absolute check`() {
val policy = LiquidityPolicy.Auto(maxAbsoluteFee = 1_000.sat, maxRelativeFeeBasisPoints = 5_000 /* 3000 = 30 % */, skipAbsoluteFeeCheck = true, inboundLiquidityTarget = null, maxAllowedFeeCredit = 0.msat)
// fee is over absolute, and it's an offchain payment so the check passes
assertNull(policy.maybeReject(amount = 4_000_000.msat, fee = 2_000_000.msat, source = LiquidityEvents.Source.OffChainPayment, logger))
assertNull(policy.maybeReject(amount = 4_000_000.msat, fee = ChannelManagementFees(miningFee = 2_000.sat, serviceFee = 0.sat), source = LiquidityEvents.Source.OffChainPayment, logger))
// fee is over absolute, but it's an on-chain payment so the check fails
assertEquals(
expected = LiquidityEvents.Rejected.Reason.TooExpensive.OverAbsoluteFee(policy.maxAbsoluteFee),
actual = policy.maybeReject(amount = 4_000_000.msat, fee = 2_000_000.msat, source = LiquidityEvents.Source.OnChainWallet, logger)?.reason
actual = policy.maybeReject(amount = 4_000_000.msat, fee = ChannelManagementFees(miningFee = 2_000.sat, serviceFee = 0.sat), source = LiquidityEvents.Source.OnChainWallet, logger)?.reason
)
}
}

0 comments on commit 78f6449

Please sign in to comment.