Skip to content

Commit

Permalink
Estimate balances of remote channels (#2272)
Browse files Browse the repository at this point in the history
We use past payments attempts to estimate the balances of all channels. This can later be used in path-finding to improve the reliability of payments.
  • Loading branch information
thomash-acinq authored Jun 8, 2022
1 parent 682e9bf commit 4722755
Show file tree
Hide file tree
Showing 14 changed files with 767 additions and 52 deletions.
1 change: 1 addition & 0 deletions eclair-core/src/main/resources/reference.conf
Original file line number Diff line number Diff line change
Expand Up @@ -230,6 +230,7 @@ eclair {
channel-exclude-duration = 60 seconds // when a temporary channel failure is returned, we exclude the channel from our payment routes for this duration
broadcast-interval = 60 seconds // see BOLT #7
init-timeout = 5 minutes
balance-estimate-half-life = 1 day // time after which the confidence of the balance estimate is halved

sync {
request-node-announcements = true // if true we will ask for node announcements when we receive channel ids that we don't know
Expand Down
3 changes: 2 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 @@ -493,7 +493,8 @@ object NodeParams extends Logging {
encodingType = EncodingType.UNCOMPRESSED,
channelRangeChunkSize = config.getInt("router.sync.channel-range-chunk-size"),
channelQueryChunkSize = config.getInt("router.sync.channel-query-chunk-size"),
pathFindingExperimentConf = getPathFindingExperimentConf(config.getConfig("router.path-finding.experiments"))
pathFindingExperimentConf = getPathFindingExperimentConf(config.getConfig("router.path-finding.experiments")),
balanceEstimateHalfLife = FiniteDuration(config.getDuration("router.balance-estimate-half-life").getSeconds, TimeUnit.SECONDS),
),
socksProxy_opt = socksProxy_opt,
maxPaymentAttempts = config.getInt("max-payment-attempts"),
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -102,6 +102,7 @@ class PaymentLifecycle(nodeParams: NodeParams, cfg: SendPaymentConfig, router: A
handleLocalFail(d, DisconnectedException, isFatal = false)

case Event(RES_ADD_SETTLED(_, htlc, fulfill: HtlcResult.Fulfill), d: WaitingForComplete) =>
router ! Router.RouteDidRelay(d.route)
Metrics.PaymentAttempt.withTag(Tags.MultiPart, value = false).record(d.failures.size + 1)
val p = PartialPayment(id, d.c.finalPayload.amount, d.cmd.amount - d.c.finalPayload.amount, htlc.channelId, Some(cfg.fullRoute(d.route)))
myStop(d.c, Right(cfg.createPaymentSent(fulfill.paymentPreimage, p :: Nil)))
Expand Down Expand Up @@ -166,13 +167,31 @@ class PaymentLifecycle(nodeParams: NodeParams, cfg: SendPaymentConfig, router: A

private def handleRemoteFail(d: WaitingForComplete, fail: UpdateFailHtlc) = {
import d._
(Sphinx.FailurePacket.decrypt(fail.reason, sharedSecrets) match {
((Sphinx.FailurePacket.decrypt(fail.reason, sharedSecrets) match {
case success@Success(e) =>
Metrics.PaymentError.withTag(Tags.Failure, Tags.FailureType(RemoteFailure(d.c.finalPayload.amount, Nil, e))).increment()
success
case failure@Failure(_) =>
Metrics.PaymentError.withTag(Tags.Failure, Tags.FailureType(UnreadableRemoteFailure(d.c.finalPayload.amount, Nil))).increment()
failure
}) match {
case res@Success(Sphinx.DecryptedFailurePacket(nodeId, failureMessage)) =>
// We have discovered some liquidity information with this payment: we update the router accordingly.
val stoppedRoute = d.route.stopAt(nodeId)
if (stoppedRoute.hops.length > 1) {
router ! Router.RouteCouldRelay(stoppedRoute)
}
failureMessage match {
case TemporaryChannelFailure(update) =>
d.route.hops.find(_.nodeId == nodeId) match {
case Some(failingHop) if ChannelRelayParams.areSame(failingHop.params, ChannelRelayParams.FromAnnouncement(update), true) =>
router ! Router.ChannelCouldNotRelay(stoppedRoute.amount, failingHop)
case _ => // otherwise the relay parameters may have changed, so it's not necessarily a liquidity issue
}
case _ => // other errors should not be used for liquidity issues
}
res
case res => res
}) match {
case Success(e@Sphinx.DecryptedFailurePacket(nodeId, failureMessage)) if nodeId == c.targetNodeId =>
// if destination node returns an error, we fail the payment immediately
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -96,7 +96,8 @@ object EclairInternalsSerializer {
.typecase(1, provide(EncodingType.COMPRESSED_ZLIB))) ::
("channelRangeChunkSize" | int32) ::
("channelQueryChunkSize" | int32) ::
("pathFindingExperimentConf" | pathFindingExperimentConfCodec)).as[RouterConf]
("pathFindingExperimentConf" | pathFindingExperimentConfCodec) ::
("balanceEstimateHalfLife" | finiteDurationCodec)).as[RouterConf]

val overrideFeaturesListCodec: Codec[List[(PublicKey, Features[Feature])]] = listOfN(uint16, publicKey ~ variableSizeBytes(uint16, featuresCodec))

Expand Down
Loading

0 comments on commit 4722755

Please sign in to comment.