Skip to content

Commit 9ca9227

Browse files
Adapt max HTLC amount to balance (#2703)
Some channels have only a few sats available to send but other nodes don't know it so they try to use them and fail. When the balance goes below configurable thresholds we now advertize a lower maxHtlcAmount. This should reduce the number of failed attempts and benefit the network.
1 parent db8e0f9 commit 9ca9227

20 files changed

+244
-84
lines changed

docs/release-notes/eclair-vnext.md

+23
Original file line numberDiff line numberDiff line change
@@ -29,6 +29,29 @@ You can now use Eclair to manage the private keys for on-chain funds monitored b
2929

3030
See `docs/BitcoinCoreKeys.md` for more details.
3131

32+
### Advertise low balance with htlc_maximum_msat
33+
34+
Eclair used to disable a channel when there was no liquidity on our side so that other nodes stop trying to use it.
35+
However, other implementations use disabled channels as a sign that the other peer is offline.
36+
To be consistent with other implementations, we now only disable channels when our peer is offline and signal that a channel has very low balance by setting htlc_maximum_msat to a low value.
37+
The balance thresholds at which to update htlc_maximum_msat are configurable like this:
38+
39+
```eclair.conf
40+
eclair.channel.channel-update {
41+
balance-thresholds = [{
42+
available-sat = 1000 // If our balance goes below this,
43+
max-htlc-sat = 0 // set the maximum HTLC amount to this (or htlc-minimum-msat if it's higher).
44+
},{
45+
available-sat = 10000
46+
max-htlc-sat = 1000
47+
}]
48+
49+
min-time-between-updates = 1 hour // minimum time between channel updates because the balance changed
50+
}
51+
```
52+
53+
This feature leaks a bit of information about the balance when the channel is almost empty, if you do not wish to use it, set `eclair.channel.channel-update.balance-thresholds = []`.
54+
3255
### API changes
3356

3457
- `bumpforceclose` can be used to make a force-close confirm faster, by spending the anchor output (#2743)

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

+30
Original file line numberDiff line numberDiff line change
@@ -164,6 +164,36 @@ eclair {
164164
}
165165

166166
quiescence-timeout = 1 minutes // maximum time we will stay quiescent (or wait to reach quiescence) before disconnecting
167+
168+
channel-update {
169+
// Balance thresholds at which to update the maximum HTLC amount
170+
// Must be in increasing order.
171+
// Set balance-thresholds = [] to disable this feature.
172+
balance-thresholds = [{
173+
available-sat = 1000 // If our balance goes below this,
174+
max-htlc-sat = 0 // set the maximum HTLC amount to this (or htlc-minimum-msat if it's higher).
175+
},{
176+
available-sat = 10000
177+
max-htlc-sat = 1000
178+
},{
179+
available-sat = 100000
180+
max-htlc-sat = 10000
181+
},{
182+
available-sat = 200000
183+
max-htlc-sat = 100000
184+
},{
185+
available-sat = 400000
186+
max-htlc-sat = 200000
187+
},{
188+
available-sat = 800000
189+
max-htlc-sat = 400000
190+
},{
191+
available-sat = 1600000
192+
max-htlc-sat = 800000
193+
}]
194+
195+
min-time-between-updates = 1 hour // minimum time between channel updates because the balance changed
196+
}
167197
}
168198

169199
balance-check-interval = 1 hour

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

+1-1
Original file line numberDiff line numberDiff line change
@@ -266,7 +266,7 @@ class EclairImpl(appKit: Kit) extends Eclair with Logging {
266266
for (nodeId <- nodes) {
267267
appKit.nodeParams.db.peers.addOrUpdateRelayFees(nodeId, RelayFees(feeBaseMsat, feeProportionalMillionths))
268268
}
269-
sendToNodes(nodes, CMD_UPDATE_RELAY_FEE(ActorRef.noSender, feeBaseMsat, feeProportionalMillionths, cltvExpiryDelta_opt = None))
269+
sendToNodes(nodes, CMD_UPDATE_RELAY_FEE(ActorRef.noSender, feeBaseMsat, feeProportionalMillionths))
270270
}
271271

272272
override def peers()(implicit timeout: Timeout): Future[Iterable[PeerInfo]] = for {

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

+3-1
Original file line numberDiff line numberDiff line change
@@ -23,7 +23,7 @@ import fr.acinq.eclair.Setup.Seeds
2323
import fr.acinq.eclair.blockchain.fee._
2424
import fr.acinq.eclair.channel.ChannelFlags
2525
import fr.acinq.eclair.channel.fsm.Channel
26-
import fr.acinq.eclair.channel.fsm.Channel.{ChannelConf, UnhandledExceptionStrategy}
26+
import fr.acinq.eclair.channel.fsm.Channel.{BalanceThreshold, ChannelConf, UnhandledExceptionStrategy}
2727
import fr.acinq.eclair.crypto.Noise.KeyPair
2828
import fr.acinq.eclair.crypto.keymanager.{ChannelKeyManager, NodeKeyManager, OnChainKeyManager}
2929
import fr.acinq.eclair.db._
@@ -522,6 +522,8 @@ object NodeParams extends Logging {
522522
maxTotalPendingChannelsPrivateNodes = maxTotalPendingChannelsPrivateNodes,
523523
remoteRbfLimits = Channel.RemoteRbfLimits(config.getInt("channel.funding.remote-rbf-limits.max-attempts"), config.getInt("channel.funding.remote-rbf-limits.attempt-delta-blocks")),
524524
quiescenceTimeout = FiniteDuration(config.getDuration("channel.quiescence-timeout").getSeconds, TimeUnit.SECONDS),
525+
balanceThresholds = config.getConfigList("channel.channel-update.balance-thresholds").asScala.map(conf => BalanceThreshold(Satoshi(conf.getLong("available-sat")), Satoshi(conf.getLong("max-htlc-sat")))).toSeq,
526+
minTimeBetweenUpdates = FiniteDuration(config.getDuration("channel.channel-update.min-time-between-updates").getSeconds, TimeUnit.SECONDS),
525527
),
526528
onChainFeeConf = OnChainFeeConf(
527529
feeTargets = feeTargets,

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

+1-1
Original file line numberDiff line numberDiff line change
@@ -216,7 +216,7 @@ final case class CMD_SPLICE(replyTo: akka.actor.typed.ActorRef[CommandResponse[C
216216
val pushAmount: MilliSatoshi = spliceIn_opt.map(_.pushAmount).getOrElse(0 msat)
217217
val spliceOutputs: List[TxOut] = spliceOut_opt.toList.map(s => TxOut(s.amount, s.scriptPubKey))
218218
}
219-
final case class CMD_UPDATE_RELAY_FEE(replyTo: ActorRef, feeBase: MilliSatoshi, feeProportionalMillionths: Long, cltvExpiryDelta_opt: Option[CltvExpiryDelta]) extends HasReplyToCommand
219+
final case class CMD_UPDATE_RELAY_FEE(replyTo: ActorRef, feeBase: MilliSatoshi, feeProportionalMillionths: Long) extends HasReplyToCommand
220220
final case class CMD_GET_CHANNEL_STATE(replyTo: ActorRef) extends HasReplyToCommand
221221
final case class CMD_GET_CHANNEL_DATA(replyTo: ActorRef) extends HasReplyToCommand
222222
final case class CMD_GET_CHANNEL_INFO(replyTo: akka.actor.typed.ActorRef[RES_GET_CHANNEL_INFO]) extends Command

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

+15-13
Original file line numberDiff line numberDiff line change
@@ -327,27 +327,29 @@ object Helpers {
327327
AnnouncementSignatures(channelParams.channelId, shortChannelId, localNodeSig, localBitcoinSig)
328328
}
329329

330-
/**
331-
* This indicates whether our side of the channel is above the reserve requested by our counterparty. In other words,
332-
* this tells if we can use the channel to make a payment.
330+
/** Computes a maximum HTLC amount adapted to the current balance to reduce chances that other nodes will try sending payments that we can't relay.
333331
*/
334-
def aboveReserve(commitments: Commitments)(implicit log: LoggingAdapter): Boolean = {
335-
commitments.active.forall(commitment => {
336-
val remoteCommit = commitment.nextRemoteCommit_opt.map(_.commit).getOrElse(commitment.remoteCommit)
337-
val toRemoteSatoshis = remoteCommit.spec.toRemote.truncateToSatoshi
338-
// NB: this is an approximation (we don't take network fees into account)
339-
val localReserve = commitment.localChannelReserve(commitments.params)
340-
val result = toRemoteSatoshis > localReserve
341-
log.debug("toRemoteSatoshis={} reserve={} aboveReserve={} for remoteCommitNumber={}", toRemoteSatoshis, localReserve, result, remoteCommit.index)
342-
result
343-
})
332+
def maxHtlcAmount(nodeParams: NodeParams, commitments: Commitments): MilliSatoshi = {
333+
if (!commitments.announceChannel) {
334+
// The channel is private, let's not change the channel update needlessly.
335+
return commitments.params.maxHtlcAmount
336+
}
337+
for (balanceThreshold <- nodeParams.channelConf.balanceThresholds) {
338+
if (commitments.availableBalanceForSend <= balanceThreshold.available) {
339+
return balanceThreshold.maxHtlcAmount.toMilliSatoshi.max(commitments.params.remoteParams.htlcMinimum).min(commitments.params.maxHtlcAmount)
340+
}
341+
}
342+
commitments.params.maxHtlcAmount
344343
}
345344

346345
def getRelayFees(nodeParams: NodeParams, remoteNodeId: PublicKey, announceChannel: Boolean): RelayFees = {
347346
val defaultFees = nodeParams.relayParams.defaultFees(announceChannel)
348347
nodeParams.db.peers.getRelayFees(remoteNodeId).getOrElse(defaultFees)
349348
}
350349

350+
def makeChannelUpdate(nodeParams: NodeParams, remoteNodeId: PublicKey, scid: ShortChannelId, commitments: Commitments, relayFees: RelayFees, enable: Boolean = true): ChannelUpdate =
351+
Announcements.makeChannelUpdate(nodeParams.chainHash, nodeParams.privateKey, remoteNodeId, scid, nodeParams.channelConf.expiryDelta, commitments.params.remoteParams.htlcMinimum, relayFees.feeBase, relayFees.feeProportionalMillionths, maxHtlcAmount(nodeParams, commitments), isPrivate = !commitments.announceChannel, enable)
352+
351353
object Funding {
352354

353355
def makeFundingInputInfo(fundingTxId: ByteVector32, fundingTxOutputIndex: Int, fundingSatoshis: Satoshi, fundingPubkey1: PublicKey, fundingPubkey2: PublicKey): InputInfo = {

0 commit comments

Comments
 (0)