Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,9 @@ project/target
DeleteMe*.*
*~
jdbcUrlFile_*.tmp
.metals/
.vscode/
.bloop/

.DS_Store

Expand Down
87 changes: 87 additions & 0 deletions docs/release-notes/eclair-vnext.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,87 @@
# Eclair vnext

<insert here a high-level description of the release>

## Major changes

<insert changes>

### bLIP-18 Inbound Routing Fees

Eclair now supports [bLIP-18 inbound routing fees](https://github.com/lightning/blips/pull/18) which proposes an optional
TLV for channel updates that allows node operators to set (and optionally advertise) inbound routing fee discounts, enabling
more flexible fee policies and incentivizing desired incoming traffic.

#### Configuration

| Configuration Parameter | Default Value | Description |
|--------------------------------------------------------------------------|---------------|----------------------------------------------------------------------------------------------------------------------------------------------------------|
| `eclair.router.path-finding.blip18-inbound-fees` | `false` | enables support for bLIP-18 inbound routing fees |
| `eclair.router.path-finding.exclude-channels-with-positive-inbound-fees` | `false` | enables exclusion of channels with positive inbound fees from path finding, helping to prevent `FeeInsufficient` errors and ensure more reliable routing |

The routing logic considers inbound fees during route selection if enabled. New logic is added to exclude channels with
positive inbound fees from route finding when configured. The relay and route calculation logic now computes total fees
as the sum of the regular (outbound) and inbound fees when applicable.

The wire protocol is updated to include the new TLV (type 55555) for bLIP-18 inbound fees in ChannelUpdate messages.
Code that (de)serializes channel updates now handles these new fields.

Inbound fees are stored per peer in a new database: a separate `inboundfees.sqlite` file when using sqlite, or a new
`inboundfees` schema when using postgres. When using sqlite, the built-in backup mechanism maintains a snapshot of it
named `inboundfees.sqlite.bak`, next to `eclair.sqlite.bak`: if you set inbound fees, include that file in your backups.

### API changes

<insert changes>

- `updaterelayfee` now accepts optional `--inboundFeeBaseMsat` and `--inboundFeeProportionalMillionths` parameters (both must be negative or zero, since only inbound discounts are supported). If omitted, existing inbound fees will be preserved.
- `updaterelayfee` now accepts an optional `--unsetInboundFees` flag that removes any previously advertised inbound fees. It cannot be combined with `--inboundFeeBaseMsat` or `--inboundFeeProportionalMillionths`.

### Miscellaneous improvements and bug fixes

<insert changes>

## Verifying signatures

You will need `gpg` and our release signing key E04E48E72C205463. Note that you can get it:

- from our website: https://acinq.co/pgp/drouinf2.asc
- from github user @sstone, a committer on eclair: https://api.github.com/users/sstone/gpg_keys

To import our signing key:

```sh
$ gpg --import drouinf2.asc
```

To verify the release file checksums and signatures:

```sh
$ gpg -d SHA256SUMS.asc > SHA256SUMS.stripped
$ sha256sum -c SHA256SUMS.stripped
```

## Building

Eclair builds are deterministic. To reproduce our builds, please use the following environment (*):

- Ubuntu 24.04.1
- Adoptium OpenJDK 21.0.6

Use the following command to generate the eclair-node package:

```sh
./mvnw clean install -DskipTests
```

That should generate `eclair-node/target/eclair-node-<version>-XXXXXXX-bin.zip` with sha256 checksums that match the one we provide and sign in `SHA256SUMS.asc`

(*) You may be able to build the exact same artefacts with other operating systems or versions of JDK 21, we have not tried everything.

## Upgrading

This release is fully compatible with previous eclair versions. You don't need to close your channels, just stop eclair, upgrade and restart.

## Changelog

<fill this section when publishing the release with `git log v0.12.0... --format=oneline --reverse`>
2 changes: 2 additions & 0 deletions eclair-core/src/main/resources/reference.conf
Original file line number Diff line number Diff line change
Expand Up @@ -466,6 +466,8 @@ eclair {
}

path-finding {
blip18-inbound-fees = false
exclude-channels-with-positive-inbound-fees = false
default {
randomize-route-selection = true // when computing a route for a payment we randomize the final selection

Expand Down
28 changes: 23 additions & 5 deletions eclair-core/src/main/scala/fr/acinq/eclair/Eclair.scala
Original file line number Diff line number Diff line change
Expand Up @@ -43,7 +43,7 @@ import fr.acinq.eclair.message.{OnionMessages, Postman}
import fr.acinq.eclair.payment._
import fr.acinq.eclair.payment.offer.{OfferCreator, OfferManager}
import fr.acinq.eclair.payment.receive.MultiPartHandler.ReceiveStandardPayment
import fr.acinq.eclair.payment.relay.Relayer.{ChannelBalance, GetOutgoingChannels, OutgoingChannels, RelayFees}
import fr.acinq.eclair.payment.relay.Relayer.{ChannelBalance, GetOutgoingChannels, InboundFees, OutgoingChannels, RelayFees}
import fr.acinq.eclair.payment.send.PaymentInitiator._
import fr.acinq.eclair.payment.send.{ClearRecipient, OfferPayment, PaymentIdentifier}
import fr.acinq.eclair.profit.PeerScorer
Expand Down Expand Up @@ -114,6 +114,8 @@ trait Eclair {

def updateRelayFee(nodes: List[PublicKey], feeBase: MilliSatoshi, feeProportionalMillionths: Long)(implicit timeout: Timeout): Future[Map[ApiTypes.ChannelIdentifier, Either[Throwable, CommandResponse[CMD_UPDATE_RELAY_FEE]]]]

def updateRelayFee(nodes: List[PublicKey], feeBase: MilliSatoshi, feeProportionalMillionths: Long, inboundFeeBase_opt: Option[MilliSatoshi], inboundFeeProportional_opt: Option[Long], unsetInboundFees: Boolean)(implicit timeout: Timeout): Future[Map[ApiTypes.ChannelIdentifier, Either[Throwable, CommandResponse[CMD_UPDATE_RELAY_FEE]]]]

def channelsInfo(toRemoteNode_opt: Option[PublicKey])(implicit timeout: Timeout): Future[Iterable[RES_GET_CHANNEL_INFO]]

def channelInfo(channel: ApiTypes.ChannelIdentifier)(implicit timeout: Timeout): Future[CommandResponse[CMD_GET_CHANNEL_INFO]]
Expand Down Expand Up @@ -311,11 +313,27 @@ class EclairImpl(val appKit: Kit) extends Eclair with Logging with SpendFromChan
sendToChannelsTyped(channels, cmdBuilder = CMD_BUMP_FORCE_CLOSE_FEE(_, confirmationTarget))
}

override def updateRelayFee(nodes: List[PublicKey], feeBaseMsat: MilliSatoshi, feeProportionalMillionths: Long)(implicit timeout: Timeout): Future[Map[ApiTypes.ChannelIdentifier, Either[Throwable, CommandResponse[CMD_UPDATE_RELAY_FEE]]]] = {
for (nodeId <- nodes) {
appKit.nodeParams.db.peers.addOrUpdateRelayFees(nodeId, RelayFees(feeBaseMsat, feeProportionalMillionths))
override def updateRelayFee(nodes: List[PublicKey], feeBaseMsat: MilliSatoshi, feeProportionalMillionths: Long)(implicit timeout: Timeout): Future[Map[ApiTypes.ChannelIdentifier, Either[Throwable, CommandResponse[CMD_UPDATE_RELAY_FEE]]]] =
updateRelayFee(nodes, feeBaseMsat, feeProportionalMillionths, None, None, unsetInboundFees = false)

override def updateRelayFee(nodes: List[PublicKey], feeBaseMsat: MilliSatoshi, feeProportionalMillionths: Long, inboundFeeBase_opt: Option[MilliSatoshi], inboundFeeProportional_opt: Option[Long], unsetInboundFees: Boolean)(implicit timeout: Timeout): Future[Map[ApiTypes.ChannelIdentifier, Either[Throwable, CommandResponse[CMD_UPDATE_RELAY_FEE]]]] = {
if ((inboundFeeBase_opt.isDefined || inboundFeeProportional_opt.isDefined) && !appKit.nodeParams.routerConf.blip18.enableInboundFees) {
Future.failed(new IllegalArgumentException("Cannot specify inbound fees when bLIP-18 support is disabled"))
} else if (!inboundFeeBase_opt.forall(value => value.toLong >= Int.MinValue && value.toLong <= 0)) {
Future.failed(new IllegalArgumentException(s"Inbound fee base must be in the range from ${Int.MinValue} to 0"))
} else if (!inboundFeeProportional_opt.forall(value => value >= Int.MinValue && value <= 0)) {
Future.failed(new IllegalArgumentException(s"Inbound fee proportional millionths must be in the range from ${Int.MinValue} to 0"))
} else {
val inboundFees_opt = InboundFees.fromOptions(inboundFeeBase_opt, inboundFeeProportional_opt)
for (nodeId <- nodes) {
appKit.nodeParams.db.peers.addOrUpdateRelayFees(nodeId, RelayFees(feeBaseMsat, feeProportionalMillionths))
inboundFees_opt match {
case Some(inboundFees) => appKit.nodeParams.db.inboundFees.addOrUpdateInboundFees(nodeId, inboundFees)
case _ => if (unsetInboundFees) appKit.nodeParams.db.inboundFees.unsetInboundFees(nodeId)
}
}
sendToNodes(nodes, CMD_UPDATE_RELAY_FEE(ActorRef.noSender, feeBaseMsat, feeProportionalMillionths, inboundFeeBase_opt, inboundFeeProportional_opt, if (inboundFees_opt.isDefined) false else unsetInboundFees))
}
sendToNodes(nodes, CMD_UPDATE_RELAY_FEE(ActorRef.noSender, feeBaseMsat, feeProportionalMillionths))
}

override def peers()(implicit timeout: Timeout): Future[Iterable[PeerInfo]] = for {
Expand Down
7 changes: 5 additions & 2 deletions eclair-core/src/main/scala/fr/acinq/eclair/NodeParams.scala
Original file line number Diff line number Diff line change
Expand Up @@ -484,7 +484,6 @@ object NodeParams extends Logging {
experimentName = name,
experimentPercentage = config.getInt("percentage"))


def getPathFindingExperimentConf(config: Config): PathFindingExperimentConf = {
val experiments = config.root.asScala.keys.map(name => name -> getPathFindingConf(config.getConfig(name), name))
PathFindingExperimentConf(experiments.toMap)
Expand Down Expand Up @@ -687,7 +686,11 @@ object NodeParams extends Logging {
pathFindingExperimentConf = getPathFindingExperimentConf(config.getConfig("router.path-finding.experiments")),
messageRouteParams = getMessageRouteParams(config.getConfig("router.message-path-finding")),
balanceEstimateHalfLife = FiniteDuration(config.getDuration("router.balance-estimate-half-life").getSeconds, TimeUnit.SECONDS),
),
blip18 = Router.Blip18Params(
enableInboundFees = config.getBoolean("router.path-finding.blip18-inbound-fees"),
excludePositiveInboundFees = config.getBoolean("router.path-finding.exclude-channels-with-positive-inbound-fees"),
),
),
socksProxy_opt = socksProxy_opt,
maxPaymentAttempts = config.getInt("max-payment-attempts"),
paymentFinalExpiry = PaymentFinalExpiryConf(CltvExpiryDelta(config.getInt("send.recipient-final-expiry.min-delta")), CltvExpiryDelta(config.getInt("send.recipient-final-expiry.max-delta"))),
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -270,7 +270,7 @@ final case class CMD_SPLICE(replyTo: akka.actor.typed.ActorRef[CommandResponse[C
val spliceOutputs: List[TxOut] = spliceOut_opt.toList.map(s => TxOut(s.amount, s.scriptPubKey))
}
final case class CMD_BUMP_FUNDING_FEE(replyTo: akka.actor.typed.ActorRef[CommandResponse[ChannelFundingCommand]], targetFeerate: FeeratePerKw, fundingFeeBudget: Satoshi, lockTime: Long, requestFunding_opt: Option[LiquidityAds.RequestFunding]) extends ChannelFundingCommand
final case class CMD_UPDATE_RELAY_FEE(replyTo: ActorRef, feeBase: MilliSatoshi, feeProportionalMillionths: Long) extends HasReplyToCommand
final case class CMD_UPDATE_RELAY_FEE(replyTo: ActorRef, feeBase: MilliSatoshi, feeProportionalMillionths: Long, inboundFeeBase_opt: Option[MilliSatoshi] = None, inboundFeeProportionalMillionths_opt: Option[Long] = None, unsetInboundFees: Boolean = false) extends HasReplyToCommand
final case class CMD_GET_CHANNEL_STATE(replyTo: ActorRef) extends HasReplyToCommand
final case class CMD_GET_CHANNEL_DATA(replyTo: ActorRef) extends HasReplyToCommand
final case class CMD_GET_CHANNEL_INFO(replyTo: akka.actor.typed.ActorRef[RES_GET_CHANNEL_INFO]) extends Command
Expand Down
16 changes: 12 additions & 4 deletions eclair-core/src/main/scala/fr/acinq/eclair/channel/Helpers.scala
Original file line number Diff line number Diff line change
Expand Up @@ -30,7 +30,7 @@ import fr.acinq.eclair.channel.fund.InteractiveTxSigningSession
import fr.acinq.eclair.crypto.keymanager.{ChannelKeys, LocalCommitmentKeys, RemoteCommitmentKeys}
import fr.acinq.eclair.crypto.{NonceGenerator, ShaChain}
import fr.acinq.eclair.db.ChannelsDb
import fr.acinq.eclair.payment.relay.Relayer.RelayFees
import fr.acinq.eclair.payment.relay.Relayer.{InboundFees, RelayFees}
import fr.acinq.eclair.router.Announcements
import fr.acinq.eclair.transactions.DirectedHtlc._
import fr.acinq.eclair.transactions.Transactions._
Expand Down Expand Up @@ -338,7 +338,7 @@ object Helpers {
}
}

def channelUpdate(nodeParams: NodeParams, shortChannelId: ShortChannelId, commitments: Commitments, relayFees: RelayFees, enable: Boolean): ChannelUpdate = {
def channelUpdate(nodeParams: NodeParams, shortChannelId: ShortChannelId, commitments: Commitments, relayFees: RelayFees, enable: Boolean, inboundFees_opt: Option[InboundFees]): ChannelUpdate = {
Announcements.makeChannelUpdate(
chainHash = nodeParams.chainHash,
nodeSecret = nodeParams.privateKey,
Expand All @@ -351,6 +351,11 @@ object Helpers {
htlcMaximumMsat = maxHtlcAmount(nodeParams, commitments),
isPrivate = !commitments.announceChannel,
enable = enable,
timestamp = TimestampSecond.now(),
// We never advertise inbound fees when bLIP-18 support is disabled: the relay wouldn't honour them, so keeping
// them in our channel_update (e.g. from before the feature was disabled) would give senders a fee they cannot
// actually use. This gate applies to all channel_update paths (fee updates, periodic refresh, disable, etc.).
inboundFees_opt = if (nodeParams.routerConf.blip18.enableInboundFees) inboundFees_opt else None
)
}

Expand Down Expand Up @@ -391,9 +396,12 @@ object Helpers {
commitments.maxHtlcValueInFlight
}

def getRelayFees(nodeParams: NodeParams, remoteNodeId: PublicKey, announceChannel: Boolean): RelayFees = {
def getRelayFees(nodeParams: NodeParams, remoteNodeId: PublicKey, announceChannel: Boolean): (RelayFees, Option[InboundFees]) = {
val defaultFees = nodeParams.relayParams.defaultFees(announceChannel)
nodeParams.db.peers.getRelayFees(remoteNodeId).getOrElse(defaultFees)
val relayFees = nodeParams.db.peers.getRelayFees(remoteNodeId).getOrElse(defaultFees)
// We only query the inbound fees database when bLIP-18 support is enabled, to avoid a useless db lookup otherwise.
val inboundFees_opt = if (nodeParams.routerConf.blip18.enableInboundFees) nodeParams.db.inboundFees.getInboundFees(remoteNodeId) else None
(relayFees, inboundFees_opt)
}

object Funding {
Expand Down
Loading