Skip to content

Commit e83eaea

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 9477caf commit e83eaea

File tree

58 files changed

+1327
-324
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

+1327
-324
lines changed

docs/release-notes/eclair-vnext.md

Lines changed: 10 additions & 1 deletion
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 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 27.1.
@@ -19,7 +28,7 @@ consolidatefeerate=0
1928

2029
### API changes
2130

22-
<insert changes>
31+
- `nodes` allows filtering nodes that offer liquidity ads (#2848)
2332

2433
### Miscellaneous improvements and bug fixes
2534

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,
@@ -215,10 +216,10 @@ final case class CMD_CLOSE(replyTo: ActorRef, scriptPubKey: Option[ByteVector],
215216
final case class CMD_FORCECLOSE(replyTo: ActorRef) extends CloseCommand
216217
final case class CMD_BUMP_FORCE_CLOSE_FEE(replyTo: akka.actor.typed.ActorRef[CommandResponse[CMD_BUMP_FORCE_CLOSE_FEE]], confirmationTarget: ConfirmationTarget) extends Command
217218

218-
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
219+
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
219220
case class SpliceIn(additionalLocalFunding: Satoshi, pushAmount: MilliSatoshi = 0 msat)
220221
case class SpliceOut(amount: Satoshi, scriptPubKey: ByteVector)
221-
final case class CMD_SPLICE(replyTo: akka.actor.typed.ActorRef[CommandResponse[CMD_SPLICE]], spliceIn_opt: Option[SpliceIn], spliceOut_opt: Option[SpliceOut]) extends Command {
222+
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 {
222223
require(spliceIn_opt.isDefined || spliceOut_opt.isDefined, "there must be a splice-in or a splice-out")
223224
val additionalLocalFunding: Satoshi = spliceIn_opt.map(_.additionalLocalFunding).getOrElse(0 sat)
224225
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)