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

Send to route #952

Merged
merged 32 commits into from May 20, 2019
Merged
Show file tree
Hide file tree
Changes from 26 commits
Commits
Show all changes
32 commits
Select commit Hold shift + click to select a range
59b8803
Use test key manager in flaky test in paymentlifecyclespec
araspitzu Apr 17, 2019
9f3e6b0
Add /sendtoroute API and functionality
araspitzu Apr 17, 2019
e723066
Use local random pamentHash for each test in paymentlifecyclespec, us…
araspitzu Apr 17, 2019
07dab5e
Merge branch 'fix_flaky_paymentlifecyclespec' into send_to_route
araspitzu Apr 17, 2019
6383690
Increase fixture setup timeout
araspitzu Apr 17, 2019
ef75c66
! Execute only PaymentLifecycleSpec on travis !
araspitzu Apr 17, 2019
d7a85af
WIP fix flaky test
araspitzu Apr 17, 2019
84ad29f
Postpone check for PENDING payment state in payment lifecycle spec
araspitzu Apr 23, 2019
ec18724
Merge branch 'fix_flaky_paymentlifecyclespec' into send_to_route
araspitzu Apr 23, 2019
39bc8d5
Revert "Execute only PaymentLifecycleSpec on travis"
araspitzu Apr 23, 2019
e7f5423
Merge branch 'fix_flaky_paymentlifecyclespec' into send_to_route
araspitzu Apr 23, 2019
4cdd736
Revert "Execute only PaymentLifecycleSpec on travis"
araspitzu Apr 23, 2019
be56cba
Merge branch 'fix_flaky_paymentlifecyclespec' into send_to_route
araspitzu Apr 23, 2019
9b94099
Renaming, reorg endpoint order in service
araspitzu Apr 23, 2019
584cf7f
Split test in payment lifecycle spec
araspitzu Apr 23, 2019
39a64d5
Merge branch 'master' into send_to_route
araspitzu Apr 23, 2019
3452c81
Finish merging master
araspitzu Apr 23, 2019
5db5b93
Use local random pamentHash for each test in paymentlifecyclespec, us…
araspitzu Apr 17, 2019
f3c270d
Merge branch 'fix_flaky_paymentlifecyclespec' into send_to_route
araspitzu Apr 25, 2019
38f3e35
Merge branch 'master' into send_to_route
araspitzu May 6, 2019
aae2171
Finish merging master
araspitzu May 6, 2019
5557989
Update eclair-core/src/main/scala/fr/acinq/eclair/payment/PaymentInit…
araspitzu May 13, 2019
8b0e022
Update eclair-core/src/main/scala/fr/acinq/eclair/payment/PaymentInit…
araspitzu May 13, 2019
3584079
Remove unused imports
araspitzu May 13, 2019
52e4938
Do not use extractor pattern in PaymentLifecycle::SendPaymentToRoute
araspitzu May 13, 2019
44acb29
Renaming PartialRouteRequest -> FinalizeRoute
araspitzu May 13, 2019
2cd06c3
Update eclair-core/src/main/scala/fr/acinq/eclair/router/Router.scala
araspitzu May 17, 2019
3aa79f7
Remove unnecessary changes, update year in copyright header
araspitzu May 17, 2019
468ec83
Merge branch 'master' into send_to_route
araspitzu May 17, 2019
9bf8f2e
/sendtoroute: support route parameter as comma separated list
araspitzu May 20, 2019
1d819a0
Merge branch 'master' into send_to_route
araspitzu May 20, 2019
00aa6d7
Add test for 'sendtoroute' in EclairImplSpec
araspitzu May 20, 2019
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
6 changes: 6 additions & 0 deletions eclair-core/src/main/scala/fr/acinq/eclair/Eclair.scala
Expand Up @@ -67,6 +67,8 @@ trait Eclair {

def findRoute(targetNodeId: PublicKey, amountMsat: Long, assistedRoutes: Seq[Seq[PaymentRequest.ExtraHop]] = Seq.empty)(implicit timeout: Timeout): Future[RouteResponse]

def sendToRoute(route: Seq[PublicKey], amountMsat: Long, paymentHash: ByteVector32, finalCltvExpiry: Long)(implicit timeout: Timeout): Future[UUID]

def audit(from_opt: Option[Long], to_opt: Option[Long])(implicit timeout: Timeout): Future[AuditResponse]

def networkFees(from_opt: Option[Long], to_opt: Option[Long])(implicit timeout: Timeout): Future[Seq[NetworkFee]]
Expand Down Expand Up @@ -161,6 +163,10 @@ class EclairImpl(appKit: Kit) extends Eclair {
(appKit.router ? RouteRequest(appKit.nodeParams.nodeId, targetNodeId, amountMsat, assistedRoutes)).mapTo[RouteResponse]
}

override def sendToRoute(route: Seq[PublicKey], amountMsat: Long, paymentHash: ByteVector32, finalCltvExpiry: Long)(implicit timeout: Timeout): Future[UUID] = {
(appKit.paymentInitiator ? SendPaymentToRoute(amountMsat, paymentHash, route, finalCltvExpiry)).mapTo[UUID]
}

override def send(recipientNodeId: PublicKey, amountMsat: Long, paymentHash: ByteVector32, assistedRoutes: Seq[Seq[PaymentRequest.ExtraHop]] = Seq.empty, minFinalCltvExpiry_opt: Option[Long] = None, maxAttempts_opt: Option[Int] = None)(implicit timeout: Timeout): Future[UUID] = {
val maxAttempts = maxAttempts_opt.getOrElse(appKit.nodeParams.maxPaymentAttempts)
val sendPayment = minFinalCltvExpiry_opt match {
Expand Down
Expand Up @@ -17,7 +17,7 @@
package fr.acinq.eclair.api

import java.util.UUID

import JsonSupport._
import akka.http.scaladsl.unmarshalling.Unmarshaller
import akka.util.Timeout
import fr.acinq.bitcoin.ByteVector32
Expand Down Expand Up @@ -57,4 +57,10 @@ object FormParamExtractors {
Timeout(str.toInt.seconds)
}

implicit val pubkeyListUnmarshaller: Unmarshaller[String, List[PublicKey]] = Unmarshaller.strict { str =>
serialization.read[List[String]](str).map { el =>
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Does that parse URL-encoded json?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

There is no URL-encoding here, this only parses the form data parameter (as json array of strings) into a list of public keys.

PublicKey(ByteVector.fromValidHex(el), checkValid = false)
}
}

}
5 changes: 5 additions & 0 deletions eclair-core/src/main/scala/fr/acinq/eclair/api/Service.scala
Expand Up @@ -221,6 +221,11 @@ trait Service extends ExtraDirectives with Logging {
complete(eclairApi.send(nodeId, amountMsat, paymentHash, maxAttempts = maxAttempts))
}
} ~
path("sendtoroute") {
formFields(amountMsatFormParam, paymentHashFormParam, "finalCltvExpiry".as[Long], "route".as[List[PublicKey]](pubkeyListUnmarshaller)) { (amountMsat, paymentHash, finalCltvExpiry, route) =>
complete(eclairApi.sendToRoute(route, amountMsat, paymentHash, finalCltvExpiry))
}
} ~
path("getsentinfo") {
formFields("id".as[UUID]) { id =>
complete(eclairApi.sentInfo(Left(id)))
Expand Down
Expand Up @@ -19,15 +19,15 @@ package fr.acinq.eclair.payment
import java.util.UUID
import akka.actor.{Actor, ActorLogging, ActorRef, Props}
import fr.acinq.eclair.NodeParams
import fr.acinq.eclair.payment.PaymentLifecycle.SendPayment
import fr.acinq.eclair.payment.PaymentLifecycle.GenericSendPayment

/**
* Created by PM on 29/08/2016.
*/
class PaymentInitiator(nodeParams: NodeParams, router: ActorRef, register: ActorRef) extends Actor with ActorLogging {

override def receive: Receive = {
case c: SendPayment =>
case c: GenericSendPayment =>
val paymentId = UUID.randomUUID()
val payFsm = context.actorOf(PaymentLifecycle.props(nodeParams, paymentId, router, register))
payFsm forward c
Expand Down
Expand Up @@ -45,6 +45,12 @@ class PaymentLifecycle(nodeParams: NodeParams, id: UUID, router: ActorRef, regis
startWith(WAITING_FOR_REQUEST, WaitingForRequest)

when(WAITING_FOR_REQUEST) {
case Event(c: SendPaymentToRoute, WaitingForRequest) =>
val send = SendPayment(c.amountMsat, c.paymentHash, c.hops.last, finalCltvExpiry = c.finalCltvExpiry, maxAttempts = 1)
paymentsDb.addOutgoingPayment(OutgoingPayment(id, c.paymentHash, None, c.amountMsat, Platform.currentTime, None, OutgoingPaymentStatus.PENDING))
router ! FinalizeRoute(c.hops)
goto(WAITING_FOR_ROUTE) using WaitingForRoute(sender, send, failures = Nil)

case Event(c: SendPayment, WaitingForRequest) =>
router ! RouteRequest(nodeParams.nodeId, c.targetNodeId, c.amountMsat, c.assistedRoutes, routeParams = c.routeParams)
paymentsDb.addOutgoingPayment(OutgoingPayment(id, c.paymentHash, None, c.amountMsat, Platform.currentTime, None, OutgoingPaymentStatus.PENDING))
Expand Down Expand Up @@ -192,13 +198,15 @@ object PaymentLifecycle {

// @formatter:off
case class ReceivePayment(amountMsat_opt: Option[MilliSatoshi], description: String, expirySeconds_opt: Option[Long] = None, extraHops: List[List[ExtraHop]] = Nil, fallbackAddress: Option[String] = None)
sealed trait GenericSendPayment
case class SendPaymentToRoute(amountMsat: Long, paymentHash: ByteVector32, hops: Seq[PublicKey], finalCltvExpiry: Long = Channel.MIN_CLTV_EXPIRY) extends GenericSendPayment
case class SendPayment(amountMsat: Long,
paymentHash: ByteVector32,
targetNodeId: PublicKey,
assistedRoutes: Seq[Seq[ExtraHop]] = Nil,
finalCltvExpiry: Long = Channel.MIN_CLTV_EXPIRY,
maxAttempts: Int,
routeParams: Option[RouteParams] = None) {
routeParams: Option[RouteParams] = None) extends GenericSendPayment {
require(amountMsat > 0, s"amountMsat must be > 0")
}

Expand Down
Expand Up @@ -67,6 +67,7 @@ case class RouteRequest(source: PublicKey,
ignoreChannels: Set[ChannelDesc] = Set.empty,
routeParams: Option[RouteParams] = None)

case class FinalizeRoute(hops:Seq[PublicKey])
case class RouteResponse(hops: Seq[Hop], ignoreNodes: Set[PublicKey], ignoreChannels: Set[ChannelDesc]) {
require(hops.size > 0, "route cannot be empty")
}
Expand Down Expand Up @@ -410,6 +411,13 @@ class Router(nodeParams: NodeParams, watcher: ActorRef, initialized: Option[Prom
sender ! d
stay

case Event(FinalizeRoute(partialHops), d) =>
// split into sublists [(a,b),(b,c), ...] then get the edges between each of those pair, then select the largest edge between them
araspitzu marked this conversation as resolved.
Show resolved Hide resolved
val edges = partialHops.sliding(2).map { case List(v1, v2) => d.graph.getEdgesBetween(v1, v2).maxBy(_.update.htlcMaximumMsat) }
val hops = edges.map(d => Hop(d.desc.a, d.desc.b, d.update)).toSeq
sender ! RouteResponse(hops, Set.empty, Set.empty)
stay

case Event(RouteRequest(start, end, amount, assistedRoutes, ignoreNodes, ignoreChannels, params_opt), d) =>
// we convert extra routing info provided in the payment request to fake channel_update
// it takes precedence over all other channel_updates we know
Expand Down
Expand Up @@ -75,7 +75,7 @@ class ApiServiceSpec extends FunSuite with ScalatestRouteTest {

override def findRoute(targetNodeId: Crypto.PublicKey, amountMsat: Long, assistedRoutes: Seq[Seq[PaymentRequest.ExtraHop]])(implicit timeout: Timeout): Future[RouteResponse] = ???

override def audit(from_opt: Option[Long], to_opt: Option[Long])(implicit timeout: Timeout): Future[AuditResponse] = ???
override def sendToRoute(route: Seq[Crypto.PublicKey], amountMsat: Long, paymentHash: ByteVector32, finalCltv: Long)(implicit timeout: Timeout): Future[UUID] = ???

override def networkFees(from_opt: Option[Long], to_opt: Option[Long])(implicit timeout: Timeout): Future[Seq[NetworkFee]] = ???

Expand All @@ -94,6 +94,8 @@ class ApiServiceSpec extends FunSuite with ScalatestRouteTest {
override def allUpdates(nodeId: Option[Crypto.PublicKey])(implicit timeout: Timeout): Future[Iterable[ChannelUpdate]] = ???

override def getInfoResponse()(implicit timeout: Timeout): Future[GetInfoResponse] = ???

override def audit(from_opt: Option[Long], to_opt: Option[Long])(implicit timeout: Timeout): Future[AuditResponse] = ???
}

implicit val formats = JsonSupport.formats
Expand Down
Expand Up @@ -45,6 +45,34 @@ class PaymentLifecycleSpec extends BaseRouterSpec {

val defaultAmountMsat = 142000000L

test("send to route") { fixture =>
import fixture._
val defaultPaymentHash = randomBytes32
val nodeParams = TestConstants.Alice.nodeParams.copy(keyManager = testKeyManager)
val paymentDb = nodeParams.db.payments
val id = UUID.randomUUID()
val paymentFSM = system.actorOf(PaymentLifecycle.props(nodeParams, id, router, TestProbe().ref))
val monitor = TestProbe()
val sender = TestProbe()
val eventListener = TestProbe()
system.eventStream.subscribe(eventListener.ref, classOf[PaymentEvent])

paymentFSM ! SubscribeTransitionCallBack(monitor.ref)
val CurrentState(_, WAITING_FOR_REQUEST) = monitor.expectMsgClass(classOf[CurrentState[_]])

// pre-computed route going from A to D
val request = SendPaymentToRoute(defaultAmountMsat, defaultPaymentHash, Seq(a,b,c,d))

sender.send(paymentFSM, request)
val Transition(_, WAITING_FOR_REQUEST, WAITING_FOR_ROUTE) = monitor.expectMsgClass(classOf[Transition[_]])
val Transition(_, WAITING_FOR_ROUTE, WAITING_FOR_PAYMENT_COMPLETE) = monitor.expectMsgClass(classOf[Transition[_]])
awaitCond(paymentDb.getOutgoingPayment(id).exists(_.status == OutgoingPaymentStatus.PENDING))
sender.send(paymentFSM, UpdateFulfillHtlc(ByteVector32.Zeroes, 0, defaultPaymentHash))

sender.expectMsgType[PaymentSucceeded]
awaitCond(paymentDb.getOutgoingPayment(id).exists(_.status == OutgoingPaymentStatus.SUCCEEDED))
}

test("payment failed (route not found)") { fixture =>
import fixture._
val defaultPaymentHash = randomBytes32
Expand Down
Expand Up @@ -90,7 +90,7 @@ abstract class BaseRouterSpec extends TestkitBaseClass {
override def withFixture(test: OneArgTest): Outcome = {
// the network will be a --(1)--> b ---(2)--> c --(3)--> d and e --(4)--> f (we are a)

within(30 seconds) {
within(120 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))
Expand All @@ -101,7 +101,7 @@ abstract class BaseRouterSpec extends TestkitBaseClass {

// let's we set up the router
val watcher = TestProbe()
val router = system.actorOf(Router.props(Alice.nodeParams, watcher.ref))
val router = system.actorOf(Router.props(Alice.nodeParams.copy(keyManager = testKeyManager), watcher.ref))
// we announce channels
router ! PeerRoutingMessage(null, remoteNodeId, chan_ab)
router ! PeerRoutingMessage(null, remoteNodeId, chan_bc)
Expand Down
22 changes: 18 additions & 4 deletions eclair-core/src/test/scala/fr/acinq/eclair/router/RouterSpec.scala
Expand Up @@ -130,10 +130,10 @@ class RouterSpec extends BaseRouterSpec {
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 buggy_ann_b = ann_b.copy(signature = ann_c.signature, timestamp = ann_b.timestamp + 1)
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Why did you need to change that?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Good catch that was some leftover from development, in 3aa79f7 i reverted the changes to RouterSpec and BaseRouterSpec (along with updating the copyright header).

sender.send(router, PeerRoutingMessage(null, remoteNodeId, buggy_ann_b))
sender.expectMsg(TransportHandler.ReadAck(buggy_ann_b))
sender.expectMsg(InvalidSignature(buggy_ann_b))
}

test("handle bad signature for ChannelUpdate") { fixture =>
Expand Down Expand Up @@ -237,6 +237,20 @@ class RouterSpec extends BaseRouterSpec {
assert(state.updates.size == 8)
}

test("given a pre-computed route add the proper channel updates") { fixture =>
import fixture._

val sender = TestProbe()
val preComputedRoute = Seq(a, b, c, d)
sender.send(router, FinalizeRoute(preComputedRoute))

val response = sender.expectMsgType[RouteResponse]
// 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))
}

test("ask for channels that we marked as stale for which we receive a new update") { fixture =>
import fixture._
val blockHeight = Globals.blockCount.get().toInt - 2020
Expand Down