Skip to content
Permalink

Comparing changes

Choose two branches to see what’s changed or to start a new pull request. If you need to, you can also or learn more about diff comparisons.

Open a pull request

Create a new pull request by comparing changes across two branches. If you need to, you can also . Learn more about diff comparisons here.
base repository: ACINQ/lightning-kmp
Failed to load repositories. Confirm that selected base ref is valid, then try again.
Loading
base: v1.8.1
Choose a base ref
...
head repository: ACINQ/lightning-kmp
Failed to load repositories. Confirm that selected head ref is valid, then try again.
Loading
compare: v1.8.2
Choose a head ref
  • 3 commits
  • 6 files changed
  • 2 contributors

Commits on Oct 9, 2024

  1. Back to SNAPSHOT

    pm47 committed Oct 9, 2024

    Verified

    This commit was signed with the committer’s verified signature.
    pm47 Pierre-Marie Padiou
    Copy the full SHA
    e99c9af View commit details

Commits on Oct 10, 2024

  1. Add option to only consider the mining fee for the absolute fee check (

    …#713)
    
    This is useful in conjunction with a non-zero liquidity target.
    pm47 authored Oct 10, 2024

    Verified

    This commit was created on GitHub.com and signed with GitHub’s verified signature.
    Copy the full SHA
    440981b View commit details
  2. Release v1.8.2

    pm47 committed Oct 10, 2024

    Verified

    This commit was signed with the committer’s verified signature.
    pm47 Pierre-Marie Padiou
    Copy the full SHA
    3071ece View commit details
2 changes: 1 addition & 1 deletion build.gradle.kts
Original file line number Diff line number Diff line change
@@ -11,7 +11,7 @@ plugins {

allprojects {
group = "fr.acinq.lightning"
version = "1.8.1"
version = "1.8.2"

repositories {
// using the local maven repository with Kotlin Multi Platform can lead to build errors that are hard to diagnose.
1 change: 1 addition & 0 deletions src/commonMain/kotlin/fr/acinq/lightning/NodeParams.kt
Original file line number Diff line number Diff line change
@@ -239,6 +239,7 @@ data class NodeParams(
maxAbsoluteFee = 2_000.sat,
maxRelativeFeeBasisPoints = 3_000 /* 3000 = 30 % */,
skipAbsoluteFeeCheck = false,
considerOnlyMiningFeeForAbsoluteFeeCheck = false,
maxAllowedFeeCredit = 0.msat
)
),
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
@@ -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)
@@ -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)
Original file line number Diff line number Diff line change
@@ -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)
@@ -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))
Original file line number Diff line number Diff line change
@@ -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
@@ -19,24 +20,26 @@ 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 considerOnlyMiningFeeForAbsoluteFeeCheck only consider the mining fee for the absolute fee check. This makes sense in `inboundLiquidityTarget` is used, and the funding rate predictable
* @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, val maxAllowedFeeCredit: MilliSatoshi) : LiquidityPolicy()
data class Auto(val inboundLiquidityTarget: Satoshi?, val maxAbsoluteFee: Satoshi, val maxRelativeFeeBasisPoints: Int, val skipAbsoluteFeeCheck: Boolean, val maxAllowedFeeCredit: MilliSatoshi, val considerOnlyMiningFeeForAbsoluteFeeCheck: Boolean = false) : 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)
considerOnlyMiningFeeForAbsoluteFeeCheck && fee.miningFee.toMilliSatoshi() > maxAbsoluteFee -> LiquidityEvents.Rejected.Reason.TooExpensive.OverAbsoluteFee(this.maxAbsoluteFee)
!considerOnlyMiningFeeForAbsoluteFeeCheck && 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
@@ -19,30 +20,42 @@ 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
)
}

@Test
fun `policy rejection mining fee check`() {
val policy = LiquidityPolicy.Auto(maxAbsoluteFee = 1_000.sat, maxRelativeFeeBasisPoints = 5_000 /* 3000 = 30 % */, skipAbsoluteFeeCheck = false, inboundLiquidityTarget = null, maxAllowedFeeCredit = 0.msat, considerOnlyMiningFeeForAbsoluteFeeCheck = true)
// total fee is over absolute, but mining fee is below and we only consider the mining fee
assertNull(policy.maybeReject(amount = 10_000_000.msat, fee = ChannelManagementFees(miningFee = 900.sat, serviceFee = 2_000.sat), source = LiquidityEvents.Source.OnChainWallet, logger))
// the mining fee is over absolute
assertEquals(
expected = LiquidityEvents.Rejected.Reason.TooExpensive.OverAbsoluteFee(policy.maxAbsoluteFee),
actual = policy.maybeReject(amount = 10_000_000.msat, fee = ChannelManagementFees(miningFee = 2_000.sat, serviceFee = 0.sat), source = LiquidityEvents.Source.OnChainWallet, logger)?.reason
)
}
}