From 62cc073d670e9e7407e2a8c5819292f7a829346f Mon Sep 17 00:00:00 2001 From: Bastien Teinturier <31281497+t-bast@users.noreply.github.com> Date: Thu, 9 Dec 2021 11:58:04 +0100 Subject: [PATCH] Remove network stats computation (#2094) We introduced a task to regularly compute network statistics (mostly about channel parameters such as expiry and fees). The goal was to use this information in the MPP split algorithm to decide whether to split a payment or not. But we haven't used it, and I'm not sure anymore that it's useful at all. If node operators are interested in network statistics, an ad-hoc on-the-fly computation would make more sense. --- docs/release-notes/eclair-vnext.md | 18 ++-- eclair-core/eclair-cli | 1 - eclair-core/src/main/resources/reference.conf | 1 - .../main/scala/fr/acinq/eclair/Eclair.scala | 14 +-- .../scala/fr/acinq/eclair/NodeParams.scala | 1 - .../remote/EclairInternalsSerializer.scala | 3 +- .../fr/acinq/eclair/router/NetworkStats.scala | 67 ------------- .../scala/fr/acinq/eclair/router/Router.scala | 34 +------ .../fr/acinq/eclair/EclairImplSpec.scala | 26 +---- .../scala/fr/acinq/eclair/TestConstants.scala | 2 - .../eclair/router/NetworkStatsSpec.scala | 97 ------------------- .../fr/acinq/eclair/router/RouterSpec.scala | 23 ----- .../eclair/api/handlers/PathFinding.scala | 6 +- .../fr/acinq/eclair/api/ApiServiceSpec.scala | 25 +---- 14 files changed, 24 insertions(+), 294 deletions(-) delete mode 100644 eclair-core/src/main/scala/fr/acinq/eclair/router/NetworkStats.scala delete mode 100644 eclair-core/src/test/scala/fr/acinq/eclair/router/NetworkStatsSpec.scala diff --git a/docs/release-notes/eclair-vnext.md b/docs/release-notes/eclair-vnext.md index eff80a93ab..0ea0de0494 100644 --- a/docs/release-notes/eclair-vnext.md +++ b/docs/release-notes/eclair-vnext.md @@ -29,10 +29,12 @@ Messages sent to Eclair can be read with the websocket API. #### Timestamps All timestamps are now returned as an object with two attributes: + - `iso`: ISO-8601 format with GMT time zone. Precision may be second or millisecond depending on the timestamp. - `unix`: seconds since epoch formats (seconds since epoch). Precision is always second. Examples: + - second-precision timestamp: - before: ```json @@ -73,6 +75,14 @@ It expects `--route` a list of `nodeId` to send the message through, the last on It also accepts `--pathId` as an encoded TLV stream in hexadecimal. Sending to a blinded route (as a reply to a previous message) is not supported. +#### Balance + +The detailed balance json format has been slightly updated for channels in state `normal` and `shutdown`, and `closing`. + +Amounts corresponding to incoming htlcs for which we knew the preimage were previously included in `toLocal`, they are +now grouped with outgoing htlcs amounts and the field has been renamed from `htlcOut` to `htlcs`. + +#### Miscellaneous This release contains many other API updates: @@ -83,16 +93,10 @@ This release contains many other API updates: - `findroute`, `findroutetonode` and `findroutebetweennodes` now accept `--maxFeeMsat` to specify an upper bound of fees (#1969) - `getsentinfo` output includes `failedNode` field for all failed routes - for `payinvoice` and `sendtonode`, `--feeThresholdSat` has been renamed to `--maxFeeFlatSat` +- the `networkstats` API has been removed Have a look at our [API documentation](https://acinq.github.io/eclair) for more details. -#### Balance - -The detailed balance json format has been slightly updated for channels in state `normal` and `shutdown`, and `closing`. - -Amounts corresponding to incoming htlcs for which we knew the preimage were previously included in `toLocal`, they are -now grouped with outgoing htlcs amounts and the field has been renamed from `htlcOut` to `htlcs`. - ### Miscellaneous improvements and bug fixes - Eclair now supports cookie authentication for Bitcoin Core RPC (#1986) diff --git a/eclair-core/eclair-cli b/eclair-core/eclair-cli index 521b0d2bf7..4b6e320164 100755 --- a/eclair-core/eclair-cli +++ b/eclair-core/eclair-cli @@ -53,7 +53,6 @@ and COMMAND is one of the available commands: - findroute - findroutetonode - findroutebetweennodes - - networkstats - nodes === Invoice === diff --git a/eclair-core/src/main/resources/reference.conf b/eclair-core/src/main/resources/reference.conf index d899adc339..b1379dbfa2 100644 --- a/eclair-core/src/main/resources/reference.conf +++ b/eclair-core/src/main/resources/reference.conf @@ -214,7 +214,6 @@ eclair { router { 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 - network-stats-interval = 6 hours // frequency at which we refresh global network statistics (expensive operation) init-timeout = 5 minutes sync { diff --git a/eclair-core/src/main/scala/fr/acinq/eclair/Eclair.scala b/eclair-core/src/main/scala/fr/acinq/eclair/Eclair.scala index b827575974..37bdbe8d00 100644 --- a/eclair-core/src/main/scala/fr/acinq/eclair/Eclair.scala +++ b/eclair-core/src/main/scala/fr/acinq/eclair/Eclair.scala @@ -42,8 +42,8 @@ import fr.acinq.eclair.payment.receive.MultiPartHandler.ReceivePayment import fr.acinq.eclair.payment.relay.Relayer.{GetOutgoingChannels, OutgoingChannels, RelayFees, UsableBalance} import fr.acinq.eclair.payment.send.MultiPartPaymentLifecycle.PreimageReceived import fr.acinq.eclair.payment.send.PaymentInitiator.{SendPaymentToNode, SendPaymentToRoute, SendPaymentToRouteResponse, SendSpontaneousPayment} +import fr.acinq.eclair.router.Router import fr.acinq.eclair.router.Router._ -import fr.acinq.eclair.router.{NetworkStats, Router} import fr.acinq.eclair.wire.protocol._ import grizzled.slf4j.Logging import scodec.bits.ByteVector @@ -126,8 +126,6 @@ trait Eclair { def channelStats(from: TimestampSecond, to: TimestampSecond)(implicit timeout: Timeout): Future[Seq[Stats]] - def networkStats()(implicit timeout: Timeout): Future[Option[NetworkStats]] - def getInvoice(paymentHash: ByteVector32)(implicit timeout: Timeout): Future[Option[PaymentRequest]] def pendingInvoices(from: TimestampSecond, to: TimestampSecond)(implicit timeout: Timeout): Future[Seq[PaymentRequest]] @@ -395,8 +393,6 @@ class EclairImpl(appKit: Kit) extends Eclair with Logging { Future(appKit.nodeParams.db.audit.stats(from.toTimestampMilli, to.toTimestampMilli)) } - override def networkStats()(implicit timeout: Timeout): Future[Option[NetworkStats]] = (appKit.router ? GetNetworkStats).mapTo[GetNetworkStatsResponse].map(_.stats) - override def allInvoices(from: TimestampSecond, to: TimestampSecond)(implicit timeout: Timeout): Future[Seq[PaymentRequest]] = Future { appKit.nodeParams.db.payments.listIncomingPayments(from.toTimestampMilli, to.toTimestampMilli).map(_.paymentRequest) } @@ -413,8 +409,6 @@ class EclairImpl(appKit: Kit) extends Eclair with Logging { Future.fromTry(appKit.nodeParams.db.payments.removeIncomingPayment(paymentHash).map(_ => s"deleted invoice $paymentHash")) } - - /** * Send a request to a channel and expect a response. * @@ -522,9 +516,9 @@ class EclairImpl(appKit: Kit) extends Eclair with Logging { Nil, userCustomTlvs) (appKit.switchboard ? OnionMessages.SendMessage(nextNodeId, message)).mapTo[MessageRelay.Status].map { - case MessageRelay.Success => SendOnionMessageResponse(sent = true, None) - case MessageRelay.Failure(f) => SendOnionMessageResponse(sent = false, Some(f.toString)) - } + case MessageRelay.Success => SendOnionMessageResponse(sent = true, None) + case MessageRelay.Failure(f) => SendOnionMessageResponse(sent = false, Some(f.toString)) + } case Attempt.Failure(cause) => Future.successful(SendOnionMessageResponse(sent = false, Some(s"the `content` field is invalid, it must contain encoded tlvs: ${cause.message}"))) } diff --git a/eclair-core/src/main/scala/fr/acinq/eclair/NodeParams.scala b/eclair-core/src/main/scala/fr/acinq/eclair/NodeParams.scala index fd5fb25611..24d03b49f3 100644 --- a/eclair-core/src/main/scala/fr/acinq/eclair/NodeParams.scala +++ b/eclair-core/src/main/scala/fr/acinq/eclair/NodeParams.scala @@ -455,7 +455,6 @@ object NodeParams extends Logging { routerConf = RouterConf( channelExcludeDuration = FiniteDuration(config.getDuration("router.channel-exclude-duration").getSeconds, TimeUnit.SECONDS), routerBroadcastInterval = FiniteDuration(config.getDuration("router.broadcast-interval").getSeconds, TimeUnit.SECONDS), - networkStatsRefreshInterval = FiniteDuration(config.getDuration("router.network-stats-interval").getSeconds, TimeUnit.SECONDS), requestNodeAnnouncements = config.getBoolean("router.sync.request-node-announcements"), encodingType = routerSyncEncodingType, channelRangeChunkSize = config.getInt("router.sync.channel-range-chunk-size"), diff --git a/eclair-core/src/main/scala/fr/acinq/eclair/remote/EclairInternalsSerializer.scala b/eclair-core/src/main/scala/fr/acinq/eclair/remote/EclairInternalsSerializer.scala index 5a23fb478e..8055b4e4d2 100644 --- a/eclair-core/src/main/scala/fr/acinq/eclair/remote/EclairInternalsSerializer.scala +++ b/eclair-core/src/main/scala/fr/acinq/eclair/remote/EclairInternalsSerializer.scala @@ -83,13 +83,12 @@ object EclairInternalsSerializer { ("experimentPercentage" | int32)).as[PathFindingConf] val pathFindingExperimentConfCodec: Codec[PathFindingExperimentConf] = ( - ("experiments" | listOfN(int32, pathFindingConfCodec).xmap[Map[String, PathFindingConf]](_.map(e => (e.experimentName -> e)).toMap, _.values.toList)) + "experiments" | listOfN(int32, pathFindingConfCodec).xmap[Map[String, PathFindingConf]](_.map(e => e.experimentName -> e).toMap, _.values.toList) ).as[PathFindingExperimentConf] val routerConfCodec: Codec[RouterConf] = ( ("channelExcludeDuration" | finiteDurationCodec) :: ("routerBroadcastInterval" | finiteDurationCodec) :: - ("networkStatsRefreshInterval" | finiteDurationCodec) :: ("requestNodeAnnouncements" | bool(8)) :: ("encodingType" | discriminated[EncodingType].by(uint8) .typecase(0, provide(EncodingType.UNCOMPRESSED)) diff --git a/eclair-core/src/main/scala/fr/acinq/eclair/router/NetworkStats.scala b/eclair-core/src/main/scala/fr/acinq/eclair/router/NetworkStats.scala deleted file mode 100644 index 9526c85d5a..0000000000 --- a/eclair-core/src/main/scala/fr/acinq/eclair/router/NetworkStats.scala +++ /dev/null @@ -1,67 +0,0 @@ -/* - * Copyright 2019 ACINQ SAS - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package fr.acinq.eclair.router - -import com.google.common.math.Quantiles.percentiles -import fr.acinq.bitcoin.Satoshi -import fr.acinq.eclair.router.Router.PublicChannel -import fr.acinq.eclair.wire.protocol.ChannelUpdate -import fr.acinq.eclair.{CltvExpiryDelta, MilliSatoshi} - -import scala.jdk.CollectionConverters._ - -/** - * Created by t-bast on 30/08/2019. - */ - -case class Stats[T](median: T, percentile5: T, percentile10: T, percentile25: T, percentile75: T, percentile90: T, percentile95: T) - -object Stats { - /** - * NB: can't name this method apply because of https://github.com/json4s/json4s/issues/507 - */ - def generate[T](values: Iterable[Long], fromDouble: Double => T): Stats[T] = { - require(values.nonEmpty, "can't compute stats on empty values") - val stats = percentiles().indexes(5, 10, 25, 50, 75, 90, 95).compute(values.map(java.lang.Long.valueOf).asJavaCollection) - Stats(fromDouble(stats.get(50)), fromDouble(stats.get(5)), fromDouble(stats.get(10)), fromDouble(stats.get(25)), fromDouble(stats.get(75)), fromDouble(stats.get(90)), fromDouble(stats.get(95))) - } -} - -case class NetworkStats(channels: Int, nodes: Int, capacity: Stats[Satoshi], cltvExpiryDelta: Stats[CltvExpiryDelta], feeBase: Stats[MilliSatoshi], feeProportional: Stats[Long]) - -object NetworkStats { - /** - * Computes various network statistics (expensive). - * Network statistics won't change noticeably very quickly, so this should not be re-computed too often. - */ - def computeStats(publicChannels: Iterable[PublicChannel]): Option[NetworkStats] = { - // We need at least one channel update to be able to compute stats. - if (publicChannels.isEmpty || publicChannels.flatMap(pc => getChannelUpdateField(pc, _ => true)).isEmpty) { - None - } else { - val capacityStats = Stats.generate(publicChannels.map(_.capacity.toLong), d => Satoshi(d.toLong)) - val cltvStats = Stats.generate(publicChannels.flatMap(pc => getChannelUpdateField(pc, u => u.cltvExpiryDelta.toInt.toLong)), d => CltvExpiryDelta(d.toInt)) - val feeBaseStats = Stats.generate(publicChannels.flatMap(pc => getChannelUpdateField(pc, u => u.feeBaseMsat.toLong)), d => MilliSatoshi(d.toLong)) - val feeProportionalStats = Stats.generate(publicChannels.flatMap(pc => getChannelUpdateField(pc, u => u.feeProportionalMillionths)), d => d.toLong) - val nodes = publicChannels.flatMap(pc => pc.ann.nodeId1 :: pc.ann.nodeId2 :: Nil).toSet.size - Some(NetworkStats(publicChannels.size, nodes, capacityStats, cltvStats, feeBaseStats, feeProportionalStats)) - } - } - - private def getChannelUpdateField[T](pc: PublicChannel, f: ChannelUpdate => T): Seq[T] = (pc.update_1_opt.toSeq ++ pc.update_2_opt.toSeq).map(f) - -} diff --git a/eclair-core/src/main/scala/fr/acinq/eclair/router/Router.scala b/eclair-core/src/main/scala/fr/acinq/eclair/router/Router.scala index 5041b7273c..b2696cde2f 100644 --- a/eclair-core/src/main/scala/fr/acinq/eclair/router/Router.scala +++ b/eclair-core/src/main/scala/fr/acinq/eclair/router/Router.scala @@ -35,7 +35,7 @@ import fr.acinq.eclair.payment.PaymentRequest.ExtraHop import fr.acinq.eclair.remote.EclairInternalsSerializer.RemoteTypes import fr.acinq.eclair.router.Graph.GraphStructure.DirectedGraph import fr.acinq.eclair.router.Graph.{HeuristicsConstants, WeightRatios} -import fr.acinq.eclair.router.Monitoring.{Metrics, Tags} +import fr.acinq.eclair.router.Monitoring.Metrics import fr.acinq.eclair.wire.protocol._ import kamon.context.Context @@ -63,7 +63,6 @@ class Router(val nodeParams: NodeParams, watcher: typed.ActorRef[ZmqWatcher.Comm startTimerWithFixedDelay(TickBroadcast.toString, TickBroadcast, nodeParams.routerConf.routerBroadcastInterval) startTimerWithFixedDelay(TickPruneStaleChannels.toString, TickPruneStaleChannels, 1 hour) - startTimerWithFixedDelay(TickComputeNetworkStats.toString, TickComputeNetworkStats, nodeParams.routerConf.networkStatsRefreshInterval) val db: NetworkDb = nodeParams.db.network @@ -99,21 +98,17 @@ class Router(val nodeParams: NodeParams, watcher: typed.ActorRef[ZmqWatcher.Comm val nodeAnn = Announcements.makeNodeAnnouncement(nodeParams.privateKey, nodeParams.alias, nodeParams.color, nodeParams.publicAddresses, nodeParams.features) self ! nodeAnn - log.info(s"computing network stats...") - val stats = NetworkStats.computeStats(initChannels.values) - log.info(s"initialization completed, ready to process messages") Try(initialized.map(_.success(Done))) - startWith(NORMAL, Data(initNodes, initChannels, stats, Stash(Map.empty, Map.empty), rebroadcast = Rebroadcast(channels = Map.empty, updates = Map.empty, nodes = Map.empty), awaiting = Map.empty, privateChannels = Map.empty, excludedChannels = Set.empty, graph, sync = Map.empty)) + startWith(NORMAL, Data(initNodes, initChannels, Stash(Map.empty, Map.empty), rebroadcast = Rebroadcast(channels = Map.empty, updates = Map.empty, nodes = Map.empty), awaiting = Map.empty, privateChannels = Map.empty, excludedChannels = Set.empty, graph, sync = Map.empty)) } when(NORMAL) { case Event(SyncProgress(progress), d: Data) => Metrics.SyncProgress.withoutTags().update(100 * progress) - if (d.stats.isEmpty && progress == 1.0 && d.channels.nonEmpty) { - log.info("initial routing sync done: computing network statistics") - self ! TickComputeNetworkStats + if (progress == 1.0 && d.channels.nonEmpty) { + log.info("initial routing sync done") } stay() @@ -122,10 +117,6 @@ class Router(val nodeParams: NodeParams, watcher: typed.ActorRef[ZmqWatcher.Comm sender() ! RoutingState(d.channels.values, d.nodes.values) stay() - case Event(GetNetworkStats, d: Data) => - sender() ! GetNetworkStatsResponse(d.stats) - stay() - case Event(GetRoutingStateStreaming, d) => val listener = sender() d.nodes @@ -164,18 +155,6 @@ class Router(val nodeParams: NodeParams, watcher: typed.ActorRef[ZmqWatcher.Comm stay() using d.copy(rebroadcast = Rebroadcast(channels = Map.empty, updates = Map.empty, nodes = Map.empty)) } - case Event(TickComputeNetworkStats, d) => - if (d.channels.nonEmpty) { - Metrics.Nodes.withoutTags().update(d.nodes.size) - Metrics.Channels.withTag(Tags.Announced, value = true).update(d.channels.size) - Metrics.Channels.withTag(Tags.Announced, value = false).update(d.privateChannels.size) - log.info("re-computing network statistics") - stay() using d.copy(stats = NetworkStats.computeStats(d.channels.values)) - } else { - log.debug("cannot compute network statistics: no public channels available") - stay() - } - case Event(TickPruneStaleChannels, d) => stay() using StaleChannels.handlePruneStaleChannels(d, nodeParams.db.network, nodeParams.currentBlockHeight) @@ -320,7 +299,6 @@ object Router { case class RouterConf(channelExcludeDuration: FiniteDuration, routerBroadcastInterval: FiniteDuration, - networkStatsRefreshInterval: FiniteDuration, requestNodeAnnouncements: Boolean, encodingType: EncodingType, channelRangeChunkSize: Int, @@ -535,8 +513,6 @@ object Router { // @formatter:off case class SendChannelQuery(chainHash: ByteVector32, remoteNodeId: PublicKey, to: ActorRef, replacePrevious: Boolean, flags_opt: Option[QueryChannelRangeTlv]) extends RemoteTypes - case object GetNetworkStats - case class GetNetworkStatsResponse(stats: Option[NetworkStats]) case object GetRoutingState case class RoutingState(channels: Iterable[PublicChannel], nodes: Iterable[NodeAnnouncement]) case object GetRoutingStateStreaming extends RemoteTypes @@ -591,7 +567,6 @@ object Router { case class Data(nodes: Map[PublicKey, NodeAnnouncement], channels: SortedMap[ShortChannelId, PublicChannel], - stats: Option[NetworkStats], stash: Stash, rebroadcast: Rebroadcast, awaiting: Map[ChannelAnnouncement, Seq[RemoteGossip]], // note: this is a seq because we want to preserve order: first actor is the one who we need to send a tcp-ack when validation is done @@ -607,7 +582,6 @@ object Router { case object TickBroadcast case object TickPruneStaleChannels - case object TickComputeNetworkStats // @formatter:on def getDesc(u: ChannelUpdate, channel: ChannelAnnouncement): ChannelDesc = { diff --git a/eclair-core/src/test/scala/fr/acinq/eclair/EclairImplSpec.scala b/eclair-core/src/test/scala/fr/acinq/eclair/EclairImplSpec.scala index d374529e2d..877ac1f926 100644 --- a/eclair-core/src/test/scala/fr/acinq/eclair/EclairImplSpec.scala +++ b/eclair-core/src/test/scala/fr/acinq/eclair/EclairImplSpec.scala @@ -36,8 +36,8 @@ import fr.acinq.eclair.payment.receive.PaymentHandler import fr.acinq.eclair.payment.relay.Relayer.RelayFees import fr.acinq.eclair.payment.send.PaymentInitiator.{SendPaymentToNode, SendPaymentToRoute, SendSpontaneousPayment} import fr.acinq.eclair.router.RouteCalculationSpec.makeUpdateShort -import fr.acinq.eclair.router.Router.{GetNetworkStats, GetNetworkStatsResponse, PredefinedNodeRoute, PublicChannel} -import fr.acinq.eclair.router.{Announcements, NetworkStats, Router, Stats} +import fr.acinq.eclair.router.Router.{PredefinedNodeRoute, PublicChannel} +import fr.acinq.eclair.router.{Announcements, Router} import fr.acinq.eclair.wire.internal.channel.ChannelCodecsSpec import fr.acinq.eclair.wire.protocol.{ChannelUpdate, Color, NodeAnnouncement} import org.mockito.Mockito @@ -264,28 +264,6 @@ class EclairImplSpec extends TestKitBaseClass with FixtureAnyFunSuiteLike with I }) } - test("router returns Network Stats") { f => - import f._ - - val capStat = Stats(30 sat, 12 sat, 14 sat, 20 sat, 40 sat, 46 sat, 48 sat) - val cltvStat = Stats(CltvExpiryDelta(32), CltvExpiryDelta(11), CltvExpiryDelta(13), CltvExpiryDelta(22), CltvExpiryDelta(42), CltvExpiryDelta(51), CltvExpiryDelta(53)) - val feeBaseStat = Stats(32 msat, 11 msat, 13 msat, 22 msat, 42 msat, 51 msat, 53 msat) - val feePropStat = Stats(32L, 11L, 13L, 22L, 42L, 51L, 53L) - val eclair = new EclairImpl(kit) - val fResp = eclair.networkStats() - f.router.expectMsg(GetNetworkStats) - - f.router.reply(GetNetworkStatsResponse(Some(new NetworkStats(1, 2, capStat, cltvStat, feeBaseStat, feePropStat)))) - - awaitCond({ - fResp.value match { - case Some(Success(Some(res))) => res.channels == 1 - case _ => false - } - }) - - } - test("close and forceclose should work both with channelId and shortChannelId") { f => import f._ diff --git a/eclair-core/src/test/scala/fr/acinq/eclair/TestConstants.scala b/eclair-core/src/test/scala/fr/acinq/eclair/TestConstants.scala index 832959837b..6257d3de6a 100644 --- a/eclair-core/src/test/scala/fr/acinq/eclair/TestConstants.scala +++ b/eclair-core/src/test/scala/fr/acinq/eclair/TestConstants.scala @@ -168,7 +168,6 @@ object TestConstants { routerConf = RouterConf( channelExcludeDuration = 60 seconds, routerBroadcastInterval = 5 seconds, - networkStatsRefreshInterval = 1 hour, requestNodeAnnouncements = true, encodingType = EncodingType.COMPRESSED_ZLIB, channelRangeChunkSize = 20, @@ -295,7 +294,6 @@ object TestConstants { routerConf = RouterConf( channelExcludeDuration = 60 seconds, routerBroadcastInterval = 5 seconds, - networkStatsRefreshInterval = 1 hour, requestNodeAnnouncements = true, encodingType = EncodingType.UNCOMPRESSED, channelRangeChunkSize = 20, diff --git a/eclair-core/src/test/scala/fr/acinq/eclair/router/NetworkStatsSpec.scala b/eclair-core/src/test/scala/fr/acinq/eclair/router/NetworkStatsSpec.scala deleted file mode 100644 index 10540278d9..0000000000 --- a/eclair-core/src/test/scala/fr/acinq/eclair/router/NetworkStatsSpec.scala +++ /dev/null @@ -1,97 +0,0 @@ -/* - * Copyright 2019 ACINQ SAS - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package fr.acinq.eclair.router - -import fr.acinq.bitcoin.Crypto.PublicKey -import fr.acinq.bitcoin.{Satoshi, SatoshiLong} -import fr.acinq.eclair.router.Router.{ChannelMeta, PublicChannel} -import fr.acinq.eclair.wire.protocol.{ChannelAnnouncement, ChannelUpdate} -import fr.acinq.eclair.{CltvExpiryDelta, MilliSatoshi, MilliSatoshiLong, ShortChannelId, TimestampSecond, TimestampSecondLong, randomBytes32, randomBytes64, randomKey} -import org.scalatest.funsuite.AnyFunSuite - -import scala.util.Random - -/** - * Created by t-bast on 30/08/2019. - */ - -class NetworkStatsSpec extends AnyFunSuite { - - import NetworkStatsSpec._ - - test("network data missing") { - assert(NetworkStats.computeStats(Nil) === None) - assert(NetworkStats.computeStats(Seq( - PublicChannel(fakeChannelAnnouncement(randomKey().publicKey, randomKey().publicKey), randomBytes32(), 10 sat, None, None, None), - PublicChannel(fakeChannelAnnouncement(randomKey().publicKey, randomKey().publicKey), randomBytes32(), 15 sat, None, None, Some(ChannelMeta(10000 msat, 3000 msat))) - )) === None) - } - - test("small network") { - val nodes = Seq.fill(6)(randomKey().publicKey) - val channels = Seq( - PublicChannel(fakeChannelAnnouncement(nodes(0), nodes(1)), randomBytes32(), 10 sat, Some(fakeChannelUpdate1(CltvExpiryDelta(10), 10 msat, 10)), Some(fakeChannelUpdate2(CltvExpiryDelta(15), 15 msat, 15)), None), - PublicChannel(fakeChannelAnnouncement(nodes(1), nodes(2)), randomBytes32(), 20 sat, None, Some(fakeChannelUpdate2(CltvExpiryDelta(25), 25 msat, 25)), None), - PublicChannel(fakeChannelAnnouncement(nodes(2), nodes(3)), randomBytes32(), 30 sat, Some(fakeChannelUpdate1(CltvExpiryDelta(30), 30 msat, 30)), Some(fakeChannelUpdate2(CltvExpiryDelta(35), 35 msat, 35)), Some(ChannelMeta(18000 msat, 12000 msat))), - PublicChannel(fakeChannelAnnouncement(nodes(3), nodes(4)), randomBytes32(), 40 sat, Some(fakeChannelUpdate1(CltvExpiryDelta(40), 40 msat, 40)), None, None), - PublicChannel(fakeChannelAnnouncement(nodes(4), nodes(5)), randomBytes32(), 50 sat, Some(fakeChannelUpdate1(CltvExpiryDelta(50), 50 msat, 50)), Some(fakeChannelUpdate2(CltvExpiryDelta(55), 55 msat, 55)), None) - ) - val Some(stats) = NetworkStats.computeStats(channels) - assert(stats.channels === 5) - assert(stats.nodes === 6) - assert(stats.capacity === Stats(30 sat, 12 sat, 14 sat, 20 sat, 40 sat, 46 sat, 48 sat)) - assert(stats.cltvExpiryDelta === Stats(CltvExpiryDelta(32), CltvExpiryDelta(11), CltvExpiryDelta(13), CltvExpiryDelta(22), CltvExpiryDelta(42), CltvExpiryDelta(51), CltvExpiryDelta(53))) - assert(stats.feeBase === Stats(32 msat, 11 msat, 13 msat, 22 msat, 42 msat, 51 msat, 53 msat)) - assert(stats.feeProportional === Stats(32, 11, 13, 22, 42, 51, 53)) - } - - test("intermediate network") { - val rand = new Random() - val nodes = Seq.fill(100)(randomKey().publicKey) - val channels = Seq.fill(500)(PublicChannel( - fakeChannelAnnouncement(nodes(rand.nextInt(nodes.size)), nodes(rand.nextInt(nodes.size))), - randomBytes32(), - Satoshi(1000 + rand.nextInt(10000)), - Some(fakeChannelUpdate1(CltvExpiryDelta(12 + rand.nextInt(144)), MilliSatoshi(21000 + rand.nextInt(79000)), rand.nextInt(1000))), - Some(fakeChannelUpdate2(CltvExpiryDelta(12 + rand.nextInt(144)), MilliSatoshi(21000 + rand.nextInt(79000)), rand.nextInt(1000))), - None - )) - val Some(stats) = NetworkStats.computeStats(channels) - assert(stats.channels === 500) - assert(stats.nodes <= 100) - assert(1000.sat <= stats.capacity.median && stats.capacity.median <= 11000.sat) - assert(stats.feeBase.percentile10 >= 21000.msat) - assert(stats.feeProportional.median <= 1000) - } - -} - -object NetworkStatsSpec { - - def fakeChannelAnnouncement(local: PublicKey, remote: PublicKey): ChannelAnnouncement = { - Announcements.makeChannelAnnouncement(randomBytes32(), ShortChannelId(42), local, remote, randomKey().publicKey, randomKey().publicKey, randomBytes64(), randomBytes64(), randomBytes64(), randomBytes64()) - } - - def fakeChannelUpdate1(cltv: CltvExpiryDelta, feeBase: MilliSatoshi, feeProportional: Long): ChannelUpdate = { - ChannelUpdate(randomBytes64(), randomBytes32(), ShortChannelId(42), 0 unixsec, ChannelUpdate.ChannelFlags.DUMMY, cltv, 1 msat, feeBase, feeProportional, None) - } - - def fakeChannelUpdate2(cltv: CltvExpiryDelta, feeBase: MilliSatoshi, feeProportional: Long): ChannelUpdate = { - ChannelUpdate(randomBytes64(), randomBytes32(), ShortChannelId(42), 0 unixsec, ChannelUpdate.ChannelFlags(isNode1 = false, isEnabled = false), cltv, 1 msat, feeBase, feeProportional, None) - } - -} \ No newline at end of file diff --git a/eclair-core/src/test/scala/fr/acinq/eclair/router/RouterSpec.scala b/eclair-core/src/test/scala/fr/acinq/eclair/router/RouterSpec.scala index 9e5c75351e..e484966e09 100644 --- a/eclair-core/src/test/scala/fr/acinq/eclair/router/RouterSpec.scala +++ b/eclair-core/src/test/scala/fr/acinq/eclair/router/RouterSpec.scala @@ -468,29 +468,6 @@ class RouterSpec extends BaseRouterSpec { )) } - test("send network statistics") { fixture => - import fixture._ - val sender = TestProbe() - sender.send(router, GetNetworkStats) - sender.expectMsg(GetNetworkStatsResponse(None)) - - // Network statistics should be computed after initial sync - router ! SyncProgress(1.0) - awaitCond({ - sender.send(router, GetNetworkStats) - sender.expectMsgType[GetNetworkStatsResponse].stats.isDefined - }) - - sender.send(router, GetNetworkStats) - val GetNetworkStatsResponse(Some(stats)) = sender.expectMsgType[GetNetworkStatsResponse] - // if you change this test update test "router returns Network Stats" in EclairImpSpec that mocks this call. - // else will break the networkstats API call - assert(stats.channels === 5) - assert(stats.nodes === 8) - assert(stats.capacity.median === 1000000.sat) - assert(stats.cltvExpiryDelta.median === CltvExpiryDelta(7)) - } - test("given a pre-defined nodes route add the proper channel updates") { fixture => import fixture._ diff --git a/eclair-node/src/main/scala/fr/acinq/eclair/api/handlers/PathFinding.scala b/eclair-node/src/main/scala/fr/acinq/eclair/api/handlers/PathFinding.scala index 5deff727c2..48cc596681 100644 --- a/eclair-node/src/main/scala/fr/acinq/eclair/api/handlers/PathFinding.scala +++ b/eclair-node/src/main/scala/fr/acinq/eclair/api/handlers/PathFinding.scala @@ -57,16 +57,12 @@ trait PathFinding { } } - val networkStats: Route = postRequest("networkstats") { implicit t => - complete(eclairApi.networkStats()) - } - val nodes: Route = postRequest("nodes") { implicit t => formFields(nodeIdsFormParam.?) { nodeIds_opt => complete(eclairApi.nodes(nodeIds_opt.map(_.toSet))) } } - val pathFindingRoutes: Route = findRoute ~ findRouteToNode ~ findRouteBetweenNodes ~ networkStats ~ nodes + val pathFindingRoutes: Route = findRoute ~ findRouteToNode ~ findRouteBetweenNodes ~ nodes } diff --git a/eclair-node/src/test/scala/fr/acinq/eclair/api/ApiServiceSpec.scala b/eclair-node/src/test/scala/fr/acinq/eclair/api/ApiServiceSpec.scala index 291003af0e..0919087e6e 100644 --- a/eclair-node/src/test/scala/fr/acinq/eclair/api/ApiServiceSpec.scala +++ b/eclair-node/src/test/scala/fr/acinq/eclair/api/ApiServiceSpec.scala @@ -46,7 +46,7 @@ import fr.acinq.eclair.payment.relay.Relayer.UsableBalance import fr.acinq.eclair.payment.send.MultiPartPaymentLifecycle.PreimageReceived import fr.acinq.eclair.payment.send.PaymentInitiator.SendPaymentToRouteResponse import fr.acinq.eclair.router.Router.PredefinedNodeRoute -import fr.acinq.eclair.router.{NetworkStats, Router, Stats} +import fr.acinq.eclair.router.Router import fr.acinq.eclair.wire.protocol.{ChannelUpdate, Color, GenericTlv, MessageOnion, NodeAddress, OnionMessagePayloadTlv, TlvStream} import org.mockito.scalatest.IdiomaticMockito import org.scalatest.funsuite.AnyFunSuite @@ -1051,29 +1051,6 @@ class ApiServiceSpec extends AnyFunSuite with ScalatestRouteTest with IdiomaticM } } - test("'networkstats' response should return expected statistics") { - val capStat = Stats(30 sat, 12 sat, 14 sat, 20 sat, 40 sat, 46 sat, 48 sat) - val cltvStat = Stats(CltvExpiryDelta(32), CltvExpiryDelta(11), CltvExpiryDelta(13), CltvExpiryDelta(22), CltvExpiryDelta(42), CltvExpiryDelta(51), CltvExpiryDelta(53)) - val feeBaseStat = Stats(32 msat, 11 msat, 13 msat, 22 msat, 42 msat, 51 msat, 53 msat) - val feePropStat = Stats(32L, 11L, 13L, 22L, 42L, 51L, 53L) - val networkStats = new NetworkStats(1, 2, capStat, cltvStat, feeBaseStat, feePropStat) - - val eclair = mock[Eclair] - val mockService = new MockService(eclair) - eclair.networkStats()(any[Timeout]) returns Future.successful(Some(networkStats)) - - Post("/networkstats") ~> - addCredentials(BasicHttpCredentials("", mockApi().password)) ~> - Route.seal(mockService.networkStats) ~> - check { - assert(handled) - assert(status == OK) - val resp = entityAs[String] - eclair.networkStats()(any[Timeout]).wasCalled(once) - matchTestJson("networkstats", resp) - } - } - test("'audit'") { val eclair = mock[Eclair] val mockService = new MockService(eclair)