Skip to content

Commit

Permalink
Add method to calculate input for a trampoline fee
Browse files Browse the repository at this point in the history
This method returns the amount that needs to be provided to
the trampoline payment handler in order to entirely consume
a given amount, without leaving any dust.

This allows the node to send all their balance over LN.
  • Loading branch information
dpad85 committed Jan 30, 2024
1 parent 55aa961 commit 8335019
Show file tree
Hide file tree
Showing 2 changed files with 88 additions and 0 deletions.
30 changes: 30 additions & 0 deletions src/commonMain/kotlin/fr/acinq/lightning/NodeParams.kt
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ import fr.acinq.lightning.Lightning.nodeFee
import fr.acinq.lightning.blockchain.fee.FeerateTolerance
import fr.acinq.lightning.blockchain.fee.OnChainFeeConf
import fr.acinq.lightning.crypto.KeyManager
import fr.acinq.lightning.io.SendPayment
import fr.acinq.lightning.payment.LiquidityPolicy
import fr.acinq.lightning.utils.msat
import fr.acinq.lightning.utils.sat
Expand All @@ -25,7 +26,36 @@ data class NodeUri(val id: PublicKey, val host: String, val port: Int)
* This class encapsulates the fees and expiry to use at a particular attempt.
*/
data class TrampolineFees(val feeBase: Satoshi, val feeProportional: Long, val cltvExpiryDelta: CltvExpiryDelta) {
/**
* Calculates the trampoline fee for [recipientAmount].
*
* @param recipientAmount the amount that will be received by the recipient.
*/
fun calculateFees(recipientAmount: MilliSatoshi): MilliSatoshi = nodeFee(feeBase.toMilliSatoshi(), feeProportional, recipientAmount)

/**
* Calculates the amount that must be provided to the trampoline payload to pay the trampoline
* fee and consume all the [finalAmount], leaving no dust.
*
* E.g.: balance is 1_000 sat, trampoline fees is 4 sat + 0.4%. To use up all balance, [SendPayment]
* should use an amount of 992_032 msat (fee=7_968 msat, around 4 sat + 0.4%).
*
* @param finalAmount what leaves the node, including fees.
* @return the amount to provide to the trampoline handler, null if there is no valid amount.
*/
fun calculateReverseAmount(finalAmount: MilliSatoshi): MilliSatoshi? {
val amountBeforeBaseFee = finalAmount - this.feeBase.toMilliSatoshi()
val proportionalFee = this.feeProportional.toDouble() / 1_000_000

// first, we approximate the amount that must be used in the trampoline payload
val amountBeforeFee = amountBeforeBaseFee.msat / (1 + proportionalFee)

// we cannot return the amount above directly: `OutgoingPaymentHandler` uses `calculateFees` to
// create the trampoline payload, which involves rounding, so the amount leaving the wallet may
// be off by 1 msat. So we reverse what the handler does to find the exact amount.
val fee = calculateFees(amountBeforeFee.toLong().msat)
return (finalAmount - fee).takeUnless { it < 0.msat }
}
}

/**
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,58 @@
package fr.acinq.lightning

import fr.acinq.lightning.utils.msat
import fr.acinq.lightning.utils.sat
import kotlin.test.Test
import kotlin.test.assertEquals

class TrampolineFeesTestsCommon {

@Test
fun `calculate fees`() {
val trampolineFees = TrampolineFees(
feeBase = 4.sat,
feeProportional = 4_000,
cltvExpiryDelta = CltvExpiryDelta(576)
)

assertEquals(4_000.msat, trampolineFees.calculateFees(0.msat))
assertEquals(4_000.msat, trampolineFees.calculateFees(1.msat))
assertEquals(4_004.msat, trampolineFees.calculateFees(1_001.msat))
assertEquals(8_000.msat, trampolineFees.calculateFees(1_000_000.msat))
assertEquals(8_003.msat, trampolineFees.calculateFees(1_000_789.msat))
assertEquals(497_827.msat, trampolineFees.calculateFees(123_456_789.msat))
}

@Test
fun `calculate reverse amount`() {
val trampolineFees = TrampolineFees(
feeBase = 4.sat,
feeProportional = 4_000,
cltvExpiryDelta = CltvExpiryDelta(576)
)

// amount available in the wallet -> amount that should be provided in the trampoline payload
val testCases = listOf(
0.msat to null,
1.msat to null,
1_000.msat to null,
4_000.msat to 0.msat,
4_001.msat to 1.msat,
4_004.msat to 4.msat,
8_000.msat to 3_985.msat,
8_016.msat to 4_000.msat,
1_000_000.msat to 992_032.msat,
100_000_000.msat to 99_597_610.msat,
123_456_789.msat to 122_960_946.msat
)

testCases.forEach { (availableAmount, expectedTrampolineAmount) ->
val trampolineAmount = trampolineFees.calculateReverseAmount(availableAmount)
println("$availableAmount: expected=$expectedTrampolineAmount actual=$trampolineAmount")
assertEquals(expectedTrampolineAmount, trampolineAmount)
if (trampolineAmount != null) {
assertEquals(availableAmount, trampolineFees.calculateFees(trampolineAmount) + trampolineAmount)
}
}
}
}

0 comments on commit 8335019

Please sign in to comment.