Skip to content

Commit

Permalink
Add max_allowed_fee_credit
Browse files Browse the repository at this point in the history
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.
  • Loading branch information
t-bast committed Sep 25, 2024
1 parent a595d44 commit 38ee3dc
Show file tree
Hide file tree
Showing 7 changed files with 119 additions and 35 deletions.
2 changes: 1 addition & 1 deletion src/commonMain/kotlin/fr/acinq/lightning/NodeEvents.kt
Original file line number Diff line number Diff line change
Expand Up @@ -50,7 +50,7 @@ sealed interface LiquidityEvents : NodeEvents {
}
data object ChannelFundingInProgress : Reason()
data object NoMatchingFundingRate : Reason()
data class MissingOffChainAmountTooLow(val missingOffChainAmount: MilliSatoshi) : Reason()
data class MissingOffChainAmountTooLow(val missingOffChainAmount: MilliSatoshi, val currentFeeCredit: MilliSatoshi) : Reason()
data class TooManyParts(val parts: Int) : Reason()
}
}
Expand Down
10 changes: 9 additions & 1 deletion src/commonMain/kotlin/fr/acinq/lightning/NodeParams.kt
Original file line number Diff line number Diff line change
Expand Up @@ -232,7 +232,15 @@ data class NodeParams(
maxPaymentAttempts = 5,
zeroConfPeers = emptySet(),
paymentRecipientExpiryParams = RecipientCltvExpiryParams(CltvExpiryDelta(75), CltvExpiryDelta(200)),
liquidityPolicy = MutableStateFlow<LiquidityPolicy>(LiquidityPolicy.Auto(inboundLiquidityTarget = null, maxAbsoluteFee = 2_000.sat, maxRelativeFeeBasisPoints = 3_000 /* 3000 = 30 % */, skipAbsoluteFeeCheck = false)),
liquidityPolicy = MutableStateFlow<LiquidityPolicy>(
LiquidityPolicy.Auto(
inboundLiquidityTarget = null,
maxAbsoluteFee = 2_000.sat,
maxRelativeFeeBasisPoints = 3_000 /* 3000 = 30 % */,
skipAbsoluteFeeCheck = false,
maxAllowedFeeCredit = 0.msat
)
),
minFinalCltvExpiryDelta = Bolt11Invoice.DEFAULT_MIN_FINAL_EXPIRY_DELTA,
maxFinalCltvExpiryDelta = CltvExpiryDelta(360),
bolt12invoiceExpiry = 60.seconds,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -232,7 +232,7 @@ class IncomingPaymentHandler(val nodeParams: NodeParams, val db: PaymentsDb) {
nodeParams._nodeEvents.emit(LiquidityEvents.Rejected(payment.amountReceived, 0.msat, LiquidityEvents.Source.OffChainPayment, LiquidityEvents.Rejected.Reason.TooManyParts(payment.parts.size)))
rejectPayment(payment, incomingPayment, TemporaryNodeFailure)
}
willAddHtlcParts.isNotEmpty() -> when (val result = validateOnTheFlyFundingRate(willAddHtlcParts.map { it.amount }.sum(), remoteFeatures, currentFeerate, remoteFundingRates)) {
willAddHtlcParts.isNotEmpty() -> when (val result = validateOnTheFlyFundingRate(willAddHtlcParts.map { it.amount }.sum(), remoteFeatures, currentFeeCredit, currentFeerate, remoteFundingRates)) {
is Either.Left -> {
logger.warning { "rejecting on-the-fly funding: reason=${result.value.reason}" }
nodeParams._nodeEvents.emit(result.value)
Expand All @@ -241,13 +241,17 @@ class IncomingPaymentHandler(val nodeParams: NodeParams, val db: PaymentsDb) {
is Either.Right -> {
val (requestedAmount, fundingRate) = result.value
val addToFeeCredit = run {
val featureOk = nodeParams.features.hasFeature(Feature.FundingFeeCredit) && remoteFeatures.hasFeature(Feature.FundingFeeCredit)
val featureOk = Features.canUseFeature(nodeParams.features, remoteFeatures, Feature.FundingFeeCredit)
// We may need to use a higher feerate than the current value depending on whether this is a new channel or not,
// and whether we have enough balance. We keep adding to our fee credit until we haven't reached the worst case
// scenario in terms of fees we need to pay, otherwise we may not have enough to actually pay the liquidity fees.
val feerateThreshold = currentFeerate * AddLiquidityForIncomingPayment.SpliceWithNoBalanceFeerateRatio
val feeCreditThreshold = fundingRate.fees(feerateThreshold, requestedAmount, requestedAmount, isChannelCreation = true).total
val amountBelowThreshold = (payment.amountReceived + currentFeeCredit).truncateToSatoshi() < feeCreditThreshold
// and whether we have enough balance. We keep adding to our fee credit until we reach the worst case scenario
// in terms of fees we need to pay, otherwise we may not have enough to actually pay the liquidity fees.
val maxFeerate = currentFeerate * AddLiquidityForIncomingPayment.SpliceWithNoBalanceFeerateRatio
val maxLiquidityFees = fundingRate.fees(maxFeerate, requestedAmount, requestedAmount, isChannelCreation = true).total.toMilliSatoshi()
val feeCreditThreshold = when (val policy = nodeParams.liquidityPolicy.value) {
LiquidityPolicy.Disable -> maxLiquidityFees
is LiquidityPolicy.Auto -> maxLiquidityFees.min(policy.maxAllowedFeeCredit)
}
val amountBelowThreshold = (willAddHtlcParts.map { it.amount }.sum() + currentFeeCredit) <= feeCreditThreshold
featureOk && amountBelowThreshold
}
when {
Expand Down Expand Up @@ -340,7 +344,13 @@ class IncomingPaymentHandler(val nodeParams: NodeParams, val db: PaymentsDb) {
return ProcessAddResult.Rejected(actions, incomingPayment)
}

private fun validateOnTheFlyFundingRate(willAddHtlcAmount: MilliSatoshi, remoteFeatures: Features, currentFeerate: FeeratePerKw, remoteFundingRates: LiquidityAds.WillFundRates?): Either<LiquidityEvents.Rejected, Pair<Satoshi, LiquidityAds.FundingRate>> {
private fun validateOnTheFlyFundingRate(
willAddHtlcAmount: MilliSatoshi,
remoteFeatures: Features,
currentFeeCredit: MilliSatoshi,
currentFeerate: FeeratePerKw,
remoteFundingRates: LiquidityAds.WillFundRates?
): Either<LiquidityEvents.Rejected, Pair<Satoshi, LiquidityAds.FundingRate>> {
return when (val liquidityPolicy = nodeParams.liquidityPolicy.value) {
is LiquidityPolicy.Disable -> Either.Left(LiquidityEvents.Rejected(willAddHtlcAmount, 0.msat, LiquidityEvents.Source.OffChainPayment, LiquidityEvents.Rejected.Reason.PolicySetToDisabled))
is LiquidityPolicy.Auto -> {
Expand All @@ -355,17 +365,17 @@ class IncomingPaymentHandler(val nodeParams: NodeParams, val db: PaymentsDb) {
// 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 canAddToFeeCredit = Features.canUseFeature(nodeParams.features, remoteFeatures, Feature.FundingFeeCredit) && (willAddHtlcAmount + currentFeeCredit) <= liquidityPolicy.maxAllowedFeeCredit
val rejected = when {
// We never reject if we can use the fee credit feature.
// We instead add payments to our fee credit until making an on-chain operation becomes acceptable.
nodeParams.features.hasFeature(Feature.FundingFeeCredit) && remoteFeatures.hasFeature(Feature.FundingFeeCredit) -> null
// 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 < fees * 2 -> LiquidityEvents.Rejected(
(willAddHtlcAmount + currentFeeCredit) < fees * 2 -> LiquidityEvents.Rejected(
requestedAmount.toMilliSatoshi(),
fees.toMilliSatoshi(),
LiquidityEvents.Source.OffChainPayment,
LiquidityEvents.Rejected.Reason.MissingOffChainAmountTooLow(willAddHtlcAmount)
LiquidityEvents.Rejected.Reason.MissingOffChainAmountTooLow(willAddHtlcAmount, currentFeeCredit)
)
else -> liquidityPolicy.maybeReject(requestedAmount.toMilliSatoshi(), fees.toMilliSatoshi(), LiquidityEvents.Source.OffChainPayment, logger)
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -19,8 +19,9 @@ sealed class LiquidityPolicy {
* @param maxAbsoluteFee max absolute fee
* @param maxRelativeFeeBasisPoints max relative fee (all included: service fee and mining fee) (1_000 bips = 10 %)
* @param skipAbsoluteFeeCheck only applies for off-chain payments, being more lax may make sense when the sender doesn't retry payments
* @param maxAllowedFeeCredit maximum amount that can be added to fee credit (see [fr.acinq.lightning.Feature.FundingFeeCredit])
*/
data class Auto(val inboundLiquidityTarget: Satoshi?, val maxAbsoluteFee: Satoshi, val maxRelativeFeeBasisPoints: Int, val skipAbsoluteFeeCheck: Boolean) : 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? {
Expand Down
5 changes: 3 additions & 2 deletions src/commonTest/kotlin/fr/acinq/lightning/io/peer/PeerTest.kt
Original file line number Diff line number Diff line change
Expand Up @@ -222,7 +222,7 @@ class PeerTest : LightningTestSuite() {
@Test
fun `swap funds into a channel`() = runSuspendTest {
val nodeParams = Pair(TestConstants.Alice.nodeParams, TestConstants.Bob.nodeParams)
nodeParams.second.liquidityPolicy.emit(LiquidityPolicy.Auto(inboundLiquidityTarget = 100_000.sat, maxAbsoluteFee = 20_000.sat, maxRelativeFeeBasisPoints = 1000, skipAbsoluteFeeCheck = false))
nodeParams.second.liquidityPolicy.emit(LiquidityPolicy.Auto(inboundLiquidityTarget = 100_000.sat, maxAbsoluteFee = 20_000.sat, maxRelativeFeeBasisPoints = 1000, skipAbsoluteFeeCheck = false, maxAllowedFeeCredit = 0.msat))
val walletParams = Pair(TestConstants.Alice.walletParams, TestConstants.Bob.walletParams)
val (_, bob, _, bob2alice) = newPeers(this, nodeParams, walletParams, automateMessaging = false)

Expand All @@ -248,7 +248,8 @@ class PeerTest : LightningTestSuite() {
inboundLiquidityTarget = 500_000.sat,
maxAbsoluteFee = 100.sat,
maxRelativeFeeBasisPoints = 10,
skipAbsoluteFeeCheck = false
skipAbsoluteFeeCheck = false,
maxAllowedFeeCredit = 0.msat,
)
nodeParams.second.liquidityPolicy.emit(bobPolicy)
val walletBob = createWallet(nodeParams.second.keyManager, 1_000_000.sat).second
Expand Down
Loading

0 comments on commit 38ee3dc

Please sign in to comment.