diff --git a/eclair-core/src/main/resources/reference.conf b/eclair-core/src/main/resources/reference.conf index ce80c4838..56327417c 100644 --- a/eclair-core/src/main/resources/reference.conf +++ b/eclair-core/src/main/resources/reference.conf @@ -79,11 +79,6 @@ eclair { revocation-timeout = 20 seconds // after sending a commit_sig, we will wait for at most that duration before disconnecting - channel-exclude-duration = 60 seconds // when a temporary channel failure is returned, we exclude the channel from our payment routes for this duration - router-broadcast-interval = 60 seconds // see BOLT #7 - - router-init-timeout = 5 minutes - ping-interval = 30 seconds ping-timeout = 10 seconds // will disconnect if peer takes longer than that to respond ping-disconnect = true // disconnect if no answer to our pings @@ -96,4 +91,11 @@ eclair { min-funding-satoshis = 100000 autoprobe-count = 0 // number of parallel tasks that send test payments to detect invalid channels + + router { + randomize-route-selection = true // when computing a route for a payment we randomize the final selection + channel-exclude-duration = 60 seconds // when a temporary channel failure is returned, we exclude the channel from our payment routes for this duration + broadcast-interval = 60 seconds // see BOLT #7 + init-timeout = 5 minutes + } } diff --git a/eclair-core/src/main/scala/fr/acinq/eclair/NodeParams.scala b/eclair-core/src/main/scala/fr/acinq/eclair/NodeParams.scala index a73558332..2210213a6 100644 --- a/eclair-core/src/main/scala/fr/acinq/eclair/NodeParams.scala +++ b/eclair-core/src/main/scala/fr/acinq/eclair/NodeParams.scala @@ -80,7 +80,9 @@ case class NodeParams(keyManager: KeyManager, paymentRequestExpiry: FiniteDuration, maxPendingPaymentRequests: Int, maxPaymentFee: Double, - minFundingSatoshis: Long) { + minFundingSatoshis: Long, + randomizeRouteSelection: Boolean + ) { val privateKey = keyManager.nodeKey.privateKey val nodeId = keyManager.nodeId @@ -209,7 +211,7 @@ object NodeParams { paymentsDb = paymentsDb, auditDb = auditDb, revocationTimeout = FiniteDuration(config.getDuration("revocation-timeout").getSeconds, TimeUnit.SECONDS), - routerBroadcastInterval = FiniteDuration(config.getDuration("router-broadcast-interval").getSeconds, TimeUnit.SECONDS), + routerBroadcastInterval = FiniteDuration(config.getDuration("router.broadcast-interval").getSeconds, TimeUnit.SECONDS), pingInterval = FiniteDuration(config.getDuration("ping-interval").getSeconds, TimeUnit.SECONDS), pingTimeout = FiniteDuration(config.getDuration("ping-timeout").getSeconds, TimeUnit.SECONDS), pingDisconnect = config.getBoolean("ping-disconnect"), @@ -218,12 +220,13 @@ object NodeParams { autoReconnect = config.getBoolean("auto-reconnect"), chainHash = chainHash, channelFlags = config.getInt("channel-flags").toByte, - channelExcludeDuration = FiniteDuration(config.getDuration("channel-exclude-duration").getSeconds, TimeUnit.SECONDS), + channelExcludeDuration = FiniteDuration(config.getDuration("router.channel-exclude-duration").getSeconds, TimeUnit.SECONDS), watcherType = watcherType, paymentRequestExpiry = FiniteDuration(config.getDuration("payment-request-expiry").getSeconds, TimeUnit.SECONDS), maxPendingPaymentRequests = config.getInt("max-pending-payment-requests"), maxPaymentFee = config.getDouble("max-payment-fee"), - minFundingSatoshis = config.getLong("min-funding-satoshis") + minFundingSatoshis = config.getLong("min-funding-satoshis"), + randomizeRouteSelection = config.getBoolean("router.randomize-route-selection") ) } } diff --git a/eclair-core/src/main/scala/fr/acinq/eclair/Setup.scala b/eclair-core/src/main/scala/fr/acinq/eclair/Setup.scala index 972fcac65..9e2cc8860 100644 --- a/eclair-core/src/main/scala/fr/acinq/eclair/Setup.scala +++ b/eclair-core/src/main/scala/fr/acinq/eclair/Setup.scala @@ -201,7 +201,7 @@ class Setup(datadir: File, } router = system.actorOf(SimpleSupervisor.props(Router.props(nodeParams, watcher, Some(routerInitialized)), "router", SupervisorStrategy.Resume)) - routerTimeout = after(FiniteDuration(config.getDuration("router-init-timeout").getSeconds, TimeUnit.SECONDS), using = system.scheduler)(Future.failed(new RuntimeException("Router initialization timed out"))) + routerTimeout = after(FiniteDuration(config.getDuration("router.init-timeout").getSeconds, TimeUnit.SECONDS), using = system.scheduler)(Future.failed(new RuntimeException("Router initialization timed out"))) _ <- Future.firstCompletedOf(routerInitialized.future :: routerTimeout :: Nil) wallet = bitcoin match { diff --git a/eclair-core/src/main/scala/fr/acinq/eclair/payment/PaymentLifecycle.scala b/eclair-core/src/main/scala/fr/acinq/eclair/payment/PaymentLifecycle.scala index f7e7985f8..f15f257f4 100644 --- a/eclair-core/src/main/scala/fr/acinq/eclair/payment/PaymentLifecycle.scala +++ b/eclair-core/src/main/scala/fr/acinq/eclair/payment/PaymentLifecycle.scala @@ -193,7 +193,7 @@ object PaymentLifecycle { /** * @param maxFeePct set by default to 3% as a safety measure (even if a route is found, if fee is higher than that payment won't be attempted) */ - case class SendPayment(amountMsat: Long, paymentHash: BinaryData, targetNodeId: PublicKey, assistedRoutes: Seq[Seq[ExtraHop]] = Nil, finalCltvExpiry: Long = Channel.MIN_CLTV_EXPIRY, maxAttempts: Int = 5, maxFeePct: Double = 0.03, randomize: Boolean = true) { + case class SendPayment(amountMsat: Long, paymentHash: BinaryData, targetNodeId: PublicKey, assistedRoutes: Seq[Seq[ExtraHop]] = Nil, finalCltvExpiry: Long = Channel.MIN_CLTV_EXPIRY, maxAttempts: Int = 5, maxFeePct: Double = 0.03, randomize: Option[Boolean] = None) { require(amountMsat > 0, s"amountMsat must be > 0") } case class CheckPayment(paymentHash: BinaryData) diff --git a/eclair-core/src/main/scala/fr/acinq/eclair/router/Router.scala b/eclair-core/src/main/scala/fr/acinq/eclair/router/Router.scala index 175f9234f..72822f6fb 100644 --- a/eclair-core/src/main/scala/fr/acinq/eclair/router/Router.scala +++ b/eclair-core/src/main/scala/fr/acinq/eclair/router/Router.scala @@ -46,7 +46,7 @@ import scala.util.{Random, Try} case class ChannelDesc(shortChannelId: ShortChannelId, a: PublicKey, b: PublicKey) case class Hop(nodeId: PublicKey, nextNodeId: PublicKey, lastUpdate: ChannelUpdate) -case class RouteRequest(source: PublicKey, target: PublicKey, amountMsat: Long, assistedRoutes: Seq[Seq[ExtraHop]] = Nil, ignoreNodes: Set[PublicKey] = Set.empty, ignoreChannels: Set[ChannelDesc] = Set.empty, randomize: Boolean = true) +case class RouteRequest(source: PublicKey, target: PublicKey, amountMsat: Long, assistedRoutes: Seq[Seq[ExtraHop]] = Nil, ignoreNodes: Set[PublicKey] = Set.empty, ignoreChannels: Set[ChannelDesc] = Set.empty, randomize: Option[Boolean] = None) case class RouteResponse(hops: Seq[Hop], ignoreNodes: Set[PublicKey], ignoreChannels: Set[ChannelDesc]) { require(hops.size > 0, "route cannot be empty") } @@ -384,7 +384,7 @@ class Router(nodeParams: NodeParams, watcher: ActorRef, initialized: Option[Prom log.info(s"finding a route $start->$end with assistedChannels={} ignoreNodes={} ignoreChannels={} excludedChannels={}", assistedUpdates.keys.mkString(","), ignoreNodes.map(_.toBin).mkString(","), ignoreChannels.mkString(","), d.excludedChannels.mkString(",")) val extraEdges = assistedUpdates.map { case (c, u) => GraphEdge(c, u) }.toSet // if we want to randomize we ask the router to make a random selection among the three best routes - val routesToFind = if(randomize) DEFAULT_ROUTES_COUNT else 1 + val routesToFind = if(randomize.getOrElse(nodeParams.randomizeRouteSelection)) DEFAULT_ROUTES_COUNT else 1 findRoute(d.graph, start, end, amount, numRoutes = routesToFind, extraEdges = extraEdges, ignoredEdges = ignoredUpdates.toSet) .map(r => sender ! RouteResponse(r, ignoreNodes, ignoreChannels)) .recover { case t => sender ! Status.Failure(t) } diff --git a/eclair-core/src/test/scala/fr/acinq/eclair/TestConstants.scala b/eclair-core/src/test/scala/fr/acinq/eclair/TestConstants.scala index 8167d5178..7fabbae7a 100644 --- a/eclair-core/src/test/scala/fr/acinq/eclair/TestConstants.scala +++ b/eclair-core/src/test/scala/fr/acinq/eclair/TestConstants.scala @@ -86,7 +86,9 @@ object TestConstants { paymentRequestExpiry = 1 hour, maxPendingPaymentRequests = 10000000, maxPaymentFee = 0.03, - minFundingSatoshis = 1000L) + minFundingSatoshis = 1000L, + randomizeRouteSelection = true + ) def channelParams = Peer.makeChannelParams( nodeParams = nodeParams, @@ -145,7 +147,8 @@ object TestConstants { paymentRequestExpiry = 1 hour, maxPendingPaymentRequests = 10000000, maxPaymentFee = 0.03, - minFundingSatoshis = 1000L) + minFundingSatoshis = 1000L, + randomizeRouteSelection = true) def channelParams = Peer.makeChannelParams( nodeParams = nodeParams, diff --git a/eclair-core/src/test/scala/fr/acinq/eclair/integration/IntegrationSpec.scala b/eclair-core/src/test/scala/fr/acinq/eclair/integration/IntegrationSpec.scala index b97fee1aa..7236b138b 100644 --- a/eclair-core/src/test/scala/fr/acinq/eclair/integration/IntegrationSpec.scala +++ b/eclair-core/src/test/scala/fr/acinq/eclair/integration/IntegrationSpec.scala @@ -251,7 +251,7 @@ class IntegrationSpec extends TestKit(ActorSystem("test")) with BitcoindService sender.send(nodes("D").paymentHandler, ReceivePayment(Some(amountMsat), "1 coffee")) val pr = sender.expectMsgType[PaymentRequest] // then we make the actual payment, do not randomize the route to make sure we route through node B - val sendReq = SendPayment(amountMsat.amount, pr.paymentHash, nodes("D").nodeParams.nodeId, randomize = false) + val sendReq = SendPayment(amountMsat.amount, pr.paymentHash, nodes("D").nodeParams.nodeId, randomize = Some(false)) sender.send(nodes("A").paymentInitiator, sendReq) // A will receive an error from B that include the updated channel update, then will retry the payment sender.expectMsgType[PaymentSucceeded](5 seconds)