Skip to content

Commit bbc18be

Browse files
dpad85pm47
andauthored
Support for new on-the-fly funding (#113)
Adds support for liquidity-ads based protocol for on-the-fly liquidity as specified in lightning/blips#36 and lightning/blips#41, implemented respectively in ACINQ/lightning-kmp#649 and ACINQ/lightning-kmp#660. ### Lightning-kmp update Phoenixd now uses the main branch of `lightning-kmp` (v1.8.0). ### Database update - `LiquidityAds.Lease` is replaced by `LiquidityAds.Purchase`, so we need to update the liquidity table. - the `receivedWith` data have been updated in lightning-kmp, and we need a new `Part.Htlc.V1` object that may contain a `LiquidityAds.FundingFee`. With the `Lease->Purchase` change, we've updated our pattern for versioning database objects. We now have `asDb()` & `asCanonical()` mapping methods and store the type of the db object inside the json (which means we don't need the `type` column anymore, except for convenience). --------- Co-authored-by: pm47 <[email protected]>
1 parent 9414518 commit bbc18be

File tree

19 files changed

+449
-214
lines changed

19 files changed

+449
-214
lines changed

buildSrc/src/main/kotlin/Versions.kt

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
object Versions {
22
val kotlin = "1.9.23"
3-
val lightningKmp = "1.7.3-FEECREDIT-11"
3+
val lightningKmp = "1.8.0"
44
val sqlDelight = "2.0.1"
55
val okio = "3.8.0"
66
val clikt = "4.2.2"

src/commonMain/kotlin/fr/acinq/lightning/bin/Api.kt

Lines changed: 12 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -27,6 +27,7 @@ import fr.acinq.lightning.bin.payments.lnurl.models.LnurlWithdraw
2727
import fr.acinq.lightning.blockchain.fee.FeeratePerByte
2828
import fr.acinq.lightning.blockchain.fee.FeeratePerKw
2929
import fr.acinq.lightning.channel.ChannelCommand
30+
import fr.acinq.lightning.channel.ChannelFundingResponse
3031
import fr.acinq.lightning.channel.states.ChannelStateWithCommitments
3132
import fr.acinq.lightning.channel.states.Closed
3233
import fr.acinq.lightning.channel.states.Closing
@@ -162,13 +163,19 @@ class Api(
162163
.filterNot { it is Closing || it is Closed }
163164
.map { it.commitments.active.first().availableBalanceForSend(it.commitments.params, it.commitments.changes) }
164165
.sum().truncateToSatoshi()
165-
call.respond(Balance(balance, nodeParams.feeCredit.value))
166+
call.respond(Balance(balance, peer.feeCreditFlow.value.truncateToSatoshi()))
166167
}
167168
get("estimateliquidityfees") {
168169
val amount = call.parameters.getLong("amountSat").sat
169170
val feerate = peer.onChainFeeratesFlow.filterNotNull().first().fundingFeerate
170-
val liquidityFees = LSP.liquidityFees(amount, feerate, isNew = peer.channels.isEmpty())
171-
call.respond(LiquidityFees(liquidityFees))
171+
val fundingRates = peer.remoteFundingRates.filterNotNull().first()
172+
when (val fundingRate = fundingRates.findRate(amount)) {
173+
null -> badRequest("no available funding rates for amount=$amount")
174+
else -> {
175+
val liquidityFees = fundingRate.fees(feerate, amount, amount, isChannelCreation = peer.channels.isEmpty())
176+
call.respond(LiquidityFees(liquidityFees))
177+
}
178+
}
172179
}
173180
get("listchannels") {
174181
call.respond(peer.channels.values.toList())
@@ -388,8 +395,8 @@ class Api(
388395
}.toEither()
389396
when (res) {
390397
is Either.Right -> when (val r = res.value) {
391-
is ChannelCommand.Commitment.Splice.Response.Created -> call.respondText(r.fundingTxId.toString())
392-
is ChannelCommand.Commitment.Splice.Response.Failure -> call.respondText(r.toString())
398+
is ChannelFundingResponse.Success -> call.respondText(r.fundingTxId.toString())
399+
is ChannelFundingResponse.Failure -> call.respondText(r.toString())
393400
else -> call.respondText("no channel available")
394401
}
395402
is Either.Left -> call.respondText(res.value.message.toString())

src/commonMain/kotlin/fr/acinq/lightning/bin/Main.kt

Lines changed: 51 additions & 39 deletions
Original file line numberDiff line numberDiff line change
@@ -40,17 +40,13 @@ import fr.acinq.lightning.bin.logs.stringTimestamp
4040
import fr.acinq.lightning.blockchain.mempool.MempoolSpaceClient
4141
import fr.acinq.lightning.blockchain.mempool.MempoolSpaceWatcher
4242
import fr.acinq.lightning.crypto.LocalKeyManager
43-
import fr.acinq.lightning.db.ChannelsDb
44-
import fr.acinq.lightning.db.Databases
45-
import fr.acinq.lightning.db.PaymentsDb
43+
import fr.acinq.lightning.db.*
4644
import fr.acinq.lightning.io.Peer
4745
import fr.acinq.lightning.io.TcpSocket
4846
import fr.acinq.lightning.logging.LoggerFactory
4947
import fr.acinq.lightning.payment.LiquidityPolicy
50-
import fr.acinq.lightning.utils.Connection
51-
import fr.acinq.lightning.utils.msat
52-
import fr.acinq.lightning.utils.sat
53-
import fr.acinq.lightning.utils.toByteVector
48+
import fr.acinq.lightning.utils.*
49+
import fr.acinq.lightning.wire.LiquidityAds
5450
import fr.acinq.phoenix.db.*
5551
import io.ktor.http.*
5652
import io.ktor.server.application.*
@@ -90,14 +86,14 @@ class Phoenixd : CliktCommand() {
9086
}
9187
private val agreeToTermsOfService by option("--agree-to-terms-of-service", hidden = true, help = "Agree to terms of service").flag()
9288
private val chain by option("--chain", help = "Bitcoin chain to use").choice(
93-
"mainnet" to Chain.Mainnet, "testnet" to Chain.Testnet
89+
"mainnet" to Chain.Mainnet, "testnet" to Chain.Testnet3
9490
).default(Chain.Mainnet, defaultForHelp = "mainnet")
9591
private val mempoolSpaceUrl by option("--mempool-space-url", help = "Custom mempool.space instance")
9692
.convert { Url(it) }
9793
.defaultLazy {
9894
when (chain) {
9995
Chain.Mainnet -> MempoolSpaceClient.OfficialMempoolMainnet
100-
Chain.Testnet -> MempoolSpaceClient.OfficialMempoolTestnet
96+
Chain.Testnet3 -> MempoolSpaceClient.OfficialMempoolTestnet
10197
else -> error("unsupported chain")
10298
}
10399
}
@@ -155,7 +151,7 @@ class Phoenixd : CliktCommand() {
155151
"off" to 0.sat,
156152
"50k" to 50_000.sat,
157153
"100k" to 100_000.sat,
158-
).default(100_000.sat, "100k")
154+
).convert { it.toMilliSatoshi() }.default(100_000.sat.toMilliSatoshi(), "100k")
159155
private val maxRelativeFeePct by option("--max-relative-fee-percent", help = "Max relative fee for on-chain operations in percent.", hidden = true)
160156
.int()
161157
.restrictTo(1..50)
@@ -244,10 +240,11 @@ class Phoenixd : CliktCommand() {
244240
)
245241
val lsp = LSP.from(chain)
246242
val liquidityPolicy = LiquidityPolicy.Auto(
247-
maxMiningFee = liquidityOptions.maxMiningFee,
243+
inboundLiquidityTarget = liquidityOptions.autoLiquidity,
244+
maxAbsoluteFee = liquidityOptions.maxMiningFee,
248245
maxRelativeFeeBasisPoints = liquidityOptions.maxRelativeFeeBasisPoints,
249-
skipMiningFeeCheck = false,
250-
maxAllowedCredit = liquidityOptions.maxFeeCredit
246+
skipAbsoluteFeeCheck = false,
247+
maxAllowedFeeCredit = liquidityOptions.maxFeeCredit
251248
)
252249
val keyManager = LocalKeyManager(seed.seed, chain, lsp.swapInXpub)
253250
val nodeParams = NodeParams(chain, loggerFactory, keyManager)
@@ -276,9 +273,6 @@ class Phoenixd : CliktCommand() {
276273
channel_close_outgoing_paymentsAdapter = Channel_close_outgoing_payments.Adapter(
277274
closing_info_typeAdapter = EnumColumnAdapter()
278275
),
279-
inbound_liquidity_outgoing_paymentsAdapter = Inbound_liquidity_outgoing_payments.Adapter(
280-
lease_typeAdapter = EnumColumnAdapter()
281-
)
282276
)
283277
val channelsDb = SqliteChannelsDb(driver, database)
284278
val paymentsDb = SqlitePaymentsDb(database)
@@ -324,39 +318,59 @@ class Phoenixd : CliktCommand() {
324318
}
325319
launch {
326320
nodeParams.nodeEvents
327-
.filterIsInstance<PaymentEvents.PaymentReceived>()
328-
.filter { it.amount > 0.msat }
321+
.filterIsInstance<PaymentEvents>()
329322
.collect {
330-
consoleLog("received lightning payment: ${it.amount.truncateToSatoshi()} (${it.receivedWith.joinToString { part -> part::class.simpleName.toString().lowercase() }})")
323+
when (it) {
324+
is PaymentEvents.PaymentReceived -> {
325+
val fee = it.receivedWith.filterIsInstance<IncomingPayment.ReceivedWith.LightningPayment>().map { it.fundingFee?.amount ?: 0.msat }.sum().truncateToSatoshi()
326+
val type = it.receivedWith.joinToString { part -> part::class.simpleName.toString().lowercase() }
327+
consoleLog("received lightning payment: ${it.amount.truncateToSatoshi()} ($type${if (fee > 0.sat) " fee=$fee" else ""})")
328+
}
329+
is PaymentEvents.PaymentSent ->
330+
when (val payment = it.payment) {
331+
is InboundLiquidityOutgoingPayment -> {
332+
val totalFee = payment.fees.truncateToSatoshi()
333+
val feePaidFromBalance = payment.feePaidFromChannelBalance.total
334+
val feePaidFromFeeCredit = payment.feeCreditUsed.truncateToSatoshi()
335+
val feeRemaining = totalFee - feePaidFromBalance - feePaidFromFeeCredit
336+
val purchaseType = payment.purchase.paymentDetails.paymentType::class.simpleName.toString().lowercase()
337+
consoleLog("purchased inbound liquidity: ${payment.purchase.amount} (totalFee=$totalFee feePaidFromBalance=$feePaidFromBalance feePaidFromFeeCredit=$feePaidFromFeeCredit feeRemaining=$feeRemaining purchaseType=$purchaseType)")
338+
}
339+
else -> {}
340+
}
341+
}
331342
}
332343
}
333344
launch {
334345
nodeParams.nodeEvents
335-
.filterIsInstance<LiquidityEvents.Decision.Rejected>()
346+
.filterIsInstance<LiquidityEvents.Rejected>()
336347
.collect {
337348
when (val reason = it.reason) {
338-
is LiquidityEvents.Decision.Rejected.Reason.OverMaxCredit -> {
339-
consoleLog(yellow("lightning payment rejected (amount=${it.amount.truncateToSatoshi()}): over max fee credit (max=${reason.maxAllowedCredit})"))
340-
}
341-
is LiquidityEvents.Decision.Rejected.Reason.TooExpensive.OverMaxMiningFee -> {
342-
consoleLog(yellow("lightning payment rejected (amount=${it.amount.truncateToSatoshi()}): over max mining fee (max=${reason.maxMiningFee})"))
343-
}
344-
is LiquidityEvents.Decision.Rejected.Reason.TooExpensive.OverRelativeFee -> {
345-
consoleLog(yellow("lightning payment rejected (amount=${it.amount.truncateToSatoshi()}): fee=${it.fee.truncateToSatoshi()} more than ${reason.maxRelativeFeeBasisPoints.toDouble() / 100}% of amount"))
346-
}
347-
LiquidityEvents.Decision.Rejected.Reason.ChannelInitializing -> {
348-
consoleLog(yellow("channels are initializing"))
349-
}
350-
LiquidityEvents.Decision.Rejected.Reason.PolicySetToDisabled -> {
349+
// TODO: put this back after rework of LiquidityPolicy to handle fee credit
350+
// is LiquidityEvents.Rejected.Reason.OverMaxCredit -> {
351+
// consoleLog(yellow("lightning payment rejected (amount=${it.amount.truncateToSatoshi()}): over max fee credit (max=${reason.maxAllowedCredit})"))
352+
// }
353+
is LiquidityEvents.Rejected.Reason.TooExpensive.OverAbsoluteFee ->
354+
consoleLog(yellow("lightning payment rejected (amount=${it.amount.truncateToSatoshi()}): over absolute fee (fee=${it.fee.truncateToSatoshi()} max=${reason.maxAbsoluteFee})"))
355+
is LiquidityEvents.Rejected.Reason.TooExpensive.OverRelativeFee ->
356+
consoleLog(yellow("lightning payment rejected (amount=${it.amount.truncateToSatoshi()}): over relative fee (fee=${it.fee.truncateToSatoshi()} max=${reason.maxRelativeFeeBasisPoints.toDouble() / 100}%)"))
357+
LiquidityEvents.Rejected.Reason.PolicySetToDisabled ->
351358
consoleLog(yellow("automated liquidity is disabled"))
352-
}
359+
LiquidityEvents.Rejected.Reason.ChannelFundingInProgress ->
360+
consoleLog(yellow("channel operation is in progress"))
361+
is LiquidityEvents.Rejected.Reason.MissingOffChainAmountTooLow ->
362+
consoleLog(yellow("missing offchain amount is too low (missingOffChainAmount=${reason.missingOffChainAmount} currentFeeCredit=${reason.currentFeeCredit}"))
363+
LiquidityEvents.Rejected.Reason.NoMatchingFundingRate ->
364+
consoleLog(yellow("no matching funding rates"))
365+
is LiquidityEvents.Rejected.Reason.TooManyParts ->
366+
consoleLog(yellow("too many payment parts"))
353367
}
354368
}
355369
}
356370
launch {
357-
nodeParams.feeCredit
358-
.drop(1) // we drop the initial value which is 0 sat
359-
.collect { feeCredit -> consoleLog("fee credit: $feeCredit") }
371+
peer.feeCreditFlow
372+
.drop(1) // we drop the initial value which is 0 msat
373+
.collect { feeCredit -> consoleLog("fee credit: ${feeCredit.truncateToSatoshi()}") }
360374
}
361375
}
362376

@@ -370,8 +384,6 @@ class Phoenixd : CliktCommand() {
370384

371385
runBlocking {
372386
peer.connectionState.first { it == Connection.ESTABLISHED }
373-
peer.registerFcmToken("super-${randomBytes32().toHex()}")
374-
peer.setAutoLiquidityParams(liquidityOptions.autoLiquidity)
375387
}
376388

377389
val server = embeddedServer(CIO, port = httpBindPort, host = httpBindIp,

src/commonMain/kotlin/fr/acinq/lightning/bin/conf/Lsp.kt

Lines changed: 1 addition & 34 deletions
Original file line numberDiff line numberDiff line change
@@ -2,12 +2,9 @@ package fr.acinq.lightning.bin.conf
22

33
import fr.acinq.bitcoin.Chain
44
import fr.acinq.bitcoin.PublicKey
5-
import fr.acinq.bitcoin.Satoshi
65
import fr.acinq.lightning.*
7-
import fr.acinq.lightning.blockchain.fee.FeeratePerKw
86
import fr.acinq.lightning.utils.msat
97
import fr.acinq.lightning.utils.sat
10-
import fr.acinq.lightning.wire.LiquidityAds
118

129

1310
data class LSP(val walletParams: WalletParams, val swapInXpub: String) {
@@ -44,7 +41,7 @@ data class LSP(val walletParams: WalletParams, val swapInXpub: String) {
4441
swapInParams
4542
)
4643
)
47-
is Chain.Testnet -> LSP(
44+
is Chain.Testnet3 -> LSP(
4845
swapInXpub = "tpubDAmCFB21J9ExKBRPDcVxSvGs9jtcf8U1wWWbS1xTYmnUsuUHPCoFdCnEGxLE3THSWcQE48GHJnyz8XPbYUivBMbLSMBifFd3G9KmafkM9og",
4946
walletParams = WalletParams(
5047
trampolineNode = NodeUri(PublicKey.fromHex("03933884aaf1d6b108397e5efe5c86bcf2d8ca8d2f700eda99db9214fc2712b134"), "13.248.222.197", 9735),
@@ -55,35 +52,5 @@ data class LSP(val walletParams: WalletParams, val swapInXpub: String) {
5552
)
5653
else -> error("unsupported chain $chain")
5754
}
58-
59-
fun liquidityFees(amount: Satoshi, feerate: FeeratePerKw, isNew: Boolean): LiquidityAds.LeaseFees {
60-
val creationFee = if (isNew) 1_000.sat else 0.sat
61-
val leaseRate = liquidityLeaseRate(amount)
62-
val leaseFees = leaseRate.fees(feerate, requestedAmount = amount, contributedAmount = amount)
63-
return leaseFees.copy(serviceFee = creationFee + leaseFees.serviceFee)
64-
}
65-
66-
private fun liquidityLeaseRate(amount: Satoshi): LiquidityAds.LeaseRate {
67-
// WARNING : THIS MUST BE KEPT IN SYNC WITH LSP OTHERWISE FUNDING REQUEST WILL BE REJECTED BY PHOENIX
68-
val fundingWeight = if (amount <= 100_000.sat) {
69-
271 * 2 // 2-inputs (wpkh) / 0-change
70-
} else if (amount <= 250_000.sat) {
71-
271 * 2 // 2-inputs (wpkh) / 0-change
72-
} else if (amount <= 500_000.sat) {
73-
271 * 4 // 4-inputs (wpkh) / 0-change
74-
} else if (amount <= 1_000_000.sat) {
75-
271 * 4 // 4-inputs (wpkh) / 0-change
76-
} else {
77-
271 * 6 // 6-inputs (wpkh) / 0-change
78-
}
79-
return LiquidityAds.LeaseRate(
80-
leaseDuration = 0,
81-
fundingWeight = fundingWeight,
82-
leaseFeeProportional = 100, // 1%
83-
leaseFeeBase = 0.sat,
84-
maxRelayFeeProportional = 100,
85-
maxRelayFeeBase = 1_000.msat
86-
)
87-
}
8855
}
8956
}

src/commonMain/kotlin/fr/acinq/lightning/bin/db/SqlitePaymentsDb.kt

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -98,6 +98,12 @@ class SqlitePaymentsDb(val database: PhoenixDatabase) : PaymentsDb {
9898
}
9999
}
100100

101+
override suspend fun getInboundLiquidityPurchase(fundingTxId: TxId): InboundLiquidityOutgoingPayment? {
102+
return withContext(Dispatchers.Default) {
103+
inboundLiquidityQueries.getByTxId(fundingTxId)
104+
}
105+
}
106+
101107
override suspend fun completeOutgoingPaymentOffchain(
102108
id: UUID,
103109
finalFailure: FinalFailure,

src/commonMain/kotlin/fr/acinq/lightning/bin/db/payments/DbTypesHelper.kt

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -29,7 +29,9 @@ object DbTypesHelper {
2929

3030
val module = SerializersModule {
3131
polymorphic(IncomingReceivedWithData.Part::class) {
32+
@Suppress("DEPRECATION")
3233
subclass(IncomingReceivedWithData.Part.Htlc.V0::class)
34+
subclass(IncomingReceivedWithData.Part.Htlc.V1::class)
3335
subclass(IncomingReceivedWithData.Part.NewChannel.V2::class)
3436
subclass(IncomingReceivedWithData.Part.SpliceIn.V0::class)
3537
subclass(IncomingReceivedWithData.Part.FeeCredit.V0::class)

0 commit comments

Comments
 (0)