1
0
Fork 0
mirror of https://github.com/ACINQ/eclair.git synced 2025-02-21 14:04:10 +01:00

Move router handlers to separate files (#1352)

Also, acknowledge all gossip with a `GossipDecision`.
This commit is contained in:
Pierre-Marie Padiou 2020-04-09 16:08:10 +02:00 committed by GitHub
parent 932f04851a
commit a58678eb0b
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
49 changed files with 2036 additions and 1556 deletions

View file

@ -34,7 +34,8 @@ import fr.acinq.eclair.payment._
import fr.acinq.eclair.payment.receive.MultiPartHandler.ReceivePayment
import fr.acinq.eclair.payment.relay.Relayer.{GetOutgoingChannels, OutgoingChannels, UsableBalance}
import fr.acinq.eclair.payment.send.PaymentInitiator.{SendPaymentRequest, SendPaymentToRouteRequest, SendPaymentToRouteResponse}
import fr.acinq.eclair.router._
import fr.acinq.eclair.router.Router._
import fr.acinq.eclair.router.{NetworkStats, RouteCalculation}
import fr.acinq.eclair.wire.{ChannelAnnouncement, ChannelUpdate, NodeAddress, NodeAnnouncement}
import scodec.bits.ByteVector
@ -232,7 +233,7 @@ class EclairImpl(appKit: Kit) extends Eclair {
override def send(externalId_opt: Option[String], recipientNodeId: PublicKey, amount: MilliSatoshi, paymentHash: ByteVector32, invoice_opt: Option[PaymentRequest], maxAttempts_opt: Option[Int], feeThreshold_opt: Option[Satoshi], maxFeePct_opt: Option[Double])(implicit timeout: Timeout): Future[UUID] = {
val maxAttempts = maxAttempts_opt.getOrElse(appKit.nodeParams.maxPaymentAttempts)
val defaultRouteParams = Router.getDefaultRouteParams(appKit.nodeParams.routerConf)
val defaultRouteParams = RouteCalculation.getDefaultRouteParams(appKit.nodeParams.routerConf)
val routeParams = defaultRouteParams.copy(
maxFeePct = maxFeePct_opt.getOrElse(defaultRouteParams.maxFeePct),
maxFeeBase = feeThreshold_opt.map(_.toMilliSatoshi).getOrElse(defaultRouteParams.maxFeeBase)

View file

@ -28,6 +28,7 @@ import fr.acinq.eclair.channel.{LocalChannelDown, LocalChannelUpdate}
import fr.acinq.eclair.crypto.TransportHandler.HandshakeCompleted
import fr.acinq.eclair.io.Peer.PeerRoutingMessage
import fr.acinq.eclair.io.{Peer, PeerConnection}
import fr.acinq.eclair.router.Router._
import fr.acinq.eclair.router._
import fr.acinq.eclair.wire._
@ -44,12 +45,12 @@ object Logs {
).flatten.toMap
/**
* Temporarily add the provided MDC to the current one, and then restore the original one.
*
* This is useful in some cases where we can't rely on the `aroundReceive` trick to set the MDC before processing a
* message because we don't have enough context. That's typically the case when handling `Terminated` messages.
*/
def withMdc(log: DiagnosticLoggingAdapter)(mdc: MDC)(f: => Any): Any = {
* Temporarily add the provided MDC to the current one, and then restore the original one.
*
* This is useful in some cases where we can't rely on the `aroundReceive` trick to set the MDC before processing a
* message because we don't have enough context. That's typically the case when handling `Terminated` messages.
*/
def withMdc[T](log: DiagnosticLoggingAdapter)(mdc: MDC)(f: => T): T = {
val mdc0 = log.mdc // backup the current mdc
try {
log.mdc(mdc0 ++ mdc) // add the new mdc to the current one

View file

@ -30,7 +30,7 @@ import fr.acinq.eclair.blockchain.fee.{FeeEstimator, FeeTargets, OnChainFeeConf}
import fr.acinq.eclair.channel.Channel
import fr.acinq.eclair.crypto.KeyManager
import fr.acinq.eclair.db._
import fr.acinq.eclair.router.RouterConf
import fr.acinq.eclair.router.Router.RouterConf
import fr.acinq.eclair.tor.Socks5ProxyParams
import fr.acinq.eclair.wire.{Color, EncodingType, NodeAddress}
import scodec.bits.ByteVector

View file

@ -21,7 +21,7 @@ import java.io.Closeable
import fr.acinq.bitcoin.Crypto.PublicKey
import fr.acinq.bitcoin.{ByteVector32, Satoshi}
import fr.acinq.eclair.ShortChannelId
import fr.acinq.eclair.router.PublicChannel
import fr.acinq.eclair.router.Router.PublicChannel
import fr.acinq.eclair.wire.{ChannelAnnouncement, ChannelUpdate, NodeAnnouncement}
import scala.collection.immutable.SortedMap

View file

@ -22,7 +22,7 @@ import java.util.UUID
import fr.acinq.bitcoin.ByteVector32
import fr.acinq.bitcoin.Crypto.PublicKey
import fr.acinq.eclair.payment._
import fr.acinq.eclair.router.{ChannelHop, Hop, NodeHop}
import fr.acinq.eclair.router.Router.{ChannelHop, Hop, NodeHop}
import fr.acinq.eclair.{MilliSatoshi, ShortChannelId}
import scala.compat.Platform

View file

@ -21,7 +21,7 @@ import java.sql.Connection
import fr.acinq.bitcoin.{ByteVector32, Crypto, Satoshi}
import fr.acinq.eclair.ShortChannelId
import fr.acinq.eclair.db.NetworkDb
import fr.acinq.eclair.router.PublicChannel
import fr.acinq.eclair.router.Router.PublicChannel
import fr.acinq.eclair.wire.LightningMessageCodecs.{channelAnnouncementCodec, channelUpdateCodec, nodeAnnouncementCodec}
import fr.acinq.eclair.wire.{ChannelAnnouncement, ChannelUpdate, NodeAnnouncement}
import grizzled.slf4j.Logging

View file

@ -26,7 +26,7 @@ import fr.acinq.eclair.crypto.Noise.KeyPair
import fr.acinq.eclair.crypto.TransportHandler
import fr.acinq.eclair.io.Monitoring.{Metrics, Tags}
import fr.acinq.eclair.io.Peer.CHANNELID_ZERO
import fr.acinq.eclair.router._
import fr.acinq.eclair.router.Router._
import fr.acinq.eclair.wire._
import fr.acinq.eclair.{wire, _}
import scodec.Attempt
@ -160,7 +160,7 @@ class PeerConnection(nodeParams: NodeParams, switchboard: ActorRef, router: Acto
val flags_opt = if (canUseChannelRangeQueriesEx) Some(QueryChannelRangeTlv.QueryFlags(QueryChannelRangeTlv.QueryFlags.WANT_ALL)) else None
if (d.nodeParams.syncWhitelist.isEmpty || d.nodeParams.syncWhitelist.contains(d.remoteNodeId)) {
log.info(s"sending sync channel range query with flags_opt=$flags_opt")
router ! SendChannelQuery(d.remoteNodeId, self, flags_opt = flags_opt)
router ! SendChannelQuery(nodeParams.chainHash, d.remoteNodeId, self, flags_opt = flags_opt)
} else {
log.info("not syncing with this peer")
}
@ -258,11 +258,12 @@ class PeerConnection(nodeParams: NodeParams, switchboard: ActorRef, router: Acto
case Event(DelayedRebroadcast(rebroadcast), d: ConnectedData) =>
val thisRemote = RemoteGossip(self, d.remoteNodeId)
/**
* Send and count in a single iteration
*/
def sendAndCount(msgs: Map[_ <: RoutingMessage, Set[GossipOrigin]]): Int = msgs.foldLeft(0) {
case (count, (_, origins)) if origins.contains(RemoteGossip(self)) =>
case (count, (_, origins)) if origins.contains(thisRemote) =>
// the announcement came from this peer, we don't send it back
count
case (count, (msg, origins)) if !timestampInRange(d.nodeParams, msg, origins, d.gossipTimestampFilter) =>
@ -321,9 +322,9 @@ class PeerConnection(nodeParams: NodeParams, switchboard: ActorRef, router: Acto
d.transport forward readAck
stay
case Event(badMessage: BadMessage, d: ConnectedData) =>
val behavior1 = badMessage match {
case InvalidSignature(r) =>
case Event(rejectedGossip: GossipDecision.Rejected, d: ConnectedData) =>
val behavior1 = rejectedGossip match {
case GossipDecision.InvalidSignature(r) =>
val bin: String = LightningMessageCodecs.meteredLightningMessageCodec.encode(r) match {
case Attempt.Successful(b) => b.toHex
case _ => "unknown"
@ -333,14 +334,14 @@ class PeerConnection(nodeParams: NodeParams, switchboard: ActorRef, router: Acto
// TODO: this doesn't actually disconnect the peer, once we introduce peer banning we should actively disconnect
d.transport ! Error(CHANNELID_ZERO, ByteVector.view(s"bad announcement sig! bin=$bin".getBytes()))
d.behavior
case InvalidAnnouncement(c) =>
case GossipDecision.InvalidAnnouncement(c) =>
// they seem to be sending us fake announcements?
log.error(s"couldn't find funding tx with valid scripts for shortChannelId=${c.shortChannelId}")
// for now we just return an error, maybe ban the peer in the future?
// TODO: this doesn't actually disconnect the peer, once we introduce peer banning we should actively disconnect
d.transport ! Error(CHANNELID_ZERO, ByteVector.view(s"couldn't verify channel! shortChannelId=${c.shortChannelId}".getBytes()))
d.behavior
case ChannelClosed(_) =>
case GossipDecision.ChannelClosed(_) =>
if (d.behavior.ignoreNetworkAnnouncement) {
// we already are ignoring announcements, we may have additional notifications for announcements that were received right before our ban
d.behavior.copy(fundingTxAlreadySpentCount = d.behavior.fundingTxAlreadySpentCount + 1)
@ -351,6 +352,15 @@ class PeerConnection(nodeParams: NodeParams, switchboard: ActorRef, router: Acto
setTimer(ResumeAnnouncements.toString, ResumeAnnouncements, IGNORE_NETWORK_ANNOUNCEMENTS_PERIOD, repeat = false)
d.behavior.copy(fundingTxAlreadySpentCount = d.behavior.fundingTxAlreadySpentCount + 1, ignoreNetworkAnnouncement = true)
}
// other rejections are not considered punishable offenses
// we are not using a catch-all on purpose, to make compiler warn us when a new error is added
case _: GossipDecision.Duplicate => d.behavior
case _: GossipDecision.NoKnownChannel => d.behavior
case _: GossipDecision.ValidationFailure => d.behavior
case _: GossipDecision.ChannelPruned => d.behavior
case _: GossipDecision.ChannelClosing => d.behavior
case _: GossipDecision.Stale => d.behavior
case _: GossipDecision.NoRelatedChannel => d.behavior
}
stay using d.copy(behavior = behavior1)
@ -373,6 +383,10 @@ class PeerConnection(nodeParams: NodeParams, switchboard: ActorRef, router: Acto
}
stop(FSM.Normal)
case Event(_: GossipDecision.Accepted, _) => stay // for now we don't do anything with those events
case Event(_: GossipDecision.Rejected, _) => stay // we got disconnected while syncing
case Event(_: Rebroadcast, _) => stay // ignored
case Event(_: DelayedRebroadcast, _) => stay // ignored
@ -386,9 +400,6 @@ class PeerConnection(nodeParams: NodeParams, switchboard: ActorRef, router: Acto
case Event(_: Pong, _) => stay // we got disconnected before receiving the pong
case Event(_: PingTimeout, _) => stay // we got disconnected after sending a ping
case Event(_: BadMessage, _) => stay // we got disconnected while syncing
}
onTransition {
@ -490,11 +501,6 @@ object PeerConnection {
case class DelayedRebroadcast(rebroadcast: Rebroadcast)
sealed trait BadMessage
case class InvalidSignature(r: RoutingMessage) extends BadMessage
case class InvalidAnnouncement(c: ChannelAnnouncement) extends BadMessage
case class ChannelClosed(c: ChannelAnnouncement) extends BadMessage
case class Behavior(fundingTxAlreadySpentCount: Int = 0, ignoreNetworkAnnouncement: Boolean = false)
// @formatter:on

View file

@ -185,4 +185,9 @@ package object eclair {
// @formatter:on
}
/**
* Apparently .getClass.getSimpleName can crash java 8 with a "Malformed class name" error
*/
def getSimpleClassName(o: Any): String = o.getClass.getName.split("\\$").last
}

View file

@ -22,7 +22,7 @@ import fr.acinq.bitcoin.ByteVector32
import fr.acinq.bitcoin.Crypto.PublicKey
import fr.acinq.eclair.MilliSatoshi
import fr.acinq.eclair.crypto.Sphinx
import fr.acinq.eclair.router.Hop
import fr.acinq.eclair.router.Router.Hop
import scala.compat.Platform

View file

@ -21,7 +21,7 @@ import fr.acinq.bitcoin.ByteVector32
import fr.acinq.bitcoin.Crypto.{PrivateKey, PublicKey}
import fr.acinq.eclair.channel.{CMD_ADD_HTLC, Upstream}
import fr.acinq.eclair.crypto.Sphinx
import fr.acinq.eclair.router.{ChannelHop, Hop, NodeHop}
import fr.acinq.eclair.router.Router.{ChannelHop, Hop, NodeHop}
import fr.acinq.eclair.wire._
import fr.acinq.eclair.{CltvExpiry, CltvExpiryDelta, Features, MilliSatoshi, UInt64, randomKey}
import scodec.bits.ByteVector

View file

@ -30,7 +30,8 @@ import fr.acinq.eclair.payment.send.MultiPartPaymentLifecycle.SendMultiPartPayme
import fr.acinq.eclair.payment.send.PaymentInitiator.SendPaymentConfig
import fr.acinq.eclair.payment.send.PaymentLifecycle.SendPayment
import fr.acinq.eclair.payment.send.{MultiPartPaymentLifecycle, PaymentError, PaymentLifecycle}
import fr.acinq.eclair.router.{RouteNotFound, RouteParams, Router}
import fr.acinq.eclair.router.Router.RouteParams
import fr.acinq.eclair.router.{RouteCalculation, RouteNotFound}
import fr.acinq.eclair.wire._
import fr.acinq.eclair.{CltvExpiry, Logs, MilliSatoshi, NodeParams, nodeFee, randomBytes32}
@ -246,7 +247,7 @@ object NodeRelayer {
private def computeRouteParams(nodeParams: NodeParams, amountIn: MilliSatoshi, expiryIn: CltvExpiry, amountOut: MilliSatoshi, expiryOut: CltvExpiry): RouteParams = {
val routeMaxCltv = expiryIn - expiryOut - nodeParams.expiryDeltaBlocks
val routeMaxFee = amountIn - amountOut - nodeFee(nodeParams.feeBase, nodeParams.feeProportionalMillionth, amountOut)
Router.getDefaultRouteParams(nodeParams.routerConf).copy(
RouteCalculation.getDefaultRouteParams(nodeParams.routerConf).copy(
maxFeeBase = routeMaxFee,
routeMaxCltv = routeMaxCltv,
maxFeePct = 0 // we disable percent-based max fee calculation, we're only interested in collecting our node fee

View file

@ -20,7 +20,8 @@ import akka.actor.{Actor, ActorLogging, ActorRef, Props}
import fr.acinq.bitcoin.Crypto.PublicKey
import fr.acinq.eclair.crypto.Sphinx.DecryptedFailurePacket
import fr.acinq.eclair.payment.{PaymentEvent, PaymentFailed, RemoteFailure}
import fr.acinq.eclair.router.{Announcements, Data, PublicChannel}
import fr.acinq.eclair.router.Announcements
import fr.acinq.eclair.router.Router.{Data, PublicChannel}
import fr.acinq.eclair.wire.IncorrectOrUnknownPaymentDetails
import fr.acinq.eclair.{LongToBtcAmount, NodeParams, randomBytes32, secureRandom}

View file

@ -32,6 +32,7 @@ import fr.acinq.eclair.payment._
import fr.acinq.eclair.payment.relay.Relayer.{GetOutgoingChannels, OutgoingChannel, OutgoingChannels}
import fr.acinq.eclair.payment.send.PaymentInitiator.SendPaymentConfig
import fr.acinq.eclair.payment.send.PaymentLifecycle.SendPayment
import fr.acinq.eclair.router.Router.{ChannelHop, GetNetworkStats, GetNetworkStatsResponse, RouteParams, TickComputeNetworkStats}
import fr.acinq.eclair.router._
import fr.acinq.eclair.wire._
import fr.acinq.eclair.{CltvExpiry, FSMDiagnosticActorLogging, Logs, LongToBtcAmount, MilliSatoshi, NodeParams, ShortChannelId, ToMilliSatoshiConversion}

View file

@ -28,7 +28,7 @@ import fr.acinq.eclair.payment._
import fr.acinq.eclair.payment.send.MultiPartPaymentLifecycle.SendMultiPartPayment
import fr.acinq.eclair.payment.send.PaymentError._
import fr.acinq.eclair.payment.send.PaymentLifecycle.{SendPayment, SendPaymentToRoute}
import fr.acinq.eclair.router.{ChannelHop, Hop, NodeHop, RouteParams}
import fr.acinq.eclair.router.Router.{ChannelHop, Hop, NodeHop, RouteParams}
import fr.acinq.eclair.wire.Onion.FinalLegacyPayload
import fr.acinq.eclair.wire._
import fr.acinq.eclair.{CltvExpiry, CltvExpiryDelta, Features, LongToBtcAmount, MilliSatoshi, NodeParams, randomBytes32}

View file

@ -33,6 +33,7 @@ import fr.acinq.eclair.payment._
import fr.acinq.eclair.payment.relay.Relayer
import fr.acinq.eclair.payment.send.PaymentInitiator.SendPaymentConfig
import fr.acinq.eclair.payment.send.PaymentLifecycle._
import fr.acinq.eclair.router.Router._
import fr.acinq.eclair.router._
import fr.acinq.eclair.wire.Onion._
import fr.acinq.eclair.wire._

View file

@ -275,7 +275,7 @@ object Graph {
case false => Seq.empty[GraphEdge]
case true =>
// we traverse the list of "previous" backward building the final list of edges that make the shortest path
val edgePath = new mutable.ArrayBuffer[GraphEdge](ROUTE_MAX_LENGTH)
val edgePath = new mutable.ArrayBuffer[GraphEdge](RouteCalculation.ROUTE_MAX_LENGTH)
var current = prev.get(sourceNode)
while (current != null) {

View file

@ -16,14 +16,23 @@
package fr.acinq.eclair.router
import fr.acinq.eclair.{LongToBtcAmount, MilliSatoshi}
import fr.acinq.eclair.router.Router.GossipDecision
import fr.acinq.eclair.{LongToBtcAmount, MilliSatoshi, getSimpleClassName}
import kamon.Kamon
import kamon.metric.Counter
object Monitoring {
object Metrics {
val FindRouteDuration = Kamon.timer("router.find-route.duration", "Path-finding duration")
val RouteLength = Kamon.histogram("router.find-route.length", "Path-finding result length")
private val GossipResult = Kamon.counter("router.gossip.result")
def gossipResult(decision: GossipDecision): Counter = decision match {
case _: GossipDecision.Accepted => GossipResult.withTag("result", "accepted")
case rejected: GossipDecision.Rejected => GossipResult.withTag("result", "rejected").withTag("reason", getSimpleClassName(rejected))
}
}
object Tags {

View file

@ -18,6 +18,7 @@ package fr.acinq.eclair.router
import com.google.common.math.Quantiles.percentiles
import fr.acinq.bitcoin.Satoshi
import fr.acinq.eclair.router.Router.PublicChannel
import fr.acinq.eclair.wire.ChannelUpdate
import fr.acinq.eclair.{CltvExpiryDelta, MilliSatoshi}

View file

@ -0,0 +1,207 @@
/*
* Copyright 2020 ACINQ SAS
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package fr.acinq.eclair.router
import akka.actor.{ActorContext, ActorRef, Status}
import akka.event.LoggingAdapter
import fr.acinq.bitcoin.Crypto.PublicKey
import fr.acinq.bitcoin.{ByteVector32, ByteVector64}
import fr.acinq.eclair.payment.PaymentRequest.ExtraHop
import fr.acinq.eclair.router.Graph.GraphStructure.DirectedGraph.graphEdgeToHop
import fr.acinq.eclair.router.Graph.GraphStructure.{DirectedGraph, GraphEdge}
import fr.acinq.eclair.router.Graph.{RichWeight, RoutingHeuristics, WeightRatios}
import fr.acinq.eclair.router.Monitoring.{Metrics, Tags}
import fr.acinq.eclair.router.Router._
import fr.acinq.eclair.wire.ChannelUpdate
import fr.acinq.eclair.{ShortChannelId, _}
import scala.compat.Platform
import scala.concurrent.duration._
import scala.util.{Random, Try}
object RouteCalculation {
def finalizeRoute(d: Data, fr: FinalizeRoute)(implicit ctx: ActorContext, log: LoggingAdapter): Data = {
implicit val sender: ActorRef = ctx.self // necessary to preserve origin when sending messages to other actors
// NB: using a capacity of 0 msat will impact the path-finding algorithm. However here we don't run any path-finding, so it's ok.
val assistedChannels: Map[ShortChannelId, AssistedChannel] = fr.assistedRoutes.flatMap(toAssistedChannels(_, fr.hops.last, 0 msat)).toMap
val extraEdges = assistedChannels.values.map(ac => GraphEdge(ChannelDesc(ac.extraHop.shortChannelId, ac.extraHop.nodeId, ac.nextNodeId), toFakeUpdate(ac.extraHop, ac.htlcMaximum))).toSet
val g = extraEdges.foldLeft(d.graph) { case (g: DirectedGraph, e: GraphEdge) => g.addEdge(e) }
// split into sublists [(a,b),(b,c), ...] then get the edges between each of those pairs
fr.hops.sliding(2).map { case List(v1, v2) => g.getEdgesBetween(v1, v2) }.toList match {
case edges if edges.nonEmpty && edges.forall(_.nonEmpty) =>
val selectedEdges = edges.map(_.maxBy(_.update.htlcMaximumMsat.getOrElse(0 msat))) // select the largest edge
val hops = selectedEdges.map(d => ChannelHop(d.desc.a, d.desc.b, d.update))
ctx.sender ! RouteResponse(hops, Set.empty, Set.empty)
case _ => // some nodes in the supplied route aren't connected in our graph
ctx.sender ! Status.Failure(new IllegalArgumentException("Not all the nodes in the supplied route are connected with public channels"))
}
d
}
def handleRouteRequest(d: Data, routerConf: RouterConf, currentBlockHeight: Long, r: RouteRequest)(implicit ctx: ActorContext, log: LoggingAdapter): Data = {
implicit val sender: ActorRef = ctx.self // necessary to preserve origin when sending messages to other actors
import r._
// we convert extra routing info provided in the payment request to fake channel_update
// it takes precedence over all other channel_updates we know
val assistedChannels: Map[ShortChannelId, AssistedChannel] = assistedRoutes.flatMap(toAssistedChannels(_, target, amount)).toMap
val extraEdges = assistedChannels.values.map(ac => GraphEdge(ChannelDesc(ac.extraHop.shortChannelId, ac.extraHop.nodeId, ac.nextNodeId), toFakeUpdate(ac.extraHop, ac.htlcMaximum))).toSet
val ignoredEdges = ignoreChannels ++ d.excludedChannels
val defaultRouteParams: RouteParams = getDefaultRouteParams(routerConf)
val params = routeParams.getOrElse(defaultRouteParams)
val routesToFind = if (params.randomize) DEFAULT_ROUTES_COUNT else 1
log.info(s"finding a route $source->$target with assistedChannels={} ignoreNodes={} ignoreChannels={} excludedChannels={}", assistedChannels.keys.mkString(","), ignoreNodes.map(_.value).mkString(","), ignoreChannels.mkString(","), d.excludedChannels.mkString(","))
log.info(s"finding a route with randomize={} params={}", routesToFind > 1, params)
findRoute(d.graph, source, target, amount, numRoutes = routesToFind, extraEdges = extraEdges, ignoredEdges = ignoredEdges, ignoredVertices = ignoreNodes, routeParams = params, currentBlockHeight)
.map(r => ctx.sender ! RouteResponse(r, ignoreNodes, ignoreChannels))
.recover { case t => ctx.sender ! Status.Failure(t) }
d
}
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, Platform.currentTime.milliseconds.toSeconds, messageFlags = 1, channelFlags = 0, extraHop.cltvExpiryDelta, htlcMinimumMsat = 0 msat, extraHop.feeBase, extraHop.feeProportionalMillionths, Some(htlcMaximum))
}
def toAssistedChannels(extraRoute: Seq[ExtraHop], targetNodeId: PublicKey, amount: MilliSatoshi): Map[ShortChannelId, AssistedChannel] = {
// BOLT 11: "For each entry, the pubkey is the node ID of the start of the channel", and the last node is the destination
// The invoice doesn't explicitly specify the channel's htlcMaximumMsat, but we can safely assume that the channel
// should be able to route the payment, so we'll compute an htlcMaximumMsat accordingly.
// We could also get the channel capacity from the blockchain (since we have the shortChannelId) but that's more expensive.
// We also need to make sure the channel isn't excluded by our heuristics.
val lastChannelCapacity = amount.max(RoutingHeuristics.CAPACITY_CHANNEL_LOW)
val nextNodeIds = extraRoute.map(_.nodeId).drop(1) :+ targetNodeId
extraRoute.zip(nextNodeIds).reverse.foldLeft((lastChannelCapacity, Map.empty[ShortChannelId, AssistedChannel])) {
case ((amount, acs), (extraHop: ExtraHop, nextNodeId)) =>
val nextAmount = amount + nodeFee(extraHop.feeBase, extraHop.feeProportionalMillionths, amount)
(nextAmount, acs + (extraHop.shortChannelId -> AssistedChannel(extraHop, nextNodeId, nextAmount)))
}._2
}
/**
* This method is used after a payment failed, and we want to exclude some nodes that we know are failing
*/
def getIgnoredChannelDesc(channels: Map[ShortChannelId, PublicChannel], ignoreNodes: Set[PublicKey]): Iterable[ChannelDesc] = {
val desc = if (ignoreNodes.isEmpty) {
Iterable.empty[ChannelDesc]
} else {
// expensive, but node blacklisting shouldn't happen often
channels.values
.filter(channelData => ignoreNodes.contains(channelData.ann.nodeId1) || ignoreNodes.contains(channelData.ann.nodeId2))
.flatMap(channelData => Vector(ChannelDesc(channelData.ann.shortChannelId, channelData.ann.nodeId1, channelData.ann.nodeId2), ChannelDesc(channelData.ann.shortChannelId, channelData.ann.nodeId2, channelData.ann.nodeId1)))
}
desc
}
/**
* https://github.com/lightningnetwork/lightning-rfc/blob/master/04-onion-routing.md#clarifications
*/
val ROUTE_MAX_LENGTH = 20
// Max allowed CLTV for a route
val DEFAULT_ROUTE_MAX_CLTV = CltvExpiryDelta(1008)
// The default number of routes we'll search for when findRoute is called with randomize = true
val DEFAULT_ROUTES_COUNT = 3
def getDefaultRouteParams(routerConf: RouterConf) = RouteParams(
randomize = routerConf.randomizeRouteSelection,
maxFeeBase = routerConf.searchMaxFeeBase.toMilliSatoshi,
maxFeePct = routerConf.searchMaxFeePct,
routeMaxLength = routerConf.searchMaxRouteLength,
routeMaxCltv = routerConf.searchMaxCltv,
ratios = routerConf.searchHeuristicsEnabled match {
case false => None
case true => Some(WeightRatios(
cltvDeltaFactor = routerConf.searchRatioCltv,
ageFactor = routerConf.searchRatioChannelAge,
capacityFactor = routerConf.searchRatioChannelCapacity
))
}
)
/**
* Find a route in the graph between localNodeId and targetNodeId, returns the route.
* Will perform a k-shortest path selection given the @param numRoutes and randomly select one of the result.
*
* @param g graph of the whole network
* @param localNodeId sender node (payer)
* @param targetNodeId target node (final recipient)
* @param amount the amount that will be sent along this route
* @param numRoutes the number of shortest-paths to find
* @param extraEdges a set of extra edges we want to CONSIDER during the search
* @param ignoredEdges a set of extra edges we want to IGNORE during the search
* @param routeParams a set of parameters that can restrict the route search
* @return the computed route to the destination @targetNodeId
*/
def findRoute(g: DirectedGraph,
localNodeId: PublicKey,
targetNodeId: PublicKey,
amount: MilliSatoshi,
numRoutes: Int,
extraEdges: Set[GraphEdge] = Set.empty,
ignoredEdges: Set[ChannelDesc] = Set.empty,
ignoredVertices: Set[PublicKey] = Set.empty,
routeParams: RouteParams,
currentBlockHeight: Long): Try[Seq[ChannelHop]] = Try {
if (localNodeId == targetNodeId) throw CannotRouteToSelf
def feeBaseOk(fee: MilliSatoshi): Boolean = fee <= routeParams.maxFeeBase
def feePctOk(fee: MilliSatoshi, amount: MilliSatoshi): Boolean = {
val maxFee = amount * routeParams.maxFeePct
fee <= maxFee
}
def feeOk(fee: MilliSatoshi, amount: MilliSatoshi): Boolean = feeBaseOk(fee) || feePctOk(fee, amount)
def lengthOk(length: Int): Boolean = length <= routeParams.routeMaxLength && length <= ROUTE_MAX_LENGTH
def cltvOk(cltv: CltvExpiryDelta): Boolean = cltv <= routeParams.routeMaxCltv
val boundaries: RichWeight => Boolean = { weight =>
feeOk(weight.cost - amount, amount) && lengthOk(weight.length) && cltvOk(weight.cltv)
}
val foundRoutes = KamonExt.time(Metrics.FindRouteDuration.withTag(Tags.NumberOfRoutes, numRoutes).withTag(Tags.Amount, Tags.amountBucket(amount))) {
Graph.yenKshortestPaths(g, localNodeId, targetNodeId, amount, ignoredEdges, ignoredVertices, extraEdges, numRoutes, routeParams.ratios, currentBlockHeight, boundaries).toList
}
foundRoutes match {
case Nil if routeParams.routeMaxLength < ROUTE_MAX_LENGTH => // if not found within the constraints we relax and repeat the search
Metrics.RouteLength.withTag(Tags.Amount, Tags.amountBucket(amount)).record(0)
return findRoute(g, localNodeId, targetNodeId, amount, numRoutes, extraEdges, ignoredEdges, ignoredVertices, routeParams.copy(routeMaxLength = ROUTE_MAX_LENGTH, routeMaxCltv = DEFAULT_ROUTE_MAX_CLTV), currentBlockHeight)
case Nil =>
Metrics.RouteLength.withTag(Tags.Amount, Tags.amountBucket(amount)).record(0)
throw RouteNotFound
case foundRoutes =>
val routes = foundRoutes.find(_.path.size == 1) match {
case Some(directRoute) => directRoute :: Nil
case _ => foundRoutes
}
// At this point 'routes' cannot be empty
val randomizedRoutes = if (routeParams.randomize) Random.shuffle(routes) else routes
val route = randomizedRoutes.head.path.map(graphEdgeToHop)
Metrics.RouteLength.withTag(Tags.Amount, Tags.amountBucket(amount)).record(route.length)
route
}
}
}

File diff suppressed because it is too large Load diff

View file

@ -0,0 +1,102 @@
/*
* Copyright 2020 ACINQ SAS
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package fr.acinq.eclair.router
import akka.actor.ActorContext
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.{ChannelAnnouncement, ChannelUpdate}
import fr.acinq.eclair.{ShortChannelId, TxCoordinates}
import scala.collection.mutable
import scala.compat.Platform
import scala.concurrent.duration._
object StaleChannels {
def handlePruneStaleChannels(d: Data, db: NetworkDb, currentBlockHeight: Long)(implicit ctx: ActorContext, log: LoggingAdapter): Data = {
// first we select channels that we will prune
val staleChannels = getStaleChannels(d.channels.values, currentBlockHeight)
val staleChannelIds = staleChannels.map(_.ann.shortChannelId)
// then we remove nodes that aren't tied to any channels anymore (and deduplicate them)
val potentialStaleNodes = staleChannels.flatMap(c => Set(c.ann.nodeId1, c.ann.nodeId2)).toSet
val channels1 = d.channels -- staleChannelIds
// no need to iterate on all nodes, just on those that are affected by current pruning
val staleNodes = potentialStaleNodes.filterNot(nodeId => hasChannels(nodeId, channels1.values))
// let's clean the db and send the events
db.removeChannels(staleChannelIds) // NB: this also removes channel updates
// we keep track of recently pruned channels so we don't revalidate them (zombie churn)
db.addToPruned(staleChannelIds)
staleChannelIds.foreach { shortChannelId =>
log.info("pruning shortChannelId={} (stale)", shortChannelId)
ctx.system.eventStream.publish(ChannelLost(shortChannelId))
}
val staleChannelsToRemove = new mutable.MutableList[ChannelDesc]
staleChannels.foreach(ca => {
staleChannelsToRemove += ChannelDesc(ca.ann.shortChannelId, ca.ann.nodeId1, ca.ann.nodeId2)
staleChannelsToRemove += ChannelDesc(ca.ann.shortChannelId, ca.ann.nodeId2, ca.ann.nodeId1)
})
val graph1 = d.graph.removeEdges(staleChannelsToRemove)
staleNodes.foreach {
nodeId =>
log.info("pruning nodeId={} (stale)", nodeId)
db.removeNode(nodeId)
ctx.system.eventStream.publish(NodeLost(nodeId))
}
d.copy(nodes = d.nodes -- staleNodes, channels = channels1, graph = graph1)
}
def isStale(u: ChannelUpdate): Boolean = isStale(u.timestamp)
def isStale(timestamp: Long): 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 = (Platform.currentTime.milliseconds - 14.days).toSeconds
timestamp < staleThresholdSeconds
}
def isAlmostStale(timestamp: Long): Boolean = {
// we define almost stale as 2 weeks minus 4 days
val staleThresholdSeconds = (Platform.currentTime.milliseconds - 10.days).toSeconds
timestamp < staleThresholdSeconds
}
/**
* Is stale a channel that:
* (1) is older than 2 weeks (2*7*144 = 2016 blocks)
* AND
* (2) has no channel_update younger than 2 weeks
*
* @param update1_opt update corresponding to one side of the channel, if we have it
* @param update2_opt update corresponding to the other side of the channel, if we have it
* @return
*/
def isStale(channel: ChannelAnnouncement, update1_opt: Option[ChannelUpdate], update2_opt: Option[ChannelUpdate], currentBlockHeight: Long): Boolean = {
// BOLT 7: "nodes MAY prune channels should the timestamp of the latest channel_update be older than 2 weeks (1209600 seconds)"
// but we don't want to prune brand new channels for which we didn't yet receive a channel update, so we keep them as long as they are less than 2 weeks (2016 blocks) old
val staleThresholdBlocks = currentBlockHeight - 2016
val TxCoordinates(blockHeight, _, _) = ShortChannelId.coordinates(channel.shortChannelId)
blockHeight < staleThresholdBlocks && update1_opt.forall(isStale) && update2_opt.forall(isStale)
}
def getStaleChannels(channels: Iterable[PublicChannel], currentBlockHeight: Long): Iterable[PublicChannel] = channels.filter(data => isStale(data.ann, data.update_1_opt, data.update_2_opt, currentBlockHeight))
}

View file

@ -0,0 +1,515 @@
/*
* Copyright 2020 ACINQ SAS
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package fr.acinq.eclair.router
import akka.actor.{ActorContext, ActorRef}
import akka.event.LoggingAdapter
import fr.acinq.bitcoin.ByteVector32
import fr.acinq.bitcoin.Crypto.PublicKey
import fr.acinq.eclair.crypto.TransportHandler
import fr.acinq.eclair.router.Router._
import fr.acinq.eclair.wire._
import fr.acinq.eclair.{ShortChannelId, serializationResult}
import kamon.Kamon
import scodec.bits.ByteVector
import shapeless.HNil
import scala.annotation.tailrec
import scala.collection.SortedSet
import scala.collection.immutable.SortedMap
import scala.compat.Platform
import scala.concurrent.duration._
import scala.util.Random
object Sync {
// maximum number of ids we can keep in a single chunk and still have an encoded reply that is smaller than 65Kb
// please note that:
// - this is based on the worst case scenario where peer want timestamps and checksums and the reply is not compressed
// - the maximum number of public channels in a single block so far is less than 300, and the maximum number of tx per block
// almost never exceeds 2800 so this is not a real limitation yet
val MAXIMUM_CHUNK_SIZE = 3200
def handleSendChannelQuery(d: Data, s: SendChannelQuery)(implicit ctx: ActorContext, log: LoggingAdapter): Data = {
implicit val sender: ActorRef = ctx.self // necessary to preserve origin when sending messages to other actors
// ask for everything
// we currently send only one query_channel_range message per peer, when we just (re)connected to it, so we don't
// have to worry about sending a new query_channel_range when another query is still in progress
val query = QueryChannelRange(s.chainHash, firstBlockNum = 0L, numberOfBlocks = Int.MaxValue.toLong, TlvStream(s.flags_opt.toList))
log.info("sending query_channel_range={}", query)
s.to ! query
// we also set a pass-all filter for now (we can update it later) for the future gossip messages, by setting
// 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 = Platform.currentTime.milliseconds.toSeconds, timestampRange = Int.MaxValue)
s.to ! filter
// clean our sync state for this peer: we receive a SendChannelQuery just when we connect/reconnect to a peer and
// will start a new complete sync process
d.copy(sync = d.sync - s.remoteNodeId)
}
def handleQueryChannelRange(channels: SortedMap[ShortChannelId, PublicChannel], routerConf: RouterConf, origin: RemoteGossip, q: QueryChannelRange)(implicit ctx: ActorContext, log: LoggingAdapter): Unit = {
implicit val sender: ActorRef = ctx.self // necessary to preserve origin when sending messages to other actors
ctx.sender ! TransportHandler.ReadAck(q)
Kamon.runWithContextEntry(remoteNodeIdKey, origin.nodeId.toString) {
Kamon.runWithSpan(Kamon.spanBuilder("query-channel-range").start(), finishSpan = true) {
log.info("received query_channel_range with firstBlockNum={} numberOfBlocks={} extendedQueryFlags_opt={}", q.firstBlockNum, q.numberOfBlocks, q.tlvStream)
// keep channel ids that are in [firstBlockNum, firstBlockNum + numberOfBlocks]
val shortChannelIds: SortedSet[ShortChannelId] = channels.keySet.filter(keep(q.firstBlockNum, q.numberOfBlocks, _))
log.info("replying with {} items for range=({}, {})", shortChannelIds.size, q.firstBlockNum, q.numberOfBlocks)
val chunks = Kamon.runWithSpan(Kamon.spanBuilder("split-channel-ids").start(), finishSpan = true) {
split(shortChannelIds, q.firstBlockNum, q.numberOfBlocks, routerConf.channelRangeChunkSize)
}
Kamon.runWithSpan(Kamon.spanBuilder("compute-timestamps-checksums").start(), finishSpan = true) {
chunks.foreach { chunk =>
val reply = buildReplyChannelRange(chunk, q.chainHash, routerConf.encodingType, q.queryFlags_opt, channels)
origin.peerConnection ! reply
}
}
}
}
}
def handleReplyChannelRange(d: Data, routerConf: RouterConf, origin: RemoteGossip, r: ReplyChannelRange)(implicit ctx: ActorContext, log: LoggingAdapter): Data = {
implicit val sender: ActorRef = ctx.self // necessary to preserve origin when sending messages to other actors
ctx.sender ! TransportHandler.ReadAck(r)
Kamon.runWithContextEntry(remoteNodeIdKey, origin.nodeId.toString) {
Kamon.runWithSpan(Kamon.spanBuilder("reply-channel-range").start(), finishSpan = true) {
@tailrec
def loop(ids: List[ShortChannelId], timestamps: List[ReplyChannelRangeTlv.Timestamps], checksums: List[ReplyChannelRangeTlv.Checksums], acc: List[ShortChannelIdAndFlag] = List.empty[ShortChannelIdAndFlag]): List[ShortChannelIdAndFlag] = {
ids match {
case Nil => acc.reverse
case head :: tail =>
val flag = computeFlag(d.channels)(head, timestamps.headOption, checksums.headOption, routerConf.requestNodeAnnouncements)
// 0 means nothing to query, just don't include it
val acc1 = if (flag != 0) ShortChannelIdAndFlag(head, flag) :: acc else acc
loop(tail, timestamps.drop(1), checksums.drop(1), acc1)
}
}
val timestamps_opt = r.timestamps_opt.map(_.timestamps).getOrElse(List.empty[ReplyChannelRangeTlv.Timestamps])
val checksums_opt = r.checksums_opt.map(_.checksums).getOrElse(List.empty[ReplyChannelRangeTlv.Checksums])
val shortChannelIdAndFlags = Kamon.runWithSpan(Kamon.spanBuilder("compute-flags").start(), finishSpan = true) {
loop(r.shortChannelIds.array, timestamps_opt, checksums_opt)
}
val (channelCount, updatesCount) = shortChannelIdAndFlags.foldLeft((0, 0)) {
case ((c, u), ShortChannelIdAndFlag(_, flag)) =>
val c1 = c + (if (QueryShortChannelIdsTlv.QueryFlagType.includeChannelAnnouncement(flag)) 1 else 0)
val u1 = u + (if (QueryShortChannelIdsTlv.QueryFlagType.includeUpdate1(flag)) 1 else 0) + (if (QueryShortChannelIdsTlv.QueryFlagType.includeUpdate2(flag)) 1 else 0)
(c1, u1)
}
log.info(s"received reply_channel_range with {} channels, we're missing {} channel announcements and {} updates, format={}", r.shortChannelIds.array.size, channelCount, updatesCount, r.shortChannelIds.encoding)
def buildQuery(chunk: List[ShortChannelIdAndFlag]): QueryShortChannelIds = {
// always encode empty lists as UNCOMPRESSED
val encoding = if (chunk.isEmpty) EncodingType.UNCOMPRESSED else r.shortChannelIds.encoding
QueryShortChannelIds(r.chainHash,
shortChannelIds = EncodedShortChannelIds(encoding, chunk.map(_.shortChannelId)),
if (r.timestamps_opt.isDefined || r.checksums_opt.isDefined)
TlvStream(QueryShortChannelIdsTlv.EncodedQueryFlags(encoding, chunk.map(_.flag)))
else
TlvStream.empty
)
}
// we update our sync data to this node (there may be multiple channel range responses and we can only query one set of ids at a time)
val replies = shortChannelIdAndFlags
.grouped(routerConf.channelQueryChunkSize)
.map(buildQuery)
.toList
val (sync1, replynow_opt) = addToSync(d.sync, origin.nodeId, replies)
// we only send a reply right away if there were no pending requests
replynow_opt.foreach(origin.peerConnection ! _)
val progress = syncProgress(sync1)
ctx.system.eventStream.publish(progress)
ctx.self ! progress
d.copy(sync = sync1)
}
}
}
def handleQueryShortChannelIds(nodes: Map[PublicKey, NodeAnnouncement], channels: SortedMap[ShortChannelId, PublicChannel], routerConf: RouterConf, origin: RemoteGossip, q: QueryShortChannelIds)(implicit ctx: ActorContext, log: LoggingAdapter): Unit = {
implicit val sender: ActorRef = ctx.self // necessary to preserve origin when sending messages to other actors
ctx.sender ! TransportHandler.ReadAck(q)
Kamon.runWithContextEntry(remoteNodeIdKey, origin.nodeId.toString) {
Kamon.runWithSpan(Kamon.spanBuilder("query-short-channel-ids").start(), finishSpan = true) {
val flags = q.queryFlags_opt.map(_.array).getOrElse(List.empty[Long])
var channelCount = 0
var updateCount = 0
var nodeCount = 0
processChannelQuery(nodes, channels)(
q.shortChannelIds.array,
flags,
ca => {
channelCount = channelCount + 1
origin.peerConnection ! ca
},
cu => {
updateCount = updateCount + 1
origin.peerConnection ! cu
},
na => {
nodeCount = nodeCount + 1
origin.peerConnection ! na
}
)
log.info("received query_short_channel_ids with {} items, sent back {} channels and {} updates and {} nodes", q.shortChannelIds.array.size, channelCount, updateCount, nodeCount)
origin.peerConnection ! ReplyShortChannelIdsEnd(q.chainHash, 1)
}
}
}
def handleReplyShortChannelIdsEnd(d: Data, origin: RemoteGossip, r: ReplyShortChannelIdsEnd)(implicit ctx: ActorContext, log: LoggingAdapter): Data = {
implicit val sender: ActorRef = ctx.self // necessary to preserve origin when sending messages to other actors
ctx.sender ! TransportHandler.ReadAck(r)
// have we more channels to ask this peer?
val sync1 = d.sync.get(origin.nodeId) match {
case Some(sync) =>
sync.pending match {
case nextRequest +: rest =>
log.info(s"asking for the next slice of short_channel_ids (remaining=${sync.pending.size}/${sync.total})")
origin.peerConnection ! nextRequest
d.sync + (origin.nodeId -> sync.copy(pending = rest))
case Nil =>
// we received reply_short_channel_ids_end for our last query and have not sent another one, we can now remove
// the remote peer from our map
log.info(s"sync complete (total=${sync.total})")
d.sync - origin.nodeId
}
case _ => d.sync
}
val progress = syncProgress(sync1)
ctx.system.eventStream.publish(progress)
ctx.self ! progress
d.copy(sync = sync1)
}
/**
* Filters channels that we want to send to nodes asking for a channel range
*/
def keep(firstBlockNum: Long, numberOfBlocks: Long, id: ShortChannelId): Boolean = {
val height = id.blockHeight
height >= firstBlockNum && height < (firstBlockNum + numberOfBlocks)
}
def shouldRequestUpdate(ourTimestamp: Long, ourChecksum: Long, theirTimestamp_opt: Option[Long], 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:
// - it is more recent than ours
// - it is different from ours, or it is the same but ours is about to be stale
// - it is not stale
val theirsIsMoreRecent = ourTimestamp < theirTimestamp
val areDifferent = ourChecksum != theirChecksum
val oursIsAlmostStale = StaleChannels.isAlmostStale(ourTimestamp)
val theirsIsStale = StaleChannels.isStale(theirTimestamp)
theirsIsMoreRecent && (areDifferent || oursIsAlmostStale) && !theirsIsStale
case (Some(theirTimestamp), None) =>
// if we only have their timestamp, we request their channel_update if theirs is more recent than ours
val theirsIsMoreRecent = ourTimestamp < theirTimestamp
val theirsIsStale = StaleChannels.isStale(theirTimestamp)
theirsIsMoreRecent && !theirsIsStale
case (None, Some(theirChecksum)) =>
// if we only have their checksum, we request their channel_update if it is different from ours
// NB: a zero checksum means that they don't have the data
val areDifferent = theirChecksum != 0 && ourChecksum != theirChecksum
areDifferent
case (None, None) =>
// if we have neither their timestamp nor their checksum we request their channel_update
true
}
}
def computeFlag(channels: SortedMap[ShortChannelId, PublicChannel])(
shortChannelId: ShortChannelId,
theirTimestamps_opt: Option[ReplyChannelRangeTlv.Timestamps],
theirChecksums_opt: Option[ReplyChannelRangeTlv.Checksums],
includeNodeAnnouncements: Boolean): Long = {
import QueryShortChannelIdsTlv.QueryFlagType._
val flagsNodes = if (includeNodeAnnouncements) INCLUDE_NODE_ANNOUNCEMENT_1 | INCLUDE_NODE_ANNOUNCEMENT_2 else 0
val flags = if (!channels.contains(shortChannelId)) {
INCLUDE_CHANNEL_ANNOUNCEMENT | INCLUDE_CHANNEL_UPDATE_1 | INCLUDE_CHANNEL_UPDATE_2
} else {
// we already know this channel
val (ourTimestamps, ourChecksums) = getChannelDigestInfo(channels)(shortChannelId)
// if they don't provide timestamps or checksums, we set appropriate default values:
// - we assume their timestamp is more recent than ours by setting timestamp = Long.MaxValue
// - we assume their update is different from ours by setting checkum = Long.MaxValue (NB: our default value for checksum is 0)
val shouldRequestUpdate1 = shouldRequestUpdate(ourTimestamps.timestamp1, ourChecksums.checksum1, theirTimestamps_opt.map(_.timestamp1), theirChecksums_opt.map(_.checksum1))
val shouldRequestUpdate2 = shouldRequestUpdate(ourTimestamps.timestamp2, ourChecksums.checksum2, theirTimestamps_opt.map(_.timestamp2), theirChecksums_opt.map(_.checksum2))
val flagUpdate1 = if (shouldRequestUpdate1) INCLUDE_CHANNEL_UPDATE_1 else 0
val flagUpdate2 = if (shouldRequestUpdate2) INCLUDE_CHANNEL_UPDATE_2 else 0
flagUpdate1 | flagUpdate2
}
if (flags == 0) 0 else flags | flagsNodes
}
/**
* Handle a query message, which includes a list of channel ids and flags.
*
* @param nodes node id -> node announcement
* @param channels channel id -> channel announcement + updates
* @param ids list of channel ids
* @param flags list of query flags, either empty one flag per channel id
* @param onChannel called when a channel announcement matches (i.e. its bit is set in the query flag and we have it)
* @param onUpdate called when a channel update matches
* @param onNode called when a node announcement matches
*
*/
def processChannelQuery(nodes: Map[PublicKey, NodeAnnouncement],
channels: SortedMap[ShortChannelId, PublicChannel])(
ids: List[ShortChannelId],
flags: List[Long],
onChannel: ChannelAnnouncement => Unit,
onUpdate: ChannelUpdate => Unit,
onNode: NodeAnnouncement => Unit)(implicit log: LoggingAdapter): Unit = {
import QueryShortChannelIdsTlv.QueryFlagType
// we loop over channel ids and query flag. We track node Ids for node announcement
// we've already sent to avoid sending them multiple times, as requested by the BOLTs
@tailrec
def loop(ids: List[ShortChannelId], flags: List[Long], numca: Int = 0, numcu: Int = 0, nodesSent: Set[PublicKey] = Set.empty[PublicKey]): (Int, Int, Int) = ids match {
case Nil => (numca, numcu, nodesSent.size)
case head :: tail if !channels.contains(head) =>
log.warning("received query for shortChannelId={} that we don't have", head)
loop(tail, flags.drop(1), numca, numcu, nodesSent)
case head :: tail =>
val numca1 = numca
val numcu1 = numcu
var sent1 = nodesSent
val pc = channels(head)
val flag_opt = flags.headOption
// no flag means send everything
val includeChannel = flag_opt.forall(QueryFlagType.includeChannelAnnouncement)
val includeUpdate1 = flag_opt.forall(QueryFlagType.includeUpdate1)
val includeUpdate2 = flag_opt.forall(QueryFlagType.includeUpdate2)
val includeNode1 = flag_opt.forall(QueryFlagType.includeNodeAnnouncement1)
val includeNode2 = flag_opt.forall(QueryFlagType.includeNodeAnnouncement2)
if (includeChannel) {
onChannel(pc.ann)
}
if (includeUpdate1) {
pc.update_1_opt.foreach { u =>
onUpdate(u)
}
}
if (includeUpdate2) {
pc.update_2_opt.foreach { u =>
onUpdate(u)
}
}
if (includeNode1 && !sent1.contains(pc.ann.nodeId1)) {
nodes.get(pc.ann.nodeId1).foreach { n =>
onNode(n)
sent1 = sent1 + pc.ann.nodeId1
}
}
if (includeNode2 && !sent1.contains(pc.ann.nodeId2)) {
nodes.get(pc.ann.nodeId2).foreach { n =>
onNode(n)
sent1 = sent1 + pc.ann.nodeId2
}
}
loop(tail, flags.drop(1), numca1, numcu1, sent1)
}
loop(ids, flags)
}
/**
* Returns overall progress on synchronization
*
* @return a sync progress indicator (1 means fully synced)
*/
def syncProgress(sync: Map[PublicKey, Syncing]): SyncProgress = {
// NB: progress is in terms of requests, not individual channels
val (pending, total) = sync.foldLeft((0, 0)) {
case ((p, t), (_, sync)) => (p + sync.pending.size, t + sync.total)
}
if (total == 0) {
SyncProgress(1)
} else {
SyncProgress((total - pending) / (1.0 * total))
}
}
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 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))
}
def crc32c(data: ByteVector): Long = {
import com.google.common.hash.Hashing
Hashing.crc32c().hashBytes(data.toArray).asInt() & 0xFFFFFFFFL
}
def getChecksum(u: ChannelUpdate): Long = {
import u._
val data = serializationResult(LightningMessageCodecs.channelUpdateChecksumCodec.encode(chainHash :: shortChannelId :: messageFlags :: channelFlags :: cltvExpiryDelta :: htlcMinimumMsat :: feeBaseMsat :: feeProportionalMillionths :: htlcMaximumMsat :: HNil))
crc32c(data)
}
case class ShortChannelIdsChunk(firstBlock: Long, numBlocks: Long, shortChannelIds: List[ShortChannelId]) {
/**
*
* @param maximumSize maximum size of the short channel ids list
* @return a chunk with at most `maximumSize` ids
*/
def enforceMaximumSize(maximumSize: Int) = {
if (shortChannelIds.size <= maximumSize) this else {
// we use a random offset here, so even if shortChannelIds.size is much bigger than maximumSize (which should
// not happen) peers will eventually receive info about all channels in this chunk
val offset = Random.nextInt(shortChannelIds.size - maximumSize + 1)
this.copy(shortChannelIds = this.shortChannelIds.slice(offset, offset + maximumSize))
}
}
}
/**
* Split short channel ids into chunks, because otherwise message could be too big
* there could be several reply_channel_range messages for a single query, but we make sure that the returned
* chunks fully covers the [firstBlockNum, numberOfBlocks] range that was requested
*
* @param shortChannelIds list of short channel ids to split
* @param firstBlockNum first block height requested by our peers
* @param numberOfBlocks number of blocks requested by our peer
* @param channelRangeChunkSize target chunk size. All ids that have the same block height will be grouped together, so
* returned chunks may still contain more than `channelRangeChunkSize` elements
* @return a list of short channel id chunks
*/
def split(shortChannelIds: SortedSet[ShortChannelId], firstBlockNum: Long, numberOfBlocks: Long, channelRangeChunkSize: Int): List[ShortChannelIdsChunk] = {
// see BOLT7: MUST encode a short_channel_id for every open channel it knows in blocks first_blocknum to first_blocknum plus number_of_blocks minus one
val it = shortChannelIds.iterator.dropWhile(_.blockHeight < firstBlockNum).takeWhile(_.blockHeight < firstBlockNum + numberOfBlocks)
if (it.isEmpty) {
List(ShortChannelIdsChunk(firstBlockNum, numberOfBlocks, List.empty))
} else {
// we want to split ids in different chunks, with the following rules by order of priority
// ids that have the same block height must be grouped in the same chunk
// chunk should contain `channelRangeChunkSize` ids
@tailrec
def loop(currentChunk: List[ShortChannelId], acc: List[ShortChannelIdsChunk]): List[ShortChannelIdsChunk] = {
if (it.hasNext) {
val id = it.next()
val currentHeight = currentChunk.head.blockHeight
if (id.blockHeight == currentHeight)
loop(id :: currentChunk, acc) // same height => always add to the current chunk
else if (currentChunk.size < channelRangeChunkSize) // different height but we're under the size target => add to the current chunk
loop(id :: currentChunk, acc) // different height and over the size target => start a new chunk
else {
// we always prepend because it's more efficient so we have to reverse the current chunk
// for the first chunk, we make sure that we start at the request first block
// for the next chunks we start at the end of the range covered by the last chunk
val first = if (acc.isEmpty) firstBlockNum else acc.head.firstBlock + acc.head.numBlocks
val count = currentChunk.head.blockHeight - first + 1
loop(id :: Nil, ShortChannelIdsChunk(first, count, currentChunk.reverse) :: acc)
}
}
else {
// for the last chunk, we make sure that we cover the requested block range
val first = if (acc.isEmpty) firstBlockNum else acc.head.firstBlock + acc.head.numBlocks
val count = numberOfBlocks - first + firstBlockNum
(ShortChannelIdsChunk(first, count, currentChunk.reverse) :: acc).reverse
}
}
val first = it.next()
val chunks = loop(first :: Nil, Nil)
// make sure that all our chunks match our max size policy
enforceMaximumSize(chunks)
}
}
/**
* Enforce max-size constraints for each chunk
*
* @param chunks list of short channel id chunks
* @return a processed list of chunks
*/
def enforceMaximumSize(chunks: List[ShortChannelIdsChunk]): List[ShortChannelIdsChunk] = chunks.map(_.enforceMaximumSize(MAXIMUM_CHUNK_SIZE))
/**
* Build a `reply_channel_range` message
*
* @param chunk chunk of scids
* @param chainHash chain hash
* @param defaultEncoding default encoding
* @param queryFlags_opt query flag set by the requester
* @param channels channels map
* @return a ReplyChannelRange object
*/
def buildReplyChannelRange(chunk: ShortChannelIdsChunk, chainHash: ByteVector32, defaultEncoding: EncodingType, queryFlags_opt: Option[QueryChannelRangeTlv.QueryFlags], channels: SortedMap[ShortChannelId, PublicChannel]): ReplyChannelRange = {
val encoding = if (chunk.shortChannelIds.isEmpty) EncodingType.UNCOMPRESSED else defaultEncoding
val (timestamps, checksums) = queryFlags_opt match {
case Some(extension) if extension.wantChecksums | extension.wantTimestamps =>
// we always compute timestamps and checksums even if we don't need both, overhead is negligible
val (timestamps, checksums) = chunk.shortChannelIds.map(getChannelDigestInfo(channels)).unzip
val encodedTimestamps = if (extension.wantTimestamps) Some(ReplyChannelRangeTlv.EncodedTimestamps(encoding, timestamps)) else None
val encodedChecksums = if (extension.wantChecksums) Some(ReplyChannelRangeTlv.EncodedChecksums(checksums)) else None
(encodedTimestamps, encodedChecksums)
case _ => (None, None)
}
ReplyChannelRange(chainHash, chunk.firstBlock, chunk.numBlocks,
complete = 1,
shortChannelIds = EncodedShortChannelIds(encoding, chunk.shortChannelIds),
timestamps = timestamps,
checksums = checksums)
}
def addToSync(syncMap: Map[PublicKey, Syncing], remoteNodeId: PublicKey, pending: List[RoutingMessage]): (Map[PublicKey, Syncing], Option[RoutingMessage]) = {
pending match {
case head +: rest =>
// they may send back several reply_channel_range messages for a single query_channel_range query, and we must not
// send another query_short_channel_ids query if they're still processing one
syncMap.get(remoteNodeId) match {
case None =>
// we don't have a pending query with this peer, let's send it
(syncMap + (remoteNodeId -> Syncing(rest, pending.size)), Some(head))
case Some(sync) =>
// we already have a pending query with this peer, add missing ids to our "sync" state
(syncMap + (remoteNodeId -> Syncing(sync.pending ++ pending, sync.total + pending.size)), None)
}
case Nil =>
// there is nothing to send
(syncMap, None)
}
}
}

View file

@ -0,0 +1,450 @@
/*
* Copyright 2020 ACINQ SAS
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package fr.acinq.eclair.router
import akka.actor.{ActorContext, ActorRef}
import akka.event.{DiagnosticLoggingAdapter, LoggingAdapter}
import fr.acinq.bitcoin.Crypto.PublicKey
import fr.acinq.bitcoin.Script.{pay2wsh, write}
import fr.acinq.eclair.blockchain.{UtxoStatus, ValidateRequest, ValidateResult, WatchSpentBasic}
import fr.acinq.eclair.channel.{BITCOIN_FUNDING_EXTERNAL_CHANNEL_SPENT, LocalChannelDown, LocalChannelUpdate}
import fr.acinq.eclair.crypto.TransportHandler
import fr.acinq.eclair.db.NetworkDb
import fr.acinq.eclair.router.Monitoring.Metrics
import fr.acinq.eclair.router.Router._
import fr.acinq.eclair.transactions.Scripts
import fr.acinq.eclair.wire._
import fr.acinq.eclair.{Logs, NodeParams, ShortChannelId, TxCoordinates}
import kamon.Kamon
object Validation {
def sendDecision(peerConnection: ActorRef, decision: GossipDecision)(implicit sender: ActorRef): Unit = {
peerConnection ! decision
Metrics.gossipResult(decision).increment()
}
def handleChannelAnnouncement(d: Data, db: NetworkDb, watcher: ActorRef, origin: RemoteGossip, c: ChannelAnnouncement)(implicit ctx: ActorContext, log: LoggingAdapter): Data = {
implicit val sender: ActorRef = ctx.self // necessary to preserve origin when sending messages to other actors
log.debug("received channel announcement for shortChannelId={} nodeId1={} nodeId2={}", c.shortChannelId, c.nodeId1, c.nodeId2)
if (d.channels.contains(c.shortChannelId)) {
origin.peerConnection ! TransportHandler.ReadAck(c)
log.debug("ignoring {} (duplicate)", c)
sendDecision(origin.peerConnection, GossipDecision.Duplicate(c))
d
} else if (d.awaiting.contains(c)) {
origin.peerConnection ! TransportHandler.ReadAck(c)
log.debug("ignoring {} (being verified)", c)
// adding the sender to the list of origins so that we don't send back the same announcement to this peer later
val origins = d.awaiting(c) :+ origin
d.copy(awaiting = d.awaiting + (c -> origins))
} else if (db.isPruned(c.shortChannelId)) {
origin.peerConnection ! TransportHandler.ReadAck(c)
// channel was pruned and we haven't received a recent channel_update, so we have no reason to revalidate it
log.debug("ignoring {} (was pruned)", c)
sendDecision(origin.peerConnection, GossipDecision.ChannelPruned(c))
d
} else if (!Announcements.checkSigs(c)) {
origin.peerConnection ! TransportHandler.ReadAck(c)
log.warning("bad signature for announcement {}", c)
sendDecision(origin.peerConnection, GossipDecision.InvalidSignature(c))
d
} else {
log.info("validating shortChannelId={}", c.shortChannelId)
Kamon.runWithContextEntry(shortChannelIdKey, c.shortChannelId) {
Kamon.runWithSpan(Kamon.spanBuilder("validate-channel").tag("shortChannelId", c.shortChannelId.toString).start(), finishSpan = false) {
watcher ! ValidateRequest(c)
}
}
// we don't acknowledge the message just yet
d.copy(awaiting = d.awaiting + (c -> Seq(origin)))
}
}
def handleChannelValidationResponse(d0: Data, nodeParams: NodeParams, watcher: ActorRef, r: ValidateResult)(implicit ctx: ActorContext, log: DiagnosticLoggingAdapter): Data = {
implicit val sender: ActorRef = ctx.self // necessary to preserve origin when sending messages to other actors
import nodeParams.db.{network => db}
import r.c
Kamon.runWithContextEntry(shortChannelIdKey, c.shortChannelId) {
Kamon.runWithSpan(Kamon.currentSpan(), finishSpan = true) {
Kamon.runWithSpan(Kamon.spanBuilder("process-validate-result").start(), finishSpan = true) {
d0.awaiting.get(c) match {
case Some(origin +: _) => origin.peerConnection ! TransportHandler.ReadAck(c) // now we can acknowledge the message, we only need to do it for the first peer that sent us the announcement
case _ => ()
}
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)
val publicChannel_opt = r match {
case ValidateResult(c, Left(t)) =>
log.warning("validation failure for shortChannelId={} reason={}", c.shortChannelId, t.getMessage)
remoteOrigins_opt.foreach(_.foreach(o => sendDecision(o.peerConnection, GossipDecision.ValidationFailure(c))))
None
case ValidateResult(c, Right((tx, UtxoStatus.Unspent))) =>
val TxCoordinates(_, _, outputIndex) = ShortChannelId.coordinates(c.shortChannelId)
val (fundingOutputScript, ok) = Kamon.runWithSpan(Kamon.spanBuilder("checked-pubkeyscript").start(), finishSpan = true) {
// let's check that the output is indeed a P2WSH multisig 2-of-2 of nodeid1 and nodeid2)
val fundingOutputScript = write(pay2wsh(Scripts.multiSig2of2(c.bitcoinKey1, c.bitcoinKey2)))
val ok = tx.txOut.size < outputIndex + 1 || fundingOutputScript != tx.txOut(outputIndex).publicKeyScript
(fundingOutputScript, ok)
}
if (ok) {
log.error(s"invalid script for shortChannelId={}: txid={} does not have script=$fundingOutputScript at outputIndex=$outputIndex ann={}", c.shortChannelId, tx.txid, c)
remoteOrigins_opt.foreach(_.foreach(o => sendDecision(o.peerConnection, GossipDecision.InvalidAnnouncement(c))))
None
} else {
watcher ! WatchSpentBasic(ctx.self, tx, outputIndex, BITCOIN_FUNDING_EXTERNAL_CHANNEL_SPENT(c.shortChannelId))
// TODO: check feature bit set
log.debug("added channel channelId={}", c.shortChannelId)
remoteOrigins_opt.foreach(_.foreach(o => sendDecision(o.peerConnection, GossipDecision.Accepted(c))))
val capacity = tx.txOut(outputIndex).amount
ctx.system.eventStream.publish(ChannelsDiscovered(SingleChannelDiscovered(c, capacity, None, None) :: Nil))
Kamon.runWithSpan(Kamon.spanBuilder("add-to-db").start(), finishSpan = true) {
db.addChannel(c, tx.txid, capacity)
}
// in case we just validated our first local channel, we announce the local node
if (!d0.nodes.contains(nodeParams.nodeId) && isRelatedTo(c, nodeParams.nodeId)) {
log.info("first local channel validated, announcing local node")
val nodeAnn = Announcements.makeNodeAnnouncement(nodeParams.privateKey, nodeParams.alias, nodeParams.color, nodeParams.publicAddresses, nodeParams.features)
ctx.self ! nodeAnn
}
Some(PublicChannel(c, tx.txid, capacity, None, None))
}
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)
// 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 {
log.debug("ignoring shortChannelId={} tx={} (funding tx already spent but spending tx isn't confirmed)", c.shortChannelId, tx.txid)
remoteOrigins_opt.foreach(_.foreach(o => sendDecision(o.peerConnection, GossipDecision.ChannelClosing(c))))
}
// there may be a record if we have just restarted
db.removeChannel(c.shortChannelId)
None
}
val span1 = Kamon.spanBuilder("reprocess-stash").start
// we also reprocess node and channel_update announcements related to channels that were just analyzed
val reprocessUpdates = d0.stash.updates.filterKeys(u => u.shortChannelId == c.shortChannelId)
val reprocessNodes = d0.stash.nodes.filterKeys(n => isRelatedTo(c, n.nodeId))
// and we remove the reprocessed messages from the stash
val stash1 = d0.stash.copy(updates = d0.stash.updates -- reprocessUpdates.keys, nodes = d0.stash.nodes -- reprocessNodes.keys)
// we remove channel from awaiting map
val awaiting1 = d0.awaiting - c
span1.finish()
publicChannel_opt match {
case Some(pc) =>
Kamon.runWithSpan(Kamon.spanBuilder("build-new-state").start, finishSpan = true) {
// note: if the channel is graduating from private to public, the implementation (in the LocalChannelUpdate handler) guarantees that we will process a new channel_update
// right after the channel_announcement, channel_updates will be moved from private to public at that time
val d1 = d0.copy(
channels = d0.channels + (c.shortChannelId -> pc),
privateChannels = d0.privateChannels - c.shortChannelId, // we remove fake announcements that we may have made before
rebroadcast = d0.rebroadcast.copy(channels = d0.rebroadcast.channels + (c -> d0.awaiting.getOrElse(c, Nil).toSet)), // we also add the newly validated channels to the rebroadcast queue
stash = stash1,
awaiting = awaiting1)
// we only reprocess updates and nodes if validation succeeded
val d2 = reprocessUpdates.foldLeft(d1) {
case (d, (u, origins)) => Validation.handleChannelUpdate(d, nodeParams.db.network, nodeParams.routerConf, origins, u, wasStashed = true)
}
val d3 = reprocessNodes.foldLeft(d2) {
case (d, (n, origins)) => Validation.handleNodeAnnouncement(d, nodeParams.db.network, origins, n, wasStashed = true)
}
d3
}
case None =>
reprocessUpdates.foreach { case (u, origins) => origins.collect { case o: RemoteGossip => sendDecision(o.peerConnection, GossipDecision.NoRelatedChannel(u)) } }
reprocessNodes.foreach { case (n, origins) => origins.collect { case o: RemoteGossip => sendDecision(o.peerConnection, GossipDecision.NoKnownChannel(n)) } }
d0.copy(stash = stash1, awaiting = awaiting1)
}
}
}
}
}
}
def handleChannelSpent(d: Data, db: NetworkDb, event: BITCOIN_FUNDING_EXTERNAL_CHANNEL_SPENT)(implicit ctx: ActorContext, log: LoggingAdapter): Data = {
implicit val sender: ActorRef = ctx.self // necessary to preserve origin when sending messages to other actors
import event.shortChannelId
val lostChannel = d.channels(shortChannelId).ann
log.info("funding tx of channelId={} has been spent", shortChannelId)
// we need to remove nodes that aren't tied to any channels anymore
val channels1 = d.channels - lostChannel.shortChannelId
val lostNodes = Seq(lostChannel.nodeId1, lostChannel.nodeId2).filterNot(nodeId => hasChannels(nodeId, channels1.values))
// let's clean the db and send the events
log.info("pruning shortChannelId={} (spent)", shortChannelId)
db.removeChannel(shortChannelId) // NB: this also removes channel updates
// we also need to remove updates from the graph
val graph1 = d.graph
.removeEdge(ChannelDesc(lostChannel.shortChannelId, lostChannel.nodeId1, lostChannel.nodeId2))
.removeEdge(ChannelDesc(lostChannel.shortChannelId, lostChannel.nodeId2, lostChannel.nodeId1))
ctx.system.eventStream.publish(ChannelLost(shortChannelId))
lostNodes.foreach {
nodeId =>
log.info("pruning nodeId={} (spent)", nodeId)
db.removeNode(nodeId)
ctx.system.eventStream.publish(NodeLost(nodeId))
}
d.copy(nodes = d.nodes -- lostNodes, channels = d.channels - shortChannelId, graph = graph1)
}
def handleNodeAnnouncement(d: Data, db: NetworkDb, origins: Set[GossipOrigin], n: NodeAnnouncement, wasStashed: Boolean = false)(implicit ctx: ActorContext, log: LoggingAdapter): Data = {
implicit val sender: ActorRef = ctx.self // necessary to preserve origin when sending messages to other actors
val remoteOrigins = origins flatMap {
case r: RemoteGossip if wasStashed =>
Some(r.peerConnection)
case RemoteGossip(peerConnection, _) =>
peerConnection ! TransportHandler.ReadAck(n)
log.debug("received node announcement for nodeId={}", n.nodeId)
Some(peerConnection)
case LocalGossip =>
log.debug("received node announcement from {}", ctx.sender)
None
}
if (d.stash.nodes.contains(n)) {
log.debug("ignoring {} (already stashed)", n)
val origins1 = d.stash.nodes(n) ++ origins
d.copy(stash = d.stash.copy(nodes = d.stash.nodes + (n -> origins1)))
} else if (d.rebroadcast.nodes.contains(n)) {
log.debug("ignoring {} (pending rebroadcast)", n)
remoteOrigins.foreach(sendDecision(_, GossipDecision.Accepted(n)))
val origins1 = d.rebroadcast.nodes(n) ++ origins
d.copy(rebroadcast = d.rebroadcast.copy(nodes = d.rebroadcast.nodes + (n -> origins1)))
} else if (d.nodes.contains(n.nodeId) && d.nodes(n.nodeId).timestamp >= n.timestamp) {
log.debug("ignoring {} (duplicate)", n)
remoteOrigins.foreach(sendDecision(_, GossipDecision.Duplicate(n)))
d
} else if (!Announcements.checkSig(n)) {
log.warning("bad signature for {}", n)
remoteOrigins.foreach(sendDecision(_, GossipDecision.InvalidSignature(n)))
d
} else if (d.nodes.contains(n.nodeId)) {
log.debug("updated node nodeId={}", n.nodeId)
remoteOrigins.foreach(sendDecision(_, GossipDecision.Accepted(n)))
ctx.system.eventStream.publish(NodeUpdated(n))
db.updateNode(n)
d.copy(nodes = d.nodes + (n.nodeId -> n), rebroadcast = d.rebroadcast.copy(nodes = d.rebroadcast.nodes + (n -> origins)))
} else if (d.channels.values.exists(c => isRelatedTo(c.ann, n.nodeId))) {
log.debug("added node nodeId={}", n.nodeId)
remoteOrigins.foreach(sendDecision(_, GossipDecision.Accepted(n)))
ctx.system.eventStream.publish(NodesDiscovered(n :: Nil))
db.addNode(n)
d.copy(nodes = d.nodes + (n.nodeId -> n), rebroadcast = d.rebroadcast.copy(nodes = d.rebroadcast.nodes + (n -> origins)))
} else if (d.awaiting.keys.exists(c => isRelatedTo(c, n.nodeId))) {
log.debug("stashing {}", n)
d.copy(stash = d.stash.copy(nodes = d.stash.nodes + (n -> origins)))
} else {
log.debug("ignoring {} (no related channel found)", n)
remoteOrigins.foreach(sendDecision(_, GossipDecision.NoKnownChannel(n)))
// there may be a record if we have just restarted
db.removeNode(n.nodeId)
d
}
}
def handleChannelUpdate(d: Data, db: NetworkDb, routerConf: RouterConf, origins: Set[GossipOrigin], u: ChannelUpdate, wasStashed: Boolean = false)(implicit ctx: ActorContext, log: LoggingAdapter): Data = {
implicit val sender: ActorRef = ctx.self // necessary to preserve origin when sending messages to other actors
val remoteOrigins = origins flatMap {
case r: RemoteGossip if wasStashed =>
Some(r.peerConnection)
case RemoteGossip(peerConnection, _) =>
peerConnection ! TransportHandler.ReadAck(u)
log.debug("received channel update for shortChannelId={}", u.shortChannelId)
Some(peerConnection)
case LocalGossip =>
log.debug("received channel update from {}", ctx.sender)
None
}
if (d.channels.contains(u.shortChannelId)) {
// related channel is already known (note: this means no related channel_update is in the stash)
val publicChannel = true
val pc = d.channels(u.shortChannelId)
val desc = getDesc(u, pc.ann)
if (d.rebroadcast.updates.contains(u)) {
log.debug("ignoring {} (pending rebroadcast)", u)
remoteOrigins.foreach(sendDecision(_, GossipDecision.Accepted(u)))
val origins1 = d.rebroadcast.updates(u) ++ origins
d.copy(rebroadcast = d.rebroadcast.copy(updates = d.rebroadcast.updates + (u -> origins1)))
} else if (StaleChannels.isStale(u)) {
log.debug("ignoring {} (stale)", u)
remoteOrigins.foreach(sendDecision(_, GossipDecision.Stale(u)))
d
} else if (pc.getChannelUpdateSameSideAs(u).exists(_.timestamp >= u.timestamp)) {
log.debug("ignoring {} (duplicate)", u)
remoteOrigins.foreach(sendDecision(_, GossipDecision.Duplicate(u)))
d
} else if (!Announcements.checkSig(u, pc.getNodeIdSameSideAs(u))) {
log.warning("bad signature for announcement shortChannelId={} {}", u.shortChannelId, u)
remoteOrigins.foreach(sendDecision(_, GossipDecision.InvalidSignature(u)))
d
} else if (pc.getChannelUpdateSameSideAs(u).isDefined) {
log.debug("updated channel_update for shortChannelId={} public={} flags={} {}", u.shortChannelId, publicChannel, u.channelFlags, u)
remoteOrigins.foreach(sendDecision(_, GossipDecision.Accepted(u)))
ctx.system.eventStream.publish(ChannelUpdatesReceived(u :: Nil))
db.updateChannel(u)
// update the graph
val graph1 = if (Announcements.isEnabled(u.channelFlags)) {
d.graph.removeEdge(desc).addEdge(desc, u)
} else {
d.graph.removeEdge(desc)
}
d.copy(channels = d.channels + (u.shortChannelId -> pc.updateChannelUpdateSameSideAs(u)), rebroadcast = d.rebroadcast.copy(updates = d.rebroadcast.updates + (u -> origins)), graph = graph1)
} else {
log.debug("added channel_update for shortChannelId={} public={} flags={} {}", u.shortChannelId, publicChannel, u.channelFlags, u)
remoteOrigins.foreach(sendDecision(_, GossipDecision.Accepted(u)))
ctx.system.eventStream.publish(ChannelUpdatesReceived(u :: Nil))
db.updateChannel(u)
// we also need to update the graph
val graph1 = d.graph.addEdge(desc, u)
d.copy(channels = d.channels + (u.shortChannelId -> pc.updateChannelUpdateSameSideAs(u)), privateChannels = d.privateChannels - u.shortChannelId, rebroadcast = d.rebroadcast.copy(updates = d.rebroadcast.updates + (u -> origins)), graph = graph1)
}
} else if (d.awaiting.keys.exists(c => c.shortChannelId == u.shortChannelId)) {
// channel is currently being validated
if (d.stash.updates.contains(u)) {
log.debug("ignoring {} (already stashed)", u)
val origins1 = d.stash.updates(u) ++ origins
d.copy(stash = d.stash.copy(updates = d.stash.updates + (u -> origins1)))
} else {
log.debug("stashing {}", u)
d.copy(stash = d.stash.copy(updates = d.stash.updates + (u -> origins)))
}
} else if (d.privateChannels.contains(u.shortChannelId)) {
val publicChannel = false
val pc = d.privateChannels(u.shortChannelId)
val desc = if (Announcements.isNode1(u.channelFlags)) ChannelDesc(u.shortChannelId, pc.nodeId1, pc.nodeId2) else ChannelDesc(u.shortChannelId, pc.nodeId2, pc.nodeId1)
if (StaleChannels.isStale(u)) {
log.debug("ignoring {} (stale)", u)
remoteOrigins.foreach(sendDecision(_, GossipDecision.Stale(u)))
d
} else if (pc.getChannelUpdateSameSideAs(u).exists(_.timestamp >= u.timestamp)) {
log.debug("ignoring {} (already know same or newer)", u)
remoteOrigins.foreach(sendDecision(_, GossipDecision.Duplicate(u)))
d
} else if (!Announcements.checkSig(u, desc.a)) {
log.warning("bad signature for announcement shortChannelId={} {}", u.shortChannelId, u)
remoteOrigins.foreach(sendDecision(_, GossipDecision.InvalidSignature(u)))
d
} else if (pc.getChannelUpdateSameSideAs(u).isDefined) {
log.debug("updated channel_update for shortChannelId={} public={} flags={} {}", u.shortChannelId, publicChannel, u.channelFlags, u)
remoteOrigins.foreach(sendDecision(_, GossipDecision.Accepted(u)))
ctx.system.eventStream.publish(ChannelUpdatesReceived(u :: Nil))
// we also need to update the graph
val graph1 = d.graph.removeEdge(desc).addEdge(desc, u)
d.copy(privateChannels = d.privateChannels + (u.shortChannelId -> pc.updateChannelUpdateSameSideAs(u)), graph = graph1)
} else {
log.debug("added channel_update for shortChannelId={} public={} flags={} {}", u.shortChannelId, publicChannel, u.channelFlags, u)
remoteOrigins.foreach(sendDecision(_, GossipDecision.Accepted(u)))
ctx.system.eventStream.publish(ChannelUpdatesReceived(u :: Nil))
// we also need to update the graph
val graph1 = d.graph.addEdge(desc, u)
d.copy(privateChannels = d.privateChannels + (u.shortChannelId -> pc.updateChannelUpdateSameSideAs(u)), graph = graph1)
}
} else if (db.isPruned(u.shortChannelId) && !StaleChannels.isStale(u)) {
// the channel was recently pruned, but if we are here, it means that the update is not stale so this is the case
// of a zombie channel coming back from the dead. they probably sent us a channel_announcement right before this update,
// but we ignored it because the channel was in the 'pruned' list. Now that we know that the channel is alive again,
// let's remove the channel from the zombie list and ask the sender to re-send announcements (channel_announcement + updates)
// about that channel. We can ignore this update since we will receive it again
log.info(s"channel shortChannelId=${u.shortChannelId} is back from the dead! requesting announcements about this channel")
remoteOrigins.foreach(sendDecision(_, GossipDecision.Duplicate(u)))
db.removeFromPruned(u.shortChannelId)
// peerConnection_opt will contain a valid peerConnection only when we're handling an update that we received from a peer, not
// when we're sending updates to ourselves
origins head match {
case RemoteGossip(peerConnection, remoteNodeId) =>
val query = QueryShortChannelIds(u.chainHash, EncodedShortChannelIds(routerConf.encodingType, List(u.shortChannelId)), TlvStream.empty)
d.sync.get(remoteNodeId) match {
case Some(sync) =>
// we already have a pending request to that node, let's add this channel to the list and we'll get it later
// TODO: we only request channels with old style channel_query
d.copy(sync = d.sync + (remoteNodeId -> sync.copy(pending = sync.pending :+ query, total = sync.total + 1)))
case None =>
// we send the query right away
peerConnection ! query
d.copy(sync = d.sync + (remoteNodeId -> Syncing(pending = Nil, total = 1)))
}
case _ =>
// we don't know which node this update came from (maybe it was stashed and the channel got pruned in the meantime or some other corner case).
// or we don't have a peerConnection to send our query to.
// anyway, that's not really a big deal because we have removed the channel from the pruned db so next time it shows up we will revalidate it
d
}
} else {
log.debug("ignoring announcement {} (unknown channel)", u)
remoteOrigins.foreach(sendDecision(_, GossipDecision.NoRelatedChannel(u)))
d
}
}
def handleLocalChannelUpdate(d: Data, db: NetworkDb, routerConf: RouterConf, localNodeId: PublicKey, watcher: ActorRef, lcu: LocalChannelUpdate)(implicit ctx: ActorContext, log: LoggingAdapter): Data = {
implicit val sender: ActorRef = ctx.self // necessary to preserve origin when sending messages to other actors
import lcu.{channelAnnouncement_opt, shortChannelId, channelUpdate => u}
d.channels.get(shortChannelId) match {
case Some(_) =>
// channel has already been announced and router knows about it, we can process the channel_update
handleChannelUpdate(d, db, routerConf, Set(LocalGossip), u)
case None =>
channelAnnouncement_opt match {
case Some(c) if d.awaiting.contains(c) =>
// channel is currently being verified, we can process the channel_update right away (it will be stashed)
handleChannelUpdate(d, db, routerConf, Set(LocalGossip), u)
case Some(c) =>
// channel wasn't announced but here is the announcement, we will process it *before* the channel_update
watcher ! ValidateRequest(c)
val d1 = d.copy(awaiting = d.awaiting + (c -> Nil)) // no origin
// maybe the local channel was pruned (can happen if we were disconnected for more than 2 weeks)
db.removeFromPruned(c.shortChannelId)
handleChannelUpdate(d1, db, routerConf, Set(LocalGossip), u)
case None if d.privateChannels.contains(shortChannelId) =>
// channel isn't announced but we already know about it, we can process the channel_update
handleChannelUpdate(d, db, routerConf, Set(LocalGossip), u)
case None =>
// channel isn't announced and we never heard of it (maybe it is a private channel or maybe it is a public channel that doesn't yet have 6 confirmations)
// let's create a corresponding private channel and process the channel_update
log.debug("adding unannounced local channel to remote={} shortChannelId={}", lcu.remoteNodeId, shortChannelId)
val d1 = d.copy(privateChannels = d.privateChannels + (shortChannelId -> PrivateChannel(localNodeId, lcu.remoteNodeId, None, None)))
handleChannelUpdate(d1, db, routerConf, Set(LocalGossip), u)
}
}
}
def handleLocalChannelDown(d: Data, localNodeId: PublicKey, lcd: LocalChannelDown)(implicit log: LoggingAdapter): Data = {
import lcd.{channelId, remoteNodeId, shortChannelId}
// a local channel has permanently gone down
if (d.channels.contains(shortChannelId)) {
// the channel was public, we will receive (or have already received) a WatchEventSpentBasic event, that will trigger a clean up of the channel
// so let's not do anything here
d
} else if (d.privateChannels.contains(shortChannelId)) {
// the channel was private or public-but-not-yet-announced, let's do the clean up
log.debug("removing private local channel and channel_update for channelId={} shortChannelId={}", channelId, shortChannelId)
val desc1 = ChannelDesc(shortChannelId, localNodeId, remoteNodeId)
val desc2 = ChannelDesc(shortChannelId, remoteNodeId, localNodeId)
// we remove the corresponding updates from the graph
val graph1 = d.graph
.removeEdge(desc1)
.removeEdge(desc2)
// and we remove the channel and channel_update from our state
d.copy(privateChannels = d.privateChannels - shortChannelId, graph = graph1)
} else {
d
}
}
}

View file

@ -38,6 +38,7 @@ sealed trait SetupMessage extends LightningMessage
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 HasTemporaryChannelId extends LightningMessage { def temporaryChannelId: ByteVector32 } // <- not in the spec
sealed trait HasChannelId extends LightningMessage { def channelId: ByteVector32 } // <- not in the spec
@ -168,7 +169,7 @@ case class ChannelAnnouncement(nodeSignature1: ByteVector64,
nodeId2: PublicKey,
bitcoinKey1: PublicKey,
bitcoinKey2: PublicKey,
unknownFields: ByteVector = ByteVector.empty) extends RoutingMessage with HasChainHash
unknownFields: ByteVector = ByteVector.empty) extends RoutingMessage with AnnouncementMessage with HasChainHash
case class Color(r: Byte, g: Byte, b: Byte) {
override def toString: String = f"#$r%02x$g%02x$b%02x" // to hexa s"# ${r}%02x ${r & 0xFF}${g & 0xFF}${b & 0xFF}"
@ -211,7 +212,7 @@ case class NodeAnnouncement(signature: ByteVector64,
rgbColor: Color,
alias: String,
addresses: List[NodeAddress],
unknownFields: ByteVector = ByteVector.empty) extends RoutingMessage with HasTimestamp
unknownFields: ByteVector = ByteVector.empty) extends RoutingMessage with AnnouncementMessage with HasTimestamp
case class ChannelUpdate(signature: ByteVector64,
chainHash: ByteVector32,
@ -224,7 +225,7 @@ case class ChannelUpdate(signature: ByteVector64,
feeBaseMsat: MilliSatoshi,
feeProportionalMillionths: Long,
htlcMaximumMsat: Option[MilliSatoshi],
unknownFields: ByteVector = ByteVector.empty) extends RoutingMessage with HasTimestamp with HasChainHash {
unknownFields: ByteVector = ByteVector.empty) extends RoutingMessage with AnnouncementMessage with HasTimestamp with HasChainHash {
require(((messageFlags & 1) != 0) == htlcMaximumMsat.isDefined, "htlcMaximumMsat is not consistent with messageFlags")
def isNode1 = Announcements.isNode1(channelFlags)

View file

@ -34,7 +34,8 @@ import fr.acinq.eclair.payment.receive.MultiPartHandler.ReceivePayment
import fr.acinq.eclair.payment.receive.PaymentHandler
import fr.acinq.eclair.payment.send.PaymentInitiator.{SendPaymentRequest, SendPaymentToRouteRequest}
import fr.acinq.eclair.router.RouteCalculationSpec.makeUpdate
import fr.acinq.eclair.router.{Announcements, GetNetworkStats, GetNetworkStatsResponse, NetworkStats, PublicChannel, Router, Stats}
import fr.acinq.eclair.router.Router.{GetNetworkStats, GetNetworkStatsResponse, PublicChannel}
import fr.acinq.eclair.router.{Announcements, NetworkStats, Router, Stats}
import org.mockito.Mockito
import org.mockito.scalatest.IdiomaticMockito
import org.scalatest.{Outcome, ParallelTestExecution, fixture}

View file

@ -20,13 +20,13 @@ import java.sql.{Connection, DriverManager}
import java.util.concurrent.atomic.AtomicLong
import fr.acinq.bitcoin.Crypto.PrivateKey
import fr.acinq.bitcoin.{Block, Btc, ByteVector32, Script}
import fr.acinq.bitcoin.{Block, ByteVector32, Script}
import fr.acinq.eclair.NodeParams.BITCOIND
import fr.acinq.eclair.blockchain.fee.{FeeEstimator, FeeTargets, FeeratesPerKw, OnChainFeeConf}
import fr.acinq.eclair.crypto.LocalKeyManager
import fr.acinq.eclair.db._
import fr.acinq.eclair.io.Peer
import fr.acinq.eclair.router.RouterConf
import fr.acinq.eclair.router.Router.RouterConf
import fr.acinq.eclair.wire.{Color, EncodingType, NodeAddress}
import scodec.bits.ByteVector

View file

@ -20,8 +20,7 @@ import java.util.UUID
import java.util.concurrent.CountDownLatch
import akka.actor.{Actor, ActorLogging, ActorRef, Props, Status}
import akka.testkit
import akka.testkit.{TestActor, TestFSMRef, TestProbe}
import akka.testkit.{TestFSMRef, TestProbe}
import fr.acinq.bitcoin.ByteVector32
import fr.acinq.bitcoin.Crypto.PublicKey
import fr.acinq.eclair.TestConstants.{Alice, Bob}
@ -32,7 +31,7 @@ import fr.acinq.eclair.payment._
import fr.acinq.eclair.payment.receive.MultiPartHandler.ReceivePayment
import fr.acinq.eclair.payment.receive.PaymentHandler
import fr.acinq.eclair.payment.relay.{CommandBuffer, Relayer}
import fr.acinq.eclair.router.ChannelHop
import fr.acinq.eclair.router.Router.ChannelHop
import fr.acinq.eclair.wire.Onion.FinalLegacyPayload
import fr.acinq.eclair.wire._
import grizzled.slf4j.Logging

View file

@ -27,7 +27,7 @@ import fr.acinq.eclair.blockchain.fee.FeeTargets
import fr.acinq.eclair.channel._
import fr.acinq.eclair.io.Peer
import fr.acinq.eclair.payment.OutgoingPacket
import fr.acinq.eclair.router.ChannelHop
import fr.acinq.eclair.router.Router.ChannelHop
import fr.acinq.eclair.wire.Onion.FinalLegacyPayload
import fr.acinq.eclair.wire._
import fr.acinq.eclair.{NodeParams, TestConstants, randomBytes32, _}

View file

@ -29,7 +29,7 @@ import fr.acinq.eclair.channel.states.StateTestsHelperMethods
import fr.acinq.eclair.payment._
import fr.acinq.eclair.payment.relay.Relayer._
import fr.acinq.eclair.payment.relay.{CommandBuffer, Origin}
import fr.acinq.eclair.router.ChannelHop
import fr.acinq.eclair.router.Router.ChannelHop
import fr.acinq.eclair.wire.Onion.FinalLegacyPayload
import fr.acinq.eclair.wire.{CommitSig, Error, FailureMessageCodecs, PermanentChannelFailure, RevokeAndAck, Shutdown, UpdateAddHtlc, UpdateFailHtlc, UpdateFailMalformedHtlc, UpdateFee, UpdateFulfillHtlc}
import fr.acinq.eclair.{CltvExpiry, CltvExpiryDelta, LongToBtcAmount, TestConstants, TestkitBaseClass, randomBytes32}

View file

@ -22,7 +22,8 @@ import fr.acinq.bitcoin.Crypto.PrivateKey
import fr.acinq.bitcoin.{Block, ByteVector32, ByteVector64, Crypto, Satoshi}
import fr.acinq.eclair.db.sqlite.SqliteNetworkDb
import fr.acinq.eclair.db.sqlite.SqliteUtils._
import fr.acinq.eclair.router.{Announcements, PublicChannel}
import fr.acinq.eclair.router.Announcements
import fr.acinq.eclair.router.Router.PublicChannel
import fr.acinq.eclair.wire.{Color, NodeAddress, Tor2}
import fr.acinq.eclair.{CltvExpiryDelta, LongToBtcAmount, ShortChannelId, TestConstants, randomBytes32, randomKey}
import org.scalatest.FunSuite

View file

@ -24,7 +24,7 @@ import fr.acinq.eclair.crypto.Sphinx
import fr.acinq.eclair.db.sqlite.SqlitePaymentsDb
import fr.acinq.eclair.db.sqlite.SqliteUtils._
import fr.acinq.eclair.payment._
import fr.acinq.eclair.router.{ChannelHop, NodeHop}
import fr.acinq.eclair.router.Router.{ChannelHop, NodeHop}
import fr.acinq.eclair.wire.{ChannelUpdate, UnknownNextPeer}
import fr.acinq.eclair.{CltvExpiryDelta, LongToBtcAmount, ShortChannelId, TestConstants, randomBytes32, randomBytes64, randomKey}
import org.scalatest.FunSuite

View file

@ -33,6 +33,7 @@ import fr.acinq.eclair.channel.ChannelCommandResponse.ChannelOpened
import fr.acinq.eclair.channel.Register.{Forward, ForwardShortId}
import fr.acinq.eclair.channel._
import fr.acinq.eclair.crypto.Sphinx.DecryptedFailurePacket
import fr.acinq.eclair.crypto.TransportHandler
import fr.acinq.eclair.db._
import fr.acinq.eclair.io.Peer
import fr.acinq.eclair.io.Peer.{Disconnect, PeerRoutingMessage}
@ -44,9 +45,11 @@ import fr.acinq.eclair.payment.relay.Relayer
import fr.acinq.eclair.payment.relay.Relayer.{GetOutgoingChannels, OutgoingChannels}
import fr.acinq.eclair.payment.send.PaymentInitiator.{SendPaymentRequest, SendTrampolinePaymentRequest}
import fr.acinq.eclair.payment.send.PaymentLifecycle.{State => _}
import fr.acinq.eclair.router.{Announcements, AnnouncementsBatchValidationSpec}
import fr.acinq.eclair.router.Graph.WeightRatios
import fr.acinq.eclair.router.Router.ROUTE_MAX_LENGTH
import fr.acinq.eclair.router.{Announcements, AnnouncementsBatchValidationSpec, PublicChannel, RouteParams}
import fr.acinq.eclair.router.RouteCalculation.ROUTE_MAX_LENGTH
import fr.acinq.eclair.router.Router.{GossipDecision, PublicChannel, RouteParams}
import fr.acinq.eclair.router.Router.{NORMAL => _, State => _, _}
import fr.acinq.eclair.transactions.Transactions
import fr.acinq.eclair.transactions.Transactions.{HtlcSuccessTx, HtlcTimeoutTx}
import fr.acinq.eclair.wire._
@ -1255,7 +1258,11 @@ class IntegrationSpec extends TestKit(ActorSystem("test")) with BitcoindService
// then we make the announcements
val announcements = channels.map(c => AnnouncementsBatchValidationSpec.makeChannelAnnouncement(c))
announcements.foreach(ann => nodes("A").router ! PeerRoutingMessage(sender.ref, remoteNodeId, ann))
announcements.foreach { ann =>
nodes("A").router ! PeerRoutingMessage(sender.ref, remoteNodeId, ann)
sender.expectMsg(TransportHandler.ReadAck(ann))
sender.expectMsg(GossipDecision.Accepted(ann))
}
awaitCond({
sender.send(nodes("D").router, 'channels)
sender.expectMsgType[Iterable[ChannelAnnouncement]](5 seconds).size == channels.size + 8 // 8 remaining channels because D->F{1-5} have disappeared

View file

@ -18,7 +18,7 @@ package fr.acinq.eclair.io
import java.net.{Inet4Address, InetSocketAddress}
import akka.actor.{ActorRef, PoisonPill}
import akka.actor.PoisonPill
import akka.testkit.{TestFSMRef, TestProbe}
import fr.acinq.bitcoin.Block
import fr.acinq.bitcoin.Crypto.{PrivateKey, PublicKey}
@ -26,6 +26,7 @@ import fr.acinq.eclair.TestConstants._
import fr.acinq.eclair._
import fr.acinq.eclair.channel.states.StateTestsHelperMethods
import fr.acinq.eclair.crypto.TransportHandler
import fr.acinq.eclair.router.Router.{GossipDecision, GossipOrigin, LocalGossip, Rebroadcast, RemoteGossip, SendChannelQuery}
import fr.acinq.eclair.router.{RoutingSyncSpec, _}
import fr.acinq.eclair.wire._
import org.scalatest.{Outcome, Tag}
@ -260,7 +261,7 @@ class PeerConnectionSpec extends TestkitBaseClass with StateTestsHelperMethods {
test("filter gossip message (no filtering)") { f =>
import f._
val probe = TestProbe()
val gossipOrigin = Set[GossipOrigin](RemoteGossip(TestProbe().ref))
val gossipOrigin = Set[GossipOrigin](RemoteGossip(TestProbe().ref, randomKey.publicKey))
connect(remoteNodeId, switchboard, router, connection, transport, peerConnection, peer)
val rebroadcast = Rebroadcast(channels.map(_ -> gossipOrigin).toMap, updates.map(_ -> gossipOrigin).toMap, nodes.map(_ -> gossipOrigin).toMap)
probe.send(peerConnection, rebroadcast)
@ -270,12 +271,12 @@ class PeerConnectionSpec extends TestkitBaseClass with StateTestsHelperMethods {
test("filter gossip message (filtered by origin)") { f =>
import f._
connect(remoteNodeId, switchboard, router, connection, transport, peerConnection, peer)
val gossipOrigin = Set[GossipOrigin](RemoteGossip(TestProbe().ref))
val pcActor: ActorRef = peerConnection
val gossipOrigin = Set[GossipOrigin](RemoteGossip(TestProbe().ref, randomKey.publicKey))
val bobOrigin = RemoteGossip(peerConnection, remoteNodeId)
val rebroadcast = Rebroadcast(
channels.map(_ -> gossipOrigin).toMap + (channels(5) -> Set(RemoteGossip(pcActor))),
updates.map(_ -> gossipOrigin).toMap + (updates(6) -> (gossipOrigin + RemoteGossip(pcActor))) + (updates(10) -> Set(RemoteGossip(pcActor))),
nodes.map(_ -> gossipOrigin).toMap + (nodes(4) -> Set(RemoteGossip(pcActor))))
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 = wire.GossipTimestampFilter(Alice.nodeParams.chainHash, 0, Long.MaxValue) // no filtering on timestamps
transport.send(peerConnection, filter)
transport.expectMsg(TransportHandler.ReadAck(filter))
@ -289,7 +290,7 @@ class PeerConnectionSpec extends TestkitBaseClass with StateTestsHelperMethods {
test("filter gossip message (filtered by timestamp)") { f =>
import f._
connect(remoteNodeId, switchboard, router, connection, transport, peerConnection, peer)
val gossipOrigin = Set[GossipOrigin](RemoteGossip(TestProbe().ref))
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 = wire.GossipTimestampFilter(Alice.nodeParams.chainHash, timestamps.head, timestamps.last - timestamps.head)
@ -307,7 +308,7 @@ class PeerConnectionSpec extends TestkitBaseClass with StateTestsHelperMethods {
import f._
val probe = TestProbe()
connect(remoteNodeId, switchboard, router, connection, transport, peerConnection, peer)
val gossipOrigin = Set[GossipOrigin](RemoteGossip(TestProbe().ref))
val gossipOrigin = Set[GossipOrigin](RemoteGossip(TestProbe().ref, randomKey.publicKey))
val rebroadcast = Rebroadcast(
channels.map(_ -> gossipOrigin).toMap + (channels(5) -> Set(LocalGossip)),
updates.map(_ -> gossipOrigin).toMap + (updates(6) -> (gossipOrigin + LocalGossip)) + (updates(10) -> Set(LocalGossip)),
@ -340,7 +341,7 @@ class PeerConnectionSpec extends TestkitBaseClass with StateTestsHelperMethods {
// let's assume that the router isn't happy with those channels because the funding tx is already spent
for (c <- channels) {
router.send(peerConnection, PeerConnection.ChannelClosed(c))
router.send(peerConnection, GossipDecision.ChannelClosed(c))
}
// peer will temporary ignore announcements coming from bob
for (ann <- channels ++ updates) {
@ -363,14 +364,14 @@ class PeerConnectionSpec extends TestkitBaseClass with StateTestsHelperMethods {
transport.expectNoMsg(1 second) // peer hasn't acknowledged the messages
// now let's assume that the router isn't happy with those channels because the announcement is invalid
router.send(peerConnection, PeerConnection.InvalidAnnouncement(channels(0)))
router.send(peerConnection, GossipDecision.InvalidAnnouncement(channels(0)))
// peer will return a connection-wide error, including the hex-encoded representation of the bad message
val error1 = transport.expectMsgType[Error]
assert(error1.channelId === Peer.CHANNELID_ZERO)
assert(new String(error1.data.toArray).startsWith("couldn't verify channel! shortChannelId="))
// let's assume that one of the sigs were invalid
router.send(peerConnection, PeerConnection.InvalidSignature(channels(0)))
router.send(peerConnection, GossipDecision.InvalidSignature(channels(0)))
// peer will return a connection-wide error, including the hex-encoded representation of the bad message
val error2 = transport.expectMsgType[Error]
assert(error2.channelId === Peer.CHANNELID_ZERO)

View file

@ -32,6 +32,7 @@ import fr.acinq.eclair.payment.send.MultiPartPaymentLifecycle._
import fr.acinq.eclair.payment.send.PaymentInitiator.SendPaymentConfig
import fr.acinq.eclair.payment.send.PaymentLifecycle.SendPayment
import fr.acinq.eclair.payment.send.{MultiPartPaymentLifecycle, PaymentError}
import fr.acinq.eclair.router.Router.{ChannelHop, GetNetworkStats, GetNetworkStatsResponse, RouteParams, TickComputeNetworkStats}
import fr.acinq.eclair.router._
import fr.acinq.eclair.wire._
import org.scalatest.{Outcome, Tag, fixture}

View file

@ -22,6 +22,7 @@ import akka.actor.{ActorRef, ActorSystem}
import akka.testkit.{TestActorRef, TestKit, TestProbe}
import fr.acinq.bitcoin.Block
import fr.acinq.eclair.Features._
import fr.acinq.eclair.UInt64.Conversions._
import fr.acinq.eclair.channel.{Channel, Upstream}
import fr.acinq.eclair.crypto.Sphinx
import fr.acinq.eclair.payment.PaymentPacketSpec._
@ -30,12 +31,11 @@ import fr.acinq.eclair.payment.send.MultiPartPaymentLifecycle.SendMultiPartPayme
import fr.acinq.eclair.payment.send.PaymentInitiator._
import fr.acinq.eclair.payment.send.PaymentLifecycle.{SendPayment, SendPaymentToRoute}
import fr.acinq.eclair.payment.send.{PaymentError, PaymentInitiator}
import fr.acinq.eclair.router.{NodeHop, RouteParams}
import fr.acinq.eclair.router.Router.{NodeHop, RouteParams}
import fr.acinq.eclair.wire.Onion.{FinalLegacyPayload, FinalTlvPayload}
import fr.acinq.eclair.wire.OnionTlv.{AmountToForward, OutgoingCltv}
import fr.acinq.eclair.wire._
import fr.acinq.eclair.wire.{Onion, OnionCodecs, OnionTlv, TrampolineFeeInsufficient, _}
import fr.acinq.eclair.{CltvExpiryDelta, LongToBtcAmount, NodeParams, TestConstants, randomBytes32, randomKey}
import fr.acinq.eclair.UInt64.Conversions._
import org.scalatest.{Outcome, Tag, fixture}
import scodec.bits.HexStringSyntax

View file

@ -38,6 +38,7 @@ import fr.acinq.eclair.payment.send.PaymentInitiator.{SendPaymentConfig, SendPay
import fr.acinq.eclair.payment.send.PaymentLifecycle
import fr.acinq.eclair.payment.send.PaymentLifecycle._
import fr.acinq.eclair.router.Announcements.{makeChannelUpdate, makeNodeAnnouncement}
import fr.acinq.eclair.router.Router.{ChannelDesc, ChannelHop, ExcludeChannel, FinalizeRoute, RouteParams, RouteRequest, RouteResponse}
import fr.acinq.eclair.router._
import fr.acinq.eclair.transactions.Scripts
import fr.acinq.eclair.wire.Onion.FinalLegacyPayload
@ -145,13 +146,13 @@ class PaymentLifecycleSpec extends BaseRouterSpec {
val payFixture = createPaymentLifecycle()
import payFixture._
val request = SendPayment(d, FinalLegacyPayload(defaultAmountMsat, defaultExpiry), 3, routePrefix = Seq(ChannelHop(a, b, channelUpdate_ab), ChannelHop(b, c, channelUpdate_bc)))
val request = SendPayment(d, FinalLegacyPayload(defaultAmountMsat, defaultExpiry), 3, routePrefix = Seq(ChannelHop(a, b, update_ab), ChannelHop(b, c, update_bc)))
sender.send(paymentFSM, request)
routerForwarder.expectMsg(RouteRequest(c, d, defaultAmountMsat, ignoreNodes = Set(a, b)))
val Transition(_, WAITING_FOR_REQUEST, WAITING_FOR_ROUTE) = monitor.expectMsgClass(classOf[Transition[_]])
awaitCond(nodeParams.db.payments.getOutgoingPayment(id).exists(_.status == OutgoingPaymentStatus.Pending))
routerForwarder.send(paymentFSM, RouteResponse(Seq(ChannelHop(c, d, channelUpdate_cd)), Set.empty, Set.empty))
routerForwarder.send(paymentFSM, RouteResponse(Seq(ChannelHop(c, d, update_cd)), Set.empty, Set.empty))
val Transition(_, WAITING_FOR_ROUTE, WAITING_FOR_PAYMENT_COMPLETE) = monitor.expectMsgClass(classOf[Transition[_]])
}
@ -159,7 +160,7 @@ class PaymentLifecycleSpec extends BaseRouterSpec {
val payFixture = createPaymentLifecycle()
import payFixture._
val request = SendPayment(c, FinalLegacyPayload(defaultAmountMsat, defaultExpiry), 3, routePrefix = Seq(ChannelHop(a, b, channelUpdate_ab), ChannelHop(b, c, channelUpdate_bc)))
val request = SendPayment(c, FinalLegacyPayload(defaultAmountMsat, defaultExpiry), 3, routePrefix = Seq(ChannelHop(a, b, update_ab), ChannelHop(b, c, update_bc)))
sender.send(paymentFSM, request)
routerForwarder.expectNoMsg(50 millis) // we don't need the router when we already have the whole route
val Transition(_, WAITING_FOR_REQUEST, WAITING_FOR_ROUTE) = monitor.expectMsgClass(classOf[Transition[_]])
@ -171,13 +172,13 @@ class PaymentLifecycleSpec extends BaseRouterSpec {
val payFixture = createPaymentLifecycle()
import payFixture._
val request = SendPayment(d, FinalLegacyPayload(defaultAmountMsat, defaultExpiry), 3, routePrefix = Seq(ChannelHop(a, b, channelUpdate_ab), ChannelHop(b, c, channelUpdate_bc)))
val request = SendPayment(d, FinalLegacyPayload(defaultAmountMsat, defaultExpiry), 3, routePrefix = Seq(ChannelHop(a, b, update_ab), ChannelHop(b, c, update_bc)))
sender.send(paymentFSM, request)
routerForwarder.expectMsg(RouteRequest(c, d, defaultAmountMsat, ignoreNodes = Set(a, b)))
val Transition(_, WAITING_FOR_REQUEST, WAITING_FOR_ROUTE) = monitor.expectMsgClass(classOf[Transition[_]])
awaitCond(nodeParams.db.payments.getOutgoingPayment(id).exists(_.status == OutgoingPaymentStatus.Pending))
routerForwarder.send(paymentFSM, RouteResponse(Seq(ChannelHop(c, d, channelUpdate_cd)), Set(a, b), Set.empty))
routerForwarder.send(paymentFSM, RouteResponse(Seq(ChannelHop(c, d, update_cd)), Set(a, b), Set.empty))
val Transition(_, WAITING_FOR_ROUTE, WAITING_FOR_PAYMENT_COMPLETE) = monitor.expectMsgClass(classOf[Transition[_]])
sender.send(paymentFSM, UpdateFailHtlc(randomBytes32, 0, randomBytes(Sphinx.FailurePacket.PacketLength)))
@ -306,14 +307,14 @@ class PaymentLifecycleSpec extends BaseRouterSpec {
val WaitingForComplete(_, _, cmd1, Nil, sharedSecrets1, _, _, hops) = paymentFSM.stateData
register.expectMsg(ForwardShortId(channelId_ab, cmd1))
val failure = TemporaryChannelFailure(channelUpdate_bc)
val failure = TemporaryChannelFailure(update_bc)
sender.send(paymentFSM, UpdateFailHtlc(ByteVector32.Zeroes, 0, Sphinx.FailurePacket.create(sharedSecrets1.head._1, failure)))
// payment lifecycle will ask the router to temporarily exclude this channel from its route calculations
routerForwarder.expectMsg(ExcludeChannel(ChannelDesc(channelUpdate_bc.shortChannelId, b, c)))
routerForwarder.expectMsg(ExcludeChannel(ChannelDesc(update_bc.shortChannelId, b, c)))
routerForwarder.forward(routerFixture.router)
// payment lifecycle forwards the embedded channelUpdate to the router
routerForwarder.expectMsg(channelUpdate_bc)
routerForwarder.expectMsg(update_bc)
awaitCond(paymentFSM.stateName == WAITING_FOR_ROUTE)
routerForwarder.expectMsg(RouteRequest(a, d, defaultAmountMsat, assistedRoutes = Nil, ignoreNodes = Set.empty, ignoreChannels = Set.empty))
routerForwarder.forward(routerFixture.router)
@ -337,7 +338,7 @@ class PaymentLifecycleSpec extends BaseRouterSpec {
register.expectMsg(ForwardShortId(channelId_ab, cmd1))
// we change the cltv expiry
val channelUpdate_bc_modified = makeChannelUpdate(Block.RegtestGenesisBlock.hash, priv_b, c, channelId_bc, CltvExpiryDelta(42), htlcMinimumMsat = channelUpdate_bc.htlcMinimumMsat, feeBaseMsat = channelUpdate_bc.feeBaseMsat, feeProportionalMillionths = channelUpdate_bc.feeProportionalMillionths, htlcMaximumMsat = channelUpdate_bc.htlcMaximumMsat.get)
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)
val failure = IncorrectCltvExpiry(CltvExpiry(5), channelUpdate_bc_modified)
// and node replies with a failure containing a new channel update
sender.send(paymentFSM, UpdateFailHtlc(ByteVector32.Zeroes, 0, Sphinx.FailurePacket.create(sharedSecrets1.head._1, failure)))
@ -354,13 +355,13 @@ class PaymentLifecycleSpec extends BaseRouterSpec {
register.expectMsg(ForwardShortId(channelId_ab, cmd2))
// we change the cltv expiry one more time
val channelUpdate_bc_modified_2 = makeChannelUpdate(Block.RegtestGenesisBlock.hash, priv_b, c, channelId_bc, CltvExpiryDelta(43), htlcMinimumMsat = channelUpdate_bc.htlcMinimumMsat, feeBaseMsat = channelUpdate_bc.feeBaseMsat, feeProportionalMillionths = channelUpdate_bc.feeProportionalMillionths, htlcMaximumMsat = channelUpdate_bc.htlcMaximumMsat.get)
val channelUpdate_bc_modified_2 = makeChannelUpdate(Block.RegtestGenesisBlock.hash, priv_b, c, channelId_bc, CltvExpiryDelta(43), htlcMinimumMsat = update_bc.htlcMinimumMsat, feeBaseMsat = update_bc.feeBaseMsat, feeProportionalMillionths = update_bc.feeProportionalMillionths, htlcMaximumMsat = update_bc.htlcMaximumMsat.get)
val failure2 = IncorrectCltvExpiry(CltvExpiry(5), channelUpdate_bc_modified_2)
// and node replies with a failure containing a new channel update
sender.send(paymentFSM, UpdateFailHtlc(ByteVector32.Zeroes, 0, Sphinx.FailurePacket.create(sharedSecrets2.head._1, failure2)))
// this time the payment lifecycle will ask the router to temporarily exclude this channel from its route calculations
routerForwarder.expectMsg(ExcludeChannel(ChannelDesc(channelUpdate_bc.shortChannelId, b, c)))
routerForwarder.expectMsg(ExcludeChannel(ChannelDesc(update_bc.shortChannelId, b, c)))
routerForwarder.forward(routerFixture.router)
// but it will still forward the embedded channelUpdate to the router
routerForwarder.expectMsg(channelUpdate_bc_modified_2)
@ -379,8 +380,8 @@ class PaymentLifecycleSpec extends BaseRouterSpec {
// we build an assisted route for channel bc and cd
val assistedRoutes = Seq(Seq(
ExtraHop(b, channelId_bc, channelUpdate_bc.feeBaseMsat, channelUpdate_bc.feeProportionalMillionths, channelUpdate_bc.cltvExpiryDelta),
ExtraHop(c, channelId_cd, channelUpdate_cd.feeBaseMsat, channelUpdate_cd.feeProportionalMillionths, channelUpdate_cd.cltvExpiryDelta)
ExtraHop(b, channelId_bc, update_bc.feeBaseMsat, update_bc.feeProportionalMillionths, update_bc.cltvExpiryDelta),
ExtraHop(c, channelId_cd, update_cd.feeBaseMsat, update_cd.feeProportionalMillionths, update_cd.cltvExpiryDelta)
))
val request = SendPayment(d, FinalLegacyPayload(defaultAmountMsat, defaultExpiry), 5, assistedRoutes = assistedRoutes)
@ -395,7 +396,7 @@ class PaymentLifecycleSpec extends BaseRouterSpec {
register.expectMsg(ForwardShortId(channelId_ab, cmd1))
// we change the cltv expiry
val channelUpdate_bc_modified = makeChannelUpdate(Block.RegtestGenesisBlock.hash, priv_b, c, channelId_bc, CltvExpiryDelta(42), htlcMinimumMsat = channelUpdate_bc.htlcMinimumMsat, feeBaseMsat = channelUpdate_bc.feeBaseMsat, feeProportionalMillionths = channelUpdate_bc.feeProportionalMillionths, htlcMaximumMsat = channelUpdate_bc.htlcMaximumMsat.get)
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)
val failure = IncorrectCltvExpiry(CltvExpiry(5), channelUpdate_bc_modified)
// and node replies with a failure containing a new channel update
sender.send(paymentFSM, UpdateFailHtlc(ByteVector32.Zeroes, 0, Sphinx.FailurePacket.create(sharedSecrets1.head._1, failure)))
@ -404,8 +405,8 @@ class PaymentLifecycleSpec extends BaseRouterSpec {
routerForwarder.expectMsg(channelUpdate_bc_modified)
awaitCond(paymentFSM.stateName == WAITING_FOR_ROUTE && nodeParams.db.payments.getOutgoingPayment(id).exists(_.status === OutgoingPaymentStatus.Pending)) // 1 failure but not final, the payment is still PENDING
val assistedRoutes1 = Seq(Seq(
ExtraHop(b, channelId_bc, channelUpdate_bc.feeBaseMsat, channelUpdate_bc.feeProportionalMillionths, channelUpdate_bc_modified.cltvExpiryDelta),
ExtraHop(c, channelId_cd, channelUpdate_cd.feeBaseMsat, channelUpdate_cd.feeProportionalMillionths, channelUpdate_cd.cltvExpiryDelta)
ExtraHop(b, channelId_bc, update_bc.feeBaseMsat, update_bc.feeProportionalMillionths, channelUpdate_bc_modified.cltvExpiryDelta),
ExtraHop(c, channelId_cd, update_cd.feeBaseMsat, update_cd.feeProportionalMillionths, update_cd.cltvExpiryDelta)
))
routerForwarder.expectMsg(RouteRequest(nodeParams.nodeId, d, defaultAmountMsat, assistedRoutes = assistedRoutes1, ignoreNodes = Set.empty, ignoreChannels = Set.empty))
routerForwarder.forward(routerFixture.router)
@ -494,10 +495,11 @@ class PaymentLifecycleSpec extends BaseRouterSpec {
val channelUpdate_bg = makeChannelUpdate(Block.RegtestGenesisBlock.hash, priv_b, g, channelId_bg, CltvExpiryDelta(9), htlcMinimumMsat = 0 msat, feeBaseMsat = 0 msat, feeProportionalMillionths = 0, htlcMaximumMsat = 500000000 msat)
val channelUpdate_gb = makeChannelUpdate(Block.RegtestGenesisBlock.hash, priv_g, b, channelId_bg, CltvExpiryDelta(9), htlcMinimumMsat = 0 msat, feeBaseMsat = 10 msat, feeProportionalMillionths = 8, htlcMaximumMsat = 500000000 msat)
assert(Router.getDesc(channelUpdate_bg, chan_bg) === ChannelDesc(chan_bg.shortChannelId, priv_b.publicKey, priv_g.publicKey))
router ! PeerRoutingMessage(null, remoteNodeId, chan_bg)
router ! PeerRoutingMessage(null, remoteNodeId, ann_g)
router ! PeerRoutingMessage(null, remoteNodeId, channelUpdate_bg)
router ! PeerRoutingMessage(null, remoteNodeId, channelUpdate_gb)
val peerConnection = TestProbe()
router ! PeerRoutingMessage(peerConnection.ref, remoteNodeId, chan_bg)
router ! PeerRoutingMessage(peerConnection.ref, remoteNodeId, ann_g)
router ! PeerRoutingMessage(peerConnection.ref, remoteNodeId, channelUpdate_bg)
router ! PeerRoutingMessage(peerConnection.ref, remoteNodeId, channelUpdate_gb)
watcher.expectMsg(ValidateRequest(chan_bg))
watcher.send(router, ValidateResult(chan_bg, Right((Transaction(version = 0, txIn = Nil, txOut = TxOut(1000000 sat, write(pay2wsh(Scripts.multiSig2of2(funding_b, funding_g)))) :: Nil, lockTime = 0), UtxoStatus.Unspent))))
watcher.expectMsgType[WatchSpentBasic]
@ -528,9 +530,9 @@ class PaymentLifecycleSpec extends BaseRouterSpec {
}
test("filter errors properly") { _ =>
val failures = LocalFailure(RouteNotFound) :: RemoteFailure(ChannelHop(a, b, channelUpdate_ab) :: Nil, Sphinx.DecryptedFailurePacket(a, TemporaryNodeFailure)) :: LocalFailure(AddHtlcFailed(ByteVector32.Zeroes, ByteVector32.Zeroes, ChannelUnavailable(ByteVector32.Zeroes), Local(UUID.randomUUID(), None), None, None)) :: LocalFailure(RouteNotFound) :: Nil
val failures = LocalFailure(RouteNotFound) :: RemoteFailure(ChannelHop(a, b, update_ab) :: Nil, Sphinx.DecryptedFailurePacket(a, TemporaryNodeFailure)) :: LocalFailure(AddHtlcFailed(ByteVector32.Zeroes, ByteVector32.Zeroes, ChannelUnavailable(ByteVector32.Zeroes), Local(UUID.randomUUID(), None), None, None)) :: LocalFailure(RouteNotFound) :: Nil
val filtered = PaymentFailure.transformForUser(failures)
assert(filtered == LocalFailure(RouteNotFound) :: RemoteFailure(ChannelHop(a, b, channelUpdate_ab) :: Nil, Sphinx.DecryptedFailurePacket(a, TemporaryNodeFailure)) :: LocalFailure(ChannelUnavailable(ByteVector32.Zeroes)) :: Nil)
assert(filtered == LocalFailure(RouteNotFound) :: RemoteFailure(ChannelHop(a, b, update_ab) :: Nil, Sphinx.DecryptedFailurePacket(a, TemporaryNodeFailure)) :: LocalFailure(ChannelUnavailable(ByteVector32.Zeroes)) :: Nil)
}
test("disable database and events") { routerFixture =>

View file

@ -27,7 +27,7 @@ import fr.acinq.eclair.crypto.Sphinx
import fr.acinq.eclair.payment.IncomingPacket.{ChannelRelayPacket, FinalPacket, NodeRelayPacket, decrypt}
import fr.acinq.eclair.payment.OutgoingPacket._
import fr.acinq.eclair.payment.PaymentRequest.Features
import fr.acinq.eclair.router.{ChannelHop, NodeHop}
import fr.acinq.eclair.router.Router.{ChannelHop, NodeHop}
import fr.acinq.eclair.wire.Onion.{FinalLegacyPayload, FinalTlvPayload, RelayLegacyPayload}
import fr.acinq.eclair.wire.OnionTlv.{AmountToForward, OutgoingCltv, PaymentData}
import fr.acinq.eclair.wire._

View file

@ -29,7 +29,7 @@ import fr.acinq.eclair.db.{OutgoingPayment, OutgoingPaymentStatus, PaymentType}
import fr.acinq.eclair.payment.OutgoingPacket.buildCommand
import fr.acinq.eclair.payment.PaymentPacketSpec._
import fr.acinq.eclair.payment.relay.{CommandBuffer, Origin, PostRestartHtlcCleaner, Relayer}
import fr.acinq.eclair.router.ChannelHop
import fr.acinq.eclair.router.Router.ChannelHop
import fr.acinq.eclair.transactions.{DirectedHtlc, IncomingHtlc, OutgoingHtlc}
import fr.acinq.eclair.wire.Onion.FinalLegacyPayload
import fr.acinq.eclair.wire._

View file

@ -28,7 +28,8 @@ import fr.acinq.eclair.payment.OutgoingPacket.{buildCommand, buildOnion, buildPa
import fr.acinq.eclair.payment.relay.Origin._
import fr.acinq.eclair.payment.relay.Relayer._
import fr.acinq.eclair.payment.relay.{CommandBuffer, Relayer}
import fr.acinq.eclair.router._
import fr.acinq.eclair.router.Router.{ChannelHop, GetNetworkStats, GetNetworkStatsResponse, NodeHop, TickComputeNetworkStats}
import fr.acinq.eclair.router.{Announcements, _}
import fr.acinq.eclair.wire.Onion.{ChannelRelayTlvPayload, FinalLegacyPayload, FinalTlvPayload, PerHopPayload}
import fr.acinq.eclair.wire._
import fr.acinq.eclair.{CltvExpiry, CltvExpiryDelta, LongToBtcAmount, NodeParams, ShortChannelId, TestConstants, TestkitBaseClass, UInt64, nodeFee, randomBytes32}

View file

@ -26,6 +26,7 @@ import fr.acinq.eclair.blockchain.{UtxoStatus, ValidateRequest, ValidateResult,
import fr.acinq.eclair.crypto.LocalKeyManager
import fr.acinq.eclair.io.Peer.PeerRoutingMessage
import fr.acinq.eclair.router.Announcements._
import fr.acinq.eclair.router.Router.ChannelDesc
import fr.acinq.eclair.transactions.Scripts
import fr.acinq.eclair.wire._
import fr.acinq.eclair.{TestkitBaseClass, randomKey, _}
@ -42,7 +43,7 @@ import scala.concurrent.duration._
abstract class BaseRouterSpec extends TestkitBaseClass {
case class FixtureParam(router: ActorRef, watcher: TestProbe)
case class FixtureParam(nodeParams: NodeParams, router: ActorRef, watcher: TestProbe)
val remoteNodeId = PrivateKey(ByteVector32(ByteVector.fill(32)(1))).publicKey
@ -55,12 +56,12 @@ abstract class BaseRouterSpec extends TestkitBaseClass {
val (priv_funding_a, priv_funding_b, priv_funding_c, priv_funding_d, priv_funding_e, priv_funding_f) = (randomKey, randomKey, randomKey, randomKey, randomKey, randomKey)
val (funding_a, funding_b, funding_c, funding_d, funding_e, funding_f) = (priv_funding_a.publicKey, priv_funding_b.publicKey, priv_funding_c.publicKey, priv_funding_d.publicKey, priv_funding_e.publicKey, priv_funding_f.publicKey)
val ann_a = makeNodeAnnouncement(priv_a, "node-A", Color(15, 10, -70), Nil, hex"0200")
val ann_b = makeNodeAnnouncement(priv_b, "node-B", Color(50, 99, -80), Nil, hex"")
val ann_c = makeNodeAnnouncement(priv_c, "node-C", Color(123, 100, -40), Nil, hex"0200")
val ann_d = makeNodeAnnouncement(priv_d, "node-D", Color(-120, -20, 60), Nil, hex"00")
val ann_e = makeNodeAnnouncement(priv_e, "node-E", Color(-50, 0, 10), Nil, hex"00")
val ann_f = makeNodeAnnouncement(priv_f, "node-F", Color(30, 10, -50), Nil, hex"00")
val node_a = makeNodeAnnouncement(priv_a, "node-A", Color(15, 10, -70), Nil, hex"0200")
val node_b = makeNodeAnnouncement(priv_b, "node-B", Color(50, 99, -80), Nil, hex"")
val node_c = makeNodeAnnouncement(priv_c, "node-C", Color(123, 100, -40), Nil, hex"0200")
val node_d = makeNodeAnnouncement(priv_d, "node-D", Color(-120, -20, 60), Nil, hex"00")
val node_e = makeNodeAnnouncement(priv_e, "node-E", Color(-50, 0, 10), Nil, hex"00")
val node_f = makeNodeAnnouncement(priv_f, "node-F", Color(30, 10, -50), Nil, hex"00")
val channelId_ab = ShortChannelId(420000, 1, 0)
val channelId_bc = ShortChannelId(420000, 2, 0)
@ -78,14 +79,14 @@ abstract class BaseRouterSpec extends TestkitBaseClass {
val chan_cd = channelAnnouncement(channelId_cd, priv_c, priv_d, priv_funding_c, priv_funding_d)
val chan_ef = channelAnnouncement(channelId_ef, priv_e, priv_f, priv_funding_e, priv_funding_f)
val channelUpdate_ab = makeChannelUpdate(Block.RegtestGenesisBlock.hash, priv_a, b, channelId_ab, CltvExpiryDelta(7), htlcMinimumMsat = 0 msat, feeBaseMsat = 10 msat, feeProportionalMillionths = 10, htlcMaximumMsat = 500000000 msat)
val channelUpdate_ba = makeChannelUpdate(Block.RegtestGenesisBlock.hash, priv_b, a, channelId_ab, CltvExpiryDelta(7), htlcMinimumMsat = 0 msat, feeBaseMsat = 10 msat, feeProportionalMillionths = 10, htlcMaximumMsat = 500000000 msat)
val channelUpdate_bc = makeChannelUpdate(Block.RegtestGenesisBlock.hash, priv_b, c, channelId_bc, CltvExpiryDelta(5), htlcMinimumMsat = 0 msat, feeBaseMsat = 10 msat, feeProportionalMillionths = 1, htlcMaximumMsat = 500000000 msat)
val channelUpdate_cb = makeChannelUpdate(Block.RegtestGenesisBlock.hash, priv_c, b, channelId_bc, CltvExpiryDelta(5), htlcMinimumMsat = 0 msat, feeBaseMsat = 10 msat, feeProportionalMillionths = 1, htlcMaximumMsat = 500000000 msat)
val channelUpdate_cd = makeChannelUpdate(Block.RegtestGenesisBlock.hash, priv_c, d, channelId_cd, CltvExpiryDelta(3), htlcMinimumMsat = 0 msat, feeBaseMsat = 10 msat, feeProportionalMillionths = 4, htlcMaximumMsat = 500000000 msat)
val channelUpdate_dc = makeChannelUpdate(Block.RegtestGenesisBlock.hash, priv_d, c, channelId_cd, CltvExpiryDelta(3), htlcMinimumMsat = 0 msat, feeBaseMsat = 10 msat, feeProportionalMillionths = 4, htlcMaximumMsat = 500000000 msat)
val channelUpdate_ef = makeChannelUpdate(Block.RegtestGenesisBlock.hash, priv_e, f, channelId_ef, CltvExpiryDelta(9), htlcMinimumMsat = 0 msat, feeBaseMsat = 10 msat, feeProportionalMillionths = 8, htlcMaximumMsat = 500000000 msat)
val channelUpdate_fe = makeChannelUpdate(Block.RegtestGenesisBlock.hash, priv_f, e, channelId_ef, CltvExpiryDelta(9), htlcMinimumMsat = 0 msat, feeBaseMsat = 10 msat, feeProportionalMillionths = 8, htlcMaximumMsat = 500000000 msat)
val update_ab = makeChannelUpdate(Block.RegtestGenesisBlock.hash, priv_a, b, channelId_ab, CltvExpiryDelta(7), htlcMinimumMsat = 0 msat, feeBaseMsat = 10 msat, feeProportionalMillionths = 10, htlcMaximumMsat = 500000000 msat)
val update_ba = makeChannelUpdate(Block.RegtestGenesisBlock.hash, priv_b, a, channelId_ab, CltvExpiryDelta(7), htlcMinimumMsat = 0 msat, feeBaseMsat = 10 msat, feeProportionalMillionths = 10, htlcMaximumMsat = 500000000 msat)
val update_bc = makeChannelUpdate(Block.RegtestGenesisBlock.hash, priv_b, c, channelId_bc, CltvExpiryDelta(5), htlcMinimumMsat = 0 msat, feeBaseMsat = 10 msat, feeProportionalMillionths = 1, htlcMaximumMsat = 500000000 msat)
val update_cb = makeChannelUpdate(Block.RegtestGenesisBlock.hash, priv_c, b, channelId_bc, CltvExpiryDelta(5), htlcMinimumMsat = 0 msat, feeBaseMsat = 10 msat, feeProportionalMillionths = 1, htlcMaximumMsat = 500000000 msat)
val update_cd = makeChannelUpdate(Block.RegtestGenesisBlock.hash, priv_c, d, channelId_cd, CltvExpiryDelta(3), htlcMinimumMsat = 0 msat, feeBaseMsat = 10 msat, feeProportionalMillionths = 4, htlcMaximumMsat = 500000000 msat)
val update_dc = makeChannelUpdate(Block.RegtestGenesisBlock.hash, priv_d, c, channelId_cd, CltvExpiryDelta(3), htlcMinimumMsat = 0 msat, feeBaseMsat = 10 msat, feeProportionalMillionths = 4, htlcMaximumMsat = 500000000 msat)
val update_ef = makeChannelUpdate(Block.RegtestGenesisBlock.hash, priv_e, f, channelId_ef, CltvExpiryDelta(9), htlcMinimumMsat = 0 msat, feeBaseMsat = 10 msat, feeProportionalMillionths = 8, htlcMaximumMsat = 500000000 msat)
val update_fe = makeChannelUpdate(Block.RegtestGenesisBlock.hash, priv_f, e, channelId_ef, CltvExpiryDelta(9), htlcMinimumMsat = 0 msat, feeBaseMsat = 10 msat, feeProportionalMillionths = 8, htlcMaximumMsat = 500000000 msat)
override def withFixture(test: OneArgTest): Outcome = {
// the network will be a --(1)--> b ---(2)--> c --(3)--> d and e --(4)--> f (we are a)
@ -93,36 +94,38 @@ abstract class BaseRouterSpec extends TestkitBaseClass {
within(30 seconds) {
// first we make sure that we correctly resolve channelId+direction to nodeId
assert(Router.getDesc(channelUpdate_ab, chan_ab) === ChannelDesc(chan_ab.shortChannelId, priv_a.publicKey, priv_b.publicKey))
assert(Router.getDesc(channelUpdate_bc, chan_bc) === ChannelDesc(chan_bc.shortChannelId, priv_b.publicKey, priv_c.publicKey))
assert(Router.getDesc(channelUpdate_cd, chan_cd) === ChannelDesc(chan_cd.shortChannelId, priv_c.publicKey, priv_d.publicKey))
assert(Router.getDesc(channelUpdate_ef, chan_ef) === ChannelDesc(chan_ef.shortChannelId, priv_e.publicKey, priv_f.publicKey))
assert(Router.getDesc(update_ab, chan_ab) === ChannelDesc(chan_ab.shortChannelId, priv_a.publicKey, priv_b.publicKey))
assert(Router.getDesc(update_bc, chan_bc) === ChannelDesc(chan_bc.shortChannelId, priv_b.publicKey, priv_c.publicKey))
assert(Router.getDesc(update_cd, chan_cd) === ChannelDesc(chan_cd.shortChannelId, priv_c.publicKey, priv_d.publicKey))
assert(Router.getDesc(update_ef, chan_ef) === ChannelDesc(chan_ef.shortChannelId, priv_e.publicKey, priv_f.publicKey))
// let's we set up the router
// let's set up the router
val peerConnection = TestProbe()
val watcher = TestProbe()
val router = system.actorOf(Router.props(Alice.nodeParams, watcher.ref))
val nodeParams = Alice.nodeParams
val router = system.actorOf(Router.props(nodeParams, watcher.ref))
// we announce channels
router ! PeerRoutingMessage(null, remoteNodeId, chan_ab)
router ! PeerRoutingMessage(null, remoteNodeId, chan_bc)
router ! PeerRoutingMessage(null, remoteNodeId, chan_cd)
router ! PeerRoutingMessage(null, remoteNodeId, chan_ef)
peerConnection.send(router, PeerRoutingMessage(peerConnection.ref, remoteNodeId, chan_ab))
peerConnection.send(router, PeerRoutingMessage(peerConnection.ref, remoteNodeId, chan_bc))
peerConnection.send(router, PeerRoutingMessage(peerConnection.ref, remoteNodeId, chan_cd))
peerConnection.send(router, PeerRoutingMessage(peerConnection.ref, remoteNodeId, chan_ef))
// then nodes
router ! PeerRoutingMessage(null, remoteNodeId, ann_a)
router ! PeerRoutingMessage(null, remoteNodeId, ann_b)
router ! PeerRoutingMessage(null, remoteNodeId, ann_c)
router ! PeerRoutingMessage(null, remoteNodeId, ann_d)
router ! PeerRoutingMessage(null, remoteNodeId, ann_e)
router ! PeerRoutingMessage(null, remoteNodeId, ann_f)
peerConnection.send(router, PeerRoutingMessage(peerConnection.ref, remoteNodeId, node_a))
peerConnection.send(router, PeerRoutingMessage(peerConnection.ref, remoteNodeId, node_b))
peerConnection.send(router, PeerRoutingMessage(peerConnection.ref, remoteNodeId, node_c))
peerConnection.send(router, PeerRoutingMessage(peerConnection.ref, remoteNodeId, node_d))
peerConnection.send(router, PeerRoutingMessage(peerConnection.ref, remoteNodeId, node_e))
peerConnection.send(router, PeerRoutingMessage(peerConnection.ref, remoteNodeId, node_f))
// then channel updates
router ! PeerRoutingMessage(null, remoteNodeId, channelUpdate_ab)
router ! PeerRoutingMessage(null, remoteNodeId, channelUpdate_ba)
router ! PeerRoutingMessage(null, remoteNodeId, channelUpdate_bc)
router ! PeerRoutingMessage(null, remoteNodeId, channelUpdate_cb)
router ! PeerRoutingMessage(null, remoteNodeId, channelUpdate_cd)
router ! PeerRoutingMessage(null, remoteNodeId, channelUpdate_dc)
router ! PeerRoutingMessage(null, remoteNodeId, channelUpdate_ef)
router ! PeerRoutingMessage(null, remoteNodeId, channelUpdate_fe)
peerConnection.send(router, PeerRoutingMessage(peerConnection.ref, remoteNodeId, update_ab))
peerConnection.send(router, PeerRoutingMessage(peerConnection.ref, remoteNodeId, update_ba))
peerConnection.send(router, PeerRoutingMessage(peerConnection.ref, remoteNodeId, update_bc))
peerConnection.send(router, PeerRoutingMessage(peerConnection.ref, remoteNodeId, update_cb))
peerConnection.send(router, PeerRoutingMessage(peerConnection.ref, remoteNodeId, update_cd))
peerConnection.send(router, PeerRoutingMessage(peerConnection.ref, remoteNodeId, update_dc))
peerConnection.send(router, PeerRoutingMessage(peerConnection.ref, remoteNodeId, update_ef))
peerConnection.send(router, PeerRoutingMessage(peerConnection.ref, remoteNodeId, update_fe))
// watcher receives the get tx requests
watcher.expectMsg(ValidateRequest(chan_ab))
watcher.expectMsg(ValidateRequest(chan_bc))
@ -151,7 +154,7 @@ abstract class BaseRouterSpec extends TestkitBaseClass {
nodes.size === 6 && channels.size === 4 && updates.size === 8
}, max = 10 seconds, interval = 1 second)
withFixture(test.toNoArgTest(FixtureParam(router, watcher)))
withFixture(test.toNoArgTest(FixtureParam(nodeParams, router, watcher)))
}
}
}

View file

@ -17,10 +17,11 @@
package fr.acinq.eclair.router
import fr.acinq.bitcoin.{Block, ByteVector32}
import fr.acinq.eclair.router.Router.ShortChannelIdsChunk
import fr.acinq.eclair.router.Router.PublicChannel
import fr.acinq.eclair.router.Sync._
import fr.acinq.eclair.wire.QueryChannelRangeTlv.QueryFlags
import fr.acinq.eclair.wire.{EncodedShortChannelIds, EncodingType, QueryChannelRange, QueryChannelRangeTlv, ReplyChannelRange}
import fr.acinq.eclair.wire.ReplyChannelRangeTlv._
import fr.acinq.eclair.wire.{EncodedShortChannelIds, EncodingType, ReplyChannelRange}
import fr.acinq.eclair.{LongToBtcAmount, ShortChannelId, randomKey}
import org.scalatest.FunSuite
import scodec.bits.ByteVector
@ -36,51 +37,51 @@ class ChannelRangeQueriesSpec extends FunSuite {
test("ask for update test") {
// they don't provide anything => we always ask for the update
assert(Router.shouldRequestUpdate(0, 0, None, None))
assert(Router.shouldRequestUpdate(Int.MaxValue, 12345, None, None))
assert(shouldRequestUpdate(0, 0, None, None))
assert(shouldRequestUpdate(Int.MaxValue, 12345, None, None))
// their update is older => don't ask
val now = Platform.currentTime / 1000
assert(!Router.shouldRequestUpdate(now, 0, Some(now - 1), None))
assert(!Router.shouldRequestUpdate(now, 0, Some(now - 1), Some(12345)))
assert(!Router.shouldRequestUpdate(now, 12344, Some(now - 1), None))
assert(!Router.shouldRequestUpdate(now, 12344, Some(now - 1), Some(12345)))
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))
assert(!shouldRequestUpdate(now, 12344, Some(now - 1), Some(12345)))
// their update is newer but stale => don't ask
val old = now - 4 * 2016 * 24 * 3600
assert(!Router.shouldRequestUpdate(old - 1, 0, Some(old), None))
assert(!Router.shouldRequestUpdate(old - 1, 0, Some(old), Some(12345)))
assert(!Router.shouldRequestUpdate(old - 1, 12344, Some(old), None))
assert(!Router.shouldRequestUpdate(old - 1, 12344, Some(old), Some(12345)))
assert(!shouldRequestUpdate(old - 1, 0, Some(old), None))
assert(!shouldRequestUpdate(old - 1, 0, Some(old), Some(12345)))
assert(!shouldRequestUpdate(old - 1, 12344, Some(old), None))
assert(!shouldRequestUpdate(old - 1, 12344, Some(old), Some(12345)))
// their update is newer but with the same checksum, and ours is stale or about to be => ask (we want to renew our update)
assert(Router.shouldRequestUpdate(old, 12345, Some(now), Some(12345)))
assert(shouldRequestUpdate(old, 12345, Some(now), Some(12345)))
// their update is newer but with the same checksum => don't ask
assert(!Router.shouldRequestUpdate(now - 1, 12345, Some(now), Some(12345)))
assert(!shouldRequestUpdate(now - 1, 12345, Some(now), Some(12345)))
// their update is newer with a different checksum => always ask
assert(Router.shouldRequestUpdate(now - 1, 0, Some(now), None))
assert(Router.shouldRequestUpdate(now - 1, 0, Some(now), Some(12345)))
assert(Router.shouldRequestUpdate(now - 1, 12344, Some(now), None))
assert(Router.shouldRequestUpdate(now - 1, 12344, Some(now), Some(12345)))
assert(shouldRequestUpdate(now - 1, 0, Some(now), None))
assert(shouldRequestUpdate(now - 1, 0, Some(now), Some(12345)))
assert(shouldRequestUpdate(now - 1, 12344, Some(now), None))
assert(shouldRequestUpdate(now - 1, 12344, Some(now), Some(12345)))
// they just provided a 0 checksum => don't ask
assert(!Router.shouldRequestUpdate(0, 0, None, Some(0)))
assert(!Router.shouldRequestUpdate(now, 1234, None, Some(0)))
assert(!shouldRequestUpdate(0, 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
assert(!Router.shouldRequestUpdate(now, 1234, None, Some(1234)))
assert(!shouldRequestUpdate(now, 1234, None, Some(1234)))
// they just provided a different checksum that is the same as us => ask
assert(Router.shouldRequestUpdate(now, 1234, None, Some(1235)))
assert(shouldRequestUpdate(now, 1234, None, Some(1235)))
}
test("compute checksums") {
assert(Router.crc32c(ByteVector.fromValidHex("00" * 32)) == 0x8a9136aaL)
assert(Router.crc32c(ByteVector.fromValidHex("FF" * 32)) == 0x62a8ab43L)
assert(Router.crc32c(ByteVector((0 to 31).map(_.toByte))) == 0x46dd794eL)
assert(Router.crc32c(ByteVector((31 to 0 by -1).map(_.toByte))) == 0x113fdb5cL)
assert(crc32c(ByteVector.fromValidHex("00" * 32)) == 0x8a9136aaL)
assert(crc32c(ByteVector.fromValidHex("FF" * 32)) == 0x62a8ab43L)
assert(crc32c(ByteVector((0 to 31).map(_.toByte))) == 0x46dd794eL)
assert(crc32c(ByteVector((31 to 0 by -1).map(_.toByte))) == 0x113fdb5cL)
}
test("compute flag tests") {
@ -110,28 +111,28 @@ class ChannelRangeQueriesSpec extends FunSuite {
import fr.acinq.eclair.wire.QueryShortChannelIdsTlv.QueryFlagType._
assert(Router.getChannelDigestInfo(channels)(ab.shortChannelId) == (Timestamps(now, now), Checksums(1697591108L, 3692323747L)))
assert(getChannelDigestInfo(channels)(ab.shortChannelId) == (Timestamps(now, now), Checksums(1697591108L, 3692323747L)))
// no extended info but we know the channel: we ask for the updates
assert(Router.computeFlag(channels)(ab.shortChannelId, None, None, false) === (INCLUDE_CHANNEL_UPDATE_1 | INCLUDE_CHANNEL_UPDATE_2))
assert(Router.computeFlag(channels)(ab.shortChannelId, None, None, true) === (INCLUDE_CHANNEL_UPDATE_1 | INCLUDE_CHANNEL_UPDATE_2 | INCLUDE_NODE_ANNOUNCEMENT_1 | INCLUDE_NODE_ANNOUNCEMENT_2))
assert(computeFlag(channels)(ab.shortChannelId, None, None, false) === (INCLUDE_CHANNEL_UPDATE_1 | INCLUDE_CHANNEL_UPDATE_2))
assert(computeFlag(channels)(ab.shortChannelId, None, None, true) === (INCLUDE_CHANNEL_UPDATE_1 | INCLUDE_CHANNEL_UPDATE_2 | INCLUDE_NODE_ANNOUNCEMENT_1 | INCLUDE_NODE_ANNOUNCEMENT_2))
// same checksums, newer timestamps: we don't ask anything
assert(Router.computeFlag(channels)(ab.shortChannelId, Some(Timestamps(now + 1, now + 1)), Some(Checksums(1697591108L, 3692323747L)), true) === 0)
assert(computeFlag(channels)(ab.shortChannelId, Some(Timestamps(now + 1, now + 1)), Some(Checksums(1697591108L, 3692323747L)), true) === 0)
// different checksums, newer timestamps: we ask for the updates
assert(Router.computeFlag(channels)(ab.shortChannelId, Some(Timestamps(now + 1, now)), Some(Checksums(154654604, 3692323747L)), true) === (INCLUDE_CHANNEL_UPDATE_1 | INCLUDE_NODE_ANNOUNCEMENT_1 | INCLUDE_NODE_ANNOUNCEMENT_2))
assert(Router.computeFlag(channels)(ab.shortChannelId, Some(Timestamps(now, now + 1)), Some(Checksums(1697591108L, 45664546)), true) === (INCLUDE_CHANNEL_UPDATE_2 | INCLUDE_NODE_ANNOUNCEMENT_1 | INCLUDE_NODE_ANNOUNCEMENT_2))
assert(Router.computeFlag(channels)(ab.shortChannelId, Some(Timestamps(now + 1, now + 1)), Some(Checksums(154654604, 45664546 + 6)), true) === (INCLUDE_CHANNEL_UPDATE_1 | INCLUDE_CHANNEL_UPDATE_2 | INCLUDE_NODE_ANNOUNCEMENT_1 | INCLUDE_NODE_ANNOUNCEMENT_2))
assert(computeFlag(channels)(ab.shortChannelId, Some(Timestamps(now + 1, now)), Some(Checksums(154654604, 3692323747L)), true) === (INCLUDE_CHANNEL_UPDATE_1 | INCLUDE_NODE_ANNOUNCEMENT_1 | INCLUDE_NODE_ANNOUNCEMENT_2))
assert(computeFlag(channels)(ab.shortChannelId, Some(Timestamps(now, now + 1)), Some(Checksums(1697591108L, 45664546)), true) === (INCLUDE_CHANNEL_UPDATE_2 | INCLUDE_NODE_ANNOUNCEMENT_1 | INCLUDE_NODE_ANNOUNCEMENT_2))
assert(computeFlag(channels)(ab.shortChannelId, Some(Timestamps(now + 1, now + 1)), Some(Checksums(154654604, 45664546 + 6)), true) === (INCLUDE_CHANNEL_UPDATE_1 | INCLUDE_CHANNEL_UPDATE_2 | INCLUDE_NODE_ANNOUNCEMENT_1 | INCLUDE_NODE_ANNOUNCEMENT_2))
// different checksums, older timestamps: we don't ask anything
assert(Router.computeFlag(channels)(ab.shortChannelId, Some(Timestamps(now - 1, now)), Some(Checksums(154654604, 3692323747L)), true) === 0)
assert(Router.computeFlag(channels)(ab.shortChannelId, Some(Timestamps(now, now - 1)), Some(Checksums(1697591108L, 45664546)), true) === 0)
assert(Router.computeFlag(channels)(ab.shortChannelId, Some(Timestamps(now - 1, now - 1)), Some(Checksums(154654604, 45664546)), true) === 0)
assert(computeFlag(channels)(ab.shortChannelId, Some(Timestamps(now - 1, now)), Some(Checksums(154654604, 3692323747L)), true) === 0)
assert(computeFlag(channels)(ab.shortChannelId, Some(Timestamps(now, now - 1)), Some(Checksums(1697591108L, 45664546)), true) === 0)
assert(computeFlag(channels)(ab.shortChannelId, Some(Timestamps(now - 1, now - 1)), Some(Checksums(154654604, 45664546)), true) === 0)
// missing channel update: we ask for it
assert(Router.computeFlag(channels)(cd.shortChannelId, Some(Timestamps(now, now)), Some(Checksums(3297511804L, 3297511804L)), true) === (INCLUDE_CHANNEL_UPDATE_2 | INCLUDE_NODE_ANNOUNCEMENT_1 | INCLUDE_NODE_ANNOUNCEMENT_2))
assert(computeFlag(channels)(cd.shortChannelId, Some(Timestamps(now, now)), Some(Checksums(3297511804L, 3297511804L)), true) === (INCLUDE_CHANNEL_UPDATE_2 | INCLUDE_NODE_ANNOUNCEMENT_1 | INCLUDE_NODE_ANNOUNCEMENT_2))
// unknown channel: we ask everything
assert(Router.computeFlag(channels)(ef.shortChannelId, None, None, false) === (INCLUDE_CHANNEL_ANNOUNCEMENT | INCLUDE_CHANNEL_UPDATE_1 | INCLUDE_CHANNEL_UPDATE_2))
assert(Router.computeFlag(channels)(ef.shortChannelId, None, None, true) === (INCLUDE_CHANNEL_ANNOUNCEMENT | INCLUDE_CHANNEL_UPDATE_1 | INCLUDE_CHANNEL_UPDATE_2 | INCLUDE_NODE_ANNOUNCEMENT_1 | INCLUDE_NODE_ANNOUNCEMENT_2))
assert(computeFlag(channels)(ef.shortChannelId, None, None, false) === (INCLUDE_CHANNEL_ANNOUNCEMENT | INCLUDE_CHANNEL_UPDATE_1 | INCLUDE_CHANNEL_UPDATE_2))
assert(computeFlag(channels)(ef.shortChannelId, None, None, true) === (INCLUDE_CHANNEL_ANNOUNCEMENT | INCLUDE_CHANNEL_UPDATE_1 | INCLUDE_CHANNEL_UPDATE_2 | INCLUDE_NODE_ANNOUNCEMENT_1 | INCLUDE_NODE_ANNOUNCEMENT_2))
}
def makeShortChannelIds(height: Int, count: Int): List[ShortChannelId] = {
@ -151,7 +152,7 @@ class ChannelRangeQueriesSpec extends FunSuite {
}
def validate(chunk: ShortChannelIdsChunk) = {
require(chunk.shortChannelIds.forall(Router.keep(chunk.firstBlock, chunk.numBlocks, _)))
require(chunk.shortChannelIds.forall(keep(chunk.firstBlock, chunk.numBlocks, _)))
}
// check that chunks contain exactly the ids they were built from are are consistent i.e each chunk covers a range that immediately follows
@ -167,7 +168,7 @@ class ChannelRangeQueriesSpec extends FunSuite {
// aggregate ids from all chunks, to check that they match our input ids exactly
val chunkIds = SortedSet.empty[ShortChannelId] ++ chunks.flatMap(_.shortChannelIds).toSet
val expected = ids.filter(Router.keep(firstBlockNum, numberOfBlocks, _))
val expected = ids.filter(keep(firstBlockNum, numberOfBlocks, _))
if (expected.isEmpty) require(chunks == List(ShortChannelIdsChunk(firstBlockNum, numberOfBlocks, Nil)))
chunks.foreach(validate)
@ -177,7 +178,7 @@ class ChannelRangeQueriesSpec extends FunSuite {
require(noOverlap(chunks))
}
test("limit channel ids chunk size") {
test("limit channel ids chunk size") {
val ids = makeShortChannelIds(1, 3)
val chunk = ShortChannelIdsChunk(0, 10, ids)
@ -200,7 +201,7 @@ class ChannelRangeQueriesSpec extends FunSuite {
val ids = Nil
val firstBlockNum = 10
val numberOfBlocks = 100
val chunks = Router.split(SortedSet.empty[ShortChannelId] ++ ids, firstBlockNum, numberOfBlocks, ids.size)
val chunks = split(SortedSet.empty[ShortChannelId] ++ ids, firstBlockNum, numberOfBlocks, ids.size)
assert(chunks == ShortChannelIdsChunk(firstBlockNum, numberOfBlocks, Nil) :: Nil)
}
@ -209,7 +210,7 @@ class ChannelRangeQueriesSpec extends FunSuite {
val ids = List(id(1000), id(1001), id(1002), id(1003), id(1004), id(1005))
val firstBlockNum = 10
val numberOfBlocks = 100
val chunks = Router.split(SortedSet.empty[ShortChannelId] ++ ids, firstBlockNum, numberOfBlocks, ids.size)
val chunks = split(SortedSet.empty[ShortChannelId] ++ ids, firstBlockNum, numberOfBlocks, ids.size)
assert(chunks == ShortChannelIdsChunk(firstBlockNum, numberOfBlocks, Nil) :: Nil)
}
@ -218,7 +219,7 @@ class ChannelRangeQueriesSpec extends FunSuite {
val ids = List(id(1000), id(1001), id(1002), id(1003), id(1004), id(1005))
val firstBlockNum = 1100
val numberOfBlocks = 100
val chunks = Router.split(SortedSet.empty[ShortChannelId] ++ ids, firstBlockNum, numberOfBlocks, ids.size)
val chunks = split(SortedSet.empty[ShortChannelId] ++ ids, firstBlockNum, numberOfBlocks, ids.size)
assert(chunks == ShortChannelIdsChunk(firstBlockNum, numberOfBlocks, Nil) :: Nil)
}
@ -227,7 +228,7 @@ class ChannelRangeQueriesSpec extends FunSuite {
val ids = List(id(1000), id(1001), id(1002), id(1003), id(1004), id(1005))
val firstBlockNum = 900
val numberOfBlocks = 200
val chunks = Router.split(SortedSet.empty[ShortChannelId] ++ ids, firstBlockNum, numberOfBlocks, ids.size)
val chunks = split(SortedSet.empty[ShortChannelId] ++ ids, firstBlockNum, numberOfBlocks, ids.size)
assert(chunks == ShortChannelIdsChunk(firstBlockNum, numberOfBlocks, ids) :: Nil)
}
@ -237,7 +238,7 @@ class ChannelRangeQueriesSpec extends FunSuite {
val ids = List(id(1000, 0), id(1000, 1), id(1000, 2), id(1000, 3), id(1000, 4), id(1000, 5))
val firstBlockNum = 900
val numberOfBlocks = 200
val chunks = Router.split(SortedSet.empty[ShortChannelId] ++ ids, firstBlockNum, numberOfBlocks, 2)
val chunks = split(SortedSet.empty[ShortChannelId] ++ ids, firstBlockNum, numberOfBlocks, 2)
assert(chunks == ShortChannelIdsChunk(firstBlockNum, numberOfBlocks, ids) :: Nil)
}
@ -246,7 +247,7 @@ class ChannelRangeQueriesSpec extends FunSuite {
val ids = List(id(1000), id(1005), id(1012), id(1013), id(1040), id(1050))
val firstBlockNum = 900
val numberOfBlocks = 200
val chunks = Router.split(SortedSet.empty[ShortChannelId] ++ ids, firstBlockNum, numberOfBlocks, 2)
val chunks = split(SortedSet.empty[ShortChannelId] ++ ids, firstBlockNum, numberOfBlocks, 2)
assert(chunks == List(
ShortChannelIdsChunk(firstBlockNum, 100 + 6, List(ids(0), ids(1))),
ShortChannelIdsChunk(1006, 8, List(ids(2), ids(3))),
@ -259,7 +260,7 @@ class ChannelRangeQueriesSpec extends FunSuite {
val ids = List(id(1000), id(1005), id(1012), id(1013), id(1040), id(1050))
val firstBlockNum = 1001
val numberOfBlocks = 200
val chunks = Router.split(SortedSet.empty[ShortChannelId] ++ ids, firstBlockNum, numberOfBlocks, 2)
val chunks = split(SortedSet.empty[ShortChannelId] ++ ids, firstBlockNum, numberOfBlocks, 2)
assert(chunks == List(
ShortChannelIdsChunk(firstBlockNum, 12, List(ids(1), ids(2))),
ShortChannelIdsChunk(1013, 1040 - 1013 + 1, List(ids(3), ids(4))),
@ -272,20 +273,20 @@ class ChannelRangeQueriesSpec extends FunSuite {
val ids = List(id(1000), id(1001), id(1002), id(1003), id(1004), id(1005))
val firstBlockNum = 900
val numberOfBlocks = 105
val chunks = Router.split(SortedSet.empty[ShortChannelId] ++ ids, firstBlockNum, numberOfBlocks, 2)
val chunks = split(SortedSet.empty[ShortChannelId] ++ ids, firstBlockNum, numberOfBlocks, 2)
assert(chunks == List(
ShortChannelIdsChunk(firstBlockNum, 100 + 2, List(ids(0), ids(1))),
ShortChannelIdsChunk(1002, 2, List(ids(2), ids(3))),
ShortChannelIdsChunk(1004, numberOfBlocks - 1004 + firstBlockNum, List(ids(4)))
))
}
}
// all ids in different blocks, chunk size == 2, first and last id outside of range
{
val ids = List(id(1000), id(1001), id(1002), id(1003), id(1004), id(1005))
val firstBlockNum = 1001
val numberOfBlocks = 4
val chunks = Router.split(SortedSet.empty[ShortChannelId] ++ ids, firstBlockNum, numberOfBlocks, 2)
val chunks = split(SortedSet.empty[ShortChannelId] ++ ids, firstBlockNum, numberOfBlocks, 2)
assert(chunks == List(
ShortChannelIdsChunk(firstBlockNum, 2, List(ids(1), ids(2))),
ShortChannelIdsChunk(1003, 2, List(ids(3), ids(4)))
@ -297,7 +298,7 @@ class ChannelRangeQueriesSpec extends FunSuite {
val ids = makeShortChannelIds(1000, 100)
val firstBlockNum = 900
val numberOfBlocks = 200
val chunks = Router.split(SortedSet.empty[ShortChannelId] ++ ids, firstBlockNum, numberOfBlocks, 10)
val chunks = split(SortedSet.empty[ShortChannelId] ++ ids, firstBlockNum, numberOfBlocks, 10)
assert(chunks == ShortChannelIdsChunk(firstBlockNum, numberOfBlocks, ids) :: Nil)
}
}
@ -307,11 +308,11 @@ class ChannelRangeQueriesSpec extends FunSuite {
val firstBlockNum = 0
val numberOfBlocks = 1000
validate(ids, firstBlockNum, numberOfBlocks, Router.split(ids, firstBlockNum, numberOfBlocks, 1))
validate(ids, firstBlockNum, numberOfBlocks, Router.split(ids, firstBlockNum, numberOfBlocks, 20))
validate(ids, firstBlockNum, numberOfBlocks, Router.split(ids, firstBlockNum, numberOfBlocks, 50))
validate(ids, firstBlockNum, numberOfBlocks, Router.split(ids, firstBlockNum, numberOfBlocks, 100))
validate(ids, firstBlockNum, numberOfBlocks, Router.split(ids, firstBlockNum, numberOfBlocks, 1000))
validate(ids, firstBlockNum, numberOfBlocks, split(ids, firstBlockNum, numberOfBlocks, 1))
validate(ids, firstBlockNum, numberOfBlocks, split(ids, firstBlockNum, numberOfBlocks, 20))
validate(ids, firstBlockNum, numberOfBlocks, split(ids, firstBlockNum, numberOfBlocks, 50))
validate(ids, firstBlockNum, numberOfBlocks, split(ids, firstBlockNum, numberOfBlocks, 100))
validate(ids, firstBlockNum, numberOfBlocks, split(ids, firstBlockNum, numberOfBlocks, 1000))
}
test("split short channel ids correctly (comprehensive tests)") {
@ -319,7 +320,7 @@ class ChannelRangeQueriesSpec extends FunSuite {
for (firstBlockNum <- 0 to 60) {
for (numberOfBlocks <- 1 to 60) {
for (chunkSize <- 1 :: 2 :: 20 :: 50 :: 100 :: 1000 :: Nil) {
validate(ids, firstBlockNum, numberOfBlocks, Router.split(ids, firstBlockNum, numberOfBlocks, chunkSize))
validate(ids, firstBlockNum, numberOfBlocks, split(ids, firstBlockNum, numberOfBlocks, chunkSize))
}
}
}
@ -327,11 +328,11 @@ class ChannelRangeQueriesSpec extends FunSuite {
test("enforce maximum size of short channel lists") {
def makeChunk(startBlock: Int, count : Int) = ShortChannelIdsChunk(startBlock, count, makeShortChannelIds(startBlock, count))
def makeChunk(startBlock: Int, count: Int) = ShortChannelIdsChunk(startBlock, count, makeShortChannelIds(startBlock, count))
def validate(before: ShortChannelIdsChunk, after: ShortChannelIdsChunk) = {
require(before.shortChannelIds.containsSlice(after.shortChannelIds))
require(after.shortChannelIds.size <= Router.MAXIMUM_CHUNK_SIZE)
require(after.shortChannelIds.size <= Sync.MAXIMUM_CHUNK_SIZE)
}
def validateChunks(before: List[ShortChannelIdsChunk], after: List[ShortChannelIdsChunk]): Unit = {
@ -341,40 +342,40 @@ class ChannelRangeQueriesSpec extends FunSuite {
// empty chunk
{
val chunks = makeChunk(0, 0) :: Nil
assert(Router.enforceMaximumSize(chunks) == chunks)
assert(enforceMaximumSize(chunks) == chunks)
}
// chunks are just below the limit
{
val chunks = makeChunk(0, Router.MAXIMUM_CHUNK_SIZE) :: makeChunk(Router.MAXIMUM_CHUNK_SIZE, Router.MAXIMUM_CHUNK_SIZE) :: Nil
assert(Router.enforceMaximumSize(chunks) == chunks)
val chunks = makeChunk(0, Sync.MAXIMUM_CHUNK_SIZE) :: makeChunk(Sync.MAXIMUM_CHUNK_SIZE, Sync.MAXIMUM_CHUNK_SIZE) :: Nil
assert(enforceMaximumSize(chunks) == chunks)
}
// fuzzy tests
{
val chunks = collection.mutable.ArrayBuffer.empty[ShortChannelIdsChunk]
// we select parameters to make sure that some chunks will have too many ids
for (i <- 0 until 100) chunks += makeChunk(0, Router.MAXIMUM_CHUNK_SIZE - 500 + Random.nextInt(1000))
val pruned = Router.enforceMaximumSize(chunks.toList)
for (i <- 0 until 100) chunks += makeChunk(0, Sync.MAXIMUM_CHUNK_SIZE - 500 + Random.nextInt(1000))
val pruned = enforceMaximumSize(chunks.toList)
validateChunks(chunks.toList, pruned)
}
}
test("do not encode empty lists as COMPRESSED_ZLIB") {
{
val reply = Router.buildReplyChannelRange(ShortChannelIdsChunk(0, 42, Nil), Block.RegtestGenesisBlock.hash, EncodingType.COMPRESSED_ZLIB, Some(QueryFlags(QueryFlags.WANT_ALL)), SortedMap())
val reply = buildReplyChannelRange(ShortChannelIdsChunk(0, 42, Nil), Block.RegtestGenesisBlock.hash, EncodingType.COMPRESSED_ZLIB, Some(QueryFlags(QueryFlags.WANT_ALL)), SortedMap())
assert(reply == ReplyChannelRange(Block.RegtestGenesisBlock.hash, 0L, 42L, 1.toByte, EncodedShortChannelIds(EncodingType.UNCOMPRESSED, Nil), Some(EncodedTimestamps(EncodingType.UNCOMPRESSED, Nil)), Some(EncodedChecksums(Nil))))
}
{
val reply = Router.buildReplyChannelRange(ShortChannelIdsChunk(0, 42, Nil), Block.RegtestGenesisBlock.hash, EncodingType.COMPRESSED_ZLIB, Some(QueryFlags(QueryFlags.WANT_TIMESTAMPS)), SortedMap())
val reply = buildReplyChannelRange(ShortChannelIdsChunk(0, 42, Nil), Block.RegtestGenesisBlock.hash, EncodingType.COMPRESSED_ZLIB, Some(QueryFlags(QueryFlags.WANT_TIMESTAMPS)), SortedMap())
assert(reply == ReplyChannelRange(Block.RegtestGenesisBlock.hash, 0L, 42L, 1.toByte, EncodedShortChannelIds(EncodingType.UNCOMPRESSED, Nil), Some(EncodedTimestamps(EncodingType.UNCOMPRESSED, Nil)), None))
}
{
val reply = Router.buildReplyChannelRange(ShortChannelIdsChunk(0, 42, Nil), Block.RegtestGenesisBlock.hash, EncodingType.COMPRESSED_ZLIB, Some(QueryFlags(QueryFlags.WANT_CHECKSUMS)), SortedMap())
val reply = buildReplyChannelRange(ShortChannelIdsChunk(0, 42, Nil), Block.RegtestGenesisBlock.hash, EncodingType.COMPRESSED_ZLIB, Some(QueryFlags(QueryFlags.WANT_CHECKSUMS)), SortedMap())
assert(reply == ReplyChannelRange(Block.RegtestGenesisBlock.hash, 0L, 42L, 1.toByte, EncodedShortChannelIds(EncodingType.UNCOMPRESSED, Nil), None, Some(EncodedChecksums(Nil))))
}
{
val reply = Router.buildReplyChannelRange(ShortChannelIdsChunk(0, 42, Nil), Block.RegtestGenesisBlock.hash, EncodingType.COMPRESSED_ZLIB, None, SortedMap())
val reply = buildReplyChannelRange(ShortChannelIdsChunk(0, 42, Nil), Block.RegtestGenesisBlock.hash, EncodingType.COMPRESSED_ZLIB, None, SortedMap())
assert(reply == ReplyChannelRange(Block.RegtestGenesisBlock.hash, 0L, 42L, 1.toByte, EncodedShortChannelIds(EncodingType.UNCOMPRESSED, Nil), None, None))
}
}

View file

@ -19,6 +19,7 @@ package fr.acinq.eclair.router
import fr.acinq.bitcoin.Crypto.PublicKey
import fr.acinq.eclair.router.Graph.GraphStructure.{DirectedGraph, GraphEdge}
import fr.acinq.eclair.router.RouteCalculationSpec._
import fr.acinq.eclair.router.Router.ChannelDesc
import fr.acinq.eclair.wire.ChannelUpdate
import fr.acinq.eclair.{LongToBtcAmount, ShortChannelId}
import org.scalatest.FunSuite

View file

@ -18,6 +18,7 @@ package fr.acinq.eclair.router
import fr.acinq.bitcoin.Crypto.PublicKey
import fr.acinq.bitcoin.Satoshi
import fr.acinq.eclair.router.Router.PublicChannel
import fr.acinq.eclair.wire.{ChannelAnnouncement, ChannelUpdate}
import fr.acinq.eclair.{CltvExpiryDelta, LongToBtcAmount, MilliSatoshi, ShortChannelId, randomBytes32, randomBytes64, randomKey}
import org.scalatest.FunSuite

View file

@ -17,11 +17,13 @@
package fr.acinq.eclair.router
import fr.acinq.bitcoin.Crypto.PublicKey
import fr.acinq.bitcoin.{Block, Btc, ByteVector32, ByteVector64, Satoshi}
import fr.acinq.bitcoin.{Block, ByteVector32, ByteVector64, Satoshi}
import fr.acinq.eclair.payment.PaymentRequest.ExtraHop
import fr.acinq.eclair.router.Graph.GraphStructure.DirectedGraph.graphEdgeToHop
import fr.acinq.eclair.router.Graph.GraphStructure.{DirectedGraph, GraphEdge}
import fr.acinq.eclair.router.Graph.{RichWeight, WeightRatios}
import fr.acinq.eclair.router.RouteCalculation._
import fr.acinq.eclair.router.Router._
import fr.acinq.eclair.transactions.Transactions
import fr.acinq.eclair.wire._
import fr.acinq.eclair.{CltvExpiryDelta, LongToBtcAmount, MilliSatoshi, ShortChannelId, ToMilliSatoshiConversion, randomKey}
@ -51,7 +53,7 @@ class RouteCalculationSpec extends FunSuite with ParallelTestExecution {
val g = makeGraph(updates)
val route = Router.findRoute(g, a, e, DEFAULT_AMOUNT_MSAT, numRoutes = 1, routeParams = DEFAULT_ROUTE_PARAMS, currentBlockHeight = 400000)
val route = findRoute(g, a, e, DEFAULT_AMOUNT_MSAT, numRoutes = 1, routeParams = DEFAULT_ROUTE_PARAMS, currentBlockHeight = 400000)
assert(route.map(hops2Ids) === Success(1 :: 2 :: 3 :: 4 :: Nil))
}
@ -74,7 +76,7 @@ class RouteCalculationSpec extends FunSuite with ParallelTestExecution {
val g = makeGraph(updates)
val route = Router.findRoute(g, a, e, DEFAULT_AMOUNT_MSAT, numRoutes = 1, routeParams = DEFAULT_ROUTE_PARAMS.copy(maxFeeBase = 1 msat), currentBlockHeight = 400000)
val route = findRoute(g, a, e, DEFAULT_AMOUNT_MSAT, numRoutes = 1, routeParams = DEFAULT_ROUTE_PARAMS.copy(maxFeeBase = 1 msat), currentBlockHeight = 400000)
assert(route.map(hops2Ids) === Success(1 :: 2 :: 3 :: 4 :: Nil))
}
@ -111,7 +113,7 @@ class RouteCalculationSpec extends FunSuite with ParallelTestExecution {
val graph = makeGraph(updates)
val Success(route) = Router.findRoute(graph, a, d, amount, numRoutes = 1, routeParams = DEFAULT_ROUTE_PARAMS, currentBlockHeight = 400000)
val Success(route) = findRoute(graph, a, d, amount, numRoutes = 1, routeParams = DEFAULT_ROUTE_PARAMS, currentBlockHeight = 400000)
val totalCost = Graph.pathWeight(hops2Edges(route), amount, isPartial = false, 0, None).cost
@ -122,7 +124,7 @@ class RouteCalculationSpec extends FunSuite with ParallelTestExecution {
val (desc, update) = makeUpdate(5L, e, f, feeBase = 1 msat, feeProportionalMillionth = 400, minHtlc = 0 msat, maxHtlc = Some(10005 msat))
val graph1 = graph.addEdge(desc, update)
val Success(route1) = Router.findRoute(graph1, a, d, amount, numRoutes = 1, routeParams = DEFAULT_ROUTE_PARAMS, currentBlockHeight = 400000)
val Success(route1) = findRoute(graph1, a, d, amount, numRoutes = 1, routeParams = DEFAULT_ROUTE_PARAMS, currentBlockHeight = 400000)
assert(hops2Ids(route1) === 1 :: 2 :: 3 :: Nil)
}
@ -137,7 +139,7 @@ class RouteCalculationSpec extends FunSuite with ParallelTestExecution {
).toMap
val g = makeGraph(updates)
val route = Router.findRoute(g, a, e, DEFAULT_AMOUNT_MSAT, numRoutes = 1, routeParams = DEFAULT_ROUTE_PARAMS, currentBlockHeight = 400000)
val route = findRoute(g, a, e, DEFAULT_AMOUNT_MSAT, numRoutes = 1, routeParams = DEFAULT_ROUTE_PARAMS, currentBlockHeight = 400000)
assert(route.map(hops2Ids) === Success(2 :: 5 :: Nil))
}
@ -152,11 +154,11 @@ class RouteCalculationSpec extends FunSuite with ParallelTestExecution {
val g = makeGraph(updates)
val route1 = Router.findRoute(g, a, e, DEFAULT_AMOUNT_MSAT, numRoutes = 1, routeParams = DEFAULT_ROUTE_PARAMS, currentBlockHeight = 400000)
val route1 = findRoute(g, a, e, DEFAULT_AMOUNT_MSAT, numRoutes = 1, routeParams = DEFAULT_ROUTE_PARAMS, currentBlockHeight = 400000)
assert(route1.map(hops2Ids) === Success(1 :: 2 :: 3 :: 4 :: Nil))
val graphWithRemovedEdge = g.removeEdge(ChannelDesc(ShortChannelId(3L), c, d))
val route2 = Router.findRoute(graphWithRemovedEdge, a, e, DEFAULT_AMOUNT_MSAT, numRoutes = 1, routeParams = DEFAULT_ROUTE_PARAMS, currentBlockHeight = 400000)
val route2 = findRoute(graphWithRemovedEdge, a, e, DEFAULT_AMOUNT_MSAT, numRoutes = 1, routeParams = DEFAULT_ROUTE_PARAMS, currentBlockHeight = 400000)
assert(route2.map(hops2Ids) === Failure(RouteNotFound))
}
@ -177,7 +179,7 @@ class RouteCalculationSpec extends FunSuite with ParallelTestExecution {
val graph = makeGraph(updates)
val route = Router.findRoute(graph, f, i, DEFAULT_AMOUNT_MSAT, numRoutes = 1, routeParams = DEFAULT_ROUTE_PARAMS, currentBlockHeight = 400000)
val route = findRoute(graph, f, i, DEFAULT_AMOUNT_MSAT, numRoutes = 1, routeParams = DEFAULT_ROUTE_PARAMS, currentBlockHeight = 400000)
assert(route.map(hops2Ids) === Success(4 :: 3 :: Nil))
}
@ -199,7 +201,7 @@ class RouteCalculationSpec extends FunSuite with ParallelTestExecution {
val graph = makeGraph(updates)
val route = Router.findRoute(graph, f, i, DEFAULT_AMOUNT_MSAT, numRoutes = 2, routeParams = DEFAULT_ROUTE_PARAMS, currentBlockHeight = 400000)
val route = findRoute(graph, f, i, DEFAULT_AMOUNT_MSAT, numRoutes = 2, routeParams = DEFAULT_ROUTE_PARAMS, currentBlockHeight = 400000)
assert(route.map(hops2Ids) === Success(4 :: Nil))
}
@ -220,7 +222,7 @@ class RouteCalculationSpec extends FunSuite with ParallelTestExecution {
val graph = makeGraph(updates)
val route = Router.findRoute(graph, f, i, DEFAULT_AMOUNT_MSAT, numRoutes = 1, routeParams = DEFAULT_ROUTE_PARAMS, currentBlockHeight = 400000)
val route = findRoute(graph, f, i, DEFAULT_AMOUNT_MSAT, numRoutes = 1, routeParams = DEFAULT_ROUTE_PARAMS, currentBlockHeight = 400000)
assert(route.map(hops2Ids) == Success(1 :: 2 :: 3 :: Nil))
}
@ -241,7 +243,7 @@ class RouteCalculationSpec extends FunSuite with ParallelTestExecution {
val graph = makeGraph(updates)
val route = Router.findRoute(graph, f, i, DEFAULT_AMOUNT_MSAT, numRoutes = 1, routeParams = DEFAULT_ROUTE_PARAMS, currentBlockHeight = 400000)
val route = findRoute(graph, f, i, DEFAULT_AMOUNT_MSAT, numRoutes = 1, routeParams = DEFAULT_ROUTE_PARAMS, currentBlockHeight = 400000)
assert(route.map(hops2Ids) === Failure(RouteNotFound))
}
@ -262,7 +264,7 @@ class RouteCalculationSpec extends FunSuite with ParallelTestExecution {
val graph = makeGraph(updates)
val route = Router.findRoute(graph, f, i, DEFAULT_AMOUNT_MSAT, numRoutes = 1, routeParams = DEFAULT_ROUTE_PARAMS, currentBlockHeight = 400000)
val route = findRoute(graph, f, i, DEFAULT_AMOUNT_MSAT, numRoutes = 1, routeParams = DEFAULT_ROUTE_PARAMS, currentBlockHeight = 400000)
assert(route.map(hops2Ids) === Success(1 :: 6 :: 3 :: Nil))
}
@ -277,7 +279,7 @@ class RouteCalculationSpec extends FunSuite with ParallelTestExecution {
val g = makeGraph(updates)
val route = Router.findRoute(g, a, e, DEFAULT_AMOUNT_MSAT, numRoutes = 1, routeParams = DEFAULT_ROUTE_PARAMS, currentBlockHeight = 400000)
val route = findRoute(g, a, e, DEFAULT_AMOUNT_MSAT, numRoutes = 1, routeParams = DEFAULT_ROUTE_PARAMS, currentBlockHeight = 400000)
assert(route.map(hops2Ids) === Success(1 :: 2 :: 3 :: 4 :: Nil))
}
@ -289,7 +291,7 @@ class RouteCalculationSpec extends FunSuite with ParallelTestExecution {
val g = makeGraph(updates)
val route = Router.findRoute(g, a, e, DEFAULT_AMOUNT_MSAT, numRoutes = 1, routeParams = DEFAULT_ROUTE_PARAMS, currentBlockHeight = 400000)
val route = findRoute(g, a, e, DEFAULT_AMOUNT_MSAT, numRoutes = 1, routeParams = DEFAULT_ROUTE_PARAMS, currentBlockHeight = 400000)
assert(route.map(hops2Ids) === Failure(RouteNotFound))
}
@ -302,7 +304,7 @@ class RouteCalculationSpec extends FunSuite with ParallelTestExecution {
val g = makeGraph(updates)
val route = Router.findRoute(g, a, e, DEFAULT_AMOUNT_MSAT, numRoutes = 1, routeParams = DEFAULT_ROUTE_PARAMS, currentBlockHeight = 400000)
val route = findRoute(g, a, e, DEFAULT_AMOUNT_MSAT, numRoutes = 1, routeParams = DEFAULT_ROUTE_PARAMS, currentBlockHeight = 400000)
assert(route.map(hops2Ids) === Failure(RouteNotFound))
}
@ -314,8 +316,8 @@ class RouteCalculationSpec extends FunSuite with ParallelTestExecution {
val g = makeGraph(updates).addVertex(a).addVertex(e)
assert(Router.findRoute(g, a, d, DEFAULT_AMOUNT_MSAT, numRoutes = 1, routeParams = DEFAULT_ROUTE_PARAMS, currentBlockHeight = 400000) === Failure(RouteNotFound))
assert(Router.findRoute(g, b, e, DEFAULT_AMOUNT_MSAT, numRoutes = 1, routeParams = DEFAULT_ROUTE_PARAMS, currentBlockHeight = 400000) === Failure(RouteNotFound))
assert(findRoute(g, a, d, DEFAULT_AMOUNT_MSAT, numRoutes = 1, routeParams = DEFAULT_ROUTE_PARAMS, currentBlockHeight = 400000) === Failure(RouteNotFound))
assert(findRoute(g, b, e, DEFAULT_AMOUNT_MSAT, numRoutes = 1, routeParams = DEFAULT_ROUTE_PARAMS, currentBlockHeight = 400000) === Failure(RouteNotFound))
}
test("route not found (amount too high OR too low)") {
@ -337,8 +339,8 @@ class RouteCalculationSpec extends FunSuite with ParallelTestExecution {
val g = makeGraph(updatesHi)
val g1 = makeGraph(updatesLo)
assert(Router.findRoute(g, a, d, highAmount, numRoutes = 1, routeParams = DEFAULT_ROUTE_PARAMS, currentBlockHeight = 400000) === Failure(RouteNotFound))
assert(Router.findRoute(g1, a, d, lowAmount, numRoutes = 1, routeParams = DEFAULT_ROUTE_PARAMS, currentBlockHeight = 400000) === Failure(RouteNotFound))
assert(findRoute(g, a, d, highAmount, numRoutes = 1, routeParams = DEFAULT_ROUTE_PARAMS, currentBlockHeight = 400000) === Failure(RouteNotFound))
assert(findRoute(g1, a, d, lowAmount, numRoutes = 1, routeParams = DEFAULT_ROUTE_PARAMS, currentBlockHeight = 400000) === Failure(RouteNotFound))
}
test("route to self") {
@ -350,7 +352,7 @@ class RouteCalculationSpec extends FunSuite with ParallelTestExecution {
val g = makeGraph(updates)
val route = Router.findRoute(g, a, a, DEFAULT_AMOUNT_MSAT, numRoutes = 1, routeParams = DEFAULT_ROUTE_PARAMS, currentBlockHeight = 400000)
val route = findRoute(g, a, a, DEFAULT_AMOUNT_MSAT, numRoutes = 1, routeParams = DEFAULT_ROUTE_PARAMS, currentBlockHeight = 400000)
assert(route.map(hops2Ids) === Failure(CannotRouteToSelf))
}
@ -364,7 +366,7 @@ class RouteCalculationSpec extends FunSuite with ParallelTestExecution {
val g = makeGraph(updates)
val route = Router.findRoute(g, a, b, DEFAULT_AMOUNT_MSAT, numRoutes = 1, routeParams = DEFAULT_ROUTE_PARAMS, currentBlockHeight = 400000)
val route = findRoute(g, a, b, DEFAULT_AMOUNT_MSAT, numRoutes = 1, routeParams = DEFAULT_ROUTE_PARAMS, currentBlockHeight = 400000)
assert(route.map(hops2Ids) === Success(1 :: Nil))
}
@ -380,10 +382,10 @@ class RouteCalculationSpec extends FunSuite with ParallelTestExecution {
val g = makeGraph(updates)
val route1 = Router.findRoute(g, a, e, DEFAULT_AMOUNT_MSAT, numRoutes = 1, routeParams = DEFAULT_ROUTE_PARAMS, currentBlockHeight = 400000)
val route1 = findRoute(g, a, e, DEFAULT_AMOUNT_MSAT, numRoutes = 1, routeParams = DEFAULT_ROUTE_PARAMS, currentBlockHeight = 400000)
assert(route1.map(hops2Ids) === Success(1 :: 2 :: 3 :: 4 :: Nil))
val route2 = Router.findRoute(g, e, a, DEFAULT_AMOUNT_MSAT, numRoutes = 1, routeParams = DEFAULT_ROUTE_PARAMS, currentBlockHeight = 400000)
val route2 = findRoute(g, e, a, DEFAULT_AMOUNT_MSAT, numRoutes = 1, routeParams = DEFAULT_ROUTE_PARAMS, currentBlockHeight = 400000)
assert(route2.map(hops2Ids) === Failure(RouteNotFound))
}
@ -412,7 +414,7 @@ class RouteCalculationSpec extends FunSuite with ParallelTestExecution {
val g = makeGraph(updates)
val hops = Router.findRoute(g, a, e, DEFAULT_AMOUNT_MSAT, numRoutes = 1, routeParams = DEFAULT_ROUTE_PARAMS, currentBlockHeight = 400000).get
val hops = findRoute(g, a, e, DEFAULT_AMOUNT_MSAT, numRoutes = 1, routeParams = DEFAULT_ROUTE_PARAMS, currentBlockHeight = 400000).get
assert(hops === ChannelHop(a, b, uab) :: ChannelHop(b, c, ubc) :: ChannelHop(c, d, ucd) :: ChannelHop(d, e, ude) :: Nil)
}
@ -431,7 +433,7 @@ class RouteCalculationSpec extends FunSuite with ParallelTestExecution {
val extraHops = extraHop1 :: extraHop2 :: extraHop3 :: extraHop4 :: Nil
val amount = 90000 sat // below RoutingHeuristics.CAPACITY_CHANNEL_LOW
val assistedChannels = Router.toAssistedChannels(extraHops, e, amount.toMilliSatoshi)
val assistedChannels = toAssistedChannels(extraHops, e, amount.toMilliSatoshi)
assert(assistedChannels(extraHop4.shortChannelId) === AssistedChannel(extraHop4, e, 100050.sat.toMilliSatoshi))
assert(assistedChannels(extraHop3.shortChannelId) === AssistedChannel(extraHop3, d, 100200.sat.toMilliSatoshi))
@ -449,7 +451,7 @@ class RouteCalculationSpec extends FunSuite with ParallelTestExecution {
val g = makeGraph(updates)
val route1 = Router.findRoute(g, a, e, DEFAULT_AMOUNT_MSAT, numRoutes = 1, ignoredEdges = Set(ChannelDesc(ShortChannelId(3L), c, d)), routeParams = DEFAULT_ROUTE_PARAMS, currentBlockHeight = 400000)
val route1 = findRoute(g, a, e, DEFAULT_AMOUNT_MSAT, numRoutes = 1, ignoredEdges = Set(ChannelDesc(ShortChannelId(3L), c, d)), routeParams = DEFAULT_ROUTE_PARAMS, currentBlockHeight = 400000)
assert(route1.map(hops2Ids) === Failure(RouteNotFound))
// verify that we left the graph untouched
@ -458,7 +460,7 @@ class RouteCalculationSpec extends FunSuite with ParallelTestExecution {
assert(g.containsVertex(d))
// make sure we can find a route if without the blacklist
val route2 = Router.findRoute(g, a, e, DEFAULT_AMOUNT_MSAT, numRoutes = 1, routeParams = DEFAULT_ROUTE_PARAMS, currentBlockHeight = 400000)
val route2 = findRoute(g, a, e, DEFAULT_AMOUNT_MSAT, numRoutes = 1, routeParams = DEFAULT_ROUTE_PARAMS, currentBlockHeight = 400000)
assert(route2.map(hops2Ids) === Success(1 :: 2 :: 3 :: 4 :: Nil))
}
@ -471,14 +473,14 @@ class RouteCalculationSpec extends FunSuite with ParallelTestExecution {
val g = makeGraph(updates)
val route = Router.findRoute(g, a, e, DEFAULT_AMOUNT_MSAT, numRoutes = 1, routeParams = DEFAULT_ROUTE_PARAMS, currentBlockHeight = 400000)
val route = findRoute(g, a, e, DEFAULT_AMOUNT_MSAT, numRoutes = 1, routeParams = DEFAULT_ROUTE_PARAMS, currentBlockHeight = 400000)
assert(route.map(hops2Ids) === Failure(RouteNotFound))
// now we add the missing edge to reach the destination
val (extraDesc, extraUpdate) = makeUpdate(4L, d, e, 5 msat, 5)
val extraGraphEdges = Set(GraphEdge(extraDesc, extraUpdate))
val route1 = Router.findRoute(g, a, e, DEFAULT_AMOUNT_MSAT, numRoutes = 1, extraEdges = extraGraphEdges, routeParams = DEFAULT_ROUTE_PARAMS, currentBlockHeight = 400000)
val route1 = findRoute(g, a, e, DEFAULT_AMOUNT_MSAT, numRoutes = 1, extraEdges = extraGraphEdges, routeParams = DEFAULT_ROUTE_PARAMS, currentBlockHeight = 400000)
assert(route1.map(hops2Ids) === Success(1 :: 2 :: 3 :: 4 :: Nil))
}
@ -492,7 +494,7 @@ class RouteCalculationSpec extends FunSuite with ParallelTestExecution {
val g = makeGraph(updates)
val route1 = Router.findRoute(g, a, e, DEFAULT_AMOUNT_MSAT, numRoutes = 1, routeParams = DEFAULT_ROUTE_PARAMS, currentBlockHeight = 400000)
val route1 = findRoute(g, a, e, DEFAULT_AMOUNT_MSAT, numRoutes = 1, routeParams = DEFAULT_ROUTE_PARAMS, currentBlockHeight = 400000)
assert(route1.map(hops2Ids) === Success(1 :: 2 :: 3 :: 4 :: Nil))
assert(route1.get(1).lastUpdate.feeBaseMsat === 10.msat)
@ -500,7 +502,7 @@ class RouteCalculationSpec extends FunSuite with ParallelTestExecution {
val extraGraphEdges = Set(GraphEdge(extraDesc, extraUpdate))
val route2 = Router.findRoute(g, a, e, DEFAULT_AMOUNT_MSAT, numRoutes = 1, extraEdges = extraGraphEdges, routeParams = DEFAULT_ROUTE_PARAMS, currentBlockHeight = 400000)
val route2 = findRoute(g, a, e, DEFAULT_AMOUNT_MSAT, numRoutes = 1, extraEdges = extraGraphEdges, routeParams = DEFAULT_ROUTE_PARAMS, currentBlockHeight = 400000)
assert(route2.map(hops2Ids) === Success(1 :: 2 :: 3 :: 4 :: Nil))
assert(route2.get(1).lastUpdate.feeBaseMsat === 5.msat)
}
@ -542,7 +544,7 @@ class RouteCalculationSpec extends FunSuite with ParallelTestExecution {
(shortChannelId, pc)
}
val ignored = Router.getIgnoredChannelDesc(publicChannels, ignoreNodes = Set(c, j, randomKey.publicKey))
val ignored = getIgnoredChannelDesc(publicChannels, ignoreNodes = Set(c, j, randomKey.publicKey))
assert(ignored.toSet.contains(ChannelDesc(ShortChannelId(2L), b, c)))
assert(ignored.toSet.contains(ChannelDesc(ShortChannelId(2L), c, b)))
@ -560,10 +562,10 @@ class RouteCalculationSpec extends FunSuite with ParallelTestExecution {
val g = makeGraph(updates)
assert(Router.findRoute(g, nodes(0), nodes(18), DEFAULT_AMOUNT_MSAT, numRoutes = 1, routeParams = DEFAULT_ROUTE_PARAMS, currentBlockHeight = 400000).map(hops2Ids) === Success(0 until 18))
assert(Router.findRoute(g, nodes(0), nodes(19), DEFAULT_AMOUNT_MSAT, numRoutes = 1, routeParams = DEFAULT_ROUTE_PARAMS, currentBlockHeight = 400000).map(hops2Ids) === Success(0 until 19))
assert(Router.findRoute(g, nodes(0), nodes(20), DEFAULT_AMOUNT_MSAT, numRoutes = 1, routeParams = DEFAULT_ROUTE_PARAMS, currentBlockHeight = 400000).map(hops2Ids) === Success(0 until 20))
assert(Router.findRoute(g, nodes(0), nodes(21), DEFAULT_AMOUNT_MSAT, numRoutes = 1, routeParams = DEFAULT_ROUTE_PARAMS, currentBlockHeight = 400000).map(hops2Ids) === Failure(RouteNotFound))
assert(findRoute(g, nodes(0), nodes(18), DEFAULT_AMOUNT_MSAT, numRoutes = 1, routeParams = DEFAULT_ROUTE_PARAMS, currentBlockHeight = 400000).map(hops2Ids) === Success(0 until 18))
assert(findRoute(g, nodes(0), nodes(19), DEFAULT_AMOUNT_MSAT, numRoutes = 1, routeParams = DEFAULT_ROUTE_PARAMS, currentBlockHeight = 400000).map(hops2Ids) === Success(0 until 19))
assert(findRoute(g, nodes(0), nodes(20), DEFAULT_AMOUNT_MSAT, numRoutes = 1, routeParams = DEFAULT_ROUTE_PARAMS, currentBlockHeight = 400000).map(hops2Ids) === Success(0 until 20))
assert(findRoute(g, nodes(0), nodes(21), DEFAULT_AMOUNT_MSAT, numRoutes = 1, routeParams = DEFAULT_ROUTE_PARAMS, currentBlockHeight = 400000).map(hops2Ids) === Failure(RouteNotFound))
}
test("ignore cheaper route when it has more than 20 hops") {
@ -579,7 +581,7 @@ class RouteCalculationSpec extends FunSuite with ParallelTestExecution {
val g = makeGraph(updates2)
val route = Router.findRoute(g, nodes(0), nodes(49), DEFAULT_AMOUNT_MSAT, numRoutes = 1, routeParams = DEFAULT_ROUTE_PARAMS, currentBlockHeight = 400000)
val route = findRoute(g, nodes(0), nodes(49), DEFAULT_AMOUNT_MSAT, numRoutes = 1, routeParams = DEFAULT_ROUTE_PARAMS, currentBlockHeight = 400000)
assert(route.map(hops2Ids) === Success(0 :: 1 :: 99 :: 48 :: Nil))
}
@ -595,7 +597,7 @@ class RouteCalculationSpec extends FunSuite with ParallelTestExecution {
makeUpdate(6, f, d, feeBase = 5 msat, 0, minHtlc = 0 msat, maxHtlc = None, CltvExpiryDelta(9))
).toMap)
val route = Router.findRoute(g, a, d, DEFAULT_AMOUNT_MSAT, numRoutes = 1, routeParams = DEFAULT_ROUTE_PARAMS.copy(routeMaxCltv = CltvExpiryDelta(28)), currentBlockHeight = 400000)
val route = findRoute(g, a, d, DEFAULT_AMOUNT_MSAT, numRoutes = 1, routeParams = DEFAULT_ROUTE_PARAMS.copy(routeMaxCltv = CltvExpiryDelta(28)), currentBlockHeight = 400000)
assert(route.map(hops2Ids) === Success(4 :: 5 :: 6 :: Nil))
}
@ -611,7 +613,7 @@ class RouteCalculationSpec extends FunSuite with ParallelTestExecution {
makeUpdate(6, b, f, feeBase = 5 msat, 0, minHtlc = 0 msat, maxHtlc = None, CltvExpiryDelta(9))
).toMap)
val route = Router.findRoute(g, a, f, DEFAULT_AMOUNT_MSAT, numRoutes = 1, routeParams = DEFAULT_ROUTE_PARAMS.copy(routeMaxLength = 3), currentBlockHeight = 400000)
val route = findRoute(g, a, f, DEFAULT_AMOUNT_MSAT, numRoutes = 1, routeParams = DEFAULT_ROUTE_PARAMS.copy(routeMaxLength = 3), currentBlockHeight = 400000)
assert(route.map(hops2Ids) === Success(1 :: 6 :: Nil))
}
@ -626,7 +628,7 @@ class RouteCalculationSpec extends FunSuite with ParallelTestExecution {
val g = makeGraph(updates)
val route1 = Router.findRoute(g, a, e, DEFAULT_AMOUNT_MSAT, numRoutes = 1, routeParams = DEFAULT_ROUTE_PARAMS, currentBlockHeight = 400000)
val route1 = findRoute(g, a, e, DEFAULT_AMOUNT_MSAT, numRoutes = 1, routeParams = DEFAULT_ROUTE_PARAMS, currentBlockHeight = 400000)
assert(route1.map(hops2Ids) === Success(1 :: 2 :: 4 :: 5 :: Nil))
}
@ -644,7 +646,7 @@ class RouteCalculationSpec extends FunSuite with ParallelTestExecution {
val g = makeGraph(updates)
val route1 = Router.findRoute(g, a, d, DEFAULT_AMOUNT_MSAT, numRoutes = 1, routeParams = DEFAULT_ROUTE_PARAMS, currentBlockHeight = 400000)
val route1 = findRoute(g, a, d, DEFAULT_AMOUNT_MSAT, numRoutes = 1, routeParams = DEFAULT_ROUTE_PARAMS, currentBlockHeight = 400000)
assert(route1.map(hops2Ids) === Success(1 :: 3 :: 5 :: Nil))
}
@ -772,7 +774,7 @@ class RouteCalculationSpec extends FunSuite with ParallelTestExecution {
makeUpdate(7L, e, c, feeBase = 9 msat, 0)
).toMap)
(for {_ <- 0 to 10} yield Router.findRoute(g, a, d, DEFAULT_AMOUNT_MSAT, numRoutes = 3, routeParams = strictFeeParams, currentBlockHeight = 400000)).map {
(for {_ <- 0 to 10} yield findRoute(g, a, d, DEFAULT_AMOUNT_MSAT, numRoutes = 3, routeParams = strictFeeParams, currentBlockHeight = 400000)).map {
case Failure(thr) => fail(thr)
case Success(someRoute) =>
@ -801,10 +803,10 @@ class RouteCalculationSpec extends FunSuite with ParallelTestExecution {
val g = makeGraph(updates)
val Success(routeFeeOptimized) = Router.findRoute(g, a, d, DEFAULT_AMOUNT_MSAT, numRoutes = 0, routeParams = DEFAULT_ROUTE_PARAMS, currentBlockHeight = 400000)
val Success(routeFeeOptimized) = findRoute(g, a, d, DEFAULT_AMOUNT_MSAT, numRoutes = 0, routeParams = DEFAULT_ROUTE_PARAMS, currentBlockHeight = 400000)
assert(hops2Nodes(routeFeeOptimized) === (a, b) :: (b, c) :: (c, d) :: Nil)
val Success(routeCltvOptimized) = Router.findRoute(g, a, d, DEFAULT_AMOUNT_MSAT, numRoutes = 0, routeParams = DEFAULT_ROUTE_PARAMS.copy(ratios = Some(WeightRatios(
val Success(routeCltvOptimized) = findRoute(g, a, d, DEFAULT_AMOUNT_MSAT, numRoutes = 0, routeParams = DEFAULT_ROUTE_PARAMS.copy(ratios = Some(WeightRatios(
cltvDeltaFactor = 1,
ageFactor = 0,
capacityFactor = 0
@ -812,7 +814,7 @@ class RouteCalculationSpec extends FunSuite with ParallelTestExecution {
assert(hops2Nodes(routeCltvOptimized) === (a, e) :: (e, f) :: (f, d) :: Nil)
val Success(routeCapacityOptimized) = Router.findRoute(g, a, d, DEFAULT_AMOUNT_MSAT, numRoutes = 0, routeParams = DEFAULT_ROUTE_PARAMS.copy(ratios = Some(WeightRatios(
val Success(routeCapacityOptimized) = findRoute(g, a, d, DEFAULT_AMOUNT_MSAT, numRoutes = 0, routeParams = DEFAULT_ROUTE_PARAMS.copy(ratios = Some(WeightRatios(
cltvDeltaFactor = 0,
ageFactor = 0,
capacityFactor = 1
@ -833,7 +835,7 @@ class RouteCalculationSpec extends FunSuite with ParallelTestExecution {
makeUpdateShort(ShortChannelId(s"${currentBlockHeight}x0x6"), f, d, feeBase = 1 msat, 0, minHtlc = 0 msat, maxHtlc = None, cltvDelta = CltvExpiryDelta(144))
).toMap)
val Success(routeScoreOptimized) = Router.findRoute(g, a, d, DEFAULT_AMOUNT_MSAT / 2, numRoutes = 1, routeParams = DEFAULT_ROUTE_PARAMS.copy(ratios = Some(WeightRatios(
val Success(routeScoreOptimized) = findRoute(g, a, d, DEFAULT_AMOUNT_MSAT / 2, numRoutes = 1, routeParams = DEFAULT_ROUTE_PARAMS.copy(ratios = Some(WeightRatios(
ageFactor = 0.33,
cltvDeltaFactor = 0.33,
capacityFactor = 0.33
@ -852,7 +854,7 @@ class RouteCalculationSpec extends FunSuite with ParallelTestExecution {
makeUpdateShort(ShortChannelId(s"0x0x6"), f, d, feeBase = 1 msat, 0, minHtlc = 0 msat, maxHtlc = None, cltvDelta = CltvExpiryDelta(12))
).toMap)
val Success(routeScoreOptimized) = Router.findRoute(g, a, d, DEFAULT_AMOUNT_MSAT, numRoutes = 1, routeParams = DEFAULT_ROUTE_PARAMS.copy(ratios = Some(WeightRatios(
val Success(routeScoreOptimized) = findRoute(g, a, d, DEFAULT_AMOUNT_MSAT, numRoutes = 1, routeParams = DEFAULT_ROUTE_PARAMS.copy(ratios = Some(WeightRatios(
ageFactor = 0.33,
cltvDeltaFactor = 0.33,
capacityFactor = 0.33
@ -873,7 +875,7 @@ class RouteCalculationSpec extends FunSuite with ParallelTestExecution {
makeUpdateShort(ShortChannelId(s"0x0x6"), f, d, feeBase = 1 msat, 0, minHtlc = 0 msat, maxHtlc = None, cltvDelta = CltvExpiryDelta(144))
).toMap)
val Success(routeScoreOptimized) = Router.findRoute(g, a, d, DEFAULT_AMOUNT_MSAT / 2, numRoutes = 1, routeParams = DEFAULT_ROUTE_PARAMS.copy(ratios = Some(WeightRatios(
val Success(routeScoreOptimized) = findRoute(g, a, d, DEFAULT_AMOUNT_MSAT / 2, numRoutes = 1, routeParams = DEFAULT_ROUTE_PARAMS.copy(ratios = Some(WeightRatios(
ageFactor = 0.33,
cltvDeltaFactor = 0.33,
capacityFactor = 0.33
@ -918,7 +920,7 @@ class RouteCalculationSpec extends FunSuite with ParallelTestExecution {
val targetNode = PublicKey(hex"024655b768ef40951b20053a5c4b951606d4d86085d51238f2c67c7dec29c792ca")
val amount = 351000 msat
val Success(route) = Router.findRoute(g, thisNode, targetNode, amount, 1, Set.empty, Set.empty, Set.empty, params, currentBlockHeight = 567634) // simulate mainnet block for heuristic
val Success(route) = findRoute(g, thisNode, targetNode, amount, 1, Set.empty, Set.empty, Set.empty, params, currentBlockHeight = 567634) // simulate mainnet block for heuristic
assert(route.size == 2)
assert(route.last.nextNodeId == targetNode)

View file

@ -25,12 +25,12 @@ import fr.acinq.eclair.blockchain._
import fr.acinq.eclair.channel.BITCOIN_FUNDING_EXTERNAL_CHANNEL_SPENT
import fr.acinq.eclair.crypto.TransportHandler
import fr.acinq.eclair.io.Peer.PeerRoutingMessage
import fr.acinq.eclair.io.PeerConnection.InvalidSignature
import fr.acinq.eclair.payment.PaymentRequest.ExtraHop
import fr.acinq.eclair.router.Announcements.makeChannelUpdate
import fr.acinq.eclair.router.Announcements.{makeChannelUpdate, makeNodeAnnouncement}
import fr.acinq.eclair.router.RouteCalculationSpec.DEFAULT_AMOUNT_MSAT
import fr.acinq.eclair.router.Router._
import fr.acinq.eclair.transactions.Scripts
import fr.acinq.eclair.wire.QueryShortChannelIds
import fr.acinq.eclair.wire.{Color, QueryShortChannelIds}
import fr.acinq.eclair.{CltvExpiryDelta, LongToBtcAmount, ShortChannelId, randomKey}
import scodec.bits._
@ -49,46 +49,212 @@ class RouterSpec extends BaseRouterSpec {
import fixture._
val eventListener = TestProbe()
system.eventStream.subscribe(eventListener.ref, classOf[NetworkEvent])
system.eventStream.subscribe(eventListener.ref, classOf[Rebroadcast])
val peerConnection = TestProbe()
val channelId_ac = ShortChannelId(420000, 5, 0)
val chan_ac = channelAnnouncement(channelId_ac, priv_a, priv_c, priv_funding_a, priv_funding_c)
val update_ac = makeChannelUpdate(Block.RegtestGenesisBlock.hash, priv_a, c, channelId_ac, CltvExpiryDelta(7), 0 msat, 766000 msat, 10, 500000000L msat)
// a-x will not be found
val priv_x = randomKey
val chan_ax = channelAnnouncement(ShortChannelId(42001), priv_a, priv_x, priv_funding_a, randomKey)
val update_ax = makeChannelUpdate(Block.RegtestGenesisBlock.hash, priv_a, priv_x.publicKey, chan_ax.shortChannelId, CltvExpiryDelta(7), 0 msat, 766000 msat, 10, 500000000L msat)
// a-y will have an invalid script
val priv_y = randomKey
val priv_funding_y = randomKey
val chan_ay = channelAnnouncement(ShortChannelId(42002), priv_a, priv_y, priv_funding_a, priv_funding_y)
val update_ay = makeChannelUpdate(Block.RegtestGenesisBlock.hash, priv_a, priv_y.publicKey, chan_ay.shortChannelId, CltvExpiryDelta(7), 0 msat, 766000 msat, 10, 500000000L msat)
// a-z will be spent
val priv_z = randomKey
val priv_funding_z = randomKey
val chan_az = channelAnnouncement(ShortChannelId(42003), priv_a, priv_z, priv_funding_a, priv_funding_z)
val update_az = makeChannelUpdate(Block.RegtestGenesisBlock.hash, priv_a, priv_z.publicKey, chan_az.shortChannelId, CltvExpiryDelta(7), 0 msat, 766000 msat, 10, 500000000L msat)
{
// 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, 500000000L msat)
val node_c = makeNodeAnnouncement(priv_c, "node-C", Color(123, 100, -40), Nil, hex"0200", timestamp = Platform.currentTime.milliseconds.toSeconds + 1)
router ! PeerRoutingMessage(peerConnection.ref, remoteNodeId, chan_ac)
peerConnection.expectNoMsg(100 millis) // we don't immediately acknowledge the announcement (back pressure)
watcher.expectMsg(ValidateRequest(chan_ac))
watcher.send(router, ValidateResult(chan_ac, Right(Transaction(version = 0, txIn = Nil, txOut = TxOut(1000000 sat, write(pay2wsh(Scripts.multiSig2of2(funding_a, funding_c)))) :: Nil, lockTime = 0), UtxoStatus.Unspent)))
peerConnection.expectMsg(TransportHandler.ReadAck(chan_ac))
peerConnection.expectMsg(GossipDecision.Accepted(chan_ac))
assert(peerConnection.sender() == router)
watcher.expectMsgType[WatchSpentBasic]
router ! PeerRoutingMessage(peerConnection.ref, remoteNodeId, update_ac)
peerConnection.expectMsg(TransportHandler.ReadAck(update_ac))
peerConnection.expectMsg(GossipDecision.Accepted(update_ac))
router ! PeerRoutingMessage(peerConnection.ref, remoteNodeId, node_c)
peerConnection.expectMsg(TransportHandler.ReadAck(node_c))
peerConnection.expectMsg(GossipDecision.Accepted(node_c))
eventListener.expectMsg(ChannelsDiscovered(SingleChannelDiscovered(chan_ac, 1000000 sat, None, None) :: Nil))
eventListener.expectMsg(ChannelUpdatesReceived(update_ac :: Nil))
eventListener.expectMsg(NodeUpdated(node_c))
peerConnection.expectNoMsg(100 millis)
eventListener.expectNoMsg(100 millis)
router ! Router.TickBroadcast
eventListener.expectMsgType[Rebroadcast]
}
router ! PeerRoutingMessage(null, remoteNodeId, chan_ac)
router ! PeerRoutingMessage(null, remoteNodeId, chan_ax)
router ! PeerRoutingMessage(null, remoteNodeId, chan_ay)
router ! PeerRoutingMessage(null, remoteNodeId, chan_az)
// router won't validate channels before it has a recent enough channel update
router ! PeerRoutingMessage(null, remoteNodeId, update_ac)
router ! PeerRoutingMessage(null, remoteNodeId, update_ax)
router ! PeerRoutingMessage(null, remoteNodeId, update_ay)
router ! PeerRoutingMessage(null, remoteNodeId, update_az)
watcher.expectMsg(ValidateRequest(chan_ac))
watcher.expectMsg(ValidateRequest(chan_ax))
watcher.expectMsg(ValidateRequest(chan_ay))
watcher.expectMsg(ValidateRequest(chan_az))
watcher.send(router, ValidateResult(chan_ac, Right(Transaction(version = 0, txIn = Nil, txOut = TxOut(1000000 sat, write(pay2wsh(Scripts.multiSig2of2(funding_a, funding_c)))) :: Nil, lockTime = 0), UtxoStatus.Unspent)))
watcher.send(router, ValidateResult(chan_ax, Left(new RuntimeException(s"funding tx not found"))))
watcher.send(router, ValidateResult(chan_ay, Right(Transaction(version = 0, txIn = Nil, txOut = TxOut(1000000 sat, write(pay2wsh(Scripts.multiSig2of2(funding_a, randomKey.publicKey)))) :: Nil, lockTime = 0), UtxoStatus.Unspent)))
watcher.send(router, ValidateResult(chan_az, Right(Transaction(version = 0, txIn = Nil, txOut = TxOut(1000000 sat, write(pay2wsh(Scripts.multiSig2of2(funding_a, priv_funding_z.publicKey)))) :: Nil, lockTime = 0), UtxoStatus.Spent(spendingTxConfirmed = true))))
watcher.expectMsgType[WatchSpentBasic]
watcher.expectNoMsg(1 second)
{
// valid channel announcement, stashing while validating channel announcement
val priv_u = randomKey
val priv_funding_u = randomKey
val chan_uc = channelAnnouncement(ShortChannelId(420000, 6, 0), priv_u, priv_c, priv_funding_u, priv_funding_c)
val update_uc = makeChannelUpdate(Block.RegtestGenesisBlock.hash, priv_u, c, chan_uc.shortChannelId, CltvExpiryDelta(7), 0 msat, 766000 msat, 10, 500000000L msat)
val node_u = makeNodeAnnouncement(priv_u, "node-U", Color(-120, -20, 60), Nil, hex"00")
router ! PeerRoutingMessage(peerConnection.ref, remoteNodeId, chan_uc)
peerConnection.expectNoMsg(200 millis) // we don't immediately acknowledge the announcement (back pressure)
watcher.expectMsg(ValidateRequest(chan_uc))
router ! PeerRoutingMessage(peerConnection.ref, remoteNodeId, update_uc)
peerConnection.expectMsg(TransportHandler.ReadAck(update_uc))
router ! PeerRoutingMessage(peerConnection.ref, remoteNodeId, node_u)
peerConnection.expectMsg(TransportHandler.ReadAck(node_u))
watcher.send(router, ValidateResult(chan_uc, Right(Transaction(version = 0, txIn = Nil, txOut = TxOut(2000000 sat, write(pay2wsh(Scripts.multiSig2of2(priv_funding_u.publicKey, funding_c)))) :: Nil, lockTime = 0), UtxoStatus.Unspent)))
peerConnection.expectMsg(TransportHandler.ReadAck(chan_uc))
peerConnection.expectMsg(GossipDecision.Accepted(chan_uc))
assert(peerConnection.sender() == router)
watcher.expectMsgType[WatchSpentBasic]
peerConnection.expectMsg(GossipDecision.Accepted(update_uc))
peerConnection.expectMsg(GossipDecision.Accepted(node_u))
eventListener.expectMsg(ChannelsDiscovered(SingleChannelDiscovered(chan_uc, 2000000 sat, None, None) :: Nil))
eventListener.expectMsg(ChannelUpdatesReceived(update_uc :: Nil))
eventListener.expectMsg(NodesDiscovered(node_u :: Nil))
peerConnection.expectNoMsg(100 millis)
eventListener.expectNoMsg(100 millis)
router ! Router.TickBroadcast
eventListener.expectMsgType[Rebroadcast]
}
{
// duplicates
router ! PeerRoutingMessage(peerConnection.ref, remoteNodeId, node_a)
peerConnection.expectMsg(TransportHandler.ReadAck(node_a))
peerConnection.expectMsg(GossipDecision.Duplicate(node_a))
router ! PeerRoutingMessage(peerConnection.ref, remoteNodeId, chan_ab)
peerConnection.expectMsg(TransportHandler.ReadAck(chan_ab))
peerConnection.expectMsg(GossipDecision.Duplicate(chan_ab))
router ! PeerRoutingMessage(peerConnection.ref, remoteNodeId, update_ab)
peerConnection.expectMsg(TransportHandler.ReadAck(update_ab))
peerConnection.expectMsg(GossipDecision.Duplicate(update_ab))
peerConnection.expectNoMsg(100 millis)
router ! Router.TickBroadcast
eventListener.expectNoMsg(100 millis)
}
{
// invalid signatures
val invalid_node_a = node_a.copy(timestamp = node_a.timestamp + 10)
val invalid_chan_a = channelAnnouncement(ShortChannelId(420000, 5, 1), priv_a, priv_c, priv_funding_a, priv_funding_c).copy(nodeId1 = randomKey.publicKey)
val invalid_update_ab = update_ab.copy(cltvExpiryDelta = CltvExpiryDelta(21), timestamp = update_ab.timestamp + 1)
router ! PeerRoutingMessage(peerConnection.ref, remoteNodeId, invalid_node_a)
peerConnection.expectMsg(TransportHandler.ReadAck(invalid_node_a))
peerConnection.expectMsg(GossipDecision.InvalidSignature(invalid_node_a))
router ! PeerRoutingMessage(peerConnection.ref, remoteNodeId, invalid_chan_a)
peerConnection.expectMsg(TransportHandler.ReadAck(invalid_chan_a))
peerConnection.expectMsg(GossipDecision.InvalidSignature(invalid_chan_a))
router ! PeerRoutingMessage(peerConnection.ref, remoteNodeId, invalid_update_ab)
peerConnection.expectMsg(TransportHandler.ReadAck(invalid_update_ab))
peerConnection.expectMsg(GossipDecision.InvalidSignature(invalid_update_ab))
peerConnection.expectNoMsg(100 millis)
router ! Router.TickBroadcast
eventListener.expectNoMsg(100 millis)
}
{
// pruned channel
val priv_v = randomKey
val priv_funding_v = randomKey
val chan_vc = channelAnnouncement(ShortChannelId(420000, 7, 0), priv_v, priv_c, priv_funding_v, priv_funding_c)
nodeParams.db.network.addToPruned(chan_vc.shortChannelId :: Nil)
router ! PeerRoutingMessage(peerConnection.ref, remoteNodeId, chan_vc)
peerConnection.expectMsg(TransportHandler.ReadAck(chan_vc))
peerConnection.expectMsg(GossipDecision.ChannelPruned(chan_vc))
peerConnection.expectNoMsg(100 millis)
router ! Router.TickBroadcast
eventListener.expectNoMsg(100 millis)
}
{
// 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, 500000000L msat, timestamp = (Platform.currentTime.milliseconds - 15.days).toSeconds)
router ! PeerRoutingMessage(peerConnection.ref, remoteNodeId, update_ab)
peerConnection.expectMsg(TransportHandler.ReadAck(update_ab))
peerConnection.expectMsg(GossipDecision.Stale(update_ab))
peerConnection.expectNoMsg(100 millis)
router ! Router.TickBroadcast
eventListener.expectNoMsg(100 millis)
}
{
// unknown channel
val priv_y = randomKey
val update_ay = makeChannelUpdate(Block.RegtestGenesisBlock.hash, priv_a, priv_y.publicKey, ShortChannelId(4646464), CltvExpiryDelta(7), 0 msat, 766000 msat, 10, 500000000L msat)
val node_y = makeNodeAnnouncement(priv_y, "node-Y", Color(123, 100, -40), Nil, hex"0200")
router ! PeerRoutingMessage(peerConnection.ref, remoteNodeId, update_ay)
peerConnection.expectMsg(TransportHandler.ReadAck(update_ay))
peerConnection.expectMsg(GossipDecision.NoRelatedChannel(update_ay))
router ! PeerRoutingMessage(peerConnection.ref, remoteNodeId, node_y)
peerConnection.expectMsg(TransportHandler.ReadAck(node_y))
peerConnection.expectMsg(GossipDecision.NoKnownChannel(node_y))
peerConnection.expectNoMsg(100 millis)
router ! Router.TickBroadcast
eventListener.expectNoMsg(100 millis)
}
{
// invalid announcement + reject stashed
val priv_y = randomKey
val priv_funding_y = randomKey // a-y will have an invalid script
val chan_ay = channelAnnouncement(ShortChannelId(42002), priv_a, priv_y, priv_funding_a, priv_funding_y)
val update_ay = makeChannelUpdate(Block.RegtestGenesisBlock.hash, priv_a, priv_y.publicKey, chan_ay.shortChannelId, CltvExpiryDelta(7), 0 msat, 766000 msat, 10, 500000000L msat)
val node_y = makeNodeAnnouncement(priv_y, "node-Y", Color(123, 100, -40), Nil, hex"0200")
router ! PeerRoutingMessage(peerConnection.ref, remoteNodeId, chan_ay)
watcher.expectMsg(ValidateRequest(chan_ay))
router ! PeerRoutingMessage(peerConnection.ref, remoteNodeId, update_ay)
peerConnection.expectMsg(TransportHandler.ReadAck(update_ay))
router ! PeerRoutingMessage(peerConnection.ref, remoteNodeId, node_y)
peerConnection.expectMsg(TransportHandler.ReadAck(node_y))
watcher.send(router, ValidateResult(chan_ay, Right(Transaction(version = 0, txIn = Nil, txOut = TxOut(1000000 sat, write(pay2wsh(Scripts.multiSig2of2(funding_a, randomKey.publicKey)))) :: Nil, lockTime = 0), UtxoStatus.Unspent)))
peerConnection.expectMsg(TransportHandler.ReadAck(chan_ay))
peerConnection.expectMsg(GossipDecision.InvalidAnnouncement(chan_ay))
peerConnection.expectMsg(GossipDecision.NoRelatedChannel(update_ay))
peerConnection.expectMsg(GossipDecision.NoKnownChannel(node_y))
peerConnection.expectNoMsg(100 millis)
router ! Router.TickBroadcast
eventListener.expectNoMsg(100 millis)
}
{
// validation failure
val priv_x = randomKey
val chan_ax = channelAnnouncement(ShortChannelId(42001), priv_a, priv_x, priv_funding_a, randomKey)
router ! PeerRoutingMessage(peerConnection.ref, remoteNodeId, chan_ax)
watcher.expectMsg(ValidateRequest(chan_ax))
watcher.send(router, ValidateResult(chan_ax, Left(new RuntimeException("funding tx not found"))))
peerConnection.expectMsg(TransportHandler.ReadAck(chan_ax))
peerConnection.expectMsg(GossipDecision.ValidationFailure(chan_ax))
peerConnection.expectNoMsg(100 millis)
router ! Router.TickBroadcast
eventListener.expectNoMsg(100 millis)
}
{
// funding tx spent (funding tx not confirmed)
val priv_z = randomKey
val priv_funding_z = randomKey
val chan_az = channelAnnouncement(ShortChannelId(42003), priv_a, priv_z, priv_funding_a, priv_funding_z)
router ! PeerRoutingMessage(peerConnection.ref, remoteNodeId, chan_az)
watcher.expectMsg(ValidateRequest(chan_az))
watcher.send(router, ValidateResult(chan_az, Right(Transaction(version = 0, txIn = Nil, txOut = TxOut(1000000 sat, write(pay2wsh(Scripts.multiSig2of2(funding_a, priv_funding_z.publicKey)))) :: Nil, lockTime = 0), UtxoStatus.Spent(spendingTxConfirmed = false))))
peerConnection.expectMsg(TransportHandler.ReadAck(chan_az))
peerConnection.expectMsg(GossipDecision.ChannelClosing(chan_az))
peerConnection.expectNoMsg(100 millis)
router ! Router.TickBroadcast
eventListener.expectNoMsg(100 millis)
}
{
// funding tx spent (funding tx confirmed)
val priv_z = randomKey
val priv_funding_z = randomKey
val chan_az = channelAnnouncement(ShortChannelId(42003), priv_a, priv_z, priv_funding_a, priv_funding_z)
router ! PeerRoutingMessage(peerConnection.ref, remoteNodeId, chan_az)
watcher.expectMsg(ValidateRequest(chan_az))
watcher.send(router, ValidateResult(chan_az, Right(Transaction(version = 0, txIn = Nil, txOut = TxOut(1000000 sat, write(pay2wsh(Scripts.multiSig2of2(funding_a, priv_funding_z.publicKey)))) :: Nil, lockTime = 0), UtxoStatus.Spent(spendingTxConfirmed = true))))
peerConnection.expectMsg(TransportHandler.ReadAck(chan_az))
peerConnection.expectMsg(GossipDecision.ChannelClosed(chan_az))
peerConnection.expectNoMsg(100 millis)
router ! Router.TickBroadcast
eventListener.expectNoMsg(100 millis)
}
watcher.expectNoMsg(100 millis)
eventListener.expectMsg(ChannelsDiscovered(SingleChannelDiscovered(chan_ac, 1000000 sat, None, None) :: Nil))
}
test("properly announce lost channels and nodes") { fixture =>
@ -122,27 +288,27 @@ class RouterSpec extends BaseRouterSpec {
val channelId_ac = ShortChannelId(420000, 5, 0)
val chan_ac = channelAnnouncement(channelId_ac, priv_a, priv_c, priv_funding_a, priv_funding_c)
val buggy_chan_ac = chan_ac.copy(nodeSignature1 = chan_ac.nodeSignature2)
sender.send(router, PeerRoutingMessage(null, remoteNodeId, buggy_chan_ac))
sender.send(router, PeerRoutingMessage(sender.ref, remoteNodeId, buggy_chan_ac))
sender.expectMsg(TransportHandler.ReadAck(buggy_chan_ac))
sender.expectMsg(InvalidSignature(buggy_chan_ac))
sender.expectMsg(GossipDecision.InvalidSignature(buggy_chan_ac))
}
test("handle bad signature for NodeAnnouncement") { fixture =>
import fixture._
val sender = TestProbe()
val buggy_ann_a = ann_a.copy(signature = ann_b.signature, timestamp = ann_a.timestamp + 1)
sender.send(router, PeerRoutingMessage(null, remoteNodeId, buggy_ann_a))
sender.expectMsg(TransportHandler.ReadAck(buggy_ann_a))
sender.expectMsg(InvalidSignature(buggy_ann_a))
val peerConnection = TestProbe()
val buggy_ann_a = node_a.copy(signature = node_b.signature, timestamp = node_a.timestamp + 1)
peerConnection.send(router, PeerRoutingMessage(peerConnection.ref, remoteNodeId, buggy_ann_a))
peerConnection.expectMsg(TransportHandler.ReadAck(buggy_ann_a))
peerConnection.expectMsg(GossipDecision.InvalidSignature(buggy_ann_a))
}
test("handle bad signature for ChannelUpdate") { fixture =>
import fixture._
val sender = TestProbe()
val buggy_channelUpdate_ab = channelUpdate_ab.copy(signature = ann_b.signature, timestamp = channelUpdate_ab.timestamp + 1)
sender.send(router, PeerRoutingMessage(null, remoteNodeId, buggy_channelUpdate_ab))
sender.expectMsg(TransportHandler.ReadAck(buggy_channelUpdate_ab))
sender.expectMsg(InvalidSignature(buggy_channelUpdate_ab))
val peerConnection = TestProbe()
val buggy_channelUpdate_ab = update_ab.copy(signature = node_b.signature, timestamp = update_ab.timestamp + 1)
peerConnection.send(router, PeerRoutingMessage(peerConnection.ref, remoteNodeId, buggy_channelUpdate_ab))
peerConnection.expectMsg(TransportHandler.ReadAck(buggy_channelUpdate_ab))
peerConnection.expectMsg(GossipDecision.InvalidSignature(buggy_channelUpdate_ab))
}
test("route not found (unreachable target)") { fixture =>
@ -196,14 +362,15 @@ class RouterSpec extends BaseRouterSpec {
test("route not found (channel disabled)") { fixture =>
import fixture._
val sender = TestProbe()
val peerConnection = TestProbe()
sender.send(router, RouteRequest(a, d, DEFAULT_AMOUNT_MSAT, routeParams = relaxedRouteParams))
val res = sender.expectMsgType[RouteResponse]
assert(res.hops.map(_.nodeId).toList === a :: b :: c :: Nil)
assert(res.hops.last.nextNodeId === d)
val channelUpdate_cd1 = makeChannelUpdate(Block.RegtestGenesisBlock.hash, priv_c, d, channelId_cd, CltvExpiryDelta(3), 0 msat, 153000 msat, 4, 500000000L msat, enable = false)
sender.send(router, PeerRoutingMessage(null, remoteNodeId, channelUpdate_cd1))
sender.expectMsg(TransportHandler.ReadAck(channelUpdate_cd1))
peerConnection.send(router, PeerRoutingMessage(peerConnection.ref, remoteNodeId, channelUpdate_cd1))
peerConnection.expectMsg(TransportHandler.ReadAck(channelUpdate_cd1))
sender.send(router, RouteRequest(a, d, DEFAULT_AMOUNT_MSAT, routeParams = relaxedRouteParams))
sender.expectMsg(Failure(RouteNotFound))
}
@ -271,7 +438,7 @@ class RouterSpec extends BaseRouterSpec {
// the route hasn't changed (nodes are the same)
assert(response.hops.map(_.nodeId).toList == preComputedRoute.dropRight(1).toList)
assert(response.hops.last.nextNodeId == preComputedRoute.last)
assert(response.hops.map(_.lastUpdate).toList == List(channelUpdate_ab, channelUpdate_bc, channelUpdate_cd))
assert(response.hops.map(_.lastUpdate).toList == List(update_ab, update_bc, update_cd))
}
test("ask for channels that we marked as stale for which we receive a new update") { fixture =>
@ -283,9 +450,9 @@ class RouterSpec extends BaseRouterSpec {
val update = makeChannelUpdate(Block.RegtestGenesisBlock.hash, priv_a, c, channelId, CltvExpiryDelta(7), 0 msat, 766000 msat, 10, 5 msat, timestamp = timestamp)
val probe = TestProbe()
probe.ignoreMsg { case _: TransportHandler.ReadAck => true }
probe.send(router, PeerRoutingMessage(null, remoteNodeId, announcement))
probe.send(router, PeerRoutingMessage(probe.ref, remoteNodeId, announcement))
watcher.expectMsgType[ValidateRequest]
probe.send(router, PeerRoutingMessage(null, remoteNodeId, update))
probe.send(router, PeerRoutingMessage(probe.ref, remoteNodeId, update))
watcher.send(router, ValidateResult(announcement, Right((Transaction(version = 0, txIn = Nil, txOut = TxOut(1000000 sat, write(pay2wsh(Scripts.multiSig2of2(funding_a, funding_c)))) :: Nil, lockTime = 0), UtxoStatus.Unspent))))
probe.send(router, TickPruneStaleChannels)
@ -296,9 +463,11 @@ class RouterSpec extends BaseRouterSpec {
val update1 = makeChannelUpdate(Block.RegtestGenesisBlock.hash, priv_a, c, channelId, CltvExpiryDelta(7), 0 msat, 766000 msat, 10, 500000000L msat, timestamp = Platform.currentTime.millisecond.toSeconds)
// we want to make sure that transport receives the query
val transport = TestProbe()
probe.send(router, PeerRoutingMessage(transport.ref, remoteNodeId, update1))
val query = transport.expectMsgType[QueryShortChannelIds]
val peerConnection = TestProbe()
peerConnection.ignoreMsg { case _: GossipDecision.Duplicate => true }
probe.send(router, PeerRoutingMessage(peerConnection.ref, remoteNodeId, update1))
peerConnection.expectMsg(TransportHandler.ReadAck(update1))
val query = peerConnection.expectMsgType[QueryShortChannelIds]
assert(query.shortChannelIds.array == List(channelId))
}

View file

@ -27,6 +27,8 @@ import fr.acinq.eclair.crypto.TransportHandler
import fr.acinq.eclair.io.Peer.PeerRoutingMessage
import fr.acinq.eclair.router.Announcements.{makeChannelUpdate, makeNodeAnnouncement}
import fr.acinq.eclair.router.BaseRouterSpec.channelAnnouncement
import fr.acinq.eclair.router.Router.{Data, GossipDecision, PublicChannel, SendChannelQuery, State}
import fr.acinq.eclair.router.Sync._
import fr.acinq.eclair.transactions.Scripts
import fr.acinq.eclair.wire._
import org.scalatest.{FunSuiteLike, ParallelTestExecution}
@ -77,10 +79,12 @@ class RoutingSyncSpec extends TestKit(ActorSystem("test")) with FunSuiteLike wit
pipe.ignoreMsg {
case _: TransportHandler.ReadAck => true
case _: GossipTimestampFilter => true
case _: GossipDecision.Duplicate => true
case _: GossipDecision.Accepted => true
}
val srcId = src.underlyingActor.nodeParams.nodeId
val tgtId = tgt.underlyingActor.nodeParams.nodeId
sender.send(src, SendChannelQuery(tgtId, pipe.ref, extendedQueryFlags_opt))
sender.send(src, SendChannelQuery(src.underlyingActor.nodeParams.chainHash, tgtId, pipe.ref, extendedQueryFlags_opt))
// src sends a query_channel_range to bob
val qcr = pipe.expectMsgType[QueryChannelRange]
pipe.send(tgt, PeerRoutingMessage(pipe.ref, srcId, qcr))
@ -253,7 +257,7 @@ class RoutingSyncSpec extends TestKit(ActorSystem("test")) with FunSuiteLike wit
val remoteNodeId = TestConstants.Bob.nodeParams.nodeId
// ask router to send a channel range query
sender.send(router, SendChannelQuery(remoteNodeId, sender.ref, None))
sender.send(router, SendChannelQuery(params.chainHash, remoteNodeId, sender.ref, None))
val QueryChannelRange(chainHash, firstBlockNum, numberOfBlocks, _) = sender.expectMsgType[QueryChannelRange]
sender.expectMsgType[GossipTimestampFilter]
@ -269,7 +273,7 @@ class RoutingSyncSpec extends TestKit(ActorSystem("test")) with FunSuiteLike wit
assert(sync.total == 1)
// simulate a re-connection
sender.send(router, SendChannelQuery(remoteNodeId, sender.ref, None))
sender.send(router, SendChannelQuery(params.chainHash, remoteNodeId, sender.ref, None))
sender.expectMsgType[QueryChannelRange]
sender.expectMsgType[GossipTimestampFilter]
assert(router.stateData.sync.get(remoteNodeId).isEmpty)
@ -282,17 +286,17 @@ class RoutingSyncSpec extends TestKit(ActorSystem("test")) with FunSuiteLike wit
val nodeidA = randomKey.publicKey
val nodeidB = randomKey.publicKey
val (sync1, _) = Router.addToSync(Map.empty, nodeidA, List(req, req, req, req))
assert(Router.syncProgress(sync1) == SyncProgress(0.25D))
val (sync1, _) = addToSync(Map.empty, nodeidA, List(req, req, req, req))
assert(syncProgress(sync1) == SyncProgress(0.25D))
val (sync2, _) = Router.addToSync(sync1, nodeidB, List(req, req, req, req, req, req, req, req, req, req, req, req))
assert(Router.syncProgress(sync2) == SyncProgress(0.125D))
val (sync2, _) = addToSync(sync1, nodeidB, List(req, req, req, req, req, req, req, req, req, req, req, req))
assert(syncProgress(sync2) == SyncProgress(0.125D))
// let's assume we made some progress
val sync3 = sync2
.updated(nodeidA, sync2(nodeidA).copy(pending = List(req)))
.updated(nodeidB, sync2(nodeidB).copy(pending = List(req)))
assert(Router.syncProgress(sync3) == SyncProgress(0.875D))
assert(syncProgress(sync3) == SyncProgress(0.875D))
}
}

View file

@ -17,7 +17,7 @@
package fr.acinq.eclair.wire
import fr.acinq.bitcoin.{Block, ByteVector32, ByteVector64}
import fr.acinq.eclair.router.Router
import fr.acinq.eclair.router.Sync
import fr.acinq.eclair.wire.LightningMessageCodecs._
import fr.acinq.eclair.wire.ReplyChannelRangeTlv._
import fr.acinq.eclair.{CltvExpiryDelta, LongToBtcAmount, ShortChannelId, UInt64}
@ -163,7 +163,7 @@ class ExtendedQueriesCodecsSpec extends FunSuite {
val check = ByteVector.fromValidHex("010276df7e70c63cc2b63ef1c062b99c6d934a80ef2fd4dae9e1d86d277f47674af3255a97fa52ade7f129263f591ed784996eba6383135896cc117a438c8029328206226e46111a0b59caaf126043eb5bbf28c34f3a5e332a1fc7b2b73cf188910f00006700000100005d50f933000000900000000000000000000003e80000000a")
assert(LightningMessageCodecs.channelUpdateCodec.encode(update).require.bytes == check.drop(2))
val checksum = Router.getChecksum(update)
val checksum = Sync.getChecksum(update)
assert(checksum == 0x1112fa30L)
}
@ -184,7 +184,7 @@ class ExtendedQueriesCodecsSpec extends FunSuite {
val check = ByteVector.fromValidHex("010206737e9e18d3e4d0ab4066ccaecdcc10e648c5f1c5413f1610747e0d463fa7fa39c1b02ea2fd694275ecfefe4fe9631f24afd182ab75b805e16cd550941f858c06226e46111a0b59caaf126043eb5bbf28c34f3a5e332a1fc7b2b73cf188910f00006d00000100005d50f935010000300000000000000000000000640000000b00000000000186a0")
assert(LightningMessageCodecs.channelUpdateCodec.encode(update).require.bytes == check.drop(2))
val checksum = Router.getChecksum(update)
val checksum = Sync.getChecksum(update)
assert(checksum == 0xf32ce968L)
}
}

View file

@ -27,7 +27,8 @@ import fr.acinq.eclair.blockchain.electrum.ElectrumClient.{ElectrumDisconnected,
import fr.acinq.eclair.channel._
import fr.acinq.eclair.gui.controllers._
import fr.acinq.eclair.payment._
import fr.acinq.eclair.router.{NORMAL => _, _}
import fr.acinq.eclair.router.{Announcements, ChannelLost, ChannelUpdatesReceived, ChannelsDiscovered, NodeLost, NodeUpdated, NodesDiscovered, SingleChannelDiscovered}
import fr.acinq.eclair.router.Router.{NORMAL => _, _}
import javafx.application.Platform
import javafx.fxml.FXMLLoader
import javafx.scene.layout.VBox

View file

@ -27,13 +27,12 @@ import fr.acinq.eclair.channel.{ChannelCommandResponse, ChannelVersion, State}
import fr.acinq.eclair.crypto.ShaChain
import fr.acinq.eclair.db.{IncomingPaymentStatus, OutgoingPaymentStatus}
import fr.acinq.eclair.payment._
import fr.acinq.eclair.router.RouteResponse
import fr.acinq.eclair.router.Router.RouteResponse
import fr.acinq.eclair.transactions.DirectedHtlc
import fr.acinq.eclair.transactions.Transactions.{InputInfo, TransactionWithInputInfo}
import fr.acinq.eclair.wire._
import fr.acinq.eclair.{CltvExpiry, CltvExpiryDelta, MilliSatoshi, ShortChannelId, UInt64}
import org.json4s.JsonAST._
import org.json4s.jackson.Serialization
import org.json4s.{CustomKeySerializer, CustomSerializer, DefaultFormats, Extraction, TypeHints, jackson}
import scodec.bits.ByteVector