Skip to content

Commit dd5f736

Browse files
committed
Add max_allowed_fee_credit
Add a parameter to our liquidity policy to limit the amount that can be allocated to fee credit. We don't want a temporary high feerate to cause wallet users to allocate too much funds towards their fee credit, which they may not use later.
1 parent aab1d7d commit dd5f736

File tree

7 files changed

+119
-35
lines changed

7 files changed

+119
-35
lines changed

src/commonMain/kotlin/fr/acinq/lightning/NodeEvents.kt

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -50,7 +50,7 @@ sealed interface LiquidityEvents : NodeEvents {
5050
}
5151
data object ChannelFundingInProgress : Reason()
5252
data object NoMatchingFundingRate : Reason()
53-
data class MissingOffChainAmountTooLow(val missingOffChainAmount: MilliSatoshi) : Reason()
53+
data class MissingOffChainAmountTooLow(val missingOffChainAmount: MilliSatoshi, val currentFeeCredit: MilliSatoshi) : Reason()
5454
data class TooManyParts(val parts: Int) : Reason()
5555
}
5656
}

src/commonMain/kotlin/fr/acinq/lightning/NodeParams.kt

Lines changed: 9 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -232,7 +232,15 @@ data class NodeParams(
232232
maxPaymentAttempts = 5,
233233
zeroConfPeers = emptySet(),
234234
paymentRecipientExpiryParams = RecipientCltvExpiryParams(CltvExpiryDelta(75), CltvExpiryDelta(200)),
235-
liquidityPolicy = MutableStateFlow<LiquidityPolicy>(LiquidityPolicy.Auto(inboundLiquidityTarget = null, maxAbsoluteFee = 2_000.sat, maxRelativeFeeBasisPoints = 3_000 /* 3000 = 30 % */, skipAbsoluteFeeCheck = false)),
235+
liquidityPolicy = MutableStateFlow<LiquidityPolicy>(
236+
LiquidityPolicy.Auto(
237+
inboundLiquidityTarget = null,
238+
maxAbsoluteFee = 2_000.sat,
239+
maxRelativeFeeBasisPoints = 3_000 /* 3000 = 30 % */,
240+
skipAbsoluteFeeCheck = false,
241+
maxAllowedFeeCredit = 0.msat
242+
)
243+
),
236244
minFinalCltvExpiryDelta = Bolt11Invoice.DEFAULT_MIN_FINAL_EXPIRY_DELTA,
237245
maxFinalCltvExpiryDelta = CltvExpiryDelta(360),
238246
bolt12invoiceExpiry = 60.seconds,

src/commonMain/kotlin/fr/acinq/lightning/payment/IncomingPaymentHandler.kt

Lines changed: 23 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -232,7 +232,7 @@ class IncomingPaymentHandler(val nodeParams: NodeParams, val db: PaymentsDb) {
232232
nodeParams._nodeEvents.emit(LiquidityEvents.Rejected(payment.amountReceived, 0.msat, LiquidityEvents.Source.OffChainPayment, LiquidityEvents.Rejected.Reason.TooManyParts(payment.parts.size)))
233233
rejectPayment(payment, incomingPayment, TemporaryNodeFailure)
234234
}
235-
willAddHtlcParts.isNotEmpty() -> when (val result = validateOnTheFlyFundingRate(willAddHtlcParts.map { it.amount }.sum(), remoteFeatures, currentFeerate, remoteFundingRates)) {
235+
willAddHtlcParts.isNotEmpty() -> when (val result = validateOnTheFlyFundingRate(willAddHtlcParts.map { it.amount }.sum(), remoteFeatures, currentFeeCredit, currentFeerate, remoteFundingRates)) {
236236
is Either.Left -> {
237237
logger.warning { "rejecting on-the-fly funding: reason=${result.value.reason}" }
238238
nodeParams._nodeEvents.emit(result.value)
@@ -241,13 +241,17 @@ class IncomingPaymentHandler(val nodeParams: NodeParams, val db: PaymentsDb) {
241241
is Either.Right -> {
242242
val (requestedAmount, fundingRate) = result.value
243243
val addToFeeCredit = run {
244-
val featureOk = nodeParams.features.hasFeature(Feature.FundingFeeCredit) && remoteFeatures.hasFeature(Feature.FundingFeeCredit)
244+
val featureOk = Features.canUseFeature(nodeParams.features, remoteFeatures, Feature.FundingFeeCredit)
245245
// We may need to use a higher feerate than the current value depending on whether this is a new channel or not,
246-
// and whether we have enough balance. We keep adding to our fee credit until we haven't reached the worst case
247-
// scenario in terms of fees we need to pay, otherwise we may not have enough to actually pay the liquidity fees.
248-
val feerateThreshold = currentFeerate * AddLiquidityForIncomingPayment.SpliceWithNoBalanceFeerateRatio
249-
val feeCreditThreshold = fundingRate.fees(feerateThreshold, requestedAmount, requestedAmount, isChannelCreation = true).total
250-
val amountBelowThreshold = (payment.amountReceived + currentFeeCredit).truncateToSatoshi() < feeCreditThreshold
246+
// and whether we have enough balance. We keep adding to our fee credit until we reach the worst case scenario
247+
// in terms of fees we need to pay, otherwise we may not have enough to actually pay the liquidity fees.
248+
val maxFeerate = currentFeerate * AddLiquidityForIncomingPayment.SpliceWithNoBalanceFeerateRatio
249+
val maxLiquidityFees = fundingRate.fees(maxFeerate, requestedAmount, requestedAmount, isChannelCreation = true).total.toMilliSatoshi()
250+
val feeCreditThreshold = when (val policy = nodeParams.liquidityPolicy.value) {
251+
LiquidityPolicy.Disable -> maxLiquidityFees
252+
is LiquidityPolicy.Auto -> maxLiquidityFees.min(policy.maxAllowedFeeCredit)
253+
}
254+
val amountBelowThreshold = (willAddHtlcParts.map { it.amount }.sum() + currentFeeCredit) <= feeCreditThreshold
251255
featureOk && amountBelowThreshold
252256
}
253257
when {
@@ -340,7 +344,13 @@ class IncomingPaymentHandler(val nodeParams: NodeParams, val db: PaymentsDb) {
340344
return ProcessAddResult.Rejected(actions, incomingPayment)
341345
}
342346

343-
private fun validateOnTheFlyFundingRate(willAddHtlcAmount: MilliSatoshi, remoteFeatures: Features, currentFeerate: FeeratePerKw, remoteFundingRates: LiquidityAds.WillFundRates?): Either<LiquidityEvents.Rejected, Pair<Satoshi, LiquidityAds.FundingRate>> {
347+
private fun validateOnTheFlyFundingRate(
348+
willAddHtlcAmount: MilliSatoshi,
349+
remoteFeatures: Features,
350+
currentFeeCredit: MilliSatoshi,
351+
currentFeerate: FeeratePerKw,
352+
remoteFundingRates: LiquidityAds.WillFundRates?
353+
): Either<LiquidityEvents.Rejected, Pair<Satoshi, LiquidityAds.FundingRate>> {
344354
return when (val liquidityPolicy = nodeParams.liquidityPolicy.value) {
345355
is LiquidityPolicy.Disable -> Either.Left(LiquidityEvents.Rejected(willAddHtlcAmount, 0.msat, LiquidityEvents.Source.OffChainPayment, LiquidityEvents.Rejected.Reason.PolicySetToDisabled))
346356
is LiquidityPolicy.Auto -> {
@@ -355,17 +365,17 @@ class IncomingPaymentHandler(val nodeParams: NodeParams, val db: PaymentsDb) {
355365
// We don't know at that point if we'll need a channel or if we already have one.
356366
// We must use the worst case fees that applies to channel creation.
357367
val fees = fundingRate.fees(currentFeerate, requestedAmount, requestedAmount, isChannelCreation = true).total
368+
val canAddToFeeCredit = Features.canUseFeature(nodeParams.features, remoteFeatures, Feature.FundingFeeCredit) && (willAddHtlcAmount + currentFeeCredit) <= liquidityPolicy.maxAllowedFeeCredit
358369
val rejected = when {
359-
// We never reject if we can use the fee credit feature.
360-
// We instead add payments to our fee credit until making an on-chain operation becomes acceptable.
361-
nodeParams.features.hasFeature(Feature.FundingFeeCredit) && remoteFeatures.hasFeature(Feature.FundingFeeCredit) -> null
370+
// We never reject if we can add payments to our fee credit until making an on-chain operation becomes acceptable.
371+
canAddToFeeCredit -> null
362372
// We only initiate on-the-fly funding if the missing amount is greater than the fees paid.
363373
// Otherwise our peer may not be able to claim the funding fees from the relayed HTLCs.
364-
willAddHtlcAmount < fees * 2 -> LiquidityEvents.Rejected(
374+
(willAddHtlcAmount + currentFeeCredit) < fees * 2 -> LiquidityEvents.Rejected(
365375
requestedAmount.toMilliSatoshi(),
366376
fees.toMilliSatoshi(),
367377
LiquidityEvents.Source.OffChainPayment,
368-
LiquidityEvents.Rejected.Reason.MissingOffChainAmountTooLow(willAddHtlcAmount)
378+
LiquidityEvents.Rejected.Reason.MissingOffChainAmountTooLow(willAddHtlcAmount, currentFeeCredit)
369379
)
370380
else -> liquidityPolicy.maybeReject(requestedAmount.toMilliSatoshi(), fees.toMilliSatoshi(), LiquidityEvents.Source.OffChainPayment, logger)
371381
}

src/commonMain/kotlin/fr/acinq/lightning/payment/LiquidityPolicy.kt

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -19,8 +19,9 @@ sealed class LiquidityPolicy {
1919
* @param maxAbsoluteFee max absolute fee
2020
* @param maxRelativeFeeBasisPoints max relative fee (all included: service fee and mining fee) (1_000 bips = 10 %)
2121
* @param skipAbsoluteFeeCheck only applies for off-chain payments, being more lax may make sense when the sender doesn't retry payments
22+
* @param maxAllowedFeeCredit maximum amount that can be added to fee credit (see [fr.acinq.lightning.Feature.FundingFeeCredit])
2223
*/
23-
data class Auto(val inboundLiquidityTarget: Satoshi?, val maxAbsoluteFee: Satoshi, val maxRelativeFeeBasisPoints: Int, val skipAbsoluteFeeCheck: Boolean) : LiquidityPolicy()
24+
data class Auto(val inboundLiquidityTarget: Satoshi?, val maxAbsoluteFee: Satoshi, val maxRelativeFeeBasisPoints: Int, val skipAbsoluteFeeCheck: Boolean, val maxAllowedFeeCredit: MilliSatoshi) : LiquidityPolicy()
2425

2526
/** Make a decision for a particular liquidity event. */
2627
fun maybeReject(amount: MilliSatoshi, fee: MilliSatoshi, source: LiquidityEvents.Source, logger: MDCLogger): LiquidityEvents.Rejected? {

src/commonTest/kotlin/fr/acinq/lightning/io/peer/PeerTest.kt

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -222,7 +222,7 @@ class PeerTest : LightningTestSuite() {
222222
@Test
223223
fun `swap funds into a channel`() = runSuspendTest {
224224
val nodeParams = Pair(TestConstants.Alice.nodeParams, TestConstants.Bob.nodeParams)
225-
nodeParams.second.liquidityPolicy.emit(LiquidityPolicy.Auto(inboundLiquidityTarget = 100_000.sat, maxAbsoluteFee = 20_000.sat, maxRelativeFeeBasisPoints = 1000, skipAbsoluteFeeCheck = false))
225+
nodeParams.second.liquidityPolicy.emit(LiquidityPolicy.Auto(inboundLiquidityTarget = 100_000.sat, maxAbsoluteFee = 20_000.sat, maxRelativeFeeBasisPoints = 1000, skipAbsoluteFeeCheck = false, maxAllowedFeeCredit = 0.msat))
226226
val walletParams = Pair(TestConstants.Alice.walletParams, TestConstants.Bob.walletParams)
227227
val (_, bob, _, bob2alice) = newPeers(this, nodeParams, walletParams, automateMessaging = false)
228228

@@ -248,7 +248,8 @@ class PeerTest : LightningTestSuite() {
248248
inboundLiquidityTarget = 500_000.sat,
249249
maxAbsoluteFee = 100.sat,
250250
maxRelativeFeeBasisPoints = 10,
251-
skipAbsoluteFeeCheck = false
251+
skipAbsoluteFeeCheck = false,
252+
maxAllowedFeeCredit = 0.msat,
252253
)
253254
nodeParams.second.liquidityPolicy.emit(bobPolicy)
254255
val walletBob = createWallet(nodeParams.second.keyManager, 1_000_000.sat).second

0 commit comments

Comments
 (0)