Skip to content

Commit d3a7283

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 885b45b commit d3a7283

File tree

58 files changed

+1334
-327
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

+1334
-327
lines changed

docs/release-notes/eclair-vnext.md

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,15 @@
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 sell their liquidity in a trustless and decentralized manner.
11+
Every node advertizes the rates at which they sell 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 27.1.
@@ -28,6 +37,7 @@ Eclair will not allow remote peers to open new obsolete channels that do not sup
2837

2938
- `channelstats` now takes optional parameters `--count` and `--skip` to control pagination. By default, it will return first 10 entries. (#2890)
3039
- `createinvoice` now takes an optional `--privateChannelIds` parameter that can be used to add routing hints through private channels. (#2909)
40+
- `nodes` allows filtering nodes that offer liquidity ads (#2848)
3141

3242
### Miscellaneous improvements and bug fixes
3343

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

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

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

@@ -479,6 +480,32 @@ object NodeParams extends Logging {
479480
val maxNoChannels = config.getInt("peer-connection.max-no-channels")
480481
require(maxNoChannels > 0, "peer-connection.max-no-channels must be > 0")
481482

483+
val willFundRates_opt = {
484+
val supportedPaymentTypes = Map(
485+
LiquidityAds.PaymentType.FromChannelBalance.rfcName -> LiquidityAds.PaymentType.FromChannelBalance
486+
)
487+
val paymentTypes: Set[LiquidityAds.PaymentType] = config.getStringList("liquidity-ads.payment-types").asScala.map(s => {
488+
supportedPaymentTypes.get(s) match {
489+
case Some(paymentType) => paymentType
490+
case None => throw new IllegalArgumentException(s"unknown liquidity ads payment type: $s")
491+
}
492+
}).toSet
493+
val fundingRates: List[LiquidityAds.FundingRate] = config.getConfigList("liquidity-ads.funding-rates").asScala.map { r =>
494+
LiquidityAds.FundingRate(
495+
minAmount = r.getLong("min-funding-amount-satoshis").sat,
496+
maxAmount = r.getLong("max-funding-amount-satoshis").sat,
497+
fundingWeight = r.getInt("funding-weight"),
498+
feeBase = r.getLong("fee-base-satoshis").sat,
499+
feeProportional = r.getInt("fee-basis-points")
500+
)
501+
}.toList
502+
if (fundingRates.nonEmpty && paymentTypes.nonEmpty) {
503+
Some(LiquidityAds.WillFundRates(fundingRates, paymentTypes))
504+
} else {
505+
None
506+
}
507+
}
508+
482509
NodeParams(
483510
nodeKeyManager = nodeKeyManager,
484511
channelKeyManager = channelKeyManager,
@@ -615,6 +642,7 @@ object NodeParams extends Logging {
615642
batchSize = config.getInt("db.revoked-htlc-info-cleaner.batch-size"),
616643
interval = FiniteDuration(config.getDuration("db.revoked-htlc-info-cleaner.interval").getSeconds, TimeUnit.SECONDS)
617644
),
645+
willFundRates_opt = willFundRates_opt,
618646
peerWakeUpConfig = PeerReadyNotifier.WakeUpConfig(
619647
enabled = config.getBoolean("peer-wake-up.enabled"),
620648
timeout = FiniteDuration(config.getDuration("peer-wake-up.timeout").getSeconds, TimeUnit.SECONDS)

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
@@ -26,7 +26,7 @@ import fr.acinq.eclair.channel.fund.{InteractiveTxBuilder, InteractiveTxSigningS
2626
import fr.acinq.eclair.io.Peer
2727
import fr.acinq.eclair.transactions.CommitmentSpec
2828
import fr.acinq.eclair.transactions.Transactions._
29-
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}
29+
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}
3030
import fr.acinq.eclair.{Alias, BlockHeight, CltvExpiry, CltvExpiryDelta, Features, InitFeature, MilliSatoshi, MilliSatoshiLong, RealShortChannelId, TimestampMilli, UInt64}
3131
import scodec.bits.ByteVector
3232

@@ -98,6 +98,7 @@ case class INPUT_INIT_CHANNEL_INITIATOR(temporaryChannelId: ByteVector32,
9898
fundingTxFeeBudget_opt: Option[Satoshi],
9999
pushAmount_opt: Option[MilliSatoshi],
100100
requireConfirmedInputs: Boolean,
101+
requestFunding_opt: Option[LiquidityAds.RequestFunding],
101102
localParams: LocalParams,
102103
remote: ActorRef,
103104
remoteInit: Init,
@@ -109,7 +110,7 @@ case class INPUT_INIT_CHANNEL_INITIATOR(temporaryChannelId: ByteVector32,
109110
require(!(channelType.features.contains(Features.ScidAlias) && channelFlags.announceChannel), "option_scid_alias is not compatible with public channels")
110111
}
111112
case class INPUT_INIT_CHANNEL_NON_INITIATOR(temporaryChannelId: ByteVector32,
112-
fundingContribution_opt: Option[Satoshi],
113+
fundingContribution_opt: Option[LiquidityAds.AddFunding],
113114
dualFunded: Boolean,
114115
pushAmount_opt: Option[MilliSatoshi],
115116
localParams: LocalParams,
@@ -214,10 +215,10 @@ final case class CMD_CLOSE(replyTo: ActorRef, scriptPubKey: Option[ByteVector],
214215
final case class CMD_FORCECLOSE(replyTo: ActorRef) extends CloseCommand
215216
final case class CMD_BUMP_FORCE_CLOSE_FEE(replyTo: akka.actor.typed.ActorRef[CommandResponse[CMD_BUMP_FORCE_CLOSE_FEE]], confirmationTarget: ConfirmationTarget) extends Command
216217

217-
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
218+
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
218219
case class SpliceIn(additionalLocalFunding: Satoshi, pushAmount: MilliSatoshi = 0 msat)
219220
case class SpliceOut(amount: Satoshi, scriptPubKey: ByteVector)
220-
final case class CMD_SPLICE(replyTo: akka.actor.typed.ActorRef[CommandResponse[CMD_SPLICE]], spliceIn_opt: Option[SpliceIn], spliceOut_opt: Option[SpliceOut]) extends Command {
221+
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 {
221222
require(spliceIn_opt.isDefined || spliceOut_opt.isDefined, "there must be a splice-in or a splice-out")
222223
val additionalLocalFunding: Satoshi = spliceIn_opt.map(_.additionalLocalFunding).getOrElse(0 sat)
223224
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, s"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 InvalidLiquidityAdsRate (override val channelId: ByteVector32) extends ChannelException(channelId, "liquidity ads funding rates don't match")
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)