From 41953ccbd2d43ec01130334b423fff0487d5c743 Mon Sep 17 00:00:00 2001 From: n1bor Date: Mon, 19 Aug 2019 23:10:23 +0100 Subject: [PATCH] add API to get general network statistics --- .../main/scala/fr/acinq/eclair/Eclair.scala | 37 +++++++++ .../scala/fr/acinq/eclair/api/Service.scala | 3 + .../fr/acinq/eclair/EclairImplSpec.scala | 80 ++++++++++++++++--- 3 files changed, 110 insertions(+), 10 deletions(-) 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 e22b621d35..b02413ac20 100644 --- a/eclair-core/src/main/scala/fr/acinq/eclair/Eclair.scala +++ b/eclair-core/src/main/scala/fr/acinq/eclair/Eclair.scala @@ -38,8 +38,20 @@ import fr.acinq.eclair.payment.{GetUsableBalances, PaymentReceived, PaymentRelay import fr.acinq.eclair.wire.{ChannelAnnouncement, ChannelUpdate, NodeAddress, NodeAnnouncement} import TimestampQueryFilters._ + case class GetInfoResponse(nodeId: PublicKey, alias: String, chainHash: ByteVector32, blockHeight: Int, publicAddresses: Seq[NodeAddress]) +case class FlagCounter(data : Map[Int,Int] = Map(0->0, 1->0, 2->0, 3->0, 4->0, 5->0, 6->0, 7->0)) { + def +(that: Byte) = { + FlagCounter((0 to 7).map{ b => + (b,data.getOrElse(b,0) + ((that & (1<> b).toInt) + }.toMap) + } +} + +case class GetNetworkInfoResponse(totalChannelCount: Long, totalNodes: Long, totalUpdates:Long, avgCltvExpiryDelta: Long, avgHtlcMinimumMsat: Long, + avgFeeBaseMsat: Long, avgFeeProportionalMillionths: Long, avgHtlcMaximumMsat: Long, messageFlags: FlagCounter, channelFlags: FlagCounter ) + case class AuditResponse(sent: Seq[PaymentSent], received: Seq[PaymentReceived], relayed: Seq[PaymentRelayed]) case class TimestampQueryFilters(from: Long, to: Long) @@ -106,6 +118,8 @@ trait Eclair { def getInfoResponse()(implicit timeout: Timeout): Future[GetInfoResponse] + def getNetworkInfoResponse()(implicit timeout: Timeout): Future[GetNetworkInfoResponse] + def usableBalances()(implicit timeout: Timeout): Future[Iterable[UsableBalances]] } @@ -177,7 +191,30 @@ class EclairImpl(appKit: Kit) extends Eclair { case Some(pk) => (appKit.router ? 'updatesMap).mapTo[Map[ChannelDesc, ChannelUpdate]].map(_.filter(e => e._1.a == pk || e._1.b == pk).values) } + override def getNetworkInfoResponse()(implicit timeout: Timeout): Future[GetNetworkInfoResponse] = { + for { + nodeCount <- allNodes().map{_.size} + channelCount <- allChannels().map{_.size} + + (updateCount: Long,avgCltvExpiryDelta: Long,avgHtlcMinimumMsat: Long,avgFeeBaseMsat: Long,avgFeeProportionalMillionths: Long,avgHtlcMaximumMsat: Long, mf: FlagCounter, cf:FlagCounter ) <- allUpdates(None).map{ + updates => updates.foldLeft(Tuple8(0L, 0L, 0L, 0L, 0L, 0L,FlagCounter(), FlagCounter())) { + (t, c) => + Tuple8(t._1 + 1, + t._2 + c.cltvExpiryDelta, + t._3 + c.htlcMinimumMsat.amount, + t._4 + c.feeBaseMsat.amount, + t._5 + c.feeProportionalMillionths, + t._6 + c.htlcMaximumMsat.getOrElse(MilliSatoshi(0L)).amount, + t._7 + c.messageFlags, + t._8 + c.channelFlags + ) + } + }.map(in => (in._1,in._2/in._1,in._3/in._1,in._4/in._1,in._5/in._1,in._6/in._1, in._7, in._8)) + } yield new GetNetworkInfoResponse(channelCount, nodeCount, updateCount,avgCltvExpiryDelta,avgHtlcMinimumMsat,avgFeeBaseMsat,avgFeeProportionalMillionths,avgHtlcMaximumMsat,mf,cf) + } + override def receive(description: String, amount_opt: Option[MilliSatoshi], expire_opt: Option[Long], fallbackAddress_opt: Option[String], paymentPreimage_opt: Option[ByteVector32])(implicit timeout: Timeout): Future[PaymentRequest] = { + fallbackAddress_opt.map { fa => fr.acinq.eclair.addressToPublicKeyScript(fa, appKit.nodeParams.chainHash) } // if it's not a bitcoin address throws an exception (appKit.paymentHandler ? ReceivePayment(description = description, amount_opt = amount_opt, expirySeconds_opt = expire_opt, fallbackAddress = fallbackAddress_opt, paymentPreimage = paymentPreimage_opt)).mapTo[PaymentRequest] } diff --git a/eclair-core/src/main/scala/fr/acinq/eclair/api/Service.scala b/eclair-core/src/main/scala/fr/acinq/eclair/api/Service.scala index 7d50d75217..198aaff9e9 100644 --- a/eclair-core/src/main/scala/fr/acinq/eclair/api/Service.scala +++ b/eclair-core/src/main/scala/fr/acinq/eclair/api/Service.scala @@ -141,6 +141,9 @@ trait Service extends ExtraDirectives with Logging { path("getinfo") { complete(eclairApi.getInfoResponse()) } ~ + path("getnetworkinfo") { + complete(eclairApi.getNetworkInfoResponse()) + } ~ path("connect") { formFields("uri".as[NodeURI]) { uri => complete(eclairApi.connect(Left(uri))) 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 728815f5a3..a01c58dee3 100644 --- a/eclair-core/src/test/scala/fr/acinq/eclair/EclairImplSpec.scala +++ b/eclair-core/src/test/scala/fr/acinq/eclair/EclairImplSpec.scala @@ -18,26 +18,27 @@ package fr.acinq.eclair import akka.actor.ActorSystem import akka.testkit.{TestKit, TestProbe} -import fr.acinq.bitcoin.{ByteVector32, Crypto, Satoshi} import akka.util.Timeout import fr.acinq.bitcoin.Crypto.PublicKey +import fr.acinq.bitcoin.{ByteVector32, Crypto, Satoshi} +import fr.acinq.eclair.TestConstants._ import fr.acinq.eclair.blockchain.TestWallet +import fr.acinq.eclair.channel.{CMD_FORCECLOSE, Register, _} +import fr.acinq.eclair.db._ import fr.acinq.eclair.io.Peer.OpenChannel +import fr.acinq.eclair.payment.LocalPaymentHandler import fr.acinq.eclair.payment.PaymentLifecycle.{ReceivePayment, SendPayment, SendPaymentToRoute} -import fr.acinq.eclair.payment.PaymentLifecycle.SendPayment import fr.acinq.eclair.payment.PaymentRequest.ExtraHop -import org.scalatest.{Matchers, Outcome, fixture} -import scodec.bits._ -import TestConstants._ -import fr.acinq.eclair.channel.{CMD_FORCECLOSE, Register} -import fr.acinq.eclair.payment.LocalPaymentHandler -import fr.acinq.eclair.channel._ -import fr.acinq.eclair.db._ +import fr.acinq.eclair.router.Announcements.makeNodeAnnouncement import fr.acinq.eclair.router.RouteCalculationSpec.makeUpdate +import fr.acinq.eclair.wire.Color import org.mockito.scalatest.IdiomaticMockito +import org.scalatest.{Outcome, fixture} +import scodec.bits._ + import scala.concurrent.Await -import scala.util.{Failure, Success} import scala.concurrent.duration._ +import scala.util.Success class EclairImplSpec extends TestKit(ActorSystem("mySystem")) with fixture.FunSuiteLike with IdiomaticMockito { @@ -156,6 +157,65 @@ class EclairImplSpec extends TestKit(ActorSystem("mySystem")) with fixture.FunSu }) } + test("get network info returns overall network statistics") { f => + import f._ + val (a, b, c, d, e) = (randomKey.publicKey, randomKey.publicKey, randomKey.publicKey, randomKey.publicKey, randomKey.publicKey) + val eclair = new EclairImpl(kit) + + // Method call being tested + val fResp = eclair.getNetworkInfoResponse() + + // Return dummy node announcement + f.router.expectMsg('nodes) + f.router.reply(Seq(makeNodeAnnouncement(randomKey, "node-A", Color(15, 10, -70), Nil))) + + // Return dummy channel information + f.router.expectMsg('channels) + val channelId_ab = ShortChannelId(420000, 1, 0) + val channelId_bc = ShortChannelId(420000, 2, 0) + val chan_ab = fr.acinq.eclair.router.BaseRouterSpec.channelAnnouncement(channelId_ab, randomKey, randomKey, randomKey, randomKey) + val chan_bc = fr.acinq.eclair.router.BaseRouterSpec.channelAnnouncement(channelId_bc, randomKey, randomKey, randomKey, randomKey) + f.router.reply(Seq(chan_ab, chan_bc)) + + // Return dummy updates + f.router.expectMsg('updates) + var updates = Seq( + makeUpdate(1L, a, b, feeBase = MilliSatoshi(0), 100, minHtlc = MilliSatoshi(10), maxHtlc = Some(MilliSatoshi(100L)), cltvDelta = 13)._2, + makeUpdate(4L, a, e, feeBase = MilliSatoshi(0), 0, minHtlc = MilliSatoshi(10), maxHtlc = Some(MilliSatoshi(100L)), cltvDelta = 12)._2, + makeUpdate(2L, b, c, feeBase = MilliSatoshi(2), 0, minHtlc = MilliSatoshi(10), maxHtlc = None, cltvDelta = 500)._2, + makeUpdate(3L, c, d, feeBase = MilliSatoshi(1), 0, minHtlc = MilliSatoshi(50), maxHtlc = None, cltvDelta = 500)._2, + makeUpdate(7L, e, c, feeBase = MilliSatoshi(2), 0, minHtlc = MilliSatoshi(50), maxHtlc = None, cltvDelta = 12)._2 + ) + val flagUpdate = makeUpdate(8L, e, b, feeBase = MilliSatoshi(2), 0, minHtlc = MilliSatoshi(50), maxHtlc = None, cltvDelta = 12)._2; + updates = updates :+ flagUpdate.copy(messageFlags = (-1).toByte, channelFlags = 7.toByte, htlcMaximumMsat = Some(MilliSatoshi(100L))) + updates = updates :+ flagUpdate.copy(messageFlags = (1).toByte, channelFlags = 1.toByte, htlcMaximumMsat = Some(MilliSatoshi(100L))) + f.router.reply(updates) + + // Expected results + // 4 updates have htlcMaximumMsat set so 0 -> 4 + // one update we set every flag to -1. So flages 1-7 are all 1 + val messageAnswer = FlagCounter(Map(0 -> 4, 1 -> 1, 2 -> 1, 3 -> 1, 4 -> 1, 5 -> 1, 6 -> 1, 7 -> 1)) + // one channel flag is 1 (00000001) and one is 7 (00000111) so 0->2 and 1-2 ->1 + val channelAnswer = FlagCounter(Map(0 -> 2, 1 -> 1, 2 -> 1, 3 -> 0, 4 -> 0, 5 -> 0, 6 -> 0, 7 -> 0)) + + awaitCond({ + fResp.value match { + case Some(Success(GetNetworkInfoResponse(totalchannelcount, totalNodes, totalUpdates, avgCltvExpiry, avgHtlcMinimumMsat, avgFeeBaseMsat, avgFeeProportionalMillionths, avgHtlcMaximumMsat, messageFlag, channelFlag))) => + (totalchannelcount == 2 && // we returned 2 channels + totalNodes == 1 && // we returned 1 node + totalUpdates == 7 && // were 7 updates + avgCltvExpiry == 151 && // avg(13,12,500,500,12,12,12) + avgHtlcMinimumMsat == 32 && // avg(10,10,10,50,50,50,50) + avgFeeBaseMsat == 1 && // avg(0,0,2,1,2,2,2) + avgFeeProportionalMillionths == 14 && // avg(100,0,0,0,0,0,0) + avgHtlcMaximumMsat == 57 && // avg(100,100,0,0,0,100,100) + messageFlag == messageAnswer && + channelFlag == channelAnswer) + case _ => false + } + }) + } + test("close and forceclose should work both with channelId and shortChannelId") { f => import f._