Skip to content

Commit

Permalink
Signal channels with a low balance by using maxHtlcAmount
Browse files Browse the repository at this point in the history
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.
  • Loading branch information
thomash-acinq committed Sep 22, 2023
1 parent 96ebbfe commit 79b9ab7
Show file tree
Hide file tree
Showing 17 changed files with 119 additions and 74 deletions.
15 changes: 15 additions & 0 deletions eclair-core/src/main/resources/reference.conf
Original file line number Diff line number Diff line change
Expand Up @@ -161,6 +161,21 @@ eclair {
}

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

// Balance thresholds at which to update the maximum HTLC amount
// Must be in increasing order.
balance-thresholds = [{
available-msat = 10000 // If our balance goes below this,
max-htlc-msat = 1 // set the maximum HTLC amount to this.
},{
available-msat = 10000000
max-htlc-msat = 10000000
},{
available-msat = 100000000
max-htlc-msat = 100000000
}]

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 @@ -519,6 +519,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.balance-thresholds").asScala.map(conf => BalanceThreshold(MilliSatoshi(conf.getLong("available-msat")), MilliSatoshi(conf.getLong("max-htlc-msat")))).toSeq,
minTimeBetweenUpdates = FiniteDuration(config.getDuration("channel.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
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
29 changes: 19 additions & 10 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,36 @@ 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 => {
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
}
val availableToSend = commitments.active.map(commitment => {
val remoteCommit = commitment.nextRemoteCommit_opt.map(_.commit).getOrElse(commitment.remoteCommit)
val toRemoteSatoshis = remoteCommit.spec.toRemote.truncateToSatoshi
val localBalance = 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
})
(localBalance - localReserve).toMilliSatoshi
}).min
for (balanceThreshold <- nodeParams.channelConf.balanceThresholds) {
if (availableToSend <= balanceThreshold.available) {
return balanceThreshold.maxHtlcAmount.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

0 comments on commit 79b9ab7

Please sign in to comment.