Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Parametric route search #844

Merged
merged 24 commits into from
Feb 11, 2019
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
24 commits
Select commit Hold shift + click to select a range
98deed9
Avoid making a copy of all the vertices in the graph in dijkstra
araspitzu Jan 30, 2019
96f66e0
Implement boundaries for graph searching with cost, cltv, and size
araspitzu Jan 30, 2019
ead28db
Enable searching for routes with size/CLTV/fee limits
araspitzu Jan 31, 2019
6559f21
expose the RouteParams in RouteRequest
araspitzu Jan 31, 2019
2ae43f5
Expose the RouteParams in SendPayment
araspitzu Jan 31, 2019
0ac2538
Rename DEFAULT_ROUTE_MAX_LENGTH
araspitzu Jan 31, 2019
2a55963
Use relaxed params for route request in integration test
araspitzu Jan 31, 2019
df77fee
If we couldn't find a route on the first attempt, retry relaxing the …
araspitzu Jan 31, 2019
ac272b7
Merge branch 'master' into route_fast_searching
araspitzu Jan 31, 2019
09aa95c
Relax maxFeePct in route request during RouterSpec
araspitzu Jan 31, 2019
05b5015
Avoid returning an empty path, collapse the route not found cases int…
araspitzu Feb 6, 2019
bfb9df3
When retrying to search for a route, relax 'maxCltv'
araspitzu Feb 6, 2019
7ac15a0
Merge branch 'master' into route_fast_searching
araspitzu Feb 6, 2019
4487b0a
Finish merging master
araspitzu Feb 6, 2019
4f7efc3
Move the default params for route searching in the conf, refactor tog…
araspitzu Feb 6, 2019
61e7174
Remove max-payment-fee in favor of router.search-max-fee-pct
araspitzu Feb 8, 2019
f95ce08
Group search params configurations into a block
araspitzu Feb 8, 2019
582f2f7
Add comments
araspitzu Feb 8, 2019
e4e69f4
Rename ROUTE_MAX_LENGTH
araspitzu Feb 8, 2019
daee9fe
Add formatter commands for tighter formatting
araspitzu Feb 8, 2019
faffac3
Merge branch 'master' into route_fast_searching
araspitzu Feb 8, 2019
c9f93e2
Rename config keys for router path-finding
araspitzu Feb 11, 2019
f0ac922
Add new config keys to IntegrationSpec conf
araspitzu Feb 11, 2019
db5f709
Formatting
araspitzu Feb 11, 2019
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
9 changes: 8 additions & 1 deletion eclair-core/src/main/resources/reference.conf
Original file line number Diff line number Diff line change
Expand Up @@ -87,7 +87,6 @@ eclair {
payment-handler = "local"
payment-request-expiry = 1 hour // default expiry for payment requests generated by this node
max-pending-payment-requests = 10000000
max-payment-fee = 0.03 // max total fee for outgoing payments, in percentage: sending a payment will not be attempted if the cheapest route found is more expensive than that
min-funding-satoshis = 100000

autoprobe-count = 0 // number of parallel tasks that send test payments to detect invalid channels
Expand All @@ -97,6 +96,14 @@ eclair {
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

// the values below will be used to perform route searching
path-finding {
max-route-length = 6 // max route length for the 'first pass', if none is found then a second pass is made with no limit
max-cltv = 1008 // max acceptable cltv expiry for the payment (1008 ~ 1 week)
fee-threshold-sat = 21 // if fee is below this value we skip the max-fee-pct check
max-fee-pct = 0.03 // route will be discarded if fee is above this value (in percentage relative to the total payment amount); doesn't apply if fee < fee-threshold-sat
}
}

socks5 {
Expand Down
21 changes: 12 additions & 9 deletions eclair-core/src/main/scala/fr/acinq/eclair/NodeParams.scala
Original file line number Diff line number Diff line change
Expand Up @@ -33,7 +33,8 @@ import fr.acinq.eclair.db._
import fr.acinq.eclair.db.sqlite._
import fr.acinq.eclair.tor.Socks5ProxyParams
import fr.acinq.eclair.wire.{Color, NodeAddress}

import fr.acinq.eclair.router.RouterConf
import fr.acinq.eclair.wire.Color
import scala.collection.JavaConversions._
import scala.concurrent.duration.FiniteDuration

Expand Down Expand Up @@ -67,7 +68,6 @@ case class NodeParams(keyManager: KeyManager,
paymentsDb: PaymentsDb,
auditDb: AuditDb,
revocationTimeout: FiniteDuration,
routerBroadcastInterval: FiniteDuration,
pingInterval: FiniteDuration,
pingTimeout: FiniteDuration,
pingDisconnect: Boolean,
Expand All @@ -76,13 +76,11 @@ case class NodeParams(keyManager: KeyManager,
autoReconnect: Boolean,
chainHash: BinaryData,
channelFlags: Byte,
channelExcludeDuration: FiniteDuration,
watcherType: WatcherType,
paymentRequestExpiry: FiniteDuration,
maxPendingPaymentRequests: Int,
maxPaymentFee: Double,
minFundingSatoshis: Long,
randomizeRouteSelection: Boolean,
routerConf: RouterConf,
socksProxy_opt: Option[Socks5ProxyParams]) {

val privateKey = keyManager.nodeKey.privateKey
Expand Down Expand Up @@ -229,7 +227,6 @@ 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),
pingInterval = FiniteDuration(config.getDuration("ping-interval").getSeconds, TimeUnit.SECONDS),
pingTimeout = FiniteDuration(config.getDuration("ping-timeout").getSeconds, TimeUnit.SECONDS),
pingDisconnect = config.getBoolean("ping-disconnect"),
Expand All @@ -238,13 +235,19 @@ object NodeParams {
autoReconnect = config.getBoolean("auto-reconnect"),
chainHash = chainHash,
channelFlags = config.getInt("channel-flags").toByte,
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"),
randomizeRouteSelection = config.getBoolean("router.randomize-route-selection"),
routerConf = RouterConf(
channelExcludeDuration = FiniteDuration(config.getDuration("router.channel-exclude-duration").getSeconds, TimeUnit.SECONDS),
routerBroadcastInterval = FiniteDuration(config.getDuration("router.broadcast-interval").getSeconds, TimeUnit.SECONDS),
randomizeRouteSelection = config.getBoolean("router.randomize-route-selection"),
searchMaxRouteLength = config.getInt("router.path-finding.max-route-length"),
searchMaxCltv = config.getInt("router.path-finding.max-cltv"),
searchMaxFeeBaseSat = config.getLong("router.path-finding.fee-threshold-sat"),
searchMaxFeePct = config.getDouble("router.path-finding.route-max-fee-pct")
),
socksProxy_opt = socksProxy_opt
)
}
Expand Down
6 changes: 3 additions & 3 deletions eclair-core/src/main/scala/fr/acinq/eclair/api/Service.scala
Original file line number Diff line number Diff line change
Expand Up @@ -266,7 +266,7 @@ trait Service extends Logging {
case JInt(amountMsat) :: JString(paymentHash) :: JString(nodeId) :: Nil =>
(Try(BinaryData(paymentHash)), Try(PublicKey(nodeId))) match {
case (Success(ph), Success(pk)) => completeRpcFuture(req.id, (paymentInitiator ?
SendPayment(amountMsat.toLong, ph, pk, maxFeePct = nodeParams.maxPaymentFee)).mapTo[PaymentResult].map {
SendPayment(amountMsat.toLong, ph, pk)).mapTo[PaymentResult].map {
case s: PaymentSucceeded => s
case f: PaymentFailed => f.copy(failures = PaymentLifecycle.transformForUser(f.failures))
})
Expand All @@ -286,8 +286,8 @@ trait Service extends Logging {
logger.debug(s"api call for sending payment with amount_msat=$amount_msat")
// optional cltv expiry
val sendPayment = pr.minFinalCltvExpiry match {
case None => SendPayment(amount_msat, pr.paymentHash, pr.nodeId, maxFeePct = nodeParams.maxPaymentFee)
case Some(minFinalCltvExpiry) => SendPayment(amount_msat, pr.paymentHash, pr.nodeId, assistedRoutes = Nil, minFinalCltvExpiry, maxFeePct = nodeParams.maxPaymentFee)
case None => SendPayment(amount_msat, pr.paymentHash, pr.nodeId)
case Some(minFinalCltvExpiry) => SendPayment(amount_msat, pr.paymentHash, pr.nodeId, assistedRoutes = Nil, minFinalCltvExpiry)
}
completeRpcFuture(req.id, (paymentInitiator ? sendPayment).mapTo[PaymentResult].map {
case s: PaymentSucceeded => s
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -43,7 +43,7 @@ class PaymentLifecycle(sourceNodeId: PublicKey, router: ActorRef, register: Acto

when(WAITING_FOR_REQUEST) {
case Event(c: SendPayment, WaitingForRequest) =>
router ! RouteRequest(sourceNodeId, c.targetNodeId, c.amountMsat, c.assistedRoutes, randomize = c.randomize)
router ! RouteRequest(sourceNodeId, c.targetNodeId, c.amountMsat, c.assistedRoutes, randomize = c.randomize, routeParams = c.routeParams)
goto(WAITING_FOR_ROUTE) using WaitingForRoute(sender, c, failures = Nil)
}

Expand All @@ -55,15 +55,8 @@ class PaymentLifecycle(sourceNodeId: PublicKey, router: ActorRef, register: Acto
val finalExpiry = Globals.blockCount.get().toInt + c.finalCltvExpiry.toInt + 1

val (cmd, sharedSecrets) = buildCommand(c.amountMsat, finalExpiry, c.paymentHash, hops)
val feePct = (cmd.amountMsat - c.amountMsat) / c.amountMsat.toDouble // c.amountMsat is required to be > 0, have to convert to double, otherwise will be rounded
if (feePct > c.maxFeePct) {
log.info(s"cheapest route found is too expensive: feePct=$feePct maxFeePct=${c.maxFeePct}")
reply(s, PaymentFailed(c.paymentHash, failures = failures :+ LocalFailure(RouteTooExpensive(feePct, c.maxFeePct))))
stop(FSM.Normal)
} else {
register ! Register.ForwardShortId(firstHop.lastUpdate.shortChannelId, cmd)
goto(WAITING_FOR_PAYMENT_COMPLETE) using WaitingForComplete(s, c, cmd, failures, sharedSecrets, ignoreNodes, ignoreChannels, hops)
}
register ! Register.ForwardShortId(firstHop.lastUpdate.shortChannelId, cmd)
goto(WAITING_FOR_PAYMENT_COMPLETE) using WaitingForComplete(s, c, cmd, failures, sharedSecrets, ignoreNodes, ignoreChannels, hops)

case Event(Status.Failure(t), WaitingForRoute(s, c, failures)) =>
reply(s, PaymentFailed(c.paymentHash, failures = failures :+ LocalFailure(t)))
Expand Down Expand Up @@ -103,12 +96,12 @@ class PaymentLifecycle(sourceNodeId: PublicKey, router: ActorRef, register: Acto
// in that case we don't know which node is sending garbage, let's try to blacklist all nodes except the one we are directly connected to and the destination node
val blacklist = hops.map(_.nextNodeId).drop(1).dropRight(1)
log.warning(s"blacklisting intermediate nodes=${blacklist.mkString(",")}")
router ! RouteRequest(sourceNodeId, c.targetNodeId, c.amountMsat, c.assistedRoutes, ignoreNodes ++ blacklist, ignoreChannels, c.randomize)
router ! RouteRequest(sourceNodeId, c.targetNodeId, c.amountMsat, c.assistedRoutes, ignoreNodes ++ blacklist, ignoreChannels, c.randomize, c.routeParams)
goto(WAITING_FOR_ROUTE) using WaitingForRoute(s, c, failures :+ UnreadableRemoteFailure(hops))
case Success(e@ErrorPacket(nodeId, failureMessage: Node)) =>
log.info(s"received 'Node' type error message from nodeId=$nodeId, trying to route around it (failure=$failureMessage)")
// let's try to route around this node
router ! RouteRequest(sourceNodeId, c.targetNodeId, c.amountMsat, c.assistedRoutes, ignoreNodes + nodeId, ignoreChannels, c.randomize)
router ! RouteRequest(sourceNodeId, c.targetNodeId, c.amountMsat, c.assistedRoutes, ignoreNodes + nodeId, ignoreChannels, c.randomize, c.routeParams)
goto(WAITING_FOR_ROUTE) using WaitingForRoute(s, c, failures :+ RemoteFailure(hops, e))
case Success(e@ErrorPacket(nodeId, failureMessage: Update)) =>
log.info(s"received 'Update' type error message from nodeId=$nodeId, retrying payment (failure=$failureMessage)")
Expand Down Expand Up @@ -136,18 +129,18 @@ class PaymentLifecycle(sourceNodeId: PublicKey, router: ActorRef, register: Acto
// in any case, we forward the update to the router
router ! failureMessage.update
// let's try again, router will have updated its state
router ! RouteRequest(sourceNodeId, c.targetNodeId, c.amountMsat, c.assistedRoutes, ignoreNodes, ignoreChannels, c.randomize)
router ! RouteRequest(sourceNodeId, c.targetNodeId, c.amountMsat, c.assistedRoutes, ignoreNodes, ignoreChannels, c.randomize, c.routeParams)
} else {
// this node is fishy, it gave us a bad sig!! let's filter it out
log.warning(s"got bad signature from node=$nodeId update=${failureMessage.update}")
router ! RouteRequest(sourceNodeId, c.targetNodeId, c.amountMsat, c.assistedRoutes, ignoreNodes + nodeId, ignoreChannels, c.randomize)
router ! RouteRequest(sourceNodeId, c.targetNodeId, c.amountMsat, c.assistedRoutes, ignoreNodes + nodeId, ignoreChannels, c.randomize, c.routeParams)
}
goto(WAITING_FOR_ROUTE) using WaitingForRoute(s, c, failures :+ RemoteFailure(hops, e))
case Success(e@ErrorPacket(nodeId, failureMessage)) =>
log.info(s"received an error message from nodeId=$nodeId, trying to use a different channel (failure=$failureMessage)")
// let's try again without the channel outgoing from nodeId
val faultyChannel = hops.find(_.nodeId == nodeId).map(hop => ChannelDesc(hop.lastUpdate.shortChannelId, hop.nodeId, hop.nextNodeId))
router ! RouteRequest(sourceNodeId, c.targetNodeId, c.amountMsat, c.assistedRoutes, ignoreNodes, ignoreChannels ++ faultyChannel.toSet, c.randomize)
router ! RouteRequest(sourceNodeId, c.targetNodeId, c.amountMsat, c.assistedRoutes, ignoreNodes, ignoreChannels ++ faultyChannel.toSet, c.randomize, c.routeParams)
goto(WAITING_FOR_ROUTE) using WaitingForRoute(s, c, failures :+ RemoteFailure(hops, e))
}

Expand All @@ -166,7 +159,7 @@ class PaymentLifecycle(sourceNodeId: PublicKey, router: ActorRef, register: Acto
} else {
log.info(s"received an error message from local, trying to use a different channel (failure=${t.getMessage})")
val faultyChannel = ChannelDesc(hops.head.lastUpdate.shortChannelId, hops.head.nodeId, hops.head.nextNodeId)
router ! RouteRequest(sourceNodeId, c.targetNodeId, c.amountMsat, c.assistedRoutes, ignoreNodes, ignoreChannels + faultyChannel, c.randomize)
router ! RouteRequest(sourceNodeId, c.targetNodeId, c.amountMsat, c.assistedRoutes, ignoreNodes, ignoreChannels + faultyChannel, c.randomize, c.routeParams)
goto(WAITING_FOR_ROUTE) using WaitingForRoute(s, c, failures :+ LocalFailure(t))
}

Expand All @@ -193,7 +186,14 @@ 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: Option[Boolean] = None) {
case class SendPayment(amountMsat: Long,
paymentHash: BinaryData,
targetNodeId: PublicKey,
assistedRoutes: Seq[Seq[ExtraHop]] = Nil,
finalCltvExpiry: Long = Channel.MIN_CLTV_EXPIRY,
maxAttempts: Int = 5,
randomize: Option[Boolean] = None,
routeParams: Option[RouteParams] = None) {
require(amountMsat > 0, s"amountMsat must be > 0")
}
case class CheckPayment(paymentHash: BinaryData)
Expand All @@ -216,10 +216,6 @@ object PaymentLifecycle {
case object WAITING_FOR_ROUTE extends State
case object WAITING_FOR_PAYMENT_COMPLETE extends State

val percentageFormatter = NumberFormat.getPercentInstance(Locale.US) // force US locale to always get "fee=0.272% max=0.1%" (otherwise depending on locale it can be "fee=0,272 % max=0,1 %")
percentageFormatter.setMaximumFractionDigits(3)
case class RouteTooExpensive(feePct: Double, maxFeePct: Double) extends RuntimeException(s"cheapest route found is too expensive: fee=${percentageFormatter.format(feePct)} max=${percentageFormatter.format(maxFeePct)}")

// @formatter:on


Expand Down
Loading