Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Adapt max HTLC amount to balance #2703

Merged
merged 5 commits into from
Oct 2, 2023
Merged
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
23 changes: 23 additions & 0 deletions docs/release-notes/eclair-vnext.md
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,29 @@ You can now use Eclair to manage the private keys for on-chain funds monitored b

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

### Advertise low balance with htlc_maximum_msat

Eclair used to disable a channel when there was no liquidity on our side so that other nodes stop trying to use it.
However, other implementations use disabled channels as a sign that the other peer is offline.
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.
The balance thresholds at which to update htlc_maximum_msat are configurable like this:

```eclair.conf
eclair.channel.channel-update {
balance-thresholds = [{
available-sat = 1000 // If our balance goes below this,
max-htlc-sat = 0 // set the maximum HTLC amount to this (or htlc-minimum-msat if it's higher).
},{
available-sat = 10000
max-htlc-sat = 1000
}]

min-time-between-updates = 1 hour // minimum time between channel updates because the balance changed
}
```

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 = []`.

### API changes

- `bumpforceclose` can be used to make a force-close confirm faster, by spending the anchor output (#2743)
Expand Down
30 changes: 30 additions & 0 deletions eclair-core/src/main/resources/reference.conf
Original file line number Diff line number Diff line change
Expand Up @@ -162,6 +162,36 @@ eclair {
}

quiescence-timeout = 1 minutes // maximum time we will stay quiescent (or wait to reach quiescence) before disconnecting

channel-update {
// Balance thresholds at which to update the maximum HTLC amount
// Must be in increasing order.
// Set balance-thresholds = [] to disable this feature.
balance-thresholds = [{
t-bast marked this conversation as resolved.
Show resolved Hide resolved
available-sat = 1000 // If our balance goes below this,
max-htlc-sat = 0 // set the maximum HTLC amount to this (or htlc-minimum-msat if it's higher).
},{
available-sat = 10000
max-htlc-sat = 1000
},{
available-sat = 100000
max-htlc-sat = 10000
},{
available-sat = 200000
max-htlc-sat = 100000
},{
available-sat = 400000
max-htlc-sat = 200000
},{
available-sat = 800000
max-htlc-sat = 400000
},{
available-sat = 1600000
max-htlc-sat = 800000
}]

min-time-between-updates = 1 hour // minimum time between channel updates because the balance changed
}
}

balance-check-interval = 1 hour
Expand Down
2 changes: 1 addition & 1 deletion eclair-core/src/main/scala/fr/acinq/eclair/Eclair.scala
Original file line number Diff line number Diff line change
Expand Up @@ -266,7 +266,7 @@ class EclairImpl(appKit: Kit) extends Eclair with Logging {
for (nodeId <- nodes) {
appKit.nodeParams.db.peers.addOrUpdateRelayFees(nodeId, RelayFees(feeBaseMsat, feeProportionalMillionths))
}
sendToNodes(nodes, CMD_UPDATE_RELAY_FEE(ActorRef.noSender, feeBaseMsat, feeProportionalMillionths, cltvExpiryDelta_opt = None))
sendToNodes(nodes, CMD_UPDATE_RELAY_FEE(ActorRef.noSender, feeBaseMsat, feeProportionalMillionths))
}

override def peers()(implicit timeout: Timeout): Future[Iterable[PeerInfo]] = for {
Expand Down
4 changes: 3 additions & 1 deletion eclair-core/src/main/scala/fr/acinq/eclair/NodeParams.scala
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,7 @@ import fr.acinq.eclair.Setup.Seeds
import fr.acinq.eclair.blockchain.fee._
import fr.acinq.eclair.channel.ChannelFlags
import fr.acinq.eclair.channel.fsm.Channel
import fr.acinq.eclair.channel.fsm.Channel.{ChannelConf, UnhandledExceptionStrategy}
import fr.acinq.eclair.channel.fsm.Channel.{BalanceThreshold, ChannelConf, UnhandledExceptionStrategy}
import fr.acinq.eclair.crypto.Noise.KeyPair
import fr.acinq.eclair.crypto.keymanager.{ChannelKeyManager, NodeKeyManager, OnChainKeyManager}
import fr.acinq.eclair.db._
Expand Down Expand Up @@ -520,6 +520,8 @@ object NodeParams extends Logging {
maxTotalPendingChannelsPrivateNodes = maxTotalPendingChannelsPrivateNodes,
remoteRbfLimits = Channel.RemoteRbfLimits(config.getInt("channel.funding.remote-rbf-limits.max-attempts"), config.getInt("channel.funding.remote-rbf-limits.attempt-delta-blocks")),
quiescenceTimeout = FiniteDuration(config.getDuration("channel.quiescence-timeout").getSeconds, TimeUnit.SECONDS),
balanceThresholds = config.getConfigList("channel.channel-update.balance-thresholds").asScala.map(conf => BalanceThreshold(Satoshi(conf.getLong("available-sat")), Satoshi(conf.getLong("max-htlc-sat")))).toSeq,
minTimeBetweenUpdates = FiniteDuration(config.getDuration("channel.channel-update.min-time-between-updates").getSeconds, TimeUnit.SECONDS),
),
onChainFeeConf = OnChainFeeConf(
feeTargets = feeTargets,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -216,7 +216,7 @@ final case class CMD_SPLICE(replyTo: akka.actor.typed.ActorRef[CommandResponse[C
val pushAmount: MilliSatoshi = spliceIn_opt.map(_.pushAmount).getOrElse(0 msat)
val spliceOutputs: List[TxOut] = spliceOut_opt.toList.map(s => TxOut(s.amount, s.scriptPubKey))
}
final case class CMD_UPDATE_RELAY_FEE(replyTo: ActorRef, feeBase: MilliSatoshi, feeProportionalMillionths: Long, cltvExpiryDelta_opt: Option[CltvExpiryDelta]) extends HasReplyToCommand
final case class CMD_UPDATE_RELAY_FEE(replyTo: ActorRef, feeBase: MilliSatoshi, feeProportionalMillionths: Long) extends HasReplyToCommand
t-bast marked this conversation as resolved.
Show resolved Hide resolved
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
28 changes: 15 additions & 13 deletions eclair-core/src/main/scala/fr/acinq/eclair/channel/Helpers.scala
Original file line number Diff line number Diff line change
Expand Up @@ -325,27 +325,29 @@ object Helpers {
AnnouncementSignatures(channelParams.channelId, shortChannelId, localNodeSig, localBitcoinSig)
}

/**
* This indicates whether our side of the channel is above the reserve requested by our counterparty. In other words,
* this tells if we can use the channel to make a payment.
/** 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.
*/
def aboveReserve(commitments: Commitments)(implicit log: LoggingAdapter): Boolean = {
commitments.active.forall(commitment => {
val remoteCommit = commitment.nextRemoteCommit_opt.map(_.commit).getOrElse(commitment.remoteCommit)
val toRemoteSatoshis = remoteCommit.spec.toRemote.truncateToSatoshi
// NB: this is an approximation (we don't take network fees into account)
val localReserve = commitment.localChannelReserve(commitments.params)
val result = toRemoteSatoshis > localReserve
log.debug("toRemoteSatoshis={} reserve={} aboveReserve={} for remoteCommitNumber={}", toRemoteSatoshis, localReserve, result, remoteCommit.index)
result
})
def maxHtlcAmount(nodeParams: NodeParams, commitments: Commitments): MilliSatoshi = {
if (!commitments.announceChannel) {
// The channel is private, let's not change the channel update needlessly.
return commitments.params.maxHtlcAmount
}
for (balanceThreshold <- nodeParams.channelConf.balanceThresholds) {
if (commitments.availableBalanceForSend <= balanceThreshold.available) {
return balanceThreshold.maxHtlcAmount.toMilliSatoshi.max(commitments.params.remoteParams.htlcMinimum).min(commitments.params.maxHtlcAmount)
}
}
commitments.params.maxHtlcAmount
}

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

def makeChannelUpdate(nodeParams: NodeParams, remoteNodeId: PublicKey, scid: ShortChannelId, commitments: Commitments, relayFees: RelayFees, enable: Boolean = true): ChannelUpdate =
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)

object Funding {

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