Skip to content

Commit e23a6f5

Browse files
committed
Add support for extensible liquidity ads
The initiator of `open_channel2`, `tx_init_rbf` and `splice_init` can request funding from the remote node. The non-initiator node will: - let the open-channel-interceptor plugin decide whether to provide liquidity for new channels or not, and how much - always honor liquidity requests on existing channels (RBF and splice) when funding rates have been configured Liquidity ads are included in the `node_announcement` message, which lets buyers compare sellers and connect to sellers that provide rates they are comfortable with. They are also included in the `init` message which allows providing different rates to specific peers. This implements lightning/bolts#1153. We currently use the temporary tlv tag 1339 while we're waiting for feedback on the spec proposal.
1 parent 8da0fc6 commit e23a6f5

File tree

58 files changed

+1322
-322
lines changed

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

58 files changed

+1322
-322
lines changed

docs/release-notes/eclair-vnext.md

Lines changed: 10 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,14 +4,23 @@
44

55
## Major changes
66

7+
### Liquidity Ads
8+
9+
This release includes an early prototype for [liquidity ads](https://github.com/lightning/bolts/pull/1153).
10+
Liquidity ads allow nodes to rent their liquidity in a trustless and decentralized manner.
11+
Every node advertizes the rates at which they lease their liquidity, and buyers connect to sellers that offer interesting rates.
12+
13+
The liquidity ads specification is still under review and will likely change.
14+
This feature isn't meant to be used on mainnet yet and is thus disabled by default.
15+
716
### Update minimal version of Bitcoin Core
817

918
With this release, eclair requires using Bitcoin Core 26.1.
1019
Newer versions of Bitcoin Core may be used, but haven't been extensively tested.
1120

1221
### API changes
1322

14-
<insert changes>
23+
- `nodes` allows filtering nodes that offer liquidity ads (#2848)
1524

1625
### Miscellaneous improvements and bug fixes
1726

eclair-core/src/main/resources/reference.conf

Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -304,6 +304,38 @@ eclair {
304304
update-fee-min-diff-ratio = 0.1
305305
}
306306

307+
// Liquidity Ads allow remote nodes to pay us to provide them with inbound liquidity.
308+
liquidity-ads {
309+
// Multiple funding rates can be provided, for different funding amounts.
310+
funding-rates = []
311+
// Sample funding rates:
312+
// funding-rates = [
313+
// {
314+
// min-funding-amount-satoshis = 100000 // minimum funding amount at this rate
315+
// max-funding-amount-satoshis = 500000 // maximum funding amount at this rate
316+
// // The seller can ask the buyer to pay for some of the weight of the funding transaction (for the inputs and
317+
// // outputs added by the seller). This field contains the transaction weight (in vbytes) that the seller asks the
318+
// // buyer to pay for. The default value matches the weight of one p2wpkh input with one p2wpkh change output.
319+
// funding-weight = 400
320+
// fee-base-satoshis = 500 // flat fee that we will receive every time we accept a lease request
321+
// fee-basis-points = 250 // proportional fee based on the amount requested by our peer (2.5%)
322+
// },
323+
// {
324+
// min-funding-amount-satoshis = 500000
325+
// max-funding-amount-satoshis = 5000000
326+
// funding-weight = 750
327+
// fee-base-satoshis = 1000
328+
// fee-basis-points = 200 // 2%
329+
// }
330+
// ]
331+
// Multiple ways of paying the liquidity fees can be provided.
332+
payment-types = [
333+
// Liquidity fees must be paid from the buyer's channel balance during the transaction creation.
334+
// This doesn't involve trust from the buyer or the seller.
335+
"from_channel_balance"
336+
]
337+
}
338+
307339
peer-connection {
308340
auth-timeout = 15 seconds // will disconnect if connection authentication doesn't happen within that timeframe
309341
init-timeout = 15 seconds // will disconnect if initialization doesn't happen within that timeframe

eclair-core/src/main/scala/fr/acinq/eclair/Eclair.scala

Lines changed: 6 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -220,6 +220,7 @@ class EclairImpl(appKit: Kit) extends Eclair with Logging {
220220
pushAmount_opt = pushAmount_opt,
221221
fundingTxFeerate_opt = fundingFeerate_opt.map(FeeratePerKw(_)),
222222
fundingTxFeeBudget_opt = Some(fundingFeeBudget),
223+
requestFunding_opt = None,
223224
channelFlags_opt = announceChannel_opt.map(announceChannel => ChannelFlags(announceChannel = announceChannel)),
224225
timeout_opt = Some(openTimeout))
225226
res <- (appKit.switchboard ? open).mapTo[OpenChannelResponse]
@@ -228,14 +229,15 @@ class EclairImpl(appKit: Kit) extends Eclair with Logging {
228229

229230
override def rbfOpen(channelId: ByteVector32, targetFeerate: FeeratePerKw, fundingFeeBudget: Satoshi, lockTime_opt: Option[Long])(implicit timeout: Timeout): Future[CommandResponse[CMD_BUMP_FUNDING_FEE]] = {
230231
sendToChannelTyped(channel = Left(channelId),
231-
cmdBuilder = CMD_BUMP_FUNDING_FEE(_, targetFeerate, fundingFeeBudget, lockTime_opt.getOrElse(appKit.nodeParams.currentBlockHeight.toLong)))
232+
cmdBuilder = CMD_BUMP_FUNDING_FEE(_, targetFeerate, fundingFeeBudget, lockTime_opt.getOrElse(appKit.nodeParams.currentBlockHeight.toLong), requestFunding_opt = None))
232233
}
233234

234235
override def spliceIn(channelId: ByteVector32, amountIn: Satoshi, pushAmount_opt: Option[MilliSatoshi])(implicit timeout: Timeout): Future[CommandResponse[CMD_SPLICE]] = {
235236
sendToChannelTyped(channel = Left(channelId),
236237
cmdBuilder = CMD_SPLICE(_,
237238
spliceIn_opt = Some(SpliceIn(additionalLocalFunding = amountIn, pushAmount = pushAmount_opt.getOrElse(0.msat))),
238-
spliceOut_opt = None
239+
spliceOut_opt = None,
240+
requestFunding_opt = None,
239241
))
240242
}
241243

@@ -250,7 +252,8 @@ class EclairImpl(appKit: Kit) extends Eclair with Logging {
250252
sendToChannelTyped(channel = Left(channelId),
251253
cmdBuilder = CMD_SPLICE(_,
252254
spliceIn_opt = None,
253-
spliceOut_opt = Some(SpliceOut(amount = amountOut, scriptPubKey = script))
255+
spliceOut_opt = Some(SpliceOut(amount = amountOut, scriptPubKey = script)),
256+
requestFunding_opt = None,
254257
))
255258
}
256259

eclair-core/src/main/scala/fr/acinq/eclair/NodeParams.scala

Lines changed: 29 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -18,7 +18,7 @@ package fr.acinq.eclair
1818

1919
import com.typesafe.config.{Config, ConfigFactory, ConfigValueType}
2020
import fr.acinq.bitcoin.scalacompat.Crypto.PublicKey
21-
import fr.acinq.bitcoin.scalacompat.{Block, BlockHash, Crypto, Satoshi}
21+
import fr.acinq.bitcoin.scalacompat.{Block, BlockHash, Crypto, Satoshi, SatoshiLong}
2222
import fr.acinq.eclair.Setup.Seeds
2323
import fr.acinq.eclair.blockchain.fee._
2424
import fr.acinq.eclair.channel.ChannelFlags
@@ -88,6 +88,7 @@ case class NodeParams(nodeKeyManager: NodeKeyManager,
8888
onionMessageConfig: OnionMessageConfig,
8989
purgeInvoicesInterval: Option[FiniteDuration],
9090
revokedHtlcInfoCleanerConfig: RevokedHtlcInfoCleaner.Config,
91+
willFundRates_opt: Option[LiquidityAds.WillFundRates],
9192
wakeUpTimeout: FiniteDuration) {
9293
val privateKey: Crypto.PrivateKey = nodeKeyManager.nodeKey.privateKey
9394

@@ -477,6 +478,32 @@ object NodeParams extends Logging {
477478
val maxNoChannels = config.getInt("peer-connection.max-no-channels")
478479
require(maxNoChannels > 0, "peer-connection.max-no-channels must be > 0")
479480

481+
val willFundRates_opt = {
482+
val supportedPaymentTypes = Map(
483+
LiquidityAds.PaymentType.FromChannelBalance.rfcName -> LiquidityAds.PaymentType.FromChannelBalance
484+
)
485+
val paymentTypes: Set[LiquidityAds.PaymentType] = config.getStringList("liquidity-ads.payment-types").asScala.map(s => {
486+
supportedPaymentTypes.get(s) match {
487+
case Some(paymentType) => paymentType
488+
case None => throw new IllegalArgumentException(s"unknown liquidity ads payment type: $s")
489+
}
490+
}).toSet
491+
val fundingRates: List[LiquidityAds.FundingLease] = config.getConfigList("liquidity-ads.funding-rates").asScala.map { r =>
492+
LiquidityAds.FundingLease.Basic(
493+
minAmount = r.getLong("min-funding-amount-satoshis").sat,
494+
maxAmount = r.getLong("max-funding-amount-satoshis").sat,
495+
fundingWeight = r.getInt("funding-weight"),
496+
leaseFeeBase = r.getLong("fee-base-satoshis").sat,
497+
leaseFeeProportional = r.getInt("fee-basis-points")
498+
)
499+
}.toList
500+
if (fundingRates.nonEmpty && paymentTypes.nonEmpty) {
501+
Some(LiquidityAds.WillFundRates(fundingRates, paymentTypes))
502+
} else {
503+
None
504+
}
505+
}
506+
480507
NodeParams(
481508
nodeKeyManager = nodeKeyManager,
482509
channelKeyManager = channelKeyManager,
@@ -612,6 +639,7 @@ object NodeParams extends Logging {
612639
batchSize = config.getInt("db.revoked-htlc-info-cleaner.batch-size"),
613640
interval = FiniteDuration(config.getDuration("db.revoked-htlc-info-cleaner.interval").getSeconds, TimeUnit.SECONDS)
614641
),
642+
willFundRates_opt = willFundRates_opt,
615643
wakeUpTimeout = 30 seconds,
616644
)
617645
}

eclair-core/src/main/scala/fr/acinq/eclair/PluginParams.scala

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -22,7 +22,7 @@ import fr.acinq.bitcoin.scalacompat.{ByteVector32, Satoshi}
2222
import fr.acinq.eclair.channel.Origin
2323
import fr.acinq.eclair.io.OpenChannelInterceptor.{DefaultParams, OpenChannelNonInitiator}
2424
import fr.acinq.eclair.payment.relay.PostRestartHtlcCleaner.IncomingHtlc
25-
import fr.acinq.eclair.wire.protocol.Error
25+
import fr.acinq.eclair.wire.protocol.{Error, LiquidityAds}
2626

2727
/** Custom plugin parameters. */
2828
trait PluginParams {
@@ -67,7 +67,7 @@ case class InterceptOpenChannelReceived(replyTo: ActorRef[InterceptOpenChannelRe
6767
}
6868

6969
sealed trait InterceptOpenChannelResponse
70-
case class AcceptOpenChannel(temporaryChannelId: ByteVector32, defaultParams: DefaultParams, localFundingAmount_opt: Option[Satoshi]) extends InterceptOpenChannelResponse
70+
case class AcceptOpenChannel(temporaryChannelId: ByteVector32, defaultParams: DefaultParams, addFunding_opt: Option[LiquidityAds.AddFunding]) extends InterceptOpenChannelResponse
7171
case class RejectOpenChannel(temporaryChannelId: ByteVector32, error: Error) extends InterceptOpenChannelResponse
7272
// @formatter:on
7373

eclair-core/src/main/scala/fr/acinq/eclair/channel/ChannelData.scala

Lines changed: 5 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -27,7 +27,7 @@ import fr.acinq.eclair.io.Peer
2727
import fr.acinq.eclair.payment.OutgoingPaymentPacket.Upstream
2828
import fr.acinq.eclair.transactions.CommitmentSpec
2929
import fr.acinq.eclair.transactions.Transactions._
30-
import fr.acinq.eclair.wire.protocol.{ChannelAnnouncement, ChannelReady, ChannelReestablish, ChannelUpdate, ClosingSigned, CommitSig, FailureMessage, FundingCreated, FundingSigned, Init, OnionRoutingPacket, OpenChannel, OpenDualFundedChannel, Shutdown, SpliceInit, Stfu, TxSignatures, UpdateAddHtlc, UpdateFailHtlc, UpdateFailMalformedHtlc, UpdateFulfillHtlc}
30+
import fr.acinq.eclair.wire.protocol.{ChannelAnnouncement, ChannelReady, ChannelReestablish, ChannelUpdate, ClosingSigned, CommitSig, FailureMessage, FundingCreated, FundingSigned, Init, LiquidityAds, OnionRoutingPacket, OpenChannel, OpenDualFundedChannel, Shutdown, SpliceInit, Stfu, TxSignatures, UpdateAddHtlc, UpdateFailHtlc, UpdateFailMalformedHtlc, UpdateFulfillHtlc}
3131
import fr.acinq.eclair.{Alias, BlockHeight, CltvExpiry, CltvExpiryDelta, Features, InitFeature, MilliSatoshi, MilliSatoshiLong, RealShortChannelId, UInt64}
3232
import scodec.bits.ByteVector
3333

@@ -99,6 +99,7 @@ case class INPUT_INIT_CHANNEL_INITIATOR(temporaryChannelId: ByteVector32,
9999
fundingTxFeeBudget_opt: Option[Satoshi],
100100
pushAmount_opt: Option[MilliSatoshi],
101101
requireConfirmedInputs: Boolean,
102+
requestFunding_opt: Option[LiquidityAds.RequestFunding],
102103
localParams: LocalParams,
103104
remote: ActorRef,
104105
remoteInit: Init,
@@ -110,7 +111,7 @@ case class INPUT_INIT_CHANNEL_INITIATOR(temporaryChannelId: ByteVector32,
110111
require(!(channelType.features.contains(Features.ScidAlias) && channelFlags.announceChannel), "option_scid_alias is not compatible with public channels")
111112
}
112113
case class INPUT_INIT_CHANNEL_NON_INITIATOR(temporaryChannelId: ByteVector32,
113-
fundingContribution_opt: Option[Satoshi],
114+
fundingContribution_opt: Option[LiquidityAds.AddFunding],
114115
dualFunded: Boolean,
115116
pushAmount_opt: Option[MilliSatoshi],
116117
localParams: LocalParams,
@@ -208,10 +209,10 @@ final case class CMD_CLOSE(replyTo: ActorRef, scriptPubKey: Option[ByteVector],
208209
final case class CMD_FORCECLOSE(replyTo: ActorRef) extends CloseCommand
209210
final case class CMD_BUMP_FORCE_CLOSE_FEE(replyTo: akka.actor.typed.ActorRef[CommandResponse[CMD_BUMP_FORCE_CLOSE_FEE]], confirmationTarget: ConfirmationTarget) extends Command
210211

211-
final case class CMD_BUMP_FUNDING_FEE(replyTo: akka.actor.typed.ActorRef[CommandResponse[CMD_BUMP_FUNDING_FEE]], targetFeerate: FeeratePerKw, fundingFeeBudget: Satoshi, lockTime: Long) extends Command
212+
final case class CMD_BUMP_FUNDING_FEE(replyTo: akka.actor.typed.ActorRef[CommandResponse[CMD_BUMP_FUNDING_FEE]], targetFeerate: FeeratePerKw, fundingFeeBudget: Satoshi, lockTime: Long, requestFunding_opt: Option[LiquidityAds.RequestFunding]) extends Command
212213
case class SpliceIn(additionalLocalFunding: Satoshi, pushAmount: MilliSatoshi = 0 msat)
213214
case class SpliceOut(amount: Satoshi, scriptPubKey: ByteVector)
214-
final case class CMD_SPLICE(replyTo: akka.actor.typed.ActorRef[CommandResponse[CMD_SPLICE]], spliceIn_opt: Option[SpliceIn], spliceOut_opt: Option[SpliceOut]) extends Command {
215+
final case class CMD_SPLICE(replyTo: akka.actor.typed.ActorRef[CommandResponse[CMD_SPLICE]], spliceIn_opt: Option[SpliceIn], spliceOut_opt: Option[SpliceOut], requestFunding_opt: Option[LiquidityAds.RequestFunding]) extends Command {
215216
require(spliceIn_opt.isDefined || spliceOut_opt.isDefined, "there must be a splice-in or a splice-out")
216217
val additionalLocalFunding: Satoshi = spliceIn_opt.map(_.additionalLocalFunding).getOrElse(0 sat)
217218
val pushAmount: MilliSatoshi = spliceIn_opt.map(_.pushAmount).getOrElse(0 msat)

eclair-core/src/main/scala/fr/acinq/eclair/channel/ChannelExceptions.scala

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -19,7 +19,7 @@ package fr.acinq.eclair.channel
1919
import fr.acinq.bitcoin.scalacompat.{BlockHash, ByteVector32, Satoshi, Transaction, TxId}
2020
import fr.acinq.eclair.blockchain.fee.FeeratePerKw
2121
import fr.acinq.eclair.wire.protocol
22-
import fr.acinq.eclair.wire.protocol.{AnnouncementSignatures, InteractiveTxMessage, UpdateAddHtlc}
22+
import fr.acinq.eclair.wire.protocol.{AnnouncementSignatures, InteractiveTxMessage, LiquidityAds, UpdateAddHtlc}
2323
import fr.acinq.eclair.{BlockHeight, CltvExpiry, CltvExpiryDelta, MilliSatoshi, UInt64}
2424
import scodec.bits.ByteVector
2525

@@ -51,6 +51,11 @@ case class ToSelfDelayTooHigh (override val channelId: Byte
5151
case class ChannelReserveTooHigh (override val channelId: ByteVector32, channelReserve: Satoshi, reserveToFundingRatio: Double, maxReserveToFundingRatio: Double) extends ChannelException(channelId, s"channelReserve too high: reserve=$channelReserve fundingRatio=$reserveToFundingRatio maxFundingRatio=$maxReserveToFundingRatio")
5252
case class ChannelReserveBelowOurDustLimit (override val channelId: ByteVector32, channelReserve: Satoshi, dustLimit: Satoshi) extends ChannelException(channelId, s"their channelReserve=$channelReserve is below our dustLimit=$dustLimit")
5353
case class ChannelReserveNotMet (override val channelId: ByteVector32, toLocal: MilliSatoshi, toRemote: MilliSatoshi, reserve: Satoshi) extends ChannelException(channelId, s"channel reserve is not met toLocal=$toLocal toRemote=$toRemote reserve=$reserve")
54+
case class MissingLiquidityAds (override val channelId: ByteVector32) extends ChannelException(channelId, "liquidity ads field is missing")
55+
case class InvalidLiquidityAdsSig (override val channelId: ByteVector32) extends ChannelException(channelId, "liquidity ads signature is invalid")
56+
case class InvalidLiquidityAdsAmount (override val channelId: ByteVector32, proposed: Satoshi, min: Satoshi) extends ChannelException(channelId, "liquidity ads funding amount is too low (expected at least $min, got $proposed)")
57+
case class InvalidLiquidityAdsPaymentType (override val channelId: ByteVector32, proposed: LiquidityAds.PaymentType, allowed: Set[LiquidityAds.PaymentType]) extends ChannelException(channelId, s"liquidity ads ${proposed.rfcName} payment type is not supported (allowed=${allowed.map(_.rfcName).mkString(", ")})")
58+
case class InvalidLiquidityAdsLease (override val channelId: ByteVector32) extends ChannelException(channelId, "liquidity ads selected lease rate does not match a lease we offer")
5459
case class ChannelFundingError (override val channelId: ByteVector32) extends ChannelException(channelId, "channel funding error")
5560
case class InvalidFundingTx (override val channelId: ByteVector32) extends ChannelException(channelId, "invalid funding tx")
5661
case class InvalidSerialId (override val channelId: ByteVector32, serialId: UInt64) extends ChannelException(channelId, s"invalid serial_id=${serialId.toByteVector.toHex}")

0 commit comments

Comments
 (0)