Skip to content

Commit

Permalink
Use a list of liquidity ads rates
Browse files Browse the repository at this point in the history
The liquidity seller may adapt their lease rates to the funding amount,
to account for the fact that higher amounts will require more inputs,
and thus a larger `funding_weight`.
  • Loading branch information
t-bast committed Mar 25, 2024
1 parent c7f4353 commit 1b0f6ec
Show file tree
Hide file tree
Showing 7 changed files with 31 additions and 10 deletions.
10 changes: 6 additions & 4 deletions src/commonMain/kotlin/fr/acinq/lightning/io/Peer.kt
Original file line number Diff line number Diff line change
Expand Up @@ -110,7 +110,7 @@ data class PhoenixAndroidLegacyInfoEvent(val info: PhoenixAndroidLegacyInfo) : P
* @param walletParams High level parameters for our node. It especially contains the Peer's [NodeUri].
* @param watcher Watches events from the Electrum client and publishes transactions and events.
* @param db Wraps the various databases persisting the channels and payments data related to the Peer.
* @param leaseRate Rate at which our peer sells their liquidity.
* @param leaseRates Rates at which our peer sells their liquidity.
* @param socketBuilder Builds the TCP socket used to connect to the Peer.
* @param initTlvStream Optional stream of TLV for the [Init] message we send to this Peer after connection. Empty by default.
*/
Expand All @@ -120,7 +120,7 @@ class Peer(
val walletParams: WalletParams,
val watcher: ElectrumWatcher,
val db: Databases,
val leaseRate: LiquidityAds.LeaseRate,
val leaseRates: List<LiquidityAds.BoundedLeaseRate>,
socketBuilder: TcpSocket.Builder?,
scope: CoroutineScope,
private val initTlvStream: TlvStream<InitTlv> = TlvStream.empty()
Expand Down Expand Up @@ -167,7 +167,7 @@ class Peer(
val eventsFlow: SharedFlow<PeerEvent> get() = _eventsFlow.asSharedFlow()

// encapsulates logic for validating incoming payments
private val incomingPaymentHandler = IncomingPaymentHandler(nodeParams, db.payments, leaseRate)
private val incomingPaymentHandler = IncomingPaymentHandler(nodeParams, db.payments, leaseRates)

// encapsulates logic for sending payments
private val outgoingPaymentHandler = OutgoingPaymentHandler(nodeParams, walletParams, db.payments)
Expand Down Expand Up @@ -1151,14 +1151,15 @@ class Peer(
is LiquidityPolicy.Disable -> LiquidityPolicy.minInboundLiquidityTarget // we don't disable creating a channel using our own wallet inputs
is LiquidityPolicy.Auto -> policy.inboundLiquidityTarget ?: LiquidityPolicy.minInboundLiquidityTarget
}
val leaseRate = LiquidityAds.chooseLeaseRate(inboundLiquidityTarget, leaseRates)
LiquidityAds.RequestRemoteFunding(inboundLiquidityTarget, currentTipFlow.filterNotNull().first().first, leaseRate)
}
val (localFundingAmount, fees) = run {
val dummyFundingScript = Helpers.Funding.makeFundingPubKeyScript(Transactions.PlaceHolderPubKey, Transactions.PlaceHolderPubKey)
val localMiningFee = Transactions.weight2fee(currentFeerates.fundingFeerate, FundingContributions.computeWeightPaid(isInitiator = true, null, dummyFundingScript, cmd.walletInputs, emptyList()))
// We directly pay the on-chain fees for our inputs/outputs of the transaction.
val localFundingAmount = cmd.totalAmount - localMiningFee
val leaseFees = leaseRate.fees(currentFeerates.fundingFeerate, requestRemoteFunding.fundingAmount, requestRemoteFunding.fundingAmount)
val leaseFees = requestRemoteFunding.rate.fees(currentFeerates.fundingFeerate, requestRemoteFunding.fundingAmount, requestRemoteFunding.fundingAmount)
// We also refund the liquidity provider for some of the on-chain fees they will pay for their inputs/outputs of the transaction.
val totalFees = TransactionFees(miningFee = localMiningFee + leaseFees.miningFee, serviceFee = leaseFees.serviceFee)
Pair(localFundingAmount, totalFees)
Expand Down Expand Up @@ -1220,6 +1221,7 @@ class Peer(
is LiquidityPolicy.Disable -> LiquidityPolicy.minInboundLiquidityTarget
is LiquidityPolicy.Auto -> policy.inboundLiquidityTarget ?: LiquidityPolicy.minInboundLiquidityTarget
}
val leaseRate = LiquidityAds.chooseLeaseRate(remoteFundingAmount, leaseRates)
val requestRemoteFunding = LiquidityAds.RequestRemoteFunding(remoteFundingAmount, currentTipFlow.filterNotNull().first().first, leaseRate)
when {
channelFundingIsInProgress() -> {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -45,7 +45,7 @@ data class OnTheFlyFundingPart(val htlc: MaybeAddHtlc, override val finalPayload
override fun toString(): String = "maybe-htlc(amount=${htlc.amount})"
}

class IncomingPaymentHandler(val nodeParams: NodeParams, val db: IncomingPaymentsDb, val leaseRate: LiquidityAds.LeaseRate) {
class IncomingPaymentHandler(val nodeParams: NodeParams, val db: IncomingPaymentsDb, val leaseRates: List<LiquidityAds.BoundedLeaseRate>) {

sealed class ProcessAddResult {
abstract val actions: List<PeerCommand>
Expand Down Expand Up @@ -255,6 +255,7 @@ class IncomingPaymentHandler(val nodeParams: NodeParams, val db: IncomingPayment
is LiquidityPolicy.Disable -> 0.msat
is LiquidityPolicy.Auto -> {
val requestedAmount = policy.inboundLiquidityTarget ?: LiquidityPolicy.minInboundLiquidityTarget
val leaseRate = LiquidityAds.chooseLeaseRate(requestedAmount, leaseRates)
leaseRate.fees(currentFeerate, requestedAmount, requestedAmount).total.toMilliSatoshi()
}
}
Expand Down
16 changes: 16 additions & 0 deletions src/commonMain/kotlin/fr/acinq/lightning/wire/LiquidityAds.kt
Original file line number Diff line number Diff line change
Expand Up @@ -82,6 +82,22 @@ object LiquidityAds {
}
}

/**
* We may want to use different lease rates based on the amount that is purchased.
* There is an ongoing discussion to directly include this information in the advertised lease rate: https://github.com/lightning/bolts/pull/1145#discussion_r1526005244
*/
data class BoundedLeaseRate(val minAmount: Satoshi, val maxAmount: Satoshi, val leaseRate: LeaseRate)

fun chooseLeaseRate(fundingAmount: Satoshi, rates: List<BoundedLeaseRate>): LeaseRate {
val sortedRates = rates.sortedBy { it.minAmount }
val matchingRate = sortedRates.firstOrNull { it.minAmount <= fundingAmount && fundingAmount <= it.maxAmount }
return when {
matchingRate != null -> matchingRate.leaseRate
fundingAmount <= sortedRates.first().minAmount -> sortedRates.first().leaseRate
else -> sortedRates.last().leaseRate
}
}

/** Request inbound liquidity from a remote peer that supports liquidity ads. */
data class RequestRemoteFunding(val fundingAmount: Satoshi, val leaseStart: Int, val rate: LeaseRate) {
private val leaseExpiry: Int = leaseStart + rate.leaseDuration
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -706,7 +706,7 @@ class IncomingPaymentHandlerTestsCommon : LightningTestSuite() {

@Test
fun `invoice expired`() = runSuspendTest {
val paymentHandler = IncomingPaymentHandler(TestConstants.Bob.nodeParams, InMemoryPaymentsDb(), TestConstants.leaseRate)
val paymentHandler = IncomingPaymentHandler(TestConstants.Bob.nodeParams, InMemoryPaymentsDb(), TestConstants.leaseRates)
val (incomingPayment, paymentSecret) = makeIncomingPayment(
payee = paymentHandler,
amount = defaultAmount,
Expand Down Expand Up @@ -1038,7 +1038,7 @@ class IncomingPaymentHandlerTestsCommon : LightningTestSuite() {

@Test
fun `purge expired incoming payments`() = runSuspendTest {
val paymentHandler = IncomingPaymentHandler(TestConstants.Bob.nodeParams, InMemoryPaymentsDb(), TestConstants.leaseRate)
val paymentHandler = IncomingPaymentHandler(TestConstants.Bob.nodeParams, InMemoryPaymentsDb(), TestConstants.leaseRates)

// create incoming payment that has expired and not been paid
val expiredInvoice = paymentHandler.createInvoice(
Expand Down Expand Up @@ -1150,7 +1150,7 @@ class IncomingPaymentHandlerTestsCommon : LightningTestSuite() {
}

private suspend fun createFixture(invoiceAmount: MilliSatoshi?): Triple<IncomingPaymentHandler, IncomingPayment, ByteVector32> {
val paymentHandler = IncomingPaymentHandler(TestConstants.Bob.nodeParams, InMemoryPaymentsDb(), TestConstants.leaseRate)
val paymentHandler = IncomingPaymentHandler(TestConstants.Bob.nodeParams, InMemoryPaymentsDb(), TestConstants.leaseRates)
// We use a liquidity policy that accepts payment values used by default in this test file.
paymentHandler.nodeParams.liquidityPolicy.emit(LiquidityPolicy.Auto(inboundLiquidityTarget = null, maxAbsoluteFee = 5_000.sat, maxRelativeFeeBasisPoints = 500, skipAbsoluteFeeCheck = false))
val (incomingPayment, paymentSecret) = makeIncomingPayment(paymentHandler, invoiceAmount)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -454,7 +454,7 @@ class OutgoingPaymentHandlerTestsCommon : LightningTestSuite() {
val outgoingPaymentHandler = OutgoingPaymentHandler(TestConstants.Alice.nodeParams, defaultWalletParams, InMemoryPaymentsDb())
// The invoice comes from Bob, our direct peer (and trampoline node).
val preimage = randomBytes32()
val incomingPaymentHandler = IncomingPaymentHandler(TestConstants.Bob.nodeParams, InMemoryPaymentsDb(), TestConstants.leaseRate)
val incomingPaymentHandler = IncomingPaymentHandler(TestConstants.Bob.nodeParams, InMemoryPaymentsDb(), TestConstants.leaseRates)
val invoice = incomingPaymentHandler.createInvoice(preimage, amount = null, Either.Left("phoenix to phoenix"), listOf())
val payment = SendPayment(UUID.randomUUID(), 300_000.msat, invoice.nodeId, invoice)

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -50,6 +50,8 @@ object TestConstants {
maxRelayFeeBase = 1_000.msat,
)

val leaseRates = listOf(LiquidityAds.BoundedLeaseRate(0.sat, 100_000_000.sat, leaseRate))

const val aliceSwapInServerXpub = "tpubDCvYeHUZisCMVTSfWDa1yevTf89NeF6TWxXUQwqkcmFrNvNdNvZQh1j4m4uTA4QcmPEwcrKVF8bJih1v16zDZacRr4j9MCAFQoSydKKy66q"
const val bobSwapInServerXpub = "tpubDDt5vQap1awkyDXx1z1cP7QFKSZHDCCpbU8nSq9jy7X2grTjUVZDePexf6gc6AHtRRzkgfPW87K6EKUVV6t3Hu2hg7YkHkmMeLSfrP85x41"

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -188,7 +188,7 @@ suspend fun buildPeer(
): Peer {
val electrum = ElectrumClient(scope, nodeParams.loggerFactory)
val watcher = ElectrumWatcher(electrum, scope, nodeParams.loggerFactory)
val peer = Peer(nodeParams, walletParams, watcher, databases, TestConstants.leaseRate, TcpSocket.Builder(), scope)
val peer = Peer(nodeParams, walletParams, watcher, databases, TestConstants.leaseRates, TcpSocket.Builder(), scope)
peer.currentTipFlow.value = currentTip
peer.onChainFeeratesFlow.value = OnChainFeerates(
fundingFeerate = FeeratePerKw(FeeratePerByte(5.sat)),
Expand Down

0 comments on commit 1b0f6ec

Please sign in to comment.