From 4b6fc328576838b6399683023ad6b5e53ef20532 Mon Sep 17 00:00:00 2001 From: rorp Date: Sun, 26 Sep 2021 15:32:33 -0700 Subject: [PATCH 01/11] Additional parameters for findroute* API calls --- .../main/scala/fr/acinq/eclair/Eclair.scala | 30 ++++++++++---- .../acinq/eclair/json/JsonSerializers.scala | 26 ++++++++----- .../api/directives/ExtraDirectives.scala | 5 ++- .../eclair/api/directives/RouteFormat.scala | 39 ++++++++++++------- .../eclair/api/handlers/PathFinding.scala | 21 +++++----- .../fr/acinq/eclair/api/ApiServiceSpec.scala | 25 +++++++++++- 6 files changed, 100 insertions(+), 46 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 fde825ac2e..b81ac7709d 100644 --- a/eclair-core/src/main/scala/fr/acinq/eclair/Eclair.scala +++ b/eclair-core/src/main/scala/fr/acinq/eclair/Eclair.scala @@ -49,6 +49,7 @@ import scodec.bits.ByteVector import java.nio.charset.StandardCharsets import java.util.UUID +import scala.collection.immutable.SortedMap import scala.concurrent.duration._ import scala.concurrent.{ExecutionContext, Future, Promise} import scala.reflect.ClassTag @@ -123,9 +124,9 @@ trait Eclair { def sendOnChain(address: String, amount: Satoshi, confirmationTarget: Long): Future[ByteVector32] - def findRoute(targetNodeId: PublicKey, amount: MilliSatoshi, pathFindingExperimentName_opt: Option[String], assistedRoutes: Seq[Seq[PaymentRequest.ExtraHop]] = Seq.empty, includeLocalChannelCost: Boolean = false)(implicit timeout: Timeout): Future[RouteResponse] + def findRoute(targetNodeId: PublicKey, amount: MilliSatoshi, pathFindingExperimentName_opt: Option[String], assistedRoutes: Seq[Seq[PaymentRequest.ExtraHop]] = Seq.empty, includeLocalChannelCost: Boolean = false, ignoredNodeIds: Seq[PublicKey] = Seq.empty, ignoredShortChannelIds: Seq[ShortChannelId] = Seq.empty, maxFee_opt: Option[MilliSatoshi] = None)(implicit timeout: Timeout): Future[RouteResponse] - def findRouteBetween(sourceNodeId: PublicKey, targetNodeId: PublicKey, amount: MilliSatoshi, pathFindingExperimentName_opt: Option[String], assistedRoutes: Seq[Seq[PaymentRequest.ExtraHop]] = Seq.empty, includeLocalChannelCost: Boolean = false)(implicit timeout: Timeout): Future[RouteResponse] + def findRouteBetween(sourceNodeId: PublicKey, targetNodeId: PublicKey, amount: MilliSatoshi, pathFindingExperimentName_opt: Option[String], assistedRoutes: Seq[Seq[PaymentRequest.ExtraHop]] = Seq.empty, includeLocalChannelCost: Boolean = false, ignoredNodeIds: Seq[PublicKey] = Seq.empty, ignoredShortChannelIds: Seq[ShortChannelId] = Seq.empty, maxFee_opt: Option[MilliSatoshi] = None)(implicit timeout: Timeout): Future[RouteResponse] def sendToRoute(amount: MilliSatoshi, recipientAmount_opt: Option[MilliSatoshi], externalId_opt: Option[String], parentId_opt: Option[UUID], invoice: PaymentRequest, finalCltvExpiryDelta: CltvExpiryDelta, route: PredefinedRoute, trampolineSecret_opt: Option[ByteVector32] = None, trampolineFees_opt: Option[MilliSatoshi] = None, trampolineExpiryDelta_opt: Option[CltvExpiryDelta] = None, trampolineNodes_opt: Seq[PublicKey] = Nil)(implicit timeout: Timeout): Future[SendPaymentToRouteResponse] @@ -280,8 +281,8 @@ class EclairImpl(appKit: Kit) extends Eclair with Logging { } } - override def findRoute(targetNodeId: PublicKey, amount: MilliSatoshi, pathFindingExperimentName_opt: Option[String], assistedRoutes: Seq[Seq[PaymentRequest.ExtraHop]] = Seq.empty, includeLocalChannelCost: Boolean = false)(implicit timeout: Timeout): Future[RouteResponse] = - findRouteBetween(appKit.nodeParams.nodeId, targetNodeId, amount, pathFindingExperimentName_opt, assistedRoutes, includeLocalChannelCost) + override def findRoute(targetNodeId: PublicKey, amount: MilliSatoshi, pathFindingExperimentName_opt: Option[String], assistedRoutes: Seq[Seq[PaymentRequest.ExtraHop]] = Seq.empty, includeLocalChannelCost: Boolean = false, ignoredNodeIds: Seq[PublicKey] = Seq.empty, ignoredShortChannelIds: Seq[ShortChannelId] = Seq.empty, maxFee_opt: Option[MilliSatoshi] = None)(implicit timeout: Timeout): Future[RouteResponse] = + findRouteBetween(appKit.nodeParams.nodeId, targetNodeId, amount, pathFindingExperimentName_opt, assistedRoutes, includeLocalChannelCost, ignoredNodeIds, ignoredShortChannelIds, maxFee_opt) private def getRouteParams(pathFindingExperimentName_opt: Option[String]): Either[IllegalArgumentException, RouteParams] = { pathFindingExperimentName_opt match { @@ -293,11 +294,15 @@ class EclairImpl(appKit: Kit) extends Eclair with Logging { } } - override def findRouteBetween(sourceNodeId: PublicKey, targetNodeId: PublicKey, amount: MilliSatoshi, pathFindingExperimentName_opt: Option[String], assistedRoutes: Seq[Seq[PaymentRequest.ExtraHop]] = Seq.empty, includeLocalChannelCost: Boolean = false)(implicit timeout: Timeout): Future[RouteResponse] = { + override def findRouteBetween(sourceNodeId: PublicKey, targetNodeId: PublicKey, amount: MilliSatoshi, pathFindingExperimentName_opt: Option[String], assistedRoutes: Seq[Seq[PaymentRequest.ExtraHop]] = Seq.empty, includeLocalChannelCost: Boolean = false, ignoredNodeIds: Seq[PublicKey] = Seq.empty, ignoredShortChannelIds: Seq[ShortChannelId] = Seq.empty, maxFee_opt: Option[MilliSatoshi] = None)(implicit timeout: Timeout): Future[RouteResponse] = { getRouteParams(pathFindingExperimentName_opt) match { case Right(routeParams) => - val maxFee = routeParams.getMaxFee(amount) - (appKit.router ? RouteRequest(sourceNodeId, targetNodeId, amount, maxFee, assistedRoutes, routeParams = routeParams.copy(includeLocalChannelCost = includeLocalChannelCost))).mapTo[RouteResponse] + val maxFee = maxFee_opt.getOrElse(routeParams.getMaxFee(amount)) + for { + ignoredChannels <- getChannelDescs(ignoredShortChannelIds.toSet) + ignore = Ignore(ignoredNodeIds.toSet, ignoredChannels) + response <- (appKit.router ? RouteRequest(sourceNodeId, targetNodeId, amount, maxFee, assistedRoutes, ignore = ignore, routeParams = routeParams.copy(includeLocalChannelCost = includeLocalChannelCost))).mapTo[RouteResponse] + } yield response case Left(t) => Future.failed(t) } } @@ -490,4 +495,15 @@ class EclairImpl(appKit: Kit) extends Eclair with Logging { val pubKeyFromSignature = Crypto.recoverPublicKey(signature, signedBytes, recoveryId) VerifiedMessage(valid = true, pubKeyFromSignature) } + + private def getChannelDescs(shortChannelIds: Set[ShortChannelId])(implicit timeout: Timeout): Future[Set[ChannelDesc]] = { + for { + channelsMap <- (appKit.router ? GetChannelsMap).mapTo[SortedMap[ShortChannelId, PublicChannel]] + } yield { + shortChannelIds.map { id => + val c = channelsMap.getOrElse(id, throw new IllegalArgumentException(s"unknown channel: $id")) + ChannelDesc(c.ann.shortChannelId, c.ann.nodeId1, c.ann.nodeId2) + } + } + } } diff --git a/eclair-core/src/main/scala/fr/acinq/eclair/json/JsonSerializers.scala b/eclair-core/src/main/scala/fr/acinq/eclair/json/JsonSerializers.scala index 8ba34d95df..111696521b 100644 --- a/eclair-core/src/main/scala/fr/acinq/eclair/json/JsonSerializers.scala +++ b/eclair-core/src/main/scala/fr/acinq/eclair/json/JsonSerializers.scala @@ -27,7 +27,7 @@ import fr.acinq.eclair.crypto.ShaChain import fr.acinq.eclair.db.FailureType.FailureType import fr.acinq.eclair.db.{IncomingPaymentStatus, OutgoingPaymentStatus} import fr.acinq.eclair.payment._ -import fr.acinq.eclair.router.Router.RouteResponse +import fr.acinq.eclair.router.Router.Route import fr.acinq.eclair.transactions.DirectedHtlc import fr.acinq.eclair.transactions.Transactions._ import fr.acinq.eclair.wire.protocol._ @@ -221,13 +221,19 @@ class ColorSerializer extends CustomSerializerOnly[Color](_ => { case c: Color => JString(c.toString) }) -class RouteResponseSerializer extends CustomSerializerOnly[RouteResponse](_ => { - case route: RouteResponse => - val nodeIds = route.routes.head.hops match { - case rest :+ last => rest.map(_.nodeId) :+ last.nodeId :+ last.nextNodeId - case Nil => Nil - } - JArray(nodeIds.toList.map(n => JString(n.toString))) +class RouteSerializer extends CustomSerializerOnly[Route](_ => { + case route: Route => + JObject( + ("amount", JLong(route.amount.toLong)), + ("hops", JArray(route.hops.map(hop => Extraction.decompose(hop)( + DefaultFormats + + new ByteVector32Serializer + + new ByteVectorSerializer + + new PublicKeySerializer + + new ShortChannelIdSerializer + + new MilliSatoshiSerializer + + new CltvExpiryDeltaSerializer + )).toList))) }) class ThrowableSerializer extends CustomSerializerOnly[Throwable](_ => { @@ -271,7 +277,7 @@ class PaymentRequestSerializer extends CustomSerializerOnly[PaymentRequest](_ => new ShortChannelIdSerializer + new MilliSatoshiSerializer + new CltvExpiryDeltaSerializer - ) + ) ) val fieldList = List(JField("prefix", JString(p.prefix)), JField("timestamp", JLong(p.timestamp)), @@ -424,7 +430,7 @@ object JsonSerializers { new CommandResponseSerializer + new InputInfoSerializer + new ColorSerializer + - new RouteResponseSerializer + + new RouteSerializer + new ThrowableSerializer + new FailureMessageSerializer + new FailureTypeSerializer + diff --git a/eclair-node/src/main/scala/fr/acinq/eclair/api/directives/ExtraDirectives.scala b/eclair-node/src/main/scala/fr/acinq/eclair/api/directives/ExtraDirectives.scala index bab2342a01..d2ed9821ab 100644 --- a/eclair-node/src/main/scala/fr/acinq/eclair/api/directives/ExtraDirectives.scala +++ b/eclair-node/src/main/scala/fr/acinq/eclair/api/directives/ExtraDirectives.scala @@ -46,7 +46,10 @@ trait ExtraDirectives extends Directives { val toFormParam: NameReceptacle[Long] = "to".as[Long] val amountMsatFormParam: NameReceptacle[MilliSatoshi] = "amountMsat".as[MilliSatoshi] val invoiceFormParam: NameReceptacle[PaymentRequest] = "invoice".as[PaymentRequest] - val routeFormat: NameUnmarshallerReceptacle[RouteFormat] = "format".as[RouteFormat](routeFormatUnmarshaller) + val routeFormatFormParam: NameUnmarshallerReceptacle[RouteFormat] = "format".as[RouteFormat](routeFormatUnmarshaller) + val ignoreNodeIdsFormParam: NameUnmarshallerReceptacle[List[PublicKey]] = "ignoreNodeIds".as[List[PublicKey]](pubkeyListUnmarshaller) + val ignoreChannelIdsFormParam: NameUnmarshallerReceptacle[List[ShortChannelId]] = "ignoreChannelIds".as[List[ShortChannelId]](shortChannelIdsUnmarshaller) + val maxFeeMsatFormParam: NameReceptacle[MilliSatoshi] = "maxFeeMsat".as[MilliSatoshi] // custom directive to fail with HTTP 404 (and JSON response) if the element was not found def completeOrNotFound[T](fut: Future[Option[T]])(implicit marshaller: ToResponseMarshaller[T]): Route = onComplete(fut) { diff --git a/eclair-node/src/main/scala/fr/acinq/eclair/api/directives/RouteFormat.scala b/eclair-node/src/main/scala/fr/acinq/eclair/api/directives/RouteFormat.scala index 60856683ca..89500ebc6e 100644 --- a/eclair-node/src/main/scala/fr/acinq/eclair/api/directives/RouteFormat.scala +++ b/eclair-node/src/main/scala/fr/acinq/eclair/api/directives/RouteFormat.scala @@ -16,38 +16,47 @@ package fr.acinq.eclair.api.directives +import akka.http.scaladsl.model.StatusCodes.OK +import akka.http.scaladsl.model.{ContentTypes, HttpResponse} +import fr.acinq.eclair.api.serde.JsonSupport._ import fr.acinq.eclair.router.Router.RouteResponse // @formatter:off sealed trait RouteFormat case object NodeIdRouteFormat extends RouteFormat case object ShortChannelIdRouteFormat extends RouteFormat +case object FullRouteFormat extends RouteFormat // @formatter:on object RouteFormat { val NODE_ID = "nodeId" val SHORT_CHANNEL_ID = "shortChannelId" + val FULL = "full" def fromString(s: String): RouteFormat = s match { case NODE_ID => NodeIdRouteFormat case SHORT_CHANNEL_ID => ShortChannelIdRouteFormat - case _ => throw new IllegalArgumentException(s"invalid route format, possible values are ($NODE_ID, $SHORT_CHANNEL_ID)") - } - - def format(route: RouteResponse, format_opt: Option[RouteFormat]): Seq[String] = format(route, format_opt.getOrElse(NodeIdRouteFormat)) - - def format(route: RouteResponse, format: RouteFormat): Seq[String] = format match { - case NodeIdRouteFormat => - val nodeIds = route.routes.head.hops match { - case rest :+ last => rest.map(_.nodeId) :+ last.nodeId :+ last.nextNodeId - case Nil => Nil - } - nodeIds.toList.map(_.toString) - case ShortChannelIdRouteFormat => - val shortChannelIds = route.routes.head.hops.map(_.lastUpdate.shortChannelId) - shortChannelIds.map(_.toString) + case FULL => FullRouteFormat + case _ => throw new IllegalArgumentException(s"invalid route format, possible values are ($NODE_ID, $SHORT_CHANNEL_ID, $FULL)") } + def format(route: RouteResponse, format_opt: Option[RouteFormat]): HttpResponse = format(route, format_opt.getOrElse(NodeIdRouteFormat)) + + def format(route: RouteResponse, format: RouteFormat): HttpResponse = + HttpResponse(OK).withEntity(ContentTypes.`application/json`, + format match { + case NodeIdRouteFormat => + val nodeIds = route.routes.head.hops match { + case rest :+ last => rest.map(_.nodeId) :+ last.nodeId :+ last.nextNodeId + case Nil => Nil + } + serialization.write(nodeIds.toList.map(_.toString)) + case ShortChannelIdRouteFormat => + val shortChannelIds = route.routes.head.hops.map(_.lastUpdate.shortChannelId) + serialization.write(shortChannelIds.toList.map(_.toString)) + case FullRouteFormat => + serialization.writePretty(route.routes) + }) } 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 1fad3a8da6..164d783456 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 @@ -33,11 +33,11 @@ trait PathFinding { private implicit def ec: ExecutionContext = actorSystem.dispatcher val findRoute: Route = postRequest("findroute") { implicit t => - formFields(invoiceFormParam, amountMsatFormParam.?, "pathFindingExperimentName".?, routeFormat.?, "includeLocalChannelCost".as[Boolean].?) { - case (invoice@PaymentRequest(_, Some(amount), _, nodeId, _, _), None, pathFindingExperimentName_opt, routeFormat, includeLocalChannelCost_opt) => - complete(eclairApi.findRoute(nodeId, amount, pathFindingExperimentName_opt, invoice.routingInfo, includeLocalChannelCost_opt.getOrElse(false)).map(r => RouteFormat.format(r, routeFormat))) - case (invoice, Some(overrideAmount), pathFindingExperimentName_opt, routeFormat, includeLocalChannelCost_opt) => - complete(eclairApi.findRoute(invoice.nodeId, overrideAmount, pathFindingExperimentName_opt, invoice.routingInfo, includeLocalChannelCost_opt.getOrElse(false)).map(r => RouteFormat.format(r, routeFormat))) + formFields(invoiceFormParam, amountMsatFormParam.?, "pathFindingExperimentName".?, routeFormatFormParam.?, "includeLocalChannelCost".as[Boolean].?, ignoreNodeIdsFormParam.?, ignoreChannelIdsFormParam.?, maxFeeMsatFormParam.?) { + case (invoice@PaymentRequest(_, Some(amount), _, nodeId, _, _), None, pathFindingExperimentName_opt, routeFormat_opt, includeLocalChannelCost_opt, ignoreNodeIds_opt, ignoreChannels_opt, maxFee_opt) => + complete(eclairApi.findRoute(nodeId, amount, pathFindingExperimentName_opt, invoice.routingInfo, includeLocalChannelCost_opt.getOrElse(false), ignoredNodeIds = ignoreNodeIds_opt.getOrElse(Nil), ignoredShortChannelIds = ignoreChannels_opt.getOrElse(Nil), maxFee_opt = maxFee_opt).map(r => RouteFormat.format(r, routeFormat_opt))) + case (invoice, Some(overrideAmount), pathFindingExperimentName_opt, routeFormat_opt, includeLocalChannelCost_opt, ignoreNodeIds_opt, ignoreChannels_opt, maxFee_opt) => + complete(eclairApi.findRoute(invoice.nodeId, overrideAmount, pathFindingExperimentName_opt, invoice.routingInfo, includeLocalChannelCost_opt.getOrElse(false), ignoredNodeIds = ignoreNodeIds_opt.getOrElse(Nil), ignoredShortChannelIds = ignoreChannels_opt.getOrElse(Nil), maxFee_opt = maxFee_opt).map(r => RouteFormat.format(r, routeFormat_opt))) case _ => reject(MalformedFormFieldRejection( "invoice", "The invoice must have an amount or you need to specify one using 'amountMsat'" )) @@ -45,16 +45,15 @@ trait PathFinding { } val findRouteToNode: Route = postRequest("findroutetonode") { implicit t => - formFields(nodeIdFormParam, amountMsatFormParam, "pathFindingExperimentName".?, routeFormat.?, "includeLocalChannelCost".as[Boolean].?) { - (nodeId, amount, pathFindingExperimentName_opt, routeFormat, includeLocalChannelCost_opt) => - complete(eclairApi.findRoute(nodeId, amount, pathFindingExperimentName_opt, includeLocalChannelCost = includeLocalChannelCost_opt.getOrElse(false)).map(r => RouteFormat.format(r, routeFormat))) + formFields(nodeIdFormParam, amountMsatFormParam, "pathFindingExperimentName".?, routeFormatFormParam.?, "includeLocalChannelCost".as[Boolean].?, ignoreNodeIdsFormParam.?, ignoreChannelIdsFormParam.?, maxFeeMsatFormParam.?) { + (nodeId, amount, pathFindingExperimentName_opt, routeFormat_opt, includeLocalChannelCost_opt, ignoreNodeIds_opt, ignoreChannels_opt, maxFee_opt) => + complete(eclairApi.findRoute(nodeId, amount, pathFindingExperimentName_opt, includeLocalChannelCost = includeLocalChannelCost_opt.getOrElse(false), ignoredNodeIds = ignoreNodeIds_opt.getOrElse(Nil), ignoredShortChannelIds = ignoreChannels_opt.getOrElse(Nil), maxFee_opt = maxFee_opt).map(r => RouteFormat.format(r, routeFormat_opt))) } } val findRouteBetweenNodes: Route = postRequest("findroutebetweennodes") { implicit t => - formFields("sourceNodeId".as[PublicKey], "targetNodeId".as[PublicKey], amountMsatFormParam, "pathFindingExperimentName".?, routeFormat.?, "includeLocalChannelCost".as[Boolean].?) { - (sourceNodeId, targetNodeId, amount, pathFindingExperimentName_opt, routeFormat, includeLocalChannelCost_opt) => - complete(eclairApi.findRouteBetween(sourceNodeId, targetNodeId, amount, pathFindingExperimentName_opt, includeLocalChannelCost = includeLocalChannelCost_opt.getOrElse(false)).map(r => RouteFormat.format(r, routeFormat))) + formFields("sourceNodeId".as[PublicKey], "targetNodeId".as[PublicKey], amountMsatFormParam, "pathFindingExperimentName".?, routeFormatFormParam.?, "includeLocalChannelCost".as[Boolean].?, ignoreNodeIdsFormParam.?, ignoreChannelIdsFormParam.?, maxFeeMsatFormParam.?) { (sourceNodeId, targetNodeId, amount, pathFindingExperimentName_opt, routeFormat_opt, includeLocalChannelCost_opt, ignoreNodeIds_opt, ignoreChannels_opt, maxFee_opt) => + complete(eclairApi.findRouteBetween(sourceNodeId, targetNodeId, amount, pathFindingExperimentName_opt, includeLocalChannelCost = includeLocalChannelCost_opt.getOrElse(false), ignoredNodeIds = ignoreNodeIds_opt.getOrElse(Nil), ignoredShortChannelIds = ignoreChannels_opt.getOrElse(Nil), maxFee_opt = maxFee_opt).map(r => RouteFormat.format(r, routeFormat_opt))) } } 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 9223f9e960..d4f8ce43b3 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.send.PaymentInitiator.SendPaymentToRouteResponse import fr.acinq.eclair.router.Router.PredefinedNodeRoute import fr.acinq.eclair.router.{NetworkStats, Router, Stats} import fr.acinq.eclair.wire.protocol.{ChannelUpdate, Color, NodeAddress} -import org.json4s.JsonAST.{JArray, JString} +import org.json4s.JsonAST.{JArray, JInt, JObject, JString} import org.mockito.scalatest.IdiomaticMockito import org.scalatest.funsuite.AnyFunSuite import org.scalatest.matchers.should.Matchers @@ -952,7 +952,7 @@ class ApiServiceSpec extends AnyFunSuite with ScalatestRouteTest with IdiomaticM } } - test("'findroute' method response should support both a node ID and channel ID formats") { + test("'findroute' method response should support nodeId, channelId and full formats") { val invoice = "lnbc12580n1pw2ywztpp554ganw404sh4yjkwnysgn3wjcxfcq7gtx53gxczkjr9nlpc3hzvqdq2wpskwctddyxqr4rqrzjqwryaup9lh50kkranzgcdnn2fgvx390wgj5jd07rwr3vxeje0glc7z9rtvqqwngqqqqqqqlgqqqqqeqqjqrrt8smgjvfj7sg38dwtr9kc9gg3era9k3t2hvq3cup0jvsrtrxuplevqgfhd3rzvhulgcxj97yjuj8gdx8mllwj4wzjd8gdjhpz3lpqqvk2plh" @@ -1039,6 +1039,27 @@ class ApiServiceSpec extends AnyFunSuite with ScalatestRouteTest with IdiomaticM ))) eclair.findRoute(PublicKey.fromBin(ByteVector.fromValidHex("036ded9bb8175d0c9fd3fad145965cf5005ec599570f35c682e710dc6001ff605e")), 456.msat, any, any)(any[Timeout]).wasCalled(threeTimes) } + Post("/findroute", FormData("format" -> "full", "invoice" -> invoice, "amountMsat" -> "456")) ~> + addCredentials(BasicHttpCredentials("", mockApi().password)) ~> + addHeader("Content-Type", "application/json") ~> + Route.seal(mockService.findRoute) ~> + check { + assert(handled) + assert(status == OK) + val responseArray = entityAs[JArray](Json4sSupport.unmarshaller, ClassTag(classOf[ErrorResponse])) + assert(responseArray.arr.size == 1) + assert(responseArray.arr.head.isInstanceOf[JObject]) + val route = responseArray.arr.head.asInstanceOf[JObject] + assert(route.obj.head == ("amount", JInt(456))) + assert(route.obj.last._1 == "hops") + val hops = route.obj.last._2.asInstanceOf[JArray] + assert(hops.arr.size == 3) + val (hop1 :: hop2 :: hop3 :: Nil) = hops.arr + assert(hop1.asInstanceOf[JObject].obj.head == ("nodeId", JString(mockHop1.nodeId.toString()))) + assert(hop2.asInstanceOf[JObject].obj.head == ("nodeId", JString(mockHop2.nodeId.toString()))) + assert(hop3.asInstanceOf[JObject].obj.head == ("nodeId", JString(mockHop3.nodeId.toString()))) + eclair.findRoute(PublicKey.fromBin(ByteVector.fromValidHex("036ded9bb8175d0c9fd3fad145965cf5005ec599570f35c682e710dc6001ff605e")), 456.msat, any, any)(any[Timeout]).wasCalled(fourTimes) + } } test("'networkstats' response should return expected statistics") { From ac52380dc9aa9c2839c7ca2fd1328ec4b4a34671 Mon Sep 17 00:00:00 2001 From: rorp Date: Thu, 30 Sep 2021 19:57:51 -0700 Subject: [PATCH 02/11] fix tests --- eclair-node/src/test/resources/api/findroute | 73 +++++++++++++++++++ .../fr/acinq/eclair/api/ApiServiceSpec.scala | 44 ++++++----- 2 files changed, 98 insertions(+), 19 deletions(-) create mode 100644 eclair-node/src/test/resources/api/findroute diff --git a/eclair-node/src/test/resources/api/findroute b/eclair-node/src/test/resources/api/findroute new file mode 100644 index 0000000000..bec34bda58 --- /dev/null +++ b/eclair-node/src/test/resources/api/findroute @@ -0,0 +1,73 @@ +[ { + "amount" : 456, + "hops" : [ { + "nodeId" : "03007e67dc5a8fd2b2ef21cb310ab6359ddb51f3f86a8b79b8b1e23bc3a6ea150a", + "nextNodeId" : "026105f6cb4862810be989385d16f04b0f748f6f2a14040338b1a534d45b4be1c1", + "lastUpdate" : { + "signature" : { + "bytes" : "92cf3f12e161391986eb2cd7106ddab41a23c734f8f1ed120fb64f4b91f98f690ecf930388e62965f8aefbf1adafcd25a572669a125396dcfb83615208754679" + }, + "chainHash" : "024b7b3626554c44dcc2454ee3812458bfa68d9fced466edfab470844cb7ffe2", + "shortChannelId" : "1x2x3", + "timestamp" : 0, + "channelFlags" : { + "isEnabled" : true, + "isNode1" : true + }, + "cltvExpiryDelta" : 0, + "htlcMinimumMsat" : 1, + "feeBaseMsat" : 1, + "feeProportionalMillionths" : 1, + "tlvStream" : { + "records" : [ ], + "unknown" : [ ] + } + } + }, { + "nodeId" : "026105f6cb4862810be989385d16f04b0f748f6f2a14040338b1a534d45b4be1c1", + "nextNodeId" : "038cfa2b5857843ee90cff91b06f692c0d8fe201921ee6387aee901d64f43699f0", + "lastUpdate" : { + "signature" : { + "bytes" : "92cf3f12e161391986eb2cd7106ddab41a23c734f8f1ed120fb64f4b91f98f690ecf930388e62965f8aefbf1adafcd25a572669a125396dcfb83615208754679" + }, + "chainHash" : "024b7b3626554c44dcc2454ee3812458bfa68d9fced466edfab470844cb7ffe2", + "shortChannelId" : "1x2x4", + "timestamp" : 0, + "channelFlags" : { + "isEnabled" : true, + "isNode1" : true + }, + "cltvExpiryDelta" : 0, + "htlcMinimumMsat" : 1, + "feeBaseMsat" : 1, + "feeProportionalMillionths" : 1, + "tlvStream" : { + "records" : [ ], + "unknown" : [ ] + } + } + }, { + "nodeId" : "038cfa2b5857843ee90cff91b06f692c0d8fe201921ee6387aee901d64f43699f0", + "nextNodeId" : "02be60276e294c6921240daae33a361d214d02578656df0e74c61a09c3196e51df", + "lastUpdate" : { + "signature" : { + "bytes" : "92cf3f12e161391986eb2cd7106ddab41a23c734f8f1ed120fb64f4b91f98f690ecf930388e62965f8aefbf1adafcd25a572669a125396dcfb83615208754679" + }, + "chainHash" : "024b7b3626554c44dcc2454ee3812458bfa68d9fced466edfab470844cb7ffe2", + "shortChannelId" : "1x2x5", + "timestamp" : 0, + "channelFlags" : { + "isEnabled" : true, + "isNode1" : true + }, + "cltvExpiryDelta" : 0, + "htlcMinimumMsat" : 1, + "feeBaseMsat" : 1, + "feeProportionalMillionths" : 1, + "tlvStream" : { + "records" : [ ], + "unknown" : [ ] + } + } + } ] +} ] \ No newline at end of file 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 d4f8ce43b3..d267138a94 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 @@ -25,7 +25,7 @@ import akka.http.scaladsl.testkit.{RouteTestTimeout, ScalatestRouteTest, WSProbe import akka.util.Timeout import de.heikoseeberger.akkahttpjson4s.Json4sSupport import fr.acinq.bitcoin.Crypto.PublicKey -import fr.acinq.bitcoin.{Block, ByteVector32, SatoshiLong} +import fr.acinq.bitcoin.{Block, ByteVector32, ByteVector64, SatoshiLong} import fr.acinq.eclair.ApiTypes.ChannelIdentifier import fr.acinq.eclair.FeatureSupport.{Mandatory, Optional} import fr.acinq.eclair.Features.{ChannelRangeQueriesExtended, OptionDataLossProtect} @@ -957,8 +957,8 @@ class ApiServiceSpec extends AnyFunSuite with ScalatestRouteTest with IdiomaticM val mockChannelUpdate1 = ChannelUpdate( - signature = randomBytes64(), - chainHash = randomBytes32(), + signature = ByteVector64.fromValidHex("92cf3f12e161391986eb2cd7106ddab41a23c734f8f1ed120fb64f4b91f98f690ecf930388e62965f8aefbf1adafcd25a572669a125396dcfb83615208754679"), + chainHash = ByteVector32.fromValidHex("024b7b3626554c44dcc2454ee3812458bfa68d9fced466edfab470844cb7ffe2"), shortChannelId = ShortChannelId(1, 2, 3), timestamp = 0, channelFlags = ChannelUpdate.ChannelFlags.DUMMY, @@ -970,11 +970,11 @@ class ApiServiceSpec extends AnyFunSuite with ScalatestRouteTest with IdiomaticM ) val mockHop1 = - Router.ChannelHop(nodeId = randomKey().publicKey, nextNodeId = randomKey().publicKey, mockChannelUpdate1) + Router.ChannelHop(nodeId = PublicKey.fromBin(ByteVector.fromValidHex("03007e67dc5a8fd2b2ef21cb310ab6359ddb51f3f86a8b79b8b1e23bc3a6ea150a")), nextNodeId = PublicKey.fromBin(ByteVector.fromValidHex("026105f6cb4862810be989385d16f04b0f748f6f2a14040338b1a534d45b4be1c1")), mockChannelUpdate1) val mockHop2 = - Router.ChannelHop(nodeId = mockHop1.nextNodeId, nextNodeId = randomKey().publicKey, mockChannelUpdate1.copy(shortChannelId = ShortChannelId(1, 2, 4))) + Router.ChannelHop(nodeId = mockHop1.nextNodeId, nextNodeId = PublicKey.fromBin(ByteVector.fromValidHex("038cfa2b5857843ee90cff91b06f692c0d8fe201921ee6387aee901d64f43699f0")), mockChannelUpdate1.copy(shortChannelId = ShortChannelId(1, 2, 4))) val mockHop3 = - Router.ChannelHop(nodeId = mockHop2.nextNodeId, nextNodeId = randomKey().publicKey, mockChannelUpdate1.copy(shortChannelId = ShortChannelId(1, 2, 5))) + Router.ChannelHop(nodeId = mockHop2.nextNodeId, nextNodeId = PublicKey.fromBin(ByteVector.fromValidHex("02be60276e294c6921240daae33a361d214d02578656df0e74c61a09c3196e51df")), mockChannelUpdate1.copy(shortChannelId = ShortChannelId(1, 2, 5))) val mockHops = Seq(mockHop1, mockHop2, mockHop3) val eclair = mock[Eclair] @@ -1039,6 +1039,7 @@ class ApiServiceSpec extends AnyFunSuite with ScalatestRouteTest with IdiomaticM ))) eclair.findRoute(PublicKey.fromBin(ByteVector.fromValidHex("036ded9bb8175d0c9fd3fad145965cf5005ec599570f35c682e710dc6001ff605e")), 456.msat, any, any)(any[Timeout]).wasCalled(threeTimes) } + Post("/findroute", FormData("format" -> "full", "invoice" -> invoice, "amountMsat" -> "456")) ~> addCredentials(BasicHttpCredentials("", mockApi().password)) ~> addHeader("Content-Type", "application/json") ~> @@ -1046,19 +1047,24 @@ class ApiServiceSpec extends AnyFunSuite with ScalatestRouteTest with IdiomaticM check { assert(handled) assert(status == OK) - val responseArray = entityAs[JArray](Json4sSupport.unmarshaller, ClassTag(classOf[ErrorResponse])) - assert(responseArray.arr.size == 1) - assert(responseArray.arr.head.isInstanceOf[JObject]) - val route = responseArray.arr.head.asInstanceOf[JObject] - assert(route.obj.head == ("amount", JInt(456))) - assert(route.obj.last._1 == "hops") - val hops = route.obj.last._2.asInstanceOf[JArray] - assert(hops.arr.size == 3) - val (hop1 :: hop2 :: hop3 :: Nil) = hops.arr - assert(hop1.asInstanceOf[JObject].obj.head == ("nodeId", JString(mockHop1.nodeId.toString()))) - assert(hop2.asInstanceOf[JObject].obj.head == ("nodeId", JString(mockHop2.nodeId.toString()))) - assert(hop3.asInstanceOf[JObject].obj.head == ("nodeId", JString(mockHop3.nodeId.toString()))) - eclair.findRoute(PublicKey.fromBin(ByteVector.fromValidHex("036ded9bb8175d0c9fd3fad145965cf5005ec599570f35c682e710dc6001ff605e")), 456.msat, any, any)(any[Timeout]).wasCalled(fourTimes) + + val response = entityAs[String] + matchTestJson("findroute", response) + + +// val responseArray = entityAs[JArray](Json4sSupport.unmarshaller, ClassTag(classOf[ErrorResponse])) +// assert(responseArray.arr.size == 1) +// assert(responseArray.arr.head.isInstanceOf[JObject]) +// val route = responseArray.arr.head.asInstanceOf[JObject] +// assert(route.obj.head == ("amount", JInt(456))) +// assert(route.obj.last._1 == "hops") +// val hops = route.obj.last._2.asInstanceOf[JArray] +// assert(hops.arr.size == 3) +// val (hop1 :: hop2 :: hop3 :: Nil) = hops.arr +// assert(hop1.asInstanceOf[JObject].obj.head == ("nodeId", JString(mockHop1.nodeId.toString()))) +// assert(hop2.asInstanceOf[JObject].obj.head == ("nodeId", JString(mockHop2.nodeId.toString()))) +// assert(hop3.asInstanceOf[JObject].obj.head == ("nodeId", JString(mockHop3.nodeId.toString()))) +// eclair.findRoute(PublicKey.fromBin(ByteVector.fromValidHex("036ded9bb8175d0c9fd3fad145965cf5005ec599570f35c682e710dc6001ff605e")), 456.msat, any, any)(any[Timeout]).wasCalled(fourTimes) } } From 019e864679c03a5750899066d30cda95953bfea9 Mon Sep 17 00:00:00 2001 From: rorp Date: Thu, 30 Sep 2021 20:10:07 -0700 Subject: [PATCH 03/11] Rename the method parameters --- .../src/main/scala/fr/acinq/eclair/Eclair.scala | 14 +++++++------- 1 file changed, 7 insertions(+), 7 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 b81ac7709d..0d475deb99 100644 --- a/eclair-core/src/main/scala/fr/acinq/eclair/Eclair.scala +++ b/eclair-core/src/main/scala/fr/acinq/eclair/Eclair.scala @@ -124,9 +124,9 @@ trait Eclair { def sendOnChain(address: String, amount: Satoshi, confirmationTarget: Long): Future[ByteVector32] - def findRoute(targetNodeId: PublicKey, amount: MilliSatoshi, pathFindingExperimentName_opt: Option[String], assistedRoutes: Seq[Seq[PaymentRequest.ExtraHop]] = Seq.empty, includeLocalChannelCost: Boolean = false, ignoredNodeIds: Seq[PublicKey] = Seq.empty, ignoredShortChannelIds: Seq[ShortChannelId] = Seq.empty, maxFee_opt: Option[MilliSatoshi] = None)(implicit timeout: Timeout): Future[RouteResponse] + def findRoute(targetNodeId: PublicKey, amount: MilliSatoshi, pathFindingExperimentName_opt: Option[String], assistedRoutes: Seq[Seq[PaymentRequest.ExtraHop]] = Seq.empty, includeLocalChannelCost: Boolean = false, ignoreNodeIds: Seq[PublicKey] = Seq.empty, ignoreShortChannelIds: Seq[ShortChannelId] = Seq.empty, maxFee_opt: Option[MilliSatoshi] = None)(implicit timeout: Timeout): Future[RouteResponse] - def findRouteBetween(sourceNodeId: PublicKey, targetNodeId: PublicKey, amount: MilliSatoshi, pathFindingExperimentName_opt: Option[String], assistedRoutes: Seq[Seq[PaymentRequest.ExtraHop]] = Seq.empty, includeLocalChannelCost: Boolean = false, ignoredNodeIds: Seq[PublicKey] = Seq.empty, ignoredShortChannelIds: Seq[ShortChannelId] = Seq.empty, maxFee_opt: Option[MilliSatoshi] = None)(implicit timeout: Timeout): Future[RouteResponse] + def findRouteBetween(sourceNodeId: PublicKey, targetNodeId: PublicKey, amount: MilliSatoshi, pathFindingExperimentName_opt: Option[String], assistedRoutes: Seq[Seq[PaymentRequest.ExtraHop]] = Seq.empty, includeLocalChannelCost: Boolean = false, ignoreNodeIds: Seq[PublicKey] = Seq.empty, ignoreShortChannelIds: Seq[ShortChannelId] = Seq.empty, maxFee_opt: Option[MilliSatoshi] = None)(implicit timeout: Timeout): Future[RouteResponse] def sendToRoute(amount: MilliSatoshi, recipientAmount_opt: Option[MilliSatoshi], externalId_opt: Option[String], parentId_opt: Option[UUID], invoice: PaymentRequest, finalCltvExpiryDelta: CltvExpiryDelta, route: PredefinedRoute, trampolineSecret_opt: Option[ByteVector32] = None, trampolineFees_opt: Option[MilliSatoshi] = None, trampolineExpiryDelta_opt: Option[CltvExpiryDelta] = None, trampolineNodes_opt: Seq[PublicKey] = Nil)(implicit timeout: Timeout): Future[SendPaymentToRouteResponse] @@ -281,8 +281,8 @@ class EclairImpl(appKit: Kit) extends Eclair with Logging { } } - override def findRoute(targetNodeId: PublicKey, amount: MilliSatoshi, pathFindingExperimentName_opt: Option[String], assistedRoutes: Seq[Seq[PaymentRequest.ExtraHop]] = Seq.empty, includeLocalChannelCost: Boolean = false, ignoredNodeIds: Seq[PublicKey] = Seq.empty, ignoredShortChannelIds: Seq[ShortChannelId] = Seq.empty, maxFee_opt: Option[MilliSatoshi] = None)(implicit timeout: Timeout): Future[RouteResponse] = - findRouteBetween(appKit.nodeParams.nodeId, targetNodeId, amount, pathFindingExperimentName_opt, assistedRoutes, includeLocalChannelCost, ignoredNodeIds, ignoredShortChannelIds, maxFee_opt) + override def findRoute(targetNodeId: PublicKey, amount: MilliSatoshi, pathFindingExperimentName_opt: Option[String], assistedRoutes: Seq[Seq[PaymentRequest.ExtraHop]] = Seq.empty, includeLocalChannelCost: Boolean = false, ignoreNodeIds: Seq[PublicKey] = Seq.empty, ignoreShortChannelIds: Seq[ShortChannelId] = Seq.empty, maxFee_opt: Option[MilliSatoshi] = None)(implicit timeout: Timeout): Future[RouteResponse] = + findRouteBetween(appKit.nodeParams.nodeId, targetNodeId, amount, pathFindingExperimentName_opt, assistedRoutes, includeLocalChannelCost, ignoreNodeIds, ignoreShortChannelIds, maxFee_opt) private def getRouteParams(pathFindingExperimentName_opt: Option[String]): Either[IllegalArgumentException, RouteParams] = { pathFindingExperimentName_opt match { @@ -294,13 +294,13 @@ class EclairImpl(appKit: Kit) extends Eclair with Logging { } } - override def findRouteBetween(sourceNodeId: PublicKey, targetNodeId: PublicKey, amount: MilliSatoshi, pathFindingExperimentName_opt: Option[String], assistedRoutes: Seq[Seq[PaymentRequest.ExtraHop]] = Seq.empty, includeLocalChannelCost: Boolean = false, ignoredNodeIds: Seq[PublicKey] = Seq.empty, ignoredShortChannelIds: Seq[ShortChannelId] = Seq.empty, maxFee_opt: Option[MilliSatoshi] = None)(implicit timeout: Timeout): Future[RouteResponse] = { + override def findRouteBetween(sourceNodeId: PublicKey, targetNodeId: PublicKey, amount: MilliSatoshi, pathFindingExperimentName_opt: Option[String], assistedRoutes: Seq[Seq[PaymentRequest.ExtraHop]] = Seq.empty, includeLocalChannelCost: Boolean = false, ignoreNodeIds: Seq[PublicKey] = Seq.empty, ignoreShortChannelIds: Seq[ShortChannelId] = Seq.empty, maxFee_opt: Option[MilliSatoshi] = None)(implicit timeout: Timeout): Future[RouteResponse] = { getRouteParams(pathFindingExperimentName_opt) match { case Right(routeParams) => val maxFee = maxFee_opt.getOrElse(routeParams.getMaxFee(amount)) for { - ignoredChannels <- getChannelDescs(ignoredShortChannelIds.toSet) - ignore = Ignore(ignoredNodeIds.toSet, ignoredChannels) + ignoredChannels <- getChannelDescs(ignoreShortChannelIds.toSet) + ignore = Ignore(ignoreNodeIds.toSet, ignoredChannels) response <- (appKit.router ? RouteRequest(sourceNodeId, targetNodeId, amount, maxFee, assistedRoutes, ignore = ignore, routeParams = routeParams.copy(includeLocalChannelCost = includeLocalChannelCost))).mapTo[RouteResponse] } yield response case Left(t) => Future.failed(t) From 6a0b177f70e74d311cc8602e2c87461fdcdd3899 Mon Sep 17 00:00:00 2001 From: rorp Date: Thu, 30 Sep 2021 20:32:51 -0700 Subject: [PATCH 04/11] fix build --- .../scala/fr/acinq/eclair/api/handlers/PathFinding.scala | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) 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 164d783456..6058480062 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 @@ -35,9 +35,9 @@ trait PathFinding { val findRoute: Route = postRequest("findroute") { implicit t => formFields(invoiceFormParam, amountMsatFormParam.?, "pathFindingExperimentName".?, routeFormatFormParam.?, "includeLocalChannelCost".as[Boolean].?, ignoreNodeIdsFormParam.?, ignoreChannelIdsFormParam.?, maxFeeMsatFormParam.?) { case (invoice@PaymentRequest(_, Some(amount), _, nodeId, _, _), None, pathFindingExperimentName_opt, routeFormat_opt, includeLocalChannelCost_opt, ignoreNodeIds_opt, ignoreChannels_opt, maxFee_opt) => - complete(eclairApi.findRoute(nodeId, amount, pathFindingExperimentName_opt, invoice.routingInfo, includeLocalChannelCost_opt.getOrElse(false), ignoredNodeIds = ignoreNodeIds_opt.getOrElse(Nil), ignoredShortChannelIds = ignoreChannels_opt.getOrElse(Nil), maxFee_opt = maxFee_opt).map(r => RouteFormat.format(r, routeFormat_opt))) + complete(eclairApi.findRoute(nodeId, amount, pathFindingExperimentName_opt, invoice.routingInfo, includeLocalChannelCost_opt.getOrElse(false), ignoreNodeIds = ignoreNodeIds_opt.getOrElse(Nil), ignoreShortChannelIds = ignoreChannels_opt.getOrElse(Nil), maxFee_opt = maxFee_opt).map(r => RouteFormat.format(r, routeFormat_opt))) case (invoice, Some(overrideAmount), pathFindingExperimentName_opt, routeFormat_opt, includeLocalChannelCost_opt, ignoreNodeIds_opt, ignoreChannels_opt, maxFee_opt) => - complete(eclairApi.findRoute(invoice.nodeId, overrideAmount, pathFindingExperimentName_opt, invoice.routingInfo, includeLocalChannelCost_opt.getOrElse(false), ignoredNodeIds = ignoreNodeIds_opt.getOrElse(Nil), ignoredShortChannelIds = ignoreChannels_opt.getOrElse(Nil), maxFee_opt = maxFee_opt).map(r => RouteFormat.format(r, routeFormat_opt))) + complete(eclairApi.findRoute(invoice.nodeId, overrideAmount, pathFindingExperimentName_opt, invoice.routingInfo, includeLocalChannelCost_opt.getOrElse(false), ignoreNodeIds = ignoreNodeIds_opt.getOrElse(Nil), ignoreShortChannelIds = ignoreChannels_opt.getOrElse(Nil), maxFee_opt = maxFee_opt).map(r => RouteFormat.format(r, routeFormat_opt))) case _ => reject(MalformedFormFieldRejection( "invoice", "The invoice must have an amount or you need to specify one using 'amountMsat'" )) @@ -47,13 +47,13 @@ trait PathFinding { val findRouteToNode: Route = postRequest("findroutetonode") { implicit t => formFields(nodeIdFormParam, amountMsatFormParam, "pathFindingExperimentName".?, routeFormatFormParam.?, "includeLocalChannelCost".as[Boolean].?, ignoreNodeIdsFormParam.?, ignoreChannelIdsFormParam.?, maxFeeMsatFormParam.?) { (nodeId, amount, pathFindingExperimentName_opt, routeFormat_opt, includeLocalChannelCost_opt, ignoreNodeIds_opt, ignoreChannels_opt, maxFee_opt) => - complete(eclairApi.findRoute(nodeId, amount, pathFindingExperimentName_opt, includeLocalChannelCost = includeLocalChannelCost_opt.getOrElse(false), ignoredNodeIds = ignoreNodeIds_opt.getOrElse(Nil), ignoredShortChannelIds = ignoreChannels_opt.getOrElse(Nil), maxFee_opt = maxFee_opt).map(r => RouteFormat.format(r, routeFormat_opt))) + complete(eclairApi.findRoute(nodeId, amount, pathFindingExperimentName_opt, includeLocalChannelCost = includeLocalChannelCost_opt.getOrElse(false), ignoreNodeIds = ignoreNodeIds_opt.getOrElse(Nil), ignoreShortChannelIds = ignoreChannels_opt.getOrElse(Nil), maxFee_opt = maxFee_opt).map(r => RouteFormat.format(r, routeFormat_opt))) } } val findRouteBetweenNodes: Route = postRequest("findroutebetweennodes") { implicit t => formFields("sourceNodeId".as[PublicKey], "targetNodeId".as[PublicKey], amountMsatFormParam, "pathFindingExperimentName".?, routeFormatFormParam.?, "includeLocalChannelCost".as[Boolean].?, ignoreNodeIdsFormParam.?, ignoreChannelIdsFormParam.?, maxFeeMsatFormParam.?) { (sourceNodeId, targetNodeId, amount, pathFindingExperimentName_opt, routeFormat_opt, includeLocalChannelCost_opt, ignoreNodeIds_opt, ignoreChannels_opt, maxFee_opt) => - complete(eclairApi.findRouteBetween(sourceNodeId, targetNodeId, amount, pathFindingExperimentName_opt, includeLocalChannelCost = includeLocalChannelCost_opt.getOrElse(false), ignoredNodeIds = ignoreNodeIds_opt.getOrElse(Nil), ignoredShortChannelIds = ignoreChannels_opt.getOrElse(Nil), maxFee_opt = maxFee_opt).map(r => RouteFormat.format(r, routeFormat_opt))) + complete(eclairApi.findRouteBetween(sourceNodeId, targetNodeId, amount, pathFindingExperimentName_opt, includeLocalChannelCost = includeLocalChannelCost_opt.getOrElse(false), ignoreNodeIds = ignoreNodeIds_opt.getOrElse(Nil), ignoreShortChannelIds = ignoreChannels_opt.getOrElse(Nil), maxFee_opt = maxFee_opt).map(r => RouteFormat.format(r, routeFormat_opt))) } } From 35ce3f0180a1c33f9bdc80761f924a0bb9e8e08b Mon Sep 17 00:00:00 2001 From: rorp Date: Sun, 3 Oct 2021 11:15:19 -0700 Subject: [PATCH 05/11] update release notes --- docs/release-notes/eclair-vnext.md | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/docs/release-notes/eclair-vnext.md b/docs/release-notes/eclair-vnext.md index e129106981..c4ea984b42 100644 --- a/docs/release-notes/eclair-vnext.md +++ b/docs/release-notes/eclair-vnext.md @@ -137,8 +137,11 @@ This release contains many API updates: - `sendtonode` doesn't support providing a `paymentHash` anymore since it uses `keysend` to send the payment (#1840) - `payinvoice`, `sendtonode`, `findroute`, `findroutetonode` and `findroutebetweennodes` let you specify `--pathFindingExperimentName` when using path-finding A/B testing (#1930) - the `--maxFeePct` parameter used in `payinvoice` and `sendtonode` must now be an integer between 0 and 100: it was previously a value between 0 and 1, which was misleading for a percentage (#1930) -- `findroute`, `findroutetonode` and `findroutebetweennodes` let you choose the format of the route returned with the `--routeFormat` parameter (supported values are `nodeId` and `shortChannelId`) (#1943) +- `findroute`, `findroutetonode` and `findroutebetweennodes` let you choose the format of the route returned with the `--routeFormat` parameter (supported values are `nodeId`, `shortChannelId` and `full`) (#1943, #1969) - `findroute`, `findroutetonode` and `findroutebetweennodes` now accept `--includeLocalChannelCost` to specify if you want to count the fees from your node like trampoline payments do (#1942) +- `findroute`, `findroutetonode` and `findroutebetweennodes` now accept `--ignoreNodeIds` to specify nodes you want to be ignored in path-finding (#1969) +- `findroute`, `findroutetonode` and `findroutebetweennodes` now accept `--ignoreChannelIds` to specify channels you want to be ignored in path-finding (#1969) +- `findroute`, `findroutetonode` and `findroutebetweennodes` now accept `--maxFeeMsat` to specify an upper bound of fees (#1969) Have a look at our [API documentation](https://acinq.github.io/eclair) for more details. From d31e6d20ab567e3532950b626e991ac31df970f0 Mon Sep 17 00:00:00 2001 From: rorp Date: Sun, 3 Oct 2021 11:21:48 -0700 Subject: [PATCH 06/11] cleanup --- .../src/main/scala/fr/acinq/eclair/Eclair.scala | 16 ++++++++++------ .../fr/acinq/eclair/api/ApiServiceSpec.scala | 17 +---------------- 2 files changed, 11 insertions(+), 22 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 0d475deb99..661c67467f 100644 --- a/eclair-core/src/main/scala/fr/acinq/eclair/Eclair.scala +++ b/eclair-core/src/main/scala/fr/acinq/eclair/Eclair.scala @@ -497,12 +497,16 @@ class EclairImpl(appKit: Kit) extends Eclair with Logging { } private def getChannelDescs(shortChannelIds: Set[ShortChannelId])(implicit timeout: Timeout): Future[Set[ChannelDesc]] = { - for { - channelsMap <- (appKit.router ? GetChannelsMap).mapTo[SortedMap[ShortChannelId, PublicChannel]] - } yield { - shortChannelIds.map { id => - val c = channelsMap.getOrElse(id, throw new IllegalArgumentException(s"unknown channel: $id")) - ChannelDesc(c.ann.shortChannelId, c.ann.nodeId1, c.ann.nodeId2) + if (shortChannelIds.isEmpty){ + Future.successful(Set.empty) + } else { + for { + channelsMap <- (appKit.router ? GetChannelsMap).mapTo[SortedMap[ShortChannelId, PublicChannel]] + } yield { + shortChannelIds.map { id => + val c = channelsMap.getOrElse(id, throw new IllegalArgumentException(s"unknown channel: $id")) + ChannelDesc(c.ann.shortChannelId, c.ann.nodeId1, c.ann.nodeId2) + } } } } 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 5df9242174..1d1382c2f8 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.send.PaymentInitiator.SendPaymentToRouteResponse import fr.acinq.eclair.router.Router.PredefinedNodeRoute import fr.acinq.eclair.router.{NetworkStats, Router, Stats} import fr.acinq.eclair.wire.protocol.{ChannelUpdate, Color, NodeAddress} -import org.json4s.JsonAST.{JArray, JInt, JObject, JString} +import org.json4s.JsonAST.{JArray, JString} import org.mockito.scalatest.IdiomaticMockito import org.scalatest.funsuite.AnyFunSuite import org.scalatest.matchers.should.Matchers @@ -1064,21 +1064,6 @@ class ApiServiceSpec extends AnyFunSuite with ScalatestRouteTest with IdiomaticM val response = entityAs[String] matchTestJson("findroute", response) - - -// val responseArray = entityAs[JArray](Json4sSupport.unmarshaller, ClassTag(classOf[ErrorResponse])) -// assert(responseArray.arr.size == 1) -// assert(responseArray.arr.head.isInstanceOf[JObject]) -// val route = responseArray.arr.head.asInstanceOf[JObject] -// assert(route.obj.head == ("amount", JInt(456))) -// assert(route.obj.last._1 == "hops") -// val hops = route.obj.last._2.asInstanceOf[JArray] -// assert(hops.arr.size == 3) -// val (hop1 :: hop2 :: hop3 :: Nil) = hops.arr -// assert(hop1.asInstanceOf[JObject].obj.head == ("nodeId", JString(mockHop1.nodeId.toString()))) -// assert(hop2.asInstanceOf[JObject].obj.head == ("nodeId", JString(mockHop2.nodeId.toString()))) -// assert(hop3.asInstanceOf[JObject].obj.head == ("nodeId", JString(mockHop3.nodeId.toString()))) -// eclair.findRoute(PublicKey.fromBin(ByteVector.fromValidHex("036ded9bb8175d0c9fd3fad145965cf5005ec599570f35c682e710dc6001ff605e")), 456.msat, any, any)(any[Timeout]).wasCalled(fourTimes) } } From d65db5d521ed71f055c72f16a4af284ece65d7c8 Mon Sep 17 00:00:00 2001 From: rorp Date: Wed, 6 Oct 2021 20:52:23 -0700 Subject: [PATCH 07/11] add docs --- docs/CircularRebalancing.md | 63 +++++++++++++++++++++++++++++++++++++ 1 file changed, 63 insertions(+) create mode 100644 docs/CircularRebalancing.md diff --git a/docs/CircularRebalancing.md b/docs/CircularRebalancing.md new file mode 100644 index 0000000000..533e7a9b33 --- /dev/null +++ b/docs/CircularRebalancing.md @@ -0,0 +1,63 @@ +## How to perform circular rebalancing + +Circular rebalancing is a popular tool for managing liquidity between channels. This document describes an approach to +rebalancing using Eclair. + +In this example we assume that there are 4 participants in the Lighting Network: `Alice`, `Bob`, `Carol`, and `Diana`. + +``` + 1x1x1 + Alice --------> Bob + ^ | +4x4x4 | | 2x2x2 + | v + Diana <------- Carol + 3x3x3 +``` + +`Alice` has two channels `1x1x1` with outbound liquidity of 5M stats, and `4x4x4` with inbound liquidity of 5M sats. +Now `Alice` wants to send some sats from channel `1x1x1` to channel `4x4x4` to be able to receive and forward payments +via both channels. + +First, `Alice` creates an invoice for the desired amount (eg. 1M sats): + +```shell +eclair-cli createinvoice --description='circular rebalancing from 1x1x1 ro 4x4x4' \ + --amountMsat=1000000000 +``` + +Eclair cannot send payments to self using `payinvoice` CLI command. Fortunately, `Alice` can use `sendtoroute` CLI +command to do so. + +However, in this case `Alice` should provide a valid route from `1x1x1` to `4x4x4`. It's pretty straightforward to +build a route for our example network: `1x1x1` `->` `2x2x2` `->` `3x3x3` `->` `4x4x4`. `Alice` specifies the route using +`--shortChannelIds` parameter as a comma separated list of short channel IDs. + +```shell +eclair-cli sendtoroute --shortChannelIds=1x1x1,2x2x2,3x3x3,4x4x4 \ + --amountMsat=1000000000 \ + --invoice= +``` + +This command will send 1M sats from channel `1x1x1` to channel `4x4x4`. + +In real life its not always easy to find the most economically viable route manually, but Eclair is here to help. +Similarly to `payinvoice`, `findroute` CLI command cannot find routes to self. But `Alice` can use a little trick with +`findroutebetweennodes`, which allows finding routes between arbitrary nodes. + +To rebalance channels `1x1x1` and `4x4x4`, `Alice` wants to find a route from `Bob` (as a source node) to `Diana` (as a +target node). In our example there's at least one route from `Bob` to `Diana` via `Alice`, and in real life there can be +many more such routes, bacause `Alice` can have way more than two channels, so `Alice`'s node should be excluded from +path-finding using `--ignoreNodeIds` parameter: + +```shell +eclair-cli findroutebetweennodes --sourceNodeId= \ + --targetNodeId= \ + --ignoreNodeIds= \ + --format=shortChannelIds +``` + +Then `Alice` simply appends the outgoing channel ID to the beginning of the found route and the incoming channel ID to +the end: `1x1x1,,4x4x4`. In our example the found route is `2x2x2,3x3x3`, so the full route will be +`1x1x1,2x2x2,3x3x3,4x4x4`. `Alice` can use this route with `sendtoroute` command to perform rebalancing. + From c2e918126c1f742dfb7cafd619806795e331f8cc Mon Sep 17 00:00:00 2001 From: rorp Date: Wed, 6 Oct 2021 20:57:52 -0700 Subject: [PATCH 08/11] typo --- docs/CircularRebalancing.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/CircularRebalancing.md b/docs/CircularRebalancing.md index 533e7a9b33..591585d514 100644 --- a/docs/CircularRebalancing.md +++ b/docs/CircularRebalancing.md @@ -54,7 +54,7 @@ path-finding using `--ignoreNodeIds` parameter: eclair-cli findroutebetweennodes --sourceNodeId= \ --targetNodeId= \ --ignoreNodeIds= \ - --format=shortChannelIds + --format=shortChannelId ``` Then `Alice` simply appends the outgoing channel ID to the beginning of the found route and the incoming channel ID to From 3030ce9b2344ecf46b4827c3bf076ca31c4b9b0b Mon Sep 17 00:00:00 2001 From: rorp Date: Tue, 19 Oct 2021 09:46:12 -0700 Subject: [PATCH 09/11] address the comments --- docs/release-notes/eclair-vnext.md | 3 +-- .../fr/acinq/eclair/api/directives/ExtraDirectives.scala | 2 +- .../scala/fr/acinq/eclair/api/handlers/PathFinding.scala | 6 +++--- .../src/test/scala/fr/acinq/eclair/api/ApiServiceSpec.scala | 2 +- 4 files changed, 6 insertions(+), 7 deletions(-) diff --git a/docs/release-notes/eclair-vnext.md b/docs/release-notes/eclair-vnext.md index 8a3c62ec44..55985d6403 100644 --- a/docs/release-notes/eclair-vnext.md +++ b/docs/release-notes/eclair-vnext.md @@ -11,9 +11,8 @@ This release contains many API updates: - `findroute`, `findroutetonode` and `findroutebetweennodes` supports new output format `full` (#1969) -- `findroute`, `findroutetonode` and `findroutebetweennodes` now accept `--includeLocalChannelCost` to specify if you want to count the fees from your node like trampoline payments do (#1942) - `findroute`, `findroutetonode` and `findroutebetweennodes` now accept `--ignoreNodeIds` to specify nodes you want to be ignored in path-finding (#1969) -- `findroute`, `findroutetonode` and `findroutebetweennodes` now accept `--ignoreChannelIds` to specify channels you want to be ignored in path-finding (#1969) +- `findroute`, `findroutetonode` and `findroutebetweennodes` now accept `--ignoreShortChannelIds` to specify channels you want to be ignored in path-finding (#1969) - `findroute`, `findroutetonode` and `findroutebetweennodes` now accept `--maxFeeMsat` to specify an upper bound of fees (#1969) Have a look at our [API documentation](https://acinq.github.io/eclair) for more details. diff --git a/eclair-node/src/main/scala/fr/acinq/eclair/api/directives/ExtraDirectives.scala b/eclair-node/src/main/scala/fr/acinq/eclair/api/directives/ExtraDirectives.scala index d2ed9821ab..361b53417e 100644 --- a/eclair-node/src/main/scala/fr/acinq/eclair/api/directives/ExtraDirectives.scala +++ b/eclair-node/src/main/scala/fr/acinq/eclair/api/directives/ExtraDirectives.scala @@ -48,7 +48,7 @@ trait ExtraDirectives extends Directives { val invoiceFormParam: NameReceptacle[PaymentRequest] = "invoice".as[PaymentRequest] val routeFormatFormParam: NameUnmarshallerReceptacle[RouteFormat] = "format".as[RouteFormat](routeFormatUnmarshaller) val ignoreNodeIdsFormParam: NameUnmarshallerReceptacle[List[PublicKey]] = "ignoreNodeIds".as[List[PublicKey]](pubkeyListUnmarshaller) - val ignoreChannelIdsFormParam: NameUnmarshallerReceptacle[List[ShortChannelId]] = "ignoreChannelIds".as[List[ShortChannelId]](shortChannelIdsUnmarshaller) + val ignoreShortChannelIdsFormParam: NameUnmarshallerReceptacle[List[ShortChannelId]] = "ignoreShortChannelIds".as[List[ShortChannelId]](shortChannelIdsUnmarshaller) val maxFeeMsatFormParam: NameReceptacle[MilliSatoshi] = "maxFeeMsat".as[MilliSatoshi] // custom directive to fail with HTTP 404 (and JSON response) if the element was not found 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 6058480062..5deff727c2 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 @@ -33,7 +33,7 @@ trait PathFinding { private implicit def ec: ExecutionContext = actorSystem.dispatcher val findRoute: Route = postRequest("findroute") { implicit t => - formFields(invoiceFormParam, amountMsatFormParam.?, "pathFindingExperimentName".?, routeFormatFormParam.?, "includeLocalChannelCost".as[Boolean].?, ignoreNodeIdsFormParam.?, ignoreChannelIdsFormParam.?, maxFeeMsatFormParam.?) { + formFields(invoiceFormParam, amountMsatFormParam.?, "pathFindingExperimentName".?, routeFormatFormParam.?, "includeLocalChannelCost".as[Boolean].?, ignoreNodeIdsFormParam.?, ignoreShortChannelIdsFormParam.?, maxFeeMsatFormParam.?) { case (invoice@PaymentRequest(_, Some(amount), _, nodeId, _, _), None, pathFindingExperimentName_opt, routeFormat_opt, includeLocalChannelCost_opt, ignoreNodeIds_opt, ignoreChannels_opt, maxFee_opt) => complete(eclairApi.findRoute(nodeId, amount, pathFindingExperimentName_opt, invoice.routingInfo, includeLocalChannelCost_opt.getOrElse(false), ignoreNodeIds = ignoreNodeIds_opt.getOrElse(Nil), ignoreShortChannelIds = ignoreChannels_opt.getOrElse(Nil), maxFee_opt = maxFee_opt).map(r => RouteFormat.format(r, routeFormat_opt))) case (invoice, Some(overrideAmount), pathFindingExperimentName_opt, routeFormat_opt, includeLocalChannelCost_opt, ignoreNodeIds_opt, ignoreChannels_opt, maxFee_opt) => @@ -45,14 +45,14 @@ trait PathFinding { } val findRouteToNode: Route = postRequest("findroutetonode") { implicit t => - formFields(nodeIdFormParam, amountMsatFormParam, "pathFindingExperimentName".?, routeFormatFormParam.?, "includeLocalChannelCost".as[Boolean].?, ignoreNodeIdsFormParam.?, ignoreChannelIdsFormParam.?, maxFeeMsatFormParam.?) { + formFields(nodeIdFormParam, amountMsatFormParam, "pathFindingExperimentName".?, routeFormatFormParam.?, "includeLocalChannelCost".as[Boolean].?, ignoreNodeIdsFormParam.?, ignoreShortChannelIdsFormParam.?, maxFeeMsatFormParam.?) { (nodeId, amount, pathFindingExperimentName_opt, routeFormat_opt, includeLocalChannelCost_opt, ignoreNodeIds_opt, ignoreChannels_opt, maxFee_opt) => complete(eclairApi.findRoute(nodeId, amount, pathFindingExperimentName_opt, includeLocalChannelCost = includeLocalChannelCost_opt.getOrElse(false), ignoreNodeIds = ignoreNodeIds_opt.getOrElse(Nil), ignoreShortChannelIds = ignoreChannels_opt.getOrElse(Nil), maxFee_opt = maxFee_opt).map(r => RouteFormat.format(r, routeFormat_opt))) } } val findRouteBetweenNodes: Route = postRequest("findroutebetweennodes") { implicit t => - formFields("sourceNodeId".as[PublicKey], "targetNodeId".as[PublicKey], amountMsatFormParam, "pathFindingExperimentName".?, routeFormatFormParam.?, "includeLocalChannelCost".as[Boolean].?, ignoreNodeIdsFormParam.?, ignoreChannelIdsFormParam.?, maxFeeMsatFormParam.?) { (sourceNodeId, targetNodeId, amount, pathFindingExperimentName_opt, routeFormat_opt, includeLocalChannelCost_opt, ignoreNodeIds_opt, ignoreChannels_opt, maxFee_opt) => + formFields("sourceNodeId".as[PublicKey], "targetNodeId".as[PublicKey], amountMsatFormParam, "pathFindingExperimentName".?, routeFormatFormParam.?, "includeLocalChannelCost".as[Boolean].?, ignoreNodeIdsFormParam.?, ignoreShortChannelIdsFormParam.?, maxFeeMsatFormParam.?) { (sourceNodeId, targetNodeId, amount, pathFindingExperimentName_opt, routeFormat_opt, includeLocalChannelCost_opt, ignoreNodeIds_opt, ignoreChannels_opt, maxFee_opt) => complete(eclairApi.findRouteBetween(sourceNodeId, targetNodeId, amount, pathFindingExperimentName_opt, includeLocalChannelCost = includeLocalChannelCost_opt.getOrElse(false), ignoreNodeIds = ignoreNodeIds_opt.getOrElse(Nil), ignoreShortChannelIds = ignoreChannels_opt.getOrElse(Nil), maxFee_opt = maxFee_opt).map(r => RouteFormat.format(r, routeFormat_opt))) } } 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 81f428e68e..659d3b2ef3 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 @@ -966,7 +966,7 @@ class ApiServiceSpec extends AnyFunSuite with ScalatestRouteTest with IdiomaticM } } - test("'findroute' method response should support nodeId, channelId and full formats") { + test("'findroute' method response should support nodeId, shortChannelId and full formats") { val invoice = "lnbc12580n1pw2ywztpp554ganw404sh4yjkwnysgn3wjcxfcq7gtx53gxczkjr9nlpc3hzvqdq2wpskwctddyxqr4rqrzjqwryaup9lh50kkranzgcdnn2fgvx390wgj5jd07rwr3vxeje0glc7z9rtvqqwngqqqqqqqlgqqqqqeqqjqrrt8smgjvfj7sg38dwtr9kc9gg3era9k3t2hvq3cup0jvsrtrxuplevqgfhd3rzvhulgcxj97yjuj8gdx8mllwj4wzjd8gdjhpz3lpqqvk2plh" From 6d40b0d315455f23c909be064dd19bcd0fc77e47 Mon Sep 17 00:00:00 2001 From: t-bast Date: Wed, 20 Oct 2021 15:09:07 +0200 Subject: [PATCH 10/11] Rework route serializers --- .../acinq/eclair/json/JsonSerializers.scala | 27 ++++--- .../eclair/api/directives/RouteFormat.scala | 25 +++---- .../acinq/eclair/api/serde/JsonSupport.scala | 3 - eclair-node/src/test/resources/api/findroute | 73 ------------------- .../src/test/resources/api/findroute-full | 1 + .../src/test/resources/api/findroute-nodeid | 1 + .../src/test/resources/api/findroute-scid | 1 + .../fr/acinq/eclair/api/ApiServiceSpec.scala | 62 ++++++---------- 8 files changed, 51 insertions(+), 142 deletions(-) delete mode 100644 eclair-node/src/test/resources/api/findroute create mode 100644 eclair-node/src/test/resources/api/findroute-full create mode 100644 eclair-node/src/test/resources/api/findroute-nodeid create mode 100644 eclair-node/src/test/resources/api/findroute-scid diff --git a/eclair-core/src/main/scala/fr/acinq/eclair/json/JsonSerializers.scala b/eclair-core/src/main/scala/fr/acinq/eclair/json/JsonSerializers.scala index 8b974fa225..dac2374f62 100644 --- a/eclair-core/src/main/scala/fr/acinq/eclair/json/JsonSerializers.scala +++ b/eclair-core/src/main/scala/fr/acinq/eclair/json/JsonSerializers.scala @@ -27,7 +27,7 @@ import fr.acinq.eclair.db.FailureType.FailureType import fr.acinq.eclair.db.{IncomingPaymentStatus, OutgoingPaymentStatus} import fr.acinq.eclair.payment.PaymentFailure.PaymentFailedSummary import fr.acinq.eclair.payment._ -import fr.acinq.eclair.router.Router.{Route, RouteResponse} +import fr.acinq.eclair.router.Router.{ChannelHop, Route} import fr.acinq.eclair.transactions.DirectedHtlc import fr.acinq.eclair.transactions.Transactions._ import fr.acinq.eclair.wire.protocol._ @@ -246,17 +246,23 @@ object ColorSerializer extends MinimalSerializer({ case c: Color => JString(c.toString) }) -object RouteSerializer extends MinimalSerializer ({ - case route: Route => - Extraction.decompose(route)(DefaultFormats + - ByteVector32Serializer + - ByteVectorSerializer + - PublicKeySerializer + - ShortChannelIdSerializer + - MilliSatoshiSerializer + - CltvExpiryDeltaSerializer) +// @formatter:off +private case class RouteFullJson(amount: MilliSatoshi, hops: Seq[ChannelHop]) +object RouteFullSerializer extends ConvertClassSerializer[Route](route => RouteFullJson(route.amount, route.hops)) + +private case class RouteNodeIdsJson(amount: MilliSatoshi, nodeIds: Seq[PublicKey]) +object RouteNodeIdsSerializer extends ConvertClassSerializer[Route](route => { + val nodeIds = route.hops match { + case rest :+ last => rest.map(_.nodeId) :+ last.nodeId :+ last.nextNodeId + case Nil => Nil + } + RouteNodeIdsJson(route.amount, nodeIds) }) +private case class RouteShortChannelIdsJson(amount: MilliSatoshi, shortChannelIds: Seq[ShortChannelId]) +object RouteShortChannelIdsSerializer extends ConvertClassSerializer[Route](route => RouteShortChannelIdsJson(route.amount, route.hops.map(_.lastUpdate.shortChannelId))) +// @formatter:on + // @formatter:off private case class PaymentFailureSummaryJson(amount: MilliSatoshi, route: Seq[PublicKey], message: String) private case class PaymentFailedSummaryJson(paymentHash: ByteVector32, destination: PublicKey, totalAmount: MilliSatoshi, pathFindingExperiment: String, failures: Seq[PaymentFailureSummaryJson]) @@ -473,7 +479,6 @@ object JsonSerializers { CommandResponseSerializer + InputInfoSerializer + ColorSerializer + - RouteSerializer + ThrowableSerializer + FailureMessageSerializer + FailureTypeSerializer + diff --git a/eclair-node/src/main/scala/fr/acinq/eclair/api/directives/RouteFormat.scala b/eclair-node/src/main/scala/fr/acinq/eclair/api/directives/RouteFormat.scala index 89500ebc6e..69d54badd9 100644 --- a/eclair-node/src/main/scala/fr/acinq/eclair/api/directives/RouteFormat.scala +++ b/eclair-node/src/main/scala/fr/acinq/eclair/api/directives/RouteFormat.scala @@ -19,7 +19,9 @@ package fr.acinq.eclair.api.directives import akka.http.scaladsl.model.StatusCodes.OK import akka.http.scaladsl.model.{ContentTypes, HttpResponse} import fr.acinq.eclair.api.serde.JsonSupport._ +import fr.acinq.eclair.json.{JsonSerializers, RouteFullSerializer, RouteNodeIdsSerializer, RouteShortChannelIdsSerializer} import fr.acinq.eclair.router.Router.RouteResponse +import org.json4s.Formats // @formatter:off sealed trait RouteFormat @@ -43,20 +45,13 @@ object RouteFormat { def format(route: RouteResponse, format_opt: Option[RouteFormat]): HttpResponse = format(route, format_opt.getOrElse(NodeIdRouteFormat)) - def format(route: RouteResponse, format: RouteFormat): HttpResponse = - HttpResponse(OK).withEntity(ContentTypes.`application/json`, - format match { - case NodeIdRouteFormat => - val nodeIds = route.routes.head.hops match { - case rest :+ last => rest.map(_.nodeId) :+ last.nodeId :+ last.nextNodeId - case Nil => Nil - } - serialization.write(nodeIds.toList.map(_.toString)) - case ShortChannelIdRouteFormat => - val shortChannelIds = route.routes.head.hops.map(_.lastUpdate.shortChannelId) - serialization.write(shortChannelIds.toList.map(_.toString)) - case FullRouteFormat => - serialization.writePretty(route.routes) - }) + def format(route: RouteResponse, format: RouteFormat): HttpResponse = { + val serializationFormats: Formats = format match { + case NodeIdRouteFormat => JsonSerializers.formats + RouteNodeIdsSerializer + case ShortChannelIdRouteFormat => JsonSerializers.formats + RouteShortChannelIdsSerializer + case FullRouteFormat => JsonSerializers.formats + RouteFullSerializer + } + HttpResponse(OK).withEntity(ContentTypes.`application/json`, serialization.write(route)(serializationFormats)) + } } diff --git a/eclair-node/src/main/scala/fr/acinq/eclair/api/serde/JsonSupport.scala b/eclair-node/src/main/scala/fr/acinq/eclair/api/serde/JsonSupport.scala index 1930e701f2..59d2e0ee6e 100644 --- a/eclair-node/src/main/scala/fr/acinq/eclair/api/serde/JsonSupport.scala +++ b/eclair-node/src/main/scala/fr/acinq/eclair/api/serde/JsonSupport.scala @@ -21,9 +21,6 @@ import fr.acinq.eclair.json.JsonSerializers import org.json4s.{Formats, Serialization} object JsonSupport extends Json4sSupport { - implicit val serialization: Serialization = JsonSerializers.serialization - implicit val formats: Formats = JsonSerializers.formats - } diff --git a/eclair-node/src/test/resources/api/findroute b/eclair-node/src/test/resources/api/findroute deleted file mode 100644 index bec34bda58..0000000000 --- a/eclair-node/src/test/resources/api/findroute +++ /dev/null @@ -1,73 +0,0 @@ -[ { - "amount" : 456, - "hops" : [ { - "nodeId" : "03007e67dc5a8fd2b2ef21cb310ab6359ddb51f3f86a8b79b8b1e23bc3a6ea150a", - "nextNodeId" : "026105f6cb4862810be989385d16f04b0f748f6f2a14040338b1a534d45b4be1c1", - "lastUpdate" : { - "signature" : { - "bytes" : "92cf3f12e161391986eb2cd7106ddab41a23c734f8f1ed120fb64f4b91f98f690ecf930388e62965f8aefbf1adafcd25a572669a125396dcfb83615208754679" - }, - "chainHash" : "024b7b3626554c44dcc2454ee3812458bfa68d9fced466edfab470844cb7ffe2", - "shortChannelId" : "1x2x3", - "timestamp" : 0, - "channelFlags" : { - "isEnabled" : true, - "isNode1" : true - }, - "cltvExpiryDelta" : 0, - "htlcMinimumMsat" : 1, - "feeBaseMsat" : 1, - "feeProportionalMillionths" : 1, - "tlvStream" : { - "records" : [ ], - "unknown" : [ ] - } - } - }, { - "nodeId" : "026105f6cb4862810be989385d16f04b0f748f6f2a14040338b1a534d45b4be1c1", - "nextNodeId" : "038cfa2b5857843ee90cff91b06f692c0d8fe201921ee6387aee901d64f43699f0", - "lastUpdate" : { - "signature" : { - "bytes" : "92cf3f12e161391986eb2cd7106ddab41a23c734f8f1ed120fb64f4b91f98f690ecf930388e62965f8aefbf1adafcd25a572669a125396dcfb83615208754679" - }, - "chainHash" : "024b7b3626554c44dcc2454ee3812458bfa68d9fced466edfab470844cb7ffe2", - "shortChannelId" : "1x2x4", - "timestamp" : 0, - "channelFlags" : { - "isEnabled" : true, - "isNode1" : true - }, - "cltvExpiryDelta" : 0, - "htlcMinimumMsat" : 1, - "feeBaseMsat" : 1, - "feeProportionalMillionths" : 1, - "tlvStream" : { - "records" : [ ], - "unknown" : [ ] - } - } - }, { - "nodeId" : "038cfa2b5857843ee90cff91b06f692c0d8fe201921ee6387aee901d64f43699f0", - "nextNodeId" : "02be60276e294c6921240daae33a361d214d02578656df0e74c61a09c3196e51df", - "lastUpdate" : { - "signature" : { - "bytes" : "92cf3f12e161391986eb2cd7106ddab41a23c734f8f1ed120fb64f4b91f98f690ecf930388e62965f8aefbf1adafcd25a572669a125396dcfb83615208754679" - }, - "chainHash" : "024b7b3626554c44dcc2454ee3812458bfa68d9fced466edfab470844cb7ffe2", - "shortChannelId" : "1x2x5", - "timestamp" : 0, - "channelFlags" : { - "isEnabled" : true, - "isNode1" : true - }, - "cltvExpiryDelta" : 0, - "htlcMinimumMsat" : 1, - "feeBaseMsat" : 1, - "feeProportionalMillionths" : 1, - "tlvStream" : { - "records" : [ ], - "unknown" : [ ] - } - } - } ] -} ] \ No newline at end of file diff --git a/eclair-node/src/test/resources/api/findroute-full b/eclair-node/src/test/resources/api/findroute-full new file mode 100644 index 0000000000..e7124ba01a --- /dev/null +++ b/eclair-node/src/test/resources/api/findroute-full @@ -0,0 +1 @@ +{"routes":[{"amount":456,"hops":[{"nodeId":"03007e67dc5a8fd2b2ef21cb310ab6359ddb51f3f86a8b79b8b1e23bc3a6ea150a","nextNodeId":"026105f6cb4862810be989385d16f04b0f748f6f2a14040338b1a534d45b4be1c1","lastUpdate":{"signature":"92cf3f12e161391986eb2cd7106ddab41a23c734f8f1ed120fb64f4b91f98f690ecf930388e62965f8aefbf1adafcd25a572669a125396dcfb83615208754679","chainHash":"024b7b3626554c44dcc2454ee3812458bfa68d9fced466edfab470844cb7ffe2","shortChannelId":"1x2x3","timestamp":0,"channelFlags":{"isEnabled":true,"isNode1":true},"cltvExpiryDelta":0,"htlcMinimumMsat":1,"feeBaseMsat":1,"feeProportionalMillionths":1,"tlvStream":{"records":[],"unknown":[]}}},{"nodeId":"026105f6cb4862810be989385d16f04b0f748f6f2a14040338b1a534d45b4be1c1","nextNodeId":"038cfa2b5857843ee90cff91b06f692c0d8fe201921ee6387aee901d64f43699f0","lastUpdate":{"signature":"92cf3f12e161391986eb2cd7106ddab41a23c734f8f1ed120fb64f4b91f98f690ecf930388e62965f8aefbf1adafcd25a572669a125396dcfb83615208754679","chainHash":"024b7b3626554c44dcc2454ee3812458bfa68d9fced466edfab470844cb7ffe2","shortChannelId":"1x2x4","timestamp":0,"channelFlags":{"isEnabled":true,"isNode1":true},"cltvExpiryDelta":0,"htlcMinimumMsat":1,"feeBaseMsat":1,"feeProportionalMillionths":1,"tlvStream":{"records":[],"unknown":[]}}},{"nodeId":"038cfa2b5857843ee90cff91b06f692c0d8fe201921ee6387aee901d64f43699f0","nextNodeId":"02be60276e294c6921240daae33a361d214d02578656df0e74c61a09c3196e51df","lastUpdate":{"signature":"92cf3f12e161391986eb2cd7106ddab41a23c734f8f1ed120fb64f4b91f98f690ecf930388e62965f8aefbf1adafcd25a572669a125396dcfb83615208754679","chainHash":"024b7b3626554c44dcc2454ee3812458bfa68d9fced466edfab470844cb7ffe2","shortChannelId":"1x2x5","timestamp":0,"channelFlags":{"isEnabled":true,"isNode1":true},"cltvExpiryDelta":0,"htlcMinimumMsat":1,"feeBaseMsat":1,"feeProportionalMillionths":1,"tlvStream":{"records":[],"unknown":[]}}}]}]} \ No newline at end of file diff --git a/eclair-node/src/test/resources/api/findroute-nodeid b/eclair-node/src/test/resources/api/findroute-nodeid new file mode 100644 index 0000000000..3671ff6df7 --- /dev/null +++ b/eclair-node/src/test/resources/api/findroute-nodeid @@ -0,0 +1 @@ +{"routes":[{"amount":456,"nodeIds":["03007e67dc5a8fd2b2ef21cb310ab6359ddb51f3f86a8b79b8b1e23bc3a6ea150a","026105f6cb4862810be989385d16f04b0f748f6f2a14040338b1a534d45b4be1c1","038cfa2b5857843ee90cff91b06f692c0d8fe201921ee6387aee901d64f43699f0","02be60276e294c6921240daae33a361d214d02578656df0e74c61a09c3196e51df"]}]} \ No newline at end of file diff --git a/eclair-node/src/test/resources/api/findroute-scid b/eclair-node/src/test/resources/api/findroute-scid new file mode 100644 index 0000000000..68b2dc39a0 --- /dev/null +++ b/eclair-node/src/test/resources/api/findroute-scid @@ -0,0 +1 @@ +{"routes":[{"amount":456,"shortChannelIds":["1x2x3","1x2x4","1x2x5"]}]} \ No newline at end of file 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 659d3b2ef3..2ae76c7ceb 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,6 @@ 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.wire.protocol.{ChannelUpdate, Color, NodeAddress} -import org.json4s.JsonAST.{JArray, JString} import org.mockito.scalatest.IdiomaticMockito import org.scalatest.funsuite.AnyFunSuite import org.scalatest.matchers.should.Matchers @@ -269,7 +268,6 @@ class ApiServiceSpec extends AnyFunSuite with ScalatestRouteTest with IdiomaticM test("'open' channels with bad channelType") { val nodeId = PublicKey(hex"030bb6a5e0c6b203c7e2180fb78c7ba4bdce46126761d8201b91ddac089cdecc87") - val channelId = ByteVector32(hex"56d7d6eda04d80138270c49709f1eadb5ab4939e5061309ccdacdb98ce637d0e") val eclair = mock[Eclair] val mockService = new MockService(eclair) @@ -782,8 +780,6 @@ class ApiServiceSpec extends AnyFunSuite with ScalatestRouteTest with IdiomaticM } test("'getreceivedinfo' 1") { - val invoice = "lnbc2500u1pvjluezpp5qqqsyqcyq5rqwzqfqqqsyqcyq5rqwzqfqqqsyqcyq5rqwzqfqypqdq5xysxxatsyp3k7enxv4jsxqzpuaztrnwngzn3kdzw5hydlzf03qdgm2hdq27cqv3agm2awhz5se903vruatfhq77w3ls4evs3ch9zw97j25emudupq63nyw24cg27h2rspfj9srp" - val defaultPayment = IncomingPayment(PaymentRequest.read(invoice), ByteVector32.One, PaymentType.Standard, 42, IncomingPaymentStatus.Pending) val eclair = mock[Eclair] val notFound = randomBytes32() eclair.receivedInfo(notFound)(any) returns Future.successful(None) @@ -967,8 +963,8 @@ class ApiServiceSpec extends AnyFunSuite with ScalatestRouteTest with IdiomaticM } test("'findroute' method response should support nodeId, shortChannelId and full formats") { - val invoice = "lnbc12580n1pw2ywztpp554ganw404sh4yjkwnysgn3wjcxfcq7gtx53gxczkjr9nlpc3hzvqdq2wpskwctddyxqr4rqrzjqwryaup9lh50kkranzgcdnn2fgvx390wgj5jd07rwr3vxeje0glc7z9rtvqqwngqqqqqqqlgqqqqqeqqjqrrt8smgjvfj7sg38dwtr9kc9gg3era9k3t2hvq3cup0jvsrtrxuplevqgfhd3rzvhulgcxj97yjuj8gdx8mllwj4wzjd8gdjhpz3lpqqvk2plh" - + val serializedInvoice = "lnbc12580n1pw2ywztpp554ganw404sh4yjkwnysgn3wjcxfcq7gtx53gxczkjr9nlpc3hzvqdq2wpskwctddyxqr4rqrzjqwryaup9lh50kkranzgcdnn2fgvx390wgj5jd07rwr3vxeje0glc7z9rtvqqwngqqqqqqqlgqqqqqeqqjqrrt8smgjvfj7sg38dwtr9kc9gg3era9k3t2hvq3cup0jvsrtrxuplevqgfhd3rzvhulgcxj97yjuj8gdx8mllwj4wzjd8gdjhpz3lpqqvk2plh" + val invoice = PaymentRequest.read(serializedInvoice) val mockChannelUpdate1 = ChannelUpdate( signature = ByteVector64.fromValidHex("92cf3f12e161391986eb2cd7106ddab41a23c734f8f1ed120fb64f4b91f98f690ecf930388e62965f8aefbf1adafcd25a572669a125396dcfb83615208754679"), @@ -983,12 +979,9 @@ class ApiServiceSpec extends AnyFunSuite with ScalatestRouteTest with IdiomaticM htlcMaximumMsat = None ) - val mockHop1 = - Router.ChannelHop(nodeId = PublicKey.fromBin(ByteVector.fromValidHex("03007e67dc5a8fd2b2ef21cb310ab6359ddb51f3f86a8b79b8b1e23bc3a6ea150a")), nextNodeId = PublicKey.fromBin(ByteVector.fromValidHex("026105f6cb4862810be989385d16f04b0f748f6f2a14040338b1a534d45b4be1c1")), mockChannelUpdate1) - val mockHop2 = - Router.ChannelHop(nodeId = mockHop1.nextNodeId, nextNodeId = PublicKey.fromBin(ByteVector.fromValidHex("038cfa2b5857843ee90cff91b06f692c0d8fe201921ee6387aee901d64f43699f0")), mockChannelUpdate1.copy(shortChannelId = ShortChannelId(1, 2, 4))) - val mockHop3 = - Router.ChannelHop(nodeId = mockHop2.nextNodeId, nextNodeId = PublicKey.fromBin(ByteVector.fromValidHex("02be60276e294c6921240daae33a361d214d02578656df0e74c61a09c3196e51df")), mockChannelUpdate1.copy(shortChannelId = ShortChannelId(1, 2, 5))) + val mockHop1 = Router.ChannelHop(PublicKey.fromBin(ByteVector.fromValidHex("03007e67dc5a8fd2b2ef21cb310ab6359ddb51f3f86a8b79b8b1e23bc3a6ea150a")), PublicKey.fromBin(ByteVector.fromValidHex("026105f6cb4862810be989385d16f04b0f748f6f2a14040338b1a534d45b4be1c1")), mockChannelUpdate1) + val mockHop2 = Router.ChannelHop(mockHop1.nextNodeId, PublicKey.fromBin(ByteVector.fromValidHex("038cfa2b5857843ee90cff91b06f692c0d8fe201921ee6387aee901d64f43699f0")), mockChannelUpdate1.copy(shortChannelId = ShortChannelId(1, 2, 4))) + val mockHop3 = Router.ChannelHop(mockHop2.nextNodeId, PublicKey.fromBin(ByteVector.fromValidHex("02be60276e294c6921240daae33a361d214d02578656df0e74c61a09c3196e51df")), mockChannelUpdate1.copy(shortChannelId = ShortChannelId(1, 2, 5))) val mockHops = Seq(mockHop1, mockHop2, mockHop3) val eclair = mock[Eclair] @@ -996,74 +989,63 @@ class ApiServiceSpec extends AnyFunSuite with ScalatestRouteTest with IdiomaticM eclair.findRoute(any, any, any, any, any, any, any, any)(any[Timeout]) returns Future.successful(Router.RouteResponse(Seq(Router.Route(456.msat, mockHops)))) // invalid format - Post("/findroute", FormData("format" -> "invalid-output-format", "invoice" -> invoice, "amountMsat" -> "456")) ~> + Post("/findroute", FormData("format" -> "invalid-output-format", "invoice" -> serializedInvoice, "amountMsat" -> "456")) ~> addCredentials(BasicHttpCredentials("", mockApi().password)) ~> addHeader("Content-Type", "application/json") ~> Route.seal(mockService.findRoute) ~> check { assert(handled) assert(status == BadRequest) - eclair.findRoute(PublicKey.fromBin(ByteVector.fromValidHex("036ded9bb8175d0c9fd3fad145965cf5005ec599570f35c682e710dc6001ff605e")), 456.msat, any, any, any, any, any, any)(any[Timeout]).wasNever(called) + eclair.findRoute(invoice.nodeId, 456.msat, any, any, any, any, any, any)(any[Timeout]).wasNever(called) } // default format - Post("/findroute", FormData("invoice" -> invoice, "amountMsat" -> "456")) ~> + Post("/findroute", FormData("invoice" -> serializedInvoice, "amountMsat" -> "456")) ~> addCredentials(BasicHttpCredentials("", mockApi().password)) ~> addHeader("Content-Type", "application/json") ~> Route.seal(mockService.findRoute) ~> check { assert(handled) assert(status == OK) - assert(entityAs[JArray](Json4sSupport.unmarshaller, ClassTag(classOf[ErrorResponse])) == JArray(List( - JString(mockHop1.nodeId.toString()), - JString(mockHop2.nodeId.toString()), - JString(mockHop3.nodeId.toString()), - JString(mockHop3.nextNodeId.toString()) - ))) - eclair.findRoute(PublicKey.fromBin(ByteVector.fromValidHex("036ded9bb8175d0c9fd3fad145965cf5005ec599570f35c682e710dc6001ff605e")), 456.msat, any, any, any, any, any, any)(any[Timeout]).wasCalled(once) + val response = entityAs[String] + matchTestJson("findroute-nodeid", response) + eclair.findRoute(invoice.nodeId, 456.msat, any, any, any, any, any, any)(any[Timeout]).wasCalled(once) } - Post("/findroute", FormData("format" -> "nodeId", "invoice" -> invoice, "amountMsat" -> "456")) ~> + Post("/findroute", FormData("format" -> "nodeId", "invoice" -> serializedInvoice, "amountMsat" -> "456")) ~> addCredentials(BasicHttpCredentials("", mockApi().password)) ~> addHeader("Content-Type", "application/json") ~> Route.seal(mockService.findRoute) ~> check { assert(handled) assert(status == OK) - assert(entityAs[JArray](Json4sSupport.unmarshaller, ClassTag(classOf[ErrorResponse])) == JArray(List( - JString(mockHop1.nodeId.toString()), - JString(mockHop2.nodeId.toString()), - JString(mockHop3.nodeId.toString()), - JString(mockHop3.nextNodeId.toString()) - ))) - eclair.findRoute(PublicKey.fromBin(ByteVector.fromValidHex("036ded9bb8175d0c9fd3fad145965cf5005ec599570f35c682e710dc6001ff605e")), 456.msat, any, any, any, any, any, any)(any[Timeout]).wasCalled(twice) + val response = entityAs[String] + matchTestJson("findroute-nodeid", response) + eclair.findRoute(invoice.nodeId, 456.msat, any, any, any, any, any, any)(any[Timeout]).wasCalled(twice) } - Post("/findroute", FormData("format" -> "shortChannelId", "invoice" -> invoice, "amountMsat" -> "456")) ~> + Post("/findroute", FormData("format" -> "shortChannelId", "invoice" -> serializedInvoice, "amountMsat" -> "456")) ~> addCredentials(BasicHttpCredentials("", mockApi().password)) ~> addHeader("Content-Type", "application/json") ~> Route.seal(mockService.findRoute) ~> check { assert(handled) assert(status == OK) - assert(entityAs[JArray](Json4sSupport.unmarshaller, ClassTag(classOf[ErrorResponse])) == JArray(List( - JString(mockHop1.lastUpdate.shortChannelId.toString()), - JString(mockHop2.lastUpdate.shortChannelId.toString()), - JString(mockHop3.lastUpdate.shortChannelId.toString()) - ))) - eclair.findRoute(PublicKey.fromBin(ByteVector.fromValidHex("036ded9bb8175d0c9fd3fad145965cf5005ec599570f35c682e710dc6001ff605e")), 456.msat, any, any, any, any, any, any)(any[Timeout]).wasCalled(threeTimes) + val response = entityAs[String] + matchTestJson("findroute-scid", response) + eclair.findRoute(invoice.nodeId, 456.msat, any, any, any, any, any, any)(any[Timeout]).wasCalled(threeTimes) } - Post("/findroute", FormData("format" -> "full", "invoice" -> invoice, "amountMsat" -> "456")) ~> + Post("/findroute", FormData("format" -> "full", "invoice" -> serializedInvoice, "amountMsat" -> "456")) ~> addCredentials(BasicHttpCredentials("", mockApi().password)) ~> addHeader("Content-Type", "application/json") ~> Route.seal(mockService.findRoute) ~> check { assert(handled) assert(status == OK) - val response = entityAs[String] - matchTestJson("findroute", response) + matchTestJson("findroute-full", response) + eclair.findRoute(invoice.nodeId, 456.msat, any, any, any, any, any, any)(any[Timeout]).wasCalled(fourTimes) } } From b02c0b74bc7eb41eb5c13bb8e2ca46e9570f4016 Mon Sep 17 00:00:00 2001 From: rorp Date: Wed, 20 Oct 2021 11:24:27 -0700 Subject: [PATCH 11/11] Merge branch 'master' into new_findroute_params --- docs/release-notes/eclair-vnext.md | 40 +++ .../scala/fr/acinq/eclair/CoinUtils.scala | 242 ------------------ .../main/scala/fr/acinq/eclair/Eclair.scala | 59 ++--- .../scala/fr/acinq/eclair/Timestamp.scala | 59 +++++ .../blockchain/bitcoind/ZmqWatcher.scala | 4 +- .../bitcoind/rpc/BitcoinCoreClient.scala | 4 +- .../watchdogs/BlockchainWatchdog.scala | 4 +- .../fr/acinq/eclair/channel/Channel.scala | 27 +- .../eclair/channel/ChannelFeatures.scala | 13 +- .../fr/acinq/eclair/channel/Helpers.scala | 18 +- .../eclair/channel/publish/TxPublisher.scala | 6 +- .../scala/fr/acinq/eclair/crypto/Sphinx.scala | 88 +++++++ .../acinq/eclair/crypto/WeakEntropyPool.scala | 5 +- .../scala/fr/acinq/eclair/db/AuditDb.scala | 13 +- .../scala/fr/acinq/eclair/db/ChannelsDb.scala | 3 +- .../fr/acinq/eclair/db/DualDatabases.scala | 29 ++- .../scala/fr/acinq/eclair/db/PaymentsDb.scala | 38 +-- .../fr/acinq/eclair/db/pg/PgAuditDb.scala | 54 ++-- .../fr/acinq/eclair/db/pg/PgChannelsDb.scala | 9 + .../fr/acinq/eclair/db/pg/PgPaymentsDb.scala | 69 ++--- .../eclair/db/sqlite/SqliteAuditDb.scala | 64 ++--- .../eclair/db/sqlite/SqliteChannelsDb.scala | 13 +- .../eclair/db/sqlite/SqliteFeeratesDb.scala | 5 +- .../eclair/db/sqlite/SqlitePaymentsDb.scala | 72 +++--- .../main/scala/fr/acinq/eclair/io/Peer.scala | 19 +- .../fr/acinq/eclair/io/PeerConnection.scala | 8 +- .../fr/acinq/eclair/io/ReconnectionTask.scala | 7 +- .../fr/acinq/eclair/io/Switchboard.scala | 3 +- .../acinq/eclair/json/JsonSerializers.scala | 21 +- .../main/scala/fr/acinq/eclair/package.scala | 8 + .../acinq/eclair/payment/PaymentEvents.scala | 27 +- .../acinq/eclair/payment/PaymentRequest.scala | 20 +- .../payment/receive/MultiPartPaymentFSM.scala | 8 +- .../relay/PostRestartHtlcCleaner.scala | 16 +- .../acinq/eclair/payment/send/Autoprobe.scala | 4 +- .../send/MultiPartPaymentLifecycle.scala | 10 +- .../payment/send/PaymentLifecycle.scala | 10 +- .../acinq/eclair/router/Announcements.scala | 17 +- .../fr/acinq/eclair/router/Monitoring.scala | 4 +- .../eclair/router/RouteCalculation.scala | 2 +- .../acinq/eclair/router/StaleChannels.scala | 14 +- .../scala/fr/acinq/eclair/router/Sync.scala | 11 +- .../fr/acinq/eclair/router/Validation.scala | 4 +- .../channel/version0/ChannelCodecs0.scala | 5 +- .../eclair/wire/protocol/CommonCodecs.scala | 4 +- .../protocol/EncryptedRecipientDataTlv.scala | 82 ++++++ .../protocol/LightningMessageCodecs.scala | 6 +- .../wire/protocol/LightningMessageTypes.scala | 10 +- .../fr/acinq/eclair/wire/protocol/Onion.scala | 20 +- .../eclair/wire/protocol/RoutingTlv.scala | 10 +- .../scala/fr/acinq/eclair/CoinUtilsSpec.scala | 115 --------- .../fr/acinq/eclair/EclairImplSpec.scala | 45 +--- .../scala/fr/acinq/eclair/TestDatabases.scala | 51 +++- .../scala/fr/acinq/eclair/TestUtils.scala | 8 +- .../fr/acinq/eclair/channel/HelpersSpec.scala | 26 +- .../ChannelStateTestsHelperMethods.scala | 6 +- .../a/WaitForAcceptChannelStateSpec.scala | 42 ++- ... => WaitForFundingInternalStateSpec.scala} | 31 ++- .../b/WaitForFundingSignedStateSpec.scala | 12 +- .../c/WaitForFundingConfirmedStateSpec.scala | 4 +- .../channel/states/h/ClosingStateSpec.scala | 12 +- .../fr/acinq/eclair/crypto/SphinxSpec.scala | 162 +++++++++++- .../fr/acinq/eclair/db/AuditDbSpec.scala | 108 ++++---- .../fr/acinq/eclair/db/ChannelsDbSpec.scala | 5 + .../fr/acinq/eclair/db/PaymentsDbSpec.scala | 236 ++++++++--------- .../eclair/db/SqliteFeeratesDbSpec.scala | 2 +- .../integration/PaymentIntegrationSpec.scala | 30 +-- .../PerformanceIntegrationSpec.scala | 8 +- .../acinq/eclair/io/PeerConnectionSpec.scala | 4 +- .../scala/fr/acinq/eclair/io/PeerSpec.scala | 4 +- .../eclair/io/ReconnectionTaskSpec.scala | 6 +- .../fr/acinq/eclair/io/SwitchboardSpec.scala | 4 +- .../eclair/json/JsonSerializersSpec.scala | 7 + .../eclair/payment/MultiPartHandlerSpec.scala | 18 +- .../MultiPartPaymentLifecycleSpec.scala | 2 +- .../eclair/payment/PaymentLifecycleSpec.scala | 8 +- .../eclair/payment/PaymentPacketSpec.scala | 4 +- .../eclair/payment/PaymentRequestSpec.scala | 35 ++- .../payment/PostRestartHtlcCleanerSpec.scala | 8 +- .../payment/relay/ChannelRelayerSpec.scala | 4 +- .../eclair/router/AnnouncementsSpec.scala | 2 +- .../router/ChannelRangeQueriesSpec.scala | 12 +- .../eclair/router/NetworkStatsSpec.scala | 6 +- .../eclair/router/RouteCalculationSpec.scala | 32 +-- .../fr/acinq/eclair/router/RouterSpec.scala | 10 +- .../acinq/eclair/router/RoutingSyncSpec.scala | 2 +- .../internal/channel/ChannelCodecsSpec.scala | 8 +- .../protocol/EncryptedRecipientDataSpec.scala | 60 +++++ .../protocol/ExtendedQueriesCodecsSpec.scala | 10 +- .../protocol/FailureMessageCodecsSpec.scala | 6 +- .../protocol/LightningMessageCodecsSpec.scala | 30 +-- .../wire/protocol/OnionCodecsSpec.scala | 4 +- .../api/directives/ErrorDirective.scala | 4 +- .../api/directives/ExtraDirectives.scala | 8 +- .../acinq/eclair/api/handlers/Channel.scala | 4 +- .../fr/acinq/eclair/api/handlers/Fees.scala | 4 +- .../acinq/eclair/api/handlers/Invoice.scala | 8 +- .../fr/acinq/eclair/api/handlers/Node.scala | 4 +- .../api/serde/FormParamExtractors.scala | 4 +- eclair-node/src/test/resources/api/findroute | 15 +- .../src/test/resources/api/received-expired | 2 +- .../src/test/resources/api/received-pending | 2 +- .../src/test/resources/api/received-success | 2 +- .../src/test/resources/api/sent-failed | 2 +- .../src/test/resources/api/sent-pending | 2 +- .../src/test/resources/api/sent-success | 2 +- .../fr/acinq/eclair/api/ApiServiceSpec.scala | 54 ++-- 107 files changed, 1442 insertions(+), 1152 deletions(-) delete mode 100644 eclair-core/src/main/scala/fr/acinq/eclair/CoinUtils.scala create mode 100644 eclair-core/src/main/scala/fr/acinq/eclair/Timestamp.scala create mode 100644 eclair-core/src/main/scala/fr/acinq/eclair/wire/protocol/EncryptedRecipientDataTlv.scala delete mode 100644 eclair-core/src/test/scala/fr/acinq/eclair/CoinUtilsSpec.scala rename eclair-core/src/test/scala/fr/acinq/eclair/channel/states/b/{WaitForFundingCreatedInternalStateSpec.scala => WaitForFundingInternalStateSpec.scala} (68%) create mode 100644 eclair-core/src/test/scala/fr/acinq/eclair/wire/protocol/EncryptedRecipientDataSpec.scala diff --git a/docs/release-notes/eclair-vnext.md b/docs/release-notes/eclair-vnext.md index 55985d6403..806cccfeec 100644 --- a/docs/release-notes/eclair-vnext.md +++ b/docs/release-notes/eclair-vnext.md @@ -8,6 +8,46 @@ ### API changes +#### 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 + { + "timestamp": 1633357961 + } + ``` + - after + ```json + { + "timestamp": { + "iso": "2021-10-04T14:32:41Z", + "unix": 1633357961 + } + } + ``` +- milli-second precision timestamp: + - before: + ```json + { + "timestamp": 1633357961456 + } + ``` + - after (note how the unix format is in second precision): + ```json + { + "timestamp": { + "iso": "2021-10-04T14:32:41.456Z", + "unix": 1633357961 + } + } + ``` + This release contains many API updates: - `findroute`, `findroutetonode` and `findroutebetweennodes` supports new output format `full` (#1969) diff --git a/eclair-core/src/main/scala/fr/acinq/eclair/CoinUtils.scala b/eclair-core/src/main/scala/fr/acinq/eclair/CoinUtils.scala deleted file mode 100644 index 1bd0fdd81b..0000000000 --- a/eclair-core/src/main/scala/fr/acinq/eclair/CoinUtils.scala +++ /dev/null @@ -1,242 +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 - -import java.text.{DecimalFormat, NumberFormat} - -import fr.acinq.bitcoin.{Btc, BtcAmount, MilliBtc, Satoshi} -import grizzled.slf4j.Logging - -import scala.util.{Failure, Success, Try} - -/** - * Internal UI utility class, useful for lossless conversion between BtcAmount. - * The issue being that Satoshi contains a Long amount and it can not be converted to MilliSatoshi without losing the decimal part. - */ -private sealed trait BtcAmountGUILossless { - def amount_msat: Long - def unit: CoinUnit - def toMilliSatoshi: MilliSatoshi = MilliSatoshi(amount_msat) -} - -private case class GUIMSat(amount_msat: Long) extends BtcAmountGUILossless { - override def unit: CoinUnit = MSatUnit -} -private case class GUISat(amount_msat: Long) extends BtcAmountGUILossless { - override def unit: CoinUnit = SatUnit -} -private case class GUIBits(amount_msat: Long) extends BtcAmountGUILossless { - override def unit: CoinUnit = BitUnit -} -private case class GUIMBtc(amount_msat: Long) extends BtcAmountGUILossless { - override def unit: CoinUnit = MBtcUnit -} -private case class GUIBtc(amount_msat: Long) extends BtcAmountGUILossless { - override def unit: CoinUnit = BtcUnit -} - -sealed trait CoinUnit { - def code: String - def shortLabel: String - def label: String - def factorToMsat: Long -} - -case object MSatUnit extends CoinUnit { - override def code: String = "msat" - override def shortLabel: String = "mSat" - override def label: String = "MilliSatoshi" - override def factorToMsat: Long = 1L -} - -case object SatUnit extends CoinUnit { - override def code: String = "sat" - override def shortLabel: String = "sat" - override def label: String = "Satoshi" - override def factorToMsat: Long = 1000L // 1 sat = 1 000 msat -} - -case object BitUnit extends CoinUnit { - override def code: String = "bits" - override def shortLabel: String = "bits" - override def label: String = "Bits" - override def factorToMsat: Long = 100 * 1000L // 1 bit = 100 sat = 100 000 msat -} - -case object MBtcUnit extends CoinUnit { - override def code: String = "mbtc" - override def shortLabel: String = "mBTC" - override def label: String = "MilliBitcoin" - override def factorToMsat: Long = 1000 * 100000L // 1 mbtc = 1 00000 000 msat -} - -case object BtcUnit extends CoinUnit { - override def code: String = "btc" - override def shortLabel: String = "BTC" - override def label: String = "Bitcoin" - override def factorToMsat: Long = 1000 * 100000 * 1000L // 1 btc = 1 000 00000 000 msat -} - -object CoinUtils extends Logging { - - // msat pattern, no decimals allowed - val MILLI_SAT_PATTERN = "#,###,###,###,###,###,##0" - - // sat pattern decimals are optional - val SAT_PATTERN = "#,###,###,###,###,##0.###" - - // bits pattern always shows 2 decimals (msat optional) - val BITS_PATTERN = "##,###,###,###,##0.00###" - - // milli btc pattern always shows 5 decimals (msat optional) - val MILLI_BTC_PATTERN = "##,###,###,##0.00000###" - - // btc pattern always shows 8 decimals (msat optional). This is the default pattern. - val BTC_PATTERN = "##,###,##0.00000000###" - - var COIN_FORMAT: NumberFormat = new DecimalFormat(BTC_PATTERN) - - def setCoinPattern(pattern: String): Unit = { - COIN_FORMAT = new DecimalFormat(pattern) - } - - def getPatternFromUnit(unit: CoinUnit): String = { - unit match { - case MSatUnit => MILLI_SAT_PATTERN - case SatUnit => SAT_PATTERN - case BitUnit => BITS_PATTERN - case MBtcUnit => MILLI_BTC_PATTERN - case BtcUnit => BTC_PATTERN - case _ => throw new IllegalArgumentException("unhandled unit") - } - } - - /** - * Converts a string amount denominated in a bitcoin unit to a Millisatoshi amount. The amount might be truncated if - * it has too many decimals because MilliSatoshi only accepts Long amount. - * - * @param amount numeric String, can be decimal. - * @param unit bitcoin unit, can be milliSatoshi, Satoshi, Bits, milliBTC, BTC. - * @return amount as a MilliSatoshi object. - * @throws NumberFormatException if the amount parameter is not numeric. - * @throws IllegalArgumentException if the unit is not equals to milliSatoshi, Satoshi or milliBTC. - */ - @throws(classOf[NumberFormatException]) - @throws(classOf[IllegalArgumentException]) - def convertStringAmountToMsat(amount: String, unit: String): MilliSatoshi = { - val amountDecimal = BigDecimal(amount) - if (amountDecimal < 0) { - throw new IllegalArgumentException("amount must be equal or greater than 0") - } - // note: we can't use the fr.acinq.bitcoin._ conversion methods because they truncate the sub-satoshi part - getUnitFromString(unit) match { - case MSatUnit => MilliSatoshi((amountDecimal * MSatUnit.factorToMsat).longValue) - case SatUnit => MilliSatoshi((amountDecimal * SatUnit.factorToMsat).longValue) - case BitUnit => MilliSatoshi((amountDecimal * BitUnit.factorToMsat).longValue) - case MBtcUnit => MilliSatoshi((amountDecimal * MBtcUnit.factorToMsat).longValue) - case BtcUnit => MilliSatoshi((amountDecimal * BtcUnit.factorToMsat).longValue) - case _ => throw new IllegalArgumentException("unhandled unit") - } - } - - def convertStringAmountToSat(amount: String, unit: String): Satoshi = - CoinUtils.convertStringAmountToMsat(amount, unit).truncateToSatoshi - - /** - * Only BtcUnit, MBtcUnit, BitUnit, SatUnit and MSatUnit codes or label are supported. - */ - def getUnitFromString(unit: String): CoinUnit = unit.toLowerCase() match { - case u if u == MSatUnit.code || u == MSatUnit.label.toLowerCase() => MSatUnit - case u if u == SatUnit.code || u == SatUnit.label.toLowerCase() => SatUnit - case u if u == BitUnit.code || u == BitUnit.label.toLowerCase() => BitUnit - case u if u == MBtcUnit.code || u == MBtcUnit.label.toLowerCase() => MBtcUnit - case u if u == BtcUnit.code || u == BtcUnit.label.toLowerCase() => BtcUnit - case u => throw new IllegalArgumentException(s"unhandled unit=$u") - } - - /** - * Converts BtcAmount to a GUI Unit (wrapper containing amount as a millisatoshi long) - * - * @param amount BtcAmount - * @param unit unit to convert to - * @return a GUICoinAmount - */ - private def convertAmountToGUIUnit(amount: BtcAmount, unit: CoinUnit): BtcAmountGUILossless = (amount, unit) match { - // amount is satoshi, convert sat -> msat - case (a: Satoshi, MSatUnit) => GUIMSat(a.toLong * SatUnit.factorToMsat) - case (a: Satoshi, SatUnit) => GUISat(a.toLong * SatUnit.factorToMsat) - case (a: Satoshi, BitUnit) => GUIBits(a.toLong * SatUnit.factorToMsat) - case (a: Satoshi, MBtcUnit) => GUIMBtc(a.toLong * SatUnit.factorToMsat) - case (a: Satoshi, BtcUnit) => GUIBtc(a.toLong * SatUnit.factorToMsat) - - // amount is mbtc - case (a: MilliBtc, MSatUnit) => GUIMSat((a.toBigDecimal * MBtcUnit.factorToMsat).toLong) - case (a: MilliBtc, SatUnit) => GUISat((a.toBigDecimal * MBtcUnit.factorToMsat).toLong) - case (a: MilliBtc, BitUnit) => GUIBits((a.toBigDecimal * MBtcUnit.factorToMsat).toLong) - case (a: MilliBtc, MBtcUnit) => GUIMBtc((a.toBigDecimal * MBtcUnit.factorToMsat).toLong) - case (a: MilliBtc, BtcUnit) => GUIBtc((a.toBigDecimal * MBtcUnit.factorToMsat).toLong) - - // amount is mbtc - case (a: Btc, MSatUnit) => GUIMSat((a.toBigDecimal * BtcUnit.factorToMsat).toLong) - case (a: Btc, SatUnit) => GUISat((a.toBigDecimal * BtcUnit.factorToMsat).toLong) - case (a: Btc, BitUnit) => GUIBits((a.toBigDecimal * BtcUnit.factorToMsat).toLong) - case (a: Btc, MBtcUnit) => GUIMBtc((a.toBigDecimal * BtcUnit.factorToMsat).toLong) - case (a: Btc, BtcUnit) => GUIBtc((a.toBigDecimal * BtcUnit.factorToMsat).toLong) - - case (_, _) => - throw new IllegalArgumentException(s"unhandled conversion from $amount to $unit") - } - - /** - * Converts the amount to the user preferred unit and returns a localized formatted String. - * This method is useful for read only displays. - * - * @param amount BtcAmount - * @param withUnit if true, append the user unit shortLabel (mBTC, BTC, mSat...) - * @return formatted amount - */ - def formatAmountInUnit(amount: BtcAmount, unit: CoinUnit, withUnit: Boolean = false): String = { - val formatted = COIN_FORMAT.format(rawAmountInUnit(amount, unit)) - if (withUnit) s"$formatted ${unit.shortLabel}" else formatted - } - - def formatAmountInUnit(amount: MilliSatoshi, unit: CoinUnit, withUnit: Boolean): String = { - val formatted = COIN_FORMAT.format(rawAmountInUnit(amount, unit)) - if (withUnit) s"$formatted ${unit.shortLabel}" else formatted - } - - /** - * Converts the amount to the user preferred unit and returns the BigDecimal value. - * This method is useful to feed numeric text input without formatting. - * - * Returns -1 if the given amount can not be converted. - * - * @param amount BtcAmount - * @return BigDecimal value of the BtcAmount - */ - def rawAmountInUnit(amount: BtcAmount, unit: CoinUnit): BigDecimal = Try(convertAmountToGUIUnit(amount, unit) match { - case a: BtcAmountGUILossless => BigDecimal(a.amount_msat) / a.unit.factorToMsat - case a => throw new IllegalArgumentException(s"unhandled unit $a") - }) match { - case Success(b) => b - case Failure(t) => - logger.error("can not convert amount to user unit", t) - -1 - } - - def rawAmountInUnit(msat: MilliSatoshi, unit: CoinUnit): BigDecimal = BigDecimal(msat.toLong) / unit.factorToMsat -} 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 661c67467f..81615933dd 100644 --- a/eclair-core/src/main/scala/fr/acinq/eclair/Eclair.scala +++ b/eclair-core/src/main/scala/fr/acinq/eclair/Eclair.scala @@ -24,7 +24,6 @@ import akka.util.Timeout import com.softwaremill.quicklens.ModifyPimp import fr.acinq.bitcoin.Crypto.PublicKey import fr.acinq.bitcoin.{ByteVector32, ByteVector64, Crypto, Satoshi} -import fr.acinq.eclair.TimestampQueryFilters._ import fr.acinq.eclair.balance.CheckBalance.GlobalBalance import fr.acinq.eclair.balance.{BalanceActor, ChannelsListener} import fr.acinq.eclair.blockchain.OnChainWallet.OnChainBalance @@ -58,25 +57,10 @@ case class GetInfoResponse(version: String, nodeId: PublicKey, alias: String, co case class AuditResponse(sent: Seq[PaymentSent], received: Seq[PaymentReceived], relayed: Seq[PaymentRelayed]) -case class TimestampQueryFilters(from: Long, to: Long) - case class SignedMessage(nodeId: PublicKey, message: String, signature: ByteVector) case class VerifiedMessage(valid: Boolean, publicKey: PublicKey) -object TimestampQueryFilters { - /** We use this in the context of timestamp filtering, when we don't need an upper bound. */ - val MaxEpochMilliseconds: Long = Duration.fromNanos(Long.MaxValue).toMillis - - def getDefaultTimestampFilters(from_opt: Option[Long], to_opt: Option[Long]): TimestampQueryFilters = { - // NB: we expect callers to use seconds, but internally we use milli-seconds everywhere. - val from = from_opt.getOrElse(0L).seconds.toMillis - val to = to_opt.map(_.seconds.toMillis).getOrElse(MaxEpochMilliseconds) - - TimestampQueryFilters(from, to) - } -} - object SignedMessage { def signedBytes(message: ByteVector): ByteVector32 = Crypto.hash256(ByteVector("Lightning Signed Message:".getBytes(StandardCharsets.UTF_8)) ++ message) @@ -130,19 +114,19 @@ trait Eclair { def sendToRoute(amount: MilliSatoshi, recipientAmount_opt: Option[MilliSatoshi], externalId_opt: Option[String], parentId_opt: Option[UUID], invoice: PaymentRequest, finalCltvExpiryDelta: CltvExpiryDelta, route: PredefinedRoute, trampolineSecret_opt: Option[ByteVector32] = None, trampolineFees_opt: Option[MilliSatoshi] = None, trampolineExpiryDelta_opt: Option[CltvExpiryDelta] = None, trampolineNodes_opt: Seq[PublicKey] = Nil)(implicit timeout: Timeout): Future[SendPaymentToRouteResponse] - def audit(from_opt: Option[Long], to_opt: Option[Long])(implicit timeout: Timeout): Future[AuditResponse] + def audit(from: TimestampSecond, to: TimestampSecond)(implicit timeout: Timeout): Future[AuditResponse] - def networkFees(from_opt: Option[Long], to_opt: Option[Long])(implicit timeout: Timeout): Future[Seq[NetworkFee]] + def networkFees(from: TimestampSecond, to: TimestampSecond)(implicit timeout: Timeout): Future[Seq[NetworkFee]] - def channelStats(from_opt: Option[Long], to_opt: Option[Long])(implicit timeout: Timeout): Future[Seq[Stats]] + 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_opt: Option[Long], to_opt: Option[Long])(implicit timeout: Timeout): Future[Seq[PaymentRequest]] + def pendingInvoices(from: TimestampSecond, to: TimestampSecond)(implicit timeout: Timeout): Future[Seq[PaymentRequest]] - def allInvoices(from_opt: Option[Long], to_opt: Option[Long])(implicit timeout: Timeout): Future[Seq[PaymentRequest]] + def allInvoices(from: TimestampSecond, to: TimestampSecond)(implicit timeout: Timeout): Future[Seq[PaymentRequest]] def allChannels()(implicit timeout: Timeout): Future[Iterable[ChannelDesc]] @@ -180,8 +164,8 @@ class EclairImpl(appKit: Kit) extends Eclair with Logging { } override def open(nodeId: PublicKey, fundingAmount: Satoshi, pushAmount_opt: Option[MilliSatoshi], channelType_opt: Option[SupportedChannelType], fundingFeeratePerByte_opt: Option[FeeratePerByte], flags_opt: Option[Int], openTimeout_opt: Option[Timeout])(implicit timeout: Timeout): Future[ChannelOpenResponse] = { - // we want the open timeout to expire *before* the default ask timeout, otherwise user won't get a generic response - val openTimeout = openTimeout_opt.getOrElse(Timeout(10 seconds)) + // we want the open timeout to expire *before* the default ask timeout, otherwise user will get a generic response + val openTimeout = openTimeout_opt.getOrElse(Timeout(20 seconds)) (appKit.switchboard ? Peer.OpenChannel( remoteNodeId = nodeId, fundingSatoshis = fundingAmount, @@ -385,35 +369,30 @@ class EclairImpl(appKit: Kit) extends Eclair with Logging { appKit.nodeParams.db.payments.getIncomingPayment(paymentHash) } - override def audit(from_opt: Option[Long], to_opt: Option[Long])(implicit timeout: Timeout): Future[AuditResponse] = { - val filter = getDefaultTimestampFilters(from_opt, to_opt) + override def audit(from: TimestampSecond, to: TimestampSecond)(implicit timeout: Timeout): Future[AuditResponse] = { Future(AuditResponse( - sent = appKit.nodeParams.db.audit.listSent(filter.from, filter.to), - received = appKit.nodeParams.db.audit.listReceived(filter.from, filter.to), - relayed = appKit.nodeParams.db.audit.listRelayed(filter.from, filter.to) + sent = appKit.nodeParams.db.audit.listSent(from.toTimestampMilli, to.toTimestampMilli), + received = appKit.nodeParams.db.audit.listReceived(from.toTimestampMilli, to.toTimestampMilli), + relayed = appKit.nodeParams.db.audit.listRelayed(from.toTimestampMilli, to.toTimestampMilli) )) } - override def networkFees(from_opt: Option[Long], to_opt: Option[Long])(implicit timeout: Timeout): Future[Seq[NetworkFee]] = { - val filter = getDefaultTimestampFilters(from_opt, to_opt) - Future(appKit.nodeParams.db.audit.listNetworkFees(filter.from, filter.to)) + override def networkFees(from: TimestampSecond, to: TimestampSecond)(implicit timeout: Timeout): Future[Seq[NetworkFee]] = { + Future(appKit.nodeParams.db.audit.listNetworkFees(from.toTimestampMilli, to.toTimestampMilli)) } - override def channelStats(from_opt: Option[Long], to_opt: Option[Long])(implicit timeout: Timeout): Future[Seq[Stats]] = { - val filter = getDefaultTimestampFilters(from_opt, to_opt) - Future(appKit.nodeParams.db.audit.stats(filter.from, filter.to)) + override def channelStats(from: TimestampSecond, to: TimestampSecond)(implicit timeout: Timeout): Future[Seq[Stats]] = { + 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_opt: Option[Long], to_opt: Option[Long])(implicit timeout: Timeout): Future[Seq[PaymentRequest]] = Future { - val filter = getDefaultTimestampFilters(from_opt, to_opt) - appKit.nodeParams.db.payments.listIncomingPayments(filter.from, filter.to).map(_.paymentRequest) + 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) } - override def pendingInvoices(from_opt: Option[Long], to_opt: Option[Long])(implicit timeout: Timeout): Future[Seq[PaymentRequest]] = Future { - val filter = getDefaultTimestampFilters(from_opt, to_opt) - appKit.nodeParams.db.payments.listPendingIncomingPayments(filter.from, filter.to).map(_.paymentRequest) + override def pendingInvoices(from: TimestampSecond, to: TimestampSecond)(implicit timeout: Timeout): Future[Seq[PaymentRequest]] = Future { + appKit.nodeParams.db.payments.listPendingIncomingPayments(from.toTimestampMilli, to.toTimestampMilli).map(_.paymentRequest) } override def getInvoice(paymentHash: ByteVector32)(implicit timeout: Timeout): Future[Option[PaymentRequest]] = Future { diff --git a/eclair-core/src/main/scala/fr/acinq/eclair/Timestamp.scala b/eclair-core/src/main/scala/fr/acinq/eclair/Timestamp.scala new file mode 100644 index 0000000000..9b2af2b0cb --- /dev/null +++ b/eclair-core/src/main/scala/fr/acinq/eclair/Timestamp.scala @@ -0,0 +1,59 @@ +/* + * 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 + +import java.sql +import java.time.Instant +import scala.concurrent.duration.{DurationLong, FiniteDuration} + +case class TimestampSecond(private val underlying: Long) extends Ordered[TimestampSecond] { + // @formatter:off + def toLong: Long = underlying + def toTimestampMilli: TimestampMilli = TimestampMilli(underlying * 1000) + def toSqlTimestamp: sql.Timestamp = sql.Timestamp.from(Instant.ofEpochSecond(underlying)) + override def toString: String = s"$underlying unixsec" + override def compare(that: TimestampSecond): Int = underlying.compareTo(that.underlying) + def +(x: Long): TimestampSecond = TimestampSecond(underlying + x) + def -(x: Long): TimestampSecond = TimestampSecond(underlying - x) + def +(x: FiniteDuration): TimestampSecond = TimestampSecond(underlying + x.toSeconds) + def -(x: FiniteDuration): TimestampSecond = TimestampSecond(underlying - x.toSeconds) + def -(x: TimestampSecond): FiniteDuration = (underlying - x.underlying).seconds + // @formatter:on +} + +object TimestampSecond { + def now(): TimestampSecond = TimestampSecond(System.currentTimeMillis() / 1000) +} + +case class TimestampMilli(private val underlying: Long) extends Ordered[TimestampMilli] { + // @formatter:off + def toLong: Long = underlying + def toSqlTimestamp: sql.Timestamp = sql.Timestamp.from(Instant.ofEpochMilli(underlying)) + override def toString: String = s"$underlying unixms" + override def compare(that: TimestampMilli): Int = underlying.compareTo(that.underlying) + def +(x: FiniteDuration): TimestampMilli = TimestampMilli(underlying + x.toMillis) + def -(x: FiniteDuration): TimestampMilli = TimestampMilli(underlying - x.toMillis) + def -(x: TimestampMilli): FiniteDuration = (underlying - x.underlying).millis + // @formatter:on +} + +object TimestampMilli { + // @formatter:off + def now(): TimestampMilli = TimestampMilli(System.currentTimeMillis()) + def fromSqlTimestamp(sqlTs: sql.Timestamp): TimestampMilli = TimestampMilli(sqlTs.getTime) + // @formatter:on +} \ No newline at end of file diff --git a/eclair-core/src/main/scala/fr/acinq/eclair/blockchain/bitcoind/ZmqWatcher.scala b/eclair-core/src/main/scala/fr/acinq/eclair/blockchain/bitcoind/ZmqWatcher.scala index e42d2e6408..829984f391 100644 --- a/eclair-core/src/main/scala/fr/acinq/eclair/blockchain/bitcoind/ZmqWatcher.scala +++ b/eclair-core/src/main/scala/fr/acinq/eclair/blockchain/bitcoind/ZmqWatcher.scala @@ -25,7 +25,7 @@ import fr.acinq.eclair.blockchain._ import fr.acinq.eclair.blockchain.bitcoind.rpc.BitcoinCoreClient import fr.acinq.eclair.blockchain.watchdogs.BlockchainWatchdog import fr.acinq.eclair.wire.protocol.ChannelAnnouncement -import fr.acinq.eclair.{KamonExt, NodeParams, ShortChannelId} +import fr.acinq.eclair.{KamonExt, NodeParams, ShortChannelId, TimestampSecond} import java.util.concurrent.atomic.AtomicLong import scala.concurrent.duration._ @@ -68,7 +68,7 @@ object ZmqWatcher { final case class ValidateResult(c: ChannelAnnouncement, fundingTx: Either[Throwable, (Transaction, UtxoStatus)]) final case class GetTxWithMeta(replyTo: ActorRef[GetTxWithMetaResponse], txid: ByteVector32) extends Command - final case class GetTxWithMetaResponse(txid: ByteVector32, tx_opt: Option[Transaction], lastBlockTimestamp: Long) + final case class GetTxWithMetaResponse(txid: ByteVector32, tx_opt: Option[Transaction], lastBlockTimestamp: TimestampSecond) sealed trait UtxoStatus object UtxoStatus { diff --git a/eclair-core/src/main/scala/fr/acinq/eclair/blockchain/bitcoind/rpc/BitcoinCoreClient.scala b/eclair-core/src/main/scala/fr/acinq/eclair/blockchain/bitcoind/rpc/BitcoinCoreClient.scala index fc97fea8ab..18f2686d5a 100644 --- a/eclair-core/src/main/scala/fr/acinq/eclair/blockchain/bitcoind/rpc/BitcoinCoreClient.scala +++ b/eclair-core/src/main/scala/fr/acinq/eclair/blockchain/bitcoind/rpc/BitcoinCoreClient.scala @@ -19,7 +19,7 @@ package fr.acinq.eclair.blockchain.bitcoind.rpc import fr.acinq.bitcoin.Crypto.PublicKey import fr.acinq.bitcoin._ import fr.acinq.eclair.ShortChannelId.coordinates -import fr.acinq.eclair.TxCoordinates +import fr.acinq.eclair.{TimestampSecond, TxCoordinates} import fr.acinq.eclair.blockchain.OnChainWallet import fr.acinq.eclair.blockchain.OnChainWallet.{MakeFundingTxResponse, OnChainBalance} import fr.acinq.eclair.blockchain.bitcoind.ZmqWatcher.{GetTxWithMetaResponse, UtxoStatus, ValidateResult} @@ -62,7 +62,7 @@ class BitcoinCoreClient(val rpcClient: BitcoinJsonRPCClient) extends OnChainWall tx_opt <- getTransaction(txid).map(Some(_)).recover { case _ => None } blockchainInfo <- rpcClient.invoke("getblockchaininfo") JInt(timestamp) = blockchainInfo \ "mediantime" - } yield GetTxWithMetaResponse(txid, tx_opt, timestamp.toLong) + } yield GetTxWithMetaResponse(txid, tx_opt, TimestampSecond(timestamp.toLong)) /** Get the number of confirmations of a given transaction. */ def getTxConfirmations(txid: ByteVector32)(implicit ec: ExecutionContext): Future[Option[Int]] = diff --git a/eclair-core/src/main/scala/fr/acinq/eclair/blockchain/watchdogs/BlockchainWatchdog.scala b/eclair-core/src/main/scala/fr/acinq/eclair/blockchain/watchdogs/BlockchainWatchdog.scala index 66745dde2c..501504ff1a 100644 --- a/eclair-core/src/main/scala/fr/acinq/eclair/blockchain/watchdogs/BlockchainWatchdog.scala +++ b/eclair-core/src/main/scala/fr/acinq/eclair/blockchain/watchdogs/BlockchainWatchdog.scala @@ -121,8 +121,10 @@ object BlockchainWatchdog { if (missingBlocks >= 6) { context.log.warn("{}: we are {} blocks late: we may be eclipsed from the bitcoin network", source, missingBlocks) context.system.eventStream ! EventStream.Publish(DangerousBlocksSkew(headers)) - } else { + } else if (missingBlocks > 0) { context.log.info("{}: we are {} blocks late", source, missingBlocks) + } else { + context.log.debug("{}: we are {} blocks late", source, missingBlocks) } Metrics.BitcoinBlocksSkew.withTag(Tags.Source, source).update(missingBlocks.toDouble) Behaviors.same diff --git a/eclair-core/src/main/scala/fr/acinq/eclair/channel/Channel.scala b/eclair-core/src/main/scala/fr/acinq/eclair/channel/Channel.scala index c92e9fcff4..60882860df 100644 --- a/eclair-core/src/main/scala/fr/acinq/eclair/channel/Channel.scala +++ b/eclair-core/src/main/scala/fr/acinq/eclair/channel/Channel.scala @@ -122,15 +122,6 @@ object Channel { // we will receive this message when we waited too long for a revocation for that commit number (NB: we explicitly specify the peer to allow for testing) case class RevocationTimeout(remoteCommitNumber: Long, peer: ActorRef) - /** - * Outgoing messages go through the [[Peer]] for logging purposes. - * - * [[Channel]] is notified asynchronously of disconnections and reconnections. To preserve sequentiality of messages, - * we need to also provide the connection that the message is valid for. If the actual connection was reset in the - * meantime, the [[Peer]] will simply drop the message. - */ - case class OutgoingMessage(msg: LightningMessage, peerConnection: ActorRef) - /** We don't immediately process [[CurrentBlockCount]] to avoid herd effects */ case class ProcessCurrentBlockCount(c: CurrentBlockCount) @@ -241,7 +232,7 @@ class Channel(val nodeParams: NodeParams, val wallet: OnChainChannelFunder, remo goto(WAIT_FOR_OPEN_CHANNEL) using DATA_WAIT_FOR_OPEN_CHANNEL(inputFundee) case Event(INPUT_RESTORED(data), _) => - log.info("restoring channel") + log.debug("restoring channel") context.system.eventStream.publish(ChannelRestored(self, data.channelId, peer, remoteNodeId, data)) txPublisher ! SetChannelId(remoteNodeId, data.channelId) data match { @@ -394,7 +385,9 @@ class Channel(val nodeParams: NodeParams, val wallet: OnChainChannelFunder, remo when(WAIT_FOR_ACCEPT_CHANNEL)(handleExceptions { case Event(accept: AcceptChannel, d@DATA_WAIT_FOR_ACCEPT_CHANNEL(INPUT_INIT_FUNDER(temporaryChannelId, fundingSatoshis, pushMsat, initialFeeratePerKw, fundingTxFeeratePerKw, localParams, _, remoteInit, _, channelConfig, channelType), open)) => Helpers.validateParamsFunder(nodeParams, channelType, localParams.initFeatures, remoteInit.features, open, accept) match { - case Left(t) => handleLocalError(t, d, Some(accept)) + case Left(t) => + channelOpenReplyToUser(Left(LocalError(t))) + handleLocalError(t, d, Some(accept)) case Right((channelFeatures, remoteShutdownScript)) => val remoteParams = RemoteParams( nodeId = remoteNodeId, @@ -551,7 +544,7 @@ class Channel(val nodeParams: NodeParams, val wallet: OnChainChannelFunder, remo originChannels = Map.empty, remoteNextCommitInfo = Right(randomKey().publicKey), // we will receive their next per-commitment point in the next message, so we temporarily put a random byte array commitInput, ShaChain.init) - val now = System.currentTimeMillis.milliseconds.toSeconds + val blockHeight = nodeParams.currentBlockHeight context.system.eventStream.publish(ChannelSignatureReceived(self, commitments)) log.info(s"publishing funding tx for channelId=$channelId fundingTxid=${commitInput.outPoint.txid}") watchFundingTx(commitments) @@ -574,7 +567,7 @@ class Channel(val nodeParams: NodeParams, val wallet: OnChainChannelFunder, remo } } - goto(WAIT_FOR_FUNDING_CONFIRMED) using DATA_WAIT_FOR_FUNDING_CONFIRMED(commitments, Some(fundingTx), now, None, Left(fundingCreated)) storing() calling publishFundingTx() + goto(WAIT_FOR_FUNDING_CONFIRMED) using DATA_WAIT_FOR_FUNDING_CONFIRMED(commitments, Some(fundingTx), blockHeight, None, Left(fundingCreated)) storing() calling publishFundingTx() } case Event(c: CloseCommand, d: DATA_WAIT_FOR_FUNDING_SIGNED) => @@ -1026,7 +1019,7 @@ class Channel(val nodeParams: NodeParams, val wallet: OnChainChannelFunder, remo goto(NORMAL) using d.copy(channelUpdate = channelUpdate1) storing() case Event(BroadcastChannelUpdate(reason), d: DATA_NORMAL) => - val age = System.currentTimeMillis.milliseconds - d.channelUpdate.timestamp.seconds + val age = TimestampSecond.now() - d.channelUpdate.timestamp val channelUpdate1 = Announcements.makeChannelUpdate(nodeParams.chainHash, nodeParams.privateKey, remoteNodeId, d.shortChannelId, d.channelUpdate.cltvExpiryDelta, d.channelUpdate.htlcMinimumMsat, d.channelUpdate.feeBaseMsat, d.channelUpdate.feeProportionalMillionths, d.commitments.capacity.toMilliSatoshi, enable = Helpers.aboveReserve(d.commitments)) reason match { case Reconnected if d.commitments.announceChannel && Announcements.areSame(channelUpdate1, d.channelUpdate) && age < REFRESH_CHANNEL_UPDATE_INTERVAL => @@ -1506,8 +1499,8 @@ class Channel(val nodeParams: NodeParams, val wallet: OnChainChannelFunder, remo } // we may need to fail some htlcs in case a commitment tx was published and they have reached the timeout threshold val timedOutHtlcs = Closing.isClosingTypeAlreadyKnown(d1) match { - case Some(c: Closing.LocalClose) => Closing.timedOutHtlcs(d.commitments.commitmentFormat, c.localCommit, c.localCommitPublished, d.commitments.localParams.dustLimit, tx) - case Some(c: Closing.RemoteClose) => Closing.timedOutHtlcs(d.commitments.commitmentFormat, c.remoteCommit, c.remoteCommitPublished, d.commitments.remoteParams.dustLimit, tx) + case Some(c: Closing.LocalClose) => Closing.trimmedOrTimedOutHtlcs(d.commitments.commitmentFormat, c.localCommit, c.localCommitPublished, d.commitments.localParams.dustLimit, tx) + case Some(c: Closing.RemoteClose) => Closing.trimmedOrTimedOutHtlcs(d.commitments.commitmentFormat, c.remoteCommit, c.remoteCommitPublished, d.commitments.remoteParams.dustLimit, tx) case _ => Set.empty[UpdateAddHtlc] // we lose htlc outputs in dataloss protection scenarios (future remote commit) } timedOutHtlcs.foreach { add => @@ -2686,7 +2679,7 @@ class Channel(val nodeParams: NodeParams, val wallet: OnChainChannelFunder, remo } private def send(msg: LightningMessage): Unit = { - peer ! OutgoingMessage(msg, activeConnection) + peer ! Peer.OutgoingMessage(msg, activeConnection) } override def mdc(currentMessage: Any): MDC = { diff --git a/eclair-core/src/main/scala/fr/acinq/eclair/channel/ChannelFeatures.scala b/eclair-core/src/main/scala/fr/acinq/eclair/channel/ChannelFeatures.scala index ea17f23cb9..4c63af0868 100644 --- a/eclair-core/src/main/scala/fr/acinq/eclair/channel/ChannelFeatures.scala +++ b/eclair-core/src/main/scala/fr/acinq/eclair/channel/ChannelFeatures.scala @@ -113,14 +113,13 @@ object ChannelTypes { } // @formatter:on + private val features2ChannelType: Map[Features, SupportedChannelType] = Set(Standard, StaticRemoteKey, AnchorOutputs, AnchorOutputsZeroFeeHtlcTx) + .map(channelType => Features(channelType.features.map(_ -> FeatureSupport.Mandatory).toMap) -> channelType) + .toMap + // NB: Bolt 2: features must exactly match in order to identify a channel type. - def fromFeatures(features: Features): ChannelType = features match { - case f if f == Features(Features.StaticRemoteKey -> FeatureSupport.Mandatory, Features.AnchorOutputsZeroFeeHtlcTx -> FeatureSupport.Mandatory) => AnchorOutputsZeroFeeHtlcTx - case f if f == Features(Features.StaticRemoteKey -> FeatureSupport.Mandatory, Features.AnchorOutputs -> FeatureSupport.Mandatory) => AnchorOutputs - case f if f == Features(Features.StaticRemoteKey -> FeatureSupport.Mandatory) => StaticRemoteKey - case f if f == Features.empty => Standard - case _ => UnsupportedChannelType(features) - } + def fromFeatures(features: Features): ChannelType = features2ChannelType.getOrElse(features, UnsupportedChannelType(features)) + /** Pick the channel type based on local and remote feature bits, as defined by the spec. */ def defaultFromFeatures(localFeatures: Features, remoteFeatures: Features): SupportedChannelType = { diff --git a/eclair-core/src/main/scala/fr/acinq/eclair/channel/Helpers.scala b/eclair-core/src/main/scala/fr/acinq/eclair/channel/Helpers.scala index 82deec7238..a223c19fec 100644 --- a/eclair-core/src/main/scala/fr/acinq/eclair/channel/Helpers.scala +++ b/eclair-core/src/main/scala/fr/acinq/eclair/channel/Helpers.scala @@ -204,8 +204,8 @@ object Helpers { * * @return the delay until the next update */ - def nextChannelUpdateRefresh(currentUpdateTimestamp: Long)(implicit log: DiagnosticLoggingAdapter): FiniteDuration = { - val age = System.currentTimeMillis.milliseconds - currentUpdateTimestamp.seconds + def nextChannelUpdateRefresh(currentUpdateTimestamp: TimestampSecond)(implicit log: DiagnosticLoggingAdapter): FiniteDuration = { + val age = TimestampSecond.now() - currentUpdateTimestamp val delay = 0.days.max(REFRESH_CHANNEL_UPDATE_INTERVAL - age) Logs.withMdc(log)(Logs.mdc(category_opt = Some(Logs.LogCategory.CONNECTION))) { log.debug("current channel_update was created {} days ago, will refresh it in {} days", age.toDays, delay.toDays) @@ -322,10 +322,10 @@ object Helpers { */ def checkLocalCommit(d: HasCommitments, nextRemoteRevocationNumber: Long): Boolean = { if (d.commitments.localCommit.index == nextRemoteRevocationNumber) { - // they just sent a new commit_sig, we have received it but they didn't receive our revocation + // we are in sync true } else if (d.commitments.localCommit.index == nextRemoteRevocationNumber + 1) { - // we are in sync + // they just sent a new commit_sig, we have received it but they didn't receive our revocation true } else if (d.commitments.localCommit.index > nextRemoteRevocationNumber + 1) { // remote is behind: we return true because things are fine on our side @@ -1027,12 +1027,13 @@ object Helpers { /** * In CLOSING state, when we are notified that a transaction has been confirmed, we analyze it to find out if one or - * more htlcs have timed out and need to be failed in an upstream channel. + * more htlcs have timed out and need to be failed in an upstream channel. Trimmed htlcs can be failed as soon as + * the commitment tx has been confirmed. * * @param tx a tx that has reached mindepth * @return a set of htlcs that need to be failed upstream */ - def timedOutHtlcs(commitmentFormat: CommitmentFormat, localCommit: LocalCommit, localCommitPublished: LocalCommitPublished, localDustLimit: Satoshi, tx: Transaction)(implicit log: LoggingAdapter): Set[UpdateAddHtlc] = { + def trimmedOrTimedOutHtlcs(commitmentFormat: CommitmentFormat, localCommit: LocalCommit, localCommitPublished: LocalCommitPublished, localDustLimit: Satoshi, tx: Transaction)(implicit log: LoggingAdapter): Set[UpdateAddHtlc] = { val untrimmedHtlcs = Transactions.trimOfferedHtlcs(localDustLimit, localCommit.spec, commitmentFormat).map(_.add) if (tx.txid == localCommit.commitTxAndRemoteSig.commitTx.tx.txid) { // the tx is a commitment tx, we can immediately fail all dust htlcs (they don't have an output in the tx) @@ -1068,12 +1069,13 @@ object Helpers { /** * In CLOSING state, when we are notified that a transaction has been confirmed, we analyze it to find out if one or - * more htlcs have timed out and need to be failed in an upstream channel. + * more htlcs have timed out and need to be failed in an upstream channel. Trimmed htlcs can be failed as soon as + * the commitment tx has been confirmed. * * @param tx a tx that has reached mindepth * @return a set of htlcs that need to be failed upstream */ - def timedOutHtlcs(commitmentFormat: CommitmentFormat, remoteCommit: RemoteCommit, remoteCommitPublished: RemoteCommitPublished, remoteDustLimit: Satoshi, tx: Transaction)(implicit log: LoggingAdapter): Set[UpdateAddHtlc] = { + def trimmedOrTimedOutHtlcs(commitmentFormat: CommitmentFormat, remoteCommit: RemoteCommit, remoteCommitPublished: RemoteCommitPublished, remoteDustLimit: Satoshi, tx: Transaction)(implicit log: LoggingAdapter): Set[UpdateAddHtlc] = { val untrimmedHtlcs = Transactions.trimReceivedHtlcs(remoteDustLimit, remoteCommit.spec, commitmentFormat).map(_.add) if (tx.txid == remoteCommit.txid) { // the tx is a commitment tx, we can immediately fail all dust htlcs (they don't have an output in the tx) diff --git a/eclair-core/src/main/scala/fr/acinq/eclair/channel/publish/TxPublisher.scala b/eclair-core/src/main/scala/fr/acinq/eclair/channel/publish/TxPublisher.scala index 420d385b8d..502393356b 100644 --- a/eclair-core/src/main/scala/fr/acinq/eclair/channel/publish/TxPublisher.scala +++ b/eclair-core/src/main/scala/fr/acinq/eclair/channel/publish/TxPublisher.scala @@ -251,8 +251,10 @@ private class TxPublisher(nodeParams: NodeParams, factory: TxPublisher.ChildFact } case WrappedCurrentBlockCount(currentBlockCount) => - log.info("{} transactions are still pending at block {}, retrying {} transactions that previously failed", pending.size, currentBlockCount, retryNextBlock.length) - retryNextBlock.foreach(cmd => timers.startSingleTimer(cmd, (1 + Random.nextLong(nodeParams.maxTxPublishRetryDelay.toMillis)).millis)) + if (retryNextBlock.nonEmpty) { + log.info("{} transactions are still pending at block {}, retrying {} transactions that previously failed", pending.size, currentBlockCount, retryNextBlock.length) + retryNextBlock.foreach(cmd => timers.startSingleTimer(cmd, (1 + Random.nextLong(nodeParams.maxTxPublishRetryDelay.toMillis)).millis)) + } run(pending, Seq.empty, channelInfo) case SetChannelId(remoteNodeId, channelId) => diff --git a/eclair-core/src/main/scala/fr/acinq/eclair/crypto/Sphinx.scala b/eclair-core/src/main/scala/fr/acinq/eclair/crypto/Sphinx.scala index 8213fe9600..73e8c88a81 100644 --- a/eclair-core/src/main/scala/fr/acinq/eclair/crypto/Sphinx.scala +++ b/eclair-core/src/main/scala/fr/acinq/eclair/crypto/Sphinx.scala @@ -372,5 +372,93 @@ object Sphinx extends Logging { } + /** + * Route blinding is a lightweight technique to provide recipient anonymity by blinding an arbitrary amount of hops at + * the end of an onion path. It can be used for payments or onion messages. + */ + object RouteBlinding { + + /** + * @param publicKey introduction node's public key (which cannot be blinded since the sender need to find a route to it). + * @param blindingEphemeralKey blinding tweak that can be used by the introduction node to derive the private key that + * lets it decrypt the encrypted payload. + * @param encryptedPayload encrypted payload that can be decrypted with the introduction node's private key and the + * blinding ephemeral key. + */ + case class IntroductionNode(publicKey: PublicKey, blindingEphemeralKey: PublicKey, encryptedPayload: ByteVector) + + /** + * @param blindedPublicKey blinded public key, which hides the real public key. + * @param blindingEphemeralKey blinding tweak that can be used by the receiving node to derive the private key that + * matches the blinded public key. + * @param encryptedPayload encrypted payload that can be decrypted with the receiving node's private key and the + * blinding ephemeral key. + */ + case class BlindedNode(blindedPublicKey: PublicKey, blindingEphemeralKey: PublicKey, encryptedPayload: ByteVector) + + /** + * @param introductionNode the first node should not be blinded, otherwise the sender cannot locate it. + * @param blindedNodes blinded nodes (not including the introduction node). + */ + case class BlindedRoute(introductionNode: IntroductionNode, blindedNodes: Seq[BlindedNode]) { + val nodeIds: Seq[PublicKey] = introductionNode.publicKey +: blindedNodes.map(_.blindedPublicKey) + val blindingEphemeralKeys: Seq[PublicKey] = introductionNode.blindingEphemeralKey +: blindedNodes.map(_.blindingEphemeralKey) + val encryptedPayloads: Seq[ByteVector] = introductionNode.encryptedPayload +: blindedNodes.map(_.encryptedPayload) + } + + /** + * Blind the provided route and encrypt intermediate nodes' payloads. + * + * @param sessionKey this node's session key. + * @param publicKeys public keys of each node on the route, starting from the introduction point. + * @param payloads payloads that should be encrypted for each node on the route. + * @return a blinded route. + */ + def create(sessionKey: PrivateKey, publicKeys: Seq[PublicKey], payloads: Seq[ByteVector]): BlindedRoute = { + require(publicKeys.length == payloads.length, "a payload must be provided for each node in the blinded path") + var e = sessionKey + val blindedHops = publicKeys.zip(payloads).map { case (publicKey, payload) => + val blindingKey = e.publicKey + val sharedSecret = computeSharedSecret(publicKey, e) + val blindedPublicKey = blind(publicKey, generateKey("blinded_node_id", sharedSecret)) + val rho = generateKey("rho", sharedSecret) + val (encryptedPayload, mac) = ChaCha20Poly1305.encrypt(rho, zeroes(12), payload, ByteVector.empty) + e = e.multiply(PrivateKey(Crypto.sha256(blindingKey.value ++ sharedSecret.bytes))) + BlindedNode(blindedPublicKey, blindingKey, encryptedPayload ++ mac) + } + val introductionNode = IntroductionNode(publicKeys.head, blindedHops.head.blindingEphemeralKey, blindedHops.head.encryptedPayload) + BlindedRoute(introductionNode, blindedHops.tail) + } + + /** + * Compute the blinded private key that must be used to decrypt an incoming blinded onion. + * + * @param privateKey this node's private key. + * @param blindingEphemeralKey unblinding ephemeral key. + * @return this node's blinded private key. + */ + def derivePrivateKey(privateKey: PrivateKey, blindingEphemeralKey: PublicKey): PrivateKey = { + val sharedSecret = computeSharedSecret(blindingEphemeralKey, privateKey) + privateKey.multiply(PrivateKey(generateKey("blinded_node_id", sharedSecret))) + } + + /** + * Decrypt the encrypted payload (usually found in the onion) that contains instructions to locate the next node. + * + * @param privateKey this node's private key. + * @param blindingEphemeralKey unblinding ephemeral key. + * @param encryptedPayload encrypted payload for this node. + * @return a tuple (decrypted payload, unblinding ephemeral key for the next node) + */ + def decryptPayload(privateKey: PrivateKey, blindingEphemeralKey: PublicKey, encryptedPayload: ByteVector): Try[(ByteVector, PublicKey)] = Try { + val sharedSecret = computeSharedSecret(blindingEphemeralKey, privateKey) + val rho = generateKey("rho", sharedSecret) + val decrypted = ChaCha20Poly1305.decrypt(rho, zeroes(12), encryptedPayload.dropRight(16), ByteVector.empty, encryptedPayload.takeRight(16)) + val nextBlindingEphemeralKey = blind(blindingEphemeralKey, computeBlindingFactor(blindingEphemeralKey, sharedSecret)) + (decrypted, nextBlindingEphemeralKey) + } + + } + } diff --git a/eclair-core/src/main/scala/fr/acinq/eclair/crypto/WeakEntropyPool.scala b/eclair-core/src/main/scala/fr/acinq/eclair/crypto/WeakEntropyPool.scala index 8a9dae4e47..69a686ea57 100644 --- a/eclair-core/src/main/scala/fr/acinq/eclair/crypto/WeakEntropyPool.scala +++ b/eclair-core/src/main/scala/fr/acinq/eclair/crypto/WeakEntropyPool.scala @@ -21,6 +21,7 @@ import akka.actor.typed.eventstream.EventStream import akka.actor.typed.scaladsl.Behaviors import fr.acinq.bitcoin.Crypto.PublicKey import fr.acinq.bitcoin.{ByteVector32, ByteVector64, Crypto} +import fr.acinq.eclair.TimestampMilli import fr.acinq.eclair.blockchain.NewBlock import fr.acinq.eclair.channel.ChannelSignatureReceived import fr.acinq.eclair.io.PeerConnected @@ -47,7 +48,7 @@ object WeakEntropyPool { sealed trait Command private case object FlushEntropy extends Command private case class WrappedNewBlock(blockHash: ByteVector32) extends Command - private case class WrappedPaymentRelayed(paymentHash: ByteVector32, relayedAt: Long) extends Command + private case class WrappedPaymentRelayed(paymentHash: ByteVector32, relayedAt: TimestampMilli) extends Command private case class WrappedPeerConnected(nodeId: PublicKey) extends Command private case class WrappedChannelSignature(wtxid: ByteVector32) extends Command private case class WrappedNodeUpdated(sig: ByteVector64) extends Command @@ -80,7 +81,7 @@ object WeakEntropyPool { case WrappedNewBlock(blockHash) => collecting(collector, collect(entropy_opt, blockHash ++ ByteVector.fromLong(System.currentTimeMillis()))) - case WrappedPaymentRelayed(paymentHash, relayedAt) => collecting(collector, collect(entropy_opt, paymentHash ++ ByteVector.fromLong(relayedAt))) + case WrappedPaymentRelayed(paymentHash, relayedAt) => collecting(collector, collect(entropy_opt, paymentHash ++ ByteVector.fromLong(relayedAt.toLong))) case WrappedPeerConnected(nodeId) => collecting(collector, collect(entropy_opt, nodeId.value ++ ByteVector.fromLong(System.currentTimeMillis()))) diff --git a/eclair-core/src/main/scala/fr/acinq/eclair/db/AuditDb.scala b/eclair-core/src/main/scala/fr/acinq/eclair/db/AuditDb.scala index a668155809..6645430e6c 100644 --- a/eclair-core/src/main/scala/fr/acinq/eclair/db/AuditDb.scala +++ b/eclair-core/src/main/scala/fr/acinq/eclair/db/AuditDb.scala @@ -18,6 +18,7 @@ package fr.acinq.eclair.db import fr.acinq.bitcoin.Crypto.PublicKey import fr.acinq.bitcoin.{ByteVector32, Satoshi} +import fr.acinq.eclair.{TimestampMilli, TimestampSecond} import fr.acinq.eclair.channel._ import fr.acinq.eclair.db.AuditDb.{NetworkFee, Stats} import fr.acinq.eclair.db.DbEventHandler.ChannelEvent @@ -45,21 +46,21 @@ trait AuditDb extends Closeable { def addPathFindingExperimentMetrics(metrics: PathFindingExperimentMetrics): Unit - def listSent(from: Long, to: Long): Seq[PaymentSent] + def listSent(from: TimestampMilli, to: TimestampMilli): Seq[PaymentSent] - def listReceived(from: Long, to: Long): Seq[PaymentReceived] + def listReceived(from: TimestampMilli, to: TimestampMilli): Seq[PaymentReceived] - def listRelayed(from: Long, to: Long): Seq[PaymentRelayed] + def listRelayed(from: TimestampMilli, to: TimestampMilli): Seq[PaymentRelayed] - def listNetworkFees(from: Long, to: Long): Seq[NetworkFee] + def listNetworkFees(from: TimestampMilli, to: TimestampMilli): Seq[NetworkFee] - def stats(from: Long, to: Long): Seq[Stats] + def stats(from: TimestampMilli, to: TimestampMilli): Seq[Stats] } object AuditDb { - case class NetworkFee(remoteNodeId: PublicKey, channelId: ByteVector32, txId: ByteVector32, fee: Satoshi, txType: String, timestamp: Long) + case class NetworkFee(remoteNodeId: PublicKey, channelId: ByteVector32, txId: ByteVector32, fee: Satoshi, txType: String, timestamp: TimestampMilli) case class Stats(channelId: ByteVector32, direction: String, avgPaymentAmount: Satoshi, paymentCount: Int, relayFee: Satoshi, networkFee: Satoshi) diff --git a/eclair-core/src/main/scala/fr/acinq/eclair/db/ChannelsDb.scala b/eclair-core/src/main/scala/fr/acinq/eclair/db/ChannelsDb.scala index 42b3320a70..b676b71a24 100644 --- a/eclair-core/src/main/scala/fr/acinq/eclair/db/ChannelsDb.scala +++ b/eclair-core/src/main/scala/fr/acinq/eclair/db/ChannelsDb.scala @@ -27,6 +27,8 @@ trait ChannelsDb extends Closeable { def addOrUpdateChannel(state: HasCommitments): Unit + def getChannel(channelId: ByteVector32): Option[HasCommitments] + def updateChannelMeta(channelId: ByteVector32, event: ChannelEvent.EventType): Unit def removeChannel(channelId: ByteVector32): Unit @@ -36,5 +38,4 @@ trait ChannelsDb extends Closeable { def addHtlcInfo(channelId: ByteVector32, commitmentNumber: Long, paymentHash: ByteVector32, cltvExpiry: CltvExpiry): Unit def listHtlcInfos(channelId: ByteVector32, commitmentNumber: Long): Seq[(ByteVector32, CltvExpiry)] - } diff --git a/eclair-core/src/main/scala/fr/acinq/eclair/db/DualDatabases.scala b/eclair-core/src/main/scala/fr/acinq/eclair/db/DualDatabases.scala index cf656a1717..968c7503a5 100644 --- a/eclair-core/src/main/scala/fr/acinq/eclair/db/DualDatabases.scala +++ b/eclair-core/src/main/scala/fr/acinq/eclair/db/DualDatabases.scala @@ -12,7 +12,7 @@ import fr.acinq.eclair.payment._ import fr.acinq.eclair.payment.relay.Relayer.RelayFees import fr.acinq.eclair.router.Router import fr.acinq.eclair.wire.protocol.{ChannelAnnouncement, ChannelUpdate, NodeAddress, NodeAnnouncement} -import fr.acinq.eclair.{CltvExpiry, MilliSatoshi, ShortChannelId} +import fr.acinq.eclair.{CltvExpiry, MilliSatoshi, ShortChannelId, TimestampMilli} import grizzled.slf4j.Logging import java.io.File @@ -176,27 +176,27 @@ case class DualAuditDb(sqlite: SqliteAuditDb, postgres: PgAuditDb) extends Audit sqlite.addPathFindingExperimentMetrics(metrics) } - override def listSent(from: Long, to: Long): Seq[PaymentSent] = { + override def listSent(from: TimestampMilli, to: TimestampMilli): Seq[PaymentSent] = { runAsync(postgres.listSent(from, to)) sqlite.listSent(from, to) } - override def listReceived(from: Long, to: Long): Seq[PaymentReceived] = { + override def listReceived(from: TimestampMilli, to: TimestampMilli): Seq[PaymentReceived] = { runAsync(postgres.listReceived(from, to)) sqlite.listReceived(from, to) } - override def listRelayed(from: Long, to: Long): Seq[PaymentRelayed] = { + override def listRelayed(from: TimestampMilli, to: TimestampMilli): Seq[PaymentRelayed] = { runAsync(postgres.listRelayed(from, to)) sqlite.listRelayed(from, to) } - override def listNetworkFees(from: Long, to: Long): Seq[AuditDb.NetworkFee] = { + override def listNetworkFees(from: TimestampMilli, to: TimestampMilli): Seq[AuditDb.NetworkFee] = { runAsync(postgres.listNetworkFees(from, to)) sqlite.listNetworkFees(from, to) } - override def stats(from: Long, to: Long): Seq[AuditDb.Stats] = { + override def stats(from: TimestampMilli, to: TimestampMilli): Seq[AuditDb.Stats] = { runAsync(postgres.stats(from, to)) sqlite.stats(from, to) } @@ -216,6 +216,11 @@ case class DualChannelsDb(sqlite: SqliteChannelsDb, postgres: PgChannelsDb) exte sqlite.addOrUpdateChannel(state) } + override def getChannel(channelId: ByteVector32): Option[HasCommitments] = { + runAsync(postgres.getChannel(channelId)) + sqlite.getChannel(channelId) + } + override def updateChannelMeta(channelId: ByteVector32, event: ChannelEvent.EventType): Unit = { runAsync(postgres.updateChannelMeta(channelId, event)) sqlite.updateChannelMeta(channelId, event) @@ -306,7 +311,7 @@ case class DualPaymentsDb(sqlite: SqlitePaymentsDb, postgres: PgPaymentsDb) exte sqlite.addIncomingPayment(pr, preimage, paymentType) } - override def receiveIncomingPayment(paymentHash: ByteVector32, amount: MilliSatoshi, receivedAt: Long): Unit = { + override def receiveIncomingPayment(paymentHash: ByteVector32, amount: MilliSatoshi, receivedAt: TimestampMilli): Unit = { runAsync(postgres.receiveIncomingPayment(paymentHash, amount, receivedAt)) sqlite.receiveIncomingPayment(paymentHash, amount, receivedAt) } @@ -316,22 +321,22 @@ case class DualPaymentsDb(sqlite: SqlitePaymentsDb, postgres: PgPaymentsDb) exte sqlite.getIncomingPayment(paymentHash) } - override def listIncomingPayments(from: Long, to: Long): Seq[IncomingPayment] = { + override def listIncomingPayments(from: TimestampMilli, to: TimestampMilli): Seq[IncomingPayment] = { runAsync(postgres.listIncomingPayments(from, to)) sqlite.listIncomingPayments(from, to) } - override def listPendingIncomingPayments(from: Long, to: Long): Seq[IncomingPayment] = { + override def listPendingIncomingPayments(from: TimestampMilli, to: TimestampMilli): Seq[IncomingPayment] = { runAsync(postgres.listPendingIncomingPayments(from, to)) sqlite.listPendingIncomingPayments(from, to) } - override def listExpiredIncomingPayments(from: Long, to: Long): Seq[IncomingPayment] = { + override def listExpiredIncomingPayments(from: TimestampMilli, to: TimestampMilli): Seq[IncomingPayment] = { runAsync(postgres.listExpiredIncomingPayments(from, to)) sqlite.listExpiredIncomingPayments(from, to) } - override def listReceivedIncomingPayments(from: Long, to: Long): Seq[IncomingPayment] = { + override def listReceivedIncomingPayments(from: TimestampMilli, to: TimestampMilli): Seq[IncomingPayment] = { runAsync(postgres.listReceivedIncomingPayments(from, to)) sqlite.listReceivedIncomingPayments(from, to) } @@ -366,7 +371,7 @@ case class DualPaymentsDb(sqlite: SqlitePaymentsDb, postgres: PgPaymentsDb) exte sqlite.listOutgoingPayments(paymentHash) } - override def listOutgoingPayments(from: Long, to: Long): Seq[OutgoingPayment] = { + override def listOutgoingPayments(from: TimestampMilli, to: TimestampMilli): Seq[OutgoingPayment] = { runAsync(postgres.listOutgoingPayments(from, to)) sqlite.listOutgoingPayments(from, to) } diff --git a/eclair-core/src/main/scala/fr/acinq/eclair/db/PaymentsDb.scala b/eclair-core/src/main/scala/fr/acinq/eclair/db/PaymentsDb.scala index 6dc271fc21..430a7dea50 100644 --- a/eclair-core/src/main/scala/fr/acinq/eclair/db/PaymentsDb.scala +++ b/eclair-core/src/main/scala/fr/acinq/eclair/db/PaymentsDb.scala @@ -20,7 +20,7 @@ import fr.acinq.bitcoin.ByteVector32 import fr.acinq.bitcoin.Crypto.PublicKey import fr.acinq.eclair.payment._ import fr.acinq.eclair.router.Router.{ChannelHop, Hop, NodeHop} -import fr.acinq.eclair.{MilliSatoshi, ShortChannelId} +import fr.acinq.eclair.{MilliSatoshi, ShortChannelId, TimestampMilli} import java.io.Closeable import java.util.UUID @@ -35,22 +35,22 @@ trait IncomingPaymentsDb { * Mark an incoming payment as received (paid). The received amount may exceed the payment request amount. * Note that this function assumes that there is a matching payment request in the DB. */ - def receiveIncomingPayment(paymentHash: ByteVector32, amount: MilliSatoshi, receivedAt: Long = System.currentTimeMillis): Unit + def receiveIncomingPayment(paymentHash: ByteVector32, amount: MilliSatoshi, receivedAt: TimestampMilli = TimestampMilli.now()): Unit /** Get information about the incoming payment (paid or not) for the given payment hash, if any. */ def getIncomingPayment(paymentHash: ByteVector32): Option[IncomingPayment] /** List all incoming payments (pending, expired and succeeded) in the given time range (milli-seconds). */ - def listIncomingPayments(from: Long, to: Long): Seq[IncomingPayment] + def listIncomingPayments(from: TimestampMilli, to: TimestampMilli): Seq[IncomingPayment] /** List all pending (not paid, not expired) incoming payments in the given time range (milli-seconds). */ - def listPendingIncomingPayments(from: Long, to: Long): Seq[IncomingPayment] + def listPendingIncomingPayments(from: TimestampMilli, to: TimestampMilli): Seq[IncomingPayment] /** List all expired (not paid) incoming payments in the given time range (milli-seconds). */ - def listExpiredIncomingPayments(from: Long, to: Long): Seq[IncomingPayment] + def listExpiredIncomingPayments(from: TimestampMilli, to: TimestampMilli): Seq[IncomingPayment] /** List all received (paid) incoming payments in the given time range (milli-seconds). */ - def listReceivedIncomingPayments(from: Long, to: Long): Seq[IncomingPayment] + def listReceivedIncomingPayments(from: TimestampMilli, to: TimestampMilli): Seq[IncomingPayment] } trait OutgoingPaymentsDb { @@ -74,7 +74,7 @@ trait OutgoingPaymentsDb { def listOutgoingPayments(paymentHash: ByteVector32): Seq[OutgoingPayment] /** List all the outgoing payment attempts in the given time range (milli-seconds). */ - def listOutgoingPayments(from: Long, to: Long): Seq[OutgoingPayment] + def listOutgoingPayments(from: TimestampMilli, to: TimestampMilli): Seq[OutgoingPayment] } @@ -99,7 +99,7 @@ case object PaymentType { case class IncomingPayment(paymentRequest: PaymentRequest, paymentPreimage: ByteVector32, paymentType: String, - createdAt: Long, + createdAt: TimestampMilli, status: IncomingPaymentStatus) sealed trait IncomingPaymentStatus @@ -118,7 +118,7 @@ object IncomingPaymentStatus { * @param amount amount of the payment received, in milli-satoshis (may exceed the payment request amount). * @param receivedAt absolute time in milli-seconds since UNIX epoch when the payment was received. */ - case class Received(amount: MilliSatoshi, receivedAt: Long) extends IncomingPaymentStatus + case class Received(amount: MilliSatoshi, receivedAt: TimestampMilli) extends IncomingPaymentStatus } @@ -146,7 +146,7 @@ case class OutgoingPayment(id: UUID, amount: MilliSatoshi, recipientAmount: MilliSatoshi, recipientNodeId: PublicKey, - createdAt: Long, + createdAt: TimestampMilli, paymentRequest: Option[PaymentRequest], status: OutgoingPaymentStatus) @@ -167,7 +167,7 @@ object OutgoingPaymentStatus { * @param route payment route used. * @param completedAt absolute time in milli-seconds since UNIX epoch when the payment was completed. */ - case class Succeeded(paymentPreimage: ByteVector32, feesPaid: MilliSatoshi, route: Seq[HopSummary], completedAt: Long) extends OutgoingPaymentStatus + case class Succeeded(paymentPreimage: ByteVector32, feesPaid: MilliSatoshi, route: Seq[HopSummary], completedAt: TimestampMilli) extends OutgoingPaymentStatus /** * Payment has failed and may be retried. @@ -175,7 +175,7 @@ object OutgoingPaymentStatus { * @param failures failed payment attempts. * @param completedAt absolute time in milli-seconds since UNIX epoch when the payment was completed. */ - case class Failed(failures: Seq[FailureSummary], completedAt: Long) extends OutgoingPaymentStatus + case class Failed(failures: Seq[FailureSummary], completedAt: TimestampMilli) extends OutgoingPaymentStatus } @@ -232,8 +232,8 @@ sealed trait PlainPayment { val paymentType: String val paymentRequest: Option[String] val finalAmount: Option[MilliSatoshi] - val createdAt: Long - val completedAt: Option[Long] + val createdAt: TimestampMilli + val completedAt: Option[TimestampMilli] } case class PlainIncomingPayment(paymentHash: ByteVector32, @@ -241,9 +241,9 @@ case class PlainIncomingPayment(paymentHash: ByteVector32, finalAmount: Option[MilliSatoshi], paymentRequest: Option[String], status: IncomingPaymentStatus, - createdAt: Long, - completedAt: Option[Long], - expireAt: Option[Long]) extends PlainPayment + createdAt: TimestampMilli, + completedAt: Option[TimestampMilli], + expireAt: Option[TimestampMilli]) extends PlainPayment case class PlainOutgoingPayment(parentId: Option[UUID], externalId: Option[String], @@ -252,5 +252,5 @@ case class PlainOutgoingPayment(parentId: Option[UUID], finalAmount: Option[MilliSatoshi], paymentRequest: Option[String], status: OutgoingPaymentStatus, - createdAt: Long, - completedAt: Option[Long]) extends PlainPayment \ No newline at end of file + createdAt: TimestampMilli, + completedAt: Option[TimestampMilli]) extends PlainPayment \ No newline at end of file diff --git a/eclair-core/src/main/scala/fr/acinq/eclair/db/pg/PgAuditDb.scala b/eclair-core/src/main/scala/fr/acinq/eclair/db/pg/PgAuditDb.scala index cba7e0bee8..0230714b1d 100644 --- a/eclair-core/src/main/scala/fr/acinq/eclair/db/pg/PgAuditDb.scala +++ b/eclair-core/src/main/scala/fr/acinq/eclair/db/pg/PgAuditDb.scala @@ -26,7 +26,7 @@ import fr.acinq.eclair.db.Monitoring.Tags.DbBackends import fr.acinq.eclair.db._ import fr.acinq.eclair.payment._ import fr.acinq.eclair.transactions.Transactions.PlaceHolderPubKey -import fr.acinq.eclair.{MilliSatoshi, MilliSatoshiLong} +import fr.acinq.eclair.{MilliSatoshi, MilliSatoshiLong, TimestampMilli} import grizzled.slf4j.Logging import java.sql.{Statement, Timestamp} @@ -45,7 +45,7 @@ class PgAuditDb(implicit ds: DataSource) extends AuditDb with Logging { import ExtendedResultSet._ import PgAuditDb._ - case class RelayedPart(channelId: ByteVector32, amount: MilliSatoshi, direction: String, relayType: String, timestamp: Long) + case class RelayedPart(channelId: ByteVector32, amount: MilliSatoshi, direction: String, relayType: String, timestamp: TimestampMilli) inTransaction { pg => using(pg.createStatement()) { statement => @@ -189,7 +189,7 @@ class PgAuditDb(implicit ds: DataSource) extends AuditDb with Logging { statement.setString(7, e.paymentPreimage.toHex) statement.setString(8, e.recipientNodeId.value.toHex) statement.setString(9, p.toChannelId.toHex) - statement.setTimestamp(10, Timestamp.from(Instant.ofEpochMilli(p.timestamp))) + statement.setTimestamp(10, p.timestamp.toSqlTimestamp) statement.addBatch() }) statement.executeBatch() @@ -204,7 +204,7 @@ class PgAuditDb(implicit ds: DataSource) extends AuditDb with Logging { statement.setLong(1, p.amount.toLong) statement.setString(2, e.paymentHash.toHex) statement.setString(3, p.fromChannelId.toHex) - statement.setTimestamp(4, Timestamp.from(Instant.ofEpochMilli(p.timestamp))) + statement.setTimestamp(4, p.timestamp.toSqlTimestamp) statement.addBatch() }) statement.executeBatch() @@ -223,7 +223,7 @@ class PgAuditDb(implicit ds: DataSource) extends AuditDb with Logging { statement.setString(1, e.paymentHash.toHex) statement.setLong(2, nextTrampolineAmount.toLong) statement.setString(3, nextTrampolineNodeId.value.toHex) - statement.setTimestamp(4, Timestamp.from(Instant.ofEpochMilli(e.timestamp))) + statement.setTimestamp(4, e.timestamp.toSqlTimestamp) statement.executeUpdate() } // trampoline relayed payments do MPP aggregation and may have M inputs and N outputs @@ -236,7 +236,7 @@ class PgAuditDb(implicit ds: DataSource) extends AuditDb with Logging { statement.setString(3, p.channelId.toHex) statement.setString(4, p.direction) statement.setString(5, p.relayType) - statement.setTimestamp(6, Timestamp.from(Instant.ofEpochMilli(e.timestamp))) + statement.setTimestamp(6, e.timestamp.toSqlTimestamp) statement.executeUpdate() } } @@ -309,8 +309,8 @@ class PgAuditDb(implicit ds: DataSource) extends AuditDb with Logging { statement.setLong(1, m.amount.toLong) statement.setLong(2, m.fees.toLong) statement.setString(3, m.status) - statement.setLong(4, m.duration) - statement.setTimestamp(5, new Timestamp(m.timestamp)) + statement.setLong(4, m.duration.toMillis) + statement.setTimestamp(5, m.timestamp.toSqlTimestamp) statement.setBoolean(6, m.isMultiPart) statement.setString(7, m.experimentName) statement.setString(8, m.recipientNodeId.value.toHex) @@ -319,11 +319,11 @@ class PgAuditDb(implicit ds: DataSource) extends AuditDb with Logging { } } - override def listSent(from: Long, to: Long): Seq[PaymentSent] = + override def listSent(from: TimestampMilli, to: TimestampMilli): Seq[PaymentSent] = inTransaction { pg => using(pg.prepareStatement("SELECT * FROM audit.sent WHERE timestamp BETWEEN ? AND ?")) { statement => - statement.setTimestamp(1, Timestamp.from(Instant.ofEpochMilli(from))) - statement.setTimestamp(2, Timestamp.from(Instant.ofEpochMilli(to))) + statement.setTimestamp(1, from.toSqlTimestamp) + statement.setTimestamp(2, to.toSqlTimestamp) statement.executeQuery() .foldLeft(Map.empty[UUID, PaymentSent]) { (sentByParentId, rs) => val parentId = UUID.fromString(rs.getString("parent_payment_id")) @@ -333,7 +333,7 @@ class PgAuditDb(implicit ds: DataSource) extends AuditDb with Logging { MilliSatoshi(rs.getLong("fees_msat")), rs.getByteVector32FromHex("to_channel_id"), None, // we don't store the route in the audit DB - rs.getTimestamp("timestamp").getTime) + TimestampMilli.fromSqlTimestamp(rs.getTimestamp("timestamp"))) val sent = sentByParentId.get(parentId) match { case Some(s) => s.copy(parts = s.parts :+ part) case None => PaymentSent( @@ -349,18 +349,18 @@ class PgAuditDb(implicit ds: DataSource) extends AuditDb with Logging { } } - override def listReceived(from: Long, to: Long): Seq[PaymentReceived] = + override def listReceived(from: TimestampMilli, to: TimestampMilli): Seq[PaymentReceived] = inTransaction { pg => using(pg.prepareStatement("SELECT * FROM audit.received WHERE timestamp BETWEEN ? AND ?")) { statement => - statement.setTimestamp(1, Timestamp.from(Instant.ofEpochMilli(from))) - statement.setTimestamp(2, Timestamp.from(Instant.ofEpochMilli(to))) + statement.setTimestamp(1, from.toSqlTimestamp) + statement.setTimestamp(2, to.toSqlTimestamp) statement.executeQuery() .foldLeft(Map.empty[ByteVector32, PaymentReceived]) { (receivedByHash, rs) => val paymentHash = rs.getByteVector32FromHex("payment_hash") val part = PaymentReceived.PartialPayment( MilliSatoshi(rs.getLong("amount_msat")), rs.getByteVector32FromHex("from_channel_id"), - rs.getTimestamp("timestamp").getTime) + TimestampMilli.fromSqlTimestamp(rs.getTimestamp("timestamp"))) val received = receivedByHash.get(paymentHash) match { case Some(r) => r.copy(parts = r.parts :+ part) case None => PaymentReceived(paymentHash, Seq(part)) @@ -370,11 +370,11 @@ class PgAuditDb(implicit ds: DataSource) extends AuditDb with Logging { } } - override def listRelayed(from: Long, to: Long): Seq[PaymentRelayed] = + override def listRelayed(from: TimestampMilli, to: TimestampMilli): Seq[PaymentRelayed] = inTransaction { pg => val trampolineByHash = using(pg.prepareStatement("SELECT * FROM audit.relayed_trampoline WHERE timestamp BETWEEN ? and ?")) { statement => - statement.setTimestamp(1, Timestamp.from(Instant.ofEpochMilli(from))) - statement.setTimestamp(2, Timestamp.from(Instant.ofEpochMilli(to))) + statement.setTimestamp(1, from.toSqlTimestamp) + statement.setTimestamp(2, to.toSqlTimestamp) statement.executeQuery() .foldLeft(Map.empty[ByteVector32, (MilliSatoshi, PublicKey)]) { (trampolineByHash, rs) => val paymentHash = rs.getByteVector32FromHex("payment_hash") @@ -384,8 +384,8 @@ class PgAuditDb(implicit ds: DataSource) extends AuditDb with Logging { } } val relayedByHash = using(pg.prepareStatement("SELECT * FROM audit.relayed WHERE timestamp BETWEEN ? and ?")) { statement => - statement.setTimestamp(1, Timestamp.from(Instant.ofEpochMilli(from))) - statement.setTimestamp(2, Timestamp.from(Instant.ofEpochMilli(to))) + statement.setTimestamp(1, from.toSqlTimestamp) + statement.setTimestamp(2, to.toSqlTimestamp) statement.executeQuery() .foldLeft(Map.empty[ByteVector32, Seq[RelayedPart]]) { (relayedByHash, rs) => val paymentHash = rs.getByteVector32FromHex("payment_hash") @@ -394,7 +394,7 @@ class PgAuditDb(implicit ds: DataSource) extends AuditDb with Logging { MilliSatoshi(rs.getLong("amount_msat")), rs.getString("direction"), rs.getString("relay_type"), - rs.getTimestamp("timestamp").getTime) + TimestampMilli.fromSqlTimestamp(rs.getTimestamp("timestamp"))) relayedByHash + (paymentHash -> (relayedByHash.getOrElse(paymentHash, Nil) :+ part)) } } @@ -416,11 +416,11 @@ class PgAuditDb(implicit ds: DataSource) extends AuditDb with Logging { }.toSeq.sortBy(_.timestamp) } - override def listNetworkFees(from: Long, to: Long): Seq[NetworkFee] = + override def listNetworkFees(from: TimestampMilli, to: TimestampMilli): Seq[NetworkFee] = inTransaction { pg => using(pg.prepareStatement("SELECT * FROM audit.transactions_confirmed INNER JOIN audit.transactions_published ON audit.transactions_published.tx_id = audit.transactions_confirmed.tx_id WHERE audit.transactions_confirmed.timestamp BETWEEN ? and ? ORDER BY audit.transactions_confirmed.timestamp")) { statement => - statement.setTimestamp(1, Timestamp.from(Instant.ofEpochMilli(from))) - statement.setTimestamp(2, Timestamp.from(Instant.ofEpochMilli(to))) + statement.setTimestamp(1, from.toSqlTimestamp) + statement.setTimestamp(2, to.toSqlTimestamp) statement.executeQuery().map { rs => NetworkFee( remoteNodeId = PublicKey(rs.getByteVectorFromHex("node_id")), @@ -428,12 +428,12 @@ class PgAuditDb(implicit ds: DataSource) extends AuditDb with Logging { txId = rs.getByteVector32FromHex("tx_id"), fee = Satoshi(rs.getLong("mining_fee_sat")), txType = rs.getString("tx_type"), - timestamp = rs.getTimestamp("timestamp").getTime) + timestamp = TimestampMilli.fromSqlTimestamp(rs.getTimestamp("timestamp"))) }.toSeq } } - override def stats(from: Long, to: Long): Seq[Stats] = { + override def stats(from: TimestampMilli, to: TimestampMilli): Seq[Stats] = { val networkFees = listNetworkFees(from, to).foldLeft(Map.empty[ByteVector32, Satoshi]) { (feeByChannelId, f) => feeByChannelId + (f.channelId -> (feeByChannelId.getOrElse(f.channelId, 0 sat) + f.fee)) } diff --git a/eclair-core/src/main/scala/fr/acinq/eclair/db/pg/PgChannelsDb.scala b/eclair-core/src/main/scala/fr/acinq/eclair/db/pg/PgChannelsDb.scala index 51d70e519e..1d6499a9c9 100644 --- a/eclair-core/src/main/scala/fr/acinq/eclair/db/pg/PgChannelsDb.scala +++ b/eclair-core/src/main/scala/fr/acinq/eclair/db/pg/PgChannelsDb.scala @@ -166,6 +166,15 @@ class PgChannelsDb(implicit ds: DataSource, lock: PgLock) extends ChannelsDb wit } } + override def getChannel(channelId: ByteVector32): Option[HasCommitments] = withMetrics("channels/get-channel", DbBackends.Postgres) { + withLock { pg => + using(pg.prepareStatement("SELECT data FROM local.channels WHERE channel_id=? AND is_closed=FALSE")) { statement => + statement.setString(1, channelId.toHex) + statement.executeQuery.mapCodec(stateDataCodec).lastOption + } + } + } + /** * Helper method to factor updating timestamp columns */ diff --git a/eclair-core/src/main/scala/fr/acinq/eclair/db/pg/PgPaymentsDb.scala b/eclair-core/src/main/scala/fr/acinq/eclair/db/pg/PgPaymentsDb.scala index ae0d3ec9ae..9e2bce977e 100644 --- a/eclair-core/src/main/scala/fr/acinq/eclair/db/pg/PgPaymentsDb.scala +++ b/eclair-core/src/main/scala/fr/acinq/eclair/db/pg/PgPaymentsDb.scala @@ -18,13 +18,13 @@ package fr.acinq.eclair.db.pg import fr.acinq.bitcoin.ByteVector32 import fr.acinq.bitcoin.Crypto.PublicKey -import fr.acinq.eclair.MilliSatoshi import fr.acinq.eclair.db.Monitoring.Metrics.withMetrics import fr.acinq.eclair.db.Monitoring.Tags.DbBackends import fr.acinq.eclair.db._ import fr.acinq.eclair.db.pg.PgUtils.PgLock import fr.acinq.eclair.payment.{PaymentFailed, PaymentRequest, PaymentSent} import fr.acinq.eclair.wire.protocol.CommonCodecs +import fr.acinq.eclair.{MilliSatoshi, TimestampMilli, TimestampMilliLong} import grizzled.slf4j.Logging import scodec.Attempt import scodec.bits.BitVector @@ -34,6 +34,7 @@ import java.sql.{ResultSet, Statement, Timestamp} import java.time.Instant import java.util.UUID import javax.sql.DataSource +import scala.concurrent.duration.DurationLong object PgPaymentsDb { val DB_NAME = "payments" @@ -112,7 +113,7 @@ class PgPaymentsDb(implicit ds: DataSource, lock: PgLock) extends PaymentsDb wit statement.setLong(6, sent.amount.toLong) statement.setLong(7, sent.recipientAmount.toLong) statement.setString(8, sent.recipientNodeId.value.toHex) - statement.setTimestamp(9, Timestamp.from(Instant.ofEpochMilli(sent.createdAt))) + statement.setTimestamp(9, sent.createdAt.toSqlTimestamp) statement.setString(10, sent.paymentRequest.map(PaymentRequest.write).orNull) statement.executeUpdate() } @@ -123,7 +124,7 @@ class PgPaymentsDb(implicit ds: DataSource, lock: PgLock) extends PaymentsDb wit withLock { pg => using(pg.prepareStatement("UPDATE payments.sent SET (completed_at, payment_preimage, fees_msat, payment_route) = (?, ?, ?, ?) WHERE id = ? AND completed_at IS NULL")) { statement => paymentResult.parts.foreach(p => { - statement.setTimestamp(1, Timestamp.from(Instant.ofEpochMilli(p.timestamp))) + statement.setTimestamp(1, p.timestamp.toSqlTimestamp) statement.setString(2, paymentResult.paymentPreimage.toHex) statement.setLong(3, p.feesPaid.toLong) statement.setBytes(4, paymentRouteCodec.encode(p.route.getOrElse(Nil).map(h => HopSummary(h)).toList).require.toByteArray) @@ -138,7 +139,7 @@ class PgPaymentsDb(implicit ds: DataSource, lock: PgLock) extends PaymentsDb wit override def updateOutgoingPayment(paymentResult: PaymentFailed): Unit = withMetrics("payments/update-outgoing-failed", DbBackends.Postgres) { withLock { pg => using(pg.prepareStatement("UPDATE payments.sent SET (completed_at, failures) = (?, ?) WHERE id = ? AND completed_at IS NULL")) { statement => - statement.setTimestamp(1, Timestamp.from(Instant.ofEpochMilli(paymentResult.timestamp))) + statement.setTimestamp(1, paymentResult.timestamp.toSqlTimestamp) statement.setBytes(2, paymentFailuresCodec.encode(paymentResult.failures.map(f => FailureSummary(f)).toList).require.toByteArray) statement.setString(3, paymentResult.id.toString) if (statement.executeUpdate() == 0) throw new IllegalArgumentException(s"Tried to mark an outgoing payment as failed but already in final status (id=${paymentResult.id})") @@ -151,7 +152,7 @@ class PgPaymentsDb(implicit ds: DataSource, lock: PgLock) extends PaymentsDb wit rs.getByteVector32FromHexNullable("payment_preimage"), rs.getMilliSatoshiNullable("fees_msat"), rs.getBitVectorOpt("payment_route"), - rs.getTimestampNullable("completed_at").map(_.getTime), + rs.getTimestampNullable("completed_at").map(TimestampMilli.fromSqlTimestamp), rs.getBitVectorOpt("failures")) OutgoingPayment( @@ -163,13 +164,13 @@ class PgPaymentsDb(implicit ds: DataSource, lock: PgLock) extends PaymentsDb wit MilliSatoshi(rs.getLong("amount_msat")), MilliSatoshi(rs.getLong("recipient_amount_msat")), PublicKey(rs.getByteVectorFromHex("recipient_node_id")), - rs.getTimestamp("created_at").getTime, + TimestampMilli(rs.getTimestamp("created_at").getTime), rs.getStringNullable("payment_request").map(PaymentRequest.read), status ) } - private def buildOutgoingPaymentStatus(preimage_opt: Option[ByteVector32], fees_opt: Option[MilliSatoshi], paymentRoute_opt: Option[BitVector], completedAt_opt: Option[Long], failures: Option[BitVector]): OutgoingPaymentStatus = { + private def buildOutgoingPaymentStatus(preimage_opt: Option[ByteVector32], fees_opt: Option[MilliSatoshi], paymentRoute_opt: Option[BitVector], completedAt_opt: Option[TimestampMilli], failures: Option[BitVector]): OutgoingPaymentStatus = { preimage_opt match { // If we have a pre-image, the payment succeeded. case Some(preimage) => OutgoingPaymentStatus.Succeeded( @@ -177,7 +178,7 @@ class PgPaymentsDb(implicit ds: DataSource, lock: PgLock) extends PaymentsDb wit case Attempt.Successful(route) => route.value case Attempt.Failure(_) => Nil }).getOrElse(Nil), - completedAt_opt.getOrElse(0) + completedAt_opt.getOrElse(0 unixms) ) case None => completedAt_opt match { // Otherwise if the payment was marked completed, it's a failure. @@ -221,11 +222,11 @@ class PgPaymentsDb(implicit ds: DataSource, lock: PgLock) extends PaymentsDb wit } } - override def listOutgoingPayments(from: Long, to: Long): Seq[OutgoingPayment] = withMetrics("payments/list-outgoing-by-timestamp", DbBackends.Postgres) { + override def listOutgoingPayments(from: TimestampMilli, to: TimestampMilli): Seq[OutgoingPayment] = withMetrics("payments/list-outgoing-by-timestamp", DbBackends.Postgres) { withLock { pg => using(pg.prepareStatement("SELECT * FROM payments.sent WHERE created_at >= ? AND created_at < ? ORDER BY created_at")) { statement => - statement.setTimestamp(1, Timestamp.from(Instant.ofEpochMilli(from))) - statement.setTimestamp(2, Timestamp.from(Instant.ofEpochMilli(to))) + statement.setTimestamp(1, from.toSqlTimestamp) + statement.setTimestamp(2, to.toSqlTimestamp) statement.executeQuery().map { rs => parseOutgoingPayment(rs) }.toSeq @@ -240,18 +241,18 @@ class PgPaymentsDb(implicit ds: DataSource, lock: PgLock) extends PaymentsDb wit statement.setString(2, preimage.toHex) statement.setString(3, paymentType) statement.setString(4, PaymentRequest.write(pr)) - statement.setTimestamp(5, Timestamp.from(Instant.ofEpochSecond(pr.timestamp))) // BOLT11 timestamp is in seconds - statement.setTimestamp(6, Timestamp.from(Instant.ofEpochSecond(pr.timestamp + pr.expiry.getOrElse(PaymentRequest.DEFAULT_EXPIRY_SECONDS.toLong)))) + statement.setTimestamp(5, pr.timestamp.toSqlTimestamp) + statement.setTimestamp(6, (pr.timestamp + pr.expiry.getOrElse(PaymentRequest.DEFAULT_EXPIRY_SECONDS).seconds).toSqlTimestamp) statement.executeUpdate() } } } - override def receiveIncomingPayment(paymentHash: ByteVector32, amount: MilliSatoshi, receivedAt: Long): Unit = withMetrics("payments/receive-incoming", DbBackends.Postgres) { + override def receiveIncomingPayment(paymentHash: ByteVector32, amount: MilliSatoshi, receivedAt: TimestampMilli): Unit = withMetrics("payments/receive-incoming", DbBackends.Postgres) { withLock { pg => using(pg.prepareStatement("UPDATE payments.received SET (received_msat, received_at) = (? + COALESCE(received_msat, 0), ?) WHERE payment_hash = ?")) { update => update.setLong(1, amount.toLong) - update.setTimestamp(2, Timestamp.from(Instant.ofEpochMilli(receivedAt))) + update.setTimestamp(2, receivedAt.toSqlTimestamp) update.setString(3, paymentHash.toHex) val updated = update.executeUpdate() if (updated == 0) { @@ -267,13 +268,13 @@ class PgPaymentsDb(implicit ds: DataSource, lock: PgLock) extends PaymentsDb wit PaymentRequest.read(paymentRequest), rs.getByteVector32FromHex("payment_preimage"), rs.getString("payment_type"), - rs.getTimestamp("created_at").getTime, - buildIncomingPaymentStatus(rs.getMilliSatoshiNullable("received_msat"), Some(paymentRequest), rs.getTimestampNullable("received_at").map(_.getTime))) + TimestampMilli.fromSqlTimestamp(rs.getTimestamp("created_at")), + buildIncomingPaymentStatus(rs.getMilliSatoshiNullable("received_msat"), Some(paymentRequest), rs.getTimestampNullable("received_at").map(TimestampMilli.fromSqlTimestamp))) } - private def buildIncomingPaymentStatus(amount_opt: Option[MilliSatoshi], serializedPaymentRequest_opt: Option[String], receivedAt_opt: Option[Long]): IncomingPaymentStatus = { + private def buildIncomingPaymentStatus(amount_opt: Option[MilliSatoshi], serializedPaymentRequest_opt: Option[String], receivedAt_opt: Option[TimestampMilli]): IncomingPaymentStatus = { amount_opt match { - case Some(amount) => IncomingPaymentStatus.Received(amount, receivedAt_opt.getOrElse(0)) + case Some(amount) => IncomingPaymentStatus.Received(amount, receivedAt_opt.getOrElse(0 unixms)) case None if serializedPaymentRequest_opt.exists(PaymentRequest.fastHasExpired) => IncomingPaymentStatus.Expired case None => IncomingPaymentStatus.Pending } @@ -288,42 +289,42 @@ class PgPaymentsDb(implicit ds: DataSource, lock: PgLock) extends PaymentsDb wit } } - override def listIncomingPayments(from: Long, to: Long): Seq[IncomingPayment] = withMetrics("payments/list-incoming", DbBackends.Postgres) { + override def listIncomingPayments(from: TimestampMilli, to: TimestampMilli): Seq[IncomingPayment] = withMetrics("payments/list-incoming", DbBackends.Postgres) { withLock { pg => using(pg.prepareStatement("SELECT * FROM payments.received WHERE created_at > ? AND created_at < ? ORDER BY created_at")) { statement => - statement.setTimestamp(1, Timestamp.from(Instant.ofEpochMilli(from))) - statement.setTimestamp(2, Timestamp.from(Instant.ofEpochMilli(to))) + statement.setTimestamp(1, from.toSqlTimestamp) + statement.setTimestamp(2, to.toSqlTimestamp) statement.executeQuery().map(parseIncomingPayment).toSeq } } } - override def listReceivedIncomingPayments(from: Long, to: Long): Seq[IncomingPayment] = withMetrics("payments/list-incoming-received", DbBackends.Postgres) { + override def listReceivedIncomingPayments(from: TimestampMilli, to: TimestampMilli): Seq[IncomingPayment] = withMetrics("payments/list-incoming-received", DbBackends.Postgres) { withLock { pg => using(pg.prepareStatement("SELECT * FROM payments.received WHERE received_msat > 0 AND created_at > ? AND created_at < ? ORDER BY created_at")) { statement => - statement.setTimestamp(1, Timestamp.from(Instant.ofEpochMilli(from))) - statement.setTimestamp(2, Timestamp.from(Instant.ofEpochMilli(to))) + statement.setTimestamp(1, from.toSqlTimestamp) + statement.setTimestamp(2, to.toSqlTimestamp) statement.executeQuery().map(parseIncomingPayment).toSeq } } } - override def listPendingIncomingPayments(from: Long, to: Long): Seq[IncomingPayment] = withMetrics("payments/list-incoming-pending", DbBackends.Postgres) { + override def listPendingIncomingPayments(from: TimestampMilli, to: TimestampMilli): Seq[IncomingPayment] = withMetrics("payments/list-incoming-pending", DbBackends.Postgres) { withLock { pg => using(pg.prepareStatement("SELECT * FROM payments.received WHERE received_msat IS NULL AND created_at > ? AND created_at < ? AND expire_at > ? ORDER BY created_at")) { statement => - statement.setTimestamp(1, Timestamp.from(Instant.ofEpochMilli(from))) - statement.setTimestamp(2, Timestamp.from(Instant.ofEpochMilli(to))) + statement.setTimestamp(1, from.toSqlTimestamp) + statement.setTimestamp(2, to.toSqlTimestamp) statement.setTimestamp(3, Timestamp.from(Instant.now())) statement.executeQuery().map(parseIncomingPayment).toSeq } } } - override def listExpiredIncomingPayments(from: Long, to: Long): Seq[IncomingPayment] = withMetrics("payments/list-incoming-expired", DbBackends.Postgres) { + override def listExpiredIncomingPayments(from: TimestampMilli, to: TimestampMilli): Seq[IncomingPayment] = withMetrics("payments/list-incoming-expired", DbBackends.Postgres) { withLock { pg => using(pg.prepareStatement("SELECT * FROM payments.received WHERE received_msat IS NULL AND created_at > ? AND created_at < ? AND expire_at < ? ORDER BY created_at")) { statement => - statement.setTimestamp(1, Timestamp.from(Instant.ofEpochMilli(from))) - statement.setTimestamp(2, Timestamp.from(Instant.ofEpochMilli(to))) + statement.setTimestamp(1, from.toSqlTimestamp) + statement.setTimestamp(2, to.toSqlTimestamp) statement.setTimestamp(3, Timestamp.from(Instant.now())) statement.executeQuery().map(parseIncomingPayment).toSeq } @@ -383,9 +384,9 @@ class PgPaymentsDb(implicit ds: DataSource, lock: PgLock) extends PaymentsDb wit val paymentType = rs.getString("payment_type") val paymentRequest_opt = rs.getStringNullable("payment_request") val amount_opt = rs.getMilliSatoshiNullable("final_amount") - val createdAt = rs.getTimestamp("created_at").getTime - val completedAt_opt = rs.getTimestampNullable("completed_at").map(_.getTime) - val expireAt_opt = rs.getTimestampNullable("expire_at").map(_.getTime) + val createdAt = TimestampMilli.fromSqlTimestamp(rs.getTimestamp("created_at")) + val completedAt_opt = rs.getTimestampNullable("completed_at").map(TimestampMilli.fromSqlTimestamp) + val expireAt_opt = rs.getTimestampNullable("expire_at").map(TimestampMilli.fromSqlTimestamp) if (rs.getString("type") == "received") { val status: IncomingPaymentStatus = buildIncomingPaymentStatus(amount_opt, paymentRequest_opt, completedAt_opt) diff --git a/eclair-core/src/main/scala/fr/acinq/eclair/db/sqlite/SqliteAuditDb.scala b/eclair-core/src/main/scala/fr/acinq/eclair/db/sqlite/SqliteAuditDb.scala index 01b7a746e5..74c01a145e 100644 --- a/eclair-core/src/main/scala/fr/acinq/eclair/db/sqlite/SqliteAuditDb.scala +++ b/eclair-core/src/main/scala/fr/acinq/eclair/db/sqlite/SqliteAuditDb.scala @@ -26,7 +26,7 @@ import fr.acinq.eclair.db.Monitoring.Tags.DbBackends import fr.acinq.eclair.db._ import fr.acinq.eclair.payment._ import fr.acinq.eclair.transactions.Transactions.PlaceHolderPubKey -import fr.acinq.eclair.{MilliSatoshi, MilliSatoshiLong} +import fr.acinq.eclair.{MilliSatoshi, MilliSatoshiLong, TimestampMilli} import grizzled.slf4j.Logging import java.sql.{Connection, Statement} @@ -43,7 +43,7 @@ class SqliteAuditDb(sqlite: Connection) extends AuditDb with Logging { import ExtendedResultSet._ import SqliteAuditDb._ - case class RelayedPart(channelId: ByteVector32, amount: MilliSatoshi, direction: String, relayType: String, timestamp: Long) + case class RelayedPart(channelId: ByteVector32, amount: MilliSatoshi, direction: String, relayType: String, timestamp: TimestampMilli) using(sqlite.createStatement(), inTransaction = true) { statement => @@ -177,7 +177,7 @@ class SqliteAuditDb(sqlite: Connection) extends AuditDb with Logging { statement.setBoolean(4, e.isFunder) statement.setBoolean(5, e.isPrivate) statement.setString(6, e.event.label) - statement.setLong(7, System.currentTimeMillis) + statement.setLong(7, TimestampMilli.now().toLong) statement.executeUpdate() } } @@ -194,7 +194,7 @@ class SqliteAuditDb(sqlite: Connection) extends AuditDb with Logging { statement.setBytes(7, e.paymentPreimage.toArray) statement.setBytes(8, e.recipientNodeId.value.toArray) statement.setBytes(9, p.toChannelId.toArray) - statement.setLong(10, p.timestamp) + statement.setLong(10, p.timestamp.toLong) statement.addBatch() }) statement.executeBatch() @@ -207,7 +207,7 @@ class SqliteAuditDb(sqlite: Connection) extends AuditDb with Logging { statement.setLong(1, p.amount.toLong) statement.setBytes(2, e.paymentHash.toArray) statement.setBytes(3, p.fromChannelId.toArray) - statement.setLong(4, p.timestamp) + statement.setLong(4, p.timestamp.toLong) statement.addBatch() }) statement.executeBatch() @@ -224,7 +224,7 @@ class SqliteAuditDb(sqlite: Connection) extends AuditDb with Logging { statement.setBytes(1, e.paymentHash.toArray) statement.setLong(2, nextTrampolineAmount.toLong) statement.setBytes(3, nextTrampolineNodeId.value.toArray) - statement.setLong(4, e.timestamp) + statement.setLong(4, e.timestamp.toLong) statement.executeUpdate() } // trampoline relayed payments do MPP aggregation and may have M inputs and N outputs @@ -238,7 +238,7 @@ class SqliteAuditDb(sqlite: Connection) extends AuditDb with Logging { statement.setBytes(3, p.channelId.toArray) statement.setString(4, p.direction) statement.setString(5, p.relayType) - statement.setLong(6, e.timestamp) + statement.setLong(6, e.timestamp.toLong) statement.executeUpdate() } } @@ -251,7 +251,7 @@ class SqliteAuditDb(sqlite: Connection) extends AuditDb with Logging { statement.setBytes(3, e.remoteNodeId.value.toArray) statement.setLong(4, e.miningFee.toLong) statement.setString(5, e.desc) - statement.setLong(6, System.currentTimeMillis) + statement.setLong(6, TimestampMilli.now().toLong) statement.executeUpdate() } } @@ -261,7 +261,7 @@ class SqliteAuditDb(sqlite: Connection) extends AuditDb with Logging { statement.setBytes(1, e.tx.txid.toArray) statement.setBytes(2, e.channelId.toArray) statement.setBytes(3, e.remoteNodeId.value.toArray) - statement.setLong(4, System.currentTimeMillis) + statement.setLong(4, TimestampMilli.now().toLong) statement.executeUpdate() } } @@ -277,7 +277,7 @@ class SqliteAuditDb(sqlite: Connection) extends AuditDb with Logging { statement.setString(3, errorName) statement.setString(4, errorMessage) statement.setBoolean(5, e.isFatal) - statement.setLong(6, System.currentTimeMillis) + statement.setLong(6, TimestampMilli.now().toLong) statement.executeUpdate() } } @@ -291,7 +291,7 @@ class SqliteAuditDb(sqlite: Connection) extends AuditDb with Logging { statement.setLong(5, u.channelUpdate.cltvExpiryDelta.toInt) statement.setLong(6, u.channelUpdate.htlcMinimumMsat.toLong) statement.setLong(7, u.channelUpdate.htlcMaximumMsat.map(_.toLong).getOrElse(-1)) - statement.setLong(8, System.currentTimeMillis) + statement.setLong(8, TimestampMilli.now().toLong) statement.executeUpdate() } } @@ -301,8 +301,8 @@ class SqliteAuditDb(sqlite: Connection) extends AuditDb with Logging { statement.setLong(1, m.amount.toLong) statement.setLong(2, m.fees.toLong) statement.setString(3, m.status) - statement.setLong(4, m.duration) - statement.setLong(5, m.timestamp) + statement.setLong(4, m.duration.toMillis) + statement.setLong(5, m.timestamp.toLong) statement.setBoolean(6, m.isMultiPart) statement.setString(7, m.experimentName) statement.setBytes(8, m.recipientNodeId.value.toArray) @@ -310,10 +310,10 @@ class SqliteAuditDb(sqlite: Connection) extends AuditDb with Logging { } } - override def listSent(from: Long, to: Long): Seq[PaymentSent] = + override def listSent(from: TimestampMilli, to: TimestampMilli): Seq[PaymentSent] = using(sqlite.prepareStatement("SELECT * FROM sent WHERE timestamp >= ? AND timestamp < ?")) { statement => - statement.setLong(1, from) - statement.setLong(2, to) + statement.setLong(1, from.toLong) + statement.setLong(2, to.toLong) statement.executeQuery() .foldLeft(Map.empty[UUID, PaymentSent]) { (sentByParentId, rs) => val parentId = UUID.fromString(rs.getString("parent_payment_id")) @@ -323,7 +323,7 @@ class SqliteAuditDb(sqlite: Connection) extends AuditDb with Logging { MilliSatoshi(rs.getLong("fees_msat")), rs.getByteVector32("to_channel_id"), None, // we don't store the route in the audit DB - rs.getLong("timestamp")) + TimestampMilli(rs.getLong("timestamp"))) val sent = sentByParentId.get(parentId) match { case Some(s) => s.copy(parts = s.parts :+ part) case None => PaymentSent( @@ -338,17 +338,17 @@ class SqliteAuditDb(sqlite: Connection) extends AuditDb with Logging { }.values.toSeq.sortBy(_.timestamp) } - override def listReceived(from: Long, to: Long): Seq[PaymentReceived] = + override def listReceived(from: TimestampMilli, to: TimestampMilli): Seq[PaymentReceived] = using(sqlite.prepareStatement("SELECT * FROM received WHERE timestamp >= ? AND timestamp < ?")) { statement => - statement.setLong(1, from) - statement.setLong(2, to) + statement.setLong(1, from.toLong) + statement.setLong(2, to.toLong) statement.executeQuery() .foldLeft(Map.empty[ByteVector32, PaymentReceived]) { (receivedByHash, rs) => val paymentHash = rs.getByteVector32("payment_hash") val part = PaymentReceived.PartialPayment( MilliSatoshi(rs.getLong("amount_msat")), rs.getByteVector32("from_channel_id"), - rs.getLong("timestamp")) + TimestampMilli(rs.getLong("timestamp"))) val received = receivedByHash.get(paymentHash) match { case Some(r) => r.copy(parts = r.parts :+ part) case None => PaymentReceived(paymentHash, Seq(part)) @@ -357,10 +357,10 @@ class SqliteAuditDb(sqlite: Connection) extends AuditDb with Logging { }.values.toSeq.sortBy(_.timestamp) } - override def listRelayed(from: Long, to: Long): Seq[PaymentRelayed] = { + override def listRelayed(from: TimestampMilli, to: TimestampMilli): Seq[PaymentRelayed] = { val trampolineByHash = using(sqlite.prepareStatement("SELECT * FROM relayed_trampoline WHERE timestamp >= ? AND timestamp < ?")) { statement => - statement.setLong(1, from) - statement.setLong(2, to) + statement.setLong(1, from.toLong) + statement.setLong(2, to.toLong) statement.executeQuery() .map { rs => val paymentHash = rs.getByteVector32("payment_hash") @@ -371,8 +371,8 @@ class SqliteAuditDb(sqlite: Connection) extends AuditDb with Logging { .toMap } val relayedByHash = using(sqlite.prepareStatement("SELECT * FROM relayed WHERE timestamp >= ? AND timestamp < ?")) { statement => - statement.setLong(1, from) - statement.setLong(2, to) + statement.setLong(1, from.toLong) + statement.setLong(2, to.toLong) statement.executeQuery() .foldLeft(Map.empty[ByteVector32, Seq[RelayedPart]]) { (relayedByHash, rs) => val paymentHash = rs.getByteVector32("payment_hash") @@ -381,7 +381,7 @@ class SqliteAuditDb(sqlite: Connection) extends AuditDb with Logging { MilliSatoshi(rs.getLong("amount_msat")), rs.getString("direction"), rs.getString("relay_type"), - rs.getLong("timestamp")) + TimestampMilli(rs.getLong("timestamp"))) relayedByHash + (paymentHash -> (relayedByHash.getOrElse(paymentHash, Nil) :+ part)) } } @@ -403,10 +403,10 @@ class SqliteAuditDb(sqlite: Connection) extends AuditDb with Logging { }.toSeq.sortBy(_.timestamp) } - override def listNetworkFees(from: Long, to: Long): Seq[NetworkFee] = + override def listNetworkFees(from: TimestampMilli, to: TimestampMilli): Seq[NetworkFee] = using(sqlite.prepareStatement("SELECT * FROM transactions_confirmed INNER JOIN transactions_published ON transactions_published.tx_id = transactions_confirmed.tx_id WHERE transactions_confirmed.timestamp >= ? AND transactions_confirmed.timestamp < ? ORDER BY transactions_confirmed.timestamp")) { statement => - statement.setLong(1, from) - statement.setLong(2, to) + statement.setLong(1, from.toLong) + statement.setLong(2, to.toLong) statement.executeQuery() .map { rs => NetworkFee( @@ -415,11 +415,11 @@ class SqliteAuditDb(sqlite: Connection) extends AuditDb with Logging { txId = rs.getByteVector32("tx_id"), fee = Satoshi(rs.getLong("mining_fee_sat")), txType = rs.getString("tx_type"), - timestamp = rs.getLong("timestamp")) + timestamp = TimestampMilli(rs.getLong("timestamp"))) }.toSeq } - override def stats(from: Long, to: Long): Seq[Stats] = { + override def stats(from: TimestampMilli, to: TimestampMilli): Seq[Stats] = { val networkFees = listNetworkFees(from, to).foldLeft(Map.empty[ByteVector32, Satoshi]) { (feeByChannelId, f) => feeByChannelId + (f.channelId -> (feeByChannelId.getOrElse(f.channelId, 0 sat) + f.fee)) } diff --git a/eclair-core/src/main/scala/fr/acinq/eclair/db/sqlite/SqliteChannelsDb.scala b/eclair-core/src/main/scala/fr/acinq/eclair/db/sqlite/SqliteChannelsDb.scala index 7fa7690c86..b7dfcad1ea 100644 --- a/eclair-core/src/main/scala/fr/acinq/eclair/db/sqlite/SqliteChannelsDb.scala +++ b/eclair-core/src/main/scala/fr/acinq/eclair/db/sqlite/SqliteChannelsDb.scala @@ -17,7 +17,7 @@ package fr.acinq.eclair.db.sqlite import fr.acinq.bitcoin.ByteVector32 -import fr.acinq.eclair.CltvExpiry +import fr.acinq.eclair.{CltvExpiry, TimestampMilli} import fr.acinq.eclair.channel.HasCommitments import fr.acinq.eclair.db.ChannelsDb import fr.acinq.eclair.db.DbEventHandler.ChannelEvent @@ -34,7 +34,7 @@ object SqliteChannelsDb { val CURRENT_VERSION = 4 } -class SqliteChannelsDb(sqlite: Connection) extends ChannelsDb with Logging { +class SqliteChannelsDb(val sqlite: Connection) extends ChannelsDb with Logging { import SqliteChannelsDb._ import SqliteUtils.ExtendedResultSet._ @@ -115,12 +115,19 @@ class SqliteChannelsDb(sqlite: Connection) extends ChannelsDb with Logging { } } + override def getChannel(channelId: ByteVector32): Option[HasCommitments] = withMetrics("channels/get-channel", DbBackends.Sqlite) { + using(sqlite.prepareStatement("SELECT data FROM local_channels WHERE channel_id=? AND is_closed=0")) { statement => + statement.setBytes(1, channelId.toArray) + statement.executeQuery.mapCodec(stateDataCodec).lastOption + } + } + /** * Helper method to factor updating timestamp columns */ private def updateChannelMetaTimestampColumn(channelId: ByteVector32, columnName: String): Unit = { using(sqlite.prepareStatement(s"UPDATE local_channels SET $columnName=? WHERE channel_id=?")) { statement => - statement.setLong(1, System.currentTimeMillis) + statement.setLong(1, TimestampMilli.now().toLong) statement.setBytes(2, channelId.toArray) statement.executeUpdate() } diff --git a/eclair-core/src/main/scala/fr/acinq/eclair/db/sqlite/SqliteFeeratesDb.scala b/eclair-core/src/main/scala/fr/acinq/eclair/db/sqlite/SqliteFeeratesDb.scala index b303acfa97..3717fa5bea 100644 --- a/eclair-core/src/main/scala/fr/acinq/eclair/db/sqlite/SqliteFeeratesDb.scala +++ b/eclair-core/src/main/scala/fr/acinq/eclair/db/sqlite/SqliteFeeratesDb.scala @@ -17,6 +17,7 @@ package fr.acinq.eclair.db.sqlite import fr.acinq.bitcoin.Satoshi +import fr.acinq.eclair.TimestampMilli import fr.acinq.eclair.blockchain.fee.{FeeratePerKB, FeeratesPerKB} import fr.acinq.eclair.db.FeeratesDb import grizzled.slf4j.Logging @@ -74,7 +75,7 @@ class SqliteFeeratesDb(sqlite: Connection) extends FeeratesDb with Logging { update.setLong(6, feeratesPerKB.blocks_72.toLong) update.setLong(7, feeratesPerKB.blocks_144.toLong) update.setLong(8, feeratesPerKB.blocks_1008.toLong) - update.setLong(9, System.currentTimeMillis()) + update.setLong(9, TimestampMilli.now().toLong) if (update.executeUpdate() == 0) { using(sqlite.prepareStatement("INSERT INTO feerates_per_kb VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?)")) { insert => insert.setLong(1, feeratesPerKB.block_1.toLong) @@ -85,7 +86,7 @@ class SqliteFeeratesDb(sqlite: Connection) extends FeeratesDb with Logging { insert.setLong(6, feeratesPerKB.blocks_72.toLong) insert.setLong(7, feeratesPerKB.blocks_144.toLong) insert.setLong(8, feeratesPerKB.blocks_1008.toLong) - insert.setLong(9, System.currentTimeMillis()) + insert.setLong(9, TimestampMilli.now().toLong) insert.executeUpdate() } } diff --git a/eclair-core/src/main/scala/fr/acinq/eclair/db/sqlite/SqlitePaymentsDb.scala b/eclair-core/src/main/scala/fr/acinq/eclair/db/sqlite/SqlitePaymentsDb.scala index 199470d5c2..4d2f5ac0b9 100644 --- a/eclair-core/src/main/scala/fr/acinq/eclair/db/sqlite/SqlitePaymentsDb.scala +++ b/eclair-core/src/main/scala/fr/acinq/eclair/db/sqlite/SqlitePaymentsDb.scala @@ -18,13 +18,13 @@ package fr.acinq.eclair.db.sqlite import fr.acinq.bitcoin.ByteVector32 import fr.acinq.bitcoin.Crypto.{PrivateKey, PublicKey} -import fr.acinq.eclair.MilliSatoshi import fr.acinq.eclair.db.Monitoring.Metrics.withMetrics import fr.acinq.eclair.db.Monitoring.Tags.DbBackends import fr.acinq.eclair.db._ import fr.acinq.eclair.db.sqlite.SqliteUtils._ import fr.acinq.eclair.payment.{PaymentFailed, PaymentRequest, PaymentSent} import fr.acinq.eclair.wire.protocol.CommonCodecs +import fr.acinq.eclair.{MilliSatoshi, TimestampMilli, TimestampMilliLong} import grizzled.slf4j.Logging import scodec.Attempt import scodec.bits.BitVector @@ -132,7 +132,7 @@ class SqlitePaymentsDb(sqlite: Connection) extends PaymentsDb with Logging { statement.setLong(6, sent.amount.toLong) statement.setLong(7, sent.recipientAmount.toLong) statement.setBytes(8, sent.recipientNodeId.value.toArray) - statement.setLong(9, sent.createdAt) + statement.setLong(9, sent.createdAt.toLong) statement.setString(10, sent.paymentRequest.map(PaymentRequest.write).orNull) statement.executeUpdate() } @@ -141,7 +141,7 @@ class SqlitePaymentsDb(sqlite: Connection) extends PaymentsDb with Logging { override def updateOutgoingPayment(paymentResult: PaymentSent): Unit = withMetrics("payments/update-outgoing-sent", DbBackends.Sqlite) { using(sqlite.prepareStatement("UPDATE sent_payments SET (completed_at, payment_preimage, fees_msat, payment_route) = (?, ?, ?, ?) WHERE id = ? AND completed_at IS NULL")) { statement => paymentResult.parts.foreach(p => { - statement.setLong(1, p.timestamp) + statement.setLong(1, p.timestamp.toLong) statement.setBytes(2, paymentResult.paymentPreimage.toArray) statement.setLong(3, p.feesPaid.toLong) statement.setBytes(4, paymentRouteCodec.encode(p.route.getOrElse(Nil).map(h => HopSummary(h)).toList).require.toByteArray) @@ -154,7 +154,7 @@ class SqlitePaymentsDb(sqlite: Connection) extends PaymentsDb with Logging { override def updateOutgoingPayment(paymentResult: PaymentFailed): Unit = withMetrics("payments/update-outgoing-failed", DbBackends.Sqlite) { using(sqlite.prepareStatement("UPDATE sent_payments SET (completed_at, failures) = (?, ?) WHERE id = ? AND completed_at IS NULL")) { statement => - statement.setLong(1, paymentResult.timestamp) + statement.setLong(1, paymentResult.timestamp.toLong) statement.setBytes(2, paymentFailuresCodec.encode(paymentResult.failures.map(f => FailureSummary(f)).toList).require.toByteArray) statement.setString(3, paymentResult.id.toString) if (statement.executeUpdate() == 0) throw new IllegalArgumentException(s"Tried to mark an outgoing payment as failed but already in final status (id=${paymentResult.id})") @@ -166,7 +166,7 @@ class SqlitePaymentsDb(sqlite: Connection) extends PaymentsDb with Logging { rs.getByteVector32Nullable("payment_preimage"), rs.getMilliSatoshiNullable("fees_msat"), rs.getBitVectorOpt("payment_route"), - rs.getLongNullable("completed_at"), + rs.getLongNullable("completed_at").map(TimestampMilli(_)), rs.getBitVectorOpt("failures")) OutgoingPayment( @@ -178,13 +178,13 @@ class SqlitePaymentsDb(sqlite: Connection) extends PaymentsDb with Logging { MilliSatoshi(rs.getLong("amount_msat")), MilliSatoshi(rs.getLong("recipient_amount_msat")), PublicKey(rs.getByteVector("recipient_node_id")), - rs.getLong("created_at"), + TimestampMilli(rs.getLong("created_at")), rs.getStringNullable("payment_request").map(PaymentRequest.read), status ) } - private def buildOutgoingPaymentStatus(preimage_opt: Option[ByteVector32], fees_opt: Option[MilliSatoshi], paymentRoute_opt: Option[BitVector], completedAt_opt: Option[Long], failures: Option[BitVector]): OutgoingPaymentStatus = { + private def buildOutgoingPaymentStatus(preimage_opt: Option[ByteVector32], fees_opt: Option[MilliSatoshi], paymentRoute_opt: Option[BitVector], completedAt_opt: Option[TimestampMilli], failures: Option[BitVector]): OutgoingPaymentStatus = { preimage_opt match { // If we have a pre-image, the payment succeeded. case Some(preimage) => OutgoingPaymentStatus.Succeeded( @@ -192,7 +192,7 @@ class SqlitePaymentsDb(sqlite: Connection) extends PaymentsDb with Logging { case Attempt.Successful(route) => route.value case Attempt.Failure(_) => Nil }).getOrElse(Nil), - completedAt_opt.getOrElse(0) + completedAt_opt.getOrElse(0 unixms) ) case None => completedAt_opt match { // Otherwise if the payment was marked completed, it's a failure. @@ -230,10 +230,10 @@ class SqlitePaymentsDb(sqlite: Connection) extends PaymentsDb with Logging { } } - override def listOutgoingPayments(from: Long, to: Long): Seq[OutgoingPayment] = withMetrics("payments/list-outgoing-by-timestamp", DbBackends.Sqlite) { + override def listOutgoingPayments(from: TimestampMilli, to: TimestampMilli): Seq[OutgoingPayment] = withMetrics("payments/list-outgoing-by-timestamp", DbBackends.Sqlite) { using(sqlite.prepareStatement("SELECT * FROM sent_payments WHERE created_at >= ? AND created_at < ? ORDER BY created_at")) { statement => - statement.setLong(1, from) - statement.setLong(2, to) + statement.setLong(1, from.toLong) + statement.setLong(2, to.toLong) statement.executeQuery().map(parseOutgoingPayment).toSeq } } @@ -244,16 +244,16 @@ class SqlitePaymentsDb(sqlite: Connection) extends PaymentsDb with Logging { statement.setBytes(2, preimage.toArray) statement.setString(3, paymentType) statement.setString(4, PaymentRequest.write(pr)) - statement.setLong(5, pr.timestamp.seconds.toMillis) // BOLT11 timestamp is in seconds - statement.setLong(6, (pr.timestamp + pr.expiry.getOrElse(PaymentRequest.DEFAULT_EXPIRY_SECONDS.toLong)).seconds.toMillis) + statement.setLong(5, pr.timestamp.toTimestampMilli.toLong) // BOLT11 timestamp is in seconds + statement.setLong(6, (pr.timestamp + pr.expiry.getOrElse(PaymentRequest.DEFAULT_EXPIRY_SECONDS)).toLong.seconds.toMillis) statement.executeUpdate() } } - override def receiveIncomingPayment(paymentHash: ByteVector32, amount: MilliSatoshi, receivedAt: Long): Unit = withMetrics("payments/receive-incoming", DbBackends.Sqlite) { + override def receiveIncomingPayment(paymentHash: ByteVector32, amount: MilliSatoshi, receivedAt: TimestampMilli): Unit = withMetrics("payments/receive-incoming", DbBackends.Sqlite) { using(sqlite.prepareStatement("UPDATE received_payments SET (received_msat, received_at) = (? + COALESCE(received_msat, 0), ?) WHERE payment_hash = ?")) { update => update.setLong(1, amount.toLong) - update.setLong(2, receivedAt) + update.setLong(2, receivedAt.toLong) update.setBytes(3, paymentHash.toArray) val updated = update.executeUpdate() if (updated == 0) { @@ -268,13 +268,13 @@ class SqlitePaymentsDb(sqlite: Connection) extends PaymentsDb with Logging { PaymentRequest.read(paymentRequest), rs.getByteVector32("payment_preimage"), rs.getString("payment_type"), - rs.getLong("created_at"), - buildIncomingPaymentStatus(rs.getMilliSatoshiNullable("received_msat"), Some(paymentRequest), rs.getLongNullable("received_at"))) + TimestampMilli(rs.getLong("created_at")), + buildIncomingPaymentStatus(rs.getMilliSatoshiNullable("received_msat"), Some(paymentRequest), rs.getLongNullable("received_at").map(TimestampMilli(_)))) } - private def buildIncomingPaymentStatus(amount_opt: Option[MilliSatoshi], serializedPaymentRequest_opt: Option[String], receivedAt_opt: Option[Long]): IncomingPaymentStatus = { + private def buildIncomingPaymentStatus(amount_opt: Option[MilliSatoshi], serializedPaymentRequest_opt: Option[String], receivedAt_opt: Option[TimestampMilli]): IncomingPaymentStatus = { amount_opt match { - case Some(amount) => IncomingPaymentStatus.Received(amount, receivedAt_opt.getOrElse(0)) + case Some(amount) => IncomingPaymentStatus.Received(amount, receivedAt_opt.getOrElse(0 unixms)) case None if serializedPaymentRequest_opt.exists(PaymentRequest.fastHasExpired) => IncomingPaymentStatus.Expired case None => IncomingPaymentStatus.Pending } @@ -287,36 +287,36 @@ class SqlitePaymentsDb(sqlite: Connection) extends PaymentsDb with Logging { } } - override def listIncomingPayments(from: Long, to: Long): Seq[IncomingPayment] = withMetrics("payments/list-incoming", DbBackends.Sqlite) { + override def listIncomingPayments(from: TimestampMilli, to: TimestampMilli): Seq[IncomingPayment] = withMetrics("payments/list-incoming", DbBackends.Sqlite) { using(sqlite.prepareStatement("SELECT * FROM received_payments WHERE created_at > ? AND created_at < ? ORDER BY created_at")) { statement => - statement.setLong(1, from) - statement.setLong(2, to) + statement.setLong(1, from.toLong) + statement.setLong(2, to.toLong) statement.executeQuery().map(parseIncomingPayment).toSeq } } - override def listReceivedIncomingPayments(from: Long, to: Long): Seq[IncomingPayment] = withMetrics("payments/list-incoming-received", DbBackends.Sqlite) { + override def listReceivedIncomingPayments(from: TimestampMilli, to: TimestampMilli): Seq[IncomingPayment] = withMetrics("payments/list-incoming-received", DbBackends.Sqlite) { using(sqlite.prepareStatement("SELECT * FROM received_payments WHERE received_msat > 0 AND created_at > ? AND created_at < ? ORDER BY created_at")) { statement => - statement.setLong(1, from) - statement.setLong(2, to) + statement.setLong(1, from.toLong) + statement.setLong(2, to.toLong) statement.executeQuery().map(parseIncomingPayment).toSeq } } - override def listPendingIncomingPayments(from: Long, to: Long): Seq[IncomingPayment] = withMetrics("payments/list-incoming-pending", DbBackends.Sqlite) { + override def listPendingIncomingPayments(from: TimestampMilli, to: TimestampMilli): Seq[IncomingPayment] = withMetrics("payments/list-incoming-pending", DbBackends.Sqlite) { using(sqlite.prepareStatement("SELECT * FROM received_payments WHERE received_msat IS NULL AND created_at > ? AND created_at < ? AND expire_at > ? ORDER BY created_at")) { statement => - statement.setLong(1, from) - statement.setLong(2, to) - statement.setLong(3, System.currentTimeMillis) + statement.setLong(1, from.toLong) + statement.setLong(2, to.toLong) + statement.setLong(3, TimestampMilli.now().toLong) statement.executeQuery().map(parseIncomingPayment).toSeq } } - override def listExpiredIncomingPayments(from: Long, to: Long): Seq[IncomingPayment] = withMetrics("payments/list-incoming-expired", DbBackends.Sqlite) { + override def listExpiredIncomingPayments(from: TimestampMilli, to: TimestampMilli): Seq[IncomingPayment] = withMetrics("payments/list-incoming-expired", DbBackends.Sqlite) { using(sqlite.prepareStatement("SELECT * FROM received_payments WHERE received_msat IS NULL AND created_at > ? AND created_at < ? AND expire_at < ? ORDER BY created_at")) { statement => - statement.setLong(1, from) - statement.setLong(2, to) - statement.setLong(3, System.currentTimeMillis) + statement.setLong(1, from.toLong) + statement.setLong(2, to.toLong) + statement.setLong(3, TimestampMilli.now().toLong) statement.executeQuery().map(parseIncomingPayment).toSeq } } @@ -373,9 +373,9 @@ class SqlitePaymentsDb(sqlite: Connection) extends PaymentsDb with Logging { val paymentType = rs.getString("payment_type") val paymentRequest_opt = rs.getStringNullable("payment_request") val amount_opt = rs.getMilliSatoshiNullable("final_amount") - val createdAt = rs.getLong("created_at") - val completedAt_opt = rs.getLongNullable("completed_at") - val expireAt_opt = rs.getLongNullable("expire_at") + val createdAt = TimestampMilli(rs.getLong("created_at")) + val completedAt_opt = rs.getLongNullable("completed_at").map(TimestampMilli(_)) + val expireAt_opt = rs.getLongNullable("expire_at").map(TimestampMilli(_)) if (rs.getString("type") == "received") { val status: IncomingPaymentStatus = buildIncomingPaymentStatus(amount_opt, paymentRequest_opt, completedAt_opt) diff --git a/eclair-core/src/main/scala/fr/acinq/eclair/io/Peer.scala b/eclair-core/src/main/scala/fr/acinq/eclair/io/Peer.scala index 69bda6a352..a8e0866d16 100644 --- a/eclair-core/src/main/scala/fr/acinq/eclair/io/Peer.scala +++ b/eclair-core/src/main/scala/fr/acinq/eclair/io/Peer.scala @@ -101,7 +101,7 @@ class Peer(val nodeParams: NodeParams, remoteNodeId: PublicKey, wallet: OnChainA sender() ! PeerConnection.ConnectionResult.AlreadyConnected stay() - case Event(Channel.OutgoingMessage(msg, peerConnection), d: ConnectedData) if peerConnection == d.peerConnection => // this is an outgoing message, but we need to make sure that this is for the current active connection + case Event(Peer.OutgoingMessage(msg, peerConnection), d: ConnectedData) if peerConnection == d.peerConnection => // this is an outgoing message, but we need to make sure that this is for the current active connection logMessage(msg, "OUT") d.peerConnection forward msg stay() @@ -173,7 +173,8 @@ class Peer(val nodeParams: NodeParams, remoteNodeId: PublicKey, wallet: OnChainA stay() using d.copy(channels = d.channels + (TemporaryChannelId(temporaryChannelId) -> channel)) case Left(ex) => log.warning(s"ignoring open_channel: ${ex.getMessage}") - d.peerConnection ! Error(msg.temporaryChannelId, ex.getMessage) + val err = Error(msg.temporaryChannelId, ex.getMessage) + self ! Peer.OutgoingMessage(err, d.peerConnection) stay() } case Some(_) => @@ -262,7 +263,7 @@ class Peer(val nodeParams: NodeParams, remoteNodeId: PublicKey, wallet: OnChainA }, d.channels.values.toSet.size) // we use toSet to dedup because a channel can have a TemporaryChannelId + a ChannelId stay() - case Event(_: Channel.OutgoingMessage, _) => stay() // we got disconnected or reconnected and this message was for the previous connection + case Event(_: Peer.OutgoingMessage, _) => stay() // we got disconnected or reconnected and this message was for the previous connection } private val reconnectionTask = context.actorOf(ReconnectionTask.props(nodeParams, remoteNodeId), "reconnection-task") @@ -337,8 +338,7 @@ class Peer(val nodeParams: NodeParams, remoteNodeId: PublicKey, wallet: OnChainA def replyUnknownChannel(peerConnection: ActorRef, unknownChannelId: ByteVector32): Unit = { val msg = Warning(unknownChannelId, "unknown channel") - logMessage(msg, "OUT") - peerConnection ! msg + self ! Peer.OutgoingMessage(msg, peerConnection) } def stopPeer(): State = { @@ -437,6 +437,15 @@ object Peer { case class PeerRoutingMessage(peerConnection: ActorRef, remoteNodeId: PublicKey, message: RoutingMessage) extends RemoteTypes + /** + * Dedicated command for outgoing messages for logging purposes. + * + * To preserve sequentiality of messages in the event of disconnections and reconnections, we provide a reference to + * the connection that the message is valid for. If the actual connection was reset in the meantime, the [[Peer]] + * will simply drop the message. + */ + case class OutgoingMessage(msg: LightningMessage, peerConnection: ActorRef) + case class Transition(previousData: Peer.Data, nextData: Peer.Data) /** diff --git a/eclair-core/src/main/scala/fr/acinq/eclair/io/PeerConnection.scala b/eclair-core/src/main/scala/fr/acinq/eclair/io/PeerConnection.scala index 8fe812ffe7..9d8790deb2 100644 --- a/eclair-core/src/main/scala/fr/acinq/eclair/io/PeerConnection.scala +++ b/eclair-core/src/main/scala/fr/acinq/eclair/io/PeerConnection.scala @@ -28,7 +28,7 @@ import fr.acinq.eclair.remote.EclairInternalsSerializer.RemoteTypes import fr.acinq.eclair.router.Router._ import fr.acinq.eclair.wire.protocol import fr.acinq.eclair.wire.protocol._ -import fr.acinq.eclair.{FSMDiagnosticActorLogging, Features, Logs} +import fr.acinq.eclair.{FSMDiagnosticActorLogging, Features, Logs, TimestampMilli, TimestampSecond} import scodec.Attempt import scodec.bits.ByteVector @@ -203,7 +203,7 @@ class PeerConnection(keyPair: KeyPair, conf: PeerConnection.Conf, switchboard: A d.expectedPong_opt match { case Some(ExpectedPong(ping, timestamp)) if ping.pongLength == data.length => // we use the pong size to correlate between pings and pongs - val latency = System.currentTimeMillis - timestamp + val latency = TimestampMilli.now() - timestamp log.debug(s"received pong with latency=$latency") cancelTimer(PingTimeout.toString()) // we don't need to call scheduleNextPing here, the next ping was already scheduled when we received that pong @@ -496,7 +496,7 @@ object PeerConnection { case class InitializingData(chainHash: ByteVector32, pendingAuth: PendingAuth, remoteNodeId: PublicKey, transport: ActorRef, peer: ActorRef, localInit: protocol.Init, doSync: Boolean) extends Data with HasTransport case class ConnectedData(chainHash: ByteVector32, remoteNodeId: PublicKey, transport: ActorRef, peer: ActorRef, localInit: protocol.Init, remoteInit: protocol.Init, rebroadcastDelay: FiniteDuration, gossipTimestampFilter: Option[GossipTimestampFilter] = None, behavior: Behavior = Behavior(), expectedPong_opt: Option[ExpectedPong] = None) extends Data with HasTransport - case class ExpectedPong(ping: Ping, timestamp: Long = System.currentTimeMillis) + case class ExpectedPong(ping: Ping, timestamp: TimestampMilli = TimestampMilli.now()) case class PingTimeout(ping: Ping) sealed trait State @@ -563,7 +563,7 @@ object PeerConnection { // Otherwise we check if this message has a timestamp that matches the timestamp filter. val matchesFilter = (msg, gossipTimestampFilter_opt) match { case (_, None) => false // BOLT 7: A node which wants any gossip messages would have to send this, otherwise [...] no gossip messages would be received. - case (hasTs: HasTimestamp, Some(GossipTimestampFilter(_, firstTimestamp, timestampRange, _))) => hasTs.timestamp >= firstTimestamp && hasTs.timestamp <= firstTimestamp + timestampRange + case (hasTs: HasTimestamp, Some(GossipTimestampFilter(_, firstTimestamp, timestampRange, _))) => hasTs.timestamp >= firstTimestamp && hasTs.timestamp <= TimestampSecond(firstTimestamp.toLong + timestampRange) case _ => true // if there is a filter and message doesn't have a timestamp (e.g. channel_announcement), then we send it } isOurGossip || matchesFilter diff --git a/eclair-core/src/main/scala/fr/acinq/eclair/io/ReconnectionTask.scala b/eclair-core/src/main/scala/fr/acinq/eclair/io/ReconnectionTask.scala index 27f4cac905..3d40af4b1b 100644 --- a/eclair-core/src/main/scala/fr/acinq/eclair/io/ReconnectionTask.scala +++ b/eclair-core/src/main/scala/fr/acinq/eclair/io/ReconnectionTask.scala @@ -17,7 +17,6 @@ package fr.acinq.eclair.io import java.net.InetSocketAddress - import akka.actor.{ActorRef, Props} import akka.cluster.Cluster import akka.cluster.pubsub.DistributedPubSub @@ -28,7 +27,7 @@ import fr.acinq.bitcoin.Crypto.PublicKey import fr.acinq.eclair.Logs.LogCategory import fr.acinq.eclair.db.{NetworkDb, PeersDb} import fr.acinq.eclair.io.Monitoring.Metrics -import fr.acinq.eclair.{FSMDiagnosticActorLogging, Logs, NodeParams} +import fr.acinq.eclair.{FSMDiagnosticActorLogging, Logs, NodeParams, TimestampMilli, TimestampSecond} import scala.concurrent.duration.{FiniteDuration, _} import scala.util.Random @@ -97,7 +96,7 @@ class ReconnectionTask(nodeParams: NodeParams, remoteNodeId: PublicKey) extends val firstNextReconnectionDelay = nodeParams.maxReconnectInterval.minus(Random.nextInt(nodeParams.maxReconnectInterval.toSeconds.toInt / 2).seconds) log.debug("first connection attempt in {}", initialDelay) (initialDelay, firstNextReconnectionDelay) - case (_, cd: ConnectingData) if System.currentTimeMillis.milliseconds - d.since < 30.seconds => + case (_, cd: ConnectingData) if TimestampMilli.now() - d.since < 30.seconds => // If our latest successful connection attempt was less than 30 seconds ago, we pick up the exponential // back-off retry delay where we left it. The goal is to address cases where the reconnection is successful, // but we are disconnected right away. @@ -185,7 +184,7 @@ object ReconnectionTask { // @formatter:off sealed trait Data case object Nothing extends Data - case class IdleData(previousData: Data, since: FiniteDuration = System.currentTimeMillis.milliseconds) extends Data + case class IdleData(previousData: Data, since: TimestampMilli = TimestampMilli.now()) extends Data case class ConnectingData(to: InetSocketAddress, nextReconnectionDelay: FiniteDuration) extends Data case class WaitingData(nextReconnectionDelay: FiniteDuration) extends Data // @formatter:on diff --git a/eclair-core/src/main/scala/fr/acinq/eclair/io/Switchboard.scala b/eclair-core/src/main/scala/fr/acinq/eclair/io/Switchboard.scala index 07f144ed5c..d1f13b7f25 100644 --- a/eclair-core/src/main/scala/fr/acinq/eclair/io/Switchboard.scala +++ b/eclair-core/src/main/scala/fr/acinq/eclair/io/Switchboard.scala @@ -49,6 +49,7 @@ class Switchboard(nodeParams: NodeParams, peerFactory: Switchboard.PeerFactory) val peerChannels = channels.groupBy(_.commitments.remoteParams.nodeId) peerChannels.foreach { case (remoteNodeId, states) => createOrGetPeer(remoteNodeId, offlineChannels = states.toSet) } + log.info("restoring {} peer(s) with {} channel(s)", peerChannels.size, channels.size) peerChannels.keySet } @@ -109,7 +110,7 @@ class Switchboard(nodeParams: NodeParams, peerFactory: Switchboard.PeerFactory) getPeer(remoteNodeId) match { case Some(peer) => peer case None => - log.info(s"creating new peer current=${context.children.size}") + log.debug(s"creating new peer (current={})", context.children.size) val peer = createPeer(remoteNodeId) peer ! Peer.Init(offlineChannels) peer diff --git a/eclair-core/src/main/scala/fr/acinq/eclair/json/JsonSerializers.scala b/eclair-core/src/main/scala/fr/acinq/eclair/json/JsonSerializers.scala index 8b974fa225..ae4ee19c4e 100644 --- a/eclair-core/src/main/scala/fr/acinq/eclair/json/JsonSerializers.scala +++ b/eclair-core/src/main/scala/fr/acinq/eclair/json/JsonSerializers.scala @@ -31,7 +31,7 @@ import fr.acinq.eclair.router.Router.{Route, RouteResponse} import fr.acinq.eclair.transactions.DirectedHtlc import fr.acinq.eclair.transactions.Transactions._ import fr.acinq.eclair.wire.protocol._ -import fr.acinq.eclair.{CltvExpiry, CltvExpiryDelta, Feature, FeatureSupport, MilliSatoshi, ShortChannelId, UInt64, UnknownFeature} +import fr.acinq.eclair.{CltvExpiry, CltvExpiryDelta, Feature, FeatureSupport, MilliSatoshi, ShortChannelId, TimestampMilli, TimestampSecond, UInt64, UnknownFeature} import org.json4s import org.json4s.JsonAST._ import org.json4s.jackson.Serialization @@ -39,6 +39,8 @@ import org.json4s.{DefaultFormats, Extraction, Formats, JDecimal, JValue, KeySer import scodec.bits.ByteVector import java.net.InetSocketAddress +import java.time.Instant +import java.time.format.DateTimeFormatter import java.util.UUID /** @@ -108,6 +110,18 @@ object UInt64Serializer extends MinimalSerializer({ case x: UInt64 => JInt(x.toBigInt) }) +// @formatter:off +private case class TimestampJson(iso: String, unix: Long) +object TimestampSecondSerializer extends ConvertClassSerializer[TimestampSecond](ts => TimestampJson( + iso = DateTimeFormatter.ISO_INSTANT.format(Instant.ofEpochSecond(ts.toLong)), + unix = ts.toLong +)) +object TimestampMilliSerializer extends ConvertClassSerializer[TimestampMilli](ts => TimestampJson( + iso = DateTimeFormatter.ISO_INSTANT.format(Instant.ofEpochMilli(ts.toLong)), + unix = ts.toLong / 1000 // we convert to standard unix timestamp with second precision +)) +// @formatter:on + object BtcSerializer extends MinimalSerializer({ case x: Btc => JDecimal(x.toDouble) }) @@ -254,6 +268,7 @@ object RouteSerializer extends MinimalSerializer ({ PublicKeySerializer + ShortChannelIdSerializer + MilliSatoshiSerializer + + TimestampSecondSerializer + CltvExpiryDeltaSerializer) }) @@ -320,7 +335,7 @@ object PaymentRequestSerializer extends MinimalSerializer({ CltvExpiryDeltaSerializer )) val fieldList = List(JField("prefix", JString(p.prefix)), - JField("timestamp", JLong(p.timestamp)), + JField("timestamp", JLong(p.timestamp.toLong)), JField("nodeId", JString(p.nodeId.toString())), JField("serialized", JString(PaymentRequest.write(p))), p.description.fold(string => JField("description", JString(string)), hash => JField("descriptionHash", JString(hash.toHex))), @@ -447,6 +462,8 @@ object JsonSerializers { ByteVector64Serializer + ChannelEventSerializer + UInt64Serializer + + TimestampSecondSerializer + + TimestampMilliSerializer + BtcSerializer + SatoshiSerializer + MilliSatoshiSerializer + diff --git a/eclair-core/src/main/scala/fr/acinq/eclair/package.scala b/eclair-core/src/main/scala/fr/acinq/eclair/package.scala index 310ac0c1c8..0ff85d1276 100644 --- a/eclair-core/src/main/scala/fr/acinq/eclair/package.scala +++ b/eclair-core/src/main/scala/fr/acinq/eclair/package.scala @@ -99,6 +99,14 @@ package object eclair { def msat = MilliSatoshi(n) } + implicit class TimestampSecondLong(private val n: Long) extends AnyVal { + def unixsec = TimestampSecond(n) + } + + implicit class TimestampMilliLong(private val n: Long) extends AnyVal { + def unixms = TimestampMilli(n) + } + // We implement Numeric to take advantage of operations such as sum, sort or min/max on iterables. implicit object NumericMilliSatoshi extends Numeric[MilliSatoshi] { // @formatter:off diff --git a/eclair-core/src/main/scala/fr/acinq/eclair/payment/PaymentEvents.scala b/eclair-core/src/main/scala/fr/acinq/eclair/payment/PaymentEvents.scala index 4b7ad673e9..e8e4651a33 100644 --- a/eclair-core/src/main/scala/fr/acinq/eclair/payment/PaymentEvents.scala +++ b/eclair-core/src/main/scala/fr/acinq/eclair/payment/PaymentEvents.scala @@ -24,9 +24,10 @@ import fr.acinq.eclair.payment.send.PaymentInitiator.SendPaymentConfig import fr.acinq.eclair.router.Announcements import fr.acinq.eclair.router.Router.{ChannelDesc, ChannelHop, Hop, Ignore} import fr.acinq.eclair.wire.protocol.{ChannelDisabled, ChannelUpdate, Node, TemporaryChannelFailure} -import fr.acinq.eclair.{MilliSatoshi, ShortChannelId} +import fr.acinq.eclair.{MilliSatoshi, ShortChannelId, TimestampMilli} import java.util.UUID +import scala.concurrent.duration.FiniteDuration import scala.util.{Failure, Success, Try} /** @@ -35,7 +36,7 @@ import scala.util.{Failure, Success, Try} sealed trait PaymentEvent { val paymentHash: ByteVector32 - val timestamp: Long + val timestamp: TimestampMilli } /** @@ -55,7 +56,7 @@ case class PaymentSent(id: UUID, paymentHash: ByteVector32, paymentPreimage: Byt val feesPaid: MilliSatoshi = amountWithFees - recipientAmount // overall fees for this payment (routing + trampoline) val trampolineFees: MilliSatoshi = parts.map(_.amount).sum - recipientAmount val nonTrampolineFees: MilliSatoshi = feesPaid - trampolineFees // routing fees to reach the first trampoline node, or the recipient if not using trampoline - val timestamp: Long = parts.map(_.timestamp).min // we use min here because we receive the proof of payment as soon as the first partial payment is fulfilled + val timestamp: TimestampMilli = parts.map(_.timestamp).min // we use min here because we receive the proof of payment as soon as the first partial payment is fulfilled } object PaymentSent { @@ -70,24 +71,24 @@ object PaymentSent { * @param route payment route used. * @param timestamp absolute time in milli-seconds since UNIX epoch when the payment was fulfilled. */ - case class PartialPayment(id: UUID, amount: MilliSatoshi, feesPaid: MilliSatoshi, toChannelId: ByteVector32, route: Option[Seq[Hop]], timestamp: Long = System.currentTimeMillis) { + case class PartialPayment(id: UUID, amount: MilliSatoshi, feesPaid: MilliSatoshi, toChannelId: ByteVector32, route: Option[Seq[Hop]], timestamp: TimestampMilli = TimestampMilli.now()) { require(route.isEmpty || route.get.nonEmpty, "route must be None or contain at least one hop") val amountWithFees: MilliSatoshi = amount + feesPaid } } -case class PaymentFailed(id: UUID, paymentHash: ByteVector32, failures: Seq[PaymentFailure], timestamp: Long = System.currentTimeMillis) extends PaymentEvent +case class PaymentFailed(id: UUID, paymentHash: ByteVector32, failures: Seq[PaymentFailure], timestamp: TimestampMilli = TimestampMilli.now()) extends PaymentEvent sealed trait PaymentRelayed extends PaymentEvent { val amountIn: MilliSatoshi val amountOut: MilliSatoshi - val timestamp: Long + val timestamp: TimestampMilli } -case class ChannelPaymentRelayed(amountIn: MilliSatoshi, amountOut: MilliSatoshi, paymentHash: ByteVector32, fromChannelId: ByteVector32, toChannelId: ByteVector32, timestamp: Long = System.currentTimeMillis) extends PaymentRelayed +case class ChannelPaymentRelayed(amountIn: MilliSatoshi, amountOut: MilliSatoshi, paymentHash: ByteVector32, fromChannelId: ByteVector32, toChannelId: ByteVector32, timestamp: TimestampMilli = TimestampMilli.now()) extends PaymentRelayed -case class TrampolinePaymentRelayed(paymentHash: ByteVector32, incoming: PaymentRelayed.Incoming, outgoing: PaymentRelayed.Outgoing, nextTrampolineNodeId: PublicKey, nextTrampolineAmount: MilliSatoshi, timestamp: Long = System.currentTimeMillis) extends PaymentRelayed { +case class TrampolinePaymentRelayed(paymentHash: ByteVector32, incoming: PaymentRelayed.Incoming, outgoing: PaymentRelayed.Outgoing, nextTrampolineNodeId: PublicKey, nextTrampolineAmount: MilliSatoshi, timestamp: TimestampMilli = TimestampMilli.now()) extends PaymentRelayed { override val amountIn: MilliSatoshi = incoming.map(_.amount).sum override val amountOut: MilliSatoshi = outgoing.map(_.amount).sum } @@ -104,16 +105,16 @@ object PaymentRelayed { case class PaymentReceived(paymentHash: ByteVector32, parts: Seq[PaymentReceived.PartialPayment]) extends PaymentEvent { require(parts.nonEmpty, "must have at least one payment part") val amount: MilliSatoshi = parts.map(_.amount).sum - val timestamp: Long = parts.map(_.timestamp).max // we use max here because we fulfill the payment only once we received all the parts + val timestamp: TimestampMilli = parts.map(_.timestamp).max // we use max here because we fulfill the payment only once we received all the parts } object PaymentReceived { - case class PartialPayment(amount: MilliSatoshi, fromChannelId: ByteVector32, timestamp: Long = System.currentTimeMillis) + case class PartialPayment(amount: MilliSatoshi, fromChannelId: ByteVector32, timestamp: TimestampMilli = TimestampMilli.now()) } -case class PaymentSettlingOnChain(id: UUID, amount: MilliSatoshi, paymentHash: ByteVector32, timestamp: Long = System.currentTimeMillis) extends PaymentEvent +case class PaymentSettlingOnChain(id: UUID, amount: MilliSatoshi, paymentHash: ByteVector32, timestamp: TimestampMilli = TimestampMilli.now()) extends PaymentEvent sealed trait PaymentFailure { // @formatter:off @@ -250,8 +251,8 @@ object PaymentFailure { case class PathFindingExperimentMetrics(amount: MilliSatoshi, fees: MilliSatoshi, status: String, - duration: Long, - timestamp: Long, + duration: FiniteDuration, + timestamp: TimestampMilli, isMultiPart: Boolean, experimentName: String, recipientNodeId: PublicKey) diff --git a/eclair-core/src/main/scala/fr/acinq/eclair/payment/PaymentRequest.scala b/eclair-core/src/main/scala/fr/acinq/eclair/payment/PaymentRequest.scala index 2b8dd2ac14..beb32089bd 100644 --- a/eclair-core/src/main/scala/fr/acinq/eclair/payment/PaymentRequest.scala +++ b/eclair-core/src/main/scala/fr/acinq/eclair/payment/PaymentRequest.scala @@ -19,7 +19,7 @@ package fr.acinq.eclair.payment import fr.acinq.bitcoin.Crypto.{PrivateKey, PublicKey} import fr.acinq.bitcoin.{Base58, Base58Check, Bech32, Block, ByteVector32, ByteVector64, Crypto} import fr.acinq.eclair.payment.PaymentRequest._ -import fr.acinq.eclair.{CltvExpiryDelta, FeatureSupport, Features, MilliSatoshi, MilliSatoshiLong, NodeParams, ShortChannelId, randomBytes32} +import fr.acinq.eclair.{CltvExpiryDelta, FeatureSupport, Features, MilliSatoshi, MilliSatoshiLong, NodeParams, ShortChannelId, TimestampSecond, randomBytes32} import scodec.bits.{BitVector, ByteOrdering, ByteVector} import scodec.codecs.{list, ubyte} import scodec.{Codec, Err} @@ -38,7 +38,7 @@ import scala.util.{Failure, Success, Try} * @param tags payment tags; must include a single PaymentHash tag and a single PaymentSecret tag. * @param signature request signature that will be checked against node id */ -case class PaymentRequest(prefix: String, amount: Option[MilliSatoshi], timestamp: Long, nodeId: PublicKey, tags: List[PaymentRequest.TaggedField], signature: ByteVector) { +case class PaymentRequest(prefix: String, amount: Option[MilliSatoshi], timestamp: TimestampSecond, nodeId: PublicKey, tags: List[PaymentRequest.TaggedField], signature: ByteVector) { amount.foreach(a => require(a > 0.msat, s"amount is not valid")) require(tags.collect { case _: PaymentRequest.PaymentHash => }.size == 1, "there must be exactly one payment hash tag") @@ -87,8 +87,8 @@ case class PaymentRequest(prefix: String, amount: Option[MilliSatoshi], timestam lazy val features: PaymentRequestFeatures = tags.collectFirst { case f: PaymentRequestFeatures => f }.getOrElse(PaymentRequestFeatures(BitVector.empty)) def isExpired: Boolean = expiry match { - case Some(expiryTime) => timestamp + expiryTime <= System.currentTimeMillis.milliseconds.toSeconds - case None => timestamp + DEFAULT_EXPIRY_SECONDS <= System.currentTimeMillis.milliseconds.toSeconds + case Some(expiryTime) => timestamp + expiryTime <= TimestampSecond.now() + case None => timestamp + DEFAULT_EXPIRY_SECONDS <= TimestampSecond.now() } /** @@ -118,7 +118,7 @@ case class PaymentRequest(prefix: String, amount: Option[MilliSatoshi], timestam object PaymentRequest { - val DEFAULT_EXPIRY_SECONDS = 3600 + val DEFAULT_EXPIRY_SECONDS: Long = 3600 val prefixes = Map( Block.RegtestGenesisBlock.hash -> "lnbcrt", @@ -135,7 +135,7 @@ object PaymentRequest { fallbackAddress: Option[String] = None, expirySeconds: Option[Long] = None, extraHops: List[List[ExtraHop]] = Nil, - timestamp: Long = System.currentTimeMillis() / 1000L, + timestamp: TimestampSecond = TimestampSecond.now(), paymentSecret: ByteVector32 = randomBytes32(), features: PaymentRequestFeatures = PaymentRequestFeatures(Features.VariableLengthOnion.mandatory, Features.PaymentSecret.mandatory)): PaymentRequest = { require(features.requirePaymentSecret, "invoices must require a payment secret") @@ -163,7 +163,7 @@ object PaymentRequest { ).sign(privateKey) } - case class Bolt11Data(timestamp: Long, taggedFields: List[TaggedField], signature: ByteVector) + case class Bolt11Data(timestamp: TimestampSecond, taggedFields: List[TaggedField], signature: ByteVector) sealed trait TaggedField @@ -444,7 +444,7 @@ object PaymentRequest { ) val bolt11DataCodec: Codec[Bolt11Data] = ( - ("timestamp" | ulong(35)) :: + ("timestamp" | ulong(35).xmapc(TimestampSecond(_))(_.toLong)) :: ("taggedFields" | fixedSizeTrailingCodec(list(taggedFieldCodec), 520)) :: ("signature" | bytes(65)) ).as[Bolt11Data] @@ -564,8 +564,8 @@ object PaymentRequest { } val timestamp = bolt11Data.timestamp expiry_opt match { - case Some(expiry) => timestamp + expiry.toLong <= System.currentTimeMillis.milliseconds.toSeconds - case None => timestamp + DEFAULT_EXPIRY_SECONDS <= System.currentTimeMillis.milliseconds.toSeconds + case Some(expiry) => timestamp + expiry.toLong <= TimestampSecond.now() + case None => timestamp + DEFAULT_EXPIRY_SECONDS <= TimestampSecond.now() } } diff --git a/eclair-core/src/main/scala/fr/acinq/eclair/payment/receive/MultiPartPaymentFSM.scala b/eclair-core/src/main/scala/fr/acinq/eclair/payment/receive/MultiPartPaymentFSM.scala index 6779a0e416..e6babcabfa 100644 --- a/eclair-core/src/main/scala/fr/acinq/eclair/payment/receive/MultiPartPaymentFSM.scala +++ b/eclair-core/src/main/scala/fr/acinq/eclair/payment/receive/MultiPartPaymentFSM.scala @@ -22,7 +22,7 @@ import fr.acinq.bitcoin.ByteVector32 import fr.acinq.eclair.payment.Monitoring.{Metrics, Tags} import fr.acinq.eclair.wire.protocol import fr.acinq.eclair.wire.protocol.{FailureMessage, IncorrectOrUnknownPaymentDetails, UpdateAddHtlc} -import fr.acinq.eclair.{FSMDiagnosticActorLogging, Logs, MilliSatoshi, NodeParams} +import fr.acinq.eclair.{FSMDiagnosticActorLogging, Logs, MilliSatoshi, NodeParams, TimestampMilli} import java.util.concurrent.TimeUnit import scala.collection.immutable.Queue @@ -41,7 +41,7 @@ class MultiPartPaymentFSM(nodeParams: NodeParams, paymentHash: ByteVector32, tot import MultiPartPaymentFSM._ - val start = System.currentTimeMillis + val start = TimestampMilli.now() startSingleTimer(PaymentTimeout.toString, PaymentTimeout, nodeParams.multiPartPaymentExpiry) @@ -97,7 +97,7 @@ class MultiPartPaymentFSM(nodeParams: NodeParams, paymentHash: ByteVector32, tot case PaymentSucceeded(parts) => // We expect the parent actor to send us a PoisonPill after receiving this message. replyTo ! MultiPartPaymentSucceeded(paymentHash, parts) - Metrics.ReceivedPaymentDuration.withTag(Tags.Success, value = true).record(System.currentTimeMillis - start, TimeUnit.MILLISECONDS) + Metrics.ReceivedPaymentDuration.withTag(Tags.Success, value = true).record((TimestampMilli.now() - start).toMillis, TimeUnit.MILLISECONDS) case d => log.error("unexpected payment success data {}", d.getClass.getSimpleName) } @@ -106,7 +106,7 @@ class MultiPartPaymentFSM(nodeParams: NodeParams, paymentHash: ByteVector32, tot case PaymentFailed(failure, parts) => // We expect the parent actor to send us a PoisonPill after receiving this message. replyTo ! MultiPartPaymentFailed(paymentHash, failure, parts) - Metrics.ReceivedPaymentDuration.withTag(Tags.Success, value = false).record(System.currentTimeMillis - start, TimeUnit.MILLISECONDS) + Metrics.ReceivedPaymentDuration.withTag(Tags.Success, value = false).record((TimestampMilli.now() - start).toMillis, TimeUnit.MILLISECONDS) case d => log.error("unexpected payment failure data {}", d.getClass.getSimpleName) } diff --git a/eclair-core/src/main/scala/fr/acinq/eclair/payment/relay/PostRestartHtlcCleaner.scala b/eclair-core/src/main/scala/fr/acinq/eclair/payment/relay/PostRestartHtlcCleaner.scala index 79dcb2c3e5..732e732621 100644 --- a/eclair-core/src/main/scala/fr/acinq/eclair/payment/relay/PostRestartHtlcCleaner.scala +++ b/eclair-core/src/main/scala/fr/acinq/eclair/payment/relay/PostRestartHtlcCleaner.scala @@ -28,7 +28,7 @@ import fr.acinq.eclair.payment.Monitoring.Tags import fr.acinq.eclair.payment.{ChannelPaymentRelayed, IncomingPacket, PaymentFailed, PaymentSent} import fr.acinq.eclair.transactions.DirectedHtlc.outgoing import fr.acinq.eclair.wire.protocol.{FailureMessage, TemporaryNodeFailure, UpdateAddHtlc} -import fr.acinq.eclair.{CustomCommitmentsPlugin, MilliSatoshiLong, NodeParams} +import fr.acinq.eclair.{CustomCommitmentsPlugin, MilliSatoshiLong, NodeParams, TimestampMilli} import scala.concurrent.Promise import scala.util.Try @@ -104,16 +104,20 @@ class PostRestartHtlcCleaner(nodeParams: NodeParams, register: ActorRef, initial // this htlc is cross signed in the current commitment, we can settle it preimage_opt match { case Some(preimage) => - log.info(s"fulfilling broken htlc=$htlc") Metrics.Resolved.withTag(Tags.Success, value = true).withTag(Metrics.Relayed, value = false).increment() if (e.currentState != CLOSED) { + log.info(s"fulfilling broken htlc=$htlc") channel ! CMD_FULFILL_HTLC(htlc.id, preimage, commit = true) + } else { + log.info(s"got preimage but upstream channel is closed for htlc=$htlc") } case None => - log.info(s"failing not relayed htlc=$htlc") Metrics.Resolved.withTag(Tags.Success, value = false).withTag(Metrics.Relayed, value = false).increment() if (e.currentState != CLOSING && e.currentState != CLOSED) { + log.info(s"failing not relayed htlc=$htlc") channel ! CMD_FAIL_HTLC(htlc.id, Right(TemporaryNodeFailure), commit = true) + } else { + log.info(s"would fail but upstream channel is closed for htlc=$htlc") } } false // the channel may very well be disconnected before we sign (=ack) the fail/fulfill, so we keep it for now @@ -163,7 +167,7 @@ class PostRestartHtlcCleaner(nodeParams: NodeParams, register: ActorRef, initial // dummy values in the DB (to make sure we store the preimage) but we don't emit an event. val dummyFinalAmount = fulfilledHtlc.amountMsat val dummyNodeId = nodeParams.nodeId - nodeParams.db.payments.addOutgoingPayment(OutgoingPayment(id, id, None, fulfilledHtlc.paymentHash, PaymentType.Standard, fulfilledHtlc.amountMsat, dummyFinalAmount, dummyNodeId, System.currentTimeMillis, None, OutgoingPaymentStatus.Pending)) + nodeParams.db.payments.addOutgoingPayment(OutgoingPayment(id, id, None, fulfilledHtlc.paymentHash, PaymentType.Standard, fulfilledHtlc.amountMsat, dummyFinalAmount, dummyNodeId, TimestampMilli.now(), None, OutgoingPaymentStatus.Pending)) nodeParams.db.payments.updateOutgoingPayment(PaymentSent(id, fulfilledHtlc.paymentHash, paymentPreimage, dummyFinalAmount, dummyNodeId, PaymentSent.PartialPayment(id, fulfilledHtlc.amountMsat, feesPaid, fulfilledHtlc.channelId, None) :: Nil)) } // There can never be more than one pending downstream HTLC for a given local origin (a multi-part payment is @@ -372,10 +376,10 @@ object PostRestartHtlcCleaner { val timedOutHtlcs: Set[Long] = (closingType_opt match { case Some(c: Closing.LocalClose) => val confirmedTxs = c.localCommitPublished.commitTx +: irrevocablySpent.filter(tx => Closing.isHtlcTimeout(tx, c.localCommitPublished)) - confirmedTxs.flatMap(tx => Closing.timedOutHtlcs(d.commitments.commitmentFormat, c.localCommit, c.localCommitPublished, d.commitments.localParams.dustLimit, tx)) + confirmedTxs.flatMap(tx => Closing.trimmedOrTimedOutHtlcs(d.commitments.commitmentFormat, c.localCommit, c.localCommitPublished, d.commitments.localParams.dustLimit, tx)) case Some(c: Closing.RemoteClose) => val confirmedTxs = c.remoteCommitPublished.commitTx +: irrevocablySpent.filter(tx => Closing.isClaimHtlcTimeout(tx, c.remoteCommitPublished)) - confirmedTxs.flatMap(tx => Closing.timedOutHtlcs(d.commitments.commitmentFormat, c.remoteCommit, c.remoteCommitPublished, d.commitments.remoteParams.dustLimit, tx)) + confirmedTxs.flatMap(tx => Closing.trimmedOrTimedOutHtlcs(d.commitments.commitmentFormat, c.remoteCommit, c.remoteCommitPublished, d.commitments.remoteParams.dustLimit, tx)) case _ => Seq.empty[UpdateAddHtlc] }).map(_.id).toSet overriddenHtlcs ++ timedOutHtlcs diff --git a/eclair-core/src/main/scala/fr/acinq/eclair/payment/send/Autoprobe.scala b/eclair-core/src/main/scala/fr/acinq/eclair/payment/send/Autoprobe.scala index 0ea44f14c3..ebcdae283f 100644 --- a/eclair-core/src/main/scala/fr/acinq/eclair/payment/send/Autoprobe.scala +++ b/eclair-core/src/main/scala/fr/acinq/eclair/payment/send/Autoprobe.scala @@ -22,7 +22,7 @@ import fr.acinq.eclair.crypto.Sphinx.DecryptedFailurePacket import fr.acinq.eclair.payment.{PaymentEvent, PaymentFailed, PaymentRequest, RemoteFailure} import fr.acinq.eclair.router.Router import fr.acinq.eclair.wire.protocol.IncorrectOrUnknownPaymentDetails -import fr.acinq.eclair.{MilliSatoshiLong, NodeParams, randomBytes32, randomLong} +import fr.acinq.eclair.{MilliSatoshiLong, NodeParams, TimestampSecond, randomBytes32, randomLong} import scodec.bits.ByteVector import scala.concurrent.duration._ @@ -56,7 +56,7 @@ class Autoprobe(nodeParams: NodeParams, router: ActorRef, paymentInitiator: Acto val fakeInvoice = PaymentRequest( PaymentRequest.prefixes(nodeParams.chainHash), Some(PAYMENT_AMOUNT_MSAT), - System.currentTimeMillis(), + TimestampSecond.now(), targetNodeId, List( PaymentRequest.PaymentHash(randomBytes32()), // we don't even know the preimage (this needs to be a secure random!) diff --git a/eclair-core/src/main/scala/fr/acinq/eclair/payment/send/MultiPartPaymentLifecycle.scala b/eclair-core/src/main/scala/fr/acinq/eclair/payment/send/MultiPartPaymentLifecycle.scala index 76bf555e1c..e8015f2442 100644 --- a/eclair-core/src/main/scala/fr/acinq/eclair/payment/send/MultiPartPaymentLifecycle.scala +++ b/eclair-core/src/main/scala/fr/acinq/eclair/payment/send/MultiPartPaymentLifecycle.scala @@ -31,7 +31,7 @@ import fr.acinq.eclair.payment.send.PaymentInitiator.SendPaymentConfig import fr.acinq.eclair.payment.send.PaymentLifecycle.SendPaymentToRoute import fr.acinq.eclair.router.Router._ import fr.acinq.eclair.wire.protocol._ -import fr.acinq.eclair.{CltvExpiry, FSMDiagnosticActorLogging, Logs, MilliSatoshi, MilliSatoshiLong, NodeParams} +import fr.acinq.eclair.{CltvExpiry, FSMDiagnosticActorLogging, Logs, MilliSatoshi, MilliSatoshiLong, NodeParams, TimestampMilli} import java.util.UUID import java.util.concurrent.TimeUnit @@ -52,7 +52,7 @@ class MultiPartPaymentLifecycle(nodeParams: NodeParams, cfg: SendPaymentConfig, val id = cfg.id val paymentHash = cfg.paymentHash - val start = System.currentTimeMillis + val start = TimestampMilli.now() private var retriedFailedChannels = false startWith(WAIT_FOR_PAYMENT_REQUEST, WaitingForRequest) @@ -104,7 +104,7 @@ class MultiPartPaymentLifecycle(nodeParams: NodeParams, cfg: SendPaymentConfig, if (cfg.storeInDb && d.pending.isEmpty && d.failures.isEmpty) { // In cases where we fail early (router error during the first attempt), the DB won't have an entry for that // payment, which may be confusing for users. - val dummyPayment = OutgoingPayment(id, cfg.parentId, cfg.externalId, paymentHash, PaymentType.Standard, cfg.recipientAmount, cfg.recipientAmount, cfg.recipientNodeId, System.currentTimeMillis, cfg.paymentRequest, OutgoingPaymentStatus.Pending) + val dummyPayment = OutgoingPayment(id, cfg.parentId, cfg.externalId, paymentHash, PaymentType.Standard, cfg.recipientAmount, cfg.recipientAmount, cfg.recipientNodeId, TimestampMilli.now(), cfg.paymentRequest, OutgoingPaymentStatus.Pending) nodeParams.db.payments.addOutgoingPayment(dummyPayment) nodeParams.db.payments.updateOutgoingPayment(PaymentFailed(id, paymentHash, failure :: Nil)) } @@ -244,7 +244,7 @@ class MultiPartPaymentLifecycle(nodeParams: NodeParams, cfg: SendPaymentConfig, "FAILURE" } } - val now = System.currentTimeMillis + val now = TimestampMilli.now() val duration = now - start if (cfg.recordPathFindingMetrics) { val fees = event match { @@ -268,7 +268,7 @@ class MultiPartPaymentLifecycle(nodeParams: NodeParams, cfg: SendPaymentConfig, Metrics.SentPaymentDuration .withTag(Tags.MultiPart, Tags.MultiPartType.Parent) .withTag(Tags.Success, value = status == "SUCCESS") - .record(duration, TimeUnit.MILLISECONDS) + .record(duration.toMillis, TimeUnit.MILLISECONDS) if (retriedFailedChannels) { Metrics.RetryFailedChannelsResult.withTag(Tags.Success, event.isRight).increment() } diff --git a/eclair-core/src/main/scala/fr/acinq/eclair/payment/send/PaymentLifecycle.scala b/eclair-core/src/main/scala/fr/acinq/eclair/payment/send/PaymentLifecycle.scala index f25f201ded..29da2b82b3 100644 --- a/eclair-core/src/main/scala/fr/acinq/eclair/payment/send/PaymentLifecycle.scala +++ b/eclair-core/src/main/scala/fr/acinq/eclair/payment/send/PaymentLifecycle.scala @@ -48,7 +48,7 @@ class PaymentLifecycle(nodeParams: NodeParams, cfg: SendPaymentConfig, router: A private val id = cfg.id private val paymentHash = cfg.paymentHash private val paymentsDb = nodeParams.db.payments - private val start = System.currentTimeMillis + private val start = TimestampMilli.now() startWith(WAITING_FOR_REQUEST, WaitingForRequest) @@ -60,7 +60,7 @@ class PaymentLifecycle(nodeParams: NodeParams, cfg: SendPaymentConfig, router: A route => self ! RouteResponse(route :: Nil) ) if (cfg.storeInDb) { - paymentsDb.addOutgoingPayment(OutgoingPayment(id, cfg.parentId, cfg.externalId, paymentHash, PaymentType.Standard, c.finalPayload.amount, cfg.recipientAmount, cfg.recipientNodeId, System.currentTimeMillis, cfg.paymentRequest, OutgoingPaymentStatus.Pending)) + paymentsDb.addOutgoingPayment(OutgoingPayment(id, cfg.parentId, cfg.externalId, paymentHash, PaymentType.Standard, c.finalPayload.amount, cfg.recipientAmount, cfg.recipientNodeId, TimestampMilli.now(), cfg.paymentRequest, OutgoingPaymentStatus.Pending)) } goto(WAITING_FOR_ROUTE) using WaitingForRoute(c, Nil, Ignore.empty) @@ -68,7 +68,7 @@ class PaymentLifecycle(nodeParams: NodeParams, cfg: SendPaymentConfig, router: A log.debug("sending {} to {}", c.finalPayload.amount, c.targetNodeId) router ! RouteRequest(nodeParams.nodeId, c.targetNodeId, c.finalPayload.amount, c.maxFee, c.assistedRoutes, routeParams = c.routeParams, paymentContext = Some(cfg.paymentContext)) if (cfg.storeInDb) { - paymentsDb.addOutgoingPayment(OutgoingPayment(id, cfg.parentId, cfg.externalId, paymentHash, PaymentType.Standard, c.finalPayload.amount, cfg.recipientAmount, cfg.recipientNodeId, System.currentTimeMillis, cfg.paymentRequest, OutgoingPaymentStatus.Pending)) + paymentsDb.addOutgoingPayment(OutgoingPayment(id, cfg.parentId, cfg.externalId, paymentHash, PaymentType.Standard, c.finalPayload.amount, cfg.recipientAmount, cfg.recipientNodeId, TimestampMilli.now(), cfg.paymentRequest, OutgoingPaymentStatus.Pending)) } goto(WAITING_FOR_ROUTE) using WaitingForRoute(c, Nil, Ignore.empty) } @@ -302,7 +302,7 @@ class PaymentLifecycle(nodeParams: NodeParams, cfg: SendPaymentConfig, router: A "FAILURE" } } - val now = System.currentTimeMillis + val now = TimestampMilli.now() val duration = now - start if (cfg.recordPathFindingMetrics) { val fees = result match { @@ -334,7 +334,7 @@ class PaymentLifecycle(nodeParams: NodeParams, cfg: SendPaymentConfig, router: A Metrics.SentPaymentDuration .withTag(Tags.MultiPart, if (cfg.id != cfg.parentId) Tags.MultiPartType.Child else Tags.MultiPartType.Disabled) .withTag(Tags.Success, value = status == "SUCCESS") - .record(duration, TimeUnit.MILLISECONDS) + .record(duration.toMillis, TimeUnit.MILLISECONDS) stop(FSM.Normal) } diff --git a/eclair-core/src/main/scala/fr/acinq/eclair/router/Announcements.scala b/eclair-core/src/main/scala/fr/acinq/eclair/router/Announcements.scala index 0fc4df8a5a..2241756093 100644 --- a/eclair-core/src/main/scala/fr/acinq/eclair/router/Announcements.scala +++ b/eclair-core/src/main/scala/fr/acinq/eclair/router/Announcements.scala @@ -18,14 +18,11 @@ package fr.acinq.eclair.router import fr.acinq.bitcoin.Crypto.{PrivateKey, PublicKey, sha256, verifySignature} import fr.acinq.bitcoin.{ByteVector32, ByteVector64, Crypto, LexicographicalOrdering} -import fr.acinq.eclair.router.Announcements.isNode1 import fr.acinq.eclair.wire.protocol._ -import fr.acinq.eclair.{CltvExpiryDelta, Features, MilliSatoshi, ShortChannelId, serializationResult} -import scodec.bits.{BitVector, ByteVector} +import fr.acinq.eclair.{CltvExpiryDelta, Features, MilliSatoshi, ShortChannelId, TimestampSecond, TimestampSecondLong, serializationResult} +import scodec.bits.ByteVector import shapeless.HNil -import scala.concurrent.duration._ - /** * Created by PM on 03/02/2017. */ @@ -34,10 +31,10 @@ object Announcements { def channelAnnouncementWitnessEncode(chainHash: ByteVector32, shortChannelId: ShortChannelId, nodeId1: PublicKey, nodeId2: PublicKey, bitcoinKey1: PublicKey, bitcoinKey2: PublicKey, features: Features, tlvStream: TlvStream[ChannelAnnouncementTlv]): ByteVector = sha256(sha256(serializationResult(LightningMessageCodecs.channelAnnouncementWitnessCodec.encode(features :: chainHash :: shortChannelId :: nodeId1 :: nodeId2 :: bitcoinKey1 :: bitcoinKey2 :: tlvStream :: HNil)))) - def nodeAnnouncementWitnessEncode(timestamp: Long, nodeId: PublicKey, rgbColor: Color, alias: String, features: Features, addresses: List[NodeAddress], tlvStream: TlvStream[NodeAnnouncementTlv]): ByteVector = + def nodeAnnouncementWitnessEncode(timestamp: TimestampSecond, nodeId: PublicKey, rgbColor: Color, alias: String, features: Features, addresses: List[NodeAddress], tlvStream: TlvStream[NodeAnnouncementTlv]): ByteVector = sha256(sha256(serializationResult(LightningMessageCodecs.nodeAnnouncementWitnessCodec.encode(features :: timestamp :: nodeId :: rgbColor :: alias :: addresses :: tlvStream :: HNil)))) - def channelUpdateWitnessEncode(chainHash: ByteVector32, shortChannelId: ShortChannelId, timestamp: Long, channelFlags: ChannelUpdate.ChannelFlags, cltvExpiryDelta: CltvExpiryDelta, htlcMinimumMsat: MilliSatoshi, feeBaseMsat: MilliSatoshi, feeProportionalMillionths: Long, htlcMaximumMsat: Option[MilliSatoshi], tlvStream: TlvStream[ChannelUpdateTlv]): ByteVector = + def channelUpdateWitnessEncode(chainHash: ByteVector32, shortChannelId: ShortChannelId, timestamp: TimestampSecond, channelFlags: ChannelUpdate.ChannelFlags, cltvExpiryDelta: CltvExpiryDelta, htlcMinimumMsat: MilliSatoshi, feeBaseMsat: MilliSatoshi, feeProportionalMillionths: Long, htlcMaximumMsat: Option[MilliSatoshi], tlvStream: TlvStream[ChannelUpdateTlv]): ByteVector = sha256(sha256(serializationResult(LightningMessageCodecs.channelUpdateWitnessCodec.encode(chainHash :: shortChannelId :: timestamp :: channelFlags :: cltvExpiryDelta :: htlcMinimumMsat :: feeBaseMsat :: feeProportionalMillionths :: htlcMaximumMsat :: tlvStream :: HNil)))) def generateChannelAnnouncementWitness(chainHash: ByteVector32, shortChannelId: ShortChannelId, localNodeId: PublicKey, remoteNodeId: PublicKey, localFundingKey: PublicKey, remoteFundingKey: PublicKey, features: Features): ByteVector = @@ -71,7 +68,7 @@ object Announcements { ) } - def makeNodeAnnouncement(nodeSecret: PrivateKey, alias: String, color: Color, nodeAddresses: List[NodeAddress], features: Features, timestamp: Long = System.currentTimeMillis.milliseconds.toSeconds): NodeAnnouncement = { + def makeNodeAnnouncement(nodeSecret: PrivateKey, alias: String, color: Color, nodeAddresses: List[NodeAddress], features: Features, timestamp: TimestampSecond = TimestampSecond.now()): NodeAnnouncement = { require(alias.length <= 32) val sortedAddresses = nodeAddresses.map { case address@(_: IPv4) => (1, address) @@ -108,7 +105,7 @@ object Announcements { * @return true if channel updates are "equal" */ def areSame(u1: ChannelUpdate, u2: ChannelUpdate): Boolean = - u1.copy(signature = ByteVector64.Zeroes, timestamp = 0) == u2.copy(signature = ByteVector64.Zeroes, timestamp = 0) + u1.copy(signature = ByteVector64.Zeroes, timestamp = 0 unixsec) == u2.copy(signature = ByteVector64.Zeroes, timestamp = 0 unixsec) def areSameIgnoreFlags(u1: ChannelUpdate, u2: ChannelUpdate): Boolean = u1.feeBaseMsat == u2.feeBaseMsat && @@ -117,7 +114,7 @@ object Announcements { u1.htlcMinimumMsat == u2.htlcMinimumMsat && u1.htlcMaximumMsat == u2.htlcMaximumMsat - def makeChannelUpdate(chainHash: ByteVector32, nodeSecret: PrivateKey, remoteNodeId: PublicKey, shortChannelId: ShortChannelId, cltvExpiryDelta: CltvExpiryDelta, htlcMinimumMsat: MilliSatoshi, feeBaseMsat: MilliSatoshi, feeProportionalMillionths: Long, htlcMaximumMsat: MilliSatoshi, enable: Boolean = true, timestamp: Long = System.currentTimeMillis.milliseconds.toSeconds): ChannelUpdate = { + def makeChannelUpdate(chainHash: ByteVector32, nodeSecret: PrivateKey, remoteNodeId: PublicKey, shortChannelId: ShortChannelId, cltvExpiryDelta: CltvExpiryDelta, htlcMinimumMsat: MilliSatoshi, feeBaseMsat: MilliSatoshi, feeProportionalMillionths: Long, htlcMaximumMsat: MilliSatoshi, enable: Boolean = true, timestamp: TimestampSecond = TimestampSecond.now()): ChannelUpdate = { val channelFlags = ChannelUpdate.ChannelFlags(isNode1 = isNode1(nodeSecret.publicKey, remoteNodeId), isEnabled = enable) val htlcMaximumMsatOpt = Some(htlcMaximumMsat) diff --git a/eclair-core/src/main/scala/fr/acinq/eclair/router/Monitoring.scala b/eclair-core/src/main/scala/fr/acinq/eclair/router/Monitoring.scala index f3b7c97c38..e4c2d2f740 100644 --- a/eclair-core/src/main/scala/fr/acinq/eclair/router/Monitoring.scala +++ b/eclair-core/src/main/scala/fr/acinq/eclair/router/Monitoring.scala @@ -56,8 +56,8 @@ object Monitoring { private val ChannelUpdateRefreshRate = Kamon.histogram("router.gossip.channel-update-refresh-rate", "Rate at which channels update their fee policy (minutes)") def channelUpdateRefreshed(update: ChannelUpdate, previous: ChannelUpdate, public: Boolean): Unit = { - val elapsed = (update.timestamp - previous.timestamp) / 60 - ChannelUpdateRefreshRate.withTag(Tags.Announced, public).record(elapsed) + val elapsed = update.timestamp - previous.timestamp + ChannelUpdateRefreshRate.withTag(Tags.Announced, public).record(elapsed.toMinutes) } private val GossipResult = Kamon.counter("router.gossip.result") diff --git a/eclair-core/src/main/scala/fr/acinq/eclair/router/RouteCalculation.scala b/eclair-core/src/main/scala/fr/acinq/eclair/router/RouteCalculation.scala index 448ae4d6b5..ed80b789a9 100644 --- a/eclair-core/src/main/scala/fr/acinq/eclair/router/RouteCalculation.scala +++ b/eclair-core/src/main/scala/fr/acinq/eclair/router/RouteCalculation.scala @@ -136,7 +136,7 @@ object RouteCalculation { private def toFakeUpdate(extraHop: ExtraHop, htlcMaximum: MilliSatoshi): ChannelUpdate = { // the `direction` bit in flags will not be accurate but it doesn't matter because it is not used // what matters is that the `disable` bit is 0 so that this update doesn't get filtered out - ChannelUpdate(signature = ByteVector64.Zeroes, chainHash = ByteVector32.Zeroes, extraHop.shortChannelId, System.currentTimeMillis.milliseconds.toSeconds, channelFlags = ChannelUpdate.ChannelFlags(isNode1 = true, isEnabled = true), extraHop.cltvExpiryDelta, htlcMinimumMsat = 0 msat, extraHop.feeBase, extraHop.feeProportionalMillionths, Some(htlcMaximum)) + ChannelUpdate(signature = ByteVector64.Zeroes, chainHash = ByteVector32.Zeroes, extraHop.shortChannelId, TimestampSecond.now(), channelFlags = ChannelUpdate.ChannelFlags(isNode1 = true, isEnabled = true), extraHop.cltvExpiryDelta, htlcMinimumMsat = 0 msat, extraHop.feeBase, extraHop.feeProportionalMillionths, Some(htlcMaximum)) } def toAssistedChannels(extraRoute: Seq[ExtraHop], targetNodeId: PublicKey, amount: MilliSatoshi): Map[ShortChannelId, AssistedChannel] = { diff --git a/eclair-core/src/main/scala/fr/acinq/eclair/router/StaleChannels.scala b/eclair-core/src/main/scala/fr/acinq/eclair/router/StaleChannels.scala index 99d0cc3f15..d6115f4ffa 100644 --- a/eclair-core/src/main/scala/fr/acinq/eclair/router/StaleChannels.scala +++ b/eclair-core/src/main/scala/fr/acinq/eclair/router/StaleChannels.scala @@ -21,7 +21,7 @@ import akka.event.LoggingAdapter import fr.acinq.eclair.db.NetworkDb import fr.acinq.eclair.router.Router.{ChannelDesc, Data, PublicChannel, hasChannels} import fr.acinq.eclair.wire.protocol.{ChannelAnnouncement, ChannelUpdate} -import fr.acinq.eclair.{ShortChannelId, TxCoordinates} +import fr.acinq.eclair.{ShortChannelId, TimestampSecond, TxCoordinates} import scala.collection.mutable import scala.concurrent.duration._ @@ -65,17 +65,17 @@ object StaleChannels { def isStale(u: ChannelUpdate): Boolean = isStale(u.timestamp) - def isStale(timestamp: Long): Boolean = { + def isStale(timestamp: TimestampSecond): Boolean = { // BOLT 7: "nodes MAY prune channels should the timestamp of the latest channel_update be older than 2 weeks" // but we don't want to prune brand new channels for which we didn't yet receive a channel update - val staleThresholdSeconds = (System.currentTimeMillis.milliseconds - 14.days).toSeconds - timestamp < staleThresholdSeconds + val staleThreshold = TimestampSecond.now() - 14.days + timestamp < staleThreshold } - def isAlmostStale(timestamp: Long): Boolean = { + def isAlmostStale(timestamp: TimestampSecond): Boolean = { // we define almost stale as 2 weeks minus 4 days - val staleThresholdSeconds = (System.currentTimeMillis.milliseconds - 10.days).toSeconds - timestamp < staleThresholdSeconds + val almostStaleThreshold = TimestampSecond.now() - 10.days + timestamp < almostStaleThreshold } /** diff --git a/eclair-core/src/main/scala/fr/acinq/eclair/router/Sync.scala b/eclair-core/src/main/scala/fr/acinq/eclair/router/Sync.scala index 98b60bbde1..15a0f25b42 100644 --- a/eclair-core/src/main/scala/fr/acinq/eclair/router/Sync.scala +++ b/eclair-core/src/main/scala/fr/acinq/eclair/router/Sync.scala @@ -24,14 +24,13 @@ import fr.acinq.eclair.crypto.TransportHandler import fr.acinq.eclair.router.Monitoring.{Metrics, Tags} import fr.acinq.eclair.router.Router._ import fr.acinq.eclair.wire.protocol._ -import fr.acinq.eclair.{ShortChannelId, serializationResult} +import fr.acinq.eclair.{ShortChannelId, TimestampSecond, TimestampSecondLong, serializationResult} import scodec.bits.ByteVector import shapeless.HNil import scala.annotation.tailrec import scala.collection.SortedSet import scala.collection.immutable.SortedMap -import scala.concurrent.duration._ import scala.util.Random object Sync { @@ -59,7 +58,7 @@ object Sync { // the first_timestamp field to the current date/time and timestamp_range to the maximum value // NB: we can't just set firstTimestamp to 0, because in that case peer would send us all past messages matching // that (i.e. the whole routing table) - val filter = GossipTimestampFilter(s.chainHash, firstTimestamp = System.currentTimeMillis.milliseconds.toSeconds, timestampRange = Int.MaxValue) + val filter = GossipTimestampFilter(s.chainHash, firstTimestamp = TimestampSecond.now(), timestampRange = Int.MaxValue) s.to ! filter // reset our sync state for this peer: we create an entry to ensure we reject duplicate queries and unsolicited reply_channel_range @@ -224,7 +223,7 @@ object Sync { height >= firstBlockNum && height < (firstBlockNum + numberOfBlocks) } - def shouldRequestUpdate(ourTimestamp: Long, ourChecksum: Long, theirTimestamp_opt: Option[Long], theirChecksum_opt: Option[Long]): Boolean = { + def shouldRequestUpdate(ourTimestamp: TimestampSecond, ourChecksum: Long, theirTimestamp_opt: Option[TimestampSecond], theirChecksum_opt: Option[Long]): Boolean = { (theirTimestamp_opt, theirChecksum_opt) match { case (Some(theirTimestamp), Some(theirChecksum)) => // we request their channel_update if all those conditions are met: @@ -372,8 +371,8 @@ object Sync { def getChannelDigestInfo(channels: SortedMap[ShortChannelId, PublicChannel])(shortChannelId: ShortChannelId): (ReplyChannelRangeTlv.Timestamps, ReplyChannelRangeTlv.Checksums) = { val c = channels(shortChannelId) - val timestamp1 = c.update_1_opt.map(_.timestamp).getOrElse(0L) - val timestamp2 = c.update_2_opt.map(_.timestamp).getOrElse(0L) + val timestamp1 = c.update_1_opt.map(_.timestamp).getOrElse(0L unixsec) + val timestamp2 = c.update_2_opt.map(_.timestamp).getOrElse(0L unixsec) val checksum1 = c.update_1_opt.map(getChecksum).getOrElse(0L) val checksum2 = c.update_2_opt.map(getChecksum).getOrElse(0L) (ReplyChannelRangeTlv.Timestamps(timestamp1 = timestamp1, timestamp2 = timestamp2), ReplyChannelRangeTlv.Checksums(checksum1 = checksum1, checksum2 = checksum2)) diff --git a/eclair-core/src/main/scala/fr/acinq/eclair/router/Validation.scala b/eclair-core/src/main/scala/fr/acinq/eclair/router/Validation.scala index d64e2cba4b..a78c9e9254 100644 --- a/eclair-core/src/main/scala/fr/acinq/eclair/router/Validation.scala +++ b/eclair-core/src/main/scala/fr/acinq/eclair/router/Validation.scala @@ -86,7 +86,7 @@ object Validation { } val remoteOrigins_opt = d0.awaiting.get(c) Logs.withMdc(log)(Logs.mdc(remoteNodeId_opt = remoteOrigins_opt.flatMap(_.headOption).map(_.nodeId))) { // in the MDC we use the node id that sent us the announcement first - log.info("got validation result for shortChannelId={} (awaiting={} stash.nodes={} stash.updates={})", c.shortChannelId, d0.awaiting.size, d0.stash.nodes.size, d0.stash.updates.size) + log.debug("got validation result for shortChannelId={} (awaiting={} stash.nodes={} stash.updates={})", c.shortChannelId, d0.awaiting.size, d0.stash.nodes.size, d0.stash.updates.size) val publicChannel_opt = r match { case ValidateResult(c, Left(t)) => log.warning("validation failure for shortChannelId={} reason={}", c.shortChannelId, t.getMessage) @@ -123,7 +123,7 @@ object Validation { } case ValidateResult(c, Right((tx, fundingTxStatus: UtxoStatus.Spent))) => if (fundingTxStatus.spendingTxConfirmed) { - log.warning("ignoring shortChannelId={} tx={} (funding tx already spent and spending tx is confirmed)", c.shortChannelId, tx.txid) + log.debug("ignoring shortChannelId={} tx={} (funding tx already spent and spending tx is confirmed)", c.shortChannelId, tx.txid) // the funding tx has been spent by a transaction that is now confirmed: peer shouldn't send us those remoteOrigins_opt.foreach(_.foreach(o => sendDecision(o.peerConnection, GossipDecision.ChannelClosed(c)))) } else { diff --git a/eclair-core/src/main/scala/fr/acinq/eclair/wire/internal/channel/version0/ChannelCodecs0.scala b/eclair-core/src/main/scala/fr/acinq/eclair/wire/internal/channel/version0/ChannelCodecs0.scala index c03e14fa3b..8d6a943ba9 100644 --- a/eclair-core/src/main/scala/fr/acinq/eclair/wire/internal/channel/version0/ChannelCodecs0.scala +++ b/eclair-core/src/main/scala/fr/acinq/eclair/wire/internal/channel/version0/ChannelCodecs0.scala @@ -18,6 +18,7 @@ package fr.acinq.eclair.wire.internal.channel.version0 import fr.acinq.bitcoin.DeterministicWallet.{ExtendedPrivateKey, KeyPath} import fr.acinq.bitcoin.{ByteVector32, ByteVector64, Crypto, OutPoint, Transaction, TxOut} +import fr.acinq.eclair.TimestampSecond import fr.acinq.eclair.channel._ import fr.acinq.eclair.crypto.ShaChain import fr.acinq.eclair.transactions.Transactions._ @@ -352,7 +353,7 @@ private[channel] object ChannelCodecs0 { val DATA_WAIT_FOR_FUNDING_CONFIRMED_COMPAT_01_Codec: Codec[DATA_WAIT_FOR_FUNDING_CONFIRMED] = ( ("commitments" | commitmentsCodec) :: ("fundingTx" | provide[Option[Transaction]](None)) :: - ("waitingSince" | provide(System.currentTimeMillis.milliseconds.toSeconds)) :: + ("waitingSince" | provide(TimestampSecond.now().toLong)) :: ("deferred" | optional(bool, fundingLockedCodec)) :: ("lastSent" | either(bool, fundingCreatedCodec, fundingSignedCodec))).as[DATA_WAIT_FOR_FUNDING_CONFIRMED].decodeOnly @@ -411,7 +412,7 @@ private[channel] object ChannelCodecs0 { val DATA_CLOSING_COMPAT_06_Codec: Codec[DATA_CLOSING] = ( ("commitments" | commitmentsCodec) :: ("fundingTx" | provide[Option[Transaction]](None)) :: - ("waitingSince" | provide(System.currentTimeMillis.milliseconds.toSeconds)) :: + ("waitingSince" | provide(TimestampSecond.now().toLong)) :: ("mutualCloseProposed" | listOfN(uint16, closingTxCodec)) :: ("mutualClosePublished" | listOfN(uint16, closingTxCodec)) :: ("localCommitPublished" | optional(bool, localCommitPublishedCodec)) :: diff --git a/eclair-core/src/main/scala/fr/acinq/eclair/wire/protocol/CommonCodecs.scala b/eclair-core/src/main/scala/fr/acinq/eclair/wire/protocol/CommonCodecs.scala index e3a070a369..973367bb92 100644 --- a/eclair-core/src/main/scala/fr/acinq/eclair/wire/protocol/CommonCodecs.scala +++ b/eclair-core/src/main/scala/fr/acinq/eclair/wire/protocol/CommonCodecs.scala @@ -20,7 +20,7 @@ import fr.acinq.bitcoin.Crypto.{PrivateKey, PublicKey} import fr.acinq.bitcoin.{ByteVector32, ByteVector64, Satoshi} import fr.acinq.eclair.blockchain.fee.FeeratePerKw import fr.acinq.eclair.crypto.Mac32 -import fr.acinq.eclair.{CltvExpiry, CltvExpiryDelta, MilliSatoshi, ShortChannelId, UInt64} +import fr.acinq.eclair.{CltvExpiry, CltvExpiryDelta, MilliSatoshi, ShortChannelId, TimestampSecond, UInt64} import org.apache.commons.codec.binary.Base32 import scodec.bits.{BitVector, ByteVector} import scodec.codecs._ @@ -69,6 +69,8 @@ object CommonCodecs { // this codec will fail if the amount does not fit on 32 bits val millisatoshi32: Codec[MilliSatoshi] = uint32.xmapc(l => MilliSatoshi(l))(_.toLong) + val timestampSecond: Codec[TimestampSecond] = uint32.xmapc(TimestampSecond(_))(_.toLong) + /** * We impose a minimal encoding on some values (such as varint and truncated int) to ensure that signed hashes can be * re-computed correctly. diff --git a/eclair-core/src/main/scala/fr/acinq/eclair/wire/protocol/EncryptedRecipientDataTlv.scala b/eclair-core/src/main/scala/fr/acinq/eclair/wire/protocol/EncryptedRecipientDataTlv.scala new file mode 100644 index 0000000000..f55c165f74 --- /dev/null +++ b/eclair-core/src/main/scala/fr/acinq/eclair/wire/protocol/EncryptedRecipientDataTlv.scala @@ -0,0 +1,82 @@ +/* + * Copyright 2021 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.wire.protocol + +import fr.acinq.bitcoin.Crypto.{PrivateKey, PublicKey} +import fr.acinq.eclair.crypto.Sphinx.RouteBlinding +import fr.acinq.eclair.{ShortChannelId, UInt64} +import scodec.bits.ByteVector + +import scala.util.Try + +sealed trait EncryptedRecipientDataTlv extends Tlv + +object EncryptedRecipientDataTlv { + + /** Some padding can be added to ensure all payloads are the same size to improve privacy. */ + case class Padding(dummy: ByteVector) extends EncryptedRecipientDataTlv + + /** Id of the outgoing channel, used to identify the next node. */ + case class OutgoingChannelId(shortChannelId: ShortChannelId) extends EncryptedRecipientDataTlv + + /** Id of the next node. */ + case class OutgoingNodeId(nodeId: PublicKey) extends EncryptedRecipientDataTlv + + /** + * The final recipient may store some data in the encrypted payload for itself to avoid storing it locally. + * It can for example put a payment_hash to verify that the route is used for the correct invoice. + */ + case class RecipientSecret(data: ByteVector) extends EncryptedRecipientDataTlv + +} + +object EncryptedRecipientDataCodecs { + + import EncryptedRecipientDataTlv._ + import fr.acinq.eclair.wire.protocol.CommonCodecs.{publicKey, shortchannelid, varint, varintoverflow} + import scodec.Codec + import scodec.codecs._ + + private val padding: Codec[Padding] = variableSizeBytesLong(varintoverflow, "padding" | bytes).as[Padding] + private val outgoingChannelId: Codec[OutgoingChannelId] = variableSizeBytesLong(varintoverflow, "short_channel_id" | shortchannelid).as[OutgoingChannelId] + private val outgoingNodeId: Codec[OutgoingNodeId] = variableSizeBytesLong(varintoverflow, "node_id" | publicKey).as[OutgoingNodeId] + private val recipientSecret: Codec[RecipientSecret] = variableSizeBytesLong(varintoverflow, "recipient_secret" | bytes).as[RecipientSecret] + + private val encryptedRecipientDataTlvCodec = discriminated[EncryptedRecipientDataTlv].by(varint) + .typecase(UInt64(1), padding) + .typecase(UInt64(2), outgoingChannelId) + .typecase(UInt64(4), outgoingNodeId) + .typecase(UInt64(6), recipientSecret) + + val encryptedRecipientDataCodec: Codec[TlvStream[EncryptedRecipientDataTlv]] = TlvCodecs.lengthPrefixedTlvStream[EncryptedRecipientDataTlv](encryptedRecipientDataTlvCodec).complete + + /** + * Decrypt and decode the contents of an encrypted_recipient_data TLV field. + * + * @param nodePrivKey this node's private key. + * @param blindingKey blinding point (usually provided in the lightning message). + * @param encryptedRecipientData encrypted recipient data (usually provided inside an onion). + * @return decrypted contents of the encrypted recipient data, which usually contain information about the next node, + * and the blinding point that should be sent to the next node. + */ + def decode(nodePrivKey: PrivateKey, blindingKey: PublicKey, encryptedRecipientData: ByteVector): Try[(TlvStream[EncryptedRecipientDataTlv], PublicKey)] = { + RouteBlinding.decryptPayload(nodePrivKey, blindingKey, encryptedRecipientData).flatMap { + case (payload, nextBlindingKey) => encryptedRecipientDataCodec.decode(payload.bits).map(r => (r.value, nextBlindingKey)).toTry + } + } + +} diff --git a/eclair-core/src/main/scala/fr/acinq/eclair/wire/protocol/LightningMessageCodecs.scala b/eclair-core/src/main/scala/fr/acinq/eclair/wire/protocol/LightningMessageCodecs.scala index 4c74cf1b63..36b9287b99 100644 --- a/eclair-core/src/main/scala/fr/acinq/eclair/wire/protocol/LightningMessageCodecs.scala +++ b/eclair-core/src/main/scala/fr/acinq/eclair/wire/protocol/LightningMessageCodecs.scala @@ -210,7 +210,7 @@ object LightningMessageCodecs { val nodeAnnouncementWitnessCodec = ("features" | featuresCodec) :: - ("timestamp" | uint32) :: + ("timestamp" | timestampSecond) :: ("nodeId" | publicKey) :: ("rgbColor" | rgb) :: ("alias" | zeropaddedstring(32)) :: @@ -250,7 +250,7 @@ object LightningMessageCodecs { val channelUpdateWitnessCodec = (("chainHash" | bytes32) :: ("shortChannelId" | shortchannelid) :: - ("timestamp" | uint32) :: + ("timestamp" | timestampSecond) :: (messageFlagsCodec >>:~ { messageFlags => channelFlagsCodec :: ("cltvExpiryDelta" | cltvExpiryDelta) :: @@ -304,7 +304,7 @@ object LightningMessageCodecs { val gossipTimestampFilterCodec: Codec[GossipTimestampFilter] = ( ("chainHash" | bytes32) :: - ("firstTimestamp" | uint32) :: + ("firstTimestamp" | timestampSecond) :: ("timestampRange" | uint32) :: ("tlvStream" | GossipTimestampFilterTlv.gossipTimestampFilterTlvCodec)).as[GossipTimestampFilter] diff --git a/eclair-core/src/main/scala/fr/acinq/eclair/wire/protocol/LightningMessageTypes.scala b/eclair-core/src/main/scala/fr/acinq/eclair/wire/protocol/LightningMessageTypes.scala index f5a1775e4f..c7773fc716 100644 --- a/eclair-core/src/main/scala/fr/acinq/eclair/wire/protocol/LightningMessageTypes.scala +++ b/eclair-core/src/main/scala/fr/acinq/eclair/wire/protocol/LightningMessageTypes.scala @@ -21,7 +21,7 @@ import fr.acinq.bitcoin.Crypto.{PrivateKey, PublicKey} import fr.acinq.bitcoin.{ByteVector32, ByteVector64, Satoshi} import fr.acinq.eclair.blockchain.fee.FeeratePerKw import fr.acinq.eclair.channel.ChannelType -import fr.acinq.eclair.{CltvExpiry, CltvExpiryDelta, Features, MilliSatoshi, ShortChannelId, UInt64} +import fr.acinq.eclair.{CltvExpiry, CltvExpiryDelta, Features, MilliSatoshi, ShortChannelId, TimestampMilli, TimestampSecond, UInt64} import scodec.bits.ByteVector import java.net.{Inet4Address, Inet6Address, InetAddress, InetSocketAddress} @@ -39,7 +39,7 @@ sealed trait ChannelMessage extends LightningMessage sealed trait HtlcMessage extends LightningMessage sealed trait RoutingMessage extends LightningMessage sealed trait AnnouncementMessage extends RoutingMessage // <- not in the spec -sealed trait HasTimestamp extends LightningMessage { def timestamp: Long } +sealed trait HasTimestamp extends LightningMessage { def timestamp: TimestampSecond } sealed trait HasTemporaryChannelId extends LightningMessage { def temporaryChannelId: ByteVector32 } // <- not in the spec sealed trait HasChannelId extends LightningMessage { def channelId: ByteVector32 } // <- not in the spec sealed trait HasChainHash extends LightningMessage { def chainHash: ByteVector32 } // <- not in the spec @@ -243,7 +243,7 @@ case class Tor3(tor3: String, port: Int) extends OnionAddress { override def soc case class NodeAnnouncement(signature: ByteVector64, features: Features, - timestamp: Long, + timestamp: TimestampSecond, nodeId: PublicKey, rgbColor: Color, alias: String, @@ -253,7 +253,7 @@ case class NodeAnnouncement(signature: ByteVector64, case class ChannelUpdate(signature: ByteVector64, chainHash: ByteVector32, shortChannelId: ShortChannelId, - timestamp: Long, + timestamp: TimestampSecond, channelFlags: ChannelUpdate.ChannelFlags, cltvExpiryDelta: CltvExpiryDelta, htlcMinimumMsat: MilliSatoshi, @@ -318,7 +318,7 @@ object ReplyChannelRange { } } -case class GossipTimestampFilter(chainHash: ByteVector32, firstTimestamp: Long, timestampRange: Long, tlvStream: TlvStream[GossipTimestampFilterTlv] = TlvStream.empty) extends RoutingMessage with HasChainHash +case class GossipTimestampFilter(chainHash: ByteVector32, firstTimestamp: TimestampSecond, timestampRange: Long, tlvStream: TlvStream[GossipTimestampFilterTlv] = TlvStream.empty) extends RoutingMessage with HasChainHash // NB: blank lines to minimize merge conflicts diff --git a/eclair-core/src/main/scala/fr/acinq/eclair/wire/protocol/Onion.scala b/eclair-core/src/main/scala/fr/acinq/eclair/wire/protocol/Onion.scala index 37ea3a676d..7adf76a155 100644 --- a/eclair-core/src/main/scala/fr/acinq/eclair/wire/protocol/Onion.scala +++ b/eclair-core/src/main/scala/fr/acinq/eclair/wire/protocol/Onion.scala @@ -96,8 +96,8 @@ TRAMPOLINE PAYMENT TO LEGACY RECIPIENT (the last trampoline node converts to a s | (encrypted) | | trampoline_onion: | | trampoline_onion: | | | (encrypted) | +-------------------------+ of 2500 msat split between multiple trampoline routes (omitted if +-----------------------+ | +-----------------------+ | | +-----------------------------+ | | +-----------------------+ | EOF | MPP not supported by invoice). | | amount_fwd: 1600 msat | | | | amount_fwd: 1500 msat | | | +-------------------------+ The remaining 1000 msat needed to reach the total 2500 msat have - | | expiry: 600042 | | | | expiry: 600000 | |--+ been sent by a via a completely separate trampoline route (not - | | node_id: t2 | | | | total_amount: 2500 msat | | | +-----------------------+ +-------------------------+ included in this diagram). + | | expiry: 600042 | | | | expiry: 600000 | |--+ been sent via a completely separate trampoline route (not included + | | node_id: t2 | | | | total_amount: 2500 msat | | | +-----------------------+ +-------------------------+ in this diagram). | +-----------------------+ | | | secret: xyz | | | | amount_fwd: 500 msat | | amount_fwd: 500 msat | | | (encrypted) | | | | node_id: f | | | | expiry: 600000 | | expiry: 600000 | | +-----------------------+ | | | invoice_features: 0x0a | | +---->| channel_id: 43 |---->| secret: xyz | @@ -144,6 +144,16 @@ object OnionTlv { */ case class PaymentData(secret: ByteVector32, totalAmount: MilliSatoshi) extends OnionTlv + /** + * Route blinding lets the recipient provide some encrypted data for each intermediate node in the blinded part of the + * route. This data cannot be decrypted or modified by the sender and usually contains information to locate the next + * node without revealing it to the sender. + */ + case class EncryptedRecipientData(data: ByteVector) extends OnionTlv + + /** Blinding ephemeral public key that should be used to derive shared secrets when using route blinding. */ + case class BlindingPoint(publicKey: PublicKey) extends OnionTlv + /** Id of the next node. */ case class OutgoingNodeId(nodeId: PublicKey) extends OnionTlv @@ -327,6 +337,10 @@ object OnionCodecs { private val paymentData: Codec[PaymentData] = variableSizeBytesLong(varintoverflow, ("payment_secret" | bytes32) :: ("total_msat" | tmillisatoshi)).as[PaymentData] + private val encryptedRecipientData: Codec[EncryptedRecipientData] = variableSizeBytesLong(varintoverflow, "encrypted_data" | bytes).as[EncryptedRecipientData] + + private val blindingPoint: Codec[BlindingPoint] = variableSizeBytesLong(varintoverflow, "blinding_key" | publicKey).as[BlindingPoint] + private val outgoingNodeId: Codec[OutgoingNodeId] = variableSizeBytesLong(varintoverflow, "node_id" | publicKey).as[OutgoingNodeId] private val invoiceFeatures: Codec[InvoiceFeatures] = variableSizeBytesLong(varintoverflow, bytes).as[InvoiceFeatures] @@ -342,6 +356,8 @@ object OnionCodecs { .typecase(UInt64(4), outgoingCltv) .typecase(UInt64(6), outgoingChannelId) .typecase(UInt64(8), paymentData) + .typecase(UInt64(10), encryptedRecipientData) + .typecase(UInt64(12), blindingPoint) // Types below aren't specified - use cautiously when deploying (be careful with backwards-compatibility). .typecase(UInt64(66097), invoiceFeatures) .typecase(UInt64(66098), outgoingNodeId) diff --git a/eclair-core/src/main/scala/fr/acinq/eclair/wire/protocol/RoutingTlv.scala b/eclair-core/src/main/scala/fr/acinq/eclair/wire/protocol/RoutingTlv.scala index 3b486699a3..c567aa86d6 100644 --- a/eclair-core/src/main/scala/fr/acinq/eclair/wire/protocol/RoutingTlv.scala +++ b/eclair-core/src/main/scala/fr/acinq/eclair/wire/protocol/RoutingTlv.scala @@ -16,9 +16,9 @@ package fr.acinq.eclair.wire.protocol -import fr.acinq.eclair.UInt64 -import fr.acinq.eclair.wire.protocol.CommonCodecs.{varint, varintoverflow} +import fr.acinq.eclair.wire.protocol.CommonCodecs.{timestampSecond, varint, varintoverflow} import fr.acinq.eclair.wire.protocol.TlvCodecs.tlvStream +import fr.acinq.eclair.{TimestampSecond, UInt64} import scodec.Codec import scodec.codecs._ @@ -97,7 +97,7 @@ object ReplyChannelRangeTlv { * @param timestamp1 timestamp for node 1, or 0 * @param timestamp2 timestamp for node 2, or 0 */ - case class Timestamps(timestamp1: Long, timestamp2: Long) + case class Timestamps(timestamp1: TimestampSecond, timestamp2: TimestampSecond) /** * Optional timestamps TLV that can be appended to ReplyChannelRange @@ -124,8 +124,8 @@ object ReplyChannelRangeTlv { } val timestampsCodec: Codec[Timestamps] = ( - ("timestamp1" | uint32) :: - ("timestamp2" | uint32) + ("timestamp1" | timestampSecond) :: + ("timestamp2" | timestampSecond) ).as[Timestamps] val encodedTimestampsCodec: Codec[EncodedTimestamps] = variableSizeBytesLong(varintoverflow, diff --git a/eclair-core/src/test/scala/fr/acinq/eclair/CoinUtilsSpec.scala b/eclair-core/src/test/scala/fr/acinq/eclair/CoinUtilsSpec.scala deleted file mode 100644 index 012bdd83c6..0000000000 --- a/eclair-core/src/test/scala/fr/acinq/eclair/CoinUtilsSpec.scala +++ /dev/null @@ -1,115 +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 - -import fr.acinq.bitcoin.{Btc, MilliBtc, Satoshi} -import org.scalatest.funsuite.AnyFunSuite -import org.scalatest.ParallelTestExecution - -class CoinUtilsSpec extends AnyFunSuite with ParallelTestExecution { - - test("Convert string amount to the correct BtcAmount") { - val am_btc: MilliSatoshi = CoinUtils.convertStringAmountToMsat("1", BtcUnit.code) - assert(am_btc == MilliSatoshi(100000000000L)) - val am_mbtc: MilliSatoshi = CoinUtils.convertStringAmountToMsat("1", MBtcUnit.code) - assert(am_mbtc == MilliSatoshi(100000000L)) - val am_bits: MilliSatoshi = CoinUtils.convertStringAmountToMsat("1", BitUnit.code) - assert(am_bits == MilliSatoshi(100000)) - val am_sat: MilliSatoshi = CoinUtils.convertStringAmountToMsat("1", SatUnit.code) - assert(am_sat == MilliSatoshi(1000)) - val am_msat: MilliSatoshi = CoinUtils.convertStringAmountToMsat("1", MSatUnit.code) - assert(am_msat == MilliSatoshi(1)) - val am_zero: MilliSatoshi = CoinUtils.convertStringAmountToMsat("0", MBtcUnit.code) - assert(am_zero == MilliSatoshi(0)) - } - - test("Convert decimal string amount to the correct BtcAmount") { - val am_btc_dec: MilliSatoshi = CoinUtils.convertStringAmountToMsat("1.23456789876", BtcUnit.code) - assert(am_btc_dec == MilliSatoshi(123456789876L)) - val am_mbtc_dec_nozero: MilliSatoshi = CoinUtils.convertStringAmountToMsat(".25", MBtcUnit.code) - assert(am_mbtc_dec_nozero == MilliSatoshi(25000000L)) - val am_mbtc_dec: MilliSatoshi = CoinUtils.convertStringAmountToMsat("1.23456789", MBtcUnit.code) - assert(am_mbtc_dec == MilliSatoshi(123456789L)) - val am_bits_dec: MilliSatoshi = CoinUtils.convertStringAmountToMsat("1.23456789", BitUnit.code) - assert(am_bits_dec == MilliSatoshi(123456)) - val am_sat_dec: MilliSatoshi = CoinUtils.convertStringAmountToMsat("1.23456789", SatUnit.code) - assert(am_sat_dec == MilliSatoshi(1234)) - val am_msat_dec: MilliSatoshi = CoinUtils.convertStringAmountToMsat("1.234", MSatUnit.code) - assert(am_msat_dec == MilliSatoshi(1)) - } - - test("Convert string amount with multiple decimal") { - intercept[IllegalArgumentException](CoinUtils.convertStringAmountToMsat(".12.3456789876", "foo")) - } - - test("Convert string amount with unknown unit") { - intercept[IllegalArgumentException](CoinUtils.convertStringAmountToMsat("1.23456789876", "foo")) - } - - test("Convert string amount with a non numerical amount") { - intercept[NumberFormatException](CoinUtils.convertStringAmountToMsat("1.abcd", MBtcUnit.code)) - } - - test("Convert string amount with an empty amount") { - intercept[NumberFormatException](CoinUtils.convertStringAmountToMsat("", MBtcUnit.code)) - } - - test("Convert string amount with a invalid numerical amount") { - intercept[NumberFormatException](CoinUtils.convertStringAmountToMsat("1.23.45", MBtcUnit.code)) - } - - test("Convert string amount with a negative numerical amount") { - intercept[IllegalArgumentException](CoinUtils.convertStringAmountToMsat("-1", MBtcUnit.code)) - } - - test("Convert any BtcAmount to a raw BigDecimal in a given unit") { - assert(CoinUtils.rawAmountInUnit(MilliSatoshi(-1234), BtcUnit) == BigDecimal(-0.00000001234)) - assert(CoinUtils.rawAmountInUnit(MilliSatoshi(0), BtcUnit) == BigDecimal(0)) - assert(CoinUtils.rawAmountInUnit(MilliSatoshi(123), BtcUnit) == BigDecimal(0.00000000123)) - assert(CoinUtils.rawAmountInUnit(MilliSatoshi(123), MBtcUnit) == BigDecimal(0.00000123)) - assert(CoinUtils.rawAmountInUnit(MilliSatoshi(123), SatUnit) == BigDecimal(0.123)) - assert(CoinUtils.rawAmountInUnit(MilliSatoshi(123), MSatUnit) == BigDecimal(123)) - assert(CoinUtils.rawAmountInUnit(MilliSatoshi(12345678), BtcUnit) == BigDecimal(0.00012345678)) - assert(CoinUtils.rawAmountInUnit(MilliSatoshi(1234567), BitUnit) == BigDecimal(12.34567)) - - assert(CoinUtils.rawAmountInUnit(Satoshi(123), BtcUnit) == BigDecimal(0.00000123)) - assert(CoinUtils.rawAmountInUnit(Satoshi(123), MBtcUnit) == BigDecimal(0.00123)) - assert(CoinUtils.rawAmountInUnit(Satoshi(123), BitUnit) == BigDecimal(1.23)) - assert(CoinUtils.rawAmountInUnit(Satoshi(123), SatUnit) == BigDecimal(123)) - assert(CoinUtils.rawAmountInUnit(Satoshi(123), MSatUnit) == BigDecimal(123000)) - - assert(CoinUtils.rawAmountInUnit(MilliBtc(123.456), BtcUnit) == BigDecimal(0.123456)) - assert(CoinUtils.rawAmountInUnit(MilliBtc(123.456), MBtcUnit) == BigDecimal(123.456)) - assert(CoinUtils.rawAmountInUnit(MilliBtc(123.45678), BitUnit) == BigDecimal(123456.78)) - assert(CoinUtils.rawAmountInUnit(MilliBtc(123.456789), SatUnit) == BigDecimal(12345678.9)) - assert(CoinUtils.rawAmountInUnit(MilliBtc(123.45678987), MSatUnit) == BigDecimal(12345678987L)) - - assert(CoinUtils.rawAmountInUnit(Btc(123.456), BtcUnit) == BigDecimal(123.456)) - assert(CoinUtils.rawAmountInUnit(Btc(123.45678987654), MBtcUnit) == BigDecimal(123456.78987654)) - assert(CoinUtils.rawAmountInUnit(Btc(123.456789876), BitUnit) == BigDecimal(123456789.876)) - assert(CoinUtils.rawAmountInUnit(Btc(1.22233333444), SatUnit) == BigDecimal(122233333.444)) - assert(CoinUtils.rawAmountInUnit(Btc(0.00011111222), MSatUnit) == BigDecimal(11111222L)) - } - - test("Format any BtcAmount to a String with a given unit") { - assert(CoinUtils.formatAmountInUnit(MilliSatoshi(123456789), BtcUnit, withUnit = true) == "0.00123456789 BTC" - || CoinUtils.formatAmountInUnit(MilliSatoshi(123456789), BtcUnit, withUnit = true) == "0,00123456789 BTC") - } -} - - - 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 f7c8b5f1a1..61eda8374f 100644 --- a/eclair-core/src/test/scala/fr/acinq/eclair/EclairImplSpec.scala +++ b/eclair-core/src/test/scala/fr/acinq/eclair/EclairImplSpec.scala @@ -25,7 +25,7 @@ import fr.acinq.bitcoin.{Block, ByteVector32, ByteVector64, Crypto, SatoshiLong} import fr.acinq.eclair.TestConstants._ import fr.acinq.eclair.blockchain.DummyOnChainWallet import fr.acinq.eclair.blockchain.fee.{FeeratePerByte, FeeratePerKw} -import fr.acinq.eclair.channel.{CMD_FORCECLOSE, Register, _} +import fr.acinq.eclair.channel._ import fr.acinq.eclair.db._ import fr.acinq.eclair.io.Peer.OpenChannel import fr.acinq.eclair.payment.PaymentRequest @@ -132,7 +132,7 @@ class EclairImplSpec extends TestKitBaseClass with FixtureAnyFunSuiteLike with I // with finalCltvExpiry val externalId2 = "487da196-a4dc-4b1e-92b4-3e5e905e9f3f" - val invoice2 = PaymentRequest("lntb", Some(123 msat), System.currentTimeMillis() / 1000L, nodePrivKey.publicKey, List(PaymentRequest.MinFinalCltvExpiry(96), PaymentRequest.PaymentHash(ByteVector32.Zeroes), PaymentRequest.Description("description")), ByteVector.empty) + val invoice2 = PaymentRequest("lntb", Some(123 msat), TimestampSecond.now(), nodePrivKey.publicKey, List(PaymentRequest.MinFinalCltvExpiry(96), PaymentRequest.PaymentHash(ByteVector32.Zeroes), PaymentRequest.Description("description")), ByteVector.empty) eclair.send(Some(externalId2), 123 msat, invoice2) val send2 = paymentInitiator.expectMsgType[SendPaymentToNode] assert(send2.externalId === Some(externalId2)) @@ -155,7 +155,7 @@ class EclairImplSpec extends TestKitBaseClass with FixtureAnyFunSuiteLike with I val invalidExternalId = "Robert'); DROP TABLE received_payments; DROP TABLE sent_payments; DROP TABLE payments;" assertThrows[IllegalArgumentException](Await.result(eclair.send(Some(invalidExternalId), 123 msat, invoice0), 50 millis)) - val expiredInvoice = invoice2.copy(timestamp = 0L) + val expiredInvoice = invoice2.copy(timestamp = 0.unixsec) assertThrows[IllegalArgumentException](Await.result(eclair.send(None, 123 msat, expiredInvoice), 50 millis)) } @@ -163,13 +163,13 @@ class EclairImplSpec extends TestKitBaseClass with FixtureAnyFunSuiteLike with I import f._ val eclair = new EclairImpl(kit) - val remoteNodeAnn1 = NodeAnnouncement(randomBytes64(), Features.empty, 42L, randomKey().publicKey, Color(42, 42, 42), "LN-rocks", Nil) - val remoteNodeAnn2 = NodeAnnouncement(randomBytes64(), Features.empty, 43L, randomKey().publicKey, Color(43, 43, 43), "LN-papers", Nil) + val remoteNodeAnn1 = NodeAnnouncement(randomBytes64(), Features.empty, TimestampSecond(42L), randomKey().publicKey, Color(42, 42, 42), "LN-rocks", Nil) + val remoteNodeAnn2 = NodeAnnouncement(randomBytes64(), Features.empty, TimestampSecond(43L), randomKey().publicKey, Color(43, 43, 43), "LN-papers", Nil) val allNodes = Seq( - NodeAnnouncement(randomBytes64(), Features.empty, 561L, randomKey().publicKey, Color(0, 0, 0), "some-node", Nil), + NodeAnnouncement(randomBytes64(), Features.empty, TimestampSecond(561L), randomKey().publicKey, Color(0, 0, 0), "some-node", Nil), remoteNodeAnn1, remoteNodeAnn2, - NodeAnnouncement(randomBytes64(), Features.empty, 1105L, randomKey().publicKey, Color(0, 0, 0), "some-other-node", Nil), + NodeAnnouncement(randomBytes64(), Features.empty, TimestampSecond(1105L), randomKey().publicKey, Color(0, 0, 0), "some-other-node", Nil), ) { @@ -354,37 +354,6 @@ class EclairImplSpec extends TestKitBaseClass with FixtureAnyFunSuiteLike with I }) } - test("networkFees/audit/allinvoices should use a default to/from filter expressed in seconds") { f => - import f._ - - val auditDb = mock[AuditDb] - val paymentDb = mock[PaymentsDb] - - auditDb.listNetworkFees(anyLong, anyLong) returns Seq.empty - auditDb.listSent(anyLong, anyLong) returns Seq.empty - auditDb.listReceived(anyLong, anyLong) returns Seq.empty - auditDb.listRelayed(anyLong, anyLong) returns Seq.empty - paymentDb.listIncomingPayments(anyLong, anyLong) returns Seq.empty - - val databases = mock[Databases] - databases.audit returns auditDb - databases.payments returns paymentDb - - val kitWithMockAudit = kit.copy(nodeParams = kit.nodeParams.copy(db = databases)) - val eclair = new EclairImpl(kitWithMockAudit) - - Await.result(eclair.networkFees(None, None), 10 seconds) - auditDb.listNetworkFees(0, TimestampQueryFilters.MaxEpochMilliseconds).wasCalled(once) // assert the call was made only once and with the specified params - - Await.result(eclair.audit(None, None), 10 seconds) - auditDb.listRelayed(0, TimestampQueryFilters.MaxEpochMilliseconds).wasCalled(once) - auditDb.listReceived(0, TimestampQueryFilters.MaxEpochMilliseconds).wasCalled(once) - auditDb.listSent(0, TimestampQueryFilters.MaxEpochMilliseconds).wasCalled(once) - - Await.result(eclair.allInvoices(None, None), 10 seconds) - paymentDb.listIncomingPayments(0, TimestampQueryFilters.MaxEpochMilliseconds).wasCalled(once) // assert the call was made only once and with the specified params - } - test("sendtoroute should pass the parameters correctly") { f => import f._ diff --git a/eclair-core/src/test/scala/fr/acinq/eclair/TestDatabases.scala b/eclair-core/src/test/scala/fr/acinq/eclair/TestDatabases.scala index d7a8ac6716..cd72aa06b0 100644 --- a/eclair-core/src/test/scala/fr/acinq/eclair/TestDatabases.scala +++ b/eclair-core/src/test/scala/fr/acinq/eclair/TestDatabases.scala @@ -3,11 +3,12 @@ package fr.acinq.eclair import akka.actor.ActorSystem import com.opentable.db.postgres.embedded.EmbeddedPostgres import com.zaxxer.hikari.HikariConfig +import fr.acinq.eclair.channel._ import fr.acinq.eclair.db._ import fr.acinq.eclair.db.pg.PgUtils.PgLock.LockFailureHandler import fr.acinq.eclair.db.pg.PgUtils.{PgLock, getVersion, using} +import fr.acinq.eclair.db.sqlite.SqliteChannelsDb import org.postgresql.jdbc.PgConnection -import org.scalatest.Assertions.convertToEqualizer import org.sqlite.SQLiteConnection import java.io.File @@ -40,13 +41,57 @@ object TestDatabases { def inMemoryDb(): Databases = { val connection = sqliteInMemory() - Databases.SqliteDatabases(connection, connection, connection) + val dbs = Databases.SqliteDatabases(connection, connection, connection) + dbs.copy(channels = new SqliteChannelsDbWithValidation(dbs.channels)) + } + + + /** + * ChannelsDb instance that wraps around an actual db instance and does additional checks + * This can be thought of as fuzzing and fills a gap between codec unit tests and database tests, by checking that channel state can be written and read consistently + * i.e that for all channel states that we generate during our tests, read(write(state)) == state + * + * This will help catch codec errors that would not be caught by unit tests because we don't test much how codecs interact with each other + * + * @param innerDb actual database instance + */ + class SqliteChannelsDbWithValidation(innerDb: SqliteChannelsDb) extends SqliteChannelsDb(innerDb.sqlite) { + override def addOrUpdateChannel(state: HasCommitments): Unit = { + + def freeze1(input: Origin): Origin = input match { + case h: Origin.LocalHot => Origin.LocalCold(h.id) + case h: Origin.TrampolineRelayedHot => Origin.TrampolineRelayedCold(h.htlcs) + case _ => input + } + + def freeze2(input: Commitments): Commitments = input.copy(originChannels = input.originChannels.view.mapValues(o => freeze1(o)).toMap) + + // payment origins are always "cold" when deserialized, so to compare a "live" channel state against a state that has been + // serialized and deserialized we need to turn "hot" payments into cold ones + def freeze3(input: HasCommitments): HasCommitments = input match { + case d: DATA_WAIT_FOR_FUNDING_CONFIRMED => d.copy(commitments = freeze2(d.commitments)) + case d: DATA_WAIT_FOR_FUNDING_LOCKED => d.copy(commitments = freeze2(d.commitments)) + case d: DATA_WAIT_FOR_REMOTE_PUBLISH_FUTURE_COMMITMENT => d.copy(commitments = freeze2(d.commitments)) + case d: DATA_NORMAL => d.copy(commitments = freeze2(d.commitments)) + case d: DATA_CLOSING => d.copy(commitments = freeze2(d.commitments)) + case d: DATA_NEGOTIATING => d.copy(commitments = freeze2(d.commitments)) + case d: DATA_SHUTDOWN => d.copy(commitments = freeze2(d.commitments)) + } + + super.addOrUpdateChannel(state) + val check = super.getChannel(state.channelId) + val frozen = freeze3(state) + require(check.contains(frozen), s"serialization/deserialization check failed, $check != $frozen") + } } case class TestSqliteDatabases() extends TestDatabases { // @formatter:off override val connection: SQLiteConnection = sqliteInMemory() - override lazy val db: Databases = Databases.SqliteDatabases(connection, connection, connection) + override lazy val db: Databases = { + val dbs = Databases.SqliteDatabases(connection, connection, connection) + dbs.copy(channels = new SqliteChannelsDbWithValidation(dbs.channels)) + } override def close(): Unit = () // @formatter:on } diff --git a/eclair-core/src/test/scala/fr/acinq/eclair/TestUtils.scala b/eclair-core/src/test/scala/fr/acinq/eclair/TestUtils.scala index b5f96759c0..9621c38daa 100644 --- a/eclair-core/src/test/scala/fr/acinq/eclair/TestUtils.scala +++ b/eclair-core/src/test/scala/fr/acinq/eclair/TestUtils.scala @@ -20,7 +20,7 @@ import akka.actor.ActorRef import akka.event.DiagnosticLoggingAdapter import akka.testkit import akka.testkit.{TestActor, TestProbe} -import fr.acinq.eclair.channel.Channel +import fr.acinq.eclair.io.Peer import fr.acinq.eclair.wire.protocol.LightningMessage import java.io.File @@ -67,15 +67,15 @@ object TestUtils { /** - * [[Channel]] encapsulates outgoing messages in [[Channel.OutgoingMessage]] due to how connection management works. + * [[Channel]] encapsulates outgoing messages in [[Peer.OutgoingMessage]] due to how connection management works. * - * This strips the [[Channel.OutgoingMessage]] outer shell and only forwards the inner [[LightningMessage]] making testing + * This strips the [[Peer.OutgoingMessage]] outer shell and only forwards the inner [[LightningMessage]] making testing * easier. You can now pass a [[TestProbe]] as a connection and only deal with incoming/outgoing [[LightningMessage]]. */ def forwardOutgoingToPipe(peer: TestProbe, pipe: ActorRef): Unit = { peer.setAutoPilot(new testkit.TestActor.AutoPilot { override def run(sender: ActorRef, msg: Any): TestActor.AutoPilot = msg match { - case Channel.OutgoingMessage(msg: LightningMessage, _: ActorRef) => + case Peer.OutgoingMessage(msg: LightningMessage, _: ActorRef) => pipe.tell(msg, sender) TestActor.KeepRunning case _ => TestActor.KeepRunning diff --git a/eclair-core/src/test/scala/fr/acinq/eclair/channel/HelpersSpec.scala b/eclair-core/src/test/scala/fr/acinq/eclair/channel/HelpersSpec.scala index 6f0a3dc694..6e9edb059b 100644 --- a/eclair-core/src/test/scala/fr/acinq/eclair/channel/HelpersSpec.scala +++ b/eclair-core/src/test/scala/fr/acinq/eclair/channel/HelpersSpec.scala @@ -25,7 +25,7 @@ import fr.acinq.eclair.channel.Helpers.Closing import fr.acinq.eclair.channel.states.{ChannelStateTestsHelperMethods, ChannelStateTestsTags} import fr.acinq.eclair.transactions.Transactions._ import fr.acinq.eclair.wire.protocol.UpdateAddHtlc -import fr.acinq.eclair.{MilliSatoshiLong, TestKitBaseClass} +import fr.acinq.eclair.{MilliSatoshiLong, TestKitBaseClass, TimestampSecond, TimestampSecondLong} import org.scalatest.Tag import org.scalatest.funsuite.AnyFunSuiteLike import scodec.bits.HexStringSyntax @@ -50,10 +50,10 @@ class HelpersSpec extends TestKitBaseClass with AnyFunSuiteLike with ChannelStat test("compute refresh delay") { import org.scalatest.matchers.should.Matchers._ implicit val log: akka.event.DiagnosticLoggingAdapter = NoLoggingDiagnostics - Helpers.nextChannelUpdateRefresh(1544400000).toSeconds should equal(0) - Helpers.nextChannelUpdateRefresh((System.currentTimeMillis.milliseconds - 9.days).toSeconds).toSeconds should equal(24 * 3600L +- 100) - Helpers.nextChannelUpdateRefresh((System.currentTimeMillis.milliseconds - 3.days).toSeconds).toSeconds should equal(7 * 24 * 3600L +- 100) - Helpers.nextChannelUpdateRefresh(System.currentTimeMillis.milliseconds.toSeconds).toSeconds should equal(10 * 24 * 3600L +- 100) + Helpers.nextChannelUpdateRefresh(1544400000 unixsec).toSeconds should equal(0) + Helpers.nextChannelUpdateRefresh(TimestampSecond.now() - 9.days).toSeconds should equal(24 * 3600L +- 100) + Helpers.nextChannelUpdateRefresh(TimestampSecond.now() - 3.days).toSeconds should equal(7 * 24 * 3600L +- 100) + Helpers.nextChannelUpdateRefresh(TimestampSecond.now()).toSeconds should equal(10 * 24 * 3600L +- 100) } case class Fixture(alice: TestFSMRef[ChannelState, ChannelData, Channel], aliceCommitPublished: LocalCommitPublished, aliceHtlcs: Set[UpdateAddHtlc], bob: TestFSMRef[ChannelState, ChannelData, Channel], bobCommitPublished: RemoteCommitPublished, bobHtlcs: Set[UpdateAddHtlc], probe: TestProbe) @@ -190,25 +190,25 @@ class HelpersSpec extends TestKitBaseClass with AnyFunSuiteLike with ChannelStat val claimHtlcSuccessTxs = getClaimHtlcSuccessTxs(remoteCommitPublished) val aliceTimedOutHtlcs = htlcTimeoutTxs.map(htlcTimeout => { - val timedOutHtlcs = Closing.timedOutHtlcs(commitmentFormat, localCommit, localCommitPublished, dustLimit, htlcTimeout.tx) + val timedOutHtlcs = Closing.trimmedOrTimedOutHtlcs(commitmentFormat, localCommit, localCommitPublished, dustLimit, htlcTimeout.tx) assert(timedOutHtlcs.size === 1) timedOutHtlcs.head }) assert(aliceTimedOutHtlcs.toSet === aliceHtlcs) val bobTimedOutHtlcs = claimHtlcTimeoutTxs.map(claimHtlcTimeout => { - val timedOutHtlcs = Closing.timedOutHtlcs(commitmentFormat, remoteCommit, remoteCommitPublished, dustLimit, claimHtlcTimeout.tx) + val timedOutHtlcs = Closing.trimmedOrTimedOutHtlcs(commitmentFormat, remoteCommit, remoteCommitPublished, dustLimit, claimHtlcTimeout.tx) assert(timedOutHtlcs.size === 1) timedOutHtlcs.head }) assert(bobTimedOutHtlcs.toSet === bobHtlcs) - htlcSuccessTxs.foreach(htlcSuccess => assert(Closing.timedOutHtlcs(commitmentFormat, localCommit, localCommitPublished, dustLimit, htlcSuccess.tx).isEmpty)) - htlcSuccessTxs.foreach(htlcSuccess => assert(Closing.timedOutHtlcs(commitmentFormat, remoteCommit, remoteCommitPublished, dustLimit, htlcSuccess.tx).isEmpty)) - claimHtlcSuccessTxs.foreach(claimHtlcSuccess => assert(Closing.timedOutHtlcs(commitmentFormat, localCommit, localCommitPublished, dustLimit, claimHtlcSuccess.tx).isEmpty)) - claimHtlcSuccessTxs.foreach(claimHtlcSuccess => assert(Closing.timedOutHtlcs(commitmentFormat, remoteCommit, remoteCommitPublished, dustLimit, claimHtlcSuccess.tx).isEmpty)) - htlcTimeoutTxs.foreach(htlcTimeout => assert(Closing.timedOutHtlcs(commitmentFormat, remoteCommit, remoteCommitPublished, dustLimit, htlcTimeout.tx).isEmpty)) - claimHtlcTimeoutTxs.foreach(claimHtlcTimeout => assert(Closing.timedOutHtlcs(commitmentFormat, localCommit, localCommitPublished, dustLimit, claimHtlcTimeout.tx).isEmpty)) + htlcSuccessTxs.foreach(htlcSuccess => assert(Closing.trimmedOrTimedOutHtlcs(commitmentFormat, localCommit, localCommitPublished, dustLimit, htlcSuccess.tx).isEmpty)) + htlcSuccessTxs.foreach(htlcSuccess => assert(Closing.trimmedOrTimedOutHtlcs(commitmentFormat, remoteCommit, remoteCommitPublished, dustLimit, htlcSuccess.tx).isEmpty)) + claimHtlcSuccessTxs.foreach(claimHtlcSuccess => assert(Closing.trimmedOrTimedOutHtlcs(commitmentFormat, localCommit, localCommitPublished, dustLimit, claimHtlcSuccess.tx).isEmpty)) + claimHtlcSuccessTxs.foreach(claimHtlcSuccess => assert(Closing.trimmedOrTimedOutHtlcs(commitmentFormat, remoteCommit, remoteCommitPublished, dustLimit, claimHtlcSuccess.tx).isEmpty)) + htlcTimeoutTxs.foreach(htlcTimeout => assert(Closing.trimmedOrTimedOutHtlcs(commitmentFormat, remoteCommit, remoteCommitPublished, dustLimit, htlcTimeout.tx).isEmpty)) + claimHtlcTimeoutTxs.foreach(claimHtlcTimeout => assert(Closing.trimmedOrTimedOutHtlcs(commitmentFormat, localCommit, localCommitPublished, dustLimit, claimHtlcTimeout.tx).isEmpty)) } test("find timed out htlcs") { diff --git a/eclair-core/src/test/scala/fr/acinq/eclair/channel/states/ChannelStateTestsHelperMethods.scala b/eclair-core/src/test/scala/fr/acinq/eclair/channel/states/ChannelStateTestsHelperMethods.scala index 4cb62e5187..b5c849e6ce 100644 --- a/eclair-core/src/test/scala/fr/acinq/eclair/channel/states/ChannelStateTestsHelperMethods.scala +++ b/eclair-core/src/test/scala/fr/acinq/eclair/channel/states/ChannelStateTestsHelperMethods.scala @@ -86,6 +86,7 @@ trait ChannelStateTestsHelperMethods extends TestKitBase { case class SetupFixture(alice: TestFSMRef[ChannelState, ChannelData, Channel], bob: TestFSMRef[ChannelState, ChannelData, Channel], + aliceOrigin: TestProbe, alice2bob: TestProbe, bob2alice: TestProbe, alice2blockchain: TestProbe, @@ -101,6 +102,7 @@ trait ChannelStateTestsHelperMethods extends TestKitBase { } def init(nodeParamsA: NodeParams = TestConstants.Alice.nodeParams, nodeParamsB: NodeParams = TestConstants.Bob.nodeParams, wallet: OnChainWallet = new DummyOnChainWallet(), tags: Set[String] = Set.empty): SetupFixture = { + val aliceOrigin = TestProbe() val alice2bob = TestProbe() val bob2alice = TestProbe() val alicePeer = TestProbe() @@ -125,9 +127,9 @@ trait ChannelStateTestsHelperMethods extends TestKitBase { .modify(_.dustLimit).setToIf(tags.contains(ChannelStateTestsTags.HighDustLimitDifferenceBobAlice))(5000 sat) .modify(_.maxRemoteDustLimit).setToIf(tags.contains(ChannelStateTestsTags.HighDustLimitDifferenceAliceBob))(10000 sat) .modify(_.maxRemoteDustLimit).setToIf(tags.contains(ChannelStateTestsTags.HighDustLimitDifferenceBobAlice))(10000 sat) - val alice: TestFSMRef[ChannelState, ChannelData, Channel] = TestFSMRef(new Channel(finalNodeParamsA, wallet, finalNodeParamsB.nodeId, alice2blockchain.ref, relayerA.ref, FakeTxPublisherFactory(alice2blockchain)), alicePeer.ref) + val alice: TestFSMRef[ChannelState, ChannelData, Channel] = TestFSMRef(new Channel(finalNodeParamsA, wallet, finalNodeParamsB.nodeId, alice2blockchain.ref, relayerA.ref, FakeTxPublisherFactory(alice2blockchain), origin_opt = Some(aliceOrigin.ref)), alicePeer.ref) val bob: TestFSMRef[ChannelState, ChannelData, Channel] = TestFSMRef(new Channel(finalNodeParamsB, wallet, finalNodeParamsA.nodeId, bob2blockchain.ref, relayerB.ref, FakeTxPublisherFactory(bob2blockchain)), bobPeer.ref) - SetupFixture(alice, bob, alice2bob, bob2alice, alice2blockchain, bob2blockchain, router, relayerA, relayerB, channelUpdateListener, wallet, alicePeer, bobPeer) + SetupFixture(alice, bob, aliceOrigin, alice2bob, bob2alice, alice2blockchain, bob2blockchain, router, relayerA, relayerB, channelUpdateListener, wallet, alicePeer, bobPeer) } def computeFeatures(setup: SetupFixture, tags: Set[String]): (LocalParams, LocalParams, SupportedChannelType) = { diff --git a/eclair-core/src/test/scala/fr/acinq/eclair/channel/states/a/WaitForAcceptChannelStateSpec.scala b/eclair-core/src/test/scala/fr/acinq/eclair/channel/states/a/WaitForAcceptChannelStateSpec.scala index b3c4a2738b..a302e51847 100644 --- a/eclair-core/src/test/scala/fr/acinq/eclair/channel/states/a/WaitForAcceptChannelStateSpec.scala +++ b/eclair-core/src/test/scala/fr/acinq/eclair/channel/states/a/WaitForAcceptChannelStateSpec.scala @@ -16,12 +16,11 @@ package fr.acinq.eclair.channel.states.a +import akka.actor.Status import akka.testkit.{TestFSMRef, TestProbe} -import fr.acinq.bitcoin.{Block, Btc, ByteVector32, Satoshi, SatoshiLong} +import fr.acinq.bitcoin.{Block, Btc, ByteVector32, SatoshiLong} import fr.acinq.eclair.TestConstants.{Alice, Bob} -import fr.acinq.eclair.blockchain.OnChainWallet.MakeFundingTxResponse -import fr.acinq.eclair.blockchain.{DummyOnChainWallet, NoOpOnChainWallet} -import fr.acinq.eclair.blockchain.fee.FeeratePerKw +import fr.acinq.eclair.blockchain.NoOpOnChainWallet import fr.acinq.eclair.channel.Channel.TickChannelOpenTimeout import fr.acinq.eclair.channel._ import fr.acinq.eclair.channel.states.{ChannelStateTestsBase, ChannelStateTestsTags} @@ -32,7 +31,6 @@ import org.scalatest.{Outcome, Tag} import scodec.bits.ByteVector import scala.concurrent.duration._ -import scala.concurrent.{Future, Promise} /** * Created by PM on 05/07/2016. @@ -40,7 +38,7 @@ import scala.concurrent.{Future, Promise} class WaitForAcceptChannelStateSpec extends TestKitBaseClass with FixtureAnyFunSuiteLike with ChannelStateTestsBase { - case class FixtureParam(alice: TestFSMRef[ChannelState, ChannelData, Channel], bob: TestFSMRef[ChannelState, ChannelData, Channel], alice2bob: TestProbe, bob2alice: TestProbe, alice2blockchain: TestProbe) + case class FixtureParam(alice: TestFSMRef[ChannelState, ChannelData, Channel], bob: TestFSMRef[ChannelState, ChannelData, Channel], aliceOrigin: TestProbe, alice2bob: TestProbe, bob2alice: TestProbe, alice2blockchain: TestProbe) override def withFixture(test: OneArgTest): Outcome = { import com.softwaremill.quicklens._ @@ -69,7 +67,7 @@ class WaitForAcceptChannelStateSpec extends TestKitBaseClass with FixtureAnyFunS alice2bob.expectMsgType[OpenChannel] alice2bob.forward(bob) awaitCond(alice.stateName == WAIT_FOR_ACCEPT_CHANNEL) - withFixture(test.toNoArgTest(FixtureParam(alice, bob, alice2bob, bob2alice, alice2blockchain))) + withFixture(test.toNoArgTest(FixtureParam(alice, bob, aliceOrigin, alice2bob, bob2alice, alice2blockchain))) } } @@ -81,6 +79,7 @@ class WaitForAcceptChannelStateSpec extends TestKitBaseClass with FixtureAnyFunS assert(accept.channelType_opt === Some(ChannelTypes.Standard)) bob2alice.forward(alice) awaitCond(alice.stateName == WAIT_FOR_FUNDING_INTERNAL) + aliceOrigin.expectNoMessage() } test("recv AcceptChannel (anchor outputs)", Tag(ChannelStateTestsTags.AnchorOutputs)) { f => @@ -90,6 +89,7 @@ class WaitForAcceptChannelStateSpec extends TestKitBaseClass with FixtureAnyFunS bob2alice.forward(alice) awaitCond(alice.stateName == WAIT_FOR_FUNDING_INTERNAL) assert(alice.stateData.asInstanceOf[DATA_WAIT_FOR_FUNDING_INTERNAL].channelFeatures.channelType === ChannelTypes.AnchorOutputs) + aliceOrigin.expectNoMessage() } test("recv AcceptChannel (anchor outputs zero fee htlc txs)", Tag(ChannelStateTestsTags.AnchorOutputsZeroFeeHtlcTxs)) { f => @@ -99,6 +99,7 @@ class WaitForAcceptChannelStateSpec extends TestKitBaseClass with FixtureAnyFunS bob2alice.forward(alice) awaitCond(alice.stateName == WAIT_FOR_FUNDING_INTERNAL) assert(alice.stateData.asInstanceOf[DATA_WAIT_FOR_FUNDING_INTERNAL].channelFeatures.channelType === ChannelTypes.AnchorOutputsZeroFeeHtlcTx) + aliceOrigin.expectNoMessage() } test("recv AcceptChannel (channel type not set)", Tag(ChannelStateTestsTags.AnchorOutputs)) { f => @@ -110,6 +111,7 @@ class WaitForAcceptChannelStateSpec extends TestKitBaseClass with FixtureAnyFunS bob2alice.forward(alice, accept.copy(tlvStream = TlvStream(ChannelTlv.UpfrontShutdownScriptTlv(ByteVector.empty)))) awaitCond(alice.stateName == WAIT_FOR_FUNDING_INTERNAL) assert(alice.stateData.asInstanceOf[DATA_WAIT_FOR_FUNDING_INTERNAL].channelFeatures.channelType === ChannelTypes.AnchorOutputs) + aliceOrigin.expectNoMessage() } test("recv AcceptChannel (non-default channel type)", Tag(ChannelStateTestsTags.AnchorOutputsZeroFeeHtlcTxs), Tag("standard-channel-type")) { f => @@ -120,6 +122,7 @@ class WaitForAcceptChannelStateSpec extends TestKitBaseClass with FixtureAnyFunS bob2alice.forward(alice, accept) awaitCond(alice.stateName == WAIT_FOR_FUNDING_INTERNAL) assert(alice.stateData.asInstanceOf[DATA_WAIT_FOR_FUNDING_INTERNAL].channelFeatures.channelType === ChannelTypes.Standard) + aliceOrigin.expectNoMessage() } test("recv AcceptChannel (non-default channel type not set)", Tag(ChannelStateTestsTags.AnchorOutputsZeroFeeHtlcTxs), Tag("standard-channel-type")) { f => @@ -131,6 +134,7 @@ class WaitForAcceptChannelStateSpec extends TestKitBaseClass with FixtureAnyFunS bob2alice.forward(alice, accept.copy(tlvStream = TlvStream(ChannelTlv.UpfrontShutdownScriptTlv(ByteVector.empty)))) alice2bob.expectMsg(Error(accept.temporaryChannelId, "invalid channel_type=anchor_outputs_zero_fee_htlc_tx, expected channel_type=standard")) awaitCond(alice.stateName == CLOSED) + aliceOrigin.expectMsgType[Status.Failure] } test("recv AcceptChannel (anchor outputs channel type without enabling the feature)") { _ => @@ -151,6 +155,7 @@ class WaitForAcceptChannelStateSpec extends TestKitBaseClass with FixtureAnyFunS bob2alice.forward(alice, accept) awaitCond(alice.stateName == WAIT_FOR_FUNDING_INTERNAL) assert(alice.stateData.asInstanceOf[DATA_WAIT_FOR_FUNDING_INTERNAL].channelFeatures.channelType === ChannelTypes.AnchorOutputs) + aliceOrigin.expectNoMessage() } test("recv AcceptChannel (invalid channel type)") { f => @@ -161,6 +166,7 @@ class WaitForAcceptChannelStateSpec extends TestKitBaseClass with FixtureAnyFunS bob2alice.forward(alice, invalidAccept) alice2bob.expectMsg(Error(accept.temporaryChannelId, "invalid channel_type=anchor_outputs, expected channel_type=standard")) awaitCond(alice.stateName == CLOSED) + aliceOrigin.expectMsgType[Status.Failure] } test("recv AcceptChannel (invalid max accepted htlcs)") { f => @@ -172,6 +178,7 @@ class WaitForAcceptChannelStateSpec extends TestKitBaseClass with FixtureAnyFunS val error = alice2bob.expectMsgType[Error] assert(error === Error(accept.temporaryChannelId, InvalidMaxAcceptedHtlcs(accept.temporaryChannelId, invalidMaxAcceptedHtlcs, Channel.MAX_ACCEPTED_HTLCS).getMessage)) awaitCond(alice.stateName == CLOSED) + aliceOrigin.expectMsgType[Status.Failure] } test("recv AcceptChannel (dust limit too low)", Tag("mainnet")) { f => @@ -183,6 +190,7 @@ class WaitForAcceptChannelStateSpec extends TestKitBaseClass with FixtureAnyFunS val error = alice2bob.expectMsgType[Error] assert(error === Error(accept.temporaryChannelId, DustLimitTooSmall(accept.temporaryChannelId, lowDustLimitSatoshis, Channel.MIN_DUST_LIMIT).getMessage)) awaitCond(alice.stateName == CLOSED) + aliceOrigin.expectMsgType[Status.Failure] } test("recv AcceptChannel (dust limit too high)") { f => @@ -193,6 +201,7 @@ class WaitForAcceptChannelStateSpec extends TestKitBaseClass with FixtureAnyFunS val error = alice2bob.expectMsgType[Error] assert(error === Error(accept.temporaryChannelId, DustLimitTooLarge(accept.temporaryChannelId, highDustLimitSatoshis, Alice.nodeParams.maxRemoteDustLimit).getMessage)) awaitCond(alice.stateName == CLOSED) + aliceOrigin.expectMsgType[Status.Failure] } test("recv AcceptChannel (to_self_delay too high)") { f => @@ -203,6 +212,7 @@ class WaitForAcceptChannelStateSpec extends TestKitBaseClass with FixtureAnyFunS val error = alice2bob.expectMsgType[Error] assert(error === Error(accept.temporaryChannelId, ToSelfDelayTooHigh(accept.temporaryChannelId, delayTooHigh, Alice.nodeParams.maxToLocalDelay).getMessage)) awaitCond(alice.stateName == CLOSED) + aliceOrigin.expectMsgType[Status.Failure] } test("recv AcceptChannel (reserve too high)") { f => @@ -214,6 +224,7 @@ class WaitForAcceptChannelStateSpec extends TestKitBaseClass with FixtureAnyFunS val error = alice2bob.expectMsgType[Error] assert(error === Error(accept.temporaryChannelId, ChannelReserveTooHigh(accept.temporaryChannelId, reserveTooHigh, 0.3, 0.05).getMessage)) awaitCond(alice.stateName == CLOSED) + aliceOrigin.expectMsgType[Status.Failure] } test("recv AcceptChannel (reserve below dust limit)") { f => @@ -224,6 +235,7 @@ class WaitForAcceptChannelStateSpec extends TestKitBaseClass with FixtureAnyFunS val error = alice2bob.expectMsgType[Error] assert(error === Error(accept.temporaryChannelId, DustLimitTooLarge(accept.temporaryChannelId, accept.dustLimitSatoshis, reserveTooSmall).getMessage)) awaitCond(alice.stateName == CLOSED) + aliceOrigin.expectMsgType[Status.Failure] } test("recv AcceptChannel (reserve below our dust limit)") { f => @@ -235,6 +247,7 @@ class WaitForAcceptChannelStateSpec extends TestKitBaseClass with FixtureAnyFunS val error = alice2bob.expectMsgType[Error] assert(error === Error(accept.temporaryChannelId, ChannelReserveBelowOurDustLimit(accept.temporaryChannelId, reserveTooSmall, open.dustLimitSatoshis).getMessage)) awaitCond(alice.stateName == CLOSED) + aliceOrigin.expectMsgType[Status.Failure] } test("recv AcceptChannel (dust limit above our reserve)", Tag("high-remote-dust-limit")) { f => @@ -246,6 +259,7 @@ class WaitForAcceptChannelStateSpec extends TestKitBaseClass with FixtureAnyFunS val error = alice2bob.expectMsgType[Error] assert(error === Error(accept.temporaryChannelId, DustLimitAboveOurChannelReserve(accept.temporaryChannelId, dustTooBig, open.channelReserveSatoshis).getMessage)) awaitCond(alice.stateName == CLOSED) + aliceOrigin.expectMsgType[Status.Failure] } test("recv AcceptChannel (wumbo size channel)", Tag(ChannelStateTestsTags.Wumbo), Tag("high-max-funding-size")) { f => @@ -254,6 +268,7 @@ class WaitForAcceptChannelStateSpec extends TestKitBaseClass with FixtureAnyFunS assert(accept.minimumDepth == 13) // with wumbo tag we use fundingSatoshis=5BTC bob2alice.forward(alice, accept) awaitCond(alice.stateName == WAIT_FOR_FUNDING_INTERNAL) + aliceOrigin.expectNoMessage() } test("recv AcceptChannel (upfront shutdown script)", Tag(ChannelStateTestsTags.OptionUpfrontShutdownScript)) { f => @@ -263,6 +278,7 @@ class WaitForAcceptChannelStateSpec extends TestKitBaseClass with FixtureAnyFunS bob2alice.forward(alice, accept) awaitCond(alice.stateName == WAIT_FOR_FUNDING_INTERNAL) assert(alice.stateData.asInstanceOf[DATA_WAIT_FOR_FUNDING_INTERNAL].remoteParams.shutdownScript == accept.upfrontShutdownScript_opt) + aliceOrigin.expectNoMessage() } test("recv AcceptChannel (empty upfront shutdown script)", Tag(ChannelStateTestsTags.OptionUpfrontShutdownScript)) { f => @@ -273,6 +289,7 @@ class WaitForAcceptChannelStateSpec extends TestKitBaseClass with FixtureAnyFunS bob2alice.forward(alice, accept1) awaitCond(alice.stateName == WAIT_FOR_FUNDING_INTERNAL) assert(alice.stateData.asInstanceOf[DATA_WAIT_FOR_FUNDING_INTERNAL].remoteParams.shutdownScript.isEmpty) + aliceOrigin.expectNoMessage() } test("recv AcceptChannel (invalid upfront shutdown script)", Tag(ChannelStateTestsTags.OptionUpfrontShutdownScript)) { f => @@ -281,12 +298,14 @@ class WaitForAcceptChannelStateSpec extends TestKitBaseClass with FixtureAnyFunS val accept1 = accept.copy(tlvStream = TlvStream(ChannelTlv.UpfrontShutdownScriptTlv(ByteVector.fromValidHex("deadbeef")))) bob2alice.forward(alice, accept1) awaitCond(alice.stateName == CLOSED) + aliceOrigin.expectMsgType[Status.Failure] } test("recv Error") { f => import f._ alice ! Error(ByteVector32.Zeroes, "oops") awaitCond(alice.stateName == CLOSED) + aliceOrigin.expectMsgType[Status.Failure] } test("recv CMD_CLOSE") { f => @@ -296,12 +315,21 @@ class WaitForAcceptChannelStateSpec extends TestKitBaseClass with FixtureAnyFunS alice ! c sender.expectMsg(RES_SUCCESS(c, ByteVector32.Zeroes)) awaitCond(alice.stateName == CLOSED) + aliceOrigin.expectMsgType[ChannelOpenResponse.ChannelClosed] + } + + test("recv INPUT_DISCONNECTED") { f => + import f._ + alice ! INPUT_DISCONNECTED + awaitCond(alice.stateName == CLOSED) + aliceOrigin.expectMsgType[Status.Failure] } test("recv TickChannelOpenTimeout") { f => import f._ alice ! TickChannelOpenTimeout awaitCond(alice.stateName == CLOSED) + aliceOrigin.expectMsgType[Status.Failure] } } diff --git a/eclair-core/src/test/scala/fr/acinq/eclair/channel/states/b/WaitForFundingCreatedInternalStateSpec.scala b/eclair-core/src/test/scala/fr/acinq/eclair/channel/states/b/WaitForFundingInternalStateSpec.scala similarity index 68% rename from eclair-core/src/test/scala/fr/acinq/eclair/channel/states/b/WaitForFundingCreatedInternalStateSpec.scala rename to eclair-core/src/test/scala/fr/acinq/eclair/channel/states/b/WaitForFundingInternalStateSpec.scala index 5f9a6bc04e..93e1684c8f 100644 --- a/eclair-core/src/test/scala/fr/acinq/eclair/channel/states/b/WaitForFundingCreatedInternalStateSpec.scala +++ b/eclair-core/src/test/scala/fr/acinq/eclair/channel/states/b/WaitForFundingInternalStateSpec.scala @@ -16,9 +16,11 @@ package fr.acinq.eclair.channel.states.b +import akka.actor.Status import akka.testkit.{TestFSMRef, TestProbe} import fr.acinq.bitcoin.ByteVector32 import fr.acinq.eclair.blockchain.NoOpOnChainWallet +import fr.acinq.eclair.channel.Channel.TickChannelOpenTimeout import fr.acinq.eclair.channel._ import fr.acinq.eclair.channel.states.ChannelStateTestsBase import fr.acinq.eclair.wire.protocol._ @@ -32,9 +34,9 @@ import scala.concurrent.duration._ * Created by PM on 05/07/2016. */ -class WaitForFundingCreatedInternalStateSpec extends TestKitBaseClass with FixtureAnyFunSuiteLike with ChannelStateTestsBase { +class WaitForFundingInternalStateSpec extends TestKitBaseClass with FixtureAnyFunSuiteLike with ChannelStateTestsBase { - case class FixtureParam(alice: TestFSMRef[ChannelState, ChannelData, Channel], alice2bob: TestProbe, bob2alice: TestProbe, alice2blockchain: TestProbe) + case class FixtureParam(alice: TestFSMRef[ChannelState, ChannelData, Channel], aliceOrigin: TestProbe, alice2bob: TestProbe, bob2alice: TestProbe, alice2blockchain: TestProbe) override def withFixture(test: OneArgTest): Outcome = { val setup = init(wallet = new NoOpOnChainWallet()) @@ -51,14 +53,22 @@ class WaitForFundingCreatedInternalStateSpec extends TestKitBaseClass with Fixtu bob2alice.expectMsgType[AcceptChannel] bob2alice.forward(alice) awaitCond(alice.stateName == WAIT_FOR_FUNDING_INTERNAL) - withFixture(test.toNoArgTest(FixtureParam(alice, alice2bob, bob2alice, alice2blockchain))) + withFixture(test.toNoArgTest(FixtureParam(alice, aliceOrigin, alice2bob, bob2alice, alice2blockchain))) } } + test("recv Status.Failure (wallet error)") { f => + import f._ + alice ! Status.Failure(new RuntimeException("insufficient funds")) + awaitCond(alice.stateName == CLOSED) + aliceOrigin.expectMsgType[Status.Failure] + } + test("recv Error") { f => import f._ alice ! Error(ByteVector32.Zeroes, "oops") awaitCond(alice.stateName == CLOSED) + aliceOrigin.expectMsgType[Status.Failure] } test("recv CMD_CLOSE") { f => @@ -68,6 +78,21 @@ class WaitForFundingCreatedInternalStateSpec extends TestKitBaseClass with Fixtu alice ! c sender.expectMsg(RES_SUCCESS(c, ByteVector32.Zeroes)) awaitCond(alice.stateName == CLOSED) + aliceOrigin.expectMsgType[ChannelOpenResponse.ChannelClosed] + } + + test("recv INPUT_DISCONNECTED") { f => + import f._ + alice ! INPUT_DISCONNECTED + awaitCond(alice.stateName == CLOSED) + aliceOrigin.expectMsgType[Status.Failure] + } + + test("recv TickChannelOpenTimeout") { f => + import f._ + alice ! TickChannelOpenTimeout + awaitCond(alice.stateName == CLOSED) + aliceOrigin.expectMsgType[Status.Failure] } } diff --git a/eclair-core/src/test/scala/fr/acinq/eclair/channel/states/b/WaitForFundingSignedStateSpec.scala b/eclair-core/src/test/scala/fr/acinq/eclair/channel/states/b/WaitForFundingSignedStateSpec.scala index ab94daf15c..1a8d538b9d 100644 --- a/eclair-core/src/test/scala/fr/acinq/eclair/channel/states/b/WaitForFundingSignedStateSpec.scala +++ b/eclair-core/src/test/scala/fr/acinq/eclair/channel/states/b/WaitForFundingSignedStateSpec.scala @@ -16,6 +16,7 @@ package fr.acinq.eclair.channel.states.b +import akka.actor.Status import akka.testkit.{TestFSMRef, TestProbe} import fr.acinq.bitcoin.{Btc, ByteVector32, ByteVector64, SatoshiLong} import fr.acinq.eclair.TestConstants.{Alice, Bob} @@ -38,7 +39,7 @@ import scala.concurrent.duration._ class WaitForFundingSignedStateSpec extends TestKitBaseClass with FixtureAnyFunSuiteLike with ChannelStateTestsBase { - case class FixtureParam(alice: TestFSMRef[ChannelState, ChannelData, Channel], alice2bob: TestProbe, bob2alice: TestProbe, alice2blockchain: TestProbe) + case class FixtureParam(alice: TestFSMRef[ChannelState, ChannelData, Channel], aliceOrigin: TestProbe, alice2bob: TestProbe, bob2alice: TestProbe, alice2blockchain: TestProbe) override def withFixture(test: OneArgTest): Outcome = { import com.softwaremill.quicklens._ @@ -73,7 +74,7 @@ class WaitForFundingSignedStateSpec extends TestKitBaseClass with FixtureAnyFunS alice2bob.forward(bob) alice2blockchain.expectMsgType[TxPublisher.SetChannelId] awaitCond(alice.stateName == WAIT_FOR_FUNDING_SIGNED) - withFixture(test.toNoArgTest(FixtureParam(alice, alice2bob, bob2alice, alice2blockchain))) + withFixture(test.toNoArgTest(FixtureParam(alice, aliceOrigin, alice2bob, bob2alice, alice2blockchain))) } } @@ -90,6 +91,7 @@ class WaitForFundingSignedStateSpec extends TestKitBaseClass with FixtureAnyFunS assert(txPublished.miningFee > 0.sat) val watchConfirmed = alice2blockchain.expectMsgType[WatchFundingConfirmed] assert(watchConfirmed.minDepth === Alice.nodeParams.minDepthBlocks) + aliceOrigin.expectMsgType[ChannelOpenResponse.ChannelOpened] } test("recv FundingSigned with valid signature (wumbo)", Tag(ChannelStateTestsTags.Wumbo)) { f => @@ -101,6 +103,7 @@ class WaitForFundingSignedStateSpec extends TestKitBaseClass with FixtureAnyFunS val watchConfirmed = alice2blockchain.expectMsgType[WatchFundingConfirmed] // when we are funder, we keep our regular min depth even for wumbo channels assert(watchConfirmed.minDepth === Alice.nodeParams.minDepthBlocks) + aliceOrigin.expectMsgType[ChannelOpenResponse.ChannelOpened] } test("recv FundingSigned with invalid signature") { f => @@ -109,6 +112,7 @@ class WaitForFundingSignedStateSpec extends TestKitBaseClass with FixtureAnyFunS alice ! FundingSigned(ByteVector32.Zeroes, ByteVector64.Zeroes) awaitCond(alice.stateName == CLOSED) alice2bob.expectMsgType[Error] + aliceOrigin.expectMsgType[Status.Failure] } test("recv CMD_CLOSE") { f => @@ -118,6 +122,7 @@ class WaitForFundingSignedStateSpec extends TestKitBaseClass with FixtureAnyFunS alice ! c sender.expectMsg(RES_SUCCESS(c, alice.stateData.asInstanceOf[DATA_WAIT_FOR_FUNDING_SIGNED].channelId)) awaitCond(alice.stateName == CLOSED) + aliceOrigin.expectMsgType[ChannelOpenResponse.ChannelClosed] } test("recv CMD_FORCECLOSE") { f => @@ -125,6 +130,7 @@ class WaitForFundingSignedStateSpec extends TestKitBaseClass with FixtureAnyFunS val sender = TestProbe() alice ! CMD_FORCECLOSE(sender.ref) awaitCond(alice.stateName == CLOSED) + aliceOrigin.expectMsgType[ChannelOpenResponse.ChannelClosed] } test("recv INPUT_DISCONNECTED") { f => @@ -134,12 +140,14 @@ class WaitForFundingSignedStateSpec extends TestKitBaseClass with FixtureAnyFunS alice ! INPUT_DISCONNECTED awaitCond(alice.stateName == CLOSED) assert(alice.underlyingActor.wallet.asInstanceOf[DummyOnChainWallet].rolledback.contains(fundingTx)) + aliceOrigin.expectMsgType[Status.Failure] } test("recv TickChannelOpenTimeout") { f => import f._ alice ! TickChannelOpenTimeout awaitCond(alice.stateName == CLOSED) + aliceOrigin.expectMsgType[Status.Failure] } } diff --git a/eclair-core/src/test/scala/fr/acinq/eclair/channel/states/c/WaitForFundingConfirmedStateSpec.scala b/eclair-core/src/test/scala/fr/acinq/eclair/channel/states/c/WaitForFundingConfirmedStateSpec.scala index 3f50da615d..1e90bd5283 100644 --- a/eclair-core/src/test/scala/fr/acinq/eclair/channel/states/c/WaitForFundingConfirmedStateSpec.scala +++ b/eclair-core/src/test/scala/fr/acinq/eclair/channel/states/c/WaitForFundingConfirmedStateSpec.scala @@ -26,7 +26,7 @@ import fr.acinq.eclair.channel.publish.TxPublisher import fr.acinq.eclair.channel.states.ChannelStateTestsBase import fr.acinq.eclair.transactions.Scripts.multiSig2of2 import fr.acinq.eclair.wire.protocol.{AcceptChannel, Error, FundingCreated, FundingLocked, FundingSigned, Init, OpenChannel} -import fr.acinq.eclair.{TestConstants, TestKitBaseClass, randomKey} +import fr.acinq.eclair.{TestConstants, TestKitBaseClass, TimestampSecond, randomKey} import org.scalatest.Outcome import org.scalatest.funsuite.FixtureAnyFunSuiteLike @@ -165,7 +165,7 @@ class WaitForFundingConfirmedStateSpec extends TestKitBaseClass with FixtureAnyF test("migrate waitingSince to waitingSinceBlocks") { f => import f._ // Before version 0.5.1, eclair used an absolute timestamp instead of a block height for funding timeouts. - val beforeMigration = bob.stateData.asInstanceOf[DATA_WAIT_FOR_FUNDING_CONFIRMED].copy(waitingSinceBlock = System.currentTimeMillis().milliseconds.toSeconds) + val beforeMigration = bob.stateData.asInstanceOf[DATA_WAIT_FOR_FUNDING_CONFIRMED].copy(waitingSinceBlock = TimestampSecond.now().toLong) bob.setState(WAIT_FOR_INIT_INTERNAL, Nothing) bob ! INPUT_RESTORED(beforeMigration) awaitCond(bob.stateName == OFFLINE) diff --git a/eclair-core/src/test/scala/fr/acinq/eclair/channel/states/h/ClosingStateSpec.scala b/eclair-core/src/test/scala/fr/acinq/eclair/channel/states/h/ClosingStateSpec.scala index 4035b968f1..5ad850e77f 100644 --- a/eclair-core/src/test/scala/fr/acinq/eclair/channel/states/h/ClosingStateSpec.scala +++ b/eclair-core/src/test/scala/fr/acinq/eclair/channel/states/h/ClosingStateSpec.scala @@ -30,7 +30,7 @@ import fr.acinq.eclair.payment.relay.Relayer._ import fr.acinq.eclair.transactions.Transactions.{AnchorOutputsCommitmentFormat, DefaultCommitmentFormat, HtlcSuccessTx, HtlcTimeoutTx, ZeroFeeHtlcTxAnchorOutputsCommitmentFormat} import fr.acinq.eclair.transactions.{Scripts, Transactions} import fr.acinq.eclair.wire.protocol._ -import fr.acinq.eclair.{CltvExpiry, Features, MilliSatoshiLong, TestConstants, TestKitBaseClass, randomBytes32, randomKey} +import fr.acinq.eclair.{CltvExpiry, Features, MilliSatoshiLong, TestConstants, TestKitBaseClass, TimestampSecond, randomBytes32, randomKey} import org.scalatest.funsuite.FixtureAnyFunSuiteLike import org.scalatest.{Outcome, Tag} import scodec.bits.ByteVector @@ -159,7 +159,7 @@ class ClosingStateSpec extends TestKitBaseClass with FixtureAnyFunSuiteLike with alice2blockchain.expectMsgType[WatchTxConfirmed] // claim-main-delayed // test starts here - alice ! GetTxWithMetaResponse(fundingTx.txid, Some(fundingTx), System.currentTimeMillis.milliseconds.toSeconds) + alice ! GetTxWithMetaResponse(fundingTx.txid, Some(fundingTx), TimestampSecond.now()) alice2bob.expectNoMessage(200 millis) alice2blockchain.expectNoMessage(200 millis) assert(alice.stateName == CLOSING) // the above expectNoMsg will make us wait, so this checks that we are still in CLOSING @@ -178,7 +178,7 @@ class ClosingStateSpec extends TestKitBaseClass with FixtureAnyFunSuiteLike with alice2blockchain.expectMsgType[WatchTxConfirmed] // claim-main-delayed // test starts here - alice ! GetTxWithMetaResponse(fundingTx.txid, None, System.currentTimeMillis.milliseconds.toSeconds) + alice ! GetTxWithMetaResponse(fundingTx.txid, None, TimestampSecond.now()) alice2bob.expectNoMessage(200 millis) assert(alice2blockchain.expectMsgType[PublishRawTx].tx === fundingTx) // we republish the funding tx assert(alice.stateName == CLOSING) // the above expectNoMsg will make us wait, so this checks that we are still in CLOSING @@ -197,7 +197,7 @@ class ClosingStateSpec extends TestKitBaseClass with FixtureAnyFunSuiteLike with bob2blockchain.expectMsgType[WatchTxConfirmed] // claim-main-delayed // test starts here - bob ! GetTxWithMetaResponse(fundingTx.txid, Some(fundingTx), System.currentTimeMillis.milliseconds.toSeconds) + bob ! GetTxWithMetaResponse(fundingTx.txid, Some(fundingTx), TimestampSecond.now()) bob2alice.expectNoMessage(200 millis) bob2blockchain.expectNoMessage(200 millis) assert(bob.stateName == CLOSING) // the above expectNoMsg will make us wait, so this checks that we are still in CLOSING @@ -216,7 +216,7 @@ class ClosingStateSpec extends TestKitBaseClass with FixtureAnyFunSuiteLike with bob2blockchain.expectMsgType[WatchTxConfirmed] // claim-main-delayed // test starts here - bob ! GetTxWithMetaResponse(fundingTx.txid, None, System.currentTimeMillis.milliseconds.toSeconds) + bob ! GetTxWithMetaResponse(fundingTx.txid, None, TimestampSecond.now()) bob2alice.expectNoMessage(200 millis) bob2blockchain.expectNoMessage(200 millis) assert(bob.stateName == CLOSING) // the above expectNoMsg will make us wait, so this checks that we are still in CLOSING @@ -236,7 +236,7 @@ class ClosingStateSpec extends TestKitBaseClass with FixtureAnyFunSuiteLike with // test starts here bob.setState(stateData = bob.stateData.asInstanceOf[DATA_CLOSING].copy(waitingSinceBlock = bob.underlyingActor.nodeParams.currentBlockHeight - Channel.FUNDING_TIMEOUT_FUNDEE - 1)) - bob ! GetTxWithMetaResponse(fundingTx.txid, None, System.currentTimeMillis.milliseconds.toSeconds) + bob ! GetTxWithMetaResponse(fundingTx.txid, None, TimestampSecond.now()) bob2alice.expectMsgType[Error] bob2blockchain.expectNoMessage(200 millis) assert(bob.stateName == CLOSED) diff --git a/eclair-core/src/test/scala/fr/acinq/eclair/crypto/SphinxSpec.scala b/eclair-core/src/test/scala/fr/acinq/eclair/crypto/SphinxSpec.scala index fbf1d27007..7818d0eb5b 100644 --- a/eclair-core/src/test/scala/fr/acinq/eclair/crypto/SphinxSpec.scala +++ b/eclair-core/src/test/scala/fr/acinq/eclair/crypto/SphinxSpec.scala @@ -18,9 +18,9 @@ package fr.acinq.eclair.crypto import fr.acinq.bitcoin.ByteVector32 import fr.acinq.bitcoin.Crypto.{PrivateKey, PublicKey} -import fr.acinq.eclair.UInt64 import fr.acinq.eclair.wire.protocol import fr.acinq.eclair.wire.protocol._ +import fr.acinq.eclair.{CltvExpiry, MilliSatoshi, ShortChannelId, UInt64, randomKey} import org.scalatest.funsuite.AnyFunSuite import scodec.bits._ @@ -363,6 +363,157 @@ class SphinxSpec extends AnyFunSuite { assert(failure === InvalidRealm) } } + + test("create blinded route (reference test vector)") { + val sessionKey = PrivateKey(hex"0101010101010101010101010101010101010101010101010101010101010101") + val blindedRoute = RouteBlinding.create(sessionKey, publicKeys, routeBlindingPayloads) + assert(blindedRoute.introductionNode.publicKey === publicKeys(0)) + assert(blindedRoute.introductionNode.blindingEphemeralKey === PublicKey(hex"031b84c5567b126440995d3ed5aaba0565d71e1834604819ff9c17f5e9d5dd078f")) + assert(blindedRoute.introductionNode.encryptedPayload === hex"a245b767bd52520bdf8179b2dc681d1a36c2ededaf59429dfc4bea342fa460c9") + assert(blindedRoute.nodeIds === Seq( + publicKeys(0), + PublicKey(hex"022b09d77fb3374ee3ed9d2153e15e9962944ad1690327cbb0a9acb7d90f168763"), + PublicKey(hex"03d9f889364dc5a173460a2a6cc565b4ca78931792115dd6ef82c0e18ced837372"), + PublicKey(hex"03bfddd2253b42fe12edd37f9071a3883830ed61a4bc347eeac63421629cf032b5"), + PublicKey(hex"03a8588bc4a0a2f0d2fb8d5c0f8d062fb4d78bfba24a85d0ddeb4fd35dd3b34110"), + )) + assert(blindedRoute.blindedNodes.map(_.blindedPublicKey) === Seq( + PublicKey(hex"022b09d77fb3374ee3ed9d2153e15e9962944ad1690327cbb0a9acb7d90f168763"), + PublicKey(hex"03d9f889364dc5a173460a2a6cc565b4ca78931792115dd6ef82c0e18ced837372"), + PublicKey(hex"03bfddd2253b42fe12edd37f9071a3883830ed61a4bc347eeac63421629cf032b5"), + PublicKey(hex"03a8588bc4a0a2f0d2fb8d5c0f8d062fb4d78bfba24a85d0ddeb4fd35dd3b34110"), + )) + assert(blindedRoute.blindingEphemeralKeys === blindedRoute.introductionNode.blindingEphemeralKey +: blindedRoute.blindedNodes.map(_.blindingEphemeralKey)) + assert(blindedRoute.blindedNodes.map(_.blindingEphemeralKey) === Seq( + PublicKey(hex"035cb4c003d58e16cc9207270b3596c2be3309eca64c36b208c946bbb599bfcad0"), + PublicKey(hex"02e105bc01a7af07074a1b0b1d9a112a1d89c6cd87cc4e2b6ba3a824731d9508bd"), + PublicKey(hex"0349164db5398925ef234002e62d2834da115b8eafc73436fab98ed12266e797cc"), + PublicKey(hex"020a6d1951916adcac22125063f62c35b3686f36e5db2f77073f3d35b19c7a118a"), + )) + assert(blindedRoute.encryptedPayloads === blindedRoute.introductionNode.encryptedPayload +: blindedRoute.blindedNodes.map(_.encryptedPayload)) + assert(blindedRoute.blindedNodes.map(_.encryptedPayload) === Seq( + hex"38748f94ead7de2a54fc43e8bb927bfc377dda7ed5a2e36b327b739c3c82a602e43e07e378f17cd46ee32d987eb8b6d03b3403acb095bd2868f640b92ea1", + hex"a5ddddd448f15208452f4d65da0d53679e9652c8f9c9882d795388a492b4060afb5f2f556e36aed51d089f60f7c94f714b34cb30f1dac0c17f3855a827cb", + hex"7ead52884542d180e76fec6ae2d137b6b4c771dc0d41390e992839dea0f4fcefb4a31589125e2ba535d0dc3bf1bc94e6c9039323579547921686d3b54c22", + hex"4642ce64cbf146ffd73299501d65c56052af4acd681d9d0882728c6f399ace90392b694d5e347612dc1417f1b31e5f5dfdfb4ca5e8a24a681898ec5784f7", + )) + + // The introduction point can decrypt its encrypted payload and obtain the next ephemeral public key. + val Success((payload0, ephKey1)) = RouteBlinding.decryptPayload(privKeys(0), blindedRoute.blindingEphemeralKeys(0), blindedRoute.encryptedPayloads(0)) + assert(payload0 === routeBlindingPayloads(0)) + assert(ephKey1 === blindedRoute.blindingEphemeralKeys(1)) + + // The next node can derive the private key used to unwrap the onion and decrypt its encrypted payload. + assert(RouteBlinding.derivePrivateKey(privKeys(1), ephKey1).publicKey === blindedRoute.nodeIds(1)) + val Success((payload1, ephKey2)) = RouteBlinding.decryptPayload(privKeys(1), ephKey1, blindedRoute.encryptedPayloads(1)) + assert(payload1 === routeBlindingPayloads(1)) + assert(ephKey2 === blindedRoute.blindingEphemeralKeys(2)) + + // The next node can derive the private key used to unwrap the onion and decrypt its encrypted payload. + assert(RouteBlinding.derivePrivateKey(privKeys(2), ephKey2).publicKey === blindedRoute.nodeIds(2)) + val Success((payload2, ephKey3)) = RouteBlinding.decryptPayload(privKeys(2), ephKey2, blindedRoute.encryptedPayloads(2)) + assert(payload2 === routeBlindingPayloads(2)) + assert(ephKey3 === blindedRoute.blindingEphemeralKeys(3)) + + // The next node can derive the private key used to unwrap the onion and decrypt its encrypted payload. + assert(RouteBlinding.derivePrivateKey(privKeys(3), ephKey3).publicKey === blindedRoute.nodeIds(3)) + val Success((payload3, ephKey4)) = RouteBlinding.decryptPayload(privKeys(3), ephKey3, blindedRoute.encryptedPayloads(3)) + assert(payload3 === routeBlindingPayloads(3)) + assert(ephKey4 === blindedRoute.blindingEphemeralKeys(4)) + + // The last node can derive the private key used to unwrap the onion and decrypt its encrypted payload. + assert(RouteBlinding.derivePrivateKey(privKeys(4), ephKey4).publicKey === blindedRoute.nodeIds(4)) + val Success((payload4, _)) = RouteBlinding.decryptPayload(privKeys(4), ephKey4, blindedRoute.encryptedPayloads(4)) + assert(payload4 === routeBlindingPayloads(4)) + } + + test("invalid blinded route") { + val encryptedPayloads = RouteBlinding.create(sessionKey, publicKeys, routeBlindingPayloads).encryptedPayloads + // Invalid node private key: + val ephKey0 = sessionKey.publicKey + assert(RouteBlinding.decryptPayload(privKeys(1), ephKey0, encryptedPayloads(0)).isFailure) + // Invalid unblinding ephemeral key: + assert(RouteBlinding.decryptPayload(privKeys(0), randomKey().publicKey, encryptedPayloads(0)).isFailure) + // Invalid encrypted payload: + assert(RouteBlinding.decryptPayload(privKeys(0), ephKey0, encryptedPayloads(1)).isFailure) + } + + test("create packet to blinded route (reference test vector)") { + // The recipient creates a blinded route containing 3 hops. + val (blindedRoute, blindingEphemeralKey0) = { + val recipientSessionKey = PrivateKey(hex"0101010101010101010101010101010101010101010101010101010101010101") + (RouteBlinding.create(recipientSessionKey, publicKeys.drop(2), routeBlindingPayloads.drop(2)), recipientSessionKey.publicKey) + } + + // The sender obtains this information (e.g. from a Bolt11 invoice) and prepends two normal hops to reach the introduction node. + val nodeIds = publicKeys.take(2) ++ blindedRoute.nodeIds + assert(blindedRoute.encryptedPayloads === Seq( + hex"192256e1c0b289eee9a509bf94455c111838cab3f47010aeedc1367aa77cf44743c6cf49726ddb96b426cdbf6767e462f940638879805b04dd97d3bb823f", + hex"38c490e3f4f29cc7af8620002fb497591e043377d19fdf4c9cc913600a4d7ae2842e538181790fe7309c85c845b360eab73c8eaa1068866d1a42fb3afb54", + hex"d2706bb65ac8e1c2a319ba53a371d97dc237132b22ce4f7439983545e37164d792dc6925a3c7cde855ac824871c2417052efa103e5b53ec49a2bb4ab7cfc", + )) + val payloads = Seq( + // The sender sends normal onion payloads to the first two hops. + TlvStream[OnionTlv](OnionTlv.AmountToForward(MilliSatoshi(500)), OnionTlv.OutgoingCltv(CltvExpiry(1000)), OnionTlv.OutgoingChannelId(ShortChannelId(10))), + TlvStream[OnionTlv](OnionTlv.AmountToForward(MilliSatoshi(450)), OnionTlv.OutgoingCltv(CltvExpiry(900)), OnionTlv.OutgoingChannelId(ShortChannelId(15))), + // The sender includes the blinding key and the first encrypted recipient data in the introduction node's payload. + TlvStream[OnionTlv](OnionTlv.AmountToForward(MilliSatoshi(400)), OnionTlv.OutgoingCltv(CltvExpiry(860)), OnionTlv.BlindingPoint(blindingEphemeralKey0), OnionTlv.EncryptedRecipientData(blindedRoute.encryptedPayloads(0))), + // The sender includes the correct encrypted recipient data in each blinded node's payload. + TlvStream[OnionTlv](OnionTlv.AmountToForward(MilliSatoshi(250)), OnionTlv.OutgoingCltv(CltvExpiry(750)), OnionTlv.EncryptedRecipientData(blindedRoute.encryptedPayloads(1))), + TlvStream[OnionTlv](OnionTlv.AmountToForward(MilliSatoshi(250)), OnionTlv.OutgoingCltv(CltvExpiry(750)), OnionTlv.EncryptedRecipientData(blindedRoute.encryptedPayloads(2))), + ).map(tlvs => OnionCodecs.tlvPerHopPayloadCodec.encode(tlvs).require.bytes) + + val senderSessionKey = PrivateKey(hex"0202020202020202020202020202020202020202020202020202020202020202") + val PacketAndSecrets(onion, sharedSecrets) = PaymentPacket.create(senderSessionKey, nodeIds, payloads, associatedData) + assert(serializePaymentOnion(onion) == hex"00024d4b6cd1361032ca9bd2aeb9d900aa4d45d9ead80ac9423374c451a7254d07666a78b866fda75eb6a991f9815c47624f46324be8a0d8cbbebf25889553edd83cbc0381ce1a5250b59f956b78e1f28ba64f1e8a77aa103ebb2a5f054f3d9271e7bf2f0cbe7c4dbd538fe2977817cb18a3dc52a3e8651a39e38167df0261b67f1d371cb24b39aa2bd3a99be0f16ac186e5d5cf2790d4f60f2e505c64ffa041f74d08b25b7b97e08daeb35df7fd9caa432b0187d3c2dd7f13de35401f79c480404da7e54739f8905fb30b0811c6579e1064182af0ca67e9eb0144796bf347135e90f17126982bc10b91c04e57e736d3accfc7e0afd5945cf8c491c1fa2faa5c3eec70568b6c2514d203ad277f5070d83f30474769d82d26bad5fab11fec659774e30fd9dea7d51f58845510b6765ad8276556ea7f8e8ba10c93991ca404b84b6e30c46cdbd4d5879fe6d6f2df60b96ef4640458be40171b76f33468b0b122b17d0dc165cb60feb41bebf98bcbefe963278c29c7f5885f3d47051888f9ca81ce486b7102572d79109d861f1da8d5a2e810f31b8373ccfc5370169a72c2fb9a10a14be97e2ac55cf27623779e27d48aae14f564b899b260ce69bcd06db4e04044eb400d894d6e6f381abb48c350588a069a72a20a9b686db068fbe58b02909de83d08a8f325a2138cca22649a9944e745f1bf146401493e53e1bf4c5a979922e2a1990f9897376c3597e81c71420f8dcccd54adb11d3483a7cd8a9ae59f3b7f97aabe16805f721403ae88562064e29a7d8c6f39658e15771ca6905aaf31d8b17271fe046033024716c1c12df9a042ab56aa3147005e0f6c5430a7b15bf3db69f705e47735bd4c6815c080b822e70a9177043e4ccb1b972b645777f614f7ebd49f18b354c06cfab44fa67e487d536a77555ba1953b89e53499203d268aefb712520b8f1a5b734ce16ece6fe3250c44e571e86a8f5877a6297aa0b36a84d7a1df571878224a2d77af58652e2e83211a2ce09845181f43ff8b9edf58ee3df045bf91afed92d927b3e4cfc3887255d14c21282ef58f5d68b6d9a872125485be0345cfd22973d75717c2c07f11962971c2b28c0f3dab672d59d5c8ec0f0746718f2966ee1444c53537ae9280dce2cf945d7ce59e2000d6fb8f985fb107591492eaa9e0b24583c445a6c959fe309c4d814ec31f5ab10bae2cf12c3fceb34e724a17c34ed2f04f133d604b564db0f56a463b7a9f453d8ea30ad2c11237d47c8161f8e532e245f4d129ed96afa78c7f9c18ee624e9a593639d911b00ea24923455022ef852e07d0068e27c93b9fa5c65facd2a298b79118405a037ddcc97c8f9281088eddfcaa3a10b810c92d961e3eff6eaf45a81da8593a2252bff0bd41253fe4f35b233553329be00babbf1c93b2a716f4ae6f52cc99018b2d45bc8a0d6e9679b16295a8a43e4d38ebfc31a8464115c31af4588579f6fcf065e6a6362f9237681333fa72a3e881ee5b65abeac5b00555813bb6de0e2b37978e902642cce7e062fa70d042ea668480edddf1a31c91faa67187425a025b938112be61d36c637d8e75e740d2ad542507df80bc2dcb48d5125571f360ee142e300833bd9100e564e84b1d9ef8be88dbac834991d2ae5e5fc217cbbe341a4a58b5e61e0a5a2ea6144607b162fb593c8d73f2d8baf7314f966b06dd2723a43d83645769f45645777ac259911115c29d5459e9001b9622486c533a99c0cc8ef566a96457643d0d3a5ba1ec45037a4dc78906ae3def722eb1b21dc9b0a912b372ed28570ca044f4c73fe7c40799b8ad6afc2d3ab2eee14a5ca0855cf452c12bf5ed4b46f1294406876eec5c48aec2713b5e6f24a44472c5ea6f1b83337e960e4a811a4547484e9f42e3c8507c1070821900b313f2b6a45632db9647cdaf595165afc5ca") + + // The first two hops can decrypt the onion as usual. + val Right(DecryptedPacket(payload0, nextPacket0, sharedSecret0)) = PaymentPacket.peel(privKeys(0), associatedData, onion) + val Right(DecryptedPacket(payload1, nextPacket1, sharedSecret1)) = PaymentPacket.peel(privKeys(1), associatedData, nextPacket0) + + // The third hop is the introduction node. + // It can decrypt the onion as usual, but the payload doesn't contain a shortChannelId or a nodeId to forward to. + // However it contains a blinding point and encrypted data, which it can decrypt to discover the next node. + val Right(DecryptedPacket(payload2, nextPacket2, sharedSecret2)) = PaymentPacket.peel(privKeys(2), associatedData, nextPacket1) + val tlvs2 = OnionCodecs.tlvPerHopPayloadCodec.decode(payload2.bits).require.value + assert(tlvs2.get[OnionTlv.BlindingPoint].map(_.publicKey) === Some(blindingEphemeralKey0)) + assert(tlvs2.get[OnionTlv.EncryptedRecipientData].nonEmpty) + val Success((recipientTlvs2, blindingEphemeralKey1)) = EncryptedRecipientDataCodecs.decode(privKeys(2), blindingEphemeralKey0, tlvs2.get[OnionTlv.EncryptedRecipientData].get.data) + assert(recipientTlvs2.get[EncryptedRecipientDataTlv.OutgoingChannelId].map(_.shortChannelId) === Some(ShortChannelId(1105))) + assert(recipientTlvs2.get[EncryptedRecipientDataTlv.OutgoingNodeId].map(_.nodeId) === Some(publicKeys(3))) + + // The fourth hop is a blinded hop. + // It receives the blinding key from the previous node (e.g. in a tlv field in update_add_htlc) which it can use to + // derive the private key corresponding to its blinded node ID and decrypt the onion. + // The payload doesn't contain a shortChannelId or a nodeId to forward to, but the encrypted data does. + val blindedPrivKey3 = RouteBlinding.derivePrivateKey(privKeys(3), blindingEphemeralKey1) + val Right(DecryptedPacket(payload3, nextPacket3, sharedSecret3)) = PaymentPacket.peel(blindedPrivKey3, associatedData, nextPacket2) + val tlvs3 = OnionCodecs.tlvPerHopPayloadCodec.decode(payload3.bits).require.value + assert(tlvs3.get[OnionTlv.EncryptedRecipientData].nonEmpty) + val Success((recipientTlvs3, blindingEphemeralKey2)) = EncryptedRecipientDataCodecs.decode(privKeys(3), blindingEphemeralKey1, tlvs3.get[OnionTlv.EncryptedRecipientData].get.data) + assert(recipientTlvs3.get[EncryptedRecipientDataTlv.OutgoingNodeId].map(_.nodeId) === Some(publicKeys(4))) + + // The fifth hop is the blinded recipient. + // It receives the blinding key from the previous node (e.g. in a tlv field in update_add_htlc) which it can use to + // derive the private key corresponding to its blinded node ID and decrypt the onion. + val blindedPrivKey4 = RouteBlinding.derivePrivateKey(privKeys(4), blindingEphemeralKey2) + val Right(DecryptedPacket(payload4, nextPacket4, sharedSecret4)) = PaymentPacket.peel(blindedPrivKey4, associatedData, nextPacket3) + val tlvs4 = OnionCodecs.tlvPerHopPayloadCodec.decode(payload4.bits).require.value + assert(tlvs4.get[OnionTlv.EncryptedRecipientData].nonEmpty) + val Success((recipientTlvs4, _)) = EncryptedRecipientDataCodecs.decode(privKeys(4), blindingEphemeralKey2, tlvs4.get[OnionTlv.EncryptedRecipientData].get.data) + assert(recipientTlvs4.get[EncryptedRecipientDataTlv.RecipientSecret].map(_.data) === Some(associatedData.bytes)) + + assert(Seq(payload0, payload1, payload2, payload3, payload4) == payloads) + assert(Seq(sharedSecret0, sharedSecret1, sharedSecret2, sharedSecret3, sharedSecret4) == sharedSecrets.map(_._1)) + + val packets = Seq(nextPacket0, nextPacket1, nextPacket2, nextPacket3, nextPacket4) + assert(packets(0).hmac == ByteVector32(hex"9890f3263d32bccb63a0e1dd1b0db6bf63fe418a0f572ed299d98d343619f098")) + assert(packets(1).hmac == ByteVector32(hex"aee30ae716b93ab6782b907b4cc996cfa3e2219d0a32a677fdeff41071fc86f3")) + assert(packets(2).hmac == ByteVector32(hex"9835e7410219117010779ec0852af38449bfa6a1a610c306cdc7eed8717ecdc9")) + assert(packets(3).hmac == ByteVector32(hex"c02a8c2e221e03f2e2d15266d5838c7f2fedfc1c4467a49d362896dcead60750")) + assert(packets(4).hmac == ByteVector32(hex"0000000000000000000000000000000000000000000000000000000000000000")) + } + } object SphinxSpec { @@ -436,5 +587,14 @@ object SphinxSpec { hex"23 f8 21 02eec7245d6b7d2ccb30380bfbe2a3648cd7a942653f5aa340edcea1f283686619" ) + // This test vector uses route blinding payloads (encrypted_recipient_data). + val routeBlindingPayloads = Seq( + hex"0f 0208000000000000002a 3903123456", + hex"2d 011900000000000000000000000000000000000000000000000000 02080000000000000231 fdffff0206c1 3b00", + hex"2d 02080000000000000451 0421032c0b7cf95324a07d05398b240174dc0c2be444d96b159aa6c7f7b1e668680991", + hex"2d 01080000000000000000 042102edabbd16b41c8371b92ef2f04c1185b4f03b6dcd52ba9b78d9d7c89c8f221145", + hex"2d 0109000000000000000000 06204242424242424242424242424242424242424242424242424242424242424242", + ) + val associatedData = ByteVector32(hex"4242424242424242424242424242424242424242424242424242424242424242") } diff --git a/eclair-core/src/test/scala/fr/acinq/eclair/db/AuditDbSpec.scala b/eclair-core/src/test/scala/fr/acinq/eclair/db/AuditDbSpec.scala index 0cb3c3b240..d61f98d641 100644 --- a/eclair-core/src/test/scala/fr/acinq/eclair/db/AuditDbSpec.scala +++ b/eclair-core/src/test/scala/fr/acinq/eclair/db/AuditDbSpec.scala @@ -71,17 +71,17 @@ class AuditDbSpec extends AnyFunSuite { val e4a = TransactionPublished(randomBytes32(), randomKey().publicKey, Transaction(0, Seq.empty, Seq.empty, 0), 42 sat, "mutual") val e4b = TransactionConfirmed(e4a.channelId, e4a.remoteNodeId, e4a.tx) val e4c = TransactionConfirmed(randomBytes32(), randomKey().publicKey, Transaction(2, Nil, TxOut(500 sat, hex"1234") :: Nil, 0)) - val pp5a = PaymentSent.PartialPayment(UUID.randomUUID(), 42000 msat, 1000 msat, randomBytes32(), None, timestamp = 0) - val pp5b = PaymentSent.PartialPayment(UUID.randomUUID(), 42100 msat, 900 msat, randomBytes32(), None, timestamp = 1) + val pp5a = PaymentSent.PartialPayment(UUID.randomUUID(), 42000 msat, 1000 msat, randomBytes32(), None, timestamp = 0 unixms) + val pp5b = PaymentSent.PartialPayment(UUID.randomUUID(), 42100 msat, 900 msat, randomBytes32(), None, timestamp = 1 unixms) val e5 = PaymentSent(UUID.randomUUID(), randomBytes32(), randomBytes32(), 84100 msat, randomKey().publicKey, pp5a :: pp5b :: Nil) - val pp6 = PaymentSent.PartialPayment(UUID.randomUUID(), 42000 msat, 1000 msat, randomBytes32(), None, timestamp = (System.currentTimeMillis.milliseconds + 10.minutes).toMillis) + val pp6 = PaymentSent.PartialPayment(UUID.randomUUID(), 42000 msat, 1000 msat, randomBytes32(), None, timestamp = TimestampMilli.now() + 10.minutes) val e6 = PaymentSent(UUID.randomUUID(), randomBytes32(), randomBytes32(), 42000 msat, randomKey().publicKey, pp6 :: Nil) val e7 = ChannelEvent(randomBytes32(), randomKey().publicKey, 456123000 sat, isFunder = true, isPrivate = false, ChannelEvent.EventType.Closed(MutualClose(null))) val e8 = ChannelErrorOccurred(null, randomBytes32(), randomKey().publicKey, null, LocalError(new RuntimeException("oops")), isFatal = true) val e9 = ChannelErrorOccurred(null, randomBytes32(), randomKey().publicKey, null, RemoteError(Error(randomBytes32(), "remote oops")), isFatal = true) val e10 = TrampolinePaymentRelayed(randomBytes32(), Seq(PaymentRelayed.Part(20000 msat, randomBytes32()), PaymentRelayed.Part(22000 msat, randomBytes32())), Seq(PaymentRelayed.Part(10000 msat, randomBytes32()), PaymentRelayed.Part(12000 msat, randomBytes32()), PaymentRelayed.Part(15000 msat, randomBytes32())), randomKey().publicKey, 30000 msat) val multiPartPaymentHash = randomBytes32() - val now = System.currentTimeMillis + val now = TimestampMilli.now() val e11 = ChannelPaymentRelayed(13000 msat, 11000 msat, multiPartPaymentHash, randomBytes32(), randomBytes32(), now) val e12 = ChannelPaymentRelayed(15000 msat, 12500 msat, multiPartPaymentHash, randomBytes32(), randomBytes32(), now) @@ -100,12 +100,12 @@ class AuditDbSpec extends AnyFunSuite { db.add(e11) db.add(e12) - assert(db.listSent(from = 0L, to = (System.currentTimeMillis.milliseconds + 15.minute).toMillis).toSet === Set(e1, e5, e6)) - assert(db.listSent(from = 100000L, to = (System.currentTimeMillis.milliseconds + 1.minute).toMillis).toList === List(e1)) - assert(db.listReceived(from = 0L, to = (System.currentTimeMillis.milliseconds + 1.minute).toMillis).toList === List(e2)) - assert(db.listRelayed(from = 0L, to = (System.currentTimeMillis.milliseconds + 1.minute).toMillis).toList === List(e3, e10, e11, e12)) - assert(db.listNetworkFees(from = 0L, to = (System.currentTimeMillis.milliseconds + 1.minute).toMillis).size === 1) - assert(db.listNetworkFees(from = 0L, to = (System.currentTimeMillis.milliseconds + 1.minute).toMillis).head.txType === "mutual") + assert(db.listSent(from = TimestampMilli(0L), to = TimestampMilli.now() + 15.minute).toSet === Set(e1, e5, e6)) + assert(db.listSent(from = TimestampMilli(100000L), to = TimestampMilli.now() + 1.minute).toList === List(e1)) + assert(db.listReceived(from = TimestampMilli(0L), to = TimestampMilli.now() + 1.minute).toList === List(e2)) + assert(db.listRelayed(from = TimestampMilli(0L), to = TimestampMilli.now() + 1.minute).toList === List(e3, e10, e11, e12)) + assert(db.listNetworkFees(from = TimestampMilli(0L), to = TimestampMilli.now() + 1.minute).size === 1) + assert(db.listNetworkFees(from = TimestampMilli(0L), to = TimestampMilli.now() + 1.minute).head.txType === "mutual") } } @@ -147,7 +147,7 @@ class AuditDbSpec extends AnyFunSuite { db.add(TransactionConfirmed(c4, n4, Transaction(0, Seq.empty, Seq(TxOut(2500 sat, hex"ffffff")), 0))) // doesn't match a published tx // NB: we only count a relay fee for the outgoing channel, no the incoming one. - assert(db.stats(0, System.currentTimeMillis + 1).toSet === Set( + assert(db.stats(0 unixms, TimestampMilli.now() + 1.milli).toSet === Set( Stats(channelId = c1, direction = "IN", avgPaymentAmount = 0 sat, paymentCount = 0, relayFee = 0 sat, networkFee = 0 sat), Stats(channelId = c1, direction = "OUT", avgPaymentAmount = 42 sat, paymentCount = 3, relayFee = 4 sat, networkFee = 0 sat), Stats(channelId = c2, direction = "IN", avgPaymentAmount = 0 sat, paymentCount = 0, relayFee = 0 sat, networkFee = 500 sat), @@ -193,9 +193,9 @@ class AuditDbSpec extends AnyFunSuite { } }) // Test starts here. - val start = System.currentTimeMillis - assert(db.stats(0, start + 1).nonEmpty) - val end = System.currentTimeMillis + val start = TimestampMilli.now() + assert(db.stats(0 unixms, start + 1.milli).nonEmpty) + val end = TimestampMilli.now() fail(s"took ${end - start}ms") } } @@ -240,7 +240,7 @@ class AuditDbSpec extends AnyFunSuite { statement.setBytes(3, ps.paymentHash.toArray) statement.setBytes(4, ps.paymentPreimage.toArray) statement.setBytes(5, ps.parts.head.toChannelId.toArray) - statement.setLong(6, ps.timestamp) + statement.setLong(6, ps.timestamp.toLong) statement.executeUpdate() } }, @@ -248,7 +248,7 @@ class AuditDbSpec extends AnyFunSuite { targetVersion = SqliteAuditDb.CURRENT_VERSION, postCheck = connection => { // existing rows in the 'sent' table will use id=00000000-0000-0000-0000-000000000000 as default - assert(dbs.audit.listSent(0, (System.currentTimeMillis.milliseconds + 1.minute).toMillis) === Seq(ps.copy(id = ZERO_UUID, parts = Seq(ps.parts.head.copy(id = ZERO_UUID))))) + assert(dbs.audit.listSent(0 unixms, TimestampMilli.now() + 1.minute) === Seq(ps.copy(id = ZERO_UUID, parts = Seq(ps.parts.head.copy(id = ZERO_UUID))))) val postMigrationDb = new SqliteAuditDb(connection) @@ -262,7 +262,7 @@ class AuditDbSpec extends AnyFunSuite { // the old record will have the UNKNOWN_UUID but the new ones will have their actual id val expected = Seq(ps.copy(id = ZERO_UUID, parts = Seq(ps.parts.head.copy(id = ZERO_UUID))), ps1) - assert(postMigrationDb.listSent(0, (System.currentTimeMillis.milliseconds + 1.minute).toMillis) === expected) + assert(postMigrationDb.listSent(0 unixms, TimestampMilli.now() + 1.minute) === expected) } ) } @@ -317,12 +317,12 @@ class AuditDbSpec extends AnyFunSuite { val dbs = TestSqliteDatabases() - val pp1 = PaymentSent.PartialPayment(UUID.randomUUID(), 500 msat, 10 msat, randomBytes32(), None, 100) - val pp2 = PaymentSent.PartialPayment(UUID.randomUUID(), 600 msat, 5 msat, randomBytes32(), None, 110) + val pp1 = PaymentSent.PartialPayment(UUID.randomUUID(), 500 msat, 10 msat, randomBytes32(), None, 100 unixms) + val pp2 = PaymentSent.PartialPayment(UUID.randomUUID(), 600 msat, 5 msat, randomBytes32(), None, 110 unixms) val ps1 = PaymentSent(UUID.randomUUID(), randomBytes32(), randomBytes32(), 1100 msat, PrivateKey(ByteVector32.One).publicKey, pp1 :: pp2 :: Nil) - val relayed1 = ChannelPaymentRelayed(600 msat, 500 msat, randomBytes32(), randomBytes32(), randomBytes32(), 105) - val relayed2 = ChannelPaymentRelayed(650 msat, 500 msat, randomBytes32(), randomBytes32(), randomBytes32(), 115) + val relayed1 = ChannelPaymentRelayed(600 msat, 500 msat, randomBytes32(), randomBytes32(), randomBytes32(), 105 unixms) + val relayed2 = ChannelPaymentRelayed(650 msat, 500 msat, randomBytes32(), randomBytes32(), randomBytes32(), 115 unixms) migrationCheck( dbs = dbs, @@ -355,7 +355,7 @@ class AuditDbSpec extends AnyFunSuite { statement.setBytes(3, ps1.paymentHash.toArray) statement.setBytes(4, ps1.paymentPreimage.toArray) statement.setBytes(5, pp.toChannelId.toArray) - statement.setLong(6, pp.timestamp) + statement.setLong(6, pp.timestamp.toLong) statement.setBytes(7, pp.id.toString.getBytes) statement.executeUpdate() } @@ -368,7 +368,7 @@ class AuditDbSpec extends AnyFunSuite { statement.setBytes(3, relayed.paymentHash.toArray) statement.setBytes(4, relayed.fromChannelId.toArray) statement.setBytes(5, relayed.toChannelId.toArray) - statement.setLong(6, relayed.timestamp) + statement.setLong(6, relayed.timestamp.toLong) statement.executeUpdate() } } @@ -380,33 +380,33 @@ class AuditDbSpec extends AnyFunSuite { using(connection.createStatement()) { statement => assert(getVersion(statement, "audit").contains(SqliteAuditDb.CURRENT_VERSION)) } - assert(migratedDb.listSent(50, 150).toSet === Set( + assert(migratedDb.listSent(50 unixms, 150 unixms).toSet === Set( ps1.copy(id = pp1.id, recipientAmount = pp1.amount, parts = pp1 :: Nil), ps1.copy(id = pp2.id, recipientAmount = pp2.amount, parts = pp2 :: Nil) )) - assert(migratedDb.listRelayed(100, 120) === Seq(relayed1, relayed2)) + assert(migratedDb.listRelayed(100 unixms, 120 unixms) === Seq(relayed1, relayed2)) val postMigrationDb = new SqliteAuditDb(connection) using(connection.createStatement()) { statement => assert(getVersion(statement, "audit").contains(SqliteAuditDb.CURRENT_VERSION)) } val ps2 = PaymentSent(UUID.randomUUID(), randomBytes32(), randomBytes32(), 1100 msat, randomKey().publicKey, Seq( - PaymentSent.PartialPayment(UUID.randomUUID(), 500 msat, 10 msat, randomBytes32(), None, 160), - PaymentSent.PartialPayment(UUID.randomUUID(), 600 msat, 5 msat, randomBytes32(), None, 165) + PaymentSent.PartialPayment(UUID.randomUUID(), 500 msat, 10 msat, randomBytes32(), None, 160 unixms), + PaymentSent.PartialPayment(UUID.randomUUID(), 600 msat, 5 msat, randomBytes32(), None, 165 unixms) )) - val relayed3 = TrampolinePaymentRelayed(randomBytes32(), Seq(PaymentRelayed.Part(450 msat, randomBytes32()), PaymentRelayed.Part(500 msat, randomBytes32())), Seq(PaymentRelayed.Part(800 msat, randomBytes32())), randomKey().publicKey, 700 msat, 150) + val relayed3 = TrampolinePaymentRelayed(randomBytes32(), Seq(PaymentRelayed.Part(450 msat, randomBytes32()), PaymentRelayed.Part(500 msat, randomBytes32())), Seq(PaymentRelayed.Part(800 msat, randomBytes32())), randomKey().publicKey, 700 msat, 150 unixms) postMigrationDb.add(ps2) - assert(postMigrationDb.listSent(155, 200) === Seq(ps2)) + assert(postMigrationDb.listSent(155 unixms, 200 unixms) === Seq(ps2)) postMigrationDb.add(relayed3) - assert(postMigrationDb.listRelayed(100, 160) === Seq(relayed1, relayed2, relayed3)) + assert(postMigrationDb.listRelayed(100 unixms, 160 unixms) === Seq(relayed1, relayed2, relayed3)) } ) } test("migrate audit database v4 -> current") { - val relayed1 = ChannelPaymentRelayed(600 msat, 500 msat, randomBytes32(), randomBytes32(), randomBytes32(), 105) - val relayed2 = TrampolinePaymentRelayed(randomBytes32(), Seq(PaymentRelayed.Part(300 msat, randomBytes32()), PaymentRelayed.Part(350 msat, randomBytes32())), Seq(PaymentRelayed.Part(600 msat, randomBytes32())), PlaceHolderPubKey, 0 msat, 110) + val relayed1 = ChannelPaymentRelayed(600 msat, 500 msat, randomBytes32(), randomBytes32(), randomBytes32(), 105 unixms) + val relayed2 = TrampolinePaymentRelayed(randomBytes32(), Seq(PaymentRelayed.Part(300 msat, randomBytes32()), PaymentRelayed.Part(350 msat, randomBytes32())), Seq(PaymentRelayed.Part(600 msat, randomBytes32())), PlaceHolderPubKey, 0 msat, 110 unixms) forAllDbs { case dbs: TestPgDatabases => @@ -439,7 +439,7 @@ class AuditDbSpec extends AnyFunSuite { statement.setString(3, relayed1.fromChannelId.toHex) statement.setString(4, "IN") statement.setString(5, "channel") - statement.setLong(6, relayed1.timestamp) + statement.setLong(6, relayed1.timestamp.toLong) statement.executeUpdate() } using(connection.prepareStatement("INSERT INTO relayed VALUES (?, ?, ?, ?, ?, ?)")) { statement => @@ -448,7 +448,7 @@ class AuditDbSpec extends AnyFunSuite { statement.setString(3, relayed1.toChannelId.toHex) statement.setString(4, "OUT") statement.setString(5, "channel") - statement.setLong(6, relayed1.timestamp) + statement.setLong(6, relayed1.timestamp.toLong) statement.executeUpdate() } for (incoming <- relayed2.incoming) { @@ -458,7 +458,7 @@ class AuditDbSpec extends AnyFunSuite { statement.setString(3, incoming.channelId.toHex) statement.setString(4, "IN") statement.setString(5, "trampoline") - statement.setLong(6, relayed2.timestamp) + statement.setLong(6, relayed2.timestamp.toLong) statement.executeUpdate() } } @@ -469,7 +469,7 @@ class AuditDbSpec extends AnyFunSuite { statement.setString(3, outgoing.channelId.toHex) statement.setString(4, "OUT") statement.setString(5, "trampoline") - statement.setLong(6, relayed2.timestamp) + statement.setLong(6, relayed2.timestamp.toLong) statement.executeUpdate() } } @@ -479,15 +479,15 @@ class AuditDbSpec extends AnyFunSuite { postCheck = connection => { val migratedDb = dbs.audit - assert(migratedDb.listRelayed(100, 120) === Seq(relayed1, relayed2)) + assert(migratedDb.listRelayed(100 unixms, 120 unixms) === Seq(relayed1, relayed2)) val postMigrationDb = new PgAuditDb()(dbs.datasource) using(connection.createStatement()) { statement => assert(getVersion(statement, "audit").contains(PgAuditDb.CURRENT_VERSION)) } - val relayed3 = TrampolinePaymentRelayed(randomBytes32(), Seq(PaymentRelayed.Part(450 msat, randomBytes32()), PaymentRelayed.Part(500 msat, randomBytes32())), Seq(PaymentRelayed.Part(800 msat, randomBytes32())), randomKey().publicKey, 700 msat, 150) + val relayed3 = TrampolinePaymentRelayed(randomBytes32(), Seq(PaymentRelayed.Part(450 msat, randomBytes32()), PaymentRelayed.Part(500 msat, randomBytes32())), Seq(PaymentRelayed.Part(800 msat, randomBytes32())), randomKey().publicKey, 700 msat, 150 unixms) postMigrationDb.add(relayed3) - assert(postMigrationDb.listRelayed(100, 160) === Seq(relayed1, relayed2, relayed3)) + assert(postMigrationDb.listRelayed(100 unixms, 160 unixms) === Seq(relayed1, relayed2, relayed3)) } ) case dbs: TestSqliteDatabases => @@ -520,7 +520,7 @@ class AuditDbSpec extends AnyFunSuite { statement.setBytes(3, relayed1.fromChannelId.toArray) statement.setString(4, "IN") statement.setString(5, "channel") - statement.setLong(6, relayed1.timestamp) + statement.setLong(6, relayed1.timestamp.toLong) statement.executeUpdate() } using(connection.prepareStatement("INSERT INTO relayed VALUES (?, ?, ?, ?, ?, ?)")) { statement => @@ -529,7 +529,7 @@ class AuditDbSpec extends AnyFunSuite { statement.setBytes(3, relayed1.toChannelId.toArray) statement.setString(4, "OUT") statement.setString(5, "channel") - statement.setLong(6, relayed1.timestamp) + statement.setLong(6, relayed1.timestamp.toLong) statement.executeUpdate() } for (incoming <- relayed2.incoming) { @@ -539,7 +539,7 @@ class AuditDbSpec extends AnyFunSuite { statement.setBytes(3, incoming.channelId.toArray) statement.setString(4, "IN") statement.setString(5, "trampoline") - statement.setLong(6, relayed2.timestamp) + statement.setLong(6, relayed2.timestamp.toLong) statement.executeUpdate() } } @@ -550,7 +550,7 @@ class AuditDbSpec extends AnyFunSuite { statement.setBytes(3, outgoing.channelId.toArray) statement.setString(4, "OUT") statement.setString(5, "trampoline") - statement.setLong(6, relayed2.timestamp) + statement.setLong(6, relayed2.timestamp.toLong) statement.executeUpdate() } } @@ -562,15 +562,15 @@ class AuditDbSpec extends AnyFunSuite { using(connection.createStatement()) { statement => assert(getVersion(statement, "audit").contains(SqliteAuditDb.CURRENT_VERSION)) } - assert(migratedDb.listRelayed(100, 120) === Seq(relayed1, relayed2)) + assert(migratedDb.listRelayed(100 unixms, 120 unixms) === Seq(relayed1, relayed2)) val postMigrationDb = new SqliteAuditDb(connection) using(connection.createStatement()) { statement => assert(getVersion(statement, "audit").contains(SqliteAuditDb.CURRENT_VERSION)) } - val relayed3 = TrampolinePaymentRelayed(randomBytes32(), Seq(PaymentRelayed.Part(450 msat, randomBytes32()), PaymentRelayed.Part(500 msat, randomBytes32())), Seq(PaymentRelayed.Part(800 msat, randomBytes32())), randomKey().publicKey, 700 msat, 150) + val relayed3 = TrampolinePaymentRelayed(randomBytes32(), Seq(PaymentRelayed.Part(450 msat, randomBytes32()), PaymentRelayed.Part(500 msat, randomBytes32())), Seq(PaymentRelayed.Part(800 msat, randomBytes32())), randomKey().publicKey, 700 msat, 150 unixms) postMigrationDb.add(relayed3) - assert(postMigrationDb.listRelayed(100, 160) === Seq(relayed1, relayed2, relayed3)) + assert(postMigrationDb.listRelayed(100 unixms, 160 unixms) === Seq(relayed1, relayed2, relayed3)) } ) } @@ -578,8 +578,8 @@ class AuditDbSpec extends AnyFunSuite { test("migrate audit database v7 -> current") { val networkFees = Seq( - NetworkFee(randomKey().publicKey, randomBytes32(), randomBytes32(), 50 sat, "test-tx-1", 500), - NetworkFee(randomKey().publicKey, randomBytes32(), randomBytes32(), 0 sat, "test-tx-2", 600), + NetworkFee(randomKey().publicKey, randomBytes32(), randomBytes32(), 50 sat, "test-tx-1", 500 unixms), + NetworkFee(randomKey().publicKey, randomBytes32(), randomBytes32(), 0 sat, "test-tx-2", 600 unixms), ) forAllDbs { @@ -630,7 +630,7 @@ class AuditDbSpec extends AnyFunSuite { statement.setString(3, tx.txId.toHex) statement.setLong(4, tx.fee.toLong) statement.setString(5, tx.txType) - statement.setTimestamp(6, Timestamp.from(Instant.ofEpochMilli(tx.timestamp))) + statement.setTimestamp(6, tx.timestamp.toSqlTimestamp) statement.executeUpdate() } } @@ -640,7 +640,7 @@ class AuditDbSpec extends AnyFunSuite { postCheck = connection => { val migratedDb = dbs.audit using(connection.createStatement()) { statement => assert(getVersion(statement, "audit").contains(PgAuditDb.CURRENT_VERSION)) } - assert(migratedDb.listNetworkFees(0, 700) === networkFees) + assert(migratedDb.listNetworkFees(0 unixms, 700 unixms) === networkFees) } ) case dbs: TestSqliteDatabases => @@ -688,7 +688,7 @@ class AuditDbSpec extends AnyFunSuite { statement.setBytes(3, tx.txId.toArray) statement.setLong(4, tx.fee.toLong) statement.setString(5, tx.txType) - statement.setLong(6, tx.timestamp) + statement.setLong(6, tx.timestamp.toLong) statement.executeUpdate() } } @@ -698,7 +698,7 @@ class AuditDbSpec extends AnyFunSuite { postCheck = connection => { val migratedDb = dbs.audit using(connection.createStatement()) { statement => assert(getVersion(statement, "audit").contains(SqliteAuditDb.CURRENT_VERSION)) } - assert(migratedDb.listNetworkFees(0, 700) === networkFees) + assert(migratedDb.listNetworkFees(0 unixms, 700 unixms) === networkFees) } ) } @@ -744,7 +744,7 @@ class AuditDbSpec extends AnyFunSuite { statement.executeUpdate() } - assert(db.listRelayed(0, 40) === Nil) + assert(db.listRelayed(0 unixms, 40 unixms) === Nil) } } @@ -760,7 +760,7 @@ class AuditDbSpec extends AnyFunSuite { test("add experiment metrics") { forAllDbs { dbs => - dbs.audit.addPathFindingExperimentMetrics(PathFindingExperimentMetrics(100000000 msat, 3000 msat, status = "SUCCESS", 37, System.currentTimeMillis, isMultiPart = false, "my-test-experiment", randomKey().publicKey)) + dbs.audit.addPathFindingExperimentMetrics(PathFindingExperimentMetrics(100000000 msat, 3000 msat, status = "SUCCESS", 37 millis, TimestampMilli.now(), isMultiPart = false, "my-test-experiment", randomKey().publicKey)) } } diff --git a/eclair-core/src/test/scala/fr/acinq/eclair/db/ChannelsDbSpec.scala b/eclair-core/src/test/scala/fr/acinq/eclair/db/ChannelsDbSpec.scala index 2c840f080d..111c1e417d 100644 --- a/eclair-core/src/test/scala/fr/acinq/eclair/db/ChannelsDbSpec.scala +++ b/eclair-core/src/test/scala/fr/acinq/eclair/db/ChannelsDbSpec.scala @@ -76,8 +76,11 @@ class ChannelsDbSpec extends AnyFunSuite { assert(db.listLocalChannels() === List(channel1)) db.addOrUpdateChannel(channel2a) assert(db.listLocalChannels() === List(channel1, channel2a)) + assert(db.getChannel(channel1.channelId).contains(channel1)) + assert(db.getChannel(channel2a.channelId).contains(channel2a)) db.addOrUpdateChannel(channel2b) assert(db.listLocalChannels() === List(channel1, channel2b)) + assert(db.getChannel(channel2b.channelId).contains(channel2b)) assert(db.listHtlcInfos(channel1.channelId, commitNumber).toList == Nil) db.addHtlcInfo(channel1.channelId, commitNumber, paymentHash1, cltvExpiry1) @@ -86,9 +89,11 @@ class ChannelsDbSpec extends AnyFunSuite { assert(db.listHtlcInfos(channel1.channelId, 43).toList == Nil) db.removeChannel(channel1.channelId) + assert(db.getChannel(channel1.channelId).isEmpty) assert(db.listLocalChannels() === List(channel2b)) assert(db.listHtlcInfos(channel1.channelId, commitNumber).toList == Nil) db.removeChannel(channel2b.channelId) + assert(db.getChannel(channel2b.channelId).isEmpty) assert(db.listLocalChannels() === Nil) } } diff --git a/eclair-core/src/test/scala/fr/acinq/eclair/db/PaymentsDbSpec.scala b/eclair-core/src/test/scala/fr/acinq/eclair/db/PaymentsDbSpec.scala index 57384a87c9..40a86a8ef9 100644 --- a/eclair-core/src/test/scala/fr/acinq/eclair/db/PaymentsDbSpec.scala +++ b/eclair-core/src/test/scala/fr/acinq/eclair/db/PaymentsDbSpec.scala @@ -26,7 +26,7 @@ import fr.acinq.eclair.db.sqlite.SqlitePaymentsDb import fr.acinq.eclair.payment._ import fr.acinq.eclair.router.Router.{ChannelHop, NodeHop} import fr.acinq.eclair.wire.protocol.{ChannelUpdate, UnknownNextPeer} -import fr.acinq.eclair.{CltvExpiryDelta, MilliSatoshiLong, ShortChannelId, TestDatabases, randomBytes32, randomBytes64, randomKey} +import fr.acinq.eclair.{CltvExpiryDelta, MilliSatoshiLong, ShortChannelId, TestDatabases, TimestampMilli, TimestampMilliLong, TimestampSecond, TimestampSecondLong, randomBytes32, randomBytes64, randomKey} import org.scalatest.funsuite.AnyFunSuite import java.time.Instant @@ -77,16 +77,16 @@ class PaymentsDbSpec extends AnyFunSuite { assert(db.getIncomingPayment(paymentHash1).isEmpty) // add a few rows - val ps1 = OutgoingPayment(UUID.randomUUID(), UUID.randomUUID(), None, paymentHash1, PaymentType.Standard, 12345 msat, 12345 msat, alice, 1000, None, OutgoingPaymentStatus.Pending) - val i1 = PaymentRequest(Block.TestnetGenesisBlock.hash, Some(500 msat), paymentHash1, davePriv, Left("Some invoice"), CltvExpiryDelta(18), expirySeconds = None, timestamp = 1) - val pr1 = IncomingPayment(i1, preimage1, PaymentType.Standard, i1.timestamp.seconds.toMillis, IncomingPaymentStatus.Received(550 msat, 1100)) + val ps1 = OutgoingPayment(UUID.randomUUID(), UUID.randomUUID(), None, paymentHash1, PaymentType.Standard, 12345 msat, 12345 msat, alice, 1000 unixms, None, OutgoingPaymentStatus.Pending) + val i1 = PaymentRequest(Block.TestnetGenesisBlock.hash, Some(500 msat), paymentHash1, davePriv, Left("Some invoice"), CltvExpiryDelta(18), expirySeconds = None, timestamp = 1 unixsec) + val pr1 = IncomingPayment(i1, preimage1, PaymentType.Standard, i1.timestamp.toTimestampMilli, IncomingPaymentStatus.Received(550 msat, 1100 unixms)) db.addOutgoingPayment(ps1) db.addIncomingPayment(i1, preimage1) - db.receiveIncomingPayment(i1.paymentHash, 550 msat, 1100) + db.receiveIncomingPayment(i1.paymentHash, 550 msat, 1100 unixms) - assert(db.listIncomingPayments(1, 1500) === Seq(pr1)) - assert(db.listOutgoingPayments(1, 1500) === Seq(ps1)) + assert(db.listIncomingPayments(1 unixms, 1500 unixms) === Seq(pr1)) + assert(db.listOutgoingPayments(1 unixms, 1500 unixms) === Seq(ps1)) } ) @@ -99,13 +99,13 @@ class PaymentsDbSpec extends AnyFunSuite { val id1 = UUID.randomUUID() val id2 = UUID.randomUUID() val id3 = UUID.randomUUID() - val ps1 = OutgoingPayment(id1, id1, None, randomBytes32(), PaymentType.Standard, 561 msat, 561 msat, PrivateKey(ByteVector32.One).publicKey, 1000, None, OutgoingPaymentStatus.Pending) - val ps2 = OutgoingPayment(id2, id2, None, randomBytes32(), PaymentType.Standard, 1105 msat, 1105 msat, PrivateKey(ByteVector32.One).publicKey, 1010, None, OutgoingPaymentStatus.Failed(Nil, 1050)) - val ps3 = OutgoingPayment(id3, id3, None, paymentHash1, PaymentType.Standard, 1729 msat, 1729 msat, PrivateKey(ByteVector32.One).publicKey, 1040, None, OutgoingPaymentStatus.Succeeded(preimage1, 0 msat, Nil, 1060)) - val i1 = PaymentRequest(Block.TestnetGenesisBlock.hash, Some(12345678 msat), paymentHash1, davePriv, Left("Some invoice"), CltvExpiryDelta(18), expirySeconds = None, timestamp = 1) - val pr1 = IncomingPayment(i1, preimage1, PaymentType.Standard, i1.timestamp.seconds.toMillis, IncomingPaymentStatus.Received(12345678 msat, 1090)) - val i2 = PaymentRequest(Block.TestnetGenesisBlock.hash, Some(12345678 msat), paymentHash2, carolPriv, Left("Another invoice"), CltvExpiryDelta(18), expirySeconds = Some(30), timestamp = 1) - val pr2 = IncomingPayment(i2, preimage2, PaymentType.Standard, i2.timestamp.seconds.toMillis, IncomingPaymentStatus.Expired) + val ps1 = OutgoingPayment(id1, id1, None, randomBytes32(), PaymentType.Standard, 561 msat, 561 msat, PrivateKey(ByteVector32.One).publicKey, 1000 unixms, None, OutgoingPaymentStatus.Pending) + val ps2 = OutgoingPayment(id2, id2, None, randomBytes32(), PaymentType.Standard, 1105 msat, 1105 msat, PrivateKey(ByteVector32.One).publicKey, 1010 unixms, None, OutgoingPaymentStatus.Failed(Nil, 1050 unixms)) + val ps3 = OutgoingPayment(id3, id3, None, paymentHash1, PaymentType.Standard, 1729 msat, 1729 msat, PrivateKey(ByteVector32.One).publicKey, 1040 unixms, None, OutgoingPaymentStatus.Succeeded(preimage1, 0 msat, Nil, 1060 unixms)) + val i1 = PaymentRequest(Block.TestnetGenesisBlock.hash, Some(12345678 msat), paymentHash1, davePriv, Left("Some invoice"), CltvExpiryDelta(18), expirySeconds = None, timestamp = 1 unixsec) + val pr1 = IncomingPayment(i1, preimage1, PaymentType.Standard, i1.timestamp.toTimestampMilli, IncomingPaymentStatus.Received(12345678 msat, 1090 unixms)) + val i2 = PaymentRequest(Block.TestnetGenesisBlock.hash, Some(12345678 msat), paymentHash2, carolPriv, Left("Another invoice"), CltvExpiryDelta(18), expirySeconds = Some(30), timestamp = 1 unixsec) + val pr2 = IncomingPayment(i2, preimage2, PaymentType.Standard, i2.timestamp.toTimestampMilli, IncomingPaymentStatus.Expired) migrationCheck( dbs = dbs, @@ -130,7 +130,7 @@ class PaymentsDbSpec extends AnyFunSuite { statement.setString(1, ps1.id.toString) statement.setBytes(2, ps1.paymentHash.toArray) statement.setLong(3, ps1.amount.toLong) - statement.setLong(4, ps1.createdAt) + statement.setLong(4, ps1.createdAt.toLong) statement.setString(5, "PENDING") statement.executeUpdate() } @@ -139,8 +139,8 @@ class PaymentsDbSpec extends AnyFunSuite { statement.setString(1, ps2.id.toString) statement.setBytes(2, ps2.paymentHash.toArray) statement.setLong(3, ps2.amount.toLong) - statement.setLong(4, ps2.createdAt) - statement.setLong(5, ps2.status.asInstanceOf[OutgoingPaymentStatus.Failed].completedAt) + statement.setLong(4, ps2.createdAt.toLong) + statement.setLong(5, ps2.status.asInstanceOf[OutgoingPaymentStatus.Failed].completedAt.toLong) statement.setString(6, "FAILED") statement.executeUpdate() } @@ -150,8 +150,8 @@ class PaymentsDbSpec extends AnyFunSuite { statement.setBytes(2, ps3.paymentHash.toArray) statement.setBytes(3, ps3.status.asInstanceOf[OutgoingPaymentStatus.Succeeded].paymentPreimage.toArray) statement.setLong(4, ps3.amount.toLong) - statement.setLong(5, ps3.createdAt) - statement.setLong(6, ps3.status.asInstanceOf[OutgoingPaymentStatus.Succeeded].completedAt) + statement.setLong(5, ps3.createdAt.toLong) + statement.setLong(6, ps3.status.asInstanceOf[OutgoingPaymentStatus.Succeeded].completedAt.toLong) statement.setString(7, "SUCCEEDED") statement.executeUpdate() } @@ -165,8 +165,8 @@ class PaymentsDbSpec extends AnyFunSuite { statement.setBytes(2, pr1.paymentPreimage.toArray) statement.setString(3, PaymentRequest.write(i1)) statement.setLong(4, pr1.status.asInstanceOf[IncomingPaymentStatus.Received].amount.toLong) - statement.setLong(5, pr1.createdAt) - statement.setLong(6, pr1.status.asInstanceOf[IncomingPaymentStatus.Received].receivedAt) + statement.setLong(5, pr1.createdAt.toLong) + statement.setLong(6, pr1.status.asInstanceOf[IncomingPaymentStatus.Received].receivedAt.toLong) statement.executeUpdate() } @@ -174,8 +174,8 @@ class PaymentsDbSpec extends AnyFunSuite { statement.setBytes(1, i2.paymentHash.toArray) statement.setBytes(2, pr2.paymentPreimage.toArray) statement.setString(3, PaymentRequest.write(i2)) - statement.setLong(4, pr2.createdAt) - statement.setLong(5, (i2.timestamp + i2.expiry.get).seconds.toMillis) + statement.setLong(4, pr2.createdAt.toLong) + statement.setLong(5, (i2.timestamp + i2.expiry.get).toLong) statement.executeUpdate() } }, @@ -186,24 +186,24 @@ class PaymentsDbSpec extends AnyFunSuite { assert(db.getIncomingPayment(i1.paymentHash) === Some(pr1)) assert(db.getIncomingPayment(i2.paymentHash) === Some(pr2)) - assert(db.listOutgoingPayments(1, 2000) === Seq(ps1, ps2, ps3)) + assert(db.listOutgoingPayments(1 unixms, 2000 unixms) === Seq(ps1, ps2, ps3)) val i3 = PaymentRequest(Block.TestnetGenesisBlock.hash, Some(561 msat), paymentHash3, alicePriv, Left("invoice #3"), CltvExpiryDelta(18), expirySeconds = Some(30)) - val pr3 = IncomingPayment(i3, preimage3, PaymentType.Standard, i3.timestamp.seconds.toMillis, IncomingPaymentStatus.Pending) + val pr3 = IncomingPayment(i3, preimage3, PaymentType.Standard, i3.timestamp.toTimestampMilli, IncomingPaymentStatus.Pending) db.addIncomingPayment(i3, pr3.paymentPreimage) - val ps4 = OutgoingPayment(UUID.randomUUID(), UUID.randomUUID(), Some("1"), randomBytes32(), PaymentType.Standard, 123 msat, 123 msat, alice, 1100, Some(i3), OutgoingPaymentStatus.Pending) - val ps5 = OutgoingPayment(UUID.randomUUID(), UUID.randomUUID(), Some("2"), randomBytes32(), PaymentType.Standard, 456 msat, 456 msat, bob, 1150, Some(i2), OutgoingPaymentStatus.Succeeded(preimage1, 42 msat, Nil, 1180)) - val ps6 = OutgoingPayment(UUID.randomUUID(), UUID.randomUUID(), Some("3"), randomBytes32(), PaymentType.Standard, 789 msat, 789 msat, bob, 1250, None, OutgoingPaymentStatus.Failed(Nil, 1300)) + val ps4 = OutgoingPayment(UUID.randomUUID(), UUID.randomUUID(), Some("1"), randomBytes32(), PaymentType.Standard, 123 msat, 123 msat, alice, 1100 unixms, Some(i3), OutgoingPaymentStatus.Pending) + val ps5 = OutgoingPayment(UUID.randomUUID(), UUID.randomUUID(), Some("2"), randomBytes32(), PaymentType.Standard, 456 msat, 456 msat, bob, 1150 unixms, Some(i2), OutgoingPaymentStatus.Succeeded(preimage1, 42 msat, Nil, 1180 unixms)) + val ps6 = OutgoingPayment(UUID.randomUUID(), UUID.randomUUID(), Some("3"), randomBytes32(), PaymentType.Standard, 789 msat, 789 msat, bob, 1250 unixms, None, OutgoingPaymentStatus.Failed(Nil, 1300 unixms)) db.addOutgoingPayment(ps4) db.addOutgoingPayment(ps5.copy(status = OutgoingPaymentStatus.Pending)) - db.updateOutgoingPayment(PaymentSent(ps5.parentId, ps5.paymentHash, preimage1, ps5.amount, ps5.recipientNodeId, Seq(PaymentSent.PartialPayment(ps5.id, ps5.amount, 42 msat, randomBytes32(), None, 1180)))) + db.updateOutgoingPayment(PaymentSent(ps5.parentId, ps5.paymentHash, preimage1, ps5.amount, ps5.recipientNodeId, Seq(PaymentSent.PartialPayment(ps5.id, ps5.amount, 42 msat, randomBytes32(), None, 1180 unixms)))) db.addOutgoingPayment(ps6.copy(status = OutgoingPaymentStatus.Pending)) - db.updateOutgoingPayment(PaymentFailed(ps6.id, ps6.paymentHash, Nil, 1300)) + db.updateOutgoingPayment(PaymentFailed(ps6.id, ps6.paymentHash, Nil, 1300 unixms)) - assert(db.listOutgoingPayments(1, 2000) === Seq(ps1, ps2, ps3, ps4, ps5, ps6)) - assert(db.listIncomingPayments(1, System.currentTimeMillis) === Seq(pr1, pr2, pr3)) - assert(db.listExpiredIncomingPayments(1, 2000) === Seq(pr2)) + assert(db.listOutgoingPayments(1 unixms, 2000 unixms) === Seq(ps1, ps2, ps3, ps4, ps5, ps6)) + assert(db.listIncomingPayments(1 unixms, TimestampMilli.now()) === Seq(pr1, pr2, pr3)) + assert(db.listExpiredIncomingPayments(1 unixms, 2000 unixms) === Seq(pr2)) }) } @@ -214,9 +214,9 @@ class PaymentsDbSpec extends AnyFunSuite { val (id1, id2, id3) = (UUID.randomUUID(), UUID.randomUUID(), UUID.randomUUID()) val parentId = UUID.randomUUID() val invoice1 = PaymentRequest(Block.TestnetGenesisBlock.hash, Some(2834 msat), paymentHash1, bobPriv, Left("invoice #1"), CltvExpiryDelta(18), expirySeconds = Some(30)) - val ps1 = OutgoingPayment(id1, id1, Some("42"), randomBytes32(), PaymentType.Standard, 561 msat, 561 msat, alice, 1000, None, OutgoingPaymentStatus.Failed(Seq(FailureSummary(FailureType.REMOTE, "no candy for you", List(HopSummary(hop_ab), HopSummary(hop_bc)))), 1020)) - val ps2 = OutgoingPayment(id2, parentId, Some("42"), paymentHash1, PaymentType.Standard, 1105 msat, 1105 msat, bob, 1010, Some(invoice1), OutgoingPaymentStatus.Pending) - val ps3 = OutgoingPayment(id3, parentId, None, paymentHash1, PaymentType.Standard, 1729 msat, 1729 msat, bob, 1040, None, OutgoingPaymentStatus.Succeeded(preimage1, 10 msat, Seq(HopSummary(hop_ab), HopSummary(hop_bc)), 1060)) + val ps1 = OutgoingPayment(id1, id1, Some("42"), randomBytes32(), PaymentType.Standard, 561 msat, 561 msat, alice, 1000 unixms, None, OutgoingPaymentStatus.Failed(Seq(FailureSummary(FailureType.REMOTE, "no candy for you", List(HopSummary(hop_ab), HopSummary(hop_bc)))), 1020 unixms)) + val ps2 = OutgoingPayment(id2, parentId, Some("42"), paymentHash1, PaymentType.Standard, 1105 msat, 1105 msat, bob, 1010 unixms, Some(invoice1), OutgoingPaymentStatus.Pending) + val ps3 = OutgoingPayment(id3, parentId, None, paymentHash1, PaymentType.Standard, 1729 msat, 1729 msat, bob, 1040 unixms, None, OutgoingPaymentStatus.Succeeded(preimage1, 10 msat, Seq(HopSummary(hop_ab), HopSummary(hop_bc)), 1060 unixms)) migrationCheck( dbs = dbs, @@ -242,8 +242,8 @@ class PaymentsDbSpec extends AnyFunSuite { statement.setBytes(4, ps1.paymentHash.toArray) statement.setLong(5, ps1.amount.toLong) statement.setBytes(6, ps1.recipientNodeId.value.toArray) - statement.setLong(7, ps1.createdAt) - statement.setLong(8, ps1.status.asInstanceOf[OutgoingPaymentStatus.Failed].completedAt) + statement.setLong(7, ps1.createdAt.toLong) + statement.setLong(8, ps1.status.asInstanceOf[OutgoingPaymentStatus.Failed].completedAt.toLong) statement.setBytes(9, SqlitePaymentsDb.paymentFailuresCodec.encode(ps1.status.asInstanceOf[OutgoingPaymentStatus.Failed].failures.toList).require.toByteArray) statement.executeUpdate() } @@ -255,7 +255,7 @@ class PaymentsDbSpec extends AnyFunSuite { statement.setBytes(4, ps2.paymentHash.toArray) statement.setLong(5, ps2.amount.toLong) statement.setBytes(6, ps2.recipientNodeId.value.toArray) - statement.setLong(7, ps2.createdAt) + statement.setLong(7, ps2.createdAt.toLong) statement.setString(8, PaymentRequest.write(invoice1)) statement.executeUpdate() } @@ -266,8 +266,8 @@ class PaymentsDbSpec extends AnyFunSuite { statement.setBytes(3, ps3.paymentHash.toArray) statement.setLong(4, ps3.amount.toLong) statement.setBytes(5, ps3.recipientNodeId.value.toArray) - statement.setLong(6, ps3.createdAt) - statement.setLong(7, ps3.status.asInstanceOf[OutgoingPaymentStatus.Succeeded].completedAt) + statement.setLong(6, ps3.createdAt.toLong) + statement.setLong(7, ps3.status.asInstanceOf[OutgoingPaymentStatus.Succeeded].completedAt.toLong) statement.setBytes(8, ps3.status.asInstanceOf[OutgoingPaymentStatus.Succeeded].paymentPreimage.toArray) statement.setLong(9, ps3.status.asInstanceOf[OutgoingPaymentStatus.Succeeded].feesPaid.toLong) statement.setBytes(10, SqlitePaymentsDb.paymentRouteCodec.encode(ps3.status.asInstanceOf[OutgoingPaymentStatus.Succeeded].route.toList).require.toByteArray) @@ -297,13 +297,13 @@ class PaymentsDbSpec extends AnyFunSuite { val id1 = UUID.randomUUID() val id2 = UUID.randomUUID() val id3 = UUID.randomUUID() - val ps1 = OutgoingPayment(id1, id1, None, randomBytes32(), PaymentType.Standard, 561 msat, 561 msat, PrivateKey(ByteVector32.One).publicKey, Instant.parse("2021-01-01T10:15:30.00Z").toEpochMilli, None, OutgoingPaymentStatus.Pending) - val ps2 = OutgoingPayment(id2, id2, None, randomBytes32(), PaymentType.Standard, 1105 msat, 1105 msat, PrivateKey(ByteVector32.One).publicKey, Instant.parse("2020-05-14T13:47:21.00Z").toEpochMilli, None, OutgoingPaymentStatus.Failed(Nil, Instant.parse("2021-05-15T04:12:40.00Z").toEpochMilli)) - val ps3 = OutgoingPayment(id3, id3, None, paymentHash1, PaymentType.Standard, 1729 msat, 1729 msat, PrivateKey(ByteVector32.One).publicKey, Instant.parse("2021-01-28T09:12:05.00Z").toEpochMilli, None, OutgoingPaymentStatus.Succeeded(preimage1, 0 msat, Nil, Instant.now().toEpochMilli)) - val i1 = PaymentRequest(Block.TestnetGenesisBlock.hash, Some(12345678 msat), paymentHash1, davePriv, Left("Some invoice"), CltvExpiryDelta(18), expirySeconds = None, timestamp = Instant.now().getEpochSecond) - val pr1 = IncomingPayment(i1, preimage1, PaymentType.Standard, i1.timestamp.seconds.toMillis, IncomingPaymentStatus.Received(12345678 msat, Instant.now().toEpochMilli)) - val i2 = PaymentRequest(Block.TestnetGenesisBlock.hash, Some(12345678 msat), paymentHash2, carolPriv, Left("Another invoice"), CltvExpiryDelta(18), expirySeconds = Some(24 * 3600), timestamp = Instant.parse("2020-12-30T10:00:55.00Z").getEpochSecond) - val pr2 = IncomingPayment(i2, preimage2, PaymentType.Standard, i2.timestamp.seconds.toMillis, IncomingPaymentStatus.Expired) + val ps1 = OutgoingPayment(id1, id1, None, randomBytes32(), PaymentType.Standard, 561 msat, 561 msat, PrivateKey(ByteVector32.One).publicKey, TimestampMilli(Instant.parse("2021-01-01T10:15:30.00Z").toEpochMilli), None, OutgoingPaymentStatus.Pending) + val ps2 = OutgoingPayment(id2, id2, None, randomBytes32(), PaymentType.Standard, 1105 msat, 1105 msat, PrivateKey(ByteVector32.One).publicKey, TimestampMilli(Instant.parse("2020-05-14T13:47:21.00Z").toEpochMilli), None, OutgoingPaymentStatus.Failed(Nil, TimestampMilli(Instant.parse("2021-05-15T04:12:40.00Z").toEpochMilli))) + val ps3 = OutgoingPayment(id3, id3, None, paymentHash1, PaymentType.Standard, 1729 msat, 1729 msat, PrivateKey(ByteVector32.One).publicKey, TimestampMilli(Instant.parse("2021-01-28T09:12:05.00Z").toEpochMilli), None, OutgoingPaymentStatus.Succeeded(preimage1, 0 msat, Nil, TimestampMilli.now())) + val i1 = PaymentRequest(Block.TestnetGenesisBlock.hash, Some(12345678 msat), paymentHash1, davePriv, Left("Some invoice"), CltvExpiryDelta(18), expirySeconds = None, timestamp = TimestampSecond.now()) + val pr1 = IncomingPayment(i1, preimage1, PaymentType.Standard, i1.timestamp.toTimestampMilli, IncomingPaymentStatus.Received(12345678 msat, TimestampMilli.now())) + val i2 = PaymentRequest(Block.TestnetGenesisBlock.hash, Some(12345678 msat), paymentHash2, carolPriv, Left("Another invoice"), CltvExpiryDelta(18), expirySeconds = Some(24 * 3600), timestamp = TimestampSecond(Instant.parse("2020-12-30T10:00:55.00Z").getEpochSecond)) + val pr2 = IncomingPayment(i2, preimage2, PaymentType.Standard, i2.timestamp.toTimestampMilli, IncomingPaymentStatus.Expired) migrationCheck( dbs = dbs, @@ -330,14 +330,14 @@ class PaymentsDbSpec extends AnyFunSuite { statement.setLong(6, sent.amount.toLong) statement.setLong(7, sent.recipientAmount.toLong) statement.setString(8, sent.recipientNodeId.value.toHex) - statement.setLong(9, sent.createdAt) + statement.setLong(9, sent.createdAt.toLong) statement.setString(10, sent.paymentRequest.map(PaymentRequest.write).orNull) sent.status match { case s: OutgoingPaymentStatus.Succeeded => - statement.setLong(11, s.completedAt) + statement.setLong(11, s.completedAt.toLong) statement.setString(12, s.paymentPreimage.toHex) case s: OutgoingPaymentStatus.Failed => - statement.setLong(11, s.completedAt) + statement.setLong(11, s.completedAt.toLong) statement.setObject(12, null) case _ => statement.setObject(11, null) @@ -353,15 +353,15 @@ class PaymentsDbSpec extends AnyFunSuite { statement.setString(2, preimage.toHex) statement.setString(3, PaymentType.Standard) statement.setString(4, PaymentRequest.write(pr)) - statement.setLong(5, pr.timestamp.seconds.toMillis) // BOLT11 timestamp is in seconds - statement.setLong(6, (pr.timestamp + pr.expiry.getOrElse(PaymentRequest.DEFAULT_EXPIRY_SECONDS.toLong)).seconds.toMillis) + statement.setLong(5, pr.timestamp.toTimestampMilli.toLong) // BOLT11 timestamp is in seconds + statement.setLong(6, (pr.timestamp + pr.expiry.getOrElse(PaymentRequest.DEFAULT_EXPIRY_SECONDS)).toTimestampMilli.toLong) statement.executeUpdate() } } using(connection.prepareStatement("UPDATE received_payments SET (received_msat, received_at) = (? + COALESCE(received_msat, 0), ?) WHERE payment_hash = ?")) { update => update.setLong(1, pr1.status.asInstanceOf[IncomingPaymentStatus.Received].amount.toLong) - update.setLong(2, pr1.status.asInstanceOf[IncomingPaymentStatus.Received].receivedAt) + update.setLong(2, pr1.status.asInstanceOf[IncomingPaymentStatus.Received].receivedAt.toLong) update.setString(3, pr1.paymentRequest.paymentHash.toHex) val updated = update.executeUpdate() if (updated == 0) { @@ -380,16 +380,16 @@ class PaymentsDbSpec extends AnyFunSuite { assert(db.getIncomingPayment(i1.paymentHash) === Some(pr1)) assert(db.getIncomingPayment(i2.paymentHash) === Some(pr2)) - assert(db.listIncomingPayments(Instant.parse("2020-01-01T00:00:00.00Z").toEpochMilli, Instant.parse("2100-12-31T23:59:59.00Z").toEpochMilli) === Seq(pr2, pr1)) - assert(db.listIncomingPayments(Instant.parse("2020-01-01T00:00:00.00Z").toEpochMilli, Instant.parse("2020-12-31T23:59:59.00Z").toEpochMilli) === Seq(pr2)) - assert(db.listIncomingPayments(Instant.parse("2010-01-01T00:00:00.00Z").toEpochMilli, Instant.parse("2011-12-31T23:59:59.00Z").toEpochMilli) === Seq.empty) - assert(db.listExpiredIncomingPayments(Instant.parse("2020-01-01T00:00:00.00Z").toEpochMilli, Instant.parse("2100-12-31T23:59:59.00Z").toEpochMilli) === Seq(pr2)) - assert(db.listExpiredIncomingPayments(Instant.parse("2020-01-01T00:00:00.00Z").toEpochMilli, Instant.parse("2020-12-31T23:59:59.00Z").toEpochMilli) === Seq(pr2)) - assert(db.listExpiredIncomingPayments(Instant.parse("2010-01-01T00:00:00.00Z").toEpochMilli, Instant.parse("2011-12-31T23:59:59.00Z").toEpochMilli) === Seq.empty) - - assert(db.listOutgoingPayments(Instant.parse("2020-01-01T00:00:00.00Z").toEpochMilli, Instant.parse("2021-12-31T23:59:59.00Z").toEpochMilli) === Seq(ps2, ps1, ps3)) - assert(db.listOutgoingPayments(Instant.parse("2010-01-01T00:00:00.00Z").toEpochMilli, Instant.parse("2021-01-15T23:59:59.00Z").toEpochMilli) === Seq(ps2, ps1)) - assert(db.listOutgoingPayments(Instant.parse("2010-01-01T00:00:00.00Z").toEpochMilli, Instant.parse("2011-12-31T23:59:59.00Z").toEpochMilli) === Seq.empty) + assert(db.listIncomingPayments(TimestampMilli(Instant.parse("2020-01-01T00:00:00.00Z").toEpochMilli), TimestampMilli(Instant.parse("2100-12-31T23:59:59.00Z").toEpochMilli)) === Seq(pr2, pr1)) + assert(db.listIncomingPayments(TimestampMilli(Instant.parse("2020-01-01T00:00:00.00Z").toEpochMilli), TimestampMilli(Instant.parse("2020-12-31T23:59:59.00Z").toEpochMilli)) === Seq(pr2)) + assert(db.listIncomingPayments(TimestampMilli(Instant.parse("2010-01-01T00:00:00.00Z").toEpochMilli), TimestampMilli(Instant.parse("2011-12-31T23:59:59.00Z").toEpochMilli)) === Seq.empty) + assert(db.listExpiredIncomingPayments(TimestampMilli(Instant.parse("2020-01-01T00:00:00.00Z").toEpochMilli), TimestampMilli(Instant.parse("2100-12-31T23:59:59.00Z").toEpochMilli)) === Seq(pr2)) + assert(db.listExpiredIncomingPayments(TimestampMilli(Instant.parse("2020-01-01T00:00:00.00Z").toEpochMilli), TimestampMilli(Instant.parse("2020-12-31T23:59:59.00Z").toEpochMilli)) === Seq(pr2)) + assert(db.listExpiredIncomingPayments(TimestampMilli(Instant.parse("2010-01-01T00:00:00.00Z").toEpochMilli), TimestampMilli(Instant.parse("2011-12-31T23:59:59.00Z").toEpochMilli)) === Seq.empty) + + assert(db.listOutgoingPayments(TimestampMilli(Instant.parse("2020-01-01T00:00:00.00Z").toEpochMilli), TimestampMilli(Instant.parse("2021-12-31T23:59:59.00Z").toEpochMilli)) === Seq(ps2, ps1, ps3)) + assert(db.listOutgoingPayments(TimestampMilli(Instant.parse("2010-01-01T00:00:00.00Z").toEpochMilli), TimestampMilli(Instant.parse("2021-01-15T23:59:59.00Z").toEpochMilli)) === Seq(ps2, ps1)) + assert(db.listOutgoingPayments(TimestampMilli(Instant.parse("2010-01-01T00:00:00.00Z").toEpochMilli), TimestampMilli(Instant.parse("2011-12-31T23:59:59.00Z").toEpochMilli)) === Seq.empty) } ) } @@ -401,22 +401,22 @@ class PaymentsDbSpec extends AnyFunSuite { // can't receive a payment without an invoice associated with it assertThrows[IllegalArgumentException](db.receiveIncomingPayment(randomBytes32(), 12345678 msat)) - val expiredInvoice1 = PaymentRequest(Block.TestnetGenesisBlock.hash, Some(561 msat), randomBytes32(), alicePriv, Left("invoice #1"), CltvExpiryDelta(18), timestamp = 1) - val expiredInvoice2 = PaymentRequest(Block.TestnetGenesisBlock.hash, Some(1105 msat), randomBytes32(), bobPriv, Left("invoice #2"), CltvExpiryDelta(18), timestamp = 2, expirySeconds = Some(30)) - val expiredPayment1 = IncomingPayment(expiredInvoice1, randomBytes32(), PaymentType.Standard, expiredInvoice1.timestamp.seconds.toMillis, IncomingPaymentStatus.Expired) - val expiredPayment2 = IncomingPayment(expiredInvoice2, randomBytes32(), PaymentType.Standard, expiredInvoice2.timestamp.seconds.toMillis, IncomingPaymentStatus.Expired) + val expiredInvoice1 = PaymentRequest(Block.TestnetGenesisBlock.hash, Some(561 msat), randomBytes32(), alicePriv, Left("invoice #1"), CltvExpiryDelta(18), timestamp = 1 unixsec) + val expiredInvoice2 = PaymentRequest(Block.TestnetGenesisBlock.hash, Some(1105 msat), randomBytes32(), bobPriv, Left("invoice #2"), CltvExpiryDelta(18), timestamp = 2 unixsec, expirySeconds = Some(30)) + val expiredPayment1 = IncomingPayment(expiredInvoice1, randomBytes32(), PaymentType.Standard, expiredInvoice1.timestamp.toTimestampMilli, IncomingPaymentStatus.Expired) + val expiredPayment2 = IncomingPayment(expiredInvoice2, randomBytes32(), PaymentType.Standard, expiredInvoice2.timestamp.toTimestampMilli, IncomingPaymentStatus.Expired) val pendingInvoice1 = PaymentRequest(Block.TestnetGenesisBlock.hash, Some(561 msat), randomBytes32(), alicePriv, Left("invoice #3"), CltvExpiryDelta(18)) val pendingInvoice2 = PaymentRequest(Block.TestnetGenesisBlock.hash, Some(1105 msat), randomBytes32(), bobPriv, Left("invoice #4"), CltvExpiryDelta(18), expirySeconds = Some(30)) - val pendingPayment1 = IncomingPayment(pendingInvoice1, randomBytes32(), PaymentType.Standard, pendingInvoice1.timestamp.seconds.toMillis, IncomingPaymentStatus.Pending) - val pendingPayment2 = IncomingPayment(pendingInvoice2, randomBytes32(), PaymentType.SwapIn, pendingInvoice2.timestamp.seconds.toMillis, IncomingPaymentStatus.Pending) + val pendingPayment1 = IncomingPayment(pendingInvoice1, randomBytes32(), PaymentType.Standard, pendingInvoice1.timestamp.toTimestampMilli, IncomingPaymentStatus.Pending) + val pendingPayment2 = IncomingPayment(pendingInvoice2, randomBytes32(), PaymentType.SwapIn, pendingInvoice2.timestamp.toTimestampMilli, IncomingPaymentStatus.Pending) val paidInvoice1 = PaymentRequest(Block.TestnetGenesisBlock.hash, Some(561 msat), randomBytes32(), alicePriv, Left("invoice #5"), CltvExpiryDelta(18)) val paidInvoice2 = PaymentRequest(Block.TestnetGenesisBlock.hash, Some(1105 msat), randomBytes32(), bobPriv, Left("invoice #6"), CltvExpiryDelta(18), expirySeconds = Some(60)) - val receivedAt1 = System.currentTimeMillis + 1 - val receivedAt2 = System.currentTimeMillis + 2 - val payment1 = IncomingPayment(paidInvoice1, randomBytes32(), PaymentType.Standard, paidInvoice1.timestamp.seconds.toMillis, IncomingPaymentStatus.Received(561 msat, receivedAt2)) - val payment2 = IncomingPayment(paidInvoice2, randomBytes32(), PaymentType.Standard, paidInvoice2.timestamp.seconds.toMillis, IncomingPaymentStatus.Received(1111 msat, receivedAt2)) + val receivedAt1 = TimestampMilli.now() + 1.milli + val receivedAt2 = TimestampMilli.now() + 2.milli + val payment1 = IncomingPayment(paidInvoice1, randomBytes32(), PaymentType.Standard, paidInvoice1.timestamp.toTimestampMilli, IncomingPaymentStatus.Received(561 msat, receivedAt2)) + val payment2 = IncomingPayment(paidInvoice2, randomBytes32(), PaymentType.Standard, paidInvoice2.timestamp.toTimestampMilli, IncomingPaymentStatus.Received(1111 msat, receivedAt2)) db.addIncomingPayment(pendingInvoice1, pendingPayment1.paymentPreimage) db.addIncomingPayment(pendingInvoice2, pendingPayment2.paymentPreimage, PaymentType.SwapIn) @@ -429,21 +429,21 @@ class PaymentsDbSpec extends AnyFunSuite { assert(db.getIncomingPayment(expiredInvoice2.paymentHash) === Some(expiredPayment2)) assert(db.getIncomingPayment(paidInvoice1.paymentHash) === Some(payment1.copy(status = IncomingPaymentStatus.Pending))) - val now = System.currentTimeMillis - assert(db.listIncomingPayments(0, now) === Seq(expiredPayment1, expiredPayment2, pendingPayment1, pendingPayment2, payment1.copy(status = IncomingPaymentStatus.Pending), payment2.copy(status = IncomingPaymentStatus.Pending))) - assert(db.listExpiredIncomingPayments(0, now) === Seq(expiredPayment1, expiredPayment2)) - assert(db.listReceivedIncomingPayments(0, now) === Nil) - assert(db.listPendingIncomingPayments(0, now) === Seq(pendingPayment1, pendingPayment2, payment1.copy(status = IncomingPaymentStatus.Pending), payment2.copy(status = IncomingPaymentStatus.Pending))) + val now = TimestampMilli.now() + assert(db.listIncomingPayments(0 unixms, now) === Seq(expiredPayment1, expiredPayment2, pendingPayment1, pendingPayment2, payment1.copy(status = IncomingPaymentStatus.Pending), payment2.copy(status = IncomingPaymentStatus.Pending))) + assert(db.listExpiredIncomingPayments(0 unixms, now) === Seq(expiredPayment1, expiredPayment2)) + assert(db.listReceivedIncomingPayments(0 unixms, now) === Nil) + assert(db.listPendingIncomingPayments(0 unixms, now) === Seq(pendingPayment1, pendingPayment2, payment1.copy(status = IncomingPaymentStatus.Pending), payment2.copy(status = IncomingPaymentStatus.Pending))) db.receiveIncomingPayment(paidInvoice1.paymentHash, 461 msat, receivedAt1) db.receiveIncomingPayment(paidInvoice1.paymentHash, 100 msat, receivedAt2) // adding another payment to this invoice should sum db.receiveIncomingPayment(paidInvoice2.paymentHash, 1111 msat, receivedAt2) assert(db.getIncomingPayment(paidInvoice1.paymentHash) === Some(payment1)) - assert(db.listIncomingPayments(0, now) === Seq(expiredPayment1, expiredPayment2, pendingPayment1, pendingPayment2, payment1, payment2)) - assert(db.listIncomingPayments(now - 60.seconds.toMillis, now) === Seq(pendingPayment1, pendingPayment2, payment1, payment2)) - assert(db.listPendingIncomingPayments(0, now) === Seq(pendingPayment1, pendingPayment2)) - assert(db.listReceivedIncomingPayments(0, now) === Seq(payment1, payment2)) + assert(db.listIncomingPayments(0 unixms, now) === Seq(expiredPayment1, expiredPayment2, pendingPayment1, pendingPayment2, payment1, payment2)) + assert(db.listIncomingPayments(now - 60.seconds, now) === Seq(pendingPayment1, pendingPayment2, payment1, payment2)) + assert(db.listPendingIncomingPayments(0 unixms, now) === Seq(pendingPayment1, pendingPayment2)) + assert(db.listReceivedIncomingPayments(0 unixms, now) === Seq(payment1, payment2)) } } @@ -452,20 +452,20 @@ class PaymentsDbSpec extends AnyFunSuite { val db = dbs.payments val parentId = UUID.randomUUID() - val i1 = PaymentRequest(Block.TestnetGenesisBlock.hash, Some(123 msat), paymentHash1, davePriv, Left("Some invoice"), CltvExpiryDelta(18), expirySeconds = None, timestamp = 0) - val s1 = OutgoingPayment(UUID.randomUUID(), parentId, None, paymentHash1, PaymentType.Standard, 123 msat, 600 msat, dave, 100, Some(i1), OutgoingPaymentStatus.Pending) - val s2 = OutgoingPayment(UUID.randomUUID(), parentId, Some("1"), paymentHash1, PaymentType.SwapOut, 456 msat, 600 msat, dave, 200, None, OutgoingPaymentStatus.Pending) + val i1 = PaymentRequest(Block.TestnetGenesisBlock.hash, Some(123 msat), paymentHash1, davePriv, Left("Some invoice"), CltvExpiryDelta(18), expirySeconds = None, timestamp = 0 unixsec) + val s1 = OutgoingPayment(UUID.randomUUID(), parentId, None, paymentHash1, PaymentType.Standard, 123 msat, 600 msat, dave, 100 unixms, Some(i1), OutgoingPaymentStatus.Pending) + val s2 = OutgoingPayment(UUID.randomUUID(), parentId, Some("1"), paymentHash1, PaymentType.SwapOut, 456 msat, 600 msat, dave, 200 unixms, None, OutgoingPaymentStatus.Pending) - assert(db.listOutgoingPayments(0, System.currentTimeMillis).isEmpty) + assert(db.listOutgoingPayments(0 unixms, TimestampMilli.now()).isEmpty) db.addOutgoingPayment(s1) db.addOutgoingPayment(s2) // can't add an outgoing payment in non-pending state - assertThrows[IllegalArgumentException](db.addOutgoingPayment(s1.copy(status = OutgoingPaymentStatus.Succeeded(randomBytes32(), 0 msat, Nil, 110)))) + assertThrows[IllegalArgumentException](db.addOutgoingPayment(s1.copy(status = OutgoingPaymentStatus.Succeeded(randomBytes32(), 0 msat, Nil, 110 unixms)))) - assert(db.listOutgoingPayments(1, 300).toList == Seq(s1, s2)) - assert(db.listOutgoingPayments(1, 150).toList == Seq(s1)) - assert(db.listOutgoingPayments(150, 250).toList == Seq(s2)) + assert(db.listOutgoingPayments(1 unixms, 300 unixms).toList == Seq(s1, s2)) + assert(db.listOutgoingPayments(1 unixms, 150 unixms).toList == Seq(s1)) + assert(db.listOutgoingPayments(150 unixms, 250 unixms).toList == Seq(s2)) assert(db.getOutgoingPayment(s1.id) === Some(s1)) assert(db.getOutgoingPayment(UUID.randomUUID()) === None) assert(db.listOutgoingPayments(s2.paymentHash) === Seq(s1, s2)) @@ -473,27 +473,27 @@ class PaymentsDbSpec extends AnyFunSuite { assert(db.listOutgoingPayments(parentId) === Seq(s1, s2)) assert(db.listOutgoingPayments(ByteVector32.Zeroes) === Nil) - val s3 = s2.copy(id = UUID.randomUUID(), amount = 789 msat, createdAt = 300) - val s4 = s2.copy(id = UUID.randomUUID(), paymentType = PaymentType.Standard, createdAt = 301) + val s3 = s2.copy(id = UUID.randomUUID(), amount = 789 msat, createdAt = 300 unixms) + val s4 = s2.copy(id = UUID.randomUUID(), paymentType = PaymentType.Standard, createdAt = 301 unixms) db.addOutgoingPayment(s3) db.addOutgoingPayment(s4) - db.updateOutgoingPayment(PaymentFailed(s3.id, s3.paymentHash, Nil, 310)) - val ss3 = s3.copy(status = OutgoingPaymentStatus.Failed(Nil, 310)) + db.updateOutgoingPayment(PaymentFailed(s3.id, s3.paymentHash, Nil, 310 unixms)) + val ss3 = s3.copy(status = OutgoingPaymentStatus.Failed(Nil, 310 unixms)) assert(db.getOutgoingPayment(s3.id) === Some(ss3)) - db.updateOutgoingPayment(PaymentFailed(s4.id, s4.paymentHash, Seq(LocalFailure(s4.amount, Seq(hop_ab), new RuntimeException("woops")), RemoteFailure(s4.amount, Seq(hop_ab, hop_bc), Sphinx.DecryptedFailurePacket(carol, UnknownNextPeer))), 320)) - val ss4 = s4.copy(status = OutgoingPaymentStatus.Failed(Seq(FailureSummary(FailureType.LOCAL, "woops", List(HopSummary(alice, bob, Some(ShortChannelId(42))))), FailureSummary(FailureType.REMOTE, "processing node does not know the next peer in the route", List(HopSummary(alice, bob, Some(ShortChannelId(42))), HopSummary(bob, carol, None)))), 320)) + db.updateOutgoingPayment(PaymentFailed(s4.id, s4.paymentHash, Seq(LocalFailure(s4.amount, Seq(hop_ab), new RuntimeException("woops")), RemoteFailure(s4.amount, Seq(hop_ab, hop_bc), Sphinx.DecryptedFailurePacket(carol, UnknownNextPeer))), 320 unixms)) + val ss4 = s4.copy(status = OutgoingPaymentStatus.Failed(Seq(FailureSummary(FailureType.LOCAL, "woops", List(HopSummary(alice, bob, Some(ShortChannelId(42))))), FailureSummary(FailureType.REMOTE, "processing node does not know the next peer in the route", List(HopSummary(alice, bob, Some(ShortChannelId(42))), HopSummary(bob, carol, None)))), 320 unixms)) assert(db.getOutgoingPayment(s4.id) === Some(ss4)) // can't update again once it's in a final state assertThrows[IllegalArgumentException](db.updateOutgoingPayment(PaymentSent(parentId, s3.paymentHash, preimage1, s3.recipientAmount, s3.recipientNodeId, Seq(PaymentSent.PartialPayment(s3.id, s3.amount, 42 msat, randomBytes32(), None))))) val paymentSent = PaymentSent(parentId, paymentHash1, preimage1, 600 msat, carol, Seq( - PaymentSent.PartialPayment(s1.id, s1.amount, 15 msat, randomBytes32(), None, 400), - PaymentSent.PartialPayment(s2.id, s2.amount, 20 msat, randomBytes32(), Some(Seq(hop_ab, hop_bc)), 410) + PaymentSent.PartialPayment(s1.id, s1.amount, 15 msat, randomBytes32(), None, 400 unixms), + PaymentSent.PartialPayment(s2.id, s2.amount, 20 msat, randomBytes32(), Some(Seq(hop_ab, hop_bc)), 410 unixms) )) - val ss1 = s1.copy(status = OutgoingPaymentStatus.Succeeded(preimage1, 15 msat, Nil, 400)) - val ss2 = s2.copy(status = OutgoingPaymentStatus.Succeeded(preimage1, 20 msat, Seq(HopSummary(alice, bob, Some(ShortChannelId(42))), HopSummary(bob, carol, None)), 410)) + val ss1 = s1.copy(status = OutgoingPaymentStatus.Succeeded(preimage1, 15 msat, Nil, 400 unixms)) + val ss2 = s2.copy(status = OutgoingPaymentStatus.Succeeded(preimage1, 20 msat, Seq(HopSummary(alice, bob, Some(ShortChannelId(42))), HopSummary(bob, carol, None)), 410 unixms)) db.updateOutgoingPayment(paymentSent) assert(db.getOutgoingPayment(s1.id) === Some(ss1)) assert(db.getOutgoingPayment(s2.id) === Some(ss2)) @@ -508,16 +508,16 @@ class PaymentsDbSpec extends AnyFunSuite { val db = new SqlitePaymentsDb(TestDatabases.sqliteInMemory()) // -- feed db with incoming payments - val expiredInvoice = PaymentRequest(Block.TestnetGenesisBlock.hash, Some(123 msat), randomBytes32(), alicePriv, Left("incoming #1"), CltvExpiryDelta(18), timestamp = 1) - val expiredPayment = IncomingPayment(expiredInvoice, randomBytes32(), PaymentType.Standard, 100, IncomingPaymentStatus.Expired) + val expiredInvoice = PaymentRequest(Block.TestnetGenesisBlock.hash, Some(123 msat), randomBytes32(), alicePriv, Left("incoming #1"), CltvExpiryDelta(18), timestamp = 1 unixsec) + val expiredPayment = IncomingPayment(expiredInvoice, randomBytes32(), PaymentType.Standard, 100 unixms, IncomingPaymentStatus.Expired) val pendingInvoice = PaymentRequest(Block.TestnetGenesisBlock.hash, Some(456 msat), randomBytes32(), alicePriv, Left("incoming #2"), CltvExpiryDelta(18)) - val pendingPayment = IncomingPayment(pendingInvoice, randomBytes32(), PaymentType.Standard, 120, IncomingPaymentStatus.Pending) + val pendingPayment = IncomingPayment(pendingInvoice, randomBytes32(), PaymentType.Standard, 120 unixms, IncomingPaymentStatus.Pending) val paidInvoice1 = PaymentRequest(Block.TestnetGenesisBlock.hash, Some(789 msat), randomBytes32(), alicePriv, Left("incoming #3"), CltvExpiryDelta(18)) - val receivedAt1 = 150 - val receivedPayment1 = IncomingPayment(paidInvoice1, randomBytes32(), PaymentType.Standard, 130, IncomingPaymentStatus.Received(561 msat, receivedAt1)) + val receivedAt1 = 150 unixms + val receivedPayment1 = IncomingPayment(paidInvoice1, randomBytes32(), PaymentType.Standard, 130 unixms, IncomingPaymentStatus.Received(561 msat, receivedAt1)) val paidInvoice2 = PaymentRequest(Block.TestnetGenesisBlock.hash, Some(888 msat), randomBytes32(), alicePriv, Left("incoming #4"), CltvExpiryDelta(18)) - val receivedAt2 = 160 - val receivedPayment2 = IncomingPayment(paidInvoice2, randomBytes32(), PaymentType.Standard, paidInvoice2.timestamp.seconds.toMillis, IncomingPaymentStatus.Received(889 msat, receivedAt2)) + val receivedAt2 = 160 unixms + val receivedPayment2 = IncomingPayment(paidInvoice2, randomBytes32(), PaymentType.Standard, paidInvoice2.timestamp.toTimestampMilli, IncomingPaymentStatus.Received(889 msat, receivedAt2)) db.addIncomingPayment(pendingInvoice, pendingPayment.paymentPreimage) db.addIncomingPayment(expiredInvoice, expiredPayment.paymentPreimage) db.addIncomingPayment(paidInvoice1, receivedPayment1.paymentPreimage) @@ -528,14 +528,14 @@ class PaymentsDbSpec extends AnyFunSuite { // -- feed db with outgoing payments val parentId1 = UUID.randomUUID() val parentId2 = UUID.randomUUID() - val invoice = PaymentRequest(Block.TestnetGenesisBlock.hash, Some(1337 msat), paymentHash1, davePriv, Left("outgoing #1"), CltvExpiryDelta(18), expirySeconds = None, timestamp = 0) + val invoice = PaymentRequest(Block.TestnetGenesisBlock.hash, Some(1337 msat), paymentHash1, davePriv, Left("outgoing #1"), CltvExpiryDelta(18), expirySeconds = None, timestamp = 0 unixsec) // 1st attempt, pending -> failed - val outgoing1 = OutgoingPayment(UUID.randomUUID(), parentId1, None, paymentHash1, PaymentType.Standard, 123 msat, 123 msat, alice, 200, Some(invoice), OutgoingPaymentStatus.Pending) + val outgoing1 = OutgoingPayment(UUID.randomUUID(), parentId1, None, paymentHash1, PaymentType.Standard, 123 msat, 123 msat, alice, 200 unixms, Some(invoice), OutgoingPaymentStatus.Pending) db.addOutgoingPayment(outgoing1) - db.updateOutgoingPayment(PaymentFailed(outgoing1.id, outgoing1.paymentHash, Nil, 210)) + db.updateOutgoingPayment(PaymentFailed(outgoing1.id, outgoing1.paymentHash, Nil, 210 unixms)) // 2nd attempt: pending - val outgoing2 = OutgoingPayment(UUID.randomUUID(), parentId1, None, paymentHash1, PaymentType.Standard, 123 msat, 123 msat, alice, 211, Some(invoice), OutgoingPaymentStatus.Pending) + val outgoing2 = OutgoingPayment(UUID.randomUUID(), parentId1, None, paymentHash1, PaymentType.Standard, 123 msat, 123 msat, alice, 211 unixms, Some(invoice), OutgoingPaymentStatus.Pending) db.addOutgoingPayment(outgoing2) // -- 1st check: result contains 2 incoming PAID, 1 outgoing PENDING. Outgoing1 must not be overridden by Outgoing2 @@ -546,15 +546,15 @@ class PaymentsDbSpec extends AnyFunSuite { assert(check1.head.asInstanceOf[PlainOutgoingPayment].status == OutgoingPaymentStatus.Pending) // failed #2 and add a successful payment (made of 2 partial payments) - db.updateOutgoingPayment(PaymentFailed(outgoing2.id, outgoing2.paymentHash, Nil, 250)) - val outgoing3 = OutgoingPayment(UUID.randomUUID(), parentId2, None, paymentHash1, PaymentType.Standard, 200 msat, 500 msat, bob, 300, Some(invoice), OutgoingPaymentStatus.Pending) - val outgoing4 = OutgoingPayment(UUID.randomUUID(), parentId2, None, paymentHash1, PaymentType.Standard, 300 msat, 500 msat, bob, 310, Some(invoice), OutgoingPaymentStatus.Pending) + db.updateOutgoingPayment(PaymentFailed(outgoing2.id, outgoing2.paymentHash, Nil, 250 unixms)) + val outgoing3 = OutgoingPayment(UUID.randomUUID(), parentId2, None, paymentHash1, PaymentType.Standard, 200 msat, 500 msat, bob, 300 unixms, Some(invoice), OutgoingPaymentStatus.Pending) + val outgoing4 = OutgoingPayment(UUID.randomUUID(), parentId2, None, paymentHash1, PaymentType.Standard, 300 msat, 500 msat, bob, 310 unixms, Some(invoice), OutgoingPaymentStatus.Pending) db.addOutgoingPayment(outgoing3) db.addOutgoingPayment(outgoing4) // complete #2 and #3 partial payments val sent = PaymentSent(parentId2, paymentHash1, preimage1, outgoing3.recipientAmount, outgoing3.recipientNodeId, Seq( - PaymentSent.PartialPayment(outgoing3.id, outgoing3.amount, 15 msat, randomBytes32(), None, 400), - PaymentSent.PartialPayment(outgoing4.id, outgoing4.amount, 20 msat, randomBytes32(), None, 410) + PaymentSent.PartialPayment(outgoing3.id, outgoing3.amount, 15 msat, randomBytes32(), None, 400 unixms), + PaymentSent.PartialPayment(outgoing4.id, outgoing4.amount, 20 msat, randomBytes32(), None, 410 unixms) )) db.updateOutgoingPayment(sent) @@ -583,7 +583,7 @@ class PaymentsDbSpec extends AnyFunSuite { object PaymentsDbSpec { val (alicePriv, bobPriv, carolPriv, davePriv) = (randomKey(), randomKey(), randomKey(), randomKey()) val (alice, bob, carol, dave) = (alicePriv.publicKey, bobPriv.publicKey, carolPriv.publicKey, davePriv.publicKey) - val hop_ab = ChannelHop(alice, bob, ChannelUpdate(randomBytes64(), randomBytes32(), ShortChannelId(42), 1, ChannelUpdate.ChannelFlags.DUMMY, CltvExpiryDelta(12), 1 msat, 1 msat, 1, None)) + val hop_ab = ChannelHop(alice, bob, ChannelUpdate(randomBytes64(), randomBytes32(), ShortChannelId(42), 1 unixsec, ChannelUpdate.ChannelFlags.DUMMY, CltvExpiryDelta(12), 1 msat, 1 msat, 1, None)) val hop_bc = NodeHop(bob, carol, CltvExpiryDelta(14), 1 msat) val (preimage1, preimage2, preimage3, preimage4) = (randomBytes32(), randomBytes32(), randomBytes32(), randomBytes32()) val (paymentHash1, paymentHash2, paymentHash3, paymentHash4) = (Crypto.sha256(preimage1), Crypto.sha256(preimage2), Crypto.sha256(preimage3), Crypto.sha256(preimage4)) diff --git a/eclair-core/src/test/scala/fr/acinq/eclair/db/SqliteFeeratesDbSpec.scala b/eclair-core/src/test/scala/fr/acinq/eclair/db/SqliteFeeratesDbSpec.scala index 536064ee11..e280b622c4 100644 --- a/eclair-core/src/test/scala/fr/acinq/eclair/db/SqliteFeeratesDbSpec.scala +++ b/eclair-core/src/test/scala/fr/acinq/eclair/db/SqliteFeeratesDbSpec.scala @@ -76,7 +76,7 @@ class SqliteFeeratesDbSpec extends AnyFunSuite { statement.setLong(5, feerate.blocks_36.toLong) statement.setLong(6, feerate.blocks_72.toLong) statement.setLong(7, feerate.blocks_144.toLong) - statement.setLong(8, System.currentTimeMillis()) + statement.setLong(8, TimestampMilli.now().toLong) statement.executeUpdate() } diff --git a/eclair-core/src/test/scala/fr/acinq/eclair/integration/PaymentIntegrationSpec.scala b/eclair-core/src/test/scala/fr/acinq/eclair/integration/PaymentIntegrationSpec.scala index 1349d7b9d9..fca9b79aba 100644 --- a/eclair-core/src/test/scala/fr/acinq/eclair/integration/PaymentIntegrationSpec.scala +++ b/eclair-core/src/test/scala/fr/acinq/eclair/integration/PaymentIntegrationSpec.scala @@ -43,7 +43,7 @@ import fr.acinq.eclair.router.Graph.WeightRatios import fr.acinq.eclair.router.Router.{GossipDecision, PublicChannel} import fr.acinq.eclair.router.{Announcements, AnnouncementsBatchValidationSpec, Router} import fr.acinq.eclair.wire.protocol.{ChannelAnnouncement, ChannelUpdate, IncorrectOrUnknownPaymentDetails, NodeAnnouncement} -import fr.acinq.eclair.{CltvExpiryDelta, Kit, MilliSatoshiLong, ShortChannelId, randomBytes32} +import fr.acinq.eclair.{CltvExpiryDelta, Kit, MilliSatoshiLong, ShortChannelId, TimestampMilli, randomBytes32} import org.json4s.JsonAST.{JString, JValue} import scodec.bits.ByteVector @@ -337,7 +337,7 @@ class PaymentIntegrationSpec extends IntegrationSpec { } test("send a multi-part payment B->D") { - val start = System.currentTimeMillis + val start = TimestampMilli.now() val sender = TestProbe() val amount = 1000000000L.msat sender.send(nodes("D").paymentHandler, ReceivePayment(Some(amount), Left("split the restaurant bill"))) @@ -364,8 +364,8 @@ class PaymentIntegrationSpec extends IntegrationSpec { assert(paymentParts.forall(p => p.parentId != p.id), paymentParts) assert(paymentParts.forall(p => p.status.asInstanceOf[OutgoingPaymentStatus.Succeeded].feesPaid > 0.msat), paymentParts) - awaitCond(nodes("B").nodeParams.db.audit.listSent(start, System.currentTimeMillis).nonEmpty) - val sent = nodes("B").nodeParams.db.audit.listSent(start, System.currentTimeMillis) + awaitCond(nodes("B").nodeParams.db.audit.listSent(start, TimestampMilli.now()).nonEmpty) + val sent = nodes("B").nodeParams.db.audit.listSent(start, TimestampMilli.now()) assert(sent.length === 1, sent) assert(sent.head.copy(parts = sent.head.parts.sortBy(_.timestamp)) === paymentSent.copy(parts = paymentSent.parts.map(_.copy(route = None)).sortBy(_.timestamp)), sent) @@ -460,7 +460,7 @@ class PaymentIntegrationSpec extends IntegrationSpec { } test("send a trampoline payment B->F1 with retry (via trampoline G)") { - val start = System.currentTimeMillis + val start = TimestampMilli.now() val sender = TestProbe() val amount = 4000000000L.msat sender.send(nodes("F").paymentHandler, ReceivePayment(Some(amount), Left("like trampoline much?"))) @@ -486,10 +486,10 @@ class PaymentIntegrationSpec extends IntegrationSpec { assert(receivedAmount === amount) awaitCond({ - val relayed = nodes("G").nodeParams.db.audit.listRelayed(start, System.currentTimeMillis).filter(_.paymentHash == pr.paymentHash) + val relayed = nodes("G").nodeParams.db.audit.listRelayed(start, TimestampMilli.now()).filter(_.paymentHash == pr.paymentHash) relayed.nonEmpty && relayed.head.amountOut >= amount }) - val relayed = nodes("G").nodeParams.db.audit.listRelayed(start, System.currentTimeMillis).filter(_.paymentHash == pr.paymentHash).head + val relayed = nodes("G").nodeParams.db.audit.listRelayed(start, TimestampMilli.now()).filter(_.paymentHash == pr.paymentHash).head assert(relayed.amountIn - relayed.amountOut > 0.msat, relayed) assert(relayed.amountIn - relayed.amountOut < 1000000.msat, relayed) @@ -502,7 +502,7 @@ class PaymentIntegrationSpec extends IntegrationSpec { } test("send a trampoline payment D->B (via trampoline C)") { - val start = System.currentTimeMillis + val start = TimestampMilli.now() val sender = TestProbe() val amount = 2500000000L.msat sender.send(nodes("B").paymentHandler, ReceivePayment(Some(amount), Left("trampoline-MPP is so #reckless"))) @@ -525,10 +525,10 @@ class PaymentIntegrationSpec extends IntegrationSpec { assert(receivedAmount === amount) awaitCond({ - val relayed = nodes("C").nodeParams.db.audit.listRelayed(start, System.currentTimeMillis).filter(_.paymentHash == pr.paymentHash) + val relayed = nodes("C").nodeParams.db.audit.listRelayed(start, TimestampMilli.now()).filter(_.paymentHash == pr.paymentHash) relayed.nonEmpty && relayed.head.amountOut >= amount }) - val relayed = nodes("C").nodeParams.db.audit.listRelayed(start, System.currentTimeMillis).filter(_.paymentHash == pr.paymentHash).head + val relayed = nodes("C").nodeParams.db.audit.listRelayed(start, TimestampMilli.now()).filter(_.paymentHash == pr.paymentHash).head assert(relayed.amountIn - relayed.amountOut > 0.msat, relayed) assert(relayed.amountIn - relayed.amountOut < 350000.msat, relayed) @@ -539,15 +539,15 @@ class PaymentIntegrationSpec extends IntegrationSpec { } assert(outgoingSuccess.map(_.amount).sum === amount + 350000.msat, outgoingSuccess) - awaitCond(nodes("D").nodeParams.db.audit.listSent(start, System.currentTimeMillis).nonEmpty) - val sent = nodes("D").nodeParams.db.audit.listSent(start, System.currentTimeMillis) + awaitCond(nodes("D").nodeParams.db.audit.listSent(start, TimestampMilli.now()).nonEmpty) + val sent = nodes("D").nodeParams.db.audit.listSent(start, TimestampMilli.now()) assert(sent.length === 1, sent) assert(sent.head.copy(parts = sent.head.parts.sortBy(_.timestamp)) === paymentSent.copy(parts = paymentSent.parts.map(_.copy(route = None)).sortBy(_.timestamp)), sent) } test("send a trampoline payment F1->A (via trampoline C, non-trampoline recipient)") { // The A -> B channel is not announced. - val start = System.currentTimeMillis + val start = TimestampMilli.now() val sender = TestProbe() sender.send(nodes("B").relayer, Relayer.GetOutgoingChannels()) val channelUpdate_ba = sender.expectMsgType[Relayer.OutgoingChannels].channels.filter(c => c.nextNodeId == nodes("A").nodeParams.nodeId).head.channelUpdate @@ -573,10 +573,10 @@ class PaymentIntegrationSpec extends IntegrationSpec { assert(receivedAmount === amount) awaitCond({ - val relayed = nodes("C").nodeParams.db.audit.listRelayed(start, System.currentTimeMillis).filter(_.paymentHash == pr.paymentHash) + val relayed = nodes("C").nodeParams.db.audit.listRelayed(start, TimestampMilli.now()).filter(_.paymentHash == pr.paymentHash) relayed.nonEmpty && relayed.head.amountOut >= amount }) - val relayed = nodes("C").nodeParams.db.audit.listRelayed(start, System.currentTimeMillis).filter(_.paymentHash == pr.paymentHash).head + val relayed = nodes("C").nodeParams.db.audit.listRelayed(start, TimestampMilli.now()).filter(_.paymentHash == pr.paymentHash).head assert(relayed.amountIn - relayed.amountOut > 0.msat, relayed) assert(relayed.amountIn - relayed.amountOut < 1000000.msat, relayed) diff --git a/eclair-core/src/test/scala/fr/acinq/eclair/integration/PerformanceIntegrationSpec.scala b/eclair-core/src/test/scala/fr/acinq/eclair/integration/PerformanceIntegrationSpec.scala index 7270d38934..9097df797c 100644 --- a/eclair-core/src/test/scala/fr/acinq/eclair/integration/PerformanceIntegrationSpec.scala +++ b/eclair-core/src/test/scala/fr/acinq/eclair/integration/PerformanceIntegrationSpec.scala @@ -19,7 +19,7 @@ package fr.acinq.eclair.integration import akka.testkit.TestProbe import com.typesafe.config.ConfigFactory import fr.acinq.bitcoin.SatoshiLong -import fr.acinq.eclair.MilliSatoshiLong +import fr.acinq.eclair.{MilliSatoshiLong, TimestampMilli} import fr.acinq.eclair.channel._ import fr.acinq.eclair.payment._ import fr.acinq.eclair.payment.receive.MultiPartHandler.ReceivePayment @@ -97,14 +97,14 @@ class PerformanceIntegrationSpec extends IntegrationSpec { val SENDERS_COUNT = 16 val PAYMENTS_COUNT = 3_000 val ec = ExecutionContext.fromExecutor(Executors.newFixedThreadPool(SENDERS_COUNT)) - val start = System.currentTimeMillis() + val start = TimestampMilli.now() val futures = (0 until PAYMENTS_COUNT).map(_ => sendPayment()(ec)) implicit val dummyEc: ExecutionContext = ExecutionContext.Implicits.global val f = Future.sequence(futures) Await.result(f, 1 hour) - val end = System.currentTimeMillis() + val end = TimestampMilli.now() val duration = end - start - println(s"$PAYMENTS_COUNT payments in ${duration}ms ${PAYMENTS_COUNT * 1000 / duration}htlc/s (senders=$SENDERS_COUNT)") + println(s"$PAYMENTS_COUNT payments in ${duration.toMillis}ms ${PAYMENTS_COUNT * 1000 / duration.toMillis}htlc/s (senders=$SENDERS_COUNT)") } } diff --git a/eclair-core/src/test/scala/fr/acinq/eclair/io/PeerConnectionSpec.scala b/eclair-core/src/test/scala/fr/acinq/eclair/io/PeerConnectionSpec.scala index f85d1d9b7e..4abce782e7 100644 --- a/eclair-core/src/test/scala/fr/acinq/eclair/io/PeerConnectionSpec.scala +++ b/eclair-core/src/test/scala/fr/acinq/eclair/io/PeerConnectionSpec.scala @@ -266,7 +266,7 @@ class PeerConnectionSpec extends TestKitBaseClass with FixtureAnyFunSuiteLike wi channels.map(_ -> gossipOrigin).toMap + (channels(5) -> Set(bobOrigin)), updates.map(_ -> gossipOrigin).toMap + (updates(6) -> (gossipOrigin + bobOrigin)) + (updates(10) -> Set(bobOrigin)), nodes.map(_ -> gossipOrigin).toMap + (nodes(4) -> Set(bobOrigin))) - val filter = protocol.GossipTimestampFilter(Alice.nodeParams.chainHash, 0, Long.MaxValue) // no filtering on timestamps + val filter = protocol.GossipTimestampFilter(Alice.nodeParams.chainHash, 0 unixsec, Long.MaxValue) // no filtering on timestamps transport.send(peerConnection, filter) transport.expectMsg(TransportHandler.ReadAck(filter)) transport.send(peerConnection, rebroadcast) @@ -282,7 +282,7 @@ class PeerConnectionSpec extends TestKitBaseClass with FixtureAnyFunSuiteLike wi val gossipOrigin = Set[GossipOrigin](RemoteGossip(TestProbe().ref, randomKey().publicKey)) val rebroadcast = Rebroadcast(channels.map(_ -> gossipOrigin).toMap, updates.map(_ -> gossipOrigin).toMap, nodes.map(_ -> gossipOrigin).toMap) val timestamps = updates.map(_.timestamp).sorted.slice(10, 30) - val filter = protocol.GossipTimestampFilter(Alice.nodeParams.chainHash, timestamps.head, timestamps.last - timestamps.head) + val filter = protocol.GossipTimestampFilter(Alice.nodeParams.chainHash, timestamps.head, (timestamps.last - timestamps.head).toSeconds) transport.send(peerConnection, filter) transport.expectMsg(TransportHandler.ReadAck(filter)) transport.send(peerConnection, rebroadcast) diff --git a/eclair-core/src/test/scala/fr/acinq/eclair/io/PeerSpec.scala b/eclair-core/src/test/scala/fr/acinq/eclair/io/PeerSpec.scala index 2d69084bb5..e518717025 100644 --- a/eclair-core/src/test/scala/fr/acinq/eclair/io/PeerSpec.scala +++ b/eclair-core/src/test/scala/fr/acinq/eclair/io/PeerSpec.scala @@ -73,7 +73,7 @@ class PeerSpec extends TestKitBaseClass with FixtureAnyFunSuiteLike with Paralle .modify(_.autoReconnect).setToIf(test.tags.contains("auto_reconnect"))(true) if (test.tags.contains("with_node_announcement")) { - val bobAnnouncement = NodeAnnouncement(randomBytes64(), Features.empty, 1, Bob.nodeParams.nodeId, Color(100.toByte, 200.toByte, 300.toByte), "node-alias", fakeIPAddress :: Nil) + val bobAnnouncement = NodeAnnouncement(randomBytes64(), Features.empty, 1 unixsec, Bob.nodeParams.nodeId, Color(100.toByte, 200.toByte, 300.toByte), "node-alias", fakeIPAddress :: Nil) aliceParams.db.network.addNode(bobAnnouncement) } @@ -140,7 +140,7 @@ class PeerSpec extends TestKitBaseClass with FixtureAnyFunSuiteLike with Paralle val mockAddress = NodeAddress.fromParts(serverAddress.getHostName, serverAddress.getPort).get // we put the server address in the node db - val ann = NodeAnnouncement(randomBytes64(), Features.empty, 1, Bob.nodeParams.nodeId, Color(100.toByte, 200.toByte, 300.toByte), "node-alias", mockAddress :: Nil) + val ann = NodeAnnouncement(randomBytes64(), Features.empty, 1 unixsec, Bob.nodeParams.nodeId, Color(100.toByte, 200.toByte, 300.toByte), "node-alias", mockAddress :: Nil) nodeParams.db.network.addNode(ann) val probe = TestProbe() diff --git a/eclair-core/src/test/scala/fr/acinq/eclair/io/ReconnectionTaskSpec.scala b/eclair-core/src/test/scala/fr/acinq/eclair/io/ReconnectionTaskSpec.scala index ee8e32c96b..d179e37eaf 100644 --- a/eclair-core/src/test/scala/fr/acinq/eclair/io/ReconnectionTaskSpec.scala +++ b/eclair-core/src/test/scala/fr/acinq/eclair/io/ReconnectionTaskSpec.scala @@ -48,7 +48,7 @@ class ReconnectionTaskSpec extends TestKitBaseClass with FixtureAnyFunSuiteLike .modify(_.autoReconnect).setToIf(test.tags.contains("auto_reconnect"))(true) if (test.tags.contains("with_node_announcements")) { - val bobAnnouncement = NodeAnnouncement(randomBytes64(), Features.empty, 1, remoteNodeId, Color(100.toByte, 200.toByte, 300.toByte), "node-alias", fakeIPAddress :: Nil) + val bobAnnouncement = NodeAnnouncement(randomBytes64(), Features.empty, 1 unixsec, remoteNodeId, Color(100.toByte, 200.toByte, 300.toByte), "node-alias", fakeIPAddress :: Nil) aliceParams.db.network.addNode(bobAnnouncement) } @@ -116,7 +116,7 @@ class ReconnectionTaskSpec extends TestKitBaseClass with FixtureAnyFunSuiteLike val TransitionWithData(ReconnectionTask.CONNECTING, ReconnectionTask.IDLE, _, _) = monitor.expectMsgType[TransitionWithData] // NB: we change the data to make it appear like we have been connected for a long time - reconnectionTask.setState(stateData = reconnectionTask.stateData.asInstanceOf[ReconnectionTask.IdleData].copy(since = 0.seconds)) + reconnectionTask.setState(stateData = reconnectionTask.stateData.asInstanceOf[ReconnectionTask.IdleData].copy(since = 0 unixms)) val TransitionWithData(ReconnectionTask.IDLE, ReconnectionTask.IDLE, _, _) = monitor.expectMsgType[TransitionWithData] // disconnection @@ -208,7 +208,7 @@ class ReconnectionTaskSpec extends TestKitBaseClass with FixtureAnyFunSuiteLike // we create a dummy tcp server and update bob's announcement to point to it val (mockServer, serverAddress) = PeerSpec.createMockServer() val mockAddress = NodeAddress.fromParts(serverAddress.getHostName, serverAddress.getPort).get - val bobAnnouncement = NodeAnnouncement(randomBytes64(), Features.empty, 1, remoteNodeId, Color(100.toByte, 200.toByte, 300.toByte), "node-alias", mockAddress :: Nil) + val bobAnnouncement = NodeAnnouncement(randomBytes64(), Features.empty, 1 unixsec, remoteNodeId, Color(100.toByte, 200.toByte, 300.toByte), "node-alias", mockAddress :: Nil) nodeParams.db.network.addNode(bobAnnouncement) val peer = TestProbe() diff --git a/eclair-core/src/test/scala/fr/acinq/eclair/io/SwitchboardSpec.scala b/eclair-core/src/test/scala/fr/acinq/eclair/io/SwitchboardSpec.scala index bd1d24d2f1..ac4c331e27 100644 --- a/eclair-core/src/test/scala/fr/acinq/eclair/io/SwitchboardSpec.scala +++ b/eclair-core/src/test/scala/fr/acinq/eclair/io/SwitchboardSpec.scala @@ -9,7 +9,7 @@ import fr.acinq.eclair.channel.ChannelIdAssigned import fr.acinq.eclair.io.Switchboard.PeerFactory import fr.acinq.eclair.wire.internal.channel.ChannelCodecsSpec import fr.acinq.eclair.wire.protocol._ -import fr.acinq.eclair.{Features, NodeParams, TestKitBaseClass, randomBytes32, randomKey} +import fr.acinq.eclair.{Features, NodeParams, TestKitBaseClass, TimestampSecondLong, randomBytes32, randomKey} import org.scalatest.funsuite.AnyFunSuiteLike import scodec.bits._ @@ -38,7 +38,7 @@ class SwitchboardSpec extends TestKitBaseClass with AnyFunSuiteLike { val (probe, peer) = (TestProbe(), TestProbe()) val remoteNodeId = PublicKey(hex"03864ef025fde8fb587d989186ce6a4a186895ee44a926bfc370e2c366597a3f8f") val remoteNodeAddress = NodeAddress.fromParts("127.0.0.1", 9735).get - nodeParams.db.network.addNode(NodeAnnouncement(ByteVector64.Zeroes, Features.empty, 0, remoteNodeId, Color(0, 0, 0), "alias", remoteNodeAddress :: Nil)) + nodeParams.db.network.addNode(NodeAnnouncement(ByteVector64.Zeroes, Features.empty, 0 unixsec, remoteNodeId, Color(0, 0, 0), "alias", remoteNodeAddress :: Nil)) val switchboard = TestActorRef(new Switchboard(nodeParams, FakePeerFactory(remoteNodeId, peer))) probe.send(switchboard, Peer.Connect(remoteNodeId, None)) diff --git a/eclair-core/src/test/scala/fr/acinq/eclair/json/JsonSerializersSpec.scala b/eclair-core/src/test/scala/fr/acinq/eclair/json/JsonSerializersSpec.scala index 96c0fbb8a0..508f99bc59 100644 --- a/eclair-core/src/test/scala/fr/acinq/eclair/json/JsonSerializersSpec.scala +++ b/eclair-core/src/test/scala/fr/acinq/eclair/json/JsonSerializersSpec.scala @@ -266,6 +266,13 @@ class JsonSerializersSpec extends AnyFunSuite with Matchers { } + test("serialize timestamps") { + val ts = TimestampSecond(1633357961) + JsonSerializers.serialization.write(ts)(JsonSerializers.formats) shouldBe """{"iso":"2021-10-04T14:32:41Z","unix":1633357961}""" + val tsms = TimestampMilli(1633357961456L) + JsonSerializers.serialization.write(tsms)(JsonSerializers.formats) shouldBe """{"iso":"2021-10-04T14:32:41.456Z","unix":1633357961}""" + } + /** utility method that strips line breaks in the expected json */ def assertJsonEquals(actual: String, expected: String) = { val cleanedExpected = expected diff --git a/eclair-core/src/test/scala/fr/acinq/eclair/payment/MultiPartHandlerSpec.scala b/eclair-core/src/test/scala/fr/acinq/eclair/payment/MultiPartHandlerSpec.scala index 18353137ac..7dab48ae41 100644 --- a/eclair-core/src/test/scala/fr/acinq/eclair/payment/MultiPartHandlerSpec.scala +++ b/eclair-core/src/test/scala/fr/acinq/eclair/payment/MultiPartHandlerSpec.scala @@ -32,7 +32,7 @@ import fr.acinq.eclair.payment.receive.MultiPartPaymentFSM.HtlcPart import fr.acinq.eclair.payment.receive.{MultiPartPaymentFSM, PaymentHandler} import fr.acinq.eclair.wire.protocol.Onion.FinalTlvPayload import fr.acinq.eclair.wire.protocol._ -import fr.acinq.eclair.{CltvExpiry, CltvExpiryDelta, Features, MilliSatoshiLong, NodeParams, ShortChannelId, TestConstants, TestKitBaseClass, randomBytes32, randomKey} +import fr.acinq.eclair.{CltvExpiry, CltvExpiryDelta, Features, MilliSatoshiLong, NodeParams, ShortChannelId, TestConstants, TestKitBaseClass, TimestampMilliLong, randomBytes32, randomKey} import org.scalatest.Outcome import org.scalatest.funsuite.FixtureAnyFunSuiteLike @@ -96,10 +96,10 @@ class MultiPartHandlerSpec extends TestKitBaseClass with FixtureAnyFunSuiteLike register.expectMsgType[Register.Forward[CMD_FULFILL_HTLC]] val paymentReceived = eventListener.expectMsgType[PaymentReceived] - assert(paymentReceived.copy(parts = paymentReceived.parts.map(_.copy(timestamp = 0))) === PaymentReceived(add.paymentHash, PartialPayment(amountMsat, add.channelId, timestamp = 0) :: Nil)) + assert(paymentReceived.copy(parts = paymentReceived.parts.map(_.copy(timestamp = 0 unixms))) === PaymentReceived(add.paymentHash, PartialPayment(amountMsat, add.channelId, timestamp = 0 unixms) :: Nil)) val received = nodeParams.db.payments.getIncomingPayment(pr.paymentHash) assert(received.isDefined && received.get.status.isInstanceOf[IncomingPaymentStatus.Received]) - assert(received.get.status.asInstanceOf[IncomingPaymentStatus.Received].copy(receivedAt = 0) === IncomingPaymentStatus.Received(amountMsat, 0)) + assert(received.get.status.asInstanceOf[IncomingPaymentStatus.Received].copy(receivedAt = 0 unixms) === IncomingPaymentStatus.Received(amountMsat, 0 unixms)) sender.expectNoMessage(50 millis) } @@ -115,10 +115,10 @@ class MultiPartHandlerSpec extends TestKitBaseClass with FixtureAnyFunSuiteLike register.expectMsgType[Register.Forward[CMD_FULFILL_HTLC]] val paymentReceived = eventListener.expectMsgType[PaymentReceived] - assert(paymentReceived.copy(parts = paymentReceived.parts.map(_.copy(timestamp = 0))) === PaymentReceived(add.paymentHash, PartialPayment(amountMsat, add.channelId, timestamp = 0) :: Nil)) + assert(paymentReceived.copy(parts = paymentReceived.parts.map(_.copy(timestamp = 0 unixms))) === PaymentReceived(add.paymentHash, PartialPayment(amountMsat, add.channelId, timestamp = 0 unixms) :: Nil)) val received = nodeParams.db.payments.getIncomingPayment(pr.paymentHash) assert(received.isDefined && received.get.status.isInstanceOf[IncomingPaymentStatus.Received]) - assert(received.get.status.asInstanceOf[IncomingPaymentStatus.Received].copy(receivedAt = 0) === IncomingPaymentStatus.Received(amountMsat, 0)) + assert(received.get.status.asInstanceOf[IncomingPaymentStatus.Received].copy(receivedAt = 0 unixms) === IncomingPaymentStatus.Received(amountMsat, 0 unixms)) sender.expectNoMessage(50 millis) } @@ -414,7 +414,7 @@ class MultiPartHandlerSpec extends TestKitBaseClass with FixtureAnyFunSuiteLike ) val paymentReceived = f.eventListener.expectMsgType[PaymentReceived] - assert(paymentReceived.parts.map(_.copy(timestamp = 0)).toSet === Set(PartialPayment(800 msat, ByteVector32.One, 0), PartialPayment(200 msat, ByteVector32.Zeroes, 0))) + assert(paymentReceived.parts.map(_.copy(timestamp = 0 unixms)).toSet === Set(PartialPayment(800 msat, ByteVector32.One, 0 unixms), PartialPayment(200 msat, ByteVector32.Zeroes, 0 unixms))) val received = nodeParams.db.payments.getIncomingPayment(pr.paymentHash) assert(received.isDefined && received.get.status.isInstanceOf[IncomingPaymentStatus.Received]) assert(received.get.status.asInstanceOf[IncomingPaymentStatus.Received].amount === 1000.msat) @@ -465,7 +465,7 @@ class MultiPartHandlerSpec extends TestKitBaseClass with FixtureAnyFunSuiteLike val paymentReceived = f.eventListener.expectMsgType[PaymentReceived] assert(paymentReceived.paymentHash === pr.paymentHash) - assert(paymentReceived.parts.map(_.copy(timestamp = 0)).toSet === Set(PartialPayment(300 msat, ByteVector32.One, 0), PartialPayment(700 msat, ByteVector32.Zeroes, 0))) + assert(paymentReceived.parts.map(_.copy(timestamp = 0 unixms)).toSet === Set(PartialPayment(300 msat, ByteVector32.One, 0 unixms), PartialPayment(700 msat, ByteVector32.Zeroes, 0 unixms))) val received = nodeParams.db.payments.getIncomingPayment(pr.paymentHash) assert(received.isDefined && received.get.status.isInstanceOf[IncomingPaymentStatus.Received]) assert(received.get.status.asInstanceOf[IncomingPaymentStatus.Received].amount === 1000.msat) @@ -491,10 +491,10 @@ class MultiPartHandlerSpec extends TestKitBaseClass with FixtureAnyFunSuiteLike register.expectMsgType[Register.Forward[CMD_FULFILL_HTLC]] val paymentReceived = eventListener.expectMsgType[PaymentReceived] - assert(paymentReceived.copy(parts = paymentReceived.parts.map(_.copy(timestamp = 0))) === PaymentReceived(add.paymentHash, PartialPayment(amountMsat, add.channelId, timestamp = 0) :: Nil)) + assert(paymentReceived.copy(parts = paymentReceived.parts.map(_.copy(timestamp = 0 unixms))) === PaymentReceived(add.paymentHash, PartialPayment(amountMsat, add.channelId, timestamp = 0 unixms) :: Nil)) val received = nodeParams.db.payments.getIncomingPayment(paymentHash) assert(received.isDefined && received.get.status.isInstanceOf[IncomingPaymentStatus.Received]) - assert(received.get.status.asInstanceOf[IncomingPaymentStatus.Received].copy(receivedAt = 0) === IncomingPaymentStatus.Received(amountMsat, 0)) + assert(received.get.status.asInstanceOf[IncomingPaymentStatus.Received].copy(receivedAt = 0 unixms) === IncomingPaymentStatus.Received(amountMsat, 0 unixms)) } test("PaymentHandler should reject KeySend payment when feature is disabled") { f => diff --git a/eclair-core/src/test/scala/fr/acinq/eclair/payment/MultiPartPaymentLifecycleSpec.scala b/eclair-core/src/test/scala/fr/acinq/eclair/payment/MultiPartPaymentLifecycleSpec.scala index 0c39c73fc8..e2b12fc7c4 100644 --- a/eclair-core/src/test/scala/fr/acinq/eclair/payment/MultiPartPaymentLifecycleSpec.scala +++ b/eclair-core/src/test/scala/fr/acinq/eclair/payment/MultiPartPaymentLifecycleSpec.scala @@ -687,7 +687,7 @@ object MultiPartPaymentLifecycleSpec { val channelId_ce = ShortChannelId(13) val channelId_ad = ShortChannelId(21) val channelId_de = ShortChannelId(22) - val defaultChannelUpdate = ChannelUpdate(randomBytes64(), Block.RegtestGenesisBlock.hash, ShortChannelId(0), 0, ChannelUpdate.ChannelFlags.DUMMY, CltvExpiryDelta(12), 1 msat, 100 msat, 0, Some(2000000 msat)) + val defaultChannelUpdate = ChannelUpdate(randomBytes64(), Block.RegtestGenesisBlock.hash, ShortChannelId(0), 0 unixsec, ChannelUpdate.ChannelFlags.DUMMY, CltvExpiryDelta(12), 1 msat, 100 msat, 0, Some(2000000 msat)) val channelUpdate_ab_1 = defaultChannelUpdate.copy(shortChannelId = channelId_ab_1) val channelUpdate_ab_2 = defaultChannelUpdate.copy(shortChannelId = channelId_ab_2) val channelUpdate_be = defaultChannelUpdate.copy(shortChannelId = channelId_be) diff --git a/eclair-core/src/test/scala/fr/acinq/eclair/payment/PaymentLifecycleSpec.scala b/eclair-core/src/test/scala/fr/acinq/eclair/payment/PaymentLifecycleSpec.scala index 2228c3764c..691a9f0610 100644 --- a/eclair-core/src/test/scala/fr/acinq/eclair/payment/PaymentLifecycleSpec.scala +++ b/eclair-core/src/test/scala/fr/acinq/eclair/payment/PaymentLifecycleSpec.scala @@ -110,7 +110,7 @@ class PaymentLifecycleSpec extends BaseRouterSpec { val Transition(_, WAITING_FOR_ROUTE, WAITING_FOR_PAYMENT_COMPLETE) = monitor.expectMsgClass(classOf[Transition[_]]) awaitCond(nodeParams.db.payments.getOutgoingPayment(id).exists(_.status == OutgoingPaymentStatus.Pending)) val Some(outgoing) = nodeParams.db.payments.getOutgoingPayment(id) - assert(outgoing.copy(createdAt = 0) === OutgoingPayment(id, parentId, Some(defaultExternalId), defaultPaymentHash, PaymentType.Standard, defaultAmountMsat, defaultAmountMsat, d, 0, Some(defaultInvoice), OutgoingPaymentStatus.Pending)) + assert(outgoing.copy(createdAt = 0 unixms) === OutgoingPayment(id, parentId, Some(defaultExternalId), defaultPaymentHash, PaymentType.Standard, defaultAmountMsat, defaultAmountMsat, d, 0 unixms, Some(defaultInvoice), OutgoingPaymentStatus.Pending)) sender.send(paymentFSM, addCompleted(HtlcResult.RemoteFulfill(UpdateFulfillHtlc(ByteVector32.Zeroes, 0, defaultPaymentPreimage)))) val ps = sender.expectMsgType[PaymentSent] @@ -138,7 +138,7 @@ class PaymentLifecycleSpec extends BaseRouterSpec { val Transition(_, WAITING_FOR_ROUTE, WAITING_FOR_PAYMENT_COMPLETE) = monitor.expectMsgClass(classOf[Transition[_]]) awaitCond(nodeParams.db.payments.getOutgoingPayment(id).exists(_.status == OutgoingPaymentStatus.Pending)) val Some(outgoing) = nodeParams.db.payments.getOutgoingPayment(id) - assert(outgoing.copy(createdAt = 0) === OutgoingPayment(id, parentId, Some(defaultExternalId), defaultPaymentHash, PaymentType.Standard, defaultAmountMsat, defaultAmountMsat, d, 0, Some(defaultInvoice), OutgoingPaymentStatus.Pending)) + assert(outgoing.copy(createdAt = 0 unixms) === OutgoingPayment(id, parentId, Some(defaultExternalId), defaultPaymentHash, PaymentType.Standard, defaultAmountMsat, defaultAmountMsat, d, 0 unixms, Some(defaultInvoice), OutgoingPaymentStatus.Pending)) sender.send(paymentFSM, addCompleted(HtlcResult.RemoteFulfill(UpdateFulfillHtlc(ByteVector32.Zeroes, 0, defaultPaymentPreimage)))) val ps = sender.expectMsgType[PaymentSent] @@ -640,7 +640,7 @@ class PaymentLifecycleSpec extends BaseRouterSpec { val Transition(_, WAITING_FOR_ROUTE, WAITING_FOR_PAYMENT_COMPLETE) = monitor.expectMsgClass(classOf[Transition[_]]) awaitCond(nodeParams.db.payments.getOutgoingPayment(id).exists(_.status === OutgoingPaymentStatus.Pending)) val Some(outgoing) = nodeParams.db.payments.getOutgoingPayment(id) - assert(outgoing.copy(createdAt = 0) === OutgoingPayment(id, parentId, Some(defaultExternalId), defaultPaymentHash, PaymentType.Standard, defaultAmountMsat, defaultAmountMsat, d, 0, Some(defaultInvoice), OutgoingPaymentStatus.Pending)) + assert(outgoing.copy(createdAt = 0 unixms) === OutgoingPayment(id, parentId, Some(defaultExternalId), defaultPaymentHash, PaymentType.Standard, defaultAmountMsat, defaultAmountMsat, d, 0 unixms, Some(defaultInvoice), OutgoingPaymentStatus.Pending)) sender.send(paymentFSM, addCompleted(HtlcResult.RemoteFulfill(UpdateFulfillHtlc(ByteVector32.Zeroes, 0, defaultPaymentPreimage)))) val ps = eventListener.expectMsgType[PaymentSent] @@ -800,7 +800,7 @@ class PaymentLifecycleSpec extends BaseRouterSpec { val WaitingForComplete(_, _, Nil, sharedSecrets1, _, _) = paymentFSM.stateData awaitCond(nodeParams.db.payments.getOutgoingPayment(id).exists(_.status == OutgoingPaymentStatus.Pending)) val Some(outgoing) = nodeParams.db.payments.getOutgoingPayment(id) - assert(outgoing.copy(createdAt = 0) === OutgoingPayment(id, parentId, Some(defaultExternalId), defaultPaymentHash, PaymentType.Standard, defaultAmountMsat, defaultAmountMsat, d, 0, Some(defaultInvoice), OutgoingPaymentStatus.Pending)) + assert(outgoing.copy(createdAt = 0 unixms) === OutgoingPayment(id, parentId, Some(defaultExternalId), defaultPaymentHash, PaymentType.Standard, defaultAmountMsat, defaultAmountMsat, d, 0 unixms, Some(defaultInvoice), OutgoingPaymentStatus.Pending)) // we change the cltv expiry val channelUpdate_bc_modified = makeChannelUpdate(Block.RegtestGenesisBlock.hash, priv_b, c, channelId_bc, CltvExpiryDelta(42), htlcMinimumMsat = update_bc.htlcMinimumMsat, feeBaseMsat = update_bc.feeBaseMsat, feeProportionalMillionths = update_bc.feeProportionalMillionths, htlcMaximumMsat = update_bc.htlcMaximumMsat.get) diff --git a/eclair-core/src/test/scala/fr/acinq/eclair/payment/PaymentPacketSpec.scala b/eclair-core/src/test/scala/fr/acinq/eclair/payment/PaymentPacketSpec.scala index 5298a87115..e9148931d4 100644 --- a/eclair-core/src/test/scala/fr/acinq/eclair/payment/PaymentPacketSpec.scala +++ b/eclair-core/src/test/scala/fr/acinq/eclair/payment/PaymentPacketSpec.scala @@ -31,7 +31,7 @@ import fr.acinq.eclair.transactions.Transactions.InputInfo import fr.acinq.eclair.wire.protocol.Onion.{ChannelRelayTlvPayload, FinalTlvPayload} import fr.acinq.eclair.wire.protocol.OnionTlv.{AmountToForward, OutgoingCltv, PaymentData} import fr.acinq.eclair.wire.protocol._ -import fr.acinq.eclair.{CltvExpiry, CltvExpiryDelta, MilliSatoshi, MilliSatoshiLong, ShortChannelId, TestConstants, nodeFee, randomBytes32, randomKey} +import fr.acinq.eclair.{CltvExpiry, CltvExpiryDelta, MilliSatoshi, MilliSatoshiLong, ShortChannelId, TestConstants, TimestampSecond, TimestampSecondLong, nodeFee, randomBytes32, randomKey} import org.scalatest.BeforeAndAfterAll import org.scalatest.funsuite.AnyFunSuite import scodec.Attempt @@ -378,7 +378,7 @@ object PaymentPacketSpec { val (priv_a, priv_b, priv_c, priv_d, priv_e) = (TestConstants.Alice.nodeKeyManager.nodeKey, TestConstants.Bob.nodeKeyManager.nodeKey, randomExtendedPrivateKey, randomExtendedPrivateKey, randomExtendedPrivateKey) val (a, b, c, d, e) = (priv_a.publicKey, priv_b.publicKey, priv_c.publicKey, priv_d.publicKey, priv_e.publicKey) val sig = Crypto.sign(Crypto.sha256(ByteVector.empty), priv_a.privateKey) - val defaultChannelUpdate = ChannelUpdate(sig, Block.RegtestGenesisBlock.hash, ShortChannelId(0), 0, ChannelUpdate.ChannelFlags.DUMMY, CltvExpiryDelta(0), 42000 msat, 0 msat, 0, Some(500000000 msat)) + val defaultChannelUpdate = ChannelUpdate(sig, Block.RegtestGenesisBlock.hash, ShortChannelId(0), 0 unixsec, ChannelUpdate.ChannelFlags.DUMMY, CltvExpiryDelta(0), 42000 msat, 0 msat, 0, Some(500000000 msat)) val channelUpdate_ab = defaultChannelUpdate.copy(shortChannelId = ShortChannelId(1), cltvExpiryDelta = CltvExpiryDelta(4), feeBaseMsat = 642000 msat, feeProportionalMillionths = 7) val channelUpdate_bc = defaultChannelUpdate.copy(shortChannelId = ShortChannelId(2), cltvExpiryDelta = CltvExpiryDelta(5), feeBaseMsat = 153000 msat, feeProportionalMillionths = 4) val channelUpdate_cd = defaultChannelUpdate.copy(shortChannelId = ShortChannelId(3), cltvExpiryDelta = CltvExpiryDelta(10), feeBaseMsat = 60000 msat, feeProportionalMillionths = 1) diff --git a/eclair-core/src/test/scala/fr/acinq/eclair/payment/PaymentRequestSpec.scala b/eclair-core/src/test/scala/fr/acinq/eclair/payment/PaymentRequestSpec.scala index 8d8fda17af..a35656e7b8 100644 --- a/eclair-core/src/test/scala/fr/acinq/eclair/payment/PaymentRequestSpec.scala +++ b/eclair-core/src/test/scala/fr/acinq/eclair/payment/PaymentRequestSpec.scala @@ -17,17 +17,16 @@ package fr.acinq.eclair.payment import fr.acinq.bitcoin.Crypto.{PrivateKey, PublicKey} -import fr.acinq.bitcoin.{Block, BtcDouble, ByteVector32, Crypto, MilliBtcDouble, Protocol, SatoshiLong} +import fr.acinq.bitcoin.{Block, BtcDouble, ByteVector32, Crypto, MilliBtcDouble, SatoshiLong} import fr.acinq.eclair.FeatureSupport.Mandatory import fr.acinq.eclair.Features.{PaymentSecret, _} import fr.acinq.eclair.payment.PaymentRequest._ -import fr.acinq.eclair.{CltvExpiryDelta, FeatureSupport, Features, MilliSatoshiLong, ShortChannelId, TestConstants, ToMilliSatoshiConversion} +import fr.acinq.eclair.{CltvExpiryDelta, FeatureSupport, Features, MilliSatoshiLong, ShortChannelId, TestConstants, TimestampSecond, TimestampSecondLong, ToMilliSatoshiConversion} import org.scalatest.funsuite.AnyFunSuite import scodec.DecodeResult import scodec.bits._ import scodec.codecs.bits -import java.nio.ByteOrder import scala.util.Success /** @@ -98,7 +97,7 @@ class PaymentRequestSpec extends AnyFunSuite { assert(pr.paymentHash.bytes == hex"0001020304050607080900010203040506070809000102030405060708090102") assert(pr.paymentSecret.map(_.bytes) === Some(hex"1111111111111111111111111111111111111111111111111111111111111111")) assert(pr.features.features === Features(VariableLengthOnion -> Mandatory, PaymentSecret -> Mandatory)) - assert(pr.timestamp == 1496314658L) + assert(pr.timestamp == TimestampSecond(1496314658L)) assert(pr.nodeId == PublicKey(hex"03e7156ae33b0a208d0744199163177e909e80176e55d97a2f221ede0f934dd9ad")) assert(pr.description == Left("Please consider supporting this project")) assert(pr.fallbackAddress() === None) @@ -113,7 +112,7 @@ class PaymentRequestSpec extends AnyFunSuite { assert(pr.amount === Some(250000000 msat)) assert(pr.paymentHash.bytes == hex"0001020304050607080900010203040506070809000102030405060708090102") assert(pr.features.features === Features(VariableLengthOnion -> Mandatory, PaymentSecret -> Mandatory)) - assert(pr.timestamp == 1496314658L) + assert(pr.timestamp == TimestampSecond(1496314658L)) assert(pr.nodeId == PublicKey(hex"03e7156ae33b0a208d0744199163177e909e80176e55d97a2f221ede0f934dd9ad")) assert(pr.description == Left("1 cup coffee")) assert(pr.fallbackAddress() === None) @@ -128,7 +127,7 @@ class PaymentRequestSpec extends AnyFunSuite { assert(pr.amount === Some(250000000 msat)) assert(pr.paymentHash.bytes == hex"0001020304050607080900010203040506070809000102030405060708090102") assert(pr.features.features === Features(VariableLengthOnion -> Mandatory, PaymentSecret -> Mandatory)) - assert(pr.timestamp == 1496314658L) + assert(pr.timestamp == TimestampSecond(1496314658L)) assert(pr.nodeId == PublicKey(hex"03e7156ae33b0a208d0744199163177e909e80176e55d97a2f221ede0f934dd9ad")) assert(pr.description == Left("ナンセンス 1杯")) assert(pr.fallbackAddress() === None) @@ -143,7 +142,7 @@ class PaymentRequestSpec extends AnyFunSuite { assert(pr.amount === Some(2000000000 msat)) assert(pr.paymentHash.bytes == hex"0001020304050607080900010203040506070809000102030405060708090102") assert(pr.features.features === Features(VariableLengthOnion -> Mandatory, PaymentSecret -> Mandatory)) - assert(pr.timestamp == 1496314658L) + assert(pr.timestamp == TimestampSecond(1496314658L)) assert(pr.nodeId == PublicKey(hex"03e7156ae33b0a208d0744199163177e909e80176e55d97a2f221ede0f934dd9ad")) assert(pr.description == Right(Crypto.sha256(ByteVector("One piece of chocolate cake, one icecream cone, one pickle, one slice of swiss cheese, one slice of salami, one lollypop, one piece of cherry pie, one sausage, one cupcake, and one slice of watermelon".getBytes)))) assert(pr.fallbackAddress() === None) @@ -158,7 +157,7 @@ class PaymentRequestSpec extends AnyFunSuite { assert(pr.amount === Some(2000000000 msat)) assert(pr.paymentHash.bytes == hex"0001020304050607080900010203040506070809000102030405060708090102") assert(pr.features.features === Features(VariableLengthOnion -> Mandatory, PaymentSecret -> Mandatory)) - assert(pr.timestamp == 1496314658L) + assert(pr.timestamp == TimestampSecond(1496314658L)) assert(pr.nodeId == PublicKey(hex"03e7156ae33b0a208d0744199163177e909e80176e55d97a2f221ede0f934dd9ad")) assert(pr.description == Right(Crypto.sha256(ByteVector.view("One piece of chocolate cake, one icecream cone, one pickle, one slice of swiss cheese, one slice of salami, one lollypop, one piece of cherry pie, one sausage, one cupcake, and one slice of watermelon".getBytes)))) assert(pr.fallbackAddress() === Some("mk2QpYatsKicvFVuTAQLBryyccRXMUaGHP")) @@ -173,7 +172,7 @@ class PaymentRequestSpec extends AnyFunSuite { assert(pr.amount === Some(2000000000 msat)) assert(pr.paymentHash.bytes == hex"0001020304050607080900010203040506070809000102030405060708090102") assert(pr.features.features === Features(VariableLengthOnion -> Mandatory, PaymentSecret -> Mandatory)) - assert(pr.timestamp == 1496314658L) + assert(pr.timestamp == TimestampSecond(1496314658L)) assert(pr.nodeId == PublicKey(hex"03e7156ae33b0a208d0744199163177e909e80176e55d97a2f221ede0f934dd9ad")) assert(pr.description == Right(Crypto.sha256(ByteVector.view("One piece of chocolate cake, one icecream cone, one pickle, one slice of swiss cheese, one slice of salami, one lollypop, one piece of cherry pie, one sausage, one cupcake, and one slice of watermelon".getBytes)))) assert(pr.fallbackAddress() === Some("1RustyRX2oai4EYYDpQGWvEL62BBGqN9T")) @@ -192,7 +191,7 @@ class PaymentRequestSpec extends AnyFunSuite { assert(pr.amount === Some(2000000000 msat)) assert(pr.paymentHash.bytes == hex"0001020304050607080900010203040506070809000102030405060708090102") assert(pr.features.features === Features(VariableLengthOnion -> Mandatory, PaymentSecret -> Mandatory)) - assert(pr.timestamp == 1496314658L) + assert(pr.timestamp == TimestampSecond(1496314658L)) assert(pr.nodeId == PublicKey(hex"03e7156ae33b0a208d0744199163177e909e80176e55d97a2f221ede0f934dd9ad")) assert(pr.description == Right(Crypto.sha256(ByteVector.view("One piece of chocolate cake, one icecream cone, one pickle, one slice of swiss cheese, one slice of salami, one lollypop, one piece of cherry pie, one sausage, one cupcake, and one slice of watermelon".getBytes)))) assert(pr.fallbackAddress() === Some("3EktnHQD7RiAE6uzMj2ZifT9YgRrkSgzQX")) @@ -207,7 +206,7 @@ class PaymentRequestSpec extends AnyFunSuite { assert(pr.amount === Some(2000000000 msat)) assert(pr.paymentHash.bytes == hex"0001020304050607080900010203040506070809000102030405060708090102") assert(pr.features.features === Features(VariableLengthOnion -> Mandatory, PaymentSecret -> Mandatory)) - assert(pr.timestamp == 1496314658L) + assert(pr.timestamp == TimestampSecond(1496314658L)) assert(pr.nodeId == PublicKey(hex"03e7156ae33b0a208d0744199163177e909e80176e55d97a2f221ede0f934dd9ad")) assert(pr.description == Right(Crypto.sha256(ByteVector.view("One piece of chocolate cake, one icecream cone, one pickle, one slice of swiss cheese, one slice of salami, one lollypop, one piece of cherry pie, one sausage, one cupcake, and one slice of watermelon".getBytes)))) assert(pr.fallbackAddress() === Some("bc1qw508d6qejxtdg4y5r3zarvary0c5xw7kv8f3t4")) @@ -223,7 +222,7 @@ class PaymentRequestSpec extends AnyFunSuite { assert(pr.paymentHash.bytes == hex"0001020304050607080900010203040506070809000102030405060708090102") assert(pr.features.features === Features(VariableLengthOnion -> Mandatory, PaymentSecret -> Mandatory)) assert(!pr.features.allowMultiPart) - assert(pr.timestamp == 1496314658L) + assert(pr.timestamp == TimestampSecond(1496314658L)) assert(pr.nodeId == PublicKey(hex"03e7156ae33b0a208d0744199163177e909e80176e55d97a2f221ede0f934dd9ad")) assert(pr.description == Right(Crypto.sha256(ByteVector.view("One piece of chocolate cake, one icecream cone, one pickle, one slice of swiss cheese, one slice of salami, one lollypop, one piece of cherry pie, one sausage, one cupcake, and one slice of watermelon".getBytes)))) assert(pr.fallbackAddress() === Some("bc1qrp33g0q5c5txsp9arysrx4k6zdkfs4nce4xj0gdcccefvpysxf3qccfmv3")) @@ -239,7 +238,7 @@ class PaymentRequestSpec extends AnyFunSuite { assert(pr.paymentHash.bytes == hex"0001020304050607080900010203040506070809000102030405060708090102") assert(pr.features.features === Features(VariableLengthOnion -> Mandatory, PaymentSecret -> Mandatory)) assert(!pr.features.allowMultiPart) - assert(pr.timestamp == 1496314658L) + assert(pr.timestamp == TimestampSecond(1496314658L)) assert(pr.nodeId == PublicKey(hex"03e7156ae33b0a208d0744199163177e909e80176e55d97a2f221ede0f934dd9ad")) assert(pr.description == Right(Crypto.sha256(ByteVector.view("One piece of chocolate cake, one icecream cone, one pickle, one slice of swiss cheese, one slice of salami, one lollypop, one piece of cherry pie, one sausage, one cupcake, and one slice of watermelon".getBytes)))) assert(pr.fallbackAddress() === Some("bc1qrp33g0q5c5txsp9arysrx4k6zdkfs4nce4xj0gdcccefvpysxf3qccfmv3")) @@ -263,7 +262,7 @@ class PaymentRequestSpec extends AnyFunSuite { assert(pr.amount === Some(2500000000L msat)) assert(pr.paymentHash.bytes === hex"0001020304050607080900010203040506070809000102030405060708090102") assert(pr.paymentSecret === Some(ByteVector32(hex"1111111111111111111111111111111111111111111111111111111111111111"))) - assert(pr.timestamp === 1496314658L) + assert(pr.timestamp === TimestampSecond(1496314658L)) assert(pr.nodeId === PublicKey(hex"03e7156ae33b0a208d0744199163177e909e80176e55d97a2f221ede0f934dd9ad")) assert(pr.description === Left("coffee beans")) assert(pr.features.bitmask === bin"1000000000000000000000000000000000000000000000000000000000000000000000000000000000000100000100000000") @@ -282,7 +281,7 @@ class PaymentRequestSpec extends AnyFunSuite { assert(pr.amount === Some(2500000000L msat)) assert(pr.paymentHash.bytes === hex"0001020304050607080900010203040506070809000102030405060708090102") assert(pr.paymentSecret === Some(ByteVector32(hex"1111111111111111111111111111111111111111111111111111111111111111"))) - assert(pr.timestamp === 1496314658L) + assert(pr.timestamp === TimestampSecond(1496314658L)) assert(pr.nodeId === PublicKey(hex"03e7156ae33b0a208d0744199163177e909e80176e55d97a2f221ede0f934dd9ad")) assert(pr.description === Left("coffee beans")) assert(pr.fallbackAddress().isEmpty) @@ -302,7 +301,7 @@ class PaymentRequestSpec extends AnyFunSuite { assert(pr.paymentHash.bytes === hex"462264ede7e14047e9b249da94fefc47f41f7d02ee9b091815a5506bc8abf75f") assert(pr.features.features === Features(VariableLengthOnion -> Mandatory, PaymentSecret -> Mandatory)) assert(pr.features.areSupported(TestConstants.Alice.nodeParams)) - assert(pr.timestamp === 1572468703L) + assert(pr.timestamp === TimestampSecond(1572468703L)) assert(pr.nodeId === PublicKey(hex"03e7156ae33b0a208d0744199163177e909e80176e55d97a2f221ede0f934dd9ad")) assert(pr.description === Left("Blockstream Store: 88.85 USD for Blockstream Ledger Nano S x 1, \"Back In My Day\" Sticker x 2, \"I Got Lightning Working\" Sticker x 2 and 1 more items")) assert(pr.fallbackAddress().isEmpty) @@ -345,7 +344,7 @@ class PaymentRequestSpec extends AnyFunSuite { assert(field1 == field) // Now with a payment request - val pr = PaymentRequest(chainHash = Block.LivenetGenesisBlock.hash, amount = Some(123 msat), paymentHash = ByteVector32(ByteVector.fill(32)(1)), privateKey = priv, description = Left("Some invoice"), minFinalCltvExpiryDelta = CltvExpiryDelta(18), expirySeconds = Some(123456), timestamp = 12345) + val pr = PaymentRequest(chainHash = Block.LivenetGenesisBlock.hash, amount = Some(123 msat), paymentHash = ByteVector32(ByteVector.fill(32)(1)), privateKey = priv, description = Left("Some invoice"), minFinalCltvExpiryDelta = CltvExpiryDelta(18), expirySeconds = Some(123456), timestamp = 12345 unixsec) assert(pr.minFinalCltvExpiryDelta === Some(CltvExpiryDelta(18))) val serialized = PaymentRequest.write(pr) val pr1 = PaymentRequest.read(serialized) @@ -356,7 +355,7 @@ class PaymentRequestSpec extends AnyFunSuite { val pr = PaymentRequest( prefix = "lntb", amount = Some(100000 msat), - timestamp = System.currentTimeMillis() / 1000L, + timestamp = TimestampSecond.now(), nodeId = nodeId, tags = List( PaymentHash(ByteVector32(ByteVector.fill(32)(1))), diff --git a/eclair-core/src/test/scala/fr/acinq/eclair/payment/PostRestartHtlcCleanerSpec.scala b/eclair-core/src/test/scala/fr/acinq/eclair/payment/PostRestartHtlcCleanerSpec.scala index 431654bd8f..f298f14728 100644 --- a/eclair-core/src/test/scala/fr/acinq/eclair/payment/PostRestartHtlcCleanerSpec.scala +++ b/eclair-core/src/test/scala/fr/acinq/eclair/payment/PostRestartHtlcCleanerSpec.scala @@ -33,7 +33,7 @@ import fr.acinq.eclair.router.Router.ChannelHop import fr.acinq.eclair.transactions.{DirectedHtlc, IncomingHtlc, OutgoingHtlc} import fr.acinq.eclair.wire.internal.channel.ChannelCodecsSpec import fr.acinq.eclair.wire.protocol._ -import fr.acinq.eclair.{CltvExpiry, CltvExpiryDelta, CustomCommitmentsPlugin, MilliSatoshi, MilliSatoshiLong, NodeParams, TestConstants, TestKitBaseClass, randomBytes32} +import fr.acinq.eclair.{CltvExpiry, CltvExpiryDelta, CustomCommitmentsPlugin, MilliSatoshi, MilliSatoshiLong, NodeParams, TestConstants, TestKitBaseClass, TimestampMilli, TimestampMilliLong, randomBytes32} import org.scalatest.funsuite.FixtureAnyFunSuiteLike import org.scalatest.{Outcome, ParallelTestExecution} import scodec.bits.ByteVector @@ -714,9 +714,9 @@ object PostRestartHtlcCleanerSpec { val origin3 = Origin.LocalCold(id3) // Prepare channels and payment state before restart. - nodeParams.db.payments.addOutgoingPayment(OutgoingPayment(id1, id1, None, paymentHash1, PaymentType.Standard, add1.amountMsat, add1.amountMsat, c, 0, None, OutgoingPaymentStatus.Pending)) - nodeParams.db.payments.addOutgoingPayment(OutgoingPayment(id2, parentId, None, paymentHash2, PaymentType.Standard, add2.amountMsat, 2500 msat, c, 0, None, OutgoingPaymentStatus.Pending)) - nodeParams.db.payments.addOutgoingPayment(OutgoingPayment(id3, parentId, None, paymentHash2, PaymentType.Standard, add3.amountMsat, 2500 msat, c, 0, None, OutgoingPaymentStatus.Pending)) + nodeParams.db.payments.addOutgoingPayment(OutgoingPayment(id1, id1, None, paymentHash1, PaymentType.Standard, add1.amountMsat, add1.amountMsat, c, 0 unixms, None, OutgoingPaymentStatus.Pending)) + nodeParams.db.payments.addOutgoingPayment(OutgoingPayment(id2, parentId, None, paymentHash2, PaymentType.Standard, add2.amountMsat, 2500 msat, c, 0 unixms, None, OutgoingPaymentStatus.Pending)) + nodeParams.db.payments.addOutgoingPayment(OutgoingPayment(id3, parentId, None, paymentHash2, PaymentType.Standard, add3.amountMsat, 2500 msat, c, 0 unixms, None, OutgoingPaymentStatus.Pending)) nodeParams.db.channels.addOrUpdateChannel(ChannelCodecsSpec.makeChannelDataNormal( Seq(add1, add2, add3).map(add => OutgoingHtlc(add)), Map(add1.id -> origin1, add2.id -> origin2, add3.id -> origin3)) diff --git a/eclair-core/src/test/scala/fr/acinq/eclair/payment/relay/ChannelRelayerSpec.scala b/eclair-core/src/test/scala/fr/acinq/eclair/payment/relay/ChannelRelayerSpec.scala index 35d40c7d19..25ea87e986 100644 --- a/eclair-core/src/test/scala/fr/acinq/eclair/payment/relay/ChannelRelayerSpec.scala +++ b/eclair-core/src/test/scala/fr/acinq/eclair/payment/relay/ChannelRelayerSpec.scala @@ -407,7 +407,7 @@ class ChannelRelayerSpec extends ScalaTestWithActorTestKit(ConfigFactory.load("a assert(fwd2.message.r === paymentPreimage) val paymentRelayed = eventListener.expectMessageType[ChannelPaymentRelayed] - assert(paymentRelayed.copy(timestamp = 0) === ChannelPaymentRelayed(r.add.amountMsat, r.payload.amountToForward, r.add.paymentHash, r.add.channelId, channelId1, timestamp = 0)) + assert(paymentRelayed.copy(timestamp = 0 unixms) === ChannelPaymentRelayed(r.add.amountMsat, r.payload.amountToForward, r.add.paymentHash, r.add.channelId, channelId1, timestamp = 0 unixms)) } } @@ -492,7 +492,7 @@ object ChannelRelayerSpec { def createLocalUpdate(shortChannelId: ShortChannelId, balance: MilliSatoshi = 10000000 msat, capacity: Satoshi = 500000 sat, enabled: Boolean = true, htlcMinimum: MilliSatoshi = 0 msat): LocalChannelUpdate = { val channelId = channelIds(shortChannelId) - val update = ChannelUpdate(ByteVector64(randomBytes(64)), Block.RegtestGenesisBlock.hash, shortChannelId, 0, ChannelUpdate.ChannelFlags(isNode1 = true, isEnabled = enabled), CltvExpiryDelta(100), htlcMinimum, 1000 msat, 100, Some(capacity.toMilliSatoshi)) + val update = ChannelUpdate(ByteVector64(randomBytes(64)), Block.RegtestGenesisBlock.hash, shortChannelId, 0 unixsec, ChannelUpdate.ChannelFlags(isNode1 = true, isEnabled = enabled), CltvExpiryDelta(100), htlcMinimum, 1000 msat, 100, Some(capacity.toMilliSatoshi)) val commitments = PaymentPacketSpec.makeCommitments(channelId, testAvailableBalanceForSend = balance, testCapacity = capacity) LocalChannelUpdate(null, channelId, shortChannelId, outgoingNodeId, None, update, commitments) } diff --git a/eclair-core/src/test/scala/fr/acinq/eclair/router/AnnouncementsSpec.scala b/eclair-core/src/test/scala/fr/acinq/eclair/router/AnnouncementsSpec.scala index 32c6283bd0..d7fd68cdbb 100644 --- a/eclair-core/src/test/scala/fr/acinq/eclair/router/AnnouncementsSpec.scala +++ b/eclair-core/src/test/scala/fr/acinq/eclair/router/AnnouncementsSpec.scala @@ -56,7 +56,7 @@ class AnnouncementsSpec extends AnyFunSuite { val ann = makeNodeAnnouncement(Alice.nodeParams.privateKey, Alice.nodeParams.alias, Alice.nodeParams.color, Alice.nodeParams.publicAddresses, Alice.nodeParams.features) assert(ann.features.hasFeature(Features.VariableLengthOnion, Some(FeatureSupport.Mandatory))) assert(checkSig(ann)) - assert(checkSig(ann.copy(timestamp = 153)) === false) + assert(checkSig(ann.copy(timestamp = 153 unixsec)) === false) } test("sort node announcement addresses") { diff --git a/eclair-core/src/test/scala/fr/acinq/eclair/router/ChannelRangeQueriesSpec.scala b/eclair-core/src/test/scala/fr/acinq/eclair/router/ChannelRangeQueriesSpec.scala index 7a7cd4876e..9bd6348636 100644 --- a/eclair-core/src/test/scala/fr/acinq/eclair/router/ChannelRangeQueriesSpec.scala +++ b/eclair-core/src/test/scala/fr/acinq/eclair/router/ChannelRangeQueriesSpec.scala @@ -23,7 +23,7 @@ import fr.acinq.eclair.wire.protocol.QueryChannelRangeTlv.QueryFlags import fr.acinq.eclair.wire.protocol.QueryShortChannelIdsTlv.QueryFlagType._ import fr.acinq.eclair.wire.protocol.ReplyChannelRangeTlv._ import fr.acinq.eclair.wire.protocol.{EncodedShortChannelIds, EncodingType, ReplyChannelRange} -import fr.acinq.eclair.{MilliSatoshiLong, ShortChannelId, randomKey} +import fr.acinq.eclair.{MilliSatoshiLong, ShortChannelId, TimestampSecond, TimestampSecondLong, randomKey} import org.scalatest.funsuite.AnyFunSuite import scodec.bits.ByteVector @@ -36,11 +36,11 @@ class ChannelRangeQueriesSpec extends AnyFunSuite { test("ask for update test") { // they don't provide anything => we always ask for the update - assert(shouldRequestUpdate(0, 0, None, None)) - assert(shouldRequestUpdate(Int.MaxValue, 12345, None, None)) + assert(shouldRequestUpdate(0 unixsec, 0, None, None)) + assert(shouldRequestUpdate(TimestampSecond(Int.MaxValue), 12345, None, None)) // their update is older => don't ask - val now = System.currentTimeMillis / 1000 + val now = TimestampSecond.now() assert(!shouldRequestUpdate(now, 0, Some(now - 1), None)) assert(!shouldRequestUpdate(now, 0, Some(now - 1), Some(12345))) assert(!shouldRequestUpdate(now, 12344, Some(now - 1), None)) @@ -66,7 +66,7 @@ class ChannelRangeQueriesSpec extends AnyFunSuite { assert(shouldRequestUpdate(now - 1, 12344, Some(now), Some(12345))) // they just provided a 0 checksum => don't ask - assert(!shouldRequestUpdate(0, 0, None, Some(0))) + assert(!shouldRequestUpdate(0 unixsec, 0, None, Some(0))) assert(!shouldRequestUpdate(now, 1234, None, Some(0))) // they just provided a checksum that is the same as us => don't ask @@ -84,7 +84,7 @@ class ChannelRangeQueriesSpec extends AnyFunSuite { } test("compute flag tests") { - val now = System.currentTimeMillis / 1000 + val now = TimestampSecond.now() val a = randomKey().publicKey val b = randomKey().publicKey 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 index 95c4cb2a2d..10540278d9 100644 --- a/eclair-core/src/test/scala/fr/acinq/eclair/router/NetworkStatsSpec.scala +++ b/eclair-core/src/test/scala/fr/acinq/eclair/router/NetworkStatsSpec.scala @@ -20,7 +20,7 @@ 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, randomBytes32, randomBytes64, randomKey} +import fr.acinq.eclair.{CltvExpiryDelta, MilliSatoshi, MilliSatoshiLong, ShortChannelId, TimestampSecond, TimestampSecondLong, randomBytes32, randomBytes64, randomKey} import org.scalatest.funsuite.AnyFunSuite import scala.util.Random @@ -87,11 +87,11 @@ object NetworkStatsSpec { } def fakeChannelUpdate1(cltv: CltvExpiryDelta, feeBase: MilliSatoshi, feeProportional: Long): ChannelUpdate = { - ChannelUpdate(randomBytes64(), randomBytes32(), ShortChannelId(42), 0, ChannelUpdate.ChannelFlags.DUMMY, cltv, 1 msat, feeBase, feeProportional, None) + 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, ChannelUpdate.ChannelFlags(isNode1 = false, isEnabled = false), cltv, 1 msat, feeBase, feeProportional, None) + 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/RouteCalculationSpec.scala b/eclair-core/src/test/scala/fr/acinq/eclair/router/RouteCalculationSpec.scala index bbf1d06a33..089856e7be 100644 --- a/eclair-core/src/test/scala/fr/acinq/eclair/router/RouteCalculationSpec.scala +++ b/eclair-core/src/test/scala/fr/acinq/eclair/router/RouteCalculationSpec.scala @@ -28,7 +28,7 @@ import fr.acinq.eclair.router.RouteCalculation._ import fr.acinq.eclair.router.Router._ import fr.acinq.eclair.transactions.Transactions import fr.acinq.eclair.wire.protocol._ -import fr.acinq.eclair.{CltvExpiryDelta, Features, MilliSatoshi, MilliSatoshiLong, ShortChannelId, ToMilliSatoshiConversion, randomKey} +import fr.acinq.eclair.{CltvExpiryDelta, Features, MilliSatoshi, MilliSatoshiLong, ShortChannelId, TimestampSecond, TimestampSecondLong, ToMilliSatoshiConversion, randomKey} import org.scalatest.funsuite.AnyFunSuite import org.scalatest.{ParallelTestExecution, Tag} import scodec.bits._ @@ -417,14 +417,14 @@ class RouteCalculationSpec extends AnyFunSuite with ParallelTestExecution { test("calculate route and return metadata") { val DUMMY_SIG = Transactions.PlaceHolderSig - val uab = ChannelUpdate(DUMMY_SIG, Block.RegtestGenesisBlock.hash, ShortChannelId(1L), 0L, ChannelUpdate.ChannelFlags.DUMMY, CltvExpiryDelta(1), 42 msat, 2500 msat, 140, None) - val uba = ChannelUpdate(DUMMY_SIG, Block.RegtestGenesisBlock.hash, ShortChannelId(1L), 1L, ChannelUpdate.ChannelFlags(isNode1 = false, isEnabled = false), CltvExpiryDelta(1), 43 msat, 2501 msat, 141, None) - val ubc = ChannelUpdate(DUMMY_SIG, Block.RegtestGenesisBlock.hash, ShortChannelId(2L), 1L, ChannelUpdate.ChannelFlags.DUMMY, CltvExpiryDelta(1), 44 msat, 2502 msat, 142, None) - val ucb = ChannelUpdate(DUMMY_SIG, Block.RegtestGenesisBlock.hash, ShortChannelId(2L), 1L, ChannelUpdate.ChannelFlags(isNode1 = false, isEnabled = false), CltvExpiryDelta(1), 45 msat, 2503 msat, 143, None) - val ucd = ChannelUpdate(DUMMY_SIG, Block.RegtestGenesisBlock.hash, ShortChannelId(3L), 1L, ChannelUpdate.ChannelFlags.DUMMY, CltvExpiryDelta(1), 46 msat, 2504 msat, 144, Some(500000000 msat)) - val udc = ChannelUpdate(DUMMY_SIG, Block.RegtestGenesisBlock.hash, ShortChannelId(3L), 1L, ChannelUpdate.ChannelFlags(isNode1 = false, isEnabled = false), CltvExpiryDelta(1), 47 msat, 2505 msat, 145, None) - val ude = ChannelUpdate(DUMMY_SIG, Block.RegtestGenesisBlock.hash, ShortChannelId(4L), 1L, ChannelUpdate.ChannelFlags.DUMMY, CltvExpiryDelta(1), 48 msat, 2506 msat, 146, None) - val ued = ChannelUpdate(DUMMY_SIG, Block.RegtestGenesisBlock.hash, ShortChannelId(4L), 1L, ChannelUpdate.ChannelFlags(isNode1 = false, isEnabled = false), CltvExpiryDelta(1), 49 msat, 2507 msat, 147, None) + val uab = ChannelUpdate(DUMMY_SIG, Block.RegtestGenesisBlock.hash, ShortChannelId(1L), 0 unixsec, ChannelUpdate.ChannelFlags.DUMMY, CltvExpiryDelta(1), 42 msat, 2500 msat, 140, None) + val uba = ChannelUpdate(DUMMY_SIG, Block.RegtestGenesisBlock.hash, ShortChannelId(1L), 1 unixsec, ChannelUpdate.ChannelFlags(isNode1 = false, isEnabled = false), CltvExpiryDelta(1), 43 msat, 2501 msat, 141, None) + val ubc = ChannelUpdate(DUMMY_SIG, Block.RegtestGenesisBlock.hash, ShortChannelId(2L), 1 unixsec, ChannelUpdate.ChannelFlags.DUMMY, CltvExpiryDelta(1), 44 msat, 2502 msat, 142, None) + val ucb = ChannelUpdate(DUMMY_SIG, Block.RegtestGenesisBlock.hash, ShortChannelId(2L), 1 unixsec, ChannelUpdate.ChannelFlags(isNode1 = false, isEnabled = false), CltvExpiryDelta(1), 45 msat, 2503 msat, 143, None) + val ucd = ChannelUpdate(DUMMY_SIG, Block.RegtestGenesisBlock.hash, ShortChannelId(3L), 1 unixsec, ChannelUpdate.ChannelFlags.DUMMY, CltvExpiryDelta(1), 46 msat, 2504 msat, 144, Some(500000000 msat)) + val udc = ChannelUpdate(DUMMY_SIG, Block.RegtestGenesisBlock.hash, ShortChannelId(3L), 1 unixsec, ChannelUpdate.ChannelFlags(isNode1 = false, isEnabled = false), CltvExpiryDelta(1), 47 msat, 2505 msat, 145, None) + val ude = ChannelUpdate(DUMMY_SIG, Block.RegtestGenesisBlock.hash, ShortChannelId(4L), 1 unixsec, ChannelUpdate.ChannelFlags.DUMMY, CltvExpiryDelta(1), 48 msat, 2506 msat, 146, None) + val ued = ChannelUpdate(DUMMY_SIG, Block.RegtestGenesisBlock.hash, ShortChannelId(4L), 1 unixsec, ChannelUpdate.ChannelFlags(isNode1 = false, isEnabled = false), CltvExpiryDelta(1), 49 msat, 2507 msat, 147, None) val edges = Seq( GraphEdge(ChannelDesc(ShortChannelId(1L), a, b), uab, DEFAULT_CAPACITY, None), @@ -918,24 +918,24 @@ class RouteCalculationSpec extends AnyFunSuite with ParallelTestExecution { ann = makeChannel(ShortChannelId("565643x1216x0").toLong, PublicKey(hex"03864ef025fde8fb587d989186ce6a4a186895ee44a926bfc370e2c366597a3f8f"), PublicKey(hex"024655b768ef40951b20053a5c4b951606d4d86085d51238f2c67c7dec29c792ca")), fundingTxid = ByteVector32.Zeroes, capacity = DEFAULT_CAPACITY, - update_1_opt = Some(ChannelUpdate(ByteVector64.Zeroes, ByteVector32.Zeroes, ShortChannelId("565643x1216x0"), 0, ChannelUpdate.ChannelFlags.DUMMY, CltvExpiryDelta(14), htlcMinimumMsat = 1 msat, feeBaseMsat = 1000 msat, 10, Some(4294967295L msat))), - update_2_opt = Some(ChannelUpdate(ByteVector64.Zeroes, ByteVector32.Zeroes, ShortChannelId("565643x1216x0"), 0, ChannelUpdate.ChannelFlags(isEnabled = true, isNode1 = false), CltvExpiryDelta(144), htlcMinimumMsat = 0 msat, feeBaseMsat = 1000 msat, 100, Some(15000000000L msat))), + update_1_opt = Some(ChannelUpdate(ByteVector64.Zeroes, ByteVector32.Zeroes, ShortChannelId("565643x1216x0"), 0 unixsec, ChannelUpdate.ChannelFlags.DUMMY, CltvExpiryDelta(14), htlcMinimumMsat = 1 msat, feeBaseMsat = 1000 msat, 10, Some(4294967295L msat))), + update_2_opt = Some(ChannelUpdate(ByteVector64.Zeroes, ByteVector32.Zeroes, ShortChannelId("565643x1216x0"), 0 unixsec, ChannelUpdate.ChannelFlags(isEnabled = true, isNode1 = false), CltvExpiryDelta(144), htlcMinimumMsat = 0 msat, feeBaseMsat = 1000 msat, 100, Some(15000000000L msat))), meta_opt = None ), ShortChannelId("542280x2156x0") -> PublicChannel( ann = makeChannel(ShortChannelId("542280x2156x0").toLong, PublicKey(hex"03864ef025fde8fb587d989186ce6a4a186895ee44a926bfc370e2c366597a3f8f"), PublicKey(hex"03cb7983dc247f9f81a0fa2dfa3ce1c255365f7279c8dd143e086ca333df10e278")), fundingTxid = ByteVector32.Zeroes, capacity = DEFAULT_CAPACITY, - update_1_opt = Some(ChannelUpdate(ByteVector64.Zeroes, ByteVector32.Zeroes, ShortChannelId("542280x2156x0"), 0, ChannelUpdate.ChannelFlags.DUMMY, CltvExpiryDelta(144), htlcMinimumMsat = 1000 msat, feeBaseMsat = 1000 msat, 100, Some(16777000000L msat))), - update_2_opt = Some(ChannelUpdate(ByteVector64.Zeroes, ByteVector32.Zeroes, ShortChannelId("542280x2156x0"), 0, ChannelUpdate.ChannelFlags(isEnabled = true, isNode1 = false), CltvExpiryDelta(144), htlcMinimumMsat = 1 msat, feeBaseMsat = 667 msat, 1, Some(16777000000L msat))), + update_1_opt = Some(ChannelUpdate(ByteVector64.Zeroes, ByteVector32.Zeroes, ShortChannelId("542280x2156x0"), 0 unixsec, ChannelUpdate.ChannelFlags.DUMMY, CltvExpiryDelta(144), htlcMinimumMsat = 1000 msat, feeBaseMsat = 1000 msat, 100, Some(16777000000L msat))), + update_2_opt = Some(ChannelUpdate(ByteVector64.Zeroes, ByteVector32.Zeroes, ShortChannelId("542280x2156x0"), 0 unixsec, ChannelUpdate.ChannelFlags(isEnabled = true, isNode1 = false), CltvExpiryDelta(144), htlcMinimumMsat = 1 msat, feeBaseMsat = 667 msat, 1, Some(16777000000L msat))), meta_opt = None ), ShortChannelId("565779x2711x0") -> PublicChannel( ann = makeChannel(ShortChannelId("565779x2711x0").toLong, PublicKey(hex"036d65409c41ab7380a43448f257809e7496b52bf92057c09c4f300cbd61c50d96"), PublicKey(hex"03864ef025fde8fb587d989186ce6a4a186895ee44a926bfc370e2c366597a3f8f")), fundingTxid = ByteVector32.Zeroes, capacity = DEFAULT_CAPACITY, - update_1_opt = Some(ChannelUpdate(ByteVector64.Zeroes, ByteVector32.Zeroes, ShortChannelId("565779x2711x0"), 0, ChannelUpdate.ChannelFlags.DUMMY, CltvExpiryDelta(144), htlcMinimumMsat = 1 msat, feeBaseMsat = 1000 msat, 100, Some(230000000L msat))), - update_2_opt = Some(ChannelUpdate(ByteVector64.Zeroes, ByteVector32.Zeroes, ShortChannelId("565779x2711x0"), 0, ChannelUpdate.ChannelFlags(isEnabled = false, isNode1 = false), CltvExpiryDelta(144), htlcMinimumMsat = 1 msat, feeBaseMsat = 1000 msat, 100, Some(230000000L msat))), + update_1_opt = Some(ChannelUpdate(ByteVector64.Zeroes, ByteVector32.Zeroes, ShortChannelId("565779x2711x0"), 0 unixsec, ChannelUpdate.ChannelFlags.DUMMY, CltvExpiryDelta(144), htlcMinimumMsat = 1 msat, feeBaseMsat = 1000 msat, 100, Some(230000000L msat))), + update_2_opt = Some(ChannelUpdate(ByteVector64.Zeroes, ByteVector32.Zeroes, ShortChannelId("565779x2711x0"), 0 unixsec, ChannelUpdate.ChannelFlags(isEnabled = false, isNode1 = false), CltvExpiryDelta(144), htlcMinimumMsat = 1 msat, feeBaseMsat = 1000 msat, 100, Some(230000000L msat))), meta_opt = None ) ) @@ -1868,7 +1868,7 @@ object RouteCalculationSpec { GraphEdge(ChannelDesc(ShortChannelId(shortChannelId), nodeId1, nodeId2), update, capacity, balance_opt) } - def makeUpdateShort(shortChannelId: ShortChannelId, nodeId1: PublicKey, nodeId2: PublicKey, feeBase: MilliSatoshi, feeProportionalMillionth: Int, minHtlc: MilliSatoshi = DEFAULT_AMOUNT_MSAT, maxHtlc: Option[MilliSatoshi] = None, cltvDelta: CltvExpiryDelta = CltvExpiryDelta(0), timestamp: Long = 0): ChannelUpdate = + def makeUpdateShort(shortChannelId: ShortChannelId, nodeId1: PublicKey, nodeId2: PublicKey, feeBase: MilliSatoshi, feeProportionalMillionth: Int, minHtlc: MilliSatoshi = DEFAULT_AMOUNT_MSAT, maxHtlc: Option[MilliSatoshi] = None, cltvDelta: CltvExpiryDelta = CltvExpiryDelta(0), timestamp: TimestampSecond = 0 unixsec): ChannelUpdate = ChannelUpdate( signature = DUMMY_SIG, chainHash = Block.RegtestGenesisBlock.hash, 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 13a2c4c2aa..5c4b52b496 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 @@ -33,7 +33,7 @@ import fr.acinq.eclair.router.RouteCalculationSpec.{DEFAULT_AMOUNT_MSAT, DEFAULT import fr.acinq.eclair.router.Router._ import fr.acinq.eclair.transactions.Scripts import fr.acinq.eclair.wire.protocol._ -import fr.acinq.eclair.{CltvExpiryDelta, Features, MilliSatoshi, MilliSatoshiLong, ShortChannelId, TestConstants, randomKey} +import fr.acinq.eclair.{CltvExpiryDelta, Features, MilliSatoshi, MilliSatoshiLong, ShortChannelId, TestConstants, TimestampSecond, randomKey} import scodec.bits._ import scala.concurrent.duration._ @@ -55,7 +55,7 @@ class RouterSpec extends BaseRouterSpec { // valid channel announcement, no stashing val chan_ac = channelAnnouncement(ShortChannelId(420000, 5, 0), priv_a, priv_c, priv_funding_a, priv_funding_c) val update_ac = makeChannelUpdate(Block.RegtestGenesisBlock.hash, priv_a, c, chan_ac.shortChannelId, CltvExpiryDelta(7), 0 msat, 766000 msat, 10, htlcMaximum) - val node_c = makeNodeAnnouncement(priv_c, "node-C", Color(123, 100, -40), Nil, TestConstants.Bob.nodeParams.features, timestamp = System.currentTimeMillis.milliseconds.toSeconds + 1) + val node_c = makeNodeAnnouncement(priv_c, "node-C", Color(123, 100, -40), Nil, TestConstants.Bob.nodeParams.features, timestamp = TimestampSecond.now() + 1) peerConnection.send(router, PeerRoutingMessage(peerConnection.ref, remoteNodeId, chan_ac)) peerConnection.expectNoMessage(100 millis) // we don't immediately acknowledge the announcement (back pressure) assert(watcher.expectMsgType[ValidateRequest].ann === chan_ac) @@ -160,7 +160,7 @@ class RouterSpec extends BaseRouterSpec { { // stale channel update - val update_ab = makeChannelUpdate(Block.RegtestGenesisBlock.hash, priv_a, priv_b.publicKey, chan_ab.shortChannelId, CltvExpiryDelta(7), 0 msat, 766000 msat, 10, htlcMaximum, timestamp = (System.currentTimeMillis.milliseconds - 15.days).toSeconds) + val update_ab = makeChannelUpdate(Block.RegtestGenesisBlock.hash, priv_a, priv_b.publicKey, chan_ab.shortChannelId, CltvExpiryDelta(7), 0 msat, 766000 msat, 10, htlcMaximum, timestamp = TimestampSecond.now() - 15.days) peerConnection.send(router, PeerRoutingMessage(peerConnection.ref, remoteNodeId, update_ab)) peerConnection.expectMsg(TransportHandler.ReadAck(update_ab)) peerConnection.expectMsg(GossipDecision.Stale(update_ab)) @@ -550,7 +550,7 @@ class RouterSpec extends BaseRouterSpec { val blockHeight = 400000 - 2020 val channelId = ShortChannelId(blockHeight, 5, 0) val announcement = channelAnnouncement(channelId, priv_a, priv_c, priv_funding_a, priv_funding_c) - val oldTimestamp = (System.currentTimeMillis.milliseconds - 14.days - 1.day).toSeconds + val oldTimestamp = TimestampSecond.now() - 14.days - 1.day val staleUpdate = makeChannelUpdate(Block.RegtestGenesisBlock.hash, priv_a, c, channelId, CltvExpiryDelta(7), 0 msat, 766000 msat, 10, 5 msat, timestamp = oldTimestamp) val peerConnection = TestProbe() peerConnection.ignoreMsg { case _: TransportHandler.ReadAck => true } @@ -567,7 +567,7 @@ class RouterSpec extends BaseRouterSpec { sender.send(router, GetRoutingState) sender.expectMsgType[RoutingState] - val recentUpdate = makeChannelUpdate(Block.RegtestGenesisBlock.hash, priv_a, c, channelId, CltvExpiryDelta(7), 0 msat, 766000 msat, 10, htlcMaximum, timestamp = System.currentTimeMillis.millisecond.toSeconds) + val recentUpdate = makeChannelUpdate(Block.RegtestGenesisBlock.hash, priv_a, c, channelId, CltvExpiryDelta(7), 0 msat, 766000 msat, 10, htlcMaximum, timestamp = TimestampSecond.now()) // we want to make sure that transport receives the query peerConnection.send(router, PeerRoutingMessage(peerConnection.ref, remoteNodeId, recentUpdate)) diff --git a/eclair-core/src/test/scala/fr/acinq/eclair/router/RoutingSyncSpec.scala b/eclair-core/src/test/scala/fr/acinq/eclair/router/RoutingSyncSpec.scala index 32d73abd44..f3fccd4a46 100644 --- a/eclair-core/src/test/scala/fr/acinq/eclair/router/RoutingSyncSpec.scala +++ b/eclair-core/src/test/scala/fr/acinq/eclair/router/RoutingSyncSpec.scala @@ -340,7 +340,7 @@ object RoutingSyncSpec { val unused: PrivateKey = randomKey() def makeFakeRoutingInfo(pub2priv: mutable.Map[PublicKey, PrivateKey])(shortChannelId: ShortChannelId): (PublicChannel, NodeAnnouncement, NodeAnnouncement) = { - val timestamp = System.currentTimeMillis / 1000 + val timestamp = TimestampSecond.now() val (priv1, priv2) = { val (priv_a, priv_b) = (randomKey(), randomKey()) if (Announcements.isNode1(priv_a.publicKey, priv_b.publicKey)) (priv_a, priv_b) else (priv_b, priv_a) diff --git a/eclair-core/src/test/scala/fr/acinq/eclair/wire/internal/channel/ChannelCodecsSpec.scala b/eclair-core/src/test/scala/fr/acinq/eclair/wire/internal/channel/ChannelCodecsSpec.scala index cf8fa14aa6..fdd1b7953b 100644 --- a/eclair-core/src/test/scala/fr/acinq/eclair/wire/internal/channel/ChannelCodecsSpec.scala +++ b/eclair-core/src/test/scala/fr/acinq/eclair/wire/internal/channel/ChannelCodecsSpec.scala @@ -80,7 +80,7 @@ class ChannelCodecsSpec extends AnyFunSuite { // let's decode the old data (this will use the old codec that provides default values for new fields) val data_new = stateDataCodec.decode(bin_old.toBitVector).require.value assert(data_new.asInstanceOf[DATA_WAIT_FOR_FUNDING_CONFIRMED].fundingTx === None) - assert(System.currentTimeMillis.milliseconds.toSeconds - data_new.asInstanceOf[DATA_WAIT_FOR_FUNDING_CONFIRMED].waitingSinceBlock < 3600) // we just set this timestamp to current time + assert(TimestampSecond.now().toLong - data_new.asInstanceOf[DATA_WAIT_FOR_FUNDING_CONFIRMED].waitingSinceBlock < 3600) // we just set this timestamp to current time // and re-encode it with the new codec val bin_new = ByteVector(stateDataCodec.encode(data_new).require.toByteVector.toArray) // data should now be encoded under the new format @@ -127,11 +127,11 @@ class ChannelCodecsSpec extends AnyFunSuite { // this test makes sure that we actually produce the same objects than previous versions of eclair val refs = Map( hex"00000303933884AAF1D6B108397E5EFE5C86BCF2D8CA8D2F700EDA99DB9214FC2712B134000456E4167E3C0EB8C856C79CA31C97C0AA0000000000000222000000012A05F2000000000000028F5C000000000000000102D0001E000BD48A2402E80B723C42EE3E42938866EC6686ABB7ABF64380000000C501A7F2974C5074E9E10DBB3F0D9B8C40932EC63ABC610FAD7EB6B21C6D081A459B000000000000011E80000001EEFFFE5C00000000000147AE00000000000001F403F000F18146779F781067ED04B4957E14F1C5623AB653039B2B1D49910240848E4E682DB20131AD64F76FAF90CD7DE26892F1BDAB82FB9E02EF6538D82FF4204B5348F02AE081A5388E9474769D69C4F60A763AE0CCDB5228A06281DE64408871A927297FDFD8818B6383985ABD4F0AC22E73791CF3A4D63C592FA2648242D34B8334B1539E823381BB1F1404C37D9C2318F5FC6B1BF7ECF5E6835B779E3BE09BADCF6DF1F51DCFBC80000000C0808000000000000EFD80000000007F00000000061A0A4880000001EDE5F3C3801203B9C79160B5123CADE575E370480B57B0C816EB35DD7B27372EDA858622EB1E808000000015FFFFFF800000000011001029DFB814F6502A68D6F83B6049E3D2948A2080084083750626532FDB437169C20023A9108146779F781067ED04B4957E14F1C5623AB653039B2B1D49910240848E4E682DB21081B30694071254D8B3B9537320C014B8CB1052E5514F5EFC19CF2EB806308D5CF1A95700AD0100000000008083B9C79160B5123CADE575E370480B57B0C816EB35DD7B27372EDA858622EB1E80800000001961B4C001618F8180000000001100102E648BA30998A28C02C2DFD9DDCD0E0BA064DA199C55186485AFAB296B94E704426FFE00000000000B000A67D9B9FAADB91650E0146B1F742E5C16006708890200239822011026A6925C659D006FEB42D639F1E42DD13224EE49AA34E71B612CF96DB66A8CD4011032C22F653C54CC5E41098227427650644266D80DED45B7387AE0FFC10E529C4680A418228110807CB47D9C1A14CB832FB361C398EA672C9542F34A90BAD4288FA6AC5FC9E9845C01101CF71CAE9252D389135D8C606225DCF1E0333CCDF1FAE84B74FC5D3D440C25F880A3A9108146779F781067ED04B4957E14F1C5623AB653039B2B1D49910240848E4E682DB21081B30694071254D8B3B9537320C014B8CB1052E5514F5EFC19CF2EB806308D5CF1A9573D7C531000000000000000000F3180000000007F00000001EDE5F3C380000000061A0A48D64CA627B243AD5915A2E5D0BAD026762028DDF3304992B83A26D6C11735FC5F01ED56D769BDE7F6A068AF1A4BCFDF950321F3A4744B01B1DDC7498677F112AE1A80000000000000000000000000000000000000658000000000000819800040D37301C10C9419287E9A3B704EB6D7F45CC145DD77DCE8A63B0A47C8AB67467D800901DCE3C8B05A891E56F2BAF1B82405ABD8640B759AEEBD939B976D42C311758F40400000000AFFFFFFC00000000008800814EFDC0A7B2815346B7C1DB024F1E94A451040042041BA83132997EDA1B8B4E10011D48840A33BCFBC0833F6825A4ABF0A78E2B11D5B2981CD958EA4C881204247273416D90840D9834A03892A6C59DCA9B990600A5C65882972A8A7AF7E0CE7975C031846AE78D4AB8002000EC0003FFFFFFFF86801076D98A575A4CDFD0E3F44D1BB3CD3BBAF3BD04C38FED439ED90D88DF932A9296801A80007FFFFFFFF4008136A9D5896669E8724C5120FB6B36C241EF3CEF68AE0316161F04A9EE3EAFF36000FC0003FFFFFFFF86780106E4B5CC4155733A2427082907338051A5DA1E7CA6432840A5528ECAFFA3FB628801B80007FFFFFFFF10020CA4E125E9126107745D4354D4187ABCDE323117857A1DCEB7CCF60B2AAFA80C6003A0000FFFFFFFFE1C0080981575FD981A73A848CC0243CB467BF451F6811DAF4D71CAD8CE8B1E96DB190C01000003FFFFFFFF867400814C747E0FD8290BE8A3B8B3F73015A261479A71780CD3A0A9270234E4B394409C00D80003FFFFFFFF90020E1B9C9B10A97F15F5E1BB27FC8AC670DF8DADEAE4EDFAFB23BDD0AC705FDF51600340000FFFFFFFFF0020AD2581F3494A17B0BE3F63516D53F028A204FD3156D8B21AA4E57A8738D2062080007FFFFFFFF0CE83B9C79160B5123CADE575E370480B57B0C816EB35DD7B27372EDA858622EB1E0B8C1E00000B8000FA46CC2C7E9AB4A37C64216CD65C944E6D73998419D1A1AD2827AB6BC85B32280230764E374064EC82A3751E789607E23BEAE93FB0EDDD5E7FA803767079662E80EAEF384E2AFCB68049D9DC246119E77BD2ED4112330760CAB6CD3671CFCE006C584B9C95E0B554261E00154D40806EA694F44751B328A9291BAD124EFD5664280936EC92D27B242737E7E3E83B4704BA367B7DA5108F2F6EDFB1C38EE721A369E77EED71B12090BAEAAAC322C1457E31AB0C4DE5D9351943F10FD747742616A1AABD09F680B37D4105A8872695EE9B97FAB8985FAA9D747D45046229BF265CEEB300A40FE23040C5F335E0515496C58EE47418B72331FCC6F47A31A9B33B8E000008692FFAFF04D2AE211E9461FB39D875D74F32E4109D21D5A03D46612000000002E307800002E0002069FCA5D3141D3A78436ECFC366E31024CBB18EAF1843EB5FADAC871B42069166C0726710955E3AD621072FCBDFCB90D79E5B1951A5EE01DB533B72429F84E2562680519DE7DE0419FB412D255F853C71588EAD94C0E6CAC7526440902123939A0B6C806CC1A501C495362CEE54DCC830052E32C414B95453D7BF0673CBAE018C23573C69C694A8F88483050257A7366B838489731E5776B6FA0F02573401176D3E7FAEEF11E95A671420586631255F51A0EC2CF4D4D9F69D587712070FE1FB9316B71868692FFAFF04D2AE211E9461FB39D875D74F32E4109D21D5A03D46612000000002E307800002E0002BA11BBBA0202012000000000000007D0000007D0000000C800000007CFFFF83000" - -> """{"type":"DATA_NORMAL","commitments":{"channelId":"07738f22c16a24795bcaebc6e09016af61902dd66bbaf64e6e5db50b0c45d63c","channelConfig":[],"channelFeatures":[],"localParams":{"nodeId":"03933884aaf1d6b108397e5efe5c86bcf2d8ca8d2f700eda99db9214fc2712b134","fundingKeyPath":{"path":[1457788542,1007597768,1455922339,479707306]},"dustLimit":546,"maxHtlcValueInFlightMsat":5000000000,"channelReserve":167772,"htlcMinimum":1,"toSelfDelay":720,"maxAcceptedHtlcs":30,"isFunder":false,"defaultFinalScriptPubKey":"a9144805d016e47885dc7c852710cdd8cd0d576f57ec87","initFeatures":{"activated":{"option_data_loss_protect":"optional","initial_routing_sync":"optional","gossip_queries":"optional"},"unknown":[]}},"remoteParams":{"nodeId":"034fe52e98a0e9d3c21b767e1b371881265d8c7578c21f5afd6d6438da10348b36","dustLimit":573,"maxHtlcValueInFlightMsat":16609443000,"channelReserve":167772,"htlcMinimum":1000,"toSelfDelay":2016,"maxAcceptedHtlcs":483,"fundingPubKey":"028cef3ef020cfda09692afc29e38ac4756ca60736563a93220481091c9cd05b64","revocationBasepoint":"02635ac9eedf5f219afbc4d125e37b5705f73c05deca71b05fe84096a691e055c1","paymentBasepoint":"034a711d28e8ed3ad389ec14ec75c199b6a45140c503bcc88110e3524e52ffbfb1","delayedPaymentBasepoint":"0316c70730b57a9e15845ce6f239e749ac78b25f44c90485a697066962a73d0467","htlcBasepoint":"03763e280986fb384631ebf8d637efd9ebcd06b6ef3c77c1375b9edbe3ea3b9f79","initFeatures":{"activated":{"option_data_loss_protect":"mandatory","gossip_queries":"optional"},"unknown":[]}},"channelFlags":1,"localCommit":{"index":7675,"spec":{"htlcs":[],"commitTxFeerate":254,"toLocal":204739729,"toRemote":16572475271},"commitTxAndRemoteSig":{"commitTx":{"txid":"e25a866b79212015e01e155e530fb547abc8276869f8740a9948e52ca231f1e4","tx":"020000000107738f22c16a24795bcaebc6e09016af61902dd66bbaf64e6e5db50b0c45d63d010000000032c3698002c31f0300000000002200205cc91746133145180585bfb3bb9a1c1740c9b43338aa30c90b5f5652d729ce0884dffc0000000000160014cfb373f55b722ca1c028d63ee85cb82c00ce11127af8a620"},"remoteSig":"4d4d24b8cb3a00dfd685ac73e3c85ba26449dc935469ce36c259f2db6cd519a865845eca78a998bc8213044e84eca0c884cdb01bda8b6e70f5c1ff821ca5388d"},"htlcTxsAndRemoteSigs":[]},"remoteCommit":{"index":7779,"spec":{"htlcs":[],"commitTxFeerate":254,"toLocal":16572475271,"toRemote":204739729},"txid":"ac994c4f64875ab22b45cba175a04cec4051bbe660932570744dad822e6bf8be","remotePerCommitmentPoint":"03daadaed37bcfed40d15e34979fbf2a0643e748e8960363bb8e930cefe2255c35"},"localChanges":{"proposed":[],"signed":[],"acked":[]},"remoteChanges":{"proposed":[],"acked":[],"signed":[]},"localNextHtlcId":203,"remoteNextHtlcId":4147,"originChannels":{},"remoteNextCommitInfo":"034dcc0704325064a1fa68edc13adb5fd173051775df73a298ec291f22ad9d19f6","commitInput":{"outPoint":"3dd6450c0bb55d6e4ef6ba6bd62d9061af1690e0c6ebca5b79246ac1228f7307:1","amountSatoshis":16777215},"remotePerCommitmentSecrets":null},"shortChannelId":"1513532x23x1","buried":true,"channelAnnouncement":{"nodeSignature1":"d2366163f4d5a51be3210b66b2e4a2736b9ccc20ce8d0d69413d5b5e42d991401183b271ba032764151ba8f3c4b03f11df5749fd876eeaf3fd401bb383cb3174","nodeSignature2":"075779c27157e5b4024ecee12308cf3bde976a0891983b0655b669b38e7e700362c25ce4af05aaa130f000aa6a04037534a7a23a8d99454948dd689277eab321","bitcoinSignature1":"4049b7649693d92139bf3f1f41da3825d1b3dbed2884797b76fd8e1c77390d1b4f3bf76b8d890485d7555619160a2bf18d58626f2ec9a8ca1f887eba3ba130b5","bitcoinSignature2":"0d55e84fb4059bea082d443934af74dcbfd5c4c2fd54eba3ea2823114df932e7759805207f1182062f99af028aa4b62c7723a0c5b9198fe637a3d18d4d99dc70","features":{"activated":{},"unknown":[]},"chainHash":"43497fd7f826957108f4a30fd9cec3aeba79972084e90ead01ea330900000000","shortChannelId":"1513532x23x1","nodeId1":"034fe52e98a0e9d3c21b767e1b371881265d8c7578c21f5afd6d6438da10348b36","nodeId2":"03933884aaf1d6b108397e5efe5c86bcf2d8ca8d2f700eda99db9214fc2712b134","bitcoinKey1":"028cef3ef020cfda09692afc29e38ac4756ca60736563a93220481091c9cd05b64","bitcoinKey2":"03660d280e24a9b16772a6e6418029719620a5caa29ebdf8339e5d700c611ab9e3","tlvStream":{"records":[],"unknown":[]}},"channelUpdate":{"signature":"4e34a547c424182812bd39b35c1c244b98f2bbb5b7d07812b9a008bb69f3fd77788f4ad338a102c331892afa8d076167a6a6cfb4eac3b890387f0fdc98b5b8c3","chainHash":"43497fd7f826957108f4a30fd9cec3aeba79972084e90ead01ea330900000000","shortChannelId":"1513532x23x1","timestamp":1560862173,"channelFlags":{"isEnabled":true,"isNode1":false},"cltvExpiryDelta":144,"htlcMinimumMsat":1000,"feeBaseMsat":1000,"feeProportionalMillionths":100,"htlcMaximumMsat":16777215000,"tlvStream":{"records":[],"unknown":[]}}}""", + -> """{"type":"DATA_NORMAL","commitments":{"channelId":"07738f22c16a24795bcaebc6e09016af61902dd66bbaf64e6e5db50b0c45d63c","channelConfig":[],"channelFeatures":[],"localParams":{"nodeId":"03933884aaf1d6b108397e5efe5c86bcf2d8ca8d2f700eda99db9214fc2712b134","fundingKeyPath":{"path":[1457788542,1007597768,1455922339,479707306]},"dustLimit":546,"maxHtlcValueInFlightMsat":5000000000,"channelReserve":167772,"htlcMinimum":1,"toSelfDelay":720,"maxAcceptedHtlcs":30,"isFunder":false,"defaultFinalScriptPubKey":"a9144805d016e47885dc7c852710cdd8cd0d576f57ec87","initFeatures":{"activated":{"option_data_loss_protect":"optional","initial_routing_sync":"optional","gossip_queries":"optional"},"unknown":[]}},"remoteParams":{"nodeId":"034fe52e98a0e9d3c21b767e1b371881265d8c7578c21f5afd6d6438da10348b36","dustLimit":573,"maxHtlcValueInFlightMsat":16609443000,"channelReserve":167772,"htlcMinimum":1000,"toSelfDelay":2016,"maxAcceptedHtlcs":483,"fundingPubKey":"028cef3ef020cfda09692afc29e38ac4756ca60736563a93220481091c9cd05b64","revocationBasepoint":"02635ac9eedf5f219afbc4d125e37b5705f73c05deca71b05fe84096a691e055c1","paymentBasepoint":"034a711d28e8ed3ad389ec14ec75c199b6a45140c503bcc88110e3524e52ffbfb1","delayedPaymentBasepoint":"0316c70730b57a9e15845ce6f239e749ac78b25f44c90485a697066962a73d0467","htlcBasepoint":"03763e280986fb384631ebf8d637efd9ebcd06b6ef3c77c1375b9edbe3ea3b9f79","initFeatures":{"activated":{"option_data_loss_protect":"mandatory","gossip_queries":"optional"},"unknown":[]}},"channelFlags":1,"localCommit":{"index":7675,"spec":{"htlcs":[],"commitTxFeerate":254,"toLocal":204739729,"toRemote":16572475271},"commitTxAndRemoteSig":{"commitTx":{"txid":"e25a866b79212015e01e155e530fb547abc8276869f8740a9948e52ca231f1e4","tx":"020000000107738f22c16a24795bcaebc6e09016af61902dd66bbaf64e6e5db50b0c45d63d010000000032c3698002c31f0300000000002200205cc91746133145180585bfb3bb9a1c1740c9b43338aa30c90b5f5652d729ce0884dffc0000000000160014cfb373f55b722ca1c028d63ee85cb82c00ce11127af8a620"},"remoteSig":"4d4d24b8cb3a00dfd685ac73e3c85ba26449dc935469ce36c259f2db6cd519a865845eca78a998bc8213044e84eca0c884cdb01bda8b6e70f5c1ff821ca5388d"},"htlcTxsAndRemoteSigs":[]},"remoteCommit":{"index":7779,"spec":{"htlcs":[],"commitTxFeerate":254,"toLocal":16572475271,"toRemote":204739729},"txid":"ac994c4f64875ab22b45cba175a04cec4051bbe660932570744dad822e6bf8be","remotePerCommitmentPoint":"03daadaed37bcfed40d15e34979fbf2a0643e748e8960363bb8e930cefe2255c35"},"localChanges":{"proposed":[],"signed":[],"acked":[]},"remoteChanges":{"proposed":[],"acked":[],"signed":[]},"localNextHtlcId":203,"remoteNextHtlcId":4147,"originChannels":{},"remoteNextCommitInfo":"034dcc0704325064a1fa68edc13adb5fd173051775df73a298ec291f22ad9d19f6","commitInput":{"outPoint":"3dd6450c0bb55d6e4ef6ba6bd62d9061af1690e0c6ebca5b79246ac1228f7307:1","amountSatoshis":16777215},"remotePerCommitmentSecrets":null},"shortChannelId":"1513532x23x1","buried":true,"channelAnnouncement":{"nodeSignature1":"d2366163f4d5a51be3210b66b2e4a2736b9ccc20ce8d0d69413d5b5e42d991401183b271ba032764151ba8f3c4b03f11df5749fd876eeaf3fd401bb383cb3174","nodeSignature2":"075779c27157e5b4024ecee12308cf3bde976a0891983b0655b669b38e7e700362c25ce4af05aaa130f000aa6a04037534a7a23a8d99454948dd689277eab321","bitcoinSignature1":"4049b7649693d92139bf3f1f41da3825d1b3dbed2884797b76fd8e1c77390d1b4f3bf76b8d890485d7555619160a2bf18d58626f2ec9a8ca1f887eba3ba130b5","bitcoinSignature2":"0d55e84fb4059bea082d443934af74dcbfd5c4c2fd54eba3ea2823114df932e7759805207f1182062f99af028aa4b62c7723a0c5b9198fe637a3d18d4d99dc70","features":{"activated":{},"unknown":[]},"chainHash":"43497fd7f826957108f4a30fd9cec3aeba79972084e90ead01ea330900000000","shortChannelId":"1513532x23x1","nodeId1":"034fe52e98a0e9d3c21b767e1b371881265d8c7578c21f5afd6d6438da10348b36","nodeId2":"03933884aaf1d6b108397e5efe5c86bcf2d8ca8d2f700eda99db9214fc2712b134","bitcoinKey1":"028cef3ef020cfda09692afc29e38ac4756ca60736563a93220481091c9cd05b64","bitcoinKey2":"03660d280e24a9b16772a6e6418029719620a5caa29ebdf8339e5d700c611ab9e3","tlvStream":{"records":[],"unknown":[]}},"channelUpdate":{"signature":"4e34a547c424182812bd39b35c1c244b98f2bbb5b7d07812b9a008bb69f3fd77788f4ad338a102c331892afa8d076167a6a6cfb4eac3b890387f0fdc98b5b8c3","chainHash":"43497fd7f826957108f4a30fd9cec3aeba79972084e90ead01ea330900000000","shortChannelId":"1513532x23x1","timestamp":{"iso":"2019-06-18T12:49:33Z","unix":1560862173},"channelFlags":{"isEnabled":true,"isNode1":false},"cltvExpiryDelta":144,"htlcMinimumMsat":1000,"feeBaseMsat":1000,"feeProportionalMillionths":100,"htlcMaximumMsat":16777215000,"tlvStream":{"records":[],"unknown":[]}}}""", hex"00000303933884AAF1D6B108397E5EFE5C86BCF2D8CA8D2F700EDA99DB9214FC2712B1340004D443ECE9D9C43A11A19B554BAAA6AD150000000000000222000000003B9ACA0000000000000249F000000000000000010090001E800BD48A22F4C80A42CC8BB29A764DBAEFC95674931FBE9A4380000000C50134D4A745996002F219B5FDBA1E045374DF589ECA06ABE23CECAE47343E65EDCF800000000000011E80000001BA90824000000000000124F800000000000001F4038500F1810AE1AF8A1D6F56F80855E26705F191BB07CD4E2434BC5BB1698E7E5880E2266201E8BFEEEEED725775B8116F6F82CF8E87835A5B45B184E56F272AD70D6078118601E06212B8C8F2E25B73EE7974FDCDF007E389B437BBFE238CCC3F3BF7121B6C5E81AA8589D21E9584B24A11F3ABBA5DAD48D121DD63C57A69CD767119C05DA159CB81A649D8CC0E136EB8DFBD2268B69DCA86F8CE4A604235A03D9D37AE7B07FC563F80000000C080800000000000271C000000000177000000002808B14600000001970039BA00123767F0F4F00D5E9FDF24177EF2872343D9F8FAEC65D3048BA575E70E00A0AB08800000000015E070F20000000000110010584241B5FB364208F6E64A80D1166DAD866186B10C015ED0283FF1C308C2105A0023A910810AE1AF8A1D6F56F80855E26705F191BB07CD4E2434BC5BB1698E7E5880E226621081DE8ADFA110DC8A94D8B9E9EF616BAE8598287C8F82AFDF0FC068697D570266FDA95700AD81000000000080B767F0F4F00D5E9FDF24177EF2872343D9F8FAEC65D3048BA575E70E00A0AB0880000000003E7AEDC0011ABE8A00000000001100101A9CE4B6AEF469590BC7BCC51DCEEAE9C86084055A63CC01E443C733FBE400B9B5B16800000000000B000A5E5700106D1A7097E4DE87EBAF1F8F2773842FA482002418228110805E84989A81F51ABD9D11889AE43E68FAD93659DEC019F1B8C0ADBF15A57B118B81101DCC1256F9306439AD3962C043FC47A5179CAAA001CCB23342BE0E8D92E4022780A4182281108074F306DA3751B84EC5FFB155BDCA7B8E02208BBDBC8D4F3327ABA557BF27CD1701102EF4AC8CC92F469DA9642D4D4162BC545F8B34ADE15B7D6F99808AA22B086B0180A3A910810AE1AF8A1D6F56F80855E26705F191BB07CD4E2434BC5BB1698E7E5880E226621081DE8ADFA110DC8A94D8B9E9EF616BAE8598287C8F82AFDF0FC068697D570266FDA9576F8099900000000000000000271C00000000017700000001970039BA000000002808B14648CE00AE97051EE10A3C361263F81A98165CE4AA7BA076933D4266E533585F24815C15DEACF0691332B38ECF23EC39982C5C978C748374A01BA9B30D501EE4F26E8000000000000000000000000000000000001224000000000000004B800040A911C460F1467952E3B99BED072F81BFB4454FF389636DCB399FE6A78113C28580091BB3F87A7806AF4FEF920BBF794391A1ECFC7D7632E98245D2BAF3870050558440000000000AF0387900000000000880082C2120DAFD9B21047B732540688B36D6C330C3588600AF68141FF8E18461082D0011D488408570D7C50EB7AB7C042AF13382F8C8DD83E6A7121A5E2DD8B4C73F2C407113310840EF456FD0886E454A6C5CF4F7B0B5D742CC143E47C157EF87E03434BEAB81337ED4AB8001C00F40003FFFFFFFEC7200403248A1D44DFA3AC9EC237D452C936400CAA86E9517CCCF2A8F77B7493CD70B6A00780001FFFFFFFF63A0041826829646B907A97FBD1455EA8673A12B8E7AA6EA790F7802E955CE3B69DE57E006E0001FFFFFFFF640081E51EB1F91218821E680B50E4B22DF8B094385BD33ACAE36BFC9E8C2F5AD2DA5400EC0003FFFFFFFEC7801047C26AD5435658D063EBCF73A5D0EEFE73ED6B73426246E8DFB3A21D1C4C7465001900007FFFFFFFE0040B115AC58BAAA900195893EA3B2AB408D2AD348AD047E3B6CB15E599625E38608006A0001FFFFFFFF7002033C39A21A38BB61F6FB33623771A9356D8885B7C12C939C770C939EF826286C200360000FFFFFFFFB4008104EF4271064A0973B053727C3E67352D00E25CAEED944F50782449CEAE8F50960001FFFFFFFF6390DD9FC3D3C0357A7F7C905DFBCA1C8D0F67E3EBB1974C122E95D79C380282AC222B21FA0007920001295AA1FB77029F7620A90EF7AE6A6CD31E4588B93264A7ADB76152D535C52E90B9E1B7C2376DABA316A6290F1A9730D4E5E44D0B1CB0EE6A795702E6A6BCDFCDA1A4BFEBFC134AB8847A5187ECE761D75D3CCB904274875680F51984800000000AC87E8001E480002E884D2A8080804800000000000001F4000001F40000003200000001BF08EB000" - -> """{"type":"DATA_NORMAL","commitments":{"channelId":"6ecfe1e9e01abd3fbe482efde50e4687b3f1f5d8cba609174aebce1c01415611","channelConfig":[],"channelFeatures":[],"localParams":{"nodeId":"03933884aaf1d6b108397e5efe5c86bcf2d8ca8d2f700eda99db9214fc2712b134","fundingKeyPath":{"path":[3561221353,3653515793,2711311691,2863050005]},"dustLimit":546,"maxHtlcValueInFlightMsat":1000000000,"channelReserve":150000,"htlcMinimum":1,"toSelfDelay":144,"maxAcceptedHtlcs":30,"isFunder":true,"defaultFinalScriptPubKey":"a91445e990148599176534ec9b75df92ace9263f7d3487","initFeatures":{"activated":{"option_data_loss_protect":"optional","initial_routing_sync":"optional","gossip_queries":"optional"},"unknown":[]}},"remoteParams":{"nodeId":"0269a94e8b32c005e4336bfb743c08a6e9beb13d940d57c479d95c8e687ccbdb9f","dustLimit":573,"maxHtlcValueInFlightMsat":14850000000,"channelReserve":150000,"htlcMinimum":1000,"toSelfDelay":1802,"maxAcceptedHtlcs":483,"fundingPubKey":"0215c35f143adeadf010abc4ce0be323760f9a9c486978b762d31cfcb101c44cc4","revocationBasepoint":"03d17fdddddae4aeeb7022dedf059f1d0f06b4b68b6309cade4e55ae1ac0f0230c","paymentBasepoint":"03c0c4257191e5c4b6e7dcf2e9fb9be00fc713686f77fc4719987e77ee2436d8bd","delayedPaymentBasepoint":"03550b13a43d2b09649423e75774bb5a91a243bac78af4d39aece23380bb42b397","htlcBasepoint":"034c93b1981c26dd71bf7a44d16d3b950df19c94c0846b407b3a6f5cf60ff8ac7f","initFeatures":{"activated":{"option_data_loss_protect":"mandatory","gossip_queries":"optional"},"unknown":[]}},"channelFlags":1,"localCommit":{"index":20024,"spec":{"htlcs":[],"commitTxFeerate":750,"toLocal":1343316620,"toRemote":13656683380},"commitTxAndRemoteSig":{"commitTx":{"txid":"65fe0b1f079fa763448df3ab8d94b1ad7d377c061121376be90b9c0c1bb0cd43","tx":"02000000016ecfe1e9e01abd3fbe482efde50e4687b3f1f5d8cba609174aebce1c0141561100000000007cf5db8002357d1400000000002200203539c96d5de8d2b2178f798a3b9dd5d390c1080ab4c79803c8878e67f7c801736b62d00000000000160014bcae0020da34e12fc9bd0fd75e3f1e4ee7085f49df013320"},"remoteSig":"bd09313503ea357b3a231135c87cd1f5b26cb3bd8033e371815b7e2b4af623173b9824adf260c8735a72c58087f88f4a2f39554003996466857c1d1b25c8044f"},"htlcTxsAndRemoteSigs":[]},"remoteCommit":{"index":20024,"spec":{"htlcs":[],"commitTxFeerate":750,"toLocal":13656683380,"toRemote":1343316620},"txid":"919c015d2e0a3dc214786c24c7f035302cb9c954f740ed267a84cdca66b0be49","remotePerCommitmentPoint":"02b82bbd59e0d22665671d9e47d8733058b92f18e906e9403753661aa03dc9e4dd"},"localChanges":{"proposed":[],"signed":[],"acked":[]},"remoteChanges":{"proposed":[],"acked":[],"signed":[]},"localNextHtlcId":9288,"remoteNextHtlcId":151,"originChannels":{},"remoteNextCommitInfo":"02a4471183c519e54b8ee66fb41cbe06fed1153fce258db72ce67f9a9e044f0a16","commitInput":{"outPoint":"115641011cceeb4a1709a6cbd8f5f1b387460ee5fd2e48be3fbd1ae0e9e1cf6e:0","amountSatoshis":15000000},"remotePerCommitmentSecrets":null},"shortChannelId":"1413373x969x0","buried":true,"channelUpdate":{"signature":"52b543f6ee053eec41521def5cd4d9a63c8b117264c94f5b6ec2a5aa6b8a5d2173c36f846edb57462d4c521e352e61a9cbc89a163961dcd4f2ae05cd4d79bf9b","chainHash":"43497fd7f826957108f4a30fd9cec3aeba79972084e90ead01ea330900000000","shortChannelId":"1413373x969x0","timestamp":1561369173,"channelFlags":{"isEnabled":true,"isNode1":false},"cltvExpiryDelta":144,"htlcMinimumMsat":1000,"feeBaseMsat":1000,"feeProportionalMillionths":100,"htlcMaximumMsat":15000000000,"tlvStream":{"records":[],"unknown":[]}}}""", + -> """{"type":"DATA_NORMAL","commitments":{"channelId":"6ecfe1e9e01abd3fbe482efde50e4687b3f1f5d8cba609174aebce1c01415611","channelConfig":[],"channelFeatures":[],"localParams":{"nodeId":"03933884aaf1d6b108397e5efe5c86bcf2d8ca8d2f700eda99db9214fc2712b134","fundingKeyPath":{"path":[3561221353,3653515793,2711311691,2863050005]},"dustLimit":546,"maxHtlcValueInFlightMsat":1000000000,"channelReserve":150000,"htlcMinimum":1,"toSelfDelay":144,"maxAcceptedHtlcs":30,"isFunder":true,"defaultFinalScriptPubKey":"a91445e990148599176534ec9b75df92ace9263f7d3487","initFeatures":{"activated":{"option_data_loss_protect":"optional","initial_routing_sync":"optional","gossip_queries":"optional"},"unknown":[]}},"remoteParams":{"nodeId":"0269a94e8b32c005e4336bfb743c08a6e9beb13d940d57c479d95c8e687ccbdb9f","dustLimit":573,"maxHtlcValueInFlightMsat":14850000000,"channelReserve":150000,"htlcMinimum":1000,"toSelfDelay":1802,"maxAcceptedHtlcs":483,"fundingPubKey":"0215c35f143adeadf010abc4ce0be323760f9a9c486978b762d31cfcb101c44cc4","revocationBasepoint":"03d17fdddddae4aeeb7022dedf059f1d0f06b4b68b6309cade4e55ae1ac0f0230c","paymentBasepoint":"03c0c4257191e5c4b6e7dcf2e9fb9be00fc713686f77fc4719987e77ee2436d8bd","delayedPaymentBasepoint":"03550b13a43d2b09649423e75774bb5a91a243bac78af4d39aece23380bb42b397","htlcBasepoint":"034c93b1981c26dd71bf7a44d16d3b950df19c94c0846b407b3a6f5cf60ff8ac7f","initFeatures":{"activated":{"option_data_loss_protect":"mandatory","gossip_queries":"optional"},"unknown":[]}},"channelFlags":1,"localCommit":{"index":20024,"spec":{"htlcs":[],"commitTxFeerate":750,"toLocal":1343316620,"toRemote":13656683380},"commitTxAndRemoteSig":{"commitTx":{"txid":"65fe0b1f079fa763448df3ab8d94b1ad7d377c061121376be90b9c0c1bb0cd43","tx":"02000000016ecfe1e9e01abd3fbe482efde50e4687b3f1f5d8cba609174aebce1c0141561100000000007cf5db8002357d1400000000002200203539c96d5de8d2b2178f798a3b9dd5d390c1080ab4c79803c8878e67f7c801736b62d00000000000160014bcae0020da34e12fc9bd0fd75e3f1e4ee7085f49df013320"},"remoteSig":"bd09313503ea357b3a231135c87cd1f5b26cb3bd8033e371815b7e2b4af623173b9824adf260c8735a72c58087f88f4a2f39554003996466857c1d1b25c8044f"},"htlcTxsAndRemoteSigs":[]},"remoteCommit":{"index":20024,"spec":{"htlcs":[],"commitTxFeerate":750,"toLocal":13656683380,"toRemote":1343316620},"txid":"919c015d2e0a3dc214786c24c7f035302cb9c954f740ed267a84cdca66b0be49","remotePerCommitmentPoint":"02b82bbd59e0d22665671d9e47d8733058b92f18e906e9403753661aa03dc9e4dd"},"localChanges":{"proposed":[],"signed":[],"acked":[]},"remoteChanges":{"proposed":[],"acked":[],"signed":[]},"localNextHtlcId":9288,"remoteNextHtlcId":151,"originChannels":{},"remoteNextCommitInfo":"02a4471183c519e54b8ee66fb41cbe06fed1153fce258db72ce67f9a9e044f0a16","commitInput":{"outPoint":"115641011cceeb4a1709a6cbd8f5f1b387460ee5fd2e48be3fbd1ae0e9e1cf6e:0","amountSatoshis":15000000},"remotePerCommitmentSecrets":null},"shortChannelId":"1413373x969x0","buried":true,"channelUpdate":{"signature":"52b543f6ee053eec41521def5cd4d9a63c8b117264c94f5b6ec2a5aa6b8a5d2173c36f846edb57462d4c521e352e61a9cbc89a163961dcd4f2ae05cd4d79bf9b","chainHash":"43497fd7f826957108f4a30fd9cec3aeba79972084e90ead01ea330900000000","shortChannelId":"1413373x969x0","timestamp":{"iso":"2019-06-24T09:39:33Z","unix":1561369173},"channelFlags":{"isEnabled":true,"isNode1":false},"cltvExpiryDelta":144,"htlcMinimumMsat":1000,"feeBaseMsat":1000,"feeProportionalMillionths":100,"htlcMaximumMsat":15000000000,"tlvStream":{"records":[],"unknown":[]}}}""", hex"0200020000000303933884aaf1d6b108397e5efe5c86bcf2d8ca8d2f700eda99db9214fc2712b13400098c4b989bbdced820a77a7186c2320e7d176a5c8b5c16d6ac2af3889d6bc8bf8080000001000000000000022200000004a817c80000000000000249f0000000000000000102d0001eff1600148061b7fbd2d84ed1884177ea785faecb2080b10302e56c8eca8d4f00df84ac34c23f49c006d57d316b7ada5c346e9d4211e11604b300000004080aa982027455aef8453d92f4706b560b61527cc217ddf14da41770e8ed6607190a1851b8000000000000023d000000037521048000000000000249f00000000000000001070a01e302eff5309b9368340edc6114d738b3590e6969bec4e95d8a080cf185e8b9ce5e4b0343bf4bfbaea5c100f1f2bf1cdf82a0ef97c9a0069a2aec631e7c3084ba929b7503c54e7d5ccfc13f1a6c7a441ffcfac86248574d1bc0fe9773836f4c724ea7b2bd03765aaac2e8fa6dbce7de5143072e9d9d5e96a1fd451d02fe4ff803f413f303f8022f3b055b0d35cde31dec5263a8ed638433e3424a4e197c06d94053985a364a5700000004808a52a1010000000000000004000000001046000000037e11d6000000000000000000245986f644357956c1674f385e0878fb7bb44ab3d57ea9911fab98af8a71e1ad1b000000002bc0e1e40000000000220020690fb50de412adf9b20a7fc6c8fb86f1bfd4ebc1ef8e2d96a5a196560798d944475221023ced05ed1ab328b67477376d68a69ecd0f371a9d5843c6c3be4d31498d516d8d2102eff5309b9368340edc6114d738b3590e6969bec4e95d8a080cf185e8b9ce5e4b52aefd013b020000000001015986f644357956c1674f385e0878fb7bb44ab3d57ea9911fab98af8a71e1ad1b0000000000c2d6178001f8d5e4000000000022002080f1dfe71a865b605593e169677c952aaa1196fc2f541ef7d21c3b1006527b61040047304402207f8c1936d0a50671c993890f887c78c6019abc2a2e8018899dcdc0e891fd2b090220046b56afa2cb7e9470073c238654ecf584bcf5c00b96b91e38335a70e2739ec901483045022100871afd240e20a171b9cba46f20555f848c5850f94ec7da7b33b9eeaf6af6653c0220119cda8cbf5f80986d6a4f0db2590c734d1de399a7060a477b5d94df0183625b01475221023ced05ed1ab328b67477376d68a69ecd0f371a9d5843c6c3be4d31498d516d8d2102eff5309b9368340edc6114d738b3590e6969bec4e95d8a080cf185e8b9ce5e4b52aed7782c20000000000000000000040000000010460000000000000000000000037e11d600b5f2287b2d5edf4df5602a3c287db3b938c3f1a943e40715886db5bd400f95d802e7e1abac1feb54ee3ac2172c9e2231f77765df57664fb44a6dc2e4aa9e6a9a6a000000000000000000000000000000000000000000000000000000000000ff03fd10fe44564e2d7e1550099785c2c1bad32a5ae0feeef6e27f0c108d18b4931d245986f644357956c1674f385e0878fb7bb44ab3d57ea9911fab98af8a71e1ad1b000000002bc0e1e40000000000220020690fb50de412adf9b20a7fc6c8fb86f1bfd4ebc1ef8e2d96a5a196560798d944475221023ced05ed1ab328b67477376d68a69ecd0f371a9d5843c6c3be4d31498d516d8d2102eff5309b9368340edc6114d738b3590e6969bec4e95d8a080cf185e8b9ce5e4b52ae0001003e0000fffffffffffc0080474b8cf7bb98217dd8dc475cb7c057a3465d466728978bbb909d0a05d4ae7bbe0001fffffffffff85986f644357956c1674f385e0878fb7bb44ab3d57ea9911fab98af8a71e1ad1b1eedce0000010000fffffd01ae98d7a81bc1aa92fcfb74ced2213e85e0d92ae8ac622bf294b3551c7c27f6f84f782f3b318e4d0eb2c67ac719a7c65afcf85bf159f6ceea9427be54920134196992f6ed0e059db72105a13ec0e799bb08896cad8b4feb7e9ec7283c309b5f43123af1bd9e913fc2db018edadde8932d6992408f10c1ad020504361972dfa7fef09bbc2b568cef3c8c006f7860106fd5984bcc271ff06c4829db2a665e59b7c0b22c311a340ff2ab9bcb74a50db10ed85503ad2d248d95af8151aca8ef96248e8f84b3075922385fbaf012f057e7ee84ecbc14c84880520b26d6fd22ab5f107db606a906efdcf0f88ffbe32dc6ecc10131e1ff0dc8d68dad89c98562557f00448b000043497fd7f826957108f4a30fd9cec3aeba79972084e90ead01ea3309000000001eedce0000010000027455aef8453d92f4706b560b61527cc217ddf14da41770e8ed6607190a1851b803933884aaf1d6b108397e5efe5c86bcf2d8ca8d2f700eda99db9214fc2712b13402eff5309b9368340edc6114d738b3590e6969bec4e95d8a080cf185e8b9ce5e4b023ced05ed1ab328b67477376d68a69ecd0f371a9d5843c6c3be4d31498d516d8d88710d73875607575f3d84bb507dd87cca5b85f0cdac84f4ccecce7af3a55897525a45070fe26c0ea43e9580d4ea4cfa62ee3273e5546911145cba6bbf56e59d8e43497fd7f826957108f4a30fd9cec3aeba79972084e90ead01ea3309000000001eedce000001000060e6eb14010100900000000000000001000003e800000064000000037e11d6000000" - -> """{"type":"DATA_NORMAL","commitments":{"channelId":"5986f644357956c1674f385e0878fb7bb44ab3d57ea9911fab98af8a71e1ad1b","channelConfig":["funding_pubkey_based_channel_keypath"],"channelFeatures":["option_static_remotekey"],"localParams":{"nodeId":"03933884aaf1d6b108397e5efe5c86bcf2d8ca8d2f700eda99db9214fc2712b134","fundingKeyPath":{"path":[2353764507,3184449568,2809819526,3258060413,392846475,1545000620,720603293,1808318336,2147483649]},"dustLimit":546,"maxHtlcValueInFlightMsat":20000000000,"channelReserve":150000,"htlcMinimum":1,"toSelfDelay":720,"maxAcceptedHtlcs":30,"isFunder":true,"defaultFinalScriptPubKey":"00148061b7fbd2d84ed1884177ea785faecb2080b103","walletStaticPaymentBasepoint":"02e56c8eca8d4f00df84ac34c23f49c006d57d316b7ada5c346e9d4211e11604b3","initFeatures":{"activated":{"option_support_large_channel":"optional","gossip_queries_ex":"optional","option_data_loss_protect":"optional","var_onion_optin":"mandatory","option_static_remotekey":"optional","payment_secret":"optional","option_shutdown_anysegwit":"optional","basic_mpp":"optional","gossip_queries":"optional"},"unknown":[]}},"remoteParams":{"nodeId":"027455aef8453d92f4706b560b61527cc217ddf14da41770e8ed6607190a1851b8","dustLimit":573,"maxHtlcValueInFlightMsat":14850000000,"channelReserve":150000,"htlcMinimum":1,"toSelfDelay":1802,"maxAcceptedHtlcs":483,"fundingPubKey":"02eff5309b9368340edc6114d738b3590e6969bec4e95d8a080cf185e8b9ce5e4b","revocationBasepoint":"0343bf4bfbaea5c100f1f2bf1cdf82a0ef97c9a0069a2aec631e7c3084ba929b75","paymentBasepoint":"03c54e7d5ccfc13f1a6c7a441ffcfac86248574d1bc0fe9773836f4c724ea7b2bd","delayedPaymentBasepoint":"03765aaac2e8fa6dbce7de5143072e9d9d5e96a1fd451d02fe4ff803f413f303f8","htlcBasepoint":"022f3b055b0d35cde31dec5263a8ed638433e3424a4e197c06d94053985a364a57","initFeatures":{"activated":{"option_upfront_shutdown_script":"optional","payment_secret":"mandatory","option_data_loss_protect":"mandatory","var_onion_optin":"optional","option_static_remotekey":"mandatory","option_support_large_channel":"optional","option_anchors_zero_fee_htlc_tx":"optional","basic_mpp":"optional","gossip_queries":"optional"},"unknown":[31]}},"channelFlags":1,"localCommit":{"index":4,"spec":{"htlcs":[],"commitTxFeerate":4166,"toLocal":15000000000,"toRemote":0},"commitTxAndRemoteSig":{"commitTx":{"txid":"fa747ecb6f718c6831cc7148cf8d65c3468d2bb6c202605e2b82d2277491222f","tx":"02000000015986f644357956c1674f385e0878fb7bb44ab3d57ea9911fab98af8a71e1ad1b0000000000c2d6178001f8d5e4000000000022002080f1dfe71a865b605593e169677c952aaa1196fc2f541ef7d21c3b1006527b61d7782c20"},"remoteSig":"871afd240e20a171b9cba46f20555f848c5850f94ec7da7b33b9eeaf6af6653c119cda8cbf5f80986d6a4f0db2590c734d1de399a7060a477b5d94df0183625b"},"htlcTxsAndRemoteSigs":[]},"remoteCommit":{"index":4,"spec":{"htlcs":[],"commitTxFeerate":4166,"toLocal":0,"toRemote":15000000000},"txid":"b5f2287b2d5edf4df5602a3c287db3b938c3f1a943e40715886db5bd400f95d8","remotePerCommitmentPoint":"02e7e1abac1feb54ee3ac2172c9e2231f77765df57664fb44a6dc2e4aa9e6a9a6a"},"localChanges":{"proposed":[],"signed":[],"acked":[]},"remoteChanges":{"proposed":[],"acked":[],"signed":[]},"localNextHtlcId":0,"remoteNextHtlcId":0,"originChannels":{},"remoteNextCommitInfo":"03fd10fe44564e2d7e1550099785c2c1bad32a5ae0feeef6e27f0c108d18b4931d","commitInput":{"outPoint":"1bade1718aaf98ab1f91a97ed5b34ab47bfb78085e384f67c156793544f68659:0","amountSatoshis":15000000},"remotePerCommitmentSecrets":null},"shortChannelId":"2026958x1x0","buried":true,"channelAnnouncement":{"nodeSignature1":"98d7a81bc1aa92fcfb74ced2213e85e0d92ae8ac622bf294b3551c7c27f6f84f782f3b318e4d0eb2c67ac719a7c65afcf85bf159f6ceea9427be549201341969","nodeSignature2":"92f6ed0e059db72105a13ec0e799bb08896cad8b4feb7e9ec7283c309b5f43123af1bd9e913fc2db018edadde8932d6992408f10c1ad020504361972dfa7fef0","bitcoinSignature1":"9bbc2b568cef3c8c006f7860106fd5984bcc271ff06c4829db2a665e59b7c0b22c311a340ff2ab9bcb74a50db10ed85503ad2d248d95af8151aca8ef96248e8f","bitcoinSignature2":"84b3075922385fbaf012f057e7ee84ecbc14c84880520b26d6fd22ab5f107db606a906efdcf0f88ffbe32dc6ecc10131e1ff0dc8d68dad89c98562557f00448b","features":{"activated":{},"unknown":[]},"chainHash":"43497fd7f826957108f4a30fd9cec3aeba79972084e90ead01ea330900000000","shortChannelId":"2026958x1x0","nodeId1":"027455aef8453d92f4706b560b61527cc217ddf14da41770e8ed6607190a1851b8","nodeId2":"03933884aaf1d6b108397e5efe5c86bcf2d8ca8d2f700eda99db9214fc2712b134","bitcoinKey1":"02eff5309b9368340edc6114d738b3590e6969bec4e95d8a080cf185e8b9ce5e4b","bitcoinKey2":"023ced05ed1ab328b67477376d68a69ecd0f371a9d5843c6c3be4d31498d516d8d","tlvStream":{"records":[],"unknown":[]}},"channelUpdate":{"signature":"710d73875607575f3d84bb507dd87cca5b85f0cdac84f4ccecce7af3a55897525a45070fe26c0ea43e9580d4ea4cfa62ee3273e5546911145cba6bbf56e59d8e","chainHash":"43497fd7f826957108f4a30fd9cec3aeba79972084e90ead01ea330900000000","shortChannelId":"2026958x1x0","timestamp":1625746196,"channelFlags":{"isEnabled":true,"isNode1":false},"cltvExpiryDelta":144,"htlcMinimumMsat":1,"feeBaseMsat":1000,"feeProportionalMillionths":100,"htlcMaximumMsat":15000000000,"tlvStream":{"records":[],"unknown":[]}}}""" + -> """{"type":"DATA_NORMAL","commitments":{"channelId":"5986f644357956c1674f385e0878fb7bb44ab3d57ea9911fab98af8a71e1ad1b","channelConfig":["funding_pubkey_based_channel_keypath"],"channelFeatures":["option_static_remotekey"],"localParams":{"nodeId":"03933884aaf1d6b108397e5efe5c86bcf2d8ca8d2f700eda99db9214fc2712b134","fundingKeyPath":{"path":[2353764507,3184449568,2809819526,3258060413,392846475,1545000620,720603293,1808318336,2147483649]},"dustLimit":546,"maxHtlcValueInFlightMsat":20000000000,"channelReserve":150000,"htlcMinimum":1,"toSelfDelay":720,"maxAcceptedHtlcs":30,"isFunder":true,"defaultFinalScriptPubKey":"00148061b7fbd2d84ed1884177ea785faecb2080b103","walletStaticPaymentBasepoint":"02e56c8eca8d4f00df84ac34c23f49c006d57d316b7ada5c346e9d4211e11604b3","initFeatures":{"activated":{"option_support_large_channel":"optional","gossip_queries_ex":"optional","option_data_loss_protect":"optional","var_onion_optin":"mandatory","option_static_remotekey":"optional","payment_secret":"optional","option_shutdown_anysegwit":"optional","basic_mpp":"optional","gossip_queries":"optional"},"unknown":[]}},"remoteParams":{"nodeId":"027455aef8453d92f4706b560b61527cc217ddf14da41770e8ed6607190a1851b8","dustLimit":573,"maxHtlcValueInFlightMsat":14850000000,"channelReserve":150000,"htlcMinimum":1,"toSelfDelay":1802,"maxAcceptedHtlcs":483,"fundingPubKey":"02eff5309b9368340edc6114d738b3590e6969bec4e95d8a080cf185e8b9ce5e4b","revocationBasepoint":"0343bf4bfbaea5c100f1f2bf1cdf82a0ef97c9a0069a2aec631e7c3084ba929b75","paymentBasepoint":"03c54e7d5ccfc13f1a6c7a441ffcfac86248574d1bc0fe9773836f4c724ea7b2bd","delayedPaymentBasepoint":"03765aaac2e8fa6dbce7de5143072e9d9d5e96a1fd451d02fe4ff803f413f303f8","htlcBasepoint":"022f3b055b0d35cde31dec5263a8ed638433e3424a4e197c06d94053985a364a57","initFeatures":{"activated":{"option_upfront_shutdown_script":"optional","payment_secret":"mandatory","option_data_loss_protect":"mandatory","var_onion_optin":"optional","option_static_remotekey":"mandatory","option_support_large_channel":"optional","option_anchors_zero_fee_htlc_tx":"optional","basic_mpp":"optional","gossip_queries":"optional"},"unknown":[31]}},"channelFlags":1,"localCommit":{"index":4,"spec":{"htlcs":[],"commitTxFeerate":4166,"toLocal":15000000000,"toRemote":0},"commitTxAndRemoteSig":{"commitTx":{"txid":"fa747ecb6f718c6831cc7148cf8d65c3468d2bb6c202605e2b82d2277491222f","tx":"02000000015986f644357956c1674f385e0878fb7bb44ab3d57ea9911fab98af8a71e1ad1b0000000000c2d6178001f8d5e4000000000022002080f1dfe71a865b605593e169677c952aaa1196fc2f541ef7d21c3b1006527b61d7782c20"},"remoteSig":"871afd240e20a171b9cba46f20555f848c5850f94ec7da7b33b9eeaf6af6653c119cda8cbf5f80986d6a4f0db2590c734d1de399a7060a477b5d94df0183625b"},"htlcTxsAndRemoteSigs":[]},"remoteCommit":{"index":4,"spec":{"htlcs":[],"commitTxFeerate":4166,"toLocal":0,"toRemote":15000000000},"txid":"b5f2287b2d5edf4df5602a3c287db3b938c3f1a943e40715886db5bd400f95d8","remotePerCommitmentPoint":"02e7e1abac1feb54ee3ac2172c9e2231f77765df57664fb44a6dc2e4aa9e6a9a6a"},"localChanges":{"proposed":[],"signed":[],"acked":[]},"remoteChanges":{"proposed":[],"acked":[],"signed":[]},"localNextHtlcId":0,"remoteNextHtlcId":0,"originChannels":{},"remoteNextCommitInfo":"03fd10fe44564e2d7e1550099785c2c1bad32a5ae0feeef6e27f0c108d18b4931d","commitInput":{"outPoint":"1bade1718aaf98ab1f91a97ed5b34ab47bfb78085e384f67c156793544f68659:0","amountSatoshis":15000000},"remotePerCommitmentSecrets":null},"shortChannelId":"2026958x1x0","buried":true,"channelAnnouncement":{"nodeSignature1":"98d7a81bc1aa92fcfb74ced2213e85e0d92ae8ac622bf294b3551c7c27f6f84f782f3b318e4d0eb2c67ac719a7c65afcf85bf159f6ceea9427be549201341969","nodeSignature2":"92f6ed0e059db72105a13ec0e799bb08896cad8b4feb7e9ec7283c309b5f43123af1bd9e913fc2db018edadde8932d6992408f10c1ad020504361972dfa7fef0","bitcoinSignature1":"9bbc2b568cef3c8c006f7860106fd5984bcc271ff06c4829db2a665e59b7c0b22c311a340ff2ab9bcb74a50db10ed85503ad2d248d95af8151aca8ef96248e8f","bitcoinSignature2":"84b3075922385fbaf012f057e7ee84ecbc14c84880520b26d6fd22ab5f107db606a906efdcf0f88ffbe32dc6ecc10131e1ff0dc8d68dad89c98562557f00448b","features":{"activated":{},"unknown":[]},"chainHash":"43497fd7f826957108f4a30fd9cec3aeba79972084e90ead01ea330900000000","shortChannelId":"2026958x1x0","nodeId1":"027455aef8453d92f4706b560b61527cc217ddf14da41770e8ed6607190a1851b8","nodeId2":"03933884aaf1d6b108397e5efe5c86bcf2d8ca8d2f700eda99db9214fc2712b134","bitcoinKey1":"02eff5309b9368340edc6114d738b3590e6969bec4e95d8a080cf185e8b9ce5e4b","bitcoinKey2":"023ced05ed1ab328b67477376d68a69ecd0f371a9d5843c6c3be4d31498d516d8d","tlvStream":{"records":[],"unknown":[]}},"channelUpdate":{"signature":"710d73875607575f3d84bb507dd87cca5b85f0cdac84f4ccecce7af3a55897525a45070fe26c0ea43e9580d4ea4cfa62ee3273e5546911145cba6bbf56e59d8e","chainHash":"43497fd7f826957108f4a30fd9cec3aeba79972084e90ead01ea330900000000","shortChannelId":"2026958x1x0","timestamp":{"iso":"2021-07-08T12:09:56Z","unix":1625746196},"channelFlags":{"isEnabled":true,"isNode1":false},"cltvExpiryDelta":144,"htlcMinimumMsat":1,"feeBaseMsat":1000,"feeProportionalMillionths":100,"htlcMaximumMsat":15000000000,"tlvStream":{"records":[],"unknown":[]}}}""" ) refs.foreach { case (oldbin, refjson) => diff --git a/eclair-core/src/test/scala/fr/acinq/eclair/wire/protocol/EncryptedRecipientDataSpec.scala b/eclair-core/src/test/scala/fr/acinq/eclair/wire/protocol/EncryptedRecipientDataSpec.scala new file mode 100644 index 0000000000..153a0b37dd --- /dev/null +++ b/eclair-core/src/test/scala/fr/acinq/eclair/wire/protocol/EncryptedRecipientDataSpec.scala @@ -0,0 +1,60 @@ +package fr.acinq.eclair.wire.protocol + +import fr.acinq.bitcoin.Crypto.PublicKey +import fr.acinq.eclair.crypto.Sphinx.RouteBlinding +import fr.acinq.eclair.wire.protocol.EncryptedRecipientDataTlv.{OutgoingChannelId, OutgoingNodeId, Padding, RecipientSecret} +import fr.acinq.eclair.{ShortChannelId, UInt64, randomKey} +import org.scalatest.funsuite.AnyFunSuiteLike +import scodec.bits.HexStringSyntax + +import scala.util.Success + +class EncryptedRecipientDataSpec extends AnyFunSuiteLike { + + test("decode encrypted recipient data") { + val sessionKey = randomKey() + val nodePrivKeys = Seq(randomKey(), randomKey(), randomKey(), randomKey()) + val payloads = Seq( + (TlvStream[EncryptedRecipientDataTlv](Padding(hex"000000"), OutgoingChannelId(ShortChannelId(561))), hex"0f 0103000000 02080000000000000231"), + (TlvStream[EncryptedRecipientDataTlv](OutgoingNodeId(PublicKey(hex"025f7117a78150fe2ef97db7cfc83bd57b2e2c0d0dd25eaf467a4a1c2a45ce1486"))), hex"23 0421025f7117a78150fe2ef97db7cfc83bd57b2e2c0d0dd25eaf467a4a1c2a45ce1486"), + (TlvStream[EncryptedRecipientDataTlv](RecipientSecret(hex"0101010101010101010101010101010101010101010101010101010101010101")), hex"22 06200101010101010101010101010101010101010101010101010101010101010101"), + (TlvStream[EncryptedRecipientDataTlv](Seq(OutgoingChannelId(ShortChannelId(42))), Seq(GenericTlv(UInt64(65535), hex"06c1"))), hex"10 0208000000000000002a fdffff0206c1"), + ) + + val blindedRoute = RouteBlinding.create(sessionKey, nodePrivKeys.map(_.publicKey), payloads.map(_._2)) + val blinding0 = sessionKey.publicKey + val Success((decryptedPayload0, blinding1)) = EncryptedRecipientDataCodecs.decode(nodePrivKeys.head, blinding0, blindedRoute.encryptedPayloads(0)) + val Success((decryptedPayload1, blinding2)) = EncryptedRecipientDataCodecs.decode(nodePrivKeys(1), blinding1, blindedRoute.encryptedPayloads(1)) + val Success((decryptedPayload2, blinding3)) = EncryptedRecipientDataCodecs.decode(nodePrivKeys(2), blinding2, blindedRoute.encryptedPayloads(2)) + val Success((decryptedPayload3, _)) = EncryptedRecipientDataCodecs.decode(nodePrivKeys(3), blinding3, blindedRoute.encryptedPayloads(3)) + assert(Seq(decryptedPayload0, decryptedPayload1, decryptedPayload2, decryptedPayload3) === payloads.map(_._1)) + } + + test("decode invalid encrypted recipient data") { + val testCases = Seq( + hex"0a 02080000000000000231 ff", // additional trailing bytes after tlv stream + hex"0b 02080000000000000231", // invalid length (too long) + hex"08 02080000000000000231", // invalid length (too short) + hex"0e 01040000 02080000000000000231", // invalid padding tlv + hex"0f 02080000000000000231 0103000000", // invalid tlv stream ordering + hex"14 02080000000000000231 10080000000000000231", // unknown even tlv field + ) + + for (testCase <- testCases) { + val nodePrivKeys = Seq(randomKey(), randomKey()) + val payloads = Seq(hex"0a 02080000000000000231", testCase) + val blindingPrivKey = randomKey() + val blindedRoute = RouteBlinding.create(blindingPrivKey, nodePrivKeys.map(_.publicKey), payloads) + // The payload for the first node is valid. + val blinding0 = blindingPrivKey.publicKey + val Success((_, blinding1)) = EncryptedRecipientDataCodecs.decode(nodePrivKeys.head, blinding0, blindedRoute.encryptedPayloads.head) + // If the first node is given invalid decryption material, it cannot decrypt recipient data. + assert(EncryptedRecipientDataCodecs.decode(nodePrivKeys.last, blinding0, blindedRoute.encryptedPayloads.head).isFailure) + assert(EncryptedRecipientDataCodecs.decode(nodePrivKeys.head, blinding1, blindedRoute.encryptedPayloads.head).isFailure) + assert(EncryptedRecipientDataCodecs.decode(nodePrivKeys.head, blinding0, blindedRoute.encryptedPayloads.last).isFailure) + // The payload for the last node is invalid, even with valid decryption material. + assert(EncryptedRecipientDataCodecs.decode(nodePrivKeys.last, blinding1, blindedRoute.encryptedPayloads.last).isFailure) + } + } + +} diff --git a/eclair-core/src/test/scala/fr/acinq/eclair/wire/protocol/ExtendedQueriesCodecsSpec.scala b/eclair-core/src/test/scala/fr/acinq/eclair/wire/protocol/ExtendedQueriesCodecsSpec.scala index f21bd976bc..bec1abf9bd 100644 --- a/eclair-core/src/test/scala/fr/acinq/eclair/wire/protocol/ExtendedQueriesCodecsSpec.scala +++ b/eclair-core/src/test/scala/fr/acinq/eclair/wire/protocol/ExtendedQueriesCodecsSpec.scala @@ -20,7 +20,7 @@ import fr.acinq.bitcoin.{Block, ByteVector32, ByteVector64} import fr.acinq.eclair.router.Sync import fr.acinq.eclair.wire.protocol.LightningMessageCodecs._ import fr.acinq.eclair.wire.protocol.ReplyChannelRangeTlv._ -import fr.acinq.eclair.{CltvExpiryDelta, MilliSatoshiLong, ShortChannelId, UInt64} +import fr.acinq.eclair.{CltvExpiryDelta, MilliSatoshiLong, ShortChannelId, TimestampSecond, TimestampSecondLong, UInt64} import org.scalatest.funsuite.AnyFunSuite import scodec.bits._ @@ -118,7 +118,7 @@ class ExtendedQueriesCodecsSpec extends AnyFunSuite { 1, 100, 1.toByte, EncodedShortChannelIds(EncodingType.UNCOMPRESSED, List(ShortChannelId(142), ShortChannelId(15465), ShortChannelId(4564676))), - Some(EncodedTimestamps(EncodingType.COMPRESSED_ZLIB, List(Timestamps(1, 1), Timestamps(2, 2), Timestamps(3, 3)))), + Some(EncodedTimestamps(EncodingType.COMPRESSED_ZLIB, List(Timestamps(1 unixsec, 1 unixsec), Timestamps(2 unixsec, 2 unixsec), Timestamps(3 unixsec, 3 unixsec)))), None) val encoded = replyChannelRangeCodec.encode(replyChannelRange).require @@ -134,7 +134,7 @@ class ExtendedQueriesCodecsSpec extends AnyFunSuite { EncodedShortChannelIds(EncodingType.UNCOMPRESSED, List(ShortChannelId(142), ShortChannelId(15465), ShortChannelId(4564676))), TlvStream( List( - EncodedTimestamps(EncodingType.COMPRESSED_ZLIB, List(Timestamps(1, 1), Timestamps(2, 2), Timestamps(3, 3))), + EncodedTimestamps(EncodingType.COMPRESSED_ZLIB, List(Timestamps(1 unixsec, 1 unixsec), Timestamps(2 unixsec, 2 unixsec), Timestamps(3 unixsec, 3 unixsec))), EncodedChecksums(List(Checksums(1, 1), Checksums(2, 2), Checksums(3, 3))) ), GenericTlv(UInt64(7), ByteVector.fromValidHex("deadbeef")) :: Nil @@ -151,7 +151,7 @@ class ExtendedQueriesCodecsSpec extends AnyFunSuite { chainHash = ByteVector32.fromValidHex("06226e46111a0b59caaf126043eb5bbf28c34f3a5e332a1fc7b2b73cf188910f"), signature = ByteVector64.fromValidHex("76df7e70c63cc2b63ef1c062b99c6d934a80ef2fd4dae9e1d86d277f47674af3255a97fa52ade7f129263f591ed784996eba6383135896cc117a438c80293282"), shortChannelId = ShortChannelId("103x1x0"), - timestamp = 1565587763L, + timestamp = TimestampSecond(1565587763L), channelFlags = ChannelUpdate.ChannelFlags.DUMMY, cltvExpiryDelta = CltvExpiryDelta(144), htlcMinimumMsat = 0 msat, @@ -171,7 +171,7 @@ class ExtendedQueriesCodecsSpec extends AnyFunSuite { chainHash = ByteVector32.fromValidHex("06226e46111a0b59caaf126043eb5bbf28c34f3a5e332a1fc7b2b73cf188910f"), signature = ByteVector64.fromValidHex("06737e9e18d3e4d0ab4066ccaecdcc10e648c5f1c5413f1610747e0d463fa7fa39c1b02ea2fd694275ecfefe4fe9631f24afd182ab75b805e16cd550941f858c"), shortChannelId = ShortChannelId("109x1x0"), - timestamp = 1565587765L, + timestamp = TimestampSecond(1565587765L), channelFlags = ChannelUpdate.ChannelFlags.DUMMY, cltvExpiryDelta = CltvExpiryDelta(48), htlcMinimumMsat = 0 msat, diff --git a/eclair-core/src/test/scala/fr/acinq/eclair/wire/protocol/FailureMessageCodecsSpec.scala b/eclair-core/src/test/scala/fr/acinq/eclair/wire/protocol/FailureMessageCodecsSpec.scala index c0fc9bc79f..2d7acc1dbb 100644 --- a/eclair-core/src/test/scala/fr/acinq/eclair/wire/protocol/FailureMessageCodecsSpec.scala +++ b/eclair-core/src/test/scala/fr/acinq/eclair/wire/protocol/FailureMessageCodecsSpec.scala @@ -19,7 +19,7 @@ package fr.acinq.eclair.wire.protocol import fr.acinq.bitcoin.{Block, ByteVector32, ByteVector64} import fr.acinq.eclair.crypto.Hmac256 import fr.acinq.eclair.wire.protocol.FailureMessageCodecs._ -import fr.acinq.eclair.{CltvExpiry, CltvExpiryDelta, MilliSatoshi, MilliSatoshiLong, ShortChannelId, UInt64, randomBytes32, randomBytes64} +import fr.acinq.eclair.{CltvExpiry, CltvExpiryDelta, MilliSatoshi, MilliSatoshiLong, ShortChannelId, TimestampSecond, TimestampSecondLong, UInt64, randomBytes32, randomBytes64} import org.scalatest.funsuite.AnyFunSuite import scodec.bits._ @@ -32,7 +32,7 @@ class FailureMessageCodecsSpec extends AnyFunSuite { signature = randomBytes64(), chainHash = Block.RegtestGenesisBlock.hash, shortChannelId = ShortChannelId(12345), - timestamp = 1234567L, + timestamp = TimestampSecond(1234567L), cltvExpiryDelta = CltvExpiryDelta(100), channelFlags = ChannelUpdate.ChannelFlags(isEnabled = true, isNode1 = false), htlcMinimumMsat = 1000 msat, @@ -148,7 +148,7 @@ class FailureMessageCodecsSpec extends AnyFunSuite { test("support encoding of channel_update with/without type in failure messages") { val tmp_channel_failure_notype = hex"10070080cc3e80149073ed487c76e48e9622bf980f78267b8a34a3f61921f2d8fce6063b08e74f34a073a13f2097337e4915bb4c001f3b5c4d81e9524ed575e1f45782196fe28c0ab6f1b372c1a6a246ae63f74f931e8365e15a089c68d619000000000008260500041300005b91b52f0003000e00000000000003e80000000100000001" val tmp_channel_failure_withtype = hex"100700820102cc3e80149073ed487c76e48e9622bf980f78267b8a34a3f61921f2d8fce6063b08e74f34a073a13f2097337e4915bb4c001f3b5c4d81e9524ed575e1f45782196fe28c0ab6f1b372c1a6a246ae63f74f931e8365e15a089c68d619000000000008260500041300005b91b52f0003000e00000000000003e80000000100000001" - val ref = TemporaryChannelFailure(ChannelUpdate(ByteVector64(hex"cc3e80149073ed487c76e48e9622bf980f78267b8a34a3f61921f2d8fce6063b08e74f34a073a13f2097337e4915bb4c001f3b5c4d81e9524ed575e1f4578219"), Block.LivenetGenesisBlock.hash, ShortChannelId(0x826050004130000L), 1536275759, ChannelUpdate.ChannelFlags(isEnabled = false, isNode1 = false), CltvExpiryDelta(14), 1000 msat, 1 msat, 1, None)) + val ref = TemporaryChannelFailure(ChannelUpdate(ByteVector64(hex"cc3e80149073ed487c76e48e9622bf980f78267b8a34a3f61921f2d8fce6063b08e74f34a073a13f2097337e4915bb4c001f3b5c4d81e9524ed575e1f4578219"), Block.LivenetGenesisBlock.hash, ShortChannelId(0x826050004130000L), 1536275759 unixsec, ChannelUpdate.ChannelFlags(isEnabled = false, isNode1 = false), CltvExpiryDelta(14), 1000 msat, 1 msat, 1, None)) val u = failureMessageCodec.decode(tmp_channel_failure_notype.toBitVector).require.value assert(u === ref) diff --git a/eclair-core/src/test/scala/fr/acinq/eclair/wire/protocol/LightningMessageCodecsSpec.scala b/eclair-core/src/test/scala/fr/acinq/eclair/wire/protocol/LightningMessageCodecsSpec.scala index 2c6a57432a..8fcbca9e59 100644 --- a/eclair-core/src/test/scala/fr/acinq/eclair/wire/protocol/LightningMessageCodecsSpec.scala +++ b/eclair-core/src/test/scala/fr/acinq/eclair/wire/protocol/LightningMessageCodecsSpec.scala @@ -264,10 +264,10 @@ class LightningMessageCodecsSpec extends AnyFunSuite { val commit_sig = CommitSig(randomBytes32(), randomBytes64(), randomBytes64() :: randomBytes64() :: randomBytes64() :: Nil) val revoke_and_ack = RevokeAndAck(randomBytes32(), scalar(0), point(1)) val channel_announcement = ChannelAnnouncement(randomBytes64(), randomBytes64(), randomBytes64(), randomBytes64(), Features(bin(7, 9)), Block.RegtestGenesisBlock.hash, ShortChannelId(1), randomKey().publicKey, randomKey().publicKey, randomKey().publicKey, randomKey().publicKey) - val node_announcement = NodeAnnouncement(randomBytes64(), Features(bin(1, 2)), 1, randomKey().publicKey, Color(100.toByte, 200.toByte, 300.toByte), "node-alias", IPv4(InetAddress.getByAddress(Array[Byte](192.toByte, 168.toByte, 1.toByte, 42.toByte)).asInstanceOf[Inet4Address], 42000) :: Nil) - val channel_update = ChannelUpdate(randomBytes64(), Block.RegtestGenesisBlock.hash, ShortChannelId(1), 2, ChannelUpdate.ChannelFlags.DUMMY, CltvExpiryDelta(3), 4 msat, 5 msat, 6, None) + val node_announcement = NodeAnnouncement(randomBytes64(), Features(bin(1, 2)), 1 unixsec, randomKey().publicKey, Color(100.toByte, 200.toByte, 300.toByte), "node-alias", IPv4(InetAddress.getByAddress(Array[Byte](192.toByte, 168.toByte, 1.toByte, 42.toByte)).asInstanceOf[Inet4Address], 42000) :: Nil) + val channel_update = ChannelUpdate(randomBytes64(), Block.RegtestGenesisBlock.hash, ShortChannelId(1), 2 unixsec, ChannelUpdate.ChannelFlags.DUMMY, CltvExpiryDelta(3), 4 msat, 5 msat, 6, None) val announcement_signatures = AnnouncementSignatures(randomBytes32(), ShortChannelId(42), randomBytes64(), randomBytes64()) - val gossip_timestamp_filter = GossipTimestampFilter(Block.RegtestGenesisBlock.blockId, 100000, 1500) + val gossip_timestamp_filter = GossipTimestampFilter(Block.RegtestGenesisBlock.blockId, 100000 unixsec, 1500) val query_short_channel_id = QueryShortChannelIds(Block.RegtestGenesisBlock.blockId, EncodedShortChannelIds(EncodingType.UNCOMPRESSED, List(ShortChannelId(142), ShortChannelId(15465), ShortChannelId(4564676))), TlvStream.empty) val unknownTlv = GenericTlv(UInt64(5), ByteVector.fromValidHex("deadbeef")) val query_channel_range = QueryChannelRange(Block.RegtestGenesisBlock.blockId, @@ -277,7 +277,7 @@ class LightningMessageCodecsSpec extends AnyFunSuite { val reply_channel_range = ReplyChannelRange(Block.RegtestGenesisBlock.blockId, 100000, 1500, 1, EncodedShortChannelIds(EncodingType.UNCOMPRESSED, List(ShortChannelId(142), ShortChannelId(15465), ShortChannelId(4564676))), TlvStream( - EncodedTimestamps(EncodingType.UNCOMPRESSED, List(Timestamps(1, 1), Timestamps(2, 2), Timestamps(3, 3))) :: EncodedChecksums(List(Checksums(1, 1), Checksums(2, 2), Checksums(3, 3))) :: Nil, + EncodedTimestamps(EncodingType.UNCOMPRESSED, List(Timestamps(1 unixsec, 1 unixsec), Timestamps(2 unixsec, 2 unixsec), Timestamps(3 unixsec, 3 unixsec))) :: EncodedChecksums(List(Checksums(1, 1), Checksums(2, 2), Checksums(3, 3))) :: Nil, unknownTlv :: Nil) ) val ping = Ping(100, bin(10, 1)) @@ -343,11 +343,11 @@ class LightningMessageCodecsSpec extends AnyFunSuite { EncodedShortChannelIds(EncodingType.COMPRESSED_ZLIB, List(ShortChannelId(142), ShortChannelId(15465), ShortChannelId(265462))), None, None) val reply_channel_range_timestamps_checksums = ReplyChannelRange(Block.RegtestGenesisBlock.blockId, 122334, 1500, 1, EncodedShortChannelIds(EncodingType.UNCOMPRESSED, List(ShortChannelId(12355), ShortChannelId(489686), ShortChannelId(4645313))), - Some(EncodedTimestamps(EncodingType.UNCOMPRESSED, List(Timestamps(164545, 948165), Timestamps(489645, 4786864), Timestamps(46456, 9788415)))), + Some(EncodedTimestamps(EncodingType.UNCOMPRESSED, List(Timestamps(164545 unixsec, 948165 unixsec), Timestamps(489645 unixsec, 4786864 unixsec), Timestamps(46456 unixsec, 9788415 unixsec)))), Some(EncodedChecksums(List(Checksums(1111, 2222), Checksums(3333, 4444), Checksums(5555, 6666))))) val reply_channel_range_timestamps_checksums_zlib = ReplyChannelRange(Block.RegtestGenesisBlock.blockId, 122334, 1500, 1, EncodedShortChannelIds(EncodingType.COMPRESSED_ZLIB, List(ShortChannelId(12355), ShortChannelId(489686), ShortChannelId(4645313))), - Some(EncodedTimestamps(EncodingType.COMPRESSED_ZLIB, List(Timestamps(164545, 948165), Timestamps(489645, 4786864), Timestamps(46456, 9788415)))), + Some(EncodedTimestamps(EncodingType.COMPRESSED_ZLIB, List(Timestamps(164545 unixsec, 948165 unixsec), Timestamps(489645 unixsec, 4786864 unixsec), Timestamps(46456 unixsec, 9788415 unixsec)))), Some(EncodedChecksums(List(Checksums(1111, 2222), Checksums(3333, 4444), Checksums(5555, 6666))))) val query_short_channel_id = QueryShortChannelIds(Block.RegtestGenesisBlock.blockId, EncodedShortChannelIds(EncodingType.UNCOMPRESSED, List(ShortChannelId(142), ShortChannelId(15465), ShortChannelId(4564676))), TlvStream.empty) val query_short_channel_id_zlib = QueryShortChannelIds(Block.RegtestGenesisBlock.blockId, EncodedShortChannelIds(EncodingType.COMPRESSED_ZLIB, List(ShortChannelId(4564), ShortChannelId(178622), ShortChannelId(4564676))), TlvStream.empty) @@ -427,7 +427,7 @@ class LightningMessageCodecsSpec extends AnyFunSuite { // this was generated by c-lightning val bin = hex"010258fff7d0e987e2cdd560e3bb5a046b4efe7b26c969c2f51da1dceec7bcb8ae1b634790503d5290c1a6c51d681cf8f4211d27ed33a257dcc1102862571bf1792306226e46111a0b59caaf126043eb5bbf28c34f3a5e332a1fc7b2b73cf188910f0005a100000200005bc75919010100060000000000000001000000010000000a000000003a699d00" val update = lightningMessageCodec.decode(bin.bits).require.value.asInstanceOf[ChannelUpdate] - assert(update === ChannelUpdate(ByteVector64(hex"58fff7d0e987e2cdd560e3bb5a046b4efe7b26c969c2f51da1dceec7bcb8ae1b634790503d5290c1a6c51d681cf8f4211d27ed33a257dcc1102862571bf17923"), ByteVector32(hex"06226e46111a0b59caaf126043eb5bbf28c34f3a5e332a1fc7b2b73cf188910f"), ShortChannelId(0x5a10000020000L), 1539791129, ChannelUpdate.ChannelFlags(isEnabled = true, isNode1 = false), CltvExpiryDelta(6), 1 msat, 1 msat, 10, Some(980000000 msat))) + assert(update === ChannelUpdate(ByteVector64(hex"58fff7d0e987e2cdd560e3bb5a046b4efe7b26c969c2f51da1dceec7bcb8ae1b634790503d5290c1a6c51d681cf8f4211d27ed33a257dcc1102862571bf17923"), ByteVector32(hex"06226e46111a0b59caaf126043eb5bbf28c34f3a5e332a1fc7b2b73cf188910f"), ShortChannelId(0x5a10000020000L), 1539791129 unixsec, ChannelUpdate.ChannelFlags(isEnabled = true, isNode1 = false), CltvExpiryDelta(6), 1 msat, 1 msat, 10, Some(980000000 msat))) val nodeId = PublicKey(hex"03370c9bac836e557eb4f017fe8f9cc047f44db39c1c4e410ff0f7be142b817ae4") assert(Announcements.checkSig(update, nodeId)) val bin2 = ByteVector(lightningMessageCodec.encode(update).require.toByteArray) @@ -437,21 +437,21 @@ class LightningMessageCodecsSpec extends AnyFunSuite { test("non-regression on channel_update") { val bins = Map( hex"3b6bb4872825450ff29d0b46f5835751329b0394a10ac792e4ba2a23b4f17bcc4e5834d1424787830be0ee3d22ac99e674d121f25d19ed931aaabb8ed0eec0fb6fe28c0ab6f1b372c1a6a246ae63f74f931e8365e15a089c68d6190000000000086a4e000a9700016137e9e9010200900000000000000001000003e800000001000000001dcd6500" -> - """{"signature":"3b6bb4872825450ff29d0b46f5835751329b0394a10ac792e4ba2a23b4f17bcc4e5834d1424787830be0ee3d22ac99e674d121f25d19ed931aaabb8ed0eec0fb","chainHash":"6fe28c0ab6f1b372c1a6a246ae63f74f931e8365e15a089c68d6190000000000","shortChannelId":"551502x2711x1","timestamp":1631054313,"channelFlags":{"isEnabled":false,"isNode1":true},"cltvExpiryDelta":144,"htlcMinimumMsat":1,"feeBaseMsat":1000,"feeProportionalMillionths":1,"htlcMaximumMsat":500000000,"tlvStream":{"records":[],"unknown":[]}}""", + """{"signature":"3b6bb4872825450ff29d0b46f5835751329b0394a10ac792e4ba2a23b4f17bcc4e5834d1424787830be0ee3d22ac99e674d121f25d19ed931aaabb8ed0eec0fb","chainHash":"6fe28c0ab6f1b372c1a6a246ae63f74f931e8365e15a089c68d6190000000000","shortChannelId":"551502x2711x1","timestamp":{"iso":"2021-09-07T22:38:33Z","unix":1631054313},"channelFlags":{"isEnabled":false,"isNode1":true},"cltvExpiryDelta":144,"htlcMinimumMsat":1,"feeBaseMsat":1000,"feeProportionalMillionths":1,"htlcMaximumMsat":500000000,"tlvStream":{"records":[],"unknown":[]}}""", hex"12540b6a236e21932622d61432f52913d9442cc09a1057c386119a286153f8681c66d2a0f17d32505ba71bb37c8edcfa9c11e151b2b38dae98b825eff1c040b36fe28c0ab6f1b372c1a6a246ae63f74f931e8365e15a089c68d619000000000008850f00058e00015e6a782e0000009000000000000003e8000003e800000002" -> - """{"signature":"12540b6a236e21932622d61432f52913d9442cc09a1057c386119a286153f8681c66d2a0f17d32505ba71bb37c8edcfa9c11e151b2b38dae98b825eff1c040b3","chainHash":"6fe28c0ab6f1b372c1a6a246ae63f74f931e8365e15a089c68d6190000000000","shortChannelId":"558351x1422x1","timestamp":1584035886,"channelFlags":{"isEnabled":true,"isNode1":true},"cltvExpiryDelta":144,"htlcMinimumMsat":1000,"feeBaseMsat":1000,"feeProportionalMillionths":2,"tlvStream":{"records":[],"unknown":[]}}""", + """{"signature":"12540b6a236e21932622d61432f52913d9442cc09a1057c386119a286153f8681c66d2a0f17d32505ba71bb37c8edcfa9c11e151b2b38dae98b825eff1c040b3","chainHash":"6fe28c0ab6f1b372c1a6a246ae63f74f931e8365e15a089c68d6190000000000","shortChannelId":"558351x1422x1","timestamp":{"iso":"2020-03-12T17:58:06Z","unix":1584035886},"channelFlags":{"isEnabled":true,"isNode1":true},"cltvExpiryDelta":144,"htlcMinimumMsat":1000,"feeBaseMsat":1000,"feeProportionalMillionths":2,"tlvStream":{"records":[],"unknown":[]}}""", hex"8efb98c939aba422a1f2ccd3e05e5471be41c54ac5d7cb27b9aaaecea45f3abb363907644c44b385d83ef6b577061847396d6d3464e4f1fa9e779395e36703ef6fe28c0ab6f1b372c1a6a246ae63f74f931e8365e15a089c68d61900000000000a79dd00098800006137f9ba0100002800000000000003e800000000000003e800000000938580c0" -> - """{"signature":"8efb98c939aba422a1f2ccd3e05e5471be41c54ac5d7cb27b9aaaecea45f3abb363907644c44b385d83ef6b577061847396d6d3464e4f1fa9e779395e36703ef","chainHash":"6fe28c0ab6f1b372c1a6a246ae63f74f931e8365e15a089c68d6190000000000","shortChannelId":"686557x2440x0","timestamp":1631058362,"channelFlags":{"isEnabled":true,"isNode1":true},"cltvExpiryDelta":40,"htlcMinimumMsat":1000,"feeBaseMsat":0,"feeProportionalMillionths":1000,"htlcMaximumMsat":2475000000,"tlvStream":{"records":[],"unknown":[]}}""", + """{"signature":"8efb98c939aba422a1f2ccd3e05e5471be41c54ac5d7cb27b9aaaecea45f3abb363907644c44b385d83ef6b577061847396d6d3464e4f1fa9e779395e36703ef","chainHash":"6fe28c0ab6f1b372c1a6a246ae63f74f931e8365e15a089c68d6190000000000","shortChannelId":"686557x2440x0","timestamp":{"iso":"2021-09-07T23:46:02Z","unix":1631058362},"channelFlags":{"isEnabled":true,"isNode1":true},"cltvExpiryDelta":40,"htlcMinimumMsat":1000,"feeBaseMsat":0,"feeProportionalMillionths":1000,"htlcMaximumMsat":2475000000,"tlvStream":{"records":[],"unknown":[]}}""", hex"4d77b955573527208ac391cf0aeffdfd5efbd99f365ef59f050e373dfbbec69337023a2ce66439cf9c56255486785af0760a08fec51cefce57e0b85ba3c594f46fe28c0ab6f1b372c1a6a246ae63f74f931e8365e15a089c68d619000000000008409700047800005df69f480000009000000000000003e8000003e800000001" -> - """{"signature":"4d77b955573527208ac391cf0aeffdfd5efbd99f365ef59f050e373dfbbec69337023a2ce66439cf9c56255486785af0760a08fec51cefce57e0b85ba3c594f4","chainHash":"6fe28c0ab6f1b372c1a6a246ae63f74f931e8365e15a089c68d6190000000000","shortChannelId":"540823x1144x0","timestamp":1576443720,"channelFlags":{"isEnabled":true,"isNode1":true},"cltvExpiryDelta":144,"htlcMinimumMsat":1000,"feeBaseMsat":1000,"feeProportionalMillionths":1,"tlvStream":{"records":[],"unknown":[]}}""", + """{"signature":"4d77b955573527208ac391cf0aeffdfd5efbd99f365ef59f050e373dfbbec69337023a2ce66439cf9c56255486785af0760a08fec51cefce57e0b85ba3c594f4","chainHash":"6fe28c0ab6f1b372c1a6a246ae63f74f931e8365e15a089c68d6190000000000","shortChannelId":"540823x1144x0","timestamp":{"iso":"2019-12-15T21:02:00Z","unix":1576443720},"channelFlags":{"isEnabled":true,"isNode1":true},"cltvExpiryDelta":144,"htlcMinimumMsat":1000,"feeBaseMsat":1000,"feeProportionalMillionths":1,"tlvStream":{"records":[],"unknown":[]}}""", hex"b212e4d88a5ce3201ec34160d90a07eeb0601207d7d53bcf2b8f99b21146d7eb00d6a5b4b80b878eac0d25c2209eda05c913851730a65260c943fec8956cb22e6fe28c0ab6f1b372c1a6a246ae63f74f931e8365e15a089c68d61900000000000a48ce0006900000613792e40100002800000000000003e8000003e8000000010000000056d35b20" -> - """{"signature":"b212e4d88a5ce3201ec34160d90a07eeb0601207d7d53bcf2b8f99b21146d7eb00d6a5b4b80b878eac0d25c2209eda05c913851730a65260c943fec8956cb22e","chainHash":"6fe28c0ab6f1b372c1a6a246ae63f74f931e8365e15a089c68d6190000000000","shortChannelId":"673998x1680x0","timestamp":1631032036,"channelFlags":{"isEnabled":true,"isNode1":true},"cltvExpiryDelta":40,"htlcMinimumMsat":1000,"feeBaseMsat":1000,"feeProportionalMillionths":1,"htlcMaximumMsat":1456692000,"tlvStream":{"records":[],"unknown":[]}}""", + """{"signature":"b212e4d88a5ce3201ec34160d90a07eeb0601207d7d53bcf2b8f99b21146d7eb00d6a5b4b80b878eac0d25c2209eda05c913851730a65260c943fec8956cb22e","chainHash":"6fe28c0ab6f1b372c1a6a246ae63f74f931e8365e15a089c68d6190000000000","shortChannelId":"673998x1680x0","timestamp":{"iso":"2021-09-07T16:27:16Z","unix":1631032036},"channelFlags":{"isEnabled":true,"isNode1":true},"cltvExpiryDelta":40,"htlcMinimumMsat":1000,"feeBaseMsat":1000,"feeProportionalMillionths":1,"htlcMaximumMsat":1456692000,"tlvStream":{"records":[],"unknown":[]}}""", hex"29396591aee1bfd292193b4329d24eb9f57ddb143f303d029ae004113a7402af015c721ddc3e5d2e36cc67c92af3bdcd22d55eaf1e532503f9972207b226984f6fe28c0ab6f1b372c1a6a246ae63f74f931e8365e15a089c68d6190000000000096f010006ea000061375a440102002800000000000003e8000003e800000001000000024e160300" -> - """{"signature":"29396591aee1bfd292193b4329d24eb9f57ddb143f303d029ae004113a7402af015c721ddc3e5d2e36cc67c92af3bdcd22d55eaf1e532503f9972207b226984f","chainHash":"6fe28c0ab6f1b372c1a6a246ae63f74f931e8365e15a089c68d6190000000000","shortChannelId":"618241x1770x0","timestamp":1631017540,"channelFlags":{"isEnabled":false,"isNode1":true},"cltvExpiryDelta":40,"htlcMinimumMsat":1000,"feeBaseMsat":1000,"feeProportionalMillionths":1,"htlcMaximumMsat":9900000000,"tlvStream":{"records":[],"unknown":[]}}""", + """{"signature":"29396591aee1bfd292193b4329d24eb9f57ddb143f303d029ae004113a7402af015c721ddc3e5d2e36cc67c92af3bdcd22d55eaf1e532503f9972207b226984f","chainHash":"6fe28c0ab6f1b372c1a6a246ae63f74f931e8365e15a089c68d6190000000000","shortChannelId":"618241x1770x0","timestamp":{"iso":"2021-09-07T12:25:40Z","unix":1631017540},"channelFlags":{"isEnabled":false,"isNode1":true},"cltvExpiryDelta":40,"htlcMinimumMsat":1000,"feeBaseMsat":1000,"feeProportionalMillionths":1,"htlcMaximumMsat":9900000000,"tlvStream":{"records":[],"unknown":[]}}""", hex"3c6de66a61f2b8803537a2d92e7b82db1b44eac664ed6b7f5c7b5360b21d7ce32e5238e98d54701fe6d5b9109b2a2d875878a12d254eb6d651843b787f1ba5de6fe28c0ab6f1b372c1a6a246ae63f74f931e8365e15a089c68d61900000000000a921f0003fc000461386d9e0100002800000000000003e8000003e80000000100000000ec08ce00" -> - """{"signature":"3c6de66a61f2b8803537a2d92e7b82db1b44eac664ed6b7f5c7b5360b21d7ce32e5238e98d54701fe6d5b9109b2a2d875878a12d254eb6d651843b787f1ba5de","chainHash":"6fe28c0ab6f1b372c1a6a246ae63f74f931e8365e15a089c68d6190000000000","shortChannelId":"692767x1020x4","timestamp":1631088030,"channelFlags":{"isEnabled":true,"isNode1":true},"cltvExpiryDelta":40,"htlcMinimumMsat":1000,"feeBaseMsat":1000,"feeProportionalMillionths":1,"htlcMaximumMsat":3960000000,"tlvStream":{"records":[],"unknown":[]}}""", + """{"signature":"3c6de66a61f2b8803537a2d92e7b82db1b44eac664ed6b7f5c7b5360b21d7ce32e5238e98d54701fe6d5b9109b2a2d875878a12d254eb6d651843b787f1ba5de","chainHash":"6fe28c0ab6f1b372c1a6a246ae63f74f931e8365e15a089c68d6190000000000","shortChannelId":"692767x1020x4","timestamp":{"iso":"2021-09-08T08:00:30Z","unix":1631088030},"channelFlags":{"isEnabled":true,"isNode1":true},"cltvExpiryDelta":40,"htlcMinimumMsat":1000,"feeBaseMsat":1000,"feeProportionalMillionths":1,"htlcMaximumMsat":3960000000,"tlvStream":{"records":[],"unknown":[]}}""", hex"180de159377d68ecc3b327594bfb7408374811f3c98b5982af1520802796025a1430a6049294ebc0030518cc9b56a574c38c316122cb674f972734d7054d0b546fe28c0ab6f1b372c1a6a246ae63f74f931e8365e15a089c68d61900000000000868f200029a00006137935701030028000000000000000000000001000002bc0000000001c9c380" -> - """{"signature":"180de159377d68ecc3b327594bfb7408374811f3c98b5982af1520802796025a1430a6049294ebc0030518cc9b56a574c38c316122cb674f972734d7054d0b54","chainHash":"6fe28c0ab6f1b372c1a6a246ae63f74f931e8365e15a089c68d6190000000000","shortChannelId":"551154x666x0","timestamp":1631032151,"channelFlags":{"isEnabled":false,"isNode1":false},"cltvExpiryDelta":40,"htlcMinimumMsat":0,"feeBaseMsat":1,"feeProportionalMillionths":700,"htlcMaximumMsat":30000000,"tlvStream":{"records":[],"unknown":[]}}""", + """{"signature":"180de159377d68ecc3b327594bfb7408374811f3c98b5982af1520802796025a1430a6049294ebc0030518cc9b56a574c38c316122cb674f972734d7054d0b54","chainHash":"6fe28c0ab6f1b372c1a6a246ae63f74f931e8365e15a089c68d6190000000000","shortChannelId":"551154x666x0","timestamp":{"iso":"2021-09-07T16:29:11Z","unix":1631032151},"channelFlags":{"isEnabled":false,"isNode1":false},"cltvExpiryDelta":40,"htlcMinimumMsat":0,"feeBaseMsat":1,"feeProportionalMillionths":700,"htlcMaximumMsat":30000000,"tlvStream":{"records":[],"unknown":[]}}""", ) for ((bin, ref) <- bins) { val decoded = channelUpdateCodec.decode(bin.bits).require diff --git a/eclair-core/src/test/scala/fr/acinq/eclair/wire/protocol/OnionCodecsSpec.scala b/eclair-core/src/test/scala/fr/acinq/eclair/wire/protocol/OnionCodecsSpec.scala index 1ffcd39516..7674321587 100644 --- a/eclair-core/src/test/scala/fr/acinq/eclair/wire/protocol/OnionCodecsSpec.scala +++ b/eclair-core/src/test/scala/fr/acinq/eclair/wire/protocol/OnionCodecsSpec.scala @@ -81,7 +81,8 @@ class OnionCodecsSpec extends AnyFunSuite { test("encode/decode variable-length (tlv) relay per-hop payload") { val testCases = Map( TlvStream[OnionTlv](AmountToForward(561 msat), OutgoingCltv(CltvExpiry(42)), OutgoingChannelId(ShortChannelId(1105))) -> hex"11 02020231 04012a 06080000000000000451", - TlvStream[OnionTlv](Seq(AmountToForward(561 msat), OutgoingCltv(CltvExpiry(42)), OutgoingChannelId(ShortChannelId(1105))), Seq(GenericTlv(65535, hex"06c1"))) -> hex"17 02020231 04012a 06080000000000000451 fdffff0206c1" + TlvStream[OnionTlv](AmountToForward(561 msat), OutgoingCltv(CltvExpiry(42)), OutgoingChannelId(ShortChannelId(1105)), EncryptedRecipientData(hex"0123456789abcdef"), BlindingPoint(PublicKey(hex"036d6caac248af96f6afa7f904f550253a0f3ef3f5aa2fe6838a95b216691468e2"))) -> hex"3e 02020231 04012a 06080000000000000451 0a080123456789abcdef 0c21036d6caac248af96f6afa7f904f550253a0f3ef3f5aa2fe6838a95b216691468e2", + TlvStream[OnionTlv](Seq(AmountToForward(561 msat), OutgoingCltv(CltvExpiry(42)), OutgoingChannelId(ShortChannelId(1105))), Seq(GenericTlv(65535, hex"06c1"))) -> hex"17 02020231 04012a 06080000000000000451 fdffff0206c1", ) for ((expected, bin) <- testCases) { @@ -144,6 +145,7 @@ class OnionCodecsSpec extends AnyFunSuite { val testCases = Map( TlvStream[OnionTlv](AmountToForward(561 msat), OutgoingCltv(CltvExpiry(42)), PaymentData(ByteVector32(hex"eec7245d6b7d2ccb30380bfbe2a3648cd7a942653f5aa340edcea1f283686619"), 0 msat)) -> hex"29 02020231 04012a 0820eec7245d6b7d2ccb30380bfbe2a3648cd7a942653f5aa340edcea1f283686619", TlvStream[OnionTlv](AmountToForward(561 msat), OutgoingCltv(CltvExpiry(42)), PaymentData(ByteVector32(hex"eec7245d6b7d2ccb30380bfbe2a3648cd7a942653f5aa340edcea1f283686619"), 1105 msat)) -> hex"2b 02020231 04012a 0822eec7245d6b7d2ccb30380bfbe2a3648cd7a942653f5aa340edcea1f2836866190451", + TlvStream[OnionTlv](AmountToForward(561 msat), OutgoingCltv(CltvExpiry(42)), PaymentData(ByteVector32(hex"eec7245d6b7d2ccb30380bfbe2a3648cd7a942653f5aa340edcea1f283686619"), 1105 msat), EncryptedRecipientData(hex"00aa11"), BlindingPoint(PublicKey(hex"036d6caac248af96f6afa7f904f550253a0f3ef3f5aa2fe6838a95b216691468e2"))) -> hex"53 02020231 04012a 0822eec7245d6b7d2ccb30380bfbe2a3648cd7a942653f5aa340edcea1f2836866190451 0a0300aa11 0c21036d6caac248af96f6afa7f904f550253a0f3ef3f5aa2fe6838a95b216691468e2", TlvStream[OnionTlv](AmountToForward(561 msat), OutgoingCltv(CltvExpiry(42)), PaymentData(ByteVector32(hex"eec7245d6b7d2ccb30380bfbe2a3648cd7a942653f5aa340edcea1f283686619"), 4294967295L msat)) -> hex"2d 02020231 04012a 0824eec7245d6b7d2ccb30380bfbe2a3648cd7a942653f5aa340edcea1f283686619ffffffff", TlvStream[OnionTlv](AmountToForward(561 msat), OutgoingCltv(CltvExpiry(42)), PaymentData(ByteVector32(hex"eec7245d6b7d2ccb30380bfbe2a3648cd7a942653f5aa340edcea1f283686619"), 4294967296L msat)) -> hex"2e 02020231 04012a 0825eec7245d6b7d2ccb30380bfbe2a3648cd7a942653f5aa340edcea1f2836866190100000000", TlvStream[OnionTlv](AmountToForward(561 msat), OutgoingCltv(CltvExpiry(42)), PaymentData(ByteVector32(hex"eec7245d6b7d2ccb30380bfbe2a3648cd7a942653f5aa340edcea1f283686619"), 1099511627775L msat)) -> hex"2e 02020231 04012a 0825eec7245d6b7d2ccb30380bfbe2a3648cd7a942653f5aa340edcea1f283686619ffffffffff", diff --git a/eclair-node/src/main/scala/fr/acinq/eclair/api/directives/ErrorDirective.scala b/eclair-node/src/main/scala/fr/acinq/eclair/api/directives/ErrorDirective.scala index a345d33e74..39bdb99c88 100644 --- a/eclair-node/src/main/scala/fr/acinq/eclair/api/directives/ErrorDirective.scala +++ b/eclair-node/src/main/scala/fr/acinq/eclair/api/directives/ErrorDirective.scala @@ -32,10 +32,10 @@ trait ErrorDirective { private val apiExceptionHandler = ExceptionHandler { case t: IllegalArgumentException => - logger.error(s"API call failed with cause=${t.getMessage}", t) + logger.error(s"API call failed with cause=${t.getMessage}") complete(StatusCodes.BadRequest, ErrorResponse(t.getMessage)) case t: Throwable => - logger.error(s"API call failed with cause=${t.getMessage}", t) + logger.error(s"API call failed with cause=${t.getMessage}") complete(StatusCodes.InternalServerError, ErrorResponse(t.getMessage)) } diff --git a/eclair-node/src/main/scala/fr/acinq/eclair/api/directives/ExtraDirectives.scala b/eclair-node/src/main/scala/fr/acinq/eclair/api/directives/ExtraDirectives.scala index 361b53417e..cc8e06376b 100644 --- a/eclair-node/src/main/scala/fr/acinq/eclair/api/directives/ExtraDirectives.scala +++ b/eclair-node/src/main/scala/fr/acinq/eclair/api/directives/ExtraDirectives.scala @@ -16,7 +16,7 @@ package fr.acinq.eclair.api.directives -import akka.http.scaladsl.common.{NameReceptacle, NameUnmarshallerReceptacle} +import akka.http.scaladsl.common.{NameDefaultUnmarshallerReceptacle, NameReceptacle, NameUnmarshallerReceptacle} import akka.http.scaladsl.marshalling.ToResponseMarshaller import akka.http.scaladsl.model.StatusCodes.NotFound import akka.http.scaladsl.model.{ContentTypes, HttpResponse} @@ -27,7 +27,7 @@ import fr.acinq.eclair.ApiTypes.ChannelIdentifier import fr.acinq.eclair.api.serde.FormParamExtractors._ import fr.acinq.eclair.api.serde.JsonSupport._ import fr.acinq.eclair.payment.PaymentRequest -import fr.acinq.eclair.{MilliSatoshi, ShortChannelId} +import fr.acinq.eclair.{MilliSatoshi, ShortChannelId, TimestampSecond, TimestampSecondLong} import scala.concurrent.Future import scala.util.{Failure, Success} @@ -42,8 +42,8 @@ trait ExtraDirectives extends Directives { val nodeIdFormParam: NameReceptacle[PublicKey] = "nodeId".as[PublicKey] val nodeIdsFormParam: NameUnmarshallerReceptacle[List[PublicKey]] = "nodeIds".as[List[PublicKey]](pubkeyListUnmarshaller) val paymentHashFormParam: NameUnmarshallerReceptacle[ByteVector32] = "paymentHash".as[ByteVector32](sha256HashUnmarshaller) - val fromFormParam: NameReceptacle[Long] = "from".as[Long] - val toFormParam: NameReceptacle[Long] = "to".as[Long] + val fromFormParam: NameDefaultUnmarshallerReceptacle[TimestampSecond] = "from".as[TimestampSecond](timestampSecondUnmarshaller).?(0 unixsec) + val toFormParam: NameDefaultUnmarshallerReceptacle[TimestampSecond] = "to".as[TimestampSecond](timestampSecondUnmarshaller).?(Long.MaxValue unixsec) val amountMsatFormParam: NameReceptacle[MilliSatoshi] = "amountMsat".as[MilliSatoshi] val invoiceFormParam: NameReceptacle[PaymentRequest] = "invoice".as[PaymentRequest] val routeFormatFormParam: NameUnmarshallerReceptacle[RouteFormat] = "format".as[RouteFormat](routeFormatUnmarshaller) diff --git a/eclair-node/src/main/scala/fr/acinq/eclair/api/handlers/Channel.scala b/eclair-node/src/main/scala/fr/acinq/eclair/api/handlers/Channel.scala index 2ee9f2feae..4ac5155d04 100644 --- a/eclair-node/src/main/scala/fr/acinq/eclair/api/handlers/Channel.scala +++ b/eclair-node/src/main/scala/fr/acinq/eclair/api/handlers/Channel.scala @@ -101,8 +101,8 @@ trait Channel { } val channelStats: Route = postRequest("channelstats") { implicit t => - formFields(fromFormParam.?, toFormParam.?) { (from_opt, to_opt) => - complete(eclairApi.channelStats(from_opt, to_opt)) + formFields(fromFormParam, toFormParam) { (from, to) => + complete(eclairApi.channelStats(from, to)) } } diff --git a/eclair-node/src/main/scala/fr/acinq/eclair/api/handlers/Fees.scala b/eclair-node/src/main/scala/fr/acinq/eclair/api/handlers/Fees.scala index 40de4c5dbb..74f5c96f1f 100644 --- a/eclair-node/src/main/scala/fr/acinq/eclair/api/handlers/Fees.scala +++ b/eclair-node/src/main/scala/fr/acinq/eclair/api/handlers/Fees.scala @@ -28,8 +28,8 @@ trait Fees { import fr.acinq.eclair.api.serde.JsonSupport.{formats, marshaller, serialization} val networkFees: Route = postRequest("networkfees") { implicit t => - formFields(fromFormParam.?, toFormParam.?) { (from_opt, to_opt) => - complete(eclairApi.networkFees(from_opt, to_opt)) + formFields(fromFormParam, toFormParam) { (from, to) => + complete(eclairApi.networkFees(from, to)) } } diff --git a/eclair-node/src/main/scala/fr/acinq/eclair/api/handlers/Invoice.scala b/eclair-node/src/main/scala/fr/acinq/eclair/api/handlers/Invoice.scala index fcfc3de8e4..ef921bb76c 100644 --- a/eclair-node/src/main/scala/fr/acinq/eclair/api/handlers/Invoice.scala +++ b/eclair-node/src/main/scala/fr/acinq/eclair/api/handlers/Invoice.scala @@ -42,14 +42,14 @@ trait Invoice { } val listInvoices: Route = postRequest("listinvoices") { implicit t => - formFields(fromFormParam.?, toFormParam.?) { (from_opt, to_opt) => - complete(eclairApi.allInvoices(from_opt, to_opt)) + formFields(fromFormParam, toFormParam) { (from, to) => + complete(eclairApi.allInvoices(from, to)) } } val listPendingInvoices: Route = postRequest("listpendinginvoices") { implicit t => - formFields(fromFormParam.?, toFormParam.?) { (from_opt, to_opt) => - complete(eclairApi.pendingInvoices(from_opt, to_opt)) + formFields(fromFormParam, toFormParam) { (from, to) => + complete(eclairApi.pendingInvoices(from, to)) } } diff --git a/eclair-node/src/main/scala/fr/acinq/eclair/api/handlers/Node.scala b/eclair-node/src/main/scala/fr/acinq/eclair/api/handlers/Node.scala index 4f6419f56d..da5cf1a23c 100644 --- a/eclair-node/src/main/scala/fr/acinq/eclair/api/handlers/Node.scala +++ b/eclair-node/src/main/scala/fr/acinq/eclair/api/handlers/Node.scala @@ -57,8 +57,8 @@ trait Node { } val audit: Route = postRequest("audit") { implicit t => - formFields(fromFormParam.?, toFormParam.?) { (from_opt, to_opt) => - complete(eclairApi.audit(from_opt, to_opt)) + formFields(fromFormParam, toFormParam) { (from, to) => + complete(eclairApi.audit(from, to)) } } diff --git a/eclair-node/src/main/scala/fr/acinq/eclair/api/serde/FormParamExtractors.scala b/eclair-node/src/main/scala/fr/acinq/eclair/api/serde/FormParamExtractors.scala index e2f064fcd9..9502b3fb69 100644 --- a/eclair-node/src/main/scala/fr/acinq/eclair/api/serde/FormParamExtractors.scala +++ b/eclair-node/src/main/scala/fr/acinq/eclair/api/serde/FormParamExtractors.scala @@ -25,7 +25,7 @@ import fr.acinq.eclair.api.serde.JsonSupport._ import fr.acinq.eclair.blockchain.fee.FeeratePerByte import fr.acinq.eclair.io.NodeURI import fr.acinq.eclair.payment.PaymentRequest -import fr.acinq.eclair.{MilliSatoshi, ShortChannelId} +import fr.acinq.eclair.{MilliSatoshi, ShortChannelId, TimestampSecond} import scodec.bits.ByteVector import java.util.UUID @@ -66,6 +66,8 @@ object FormParamExtractors { implicit val routeFormatUnmarshaller: Unmarshaller[String, RouteFormat] = Unmarshaller.strict { str => RouteFormat.fromString(str) } + implicit val timestampSecondUnmarshaller: Unmarshaller[String, TimestampSecond] = Unmarshaller.strict { str => TimestampSecond(str.toLong) } + private def listUnmarshaller[T](unmarshal: String => T): Unmarshaller[String, List[T]] = Unmarshaller.strict { str => Try(serialization.read[List[String]](str).map(unmarshal)) .recoverWith(_ => Try(str.split(",").toList.map(unmarshal))) diff --git a/eclair-node/src/test/resources/api/findroute b/eclair-node/src/test/resources/api/findroute index bec34bda58..5f9c7d22f9 100644 --- a/eclair-node/src/test/resources/api/findroute +++ b/eclair-node/src/test/resources/api/findroute @@ -9,7 +9,10 @@ }, "chainHash" : "024b7b3626554c44dcc2454ee3812458bfa68d9fced466edfab470844cb7ffe2", "shortChannelId" : "1x2x3", - "timestamp" : 0, + "timestamp" : { + "iso" : "1970-01-01T00:00:00Z", + "unix" : 0 + }, "channelFlags" : { "isEnabled" : true, "isNode1" : true @@ -32,7 +35,10 @@ }, "chainHash" : "024b7b3626554c44dcc2454ee3812458bfa68d9fced466edfab470844cb7ffe2", "shortChannelId" : "1x2x4", - "timestamp" : 0, + "timestamp" : { + "iso" : "1970-01-01T00:00:00Z", + "unix" : 0 + }, "channelFlags" : { "isEnabled" : true, "isNode1" : true @@ -55,7 +61,10 @@ }, "chainHash" : "024b7b3626554c44dcc2454ee3812458bfa68d9fced466edfab470844cb7ffe2", "shortChannelId" : "1x2x5", - "timestamp" : 0, + "timestamp" : { + "iso" : "1970-01-01T00:00:00Z", + "unix" : 0 + }, "channelFlags" : { "isEnabled" : true, "isNode1" : true diff --git a/eclair-node/src/test/resources/api/received-expired b/eclair-node/src/test/resources/api/received-expired index edb6d000b4..d754eceb51 100644 --- a/eclair-node/src/test/resources/api/received-expired +++ b/eclair-node/src/test/resources/api/received-expired @@ -1 +1 @@ -{"paymentRequest":{"prefix":"lnbc","timestamp":1496314658,"nodeId":"03e7156ae33b0a208d0744199163177e909e80176e55d97a2f221ede0f934dd9ad","serialized":"lnbc2500u1pvjluezpp5qqqsyqcyq5rqwzqfqqqsyqcyq5rqwzqfqqqsyqcyq5rqwzqfqypqdq5xysxxatsyp3k7enxv4jsxqzpuaztrnwngzn3kdzw5hydlzf03qdgm2hdq27cqv3agm2awhz5se903vruatfhq77w3ls4evs3ch9zw97j25emudupq63nyw24cg27h2rspfj9srp","description":"1 cup coffee","paymentHash":"0001020304050607080900010203040506070809000102030405060708090102","expiry":60,"amount":250000000,"features":{"activated":{},"unknown":[]},"routingInfo":[]},"paymentPreimage":"0100000000000000000000000000000000000000000000000000000000000000","paymentType":"Standard","createdAt":42,"status":{"type":"expired"}} \ No newline at end of file +{"paymentRequest":{"prefix":"lnbc","timestamp":1496314658,"nodeId":"03e7156ae33b0a208d0744199163177e909e80176e55d97a2f221ede0f934dd9ad","serialized":"lnbc2500u1pvjluezpp5qqqsyqcyq5rqwzqfqqqsyqcyq5rqwzqfqqqsyqcyq5rqwzqfqypqdq5xysxxatsyp3k7enxv4jsxqzpuaztrnwngzn3kdzw5hydlzf03qdgm2hdq27cqv3agm2awhz5se903vruatfhq77w3ls4evs3ch9zw97j25emudupq63nyw24cg27h2rspfj9srp","description":"1 cup coffee","paymentHash":"0001020304050607080900010203040506070809000102030405060708090102","expiry":60,"amount":250000000,"features":{"activated":{},"unknown":[]},"routingInfo":[]},"paymentPreimage":"0100000000000000000000000000000000000000000000000000000000000000","paymentType":"Standard","createdAt":{"iso":"1970-01-01T00:00:00.042Z","unix":0},"status":{"type":"expired"}} \ No newline at end of file diff --git a/eclair-node/src/test/resources/api/received-pending b/eclair-node/src/test/resources/api/received-pending index 89c0e9e53f..9e43ab9b12 100644 --- a/eclair-node/src/test/resources/api/received-pending +++ b/eclair-node/src/test/resources/api/received-pending @@ -1 +1 @@ -{"paymentRequest":{"prefix":"lnbc","timestamp":1496314658,"nodeId":"03e7156ae33b0a208d0744199163177e909e80176e55d97a2f221ede0f934dd9ad","serialized":"lnbc2500u1pvjluezpp5qqqsyqcyq5rqwzqfqqqsyqcyq5rqwzqfqqqsyqcyq5rqwzqfqypqdq5xysxxatsyp3k7enxv4jsxqzpuaztrnwngzn3kdzw5hydlzf03qdgm2hdq27cqv3agm2awhz5se903vruatfhq77w3ls4evs3ch9zw97j25emudupq63nyw24cg27h2rspfj9srp","description":"1 cup coffee","paymentHash":"0001020304050607080900010203040506070809000102030405060708090102","expiry":60,"amount":250000000,"features":{"activated":{},"unknown":[]},"routingInfo":[]},"paymentPreimage":"0100000000000000000000000000000000000000000000000000000000000000","paymentType":"Standard","createdAt":42,"status":{"type":"pending"}} \ No newline at end of file +{"paymentRequest":{"prefix":"lnbc","timestamp":1496314658,"nodeId":"03e7156ae33b0a208d0744199163177e909e80176e55d97a2f221ede0f934dd9ad","serialized":"lnbc2500u1pvjluezpp5qqqsyqcyq5rqwzqfqqqsyqcyq5rqwzqfqqqsyqcyq5rqwzqfqypqdq5xysxxatsyp3k7enxv4jsxqzpuaztrnwngzn3kdzw5hydlzf03qdgm2hdq27cqv3agm2awhz5se903vruatfhq77w3ls4evs3ch9zw97j25emudupq63nyw24cg27h2rspfj9srp","description":"1 cup coffee","paymentHash":"0001020304050607080900010203040506070809000102030405060708090102","expiry":60,"amount":250000000,"features":{"activated":{},"unknown":[]},"routingInfo":[]},"paymentPreimage":"0100000000000000000000000000000000000000000000000000000000000000","paymentType":"Standard","createdAt":{"iso":"1970-01-01T00:00:00.042Z","unix":0},"status":{"type":"pending"}} \ No newline at end of file diff --git a/eclair-node/src/test/resources/api/received-success b/eclair-node/src/test/resources/api/received-success index 0214913c86..b54154b991 100644 --- a/eclair-node/src/test/resources/api/received-success +++ b/eclair-node/src/test/resources/api/received-success @@ -1 +1 @@ -{"paymentRequest":{"prefix":"lnbc","timestamp":1496314658,"nodeId":"03e7156ae33b0a208d0744199163177e909e80176e55d97a2f221ede0f934dd9ad","serialized":"lnbc2500u1pvjluezpp5qqqsyqcyq5rqwzqfqqqsyqcyq5rqwzqfqqqsyqcyq5rqwzqfqypqdq5xysxxatsyp3k7enxv4jsxqzpuaztrnwngzn3kdzw5hydlzf03qdgm2hdq27cqv3agm2awhz5se903vruatfhq77w3ls4evs3ch9zw97j25emudupq63nyw24cg27h2rspfj9srp","description":"1 cup coffee","paymentHash":"0001020304050607080900010203040506070809000102030405060708090102","expiry":60,"amount":250000000,"features":{"activated":{},"unknown":[]},"routingInfo":[]},"paymentPreimage":"0100000000000000000000000000000000000000000000000000000000000000","paymentType":"Standard","createdAt":42,"status":{"type":"received","amount":42,"receivedAt":45}} \ No newline at end of file +{"paymentRequest":{"prefix":"lnbc","timestamp":1496314658,"nodeId":"03e7156ae33b0a208d0744199163177e909e80176e55d97a2f221ede0f934dd9ad","serialized":"lnbc2500u1pvjluezpp5qqqsyqcyq5rqwzqfqqqsyqcyq5rqwzqfqqqsyqcyq5rqwzqfqypqdq5xysxxatsyp3k7enxv4jsxqzpuaztrnwngzn3kdzw5hydlzf03qdgm2hdq27cqv3agm2awhz5se903vruatfhq77w3ls4evs3ch9zw97j25emudupq63nyw24cg27h2rspfj9srp","description":"1 cup coffee","paymentHash":"0001020304050607080900010203040506070809000102030405060708090102","expiry":60,"amount":250000000,"features":{"activated":{},"unknown":[]},"routingInfo":[]},"paymentPreimage":"0100000000000000000000000000000000000000000000000000000000000000","paymentType":"Standard","createdAt":{"iso":"1970-01-01T00:00:00.042Z","unix":0},"status":{"type":"received","amount":42,"receivedAt":{"iso":"2021-10-05T13:12:23.777Z","unix":1633439543}}} \ No newline at end of file diff --git a/eclair-node/src/test/resources/api/sent-failed b/eclair-node/src/test/resources/api/sent-failed index d9f5d99dd7..a86e01c5bc 100644 --- a/eclair-node/src/test/resources/api/sent-failed +++ b/eclair-node/src/test/resources/api/sent-failed @@ -1 +1 @@ -[{"id":"00000000-0000-0000-0000-000000000000","parentId":"11111111-1111-1111-1111-111111111111","paymentHash":"0000000000000000000000000000000000000000000000000000000000000000","paymentType":"Standard","amount":42,"recipientAmount":50,"recipientNodeId":"03af0ed6052cf28d670665549bc86f4b721c9fdb309d40c58f5811f63966e005d0","createdAt":1,"status":{"type":"failed","failures":[],"completedAt":2}}] \ No newline at end of file +[{"id":"00000000-0000-0000-0000-000000000000","parentId":"11111111-1111-1111-1111-111111111111","paymentHash":"0000000000000000000000000000000000000000000000000000000000000000","paymentType":"Standard","amount":42,"recipientAmount":50,"recipientNodeId":"03af0ed6052cf28d670665549bc86f4b721c9fdb309d40c58f5811f63966e005d0","createdAt":{"iso":"2021-10-05T13:10:29.123Z","unix":1633439429},"status":{"type":"failed","failures":[],"completedAt":{"iso":"2021-10-05T13:12:23.777Z","unix":1633439543}}}] \ No newline at end of file diff --git a/eclair-node/src/test/resources/api/sent-pending b/eclair-node/src/test/resources/api/sent-pending index 46c8d7375c..caebcf3f5d 100644 --- a/eclair-node/src/test/resources/api/sent-pending +++ b/eclair-node/src/test/resources/api/sent-pending @@ -1 +1 @@ -[{"id":"00000000-0000-0000-0000-000000000000","parentId":"11111111-1111-1111-1111-111111111111","paymentHash":"0000000000000000000000000000000000000000000000000000000000000000","paymentType":"Standard","amount":42,"recipientAmount":50,"recipientNodeId":"03af0ed6052cf28d670665549bc86f4b721c9fdb309d40c58f5811f63966e005d0","createdAt":1,"status":{"type":"pending"}}] \ No newline at end of file +[{"id":"00000000-0000-0000-0000-000000000000","parentId":"11111111-1111-1111-1111-111111111111","paymentHash":"0000000000000000000000000000000000000000000000000000000000000000","paymentType":"Standard","amount":42,"recipientAmount":50,"recipientNodeId":"03af0ed6052cf28d670665549bc86f4b721c9fdb309d40c58f5811f63966e005d0","createdAt":{"iso":"2021-10-05T13:10:29.123Z","unix":1633439429},"status":{"type":"pending"}}] \ No newline at end of file diff --git a/eclair-node/src/test/resources/api/sent-success b/eclair-node/src/test/resources/api/sent-success index cc34b1c335..bec125f93b 100644 --- a/eclair-node/src/test/resources/api/sent-success +++ b/eclair-node/src/test/resources/api/sent-success @@ -1 +1 @@ -[{"id":"00000000-0000-0000-0000-000000000000","parentId":"11111111-1111-1111-1111-111111111111","paymentHash":"0000000000000000000000000000000000000000000000000000000000000000","paymentType":"Standard","amount":42,"recipientAmount":50,"recipientNodeId":"03af0ed6052cf28d670665549bc86f4b721c9fdb309d40c58f5811f63966e005d0","createdAt":1,"status":{"type":"sent","paymentPreimage":"0100000000000000000000000000000000000000000000000000000000000000","feesPaid":5,"route":[],"completedAt":3}}] \ No newline at end of file +[{"id":"00000000-0000-0000-0000-000000000000","parentId":"11111111-1111-1111-1111-111111111111","paymentHash":"0000000000000000000000000000000000000000000000000000000000000000","paymentType":"Standard","amount":42,"recipientAmount":50,"recipientNodeId":"03af0ed6052cf28d670665549bc86f4b721c9fdb309d40c58f5811f63966e005d0","createdAt":{"iso":"2021-10-05T13:10:29.123Z","unix":1633439429},"status":{"type":"sent","paymentPreimage":"0100000000000000000000000000000000000000000000000000000000000000","feesPaid":5,"route":[],"completedAt":{"iso":"2021-10-05T13:12:23.777Z","unix":1633439543}}}] \ No newline at end of file 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 659d3b2ef3..39dc10e7be 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 @@ -612,7 +612,7 @@ class ApiServiceSpec extends AnyFunSuite with ScalatestRouteTest with IdiomaticM } val uuid = UUID.fromString("487da196-a4dc-4b1e-92b4-3e5e905e9f3f") - val paymentSent = PaymentSent(uuid, ByteVector32.Zeroes, ByteVector32.One, 25 msat, aliceNodeId, Seq(PaymentSent.PartialPayment(uuid, 21 msat, 1 msat, ByteVector32.Zeroes, None, 1553784337711L))) + val paymentSent = PaymentSent(uuid, ByteVector32.Zeroes, ByteVector32.One, 25 msat, aliceNodeId, Seq(PaymentSent.PartialPayment(uuid, 21 msat, 1 msat, ByteVector32.Zeroes, None, TimestampMilli(1553784337711L)))) eclair.sendBlocking(any, any, any, any, any, any, any)(any[Timeout]).returns(Future.successful(Right(paymentSent))) Post("/payinvoice", FormData("invoice" -> invoice, "blocking" -> "true").toEntity) ~> addCredentials(BasicHttpCredentials("", mockApi().password)) ~> @@ -621,11 +621,11 @@ class ApiServiceSpec extends AnyFunSuite with ScalatestRouteTest with IdiomaticM assert(handled) assert(status == OK) val response = entityAs[String] - val expected = """{"type":"payment-sent","id":"487da196-a4dc-4b1e-92b4-3e5e905e9f3f","paymentHash":"0000000000000000000000000000000000000000000000000000000000000000","paymentPreimage":"0100000000000000000000000000000000000000000000000000000000000000","recipientAmount":25,"recipientNodeId":"03af0ed6052cf28d670665549bc86f4b721c9fdb309d40c58f5811f63966e005d0","parts":[{"id":"487da196-a4dc-4b1e-92b4-3e5e905e9f3f","amount":21,"feesPaid":1,"toChannelId":"0000000000000000000000000000000000000000000000000000000000000000","timestamp":1553784337711}]}""" + val expected = """{"type":"payment-sent","id":"487da196-a4dc-4b1e-92b4-3e5e905e9f3f","paymentHash":"0000000000000000000000000000000000000000000000000000000000000000","paymentPreimage":"0100000000000000000000000000000000000000000000000000000000000000","recipientAmount":25,"recipientNodeId":"03af0ed6052cf28d670665549bc86f4b721c9fdb309d40c58f5811f63966e005d0","parts":[{"id":"487da196-a4dc-4b1e-92b4-3e5e905e9f3f","amount":21,"feesPaid":1,"toChannelId":"0000000000000000000000000000000000000000000000000000000000000000","timestamp":{"iso":"2019-03-28T14:45:37.711Z","unix":1553784337}}]}""" assert(response === expected) } - val paymentFailed = PaymentFailed(uuid, ByteVector32.Zeroes, failures = Seq.empty, timestamp = 1553784963659L) + val paymentFailed = PaymentFailed(uuid, ByteVector32.Zeroes, failures = Seq.empty, timestamp = TimestampMilli(1553784963659L)) eclair.sendBlocking(any, any, any, any, any, any, any)(any[Timeout]).returns(Future.successful(Right(paymentFailed))) Post("/payinvoice", FormData("invoice" -> invoice, "blocking" -> "true").toEntity) ~> addCredentials(BasicHttpCredentials("", mockApi().password)) ~> @@ -634,7 +634,7 @@ class ApiServiceSpec extends AnyFunSuite with ScalatestRouteTest with IdiomaticM assert(handled) assert(status == OK) val response = entityAs[String] - val expected = """{"type":"payment-failed","id":"487da196-a4dc-4b1e-92b4-3e5e905e9f3f","paymentHash":"0000000000000000000000000000000000000000000000000000000000000000","failures":[],"timestamp":1553784963659}""" + val expected = """{"type":"payment-failed","id":"487da196-a4dc-4b1e-92b4-3e5e905e9f3f","paymentHash":"0000000000000000000000000000000000000000000000000000000000000000","failures":[],"timestamp":{"iso":"2019-03-28T14:56:03.659Z","unix":1553784963}}""" assert(response === expected) } } @@ -783,7 +783,7 @@ class ApiServiceSpec extends AnyFunSuite with ScalatestRouteTest with IdiomaticM test("'getreceivedinfo' 1") { val invoice = "lnbc2500u1pvjluezpp5qqqsyqcyq5rqwzqfqqqsyqcyq5rqwzqfqqqsyqcyq5rqwzqfqypqdq5xysxxatsyp3k7enxv4jsxqzpuaztrnwngzn3kdzw5hydlzf03qdgm2hdq27cqv3agm2awhz5se903vruatfhq77w3ls4evs3ch9zw97j25emudupq63nyw24cg27h2rspfj9srp" - val defaultPayment = IncomingPayment(PaymentRequest.read(invoice), ByteVector32.One, PaymentType.Standard, 42, IncomingPaymentStatus.Pending) + val defaultPayment = IncomingPayment(PaymentRequest.read(invoice), ByteVector32.One, PaymentType.Standard, 42 unixms, IncomingPaymentStatus.Pending) val eclair = mock[Eclair] val notFound = randomBytes32() eclair.receivedInfo(notFound)(any) returns Future.successful(None) @@ -803,7 +803,7 @@ class ApiServiceSpec extends AnyFunSuite with ScalatestRouteTest with IdiomaticM test("'getreceivedinfo' 2") { val invoice = "lnbc2500u1pvjluezpp5qqqsyqcyq5rqwzqfqqqsyqcyq5rqwzqfqqqsyqcyq5rqwzqfqypqdq5xysxxatsyp3k7enxv4jsxqzpuaztrnwngzn3kdzw5hydlzf03qdgm2hdq27cqv3agm2awhz5se903vruatfhq77w3ls4evs3ch9zw97j25emudupq63nyw24cg27h2rspfj9srp" - val defaultPayment = IncomingPayment(PaymentRequest.read(invoice), ByteVector32.One, PaymentType.Standard, 42, IncomingPaymentStatus.Pending) + val defaultPayment = IncomingPayment(PaymentRequest.read(invoice), ByteVector32.One, PaymentType.Standard, 42 unixms, IncomingPaymentStatus.Pending) val eclair = mock[Eclair] val pending = randomBytes32() eclair.receivedInfo(pending)(any) returns Future.successful(Some(defaultPayment)) @@ -823,7 +823,7 @@ class ApiServiceSpec extends AnyFunSuite with ScalatestRouteTest with IdiomaticM test("'getreceivedinfo' 3") { val invoice = "lnbc2500u1pvjluezpp5qqqsyqcyq5rqwzqfqqqsyqcyq5rqwzqfqqqsyqcyq5rqwzqfqypqdq5xysxxatsyp3k7enxv4jsxqzpuaztrnwngzn3kdzw5hydlzf03qdgm2hdq27cqv3agm2awhz5se903vruatfhq77w3ls4evs3ch9zw97j25emudupq63nyw24cg27h2rspfj9srp" - val defaultPayment = IncomingPayment(PaymentRequest.read(invoice), ByteVector32.One, PaymentType.Standard, 42, IncomingPaymentStatus.Pending) + val defaultPayment = IncomingPayment(PaymentRequest.read(invoice), ByteVector32.One, PaymentType.Standard, 42 unixms, IncomingPaymentStatus.Pending) val eclair = mock[Eclair] val expired = randomBytes32() eclair.receivedInfo(expired)(any) returns Future.successful(Some(defaultPayment.copy(status = IncomingPaymentStatus.Expired))) @@ -843,10 +843,10 @@ class ApiServiceSpec extends AnyFunSuite with ScalatestRouteTest with IdiomaticM test("'getreceivedinfo' 4") { val invoice = "lnbc2500u1pvjluezpp5qqqsyqcyq5rqwzqfqqqsyqcyq5rqwzqfqqqsyqcyq5rqwzqfqypqdq5xysxxatsyp3k7enxv4jsxqzpuaztrnwngzn3kdzw5hydlzf03qdgm2hdq27cqv3agm2awhz5se903vruatfhq77w3ls4evs3ch9zw97j25emudupq63nyw24cg27h2rspfj9srp" - val defaultPayment = IncomingPayment(PaymentRequest.read(invoice), ByteVector32.One, PaymentType.Standard, 42, IncomingPaymentStatus.Pending) + val defaultPayment = IncomingPayment(PaymentRequest.read(invoice), ByteVector32.One, PaymentType.Standard, 42 unixms, IncomingPaymentStatus.Pending) val eclair = mock[Eclair] val received = randomBytes32() - eclair.receivedInfo(received)(any) returns Future.successful(Some(defaultPayment.copy(status = IncomingPaymentStatus.Received(42 msat, 45)))) + eclair.receivedInfo(received)(any) returns Future.successful(Some(defaultPayment.copy(status = IncomingPaymentStatus.Received(42 msat, TimestampMilli(1633439543777L))))) val mockService = new MockService(eclair) Post("/getreceivedinfo", FormData("paymentHash" -> received.toHex).toEntity) ~> @@ -862,7 +862,7 @@ class ApiServiceSpec extends AnyFunSuite with ScalatestRouteTest with IdiomaticM } test("'getsentinfo' 1") { - val defaultPayment = OutgoingPayment(UUID.fromString("00000000-0000-0000-0000-000000000000"), UUID.fromString("11111111-1111-1111-1111-111111111111"), None, ByteVector32.Zeroes, PaymentType.Standard, 42 msat, 50 msat, aliceNodeId, 1, None, OutgoingPaymentStatus.Pending) + val defaultPayment = OutgoingPayment(UUID.fromString("00000000-0000-0000-0000-000000000000"), UUID.fromString("11111111-1111-1111-1111-111111111111"), None, ByteVector32.Zeroes, PaymentType.Standard, 42 msat, 50 msat, aliceNodeId, TimestampMilli(1633439429123L), None, OutgoingPaymentStatus.Pending) val eclair = mock[Eclair] val pending = UUID.randomUUID() eclair.sentInfo(Left(pending))(any) returns Future.successful(Seq(defaultPayment)) @@ -881,10 +881,10 @@ class ApiServiceSpec extends AnyFunSuite with ScalatestRouteTest with IdiomaticM } test("'getsentinfo' 2") { - val defaultPayment = OutgoingPayment(UUID.fromString("00000000-0000-0000-0000-000000000000"), UUID.fromString("11111111-1111-1111-1111-111111111111"), None, ByteVector32.Zeroes, PaymentType.Standard, 42 msat, 50 msat, aliceNodeId, 1, None, OutgoingPaymentStatus.Pending) + val defaultPayment = OutgoingPayment(UUID.fromString("00000000-0000-0000-0000-000000000000"), UUID.fromString("11111111-1111-1111-1111-111111111111"), None, ByteVector32.Zeroes, PaymentType.Standard, 42 msat, 50 msat, aliceNodeId, TimestampMilli(1633439429123L), None, OutgoingPaymentStatus.Pending) val eclair = mock[Eclair] val failed = UUID.randomUUID() - eclair.sentInfo(Left(failed))(any) returns Future.successful(Seq(defaultPayment.copy(status = OutgoingPaymentStatus.Failed(Nil, 2)))) + eclair.sentInfo(Left(failed))(any) returns Future.successful(Seq(defaultPayment.copy(status = OutgoingPaymentStatus.Failed(Nil, TimestampMilli(1633439543777L))))) val mockService = new MockService(eclair) Post("/getsentinfo", FormData("id" -> failed.toString).toEntity) ~> @@ -900,10 +900,10 @@ class ApiServiceSpec extends AnyFunSuite with ScalatestRouteTest with IdiomaticM } test("'getsentinfo' 3") { - val defaultPayment = OutgoingPayment(UUID.fromString("00000000-0000-0000-0000-000000000000"), UUID.fromString("11111111-1111-1111-1111-111111111111"), None, ByteVector32.Zeroes, PaymentType.Standard, 42 msat, 50 msat, aliceNodeId, 1, None, OutgoingPaymentStatus.Pending) + val defaultPayment = OutgoingPayment(UUID.fromString("00000000-0000-0000-0000-000000000000"), UUID.fromString("11111111-1111-1111-1111-111111111111"), None, ByteVector32.Zeroes, PaymentType.Standard, 42 msat, 50 msat, aliceNodeId, TimestampMilli(1633439429123L), None, OutgoingPaymentStatus.Pending) val eclair = mock[Eclair] val sent = UUID.randomUUID() - eclair.sentInfo(Left(sent))(any) returns Future.successful(Seq(defaultPayment.copy(status = OutgoingPaymentStatus.Succeeded(ByteVector32.One, 5 msat, Nil, 3)))) + eclair.sentInfo(Left(sent))(any) returns Future.successful(Seq(defaultPayment.copy(status = OutgoingPaymentStatus.Succeeded(ByteVector32.One, 5 msat, Nil, TimestampMilli(1633439543777L))))) val mockService = new MockService(eclair) Post("/getsentinfo", FormData("id" -> sent.toString).toEntity) ~> @@ -974,7 +974,7 @@ class ApiServiceSpec extends AnyFunSuite with ScalatestRouteTest with IdiomaticM signature = ByteVector64.fromValidHex("92cf3f12e161391986eb2cd7106ddab41a23c734f8f1ed120fb64f4b91f98f690ecf930388e62965f8aefbf1adafcd25a572669a125396dcfb83615208754679"), chainHash = ByteVector32.fromValidHex("024b7b3626554c44dcc2454ee3812458bfa68d9fced466edfab470844cb7ffe2"), shortChannelId = ShortChannelId(1, 2, 3), - timestamp = 0, + timestamp = 0 unixsec, channelFlags = ChannelUpdate.ChannelFlags.DUMMY, cltvExpiryDelta = CltvExpiryDelta(0), htlcMinimumMsat = MilliSatoshi(1), @@ -1099,38 +1099,38 @@ class ApiServiceSpec extends AnyFunSuite with ScalatestRouteTest with IdiomaticM addCredentials(BasicHttpCredentials("", mockApi().password)) ~> mockService.webSocket ~> check { - val pf = PaymentFailed(fixedUUID, ByteVector32.Zeroes, failures = Seq.empty, timestamp = 1553784963659L) - val expectedSerializedPf = """{"type":"payment-failed","id":"487da196-a4dc-4b1e-92b4-3e5e905e9f3f","paymentHash":"0000000000000000000000000000000000000000000000000000000000000000","failures":[],"timestamp":1553784963659}""" + val pf = PaymentFailed(fixedUUID, ByteVector32.Zeroes, failures = Seq.empty, timestamp = TimestampMilli(1553784963659L)) + val expectedSerializedPf = """{"type":"payment-failed","id":"487da196-a4dc-4b1e-92b4-3e5e905e9f3f","paymentHash":"0000000000000000000000000000000000000000000000000000000000000000","failures":[],"timestamp":{"iso":"2019-03-28T14:56:03.659Z","unix":1553784963}}""" assert(serialization.write(pf) === expectedSerializedPf) system.eventStream.publish(pf) wsClient.expectMessage(expectedSerializedPf) - val ps = PaymentSent(fixedUUID, ByteVector32.Zeroes, ByteVector32.One, 25 msat, aliceNodeId, Seq(PaymentSent.PartialPayment(fixedUUID, 21 msat, 1 msat, ByteVector32.Zeroes, None, 1553784337711L))) - val expectedSerializedPs = """{"type":"payment-sent","id":"487da196-a4dc-4b1e-92b4-3e5e905e9f3f","paymentHash":"0000000000000000000000000000000000000000000000000000000000000000","paymentPreimage":"0100000000000000000000000000000000000000000000000000000000000000","recipientAmount":25,"recipientNodeId":"03af0ed6052cf28d670665549bc86f4b721c9fdb309d40c58f5811f63966e005d0","parts":[{"id":"487da196-a4dc-4b1e-92b4-3e5e905e9f3f","amount":21,"feesPaid":1,"toChannelId":"0000000000000000000000000000000000000000000000000000000000000000","timestamp":1553784337711}]}""" + val ps = PaymentSent(fixedUUID, ByteVector32.Zeroes, ByteVector32.One, 25 msat, aliceNodeId, Seq(PaymentSent.PartialPayment(fixedUUID, 21 msat, 1 msat, ByteVector32.Zeroes, None, TimestampMilli(1553784337711L)))) + val expectedSerializedPs = """{"type":"payment-sent","id":"487da196-a4dc-4b1e-92b4-3e5e905e9f3f","paymentHash":"0000000000000000000000000000000000000000000000000000000000000000","paymentPreimage":"0100000000000000000000000000000000000000000000000000000000000000","recipientAmount":25,"recipientNodeId":"03af0ed6052cf28d670665549bc86f4b721c9fdb309d40c58f5811f63966e005d0","parts":[{"id":"487da196-a4dc-4b1e-92b4-3e5e905e9f3f","amount":21,"feesPaid":1,"toChannelId":"0000000000000000000000000000000000000000000000000000000000000000","timestamp":{"iso":"2019-03-28T14:45:37.711Z","unix":1553784337}}]}""" assert(serialization.write(ps) === expectedSerializedPs) system.eventStream.publish(ps) wsClient.expectMessage(expectedSerializedPs) - val prel = ChannelPaymentRelayed(21 msat, 20 msat, ByteVector32.Zeroes, ByteVector32.Zeroes, ByteVector32.One, 1553784963659L) - val expectedSerializedPrel = """{"type":"payment-relayed","amountIn":21,"amountOut":20,"paymentHash":"0000000000000000000000000000000000000000000000000000000000000000","fromChannelId":"0000000000000000000000000000000000000000000000000000000000000000","toChannelId":"0100000000000000000000000000000000000000000000000000000000000000","timestamp":1553784963659}""" + val prel = ChannelPaymentRelayed(21 msat, 20 msat, ByteVector32.Zeroes, ByteVector32.Zeroes, ByteVector32.One, TimestampMilli(1553784963659L)) + val expectedSerializedPrel = """{"type":"payment-relayed","amountIn":21,"amountOut":20,"paymentHash":"0000000000000000000000000000000000000000000000000000000000000000","fromChannelId":"0000000000000000000000000000000000000000000000000000000000000000","toChannelId":"0100000000000000000000000000000000000000000000000000000000000000","timestamp":{"iso":"2019-03-28T14:56:03.659Z","unix":1553784963}}""" assert(serialization.write(prel) === expectedSerializedPrel) system.eventStream.publish(prel) wsClient.expectMessage(expectedSerializedPrel) - val ptrel = TrampolinePaymentRelayed(ByteVector32.Zeroes, Seq(PaymentRelayed.Part(21 msat, ByteVector32.Zeroes)), Seq(PaymentRelayed.Part(8 msat, ByteVector32.Zeroes), PaymentRelayed.Part(10 msat, ByteVector32.One)), bobNodeId, 17 msat, 1553784963659L) - val expectedSerializedPtrel = """{"type":"trampoline-payment-relayed","paymentHash":"0000000000000000000000000000000000000000000000000000000000000000","incoming":[{"amount":21,"channelId":"0000000000000000000000000000000000000000000000000000000000000000"}],"outgoing":[{"amount":8,"channelId":"0000000000000000000000000000000000000000000000000000000000000000"},{"amount":10,"channelId":"0100000000000000000000000000000000000000000000000000000000000000"}],"nextTrampolineNodeId":"039dc0e0b1d25905e44fdf6f8e89755a5e219685840d0bc1d28d3308f9628a3585","nextTrampolineAmount":17,"timestamp":1553784963659}""" + val ptrel = TrampolinePaymentRelayed(ByteVector32.Zeroes, Seq(PaymentRelayed.Part(21 msat, ByteVector32.Zeroes)), Seq(PaymentRelayed.Part(8 msat, ByteVector32.Zeroes), PaymentRelayed.Part(10 msat, ByteVector32.One)), bobNodeId, 17 msat, TimestampMilli(1553784963659L)) + val expectedSerializedPtrel = """{"type":"trampoline-payment-relayed","paymentHash":"0000000000000000000000000000000000000000000000000000000000000000","incoming":[{"amount":21,"channelId":"0000000000000000000000000000000000000000000000000000000000000000"}],"outgoing":[{"amount":8,"channelId":"0000000000000000000000000000000000000000000000000000000000000000"},{"amount":10,"channelId":"0100000000000000000000000000000000000000000000000000000000000000"}],"nextTrampolineNodeId":"039dc0e0b1d25905e44fdf6f8e89755a5e219685840d0bc1d28d3308f9628a3585","nextTrampolineAmount":17,"timestamp":{"iso":"2019-03-28T14:56:03.659Z","unix":1553784963}}""" assert(serialization.write(ptrel) === expectedSerializedPtrel) system.eventStream.publish(ptrel) wsClient.expectMessage(expectedSerializedPtrel) - val precv = PaymentReceived(ByteVector32.Zeroes, Seq(PaymentReceived.PartialPayment(21 msat, ByteVector32.Zeroes, 1553784963659L))) - val expectedSerializedPrecv = """{"type":"payment-received","paymentHash":"0000000000000000000000000000000000000000000000000000000000000000","parts":[{"amount":21,"fromChannelId":"0000000000000000000000000000000000000000000000000000000000000000","timestamp":1553784963659}]}""" + val precv = PaymentReceived(ByteVector32.Zeroes, Seq(PaymentReceived.PartialPayment(21 msat, ByteVector32.Zeroes, TimestampMilli(1553784963659L)))) + val expectedSerializedPrecv = """{"type":"payment-received","paymentHash":"0000000000000000000000000000000000000000000000000000000000000000","parts":[{"amount":21,"fromChannelId":"0000000000000000000000000000000000000000000000000000000000000000","timestamp":{"iso":"2019-03-28T14:56:03.659Z","unix":1553784963}}]}""" assert(serialization.write(precv) === expectedSerializedPrecv) system.eventStream.publish(precv) wsClient.expectMessage(expectedSerializedPrecv) - val pset = PaymentSettlingOnChain(fixedUUID, 21 msat, ByteVector32.One, timestamp = 1553785442676L) - val expectedSerializedPset = """{"type":"payment-settling-onchain","id":"487da196-a4dc-4b1e-92b4-3e5e905e9f3f","amount":21,"paymentHash":"0100000000000000000000000000000000000000000000000000000000000000","timestamp":1553785442676}""" + val pset = PaymentSettlingOnChain(fixedUUID, 21 msat, ByteVector32.One, timestamp = TimestampMilli(1553785442676L)) + val expectedSerializedPset = """{"type":"payment-settling-onchain","id":"487da196-a4dc-4b1e-92b4-3e5e905e9f3f","amount":21,"paymentHash":"0100000000000000000000000000000000000000000000000000000000000000","timestamp":{"iso":"2019-03-28T15:04:02.676Z","unix":1553785442}}""" assert(serialization.write(pset) === expectedSerializedPset) system.eventStream.publish(pset) wsClient.expectMessage(expectedSerializedPset)