1
0
mirror of https://github.com/ACINQ/eclair.git synced 2025-01-19 05:33:59 +01:00

Relay Trampoline payments (#1220)

Start relaying trampoline payments with multi-part aggregation (disabled by default,
must be enabled with config).
Recovery after a restart is correctly handled, even if payments were being forwarded.
No DB schema update in this commit.

The trampoline UX will be somewhat bad because many improvements/polish are missing.
Some shortcuts were taken, a few hacks here and there need to be fixed, but nothing too scary.
Those improvements will be done in separate commits before the next release.
This commit is contained in:
Bastien Teinturier 2019-12-18 14:34:52 +01:00 committed by GitHub
parent 2d95168749
commit 611f0cfebe
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
43 changed files with 1960 additions and 521 deletions

View File

@ -54,6 +54,7 @@ case class CltvExpiryDelta(private val underlying: Int) extends Ordered[CltvExpi
// @formatter:off
def +(other: Int): CltvExpiryDelta = CltvExpiryDelta(underlying + other)
def +(other: CltvExpiryDelta): CltvExpiryDelta = CltvExpiryDelta(underlying + other.underlying)
def -(other: CltvExpiryDelta): CltvExpiryDelta = CltvExpiryDelta(underlying - other.underlying)
def compare(other: CltvExpiryDelta): Int = underlying.compareTo(other.underlying)
def toInt: Int = underlying
// @formatter:on

View File

@ -30,7 +30,7 @@ import fr.acinq.eclair.channel._
import fr.acinq.eclair.db.{IncomingPayment, NetworkFee, OutgoingPayment, Stats}
import fr.acinq.eclair.io.Peer.{GetPeerInfo, PeerInfo}
import fr.acinq.eclair.io.{NodeURI, Peer}
import fr.acinq.eclair.payment.send.PaymentInitiator.SendPaymentRequest
import fr.acinq.eclair.payment.send.PaymentInitiator.{SendPaymentRequest, SendTrampolinePaymentRequest}
import fr.acinq.eclair.payment.relay.Relayer.{GetOutgoingChannels, OutgoingChannels, UsableBalance}
import fr.acinq.eclair.payment._
import fr.acinq.eclair.payment.receive.MultiPartHandler.ReceivePayment
@ -88,6 +88,8 @@ trait Eclair {
def send(externalId_opt: Option[String], recipientNodeId: PublicKey, amount: MilliSatoshi, paymentHash: ByteVector32, invoice_opt: Option[PaymentRequest] = None, maxAttempts_opt: Option[Int] = None, feeThresholdSat_opt: Option[Satoshi] = None, maxFeePct_opt: Option[Double] = None)(implicit timeout: Timeout): Future[UUID]
def sendToTrampoline(invoice: PaymentRequest, trampolineId: PublicKey, trampolineFees: MilliSatoshi, trampolineExpiryDelta: CltvExpiryDelta)(implicit timeout: Timeout): Future[UUID]
def sentInfo(id: Either[UUID, ByteVector32])(implicit timeout: Timeout): Future[Seq[OutgoingPayment]]
def findRoute(targetNodeId: PublicKey, amount: MilliSatoshi, assistedRoutes: Seq[Seq[PaymentRequest.ExtraHop]] = Seq.empty)(implicit timeout: Timeout): Future[RouteResponse]
@ -244,6 +246,12 @@ class EclairImpl(appKit: Kit) extends Eclair {
}
}
override def sendToTrampoline(invoice: PaymentRequest, trampolineId: PublicKey, trampolineFees: MilliSatoshi, trampolineExpiryDelta: CltvExpiryDelta)(implicit timeout: Timeout): Future[UUID] = {
val defaultRouteParams = Router.getDefaultRouteParams(appKit.nodeParams.routerConf)
val sendPayment = SendTrampolinePaymentRequest(invoice.amount.get, trampolineFees, invoice, trampolineId, invoice.minFinalCltvExpiryDelta.getOrElse(Channel.MIN_CLTV_EXPIRY_DELTA), trampolineExpiryDelta, Some(defaultRouteParams))
(appKit.paymentInitiator ? sendPayment).mapTo[UUID]
}
override def sentInfo(id: Either[UUID, ByteVector32])(implicit timeout: Timeout): Future[Seq[OutgoingPayment]] = Future {
id match {
case Left(uuid) => appKit.nodeParams.db.payments.listOutgoingPayments(uuid)

View File

@ -43,10 +43,10 @@ import fr.acinq.eclair.channel.Register
import fr.acinq.eclair.crypto.LocalKeyManager
import fr.acinq.eclair.db.{BackupHandler, Databases}
import fr.acinq.eclair.io.{Authenticator, Server, Switchboard}
import fr.acinq.eclair.payment.receive.PaymentHandler
import fr.acinq.eclair.payment.send.{Autoprobe, PaymentInitiator}
import fr.acinq.eclair.payment.Auditor
import fr.acinq.eclair.payment.receive.PaymentHandler
import fr.acinq.eclair.payment.relay.{CommandBuffer, Relayer}
import fr.acinq.eclair.payment.send.{Autoprobe, PaymentInitiator}
import fr.acinq.eclair.router._
import fr.acinq.eclair.tor.TorProtocolHandler.OnionServiceVersion
import fr.acinq.eclair.tor.{Controller, TorProtocolHandler}
@ -210,6 +210,7 @@ class Setup(datadir: File,
zmqTxConnected = Promise[Done]()
tcpBound = Promise[Done]()
routerInitialized = Promise[Done]()
postRestartCleanUpInitialized = Promise[Done]()
defaultFeerates = {
val confDefaultFeerates = FeeratesPerKB(
@ -284,8 +285,11 @@ class Setup(datadir: File,
register = system.actorOf(SimpleSupervisor.props(Props(new Register), "register", SupervisorStrategy.Resume))
commandBuffer = system.actorOf(SimpleSupervisor.props(Props(new CommandBuffer(nodeParams, register)), "command-buffer", SupervisorStrategy.Resume))
paymentHandler = system.actorOf(SimpleSupervisor.props(PaymentHandler.props(nodeParams, commandBuffer), "payment-handler", SupervisorStrategy.Resume))
relayer = system.actorOf(SimpleSupervisor.props(Relayer.props(nodeParams, register, commandBuffer, paymentHandler), "relayer", SupervisorStrategy.Resume))
relayer = system.actorOf(SimpleSupervisor.props(Relayer.props(nodeParams, router, register, commandBuffer, paymentHandler, Some(postRestartCleanUpInitialized)), "relayer", SupervisorStrategy.Resume))
authenticator = system.actorOf(SimpleSupervisor.props(Authenticator.props(nodeParams), "authenticator", SupervisorStrategy.Resume))
// Before initializing the switchboard (which re-connects us to the network) and the user-facing parts of the system,
// we want to make sure the handler for post-restart broken HTLCs has finished initializing.
_ <- postRestartCleanUpInitialized.future
switchboard = system.actorOf(SimpleSupervisor.props(Switchboard.props(nodeParams, authenticator, watcher, router, relayer, paymentHandler, wallet), "switchboard", SupervisorStrategy.Resume))
server = system.actorOf(SimpleSupervisor.props(Server.props(nodeParams, authenticator, serverBindingAddress, Some(tcpBound)), "server", SupervisorStrategy.Restart))
paymentInitiator = system.actorOf(SimpleSupervisor.props(PaymentInitiator.props(nodeParams, router, relayer, register), "payment-initiator", SupervisorStrategy.Restart))

View File

@ -2230,6 +2230,7 @@ class Channel(val nodeParams: NodeParams, val wallet: EclairWallet, remoteNodeId
def origin(c: CMD_ADD_HTLC): Origin = c.upstream match {
case Upstream.Local(id) => Origin.Local(id, Some(sender)) // we were the origin of the payment
case Upstream.Relayed(u) => Origin.Relayed(u.channelId, u.id, u.amountMsat, c.amount) // this is a relayed payment to an outgoing channel
case Upstream.TrampolineRelayed(us) => Origin.TrampolineRelayed(us.map(u => (u.channelId, u.id)).toList, Some(sender)) // this is a relayed payment to an outgoing node
}
def feePaid(fee: Satoshi, tx: Transaction, desc: String, channelId: ByteVector32): Unit = Try { // this may fail with an NPE in tests because context has been cleaned up, but it's not a big deal

View File

@ -110,6 +110,11 @@ object Upstream {
final case class Local(id: UUID) extends Upstream
/** Our node forwarded a single incoming HTLC to an outgoing channel. */
final case class Relayed(add: UpdateAddHtlc) extends Upstream
/** Our node forwarded an incoming HTLC set to a remote outgoing node (potentially producing multiple downstream HTLCs). */
final case class TrampolineRelayed(adds: Seq[UpdateAddHtlc]) extends Upstream {
val amountIn: MilliSatoshi = adds.map(_.amountMsat).sum
val expiryIn: CltvExpiry = adds.map(_.cltvExpiry).min
}
}
sealed trait Command

View File

@ -55,7 +55,7 @@ case class Commitments(channelVersion: ChannelVersion,
localCommit: LocalCommit, remoteCommit: RemoteCommit,
localChanges: LocalChanges, remoteChanges: RemoteChanges,
localNextHtlcId: Long, remoteNextHtlcId: Long,
originChannels: Map[Long, Origin], // for outgoing htlcs relayed through us, the id of the previous channel
originChannels: Map[Long, Origin], // for outgoing htlcs relayed through us, details about the corresponding incoming htlcs
remoteNextCommitInfo: Either[WaitingForRevocation, PublicKey],
commitInput: InputInfo,
remotePerCommitmentSecrets: ShaChain, channelId: ByteVector32) {

View File

@ -135,8 +135,15 @@ class SqliteAuditDb(sqlite: Connection) extends AuditDb with Logging {
statement.setLong(1, e.amountIn.toLong)
statement.setLong(2, e.amountOut.toLong)
statement.setBytes(3, e.paymentHash.toArray)
statement.setBytes(4, e.fromChannelId.toArray)
statement.setBytes(5, e.toChannelId.toArray)
e match {
case ChannelPaymentRelayed(_, _, _, fromChannelId, toChannelId, _) =>
statement.setBytes(4, fromChannelId.toArray)
statement.setBytes(5, toChannelId.toArray)
case TrampolinePaymentRelayed(_, _, _, _, fromChannelIds, toChannelIds, _) =>
// TODO: @t-bast: we should change the DB schema to allow accurate Trampoline reporting
statement.setBytes(4, fromChannelIds.head.toArray)
statement.setBytes(5, toChannelIds.head.toArray)
}
statement.setLong(6, e.timestamp)
statement.executeUpdate()
}
@ -214,7 +221,7 @@ class SqliteAuditDb(sqlite: Connection) extends AuditDb with Logging {
val rs = statement.executeQuery()
var q: Queue[PaymentRelayed] = Queue()
while (rs.next()) {
q = q :+ PaymentRelayed(
q = q :+ ChannelPaymentRelayed(
amountIn = MilliSatoshi(rs.getLong("amount_in_msat")),
amountOut = MilliSatoshi(rs.getLong("amount_out_msat")),
paymentHash = rs.getByteVector32("payment_hash"),

View File

@ -19,20 +19,12 @@ package fr.acinq.eclair.io
import java.net.InetSocketAddress
import akka.actor.{Actor, ActorLogging, ActorRef, OneForOneStrategy, Props, Status, SupervisorStrategy}
import akka.event.LoggingAdapter
import fr.acinq.bitcoin.ByteVector32
import fr.acinq.bitcoin.Crypto.{PrivateKey, PublicKey}
import fr.acinq.bitcoin.Crypto.PublicKey
import fr.acinq.eclair.NodeParams
import fr.acinq.eclair.blockchain.EclairWallet
import fr.acinq.eclair.channel.Helpers.Closing
import fr.acinq.eclair.channel._
import fr.acinq.eclair.db.{IncomingPayment, IncomingPaymentStatus, IncomingPaymentsDb, PendingRelayDb}
import fr.acinq.eclair.payment.IncomingPacket
import fr.acinq.eclair.payment.relay.Origin
import fr.acinq.eclair.router.Rebroadcast
import fr.acinq.eclair.transactions.{DirectedHtlc, IN, OUT}
import fr.acinq.eclair.wire.{TemporaryNodeFailure, UpdateAddHtlc}
import scodec.bits.ByteVector
/**
* Ties network connections to peers.
@ -42,13 +34,12 @@ class Switchboard(nodeParams: NodeParams, authenticator: ActorRef, watcher: Acto
import Switchboard._
// we pass these to helpers classes so that they have the logging context
implicit def implicitLog: LoggingAdapter = log
authenticator ! self
// we load peers and channels from database
{
val peers = nodeParams.db.peers.listPeers()
// Check if channels that are still in CLOSING state have actually been closed. This can happen when the app is stopped
// just after a channel state has transitioned to CLOSED and before it has effectively been removed.
// Closed channels will be removed, other channels will be restored.
@ -57,16 +48,6 @@ class Switchboard(nodeParams: NodeParams, authenticator: ActorRef, watcher: Acto
log.info(s"closing channel ${c.channelId}")
nodeParams.db.channels.removeChannel(c.channelId)
})
val peers = nodeParams.db.peers.listPeers()
checkBrokenHtlcsLink(channels, nodeParams.db.payments, nodeParams.privateKey, nodeParams.globalFeatures) match {
case Nil => ()
case brokenHtlcs =>
val brokenHtlcKiller = context.system.actorOf(Props[HtlcReaper], name = "htlc-reaper")
brokenHtlcKiller ! brokenHtlcs
}
cleanupRelayDb(channels, nodeParams.db.pendingRelay)
channels
.groupBy(_.commitments.remoteParams.nodeId)
@ -151,115 +132,4 @@ object Switchboard {
def peerActorName(remoteNodeId: PublicKey): String = s"peer-$remoteNodeId"
/**
* If we have stopped eclair while it was handling HTLCs, it is possible that we are in a state were an incoming HTLC
* was committed by both sides, but we didn't have time to send and/or sign the corresponding HTLC to the downstream
* node (if we're an intermediate node) or didn't have time to fail/fulfill the payment (if we're the recipient).
*
* In that case, if we do nothing, the incoming HTLC will eventually expire and we won't lose money, but the channel
* will get closed, which is a major inconvenience.
*
* This check will detect this and will allow us to fast-settle HTLCs and thus preserve channels.
*/
def checkBrokenHtlcsLink(channels: Seq[HasCommitments], paymentsDb: IncomingPaymentsDb, privateKey: PrivateKey, features: ByteVector)(implicit log: LoggingAdapter): Seq[(UpdateAddHtlc, Option[ByteVector32])] = {
// We are interested in incoming HTLCs, that have been *cross-signed* (otherwise they wouldn't have been relayed).
// They signed it first, so the HTLC will first appear in our commitment tx, and later on in their commitment when
// we subsequently sign it. That's why we need to look in *their* commitment with direction=OUT.
val htlcs_in = channels
.flatMap(_.commitments.remoteCommit.spec.htlcs)
.filter(_.direction == OUT)
.map(_.add)
.map(IncomingPacket.decrypt(_, privateKey, features))
.collect {
case Right(IncomingPacket.ChannelRelayPacket(add, _, _)) => (add, None) // we consider all relayed htlcs
case Right(IncomingPacket.FinalPacket(add, _)) => paymentsDb.getIncomingPayment(add.paymentHash) match {
case Some(IncomingPayment(_, preimage, _, IncomingPaymentStatus.Received(_, _))) => (add, Some(preimage)) // incoming payment that succeeded
case _ => (add, None) // incoming payment that didn't succeed
}
}
// TODO: @t-bast: will need to update this to take into account trampoline-relayed (and thoroughly test).
// Here we do it differently because we need the origin information.
val relayed_out = channels
.flatMap(_.commitments.originChannels.values)
.collect { case r: Origin.Relayed => r }
.toSet
val htlcs_broken = htlcs_in.filterNot {
case (htlc_in, _) => relayed_out.exists(r => r.originChannelId == htlc_in.channelId && r.originHtlcId == htlc_in.id)
}
log.info(s"htlcs_in=${htlcs_in.size} htlcs_out=${relayed_out.size} htlcs_broken=${htlcs_broken.size}")
htlcs_broken
}
/**
* We store [[CMD_FULFILL_HTLC]]/[[CMD_FAIL_HTLC]]/[[CMD_FAIL_MALFORMED_HTLC]]
* in a database (see [[fr.acinq.eclair.payment.relay.CommandBuffer]]) because we
* don't want to lose preimages, or to forget to fail incoming htlcs, which
* would lead to unwanted channel closings.
*
* Because of the way our watcher works, in a scenario where a downstream
* channel has gone to the blockchain, it may send several times the same
* command, and the upstream channel may have disappeared in the meantime.
*
* That's why we need to periodically clean up the pending relay db.
*/
def cleanupRelayDb(channels: Seq[HasCommitments], relayDb: PendingRelayDb)(implicit log: LoggingAdapter): Int = {
// We are interested in incoming HTLCs, that have been *cross-signed* (otherwise they wouldn't have been relayed).
// If the HTLC is not in their commitment, it means that we have already fulfilled/failed it and that we can remove
// the command from the pending relay db.
val channel2Htlc: Set[(ByteVector32, Long)] =
channels
.flatMap(_.commitments.remoteCommit.spec.htlcs)
.filter(_.direction == OUT)
.map(htlc => (htlc.add.channelId, htlc.add.id))
.toSet
val pendingRelay: Set[(ByteVector32, Long)] = relayDb.listPendingRelay()
val toClean = pendingRelay -- channel2Htlc
toClean.foreach {
case (channelId, htlcId) =>
log.info(s"cleaning up channelId=$channelId htlcId=$htlcId from relay db")
relayDb.removePendingRelay(channelId, htlcId)
}
toClean.size
}
}
class HtlcReaper extends Actor with ActorLogging {
context.system.eventStream.subscribe(self, classOf[ChannelStateChanged])
override def receive: Receive = {
case initialHtlcs: Seq[(UpdateAddHtlc, Option[ByteVector32])]@unchecked => context become main(initialHtlcs)
}
def main(htlcs: Seq[(UpdateAddHtlc, Option[ByteVector32])]): Receive = {
case ChannelStateChanged(channel, _, _, WAIT_FOR_INIT_INTERNAL | OFFLINE | SYNCING, NORMAL | SHUTDOWN | CLOSING, data: HasCommitments) =>
val acked = htlcs
.filter(_._1.channelId == data.channelId) // only consider htlcs related to this channel
.filter {
case (htlc, preimage) if Commitments.getHtlcCrossSigned(data.commitments, IN, htlc.id).isDefined =>
// this htlc is cross signed in the current commitment, we can settle it
preimage match {
case Some(preimage) =>
log.info(s"fulfilling broken htlc=$htlc")
channel ! CMD_FULFILL_HTLC(htlc.id, preimage, commit = true)
case None =>
log.info(s"failing broken htlc=$htlc")
channel ! CMD_FAIL_HTLC(htlc.id, Right(TemporaryNodeFailure), commit = true)
}
false // the channel may very well be disconnected before we sign (=ack) the fail/fulfill, so we keep it for now
case _ =>
true // the htlc has already been failed, we can forget about it now
}
acked.foreach { case (htlc, _) => log.info(s"forgetting htlc id=${htlc.id} channelId=${htlc.channelId}") }
context become main(htlcs diff acked)
}
}

View File

@ -81,14 +81,20 @@ class Auditor(nodeParams: NodeParams) extends Actor with ActorLogging {
db.add(e)
case e: PaymentRelayed =>
val relayType = e match {
case _: ChannelPaymentRelayed => "channel"
case _: TrampolinePaymentRelayed => "trampoline"
}
Kamon
.histogram("payment.hist")
.withTag("direction", "relayed")
.withTag("relay", relayType)
.withTag("type", "total")
.record(e.amountIn.truncateToSatoshi.toLong)
Kamon
.histogram("payment.hist")
.withTag("direction", "relayed")
.withTag("relay", relayType)
.withTag("type", "fee")
.record((e.amountIn - e.amountOut).truncateToSatoshi.toLong)
db.add(e)

View File

@ -19,6 +19,7 @@ package fr.acinq.eclair.payment
import java.util.UUID
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.ChannelHop
@ -53,7 +54,14 @@ object PaymentSent {
case class PaymentFailed(id: UUID, paymentHash: ByteVector32, failures: Seq[PaymentFailure], timestamp: Long = Platform.currentTime) extends PaymentEvent
case class PaymentRelayed(amountIn: MilliSatoshi, amountOut: MilliSatoshi, paymentHash: ByteVector32, fromChannelId: ByteVector32, toChannelId: ByteVector32, timestamp: Long = Platform.currentTime) extends PaymentEvent
sealed trait PaymentRelayed extends PaymentEvent {
val amountIn: MilliSatoshi
val amountOut: MilliSatoshi
}
case class ChannelPaymentRelayed(amountIn: MilliSatoshi, amountOut: MilliSatoshi, paymentHash: ByteVector32, fromChannelId: ByteVector32, toChannelId: ByteVector32, timestamp: Long = Platform.currentTime) extends PaymentRelayed
case class TrampolinePaymentRelayed(amountIn: MilliSatoshi, amountOut: MilliSatoshi, paymentHash: ByteVector32, toNodeId: PublicKey, fromChannelIds: Seq[ByteVector32], toChannelIds: Seq[ByteVector32], timestamp: Long = Platform.currentTime) extends PaymentRelayed
case class PaymentReceived(paymentHash: ByteVector32, parts: Seq[PaymentReceived.PartialPayment]) extends PaymentEvent {
require(parts.nonEmpty, "must have at least one subpayment")

View File

@ -16,8 +16,6 @@
package fr.acinq.eclair.payment
import java.util.UUID
import akka.event.LoggingAdapter
import fr.acinq.bitcoin.ByteVector32
import fr.acinq.bitcoin.Crypto.{PrivateKey, PublicKey}
@ -238,9 +236,9 @@ object OutgoingPacket {
*
* @return the command and the onion shared secrets (used to decrypt the error in case of payment failure)
*/
def buildCommand(id: UUID, paymentHash: ByteVector32, hops: Seq[ChannelHop], finalPayload: Onion.FinalPayload): (CMD_ADD_HTLC, Seq[(ByteVector32, PublicKey)]) = {
def buildCommand(upstream: Upstream, paymentHash: ByteVector32, hops: Seq[ChannelHop], finalPayload: Onion.FinalPayload): (CMD_ADD_HTLC, Seq[(ByteVector32, PublicKey)]) = {
val (firstAmount, firstExpiry, onion) = buildPacket(Sphinx.PaymentPacket)(paymentHash, hops, finalPayload)
CMD_ADD_HTLC(firstAmount, paymentHash, firstExpiry, onion.packet, Upstream.Local(id), commit = true) -> onion.sharedSecrets
CMD_ADD_HTLC(firstAmount, paymentHash, firstExpiry, onion.packet, upstream, commit = true) -> onion.sharedSecrets
}
}

View File

@ -336,6 +336,13 @@ object PaymentRequest {
lazy val allowTrampoline: Boolean = hasFeature(bitmask, TRAMPOLINE_PAYMENT_MANDATORY) || hasFeature(bitmask, TRAMPOLINE_PAYMENT_OPTIONAL)
override def toString: String = s"Features(${bitmask.toBin})"
// When converting from BitVector to ByteVector, scodec pads right instead of left so we have to do this ourselves.
// We also want to enforce a minimal encoding of the feature bytes.
def toByteVector: ByteVector = {
val pad = if (bitmask.length % 8 == 0) 0 else 8 - bitmask.length % 8
bitmask.padLeft(bitmask.length + pad).bytes.dropWhile(_ == 0)
}
}
object Features {

View File

@ -45,6 +45,7 @@ class MultiPartPaymentFSM(nodeParams: NodeParams, paymentHash: ByteVector32, tot
when(WAITING_FOR_HTLC) {
case Event(PaymentTimeout, d: WaitingForHtlc) =>
log.warning(s"multi-part payment timed out (received ${d.paidAmount} expected $totalAmount)")
goto(PAYMENT_FAILED) using PaymentFailed(wire.PaymentTimeout, d.parts)
case Event(MultiPartHtlc(totalAmount2, htlc), d: WaitingForHtlc) =>
@ -77,6 +78,7 @@ class MultiPartPaymentFSM(nodeParams: NodeParams, paymentHash: ByteVector32, tot
// The LocalPaymentHandler will create a new instance of MultiPartPaymentHandler to handle a new attempt.
case Event(MultiPartHtlc(_, htlc), PaymentFailed(failure, _)) =>
require(htlc.paymentHash == paymentHash, s"invalid payment hash (expected $paymentHash, received ${htlc.paymentHash}")
log.info(s"received extraneous htlc for payment hash $paymentHash")
parent ! ExtraHtlcReceived(paymentHash, PendingPayment(htlc.id, PartialPayment(htlc.amountMsat, htlc.channelId)), Some(failure))
stay
}

View File

@ -0,0 +1,243 @@
/*
* Copyright 2019 ACINQ SAS
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package fr.acinq.eclair.payment.relay
import java.util.UUID
import akka.actor.{Actor, ActorRef, DiagnosticActorLogging, PoisonPill, Props}
import akka.event.Logging.MDC
import fr.acinq.bitcoin.ByteVector32
import fr.acinq.eclair.Features.{BASIC_MULTI_PART_PAYMENT_MANDATORY, BASIC_MULTI_PART_PAYMENT_OPTIONAL}
import fr.acinq.eclair.channel.{CMD_FAIL_HTLC, CMD_FULFILL_HTLC, Upstream}
import fr.acinq.eclair.payment.receive.MultiPartPaymentFSM
import fr.acinq.eclair.payment.send.MultiPartPaymentLifecycle.SendMultiPartPayment
import fr.acinq.eclair.payment.send.PaymentInitiator.SendPaymentConfig
import fr.acinq.eclair.payment.send.PaymentLifecycle.SendPayment
import fr.acinq.eclair.payment.send.{MultiPartPaymentLifecycle, PaymentLifecycle}
import fr.acinq.eclair.payment.{IncomingPacket, PaymentFailed, PaymentSent, TrampolinePaymentRelayed}
import fr.acinq.eclair.router.{RouteParams, Router}
import fr.acinq.eclair.wire._
import fr.acinq.eclair.{CltvExpiry, Features, Logs, MilliSatoshi, NodeParams, nodeFee, randomBytes32}
import scala.collection.immutable.Queue
/**
* Created by t-bast on 10/10/2019.
*/
/**
* The Node Relayer is used to relay an upstream payment to a downstream remote node (which is not necessarily a direct peer).
* It aggregates incoming HTLCs (in case multi-part was used upstream) and then forwards the requested amount (using the
* router to find a route to the remote node and potentially splitting the payment using multi-part).
*/
class NodeRelayer(nodeParams: NodeParams, relayer: ActorRef, router: ActorRef, commandBuffer: ActorRef, register: ActorRef) extends Actor with DiagnosticActorLogging {
// TODO: @t-bast: if fees/cltv insufficient (could not find route) send special error (sender should retry with higher fees/cltv)?
// TODO: @t-bast: add Kamon counters to monitor the size of pendingIncoming/Outgoing?
import NodeRelayer._
override def receive: Receive = main(Map.empty, Map.empty)
def main(pendingIncoming: Map[ByteVector32, PendingRelay], pendingOutgoing: Map[UUID, PendingResult]): Receive = {
// We make sure we receive all payment parts before forwarding to the next trampoline node.
case IncomingPacket.NodeRelayPacket(add, outer, inner, next) => outer.paymentSecret match {
case None =>
log.warning(s"rejecting htlcId=${add.id} channelId=${add.channelId}: missing payment secret")
rejectHtlc(add.id, add.channelId, add.amountMsat)
case Some(secret) => pendingIncoming.get(add.paymentHash) match {
case Some(relay) =>
if (relay.secret != secret) {
log.warning(s"rejecting htlcId=${add.id} channelId=${add.channelId}: payment secret doesn't match other HTLCs in the set")
rejectHtlc(add.id, add.channelId, add.amountMsat)
} else {
relay.handler ! MultiPartPaymentFSM.MultiPartHtlc(outer.totalAmount, add)
context become main(pendingIncoming + (add.paymentHash -> relay.copy(htlcs = relay.htlcs :+ add)), pendingOutgoing)
}
case None =>
val handler = context.actorOf(MultiPartPaymentFSM.props(nodeParams, add.paymentHash, outer.totalAmount, self))
handler ! MultiPartPaymentFSM.MultiPartHtlc(outer.totalAmount, add)
context become main(pendingIncoming + (add.paymentHash -> PendingRelay(Queue(add), secret, inner, next, handler)), pendingOutgoing)
}
}
// We always fail extraneous HTLCs. They are a spec violation from the sender, but harmless in the relay case.
// By failing them fast (before the payment has reached the final recipient) there's a good chance the sender
// won't lose any money.
case MultiPartPaymentFSM.ExtraHtlcReceived(_, p, failure) => rejectHtlc(p.htlcId, p.payment.fromChannelId, p.payment.amount, failure)
case MultiPartPaymentFSM.MultiPartHtlcFailed(paymentHash, failure, parts) =>
log.warning(s"could not relay payment (paidAmount=${parts.map(_.payment.amount).sum} failure=$failure)")
pendingIncoming.get(paymentHash).foreach(_.handler ! PoisonPill)
parts.foreach(p => rejectHtlc(p.htlcId, p.payment.fromChannelId, p.payment.amount, Some(failure)))
context become main(pendingIncoming - paymentHash, pendingOutgoing)
case MultiPartPaymentFSM.MultiPartHtlcSucceeded(paymentHash, parts) => pendingIncoming.get(paymentHash) match {
case Some(PendingRelay(htlcs, _, nextPayload, nextPacket, handler)) =>
val upstream = Upstream.TrampolineRelayed(htlcs)
handler ! PoisonPill
validateRelay(nodeParams, upstream, nextPayload) match {
case Some(failure) =>
log.warning(s"rejecting trampoline payment (amountIn=${upstream.amountIn} expiryIn=${upstream.expiryIn} amountOut=${nextPayload.amountToForward} expiryOut=${nextPayload.outgoingCltv} htlcCount=${parts.length} reason=$failure)")
rejectPayment(upstream, Some(failure))
case None =>
log.info(s"relaying trampoline payment (amountIn=${upstream.amountIn} expiryIn=${upstream.expiryIn} amountOut=${nextPayload.amountToForward} expiryOut=${nextPayload.outgoingCltv} htlcCount=${parts.length})")
val paymentId = relay(paymentHash, upstream, nextPayload, nextPacket)
context become main(pendingIncoming - paymentHash, pendingOutgoing + (paymentId -> PendingResult(upstream, nextPayload)))
}
case None => throw new RuntimeException(s"could not find pending incoming payment (paymentHash=$paymentHash)")
}
case PaymentSent(id, paymentHash, paymentPreimage, parts) =>
log.debug("trampoline payment successfully relayed")
pendingOutgoing.get(id).foreach {
case PendingResult(upstream, nextPayload) =>
fulfillPayment(upstream, paymentPreimage)
val fromChannelIds = upstream.adds.map(_.channelId)
val toChannelIds = parts.map(_.toChannelId)
context.system.eventStream.publish(TrampolinePaymentRelayed(upstream.amountIn, nextPayload.amountToForward, paymentHash, nextPayload.outgoingNodeId, fromChannelIds, toChannelIds))
}
context become main(pendingIncoming, pendingOutgoing - id)
case PaymentFailed(id, _, _, _) =>
// TODO: @t-bast: try to extract the most meaningful error to return upstream (from the downstream failures)
// - if local failure because balance too low: we should send a TEMPORARY failure upstream (they should retry when we have more balance available)
// - if local failure because route not found: sender probably need to raise fees/cltv?
log.debug("trampoline payment failed")
pendingOutgoing.get(id).foreach { case PendingResult(upstream, _) => rejectPayment(upstream) }
context become main(pendingIncoming, pendingOutgoing - id)
case ack: CommandBuffer.CommandAck => commandBuffer forward ack
}
def spawnOutgoingPayFSM(cfg: SendPaymentConfig, multiPart: Boolean): ActorRef = {
if (multiPart) {
context.actorOf(MultiPartPaymentLifecycle.props(nodeParams, cfg, relayer, router, register))
} else {
context.actorOf(PaymentLifecycle.props(nodeParams, cfg, router, register))
}
}
private def relay(paymentHash: ByteVector32, upstream: Upstream.TrampolineRelayed, payloadOut: Onion.NodeRelayPayload, packetOut: OnionRoutingPacket): UUID = {
val paymentId = UUID.randomUUID()
val paymentCfg = SendPaymentConfig(paymentId, paymentId, None, paymentHash, payloadOut.outgoingNodeId, upstream, None, storeInDb = false, publishEvent = false)
val routeParams = computeRouteParams(nodeParams, upstream.amountIn, upstream.expiryIn, payloadOut.amountToForward, payloadOut.outgoingCltv)
payloadOut.invoiceFeatures match {
case Some(invoiceFeatures) =>
log.debug("relaying trampoline payment to non-trampoline recipient")
val routingHints = payloadOut.invoiceRoutingInfo.map(_.map(_.toSeq).toSeq).getOrElse(Nil)
val allowMultiPart = Features.hasFeature(invoiceFeatures, BASIC_MULTI_PART_PAYMENT_OPTIONAL) || Features.hasFeature(invoiceFeatures, BASIC_MULTI_PART_PAYMENT_MANDATORY)
val payFSM = spawnOutgoingPayFSM(paymentCfg, allowMultiPart)
if (allowMultiPart) {
if (payloadOut.paymentSecret.isEmpty) {
log.warning("payment relay to non-trampoline node will likely fail: sender didn't include the invoice payment secret")
}
val payment = SendMultiPartPayment(paymentHash, payloadOut.paymentSecret.getOrElse(randomBytes32), payloadOut.outgoingNodeId, payloadOut.amountToForward, payloadOut.outgoingCltv, nodeParams.maxPaymentAttempts, routingHints, Some(routeParams))
payFSM ! payment
} else {
val finalPayload = Onion.createSinglePartPayload(payloadOut.amountToForward, payloadOut.outgoingCltv, payloadOut.paymentSecret)
val payment = SendPayment(paymentHash, payloadOut.outgoingNodeId, finalPayload, nodeParams.maxPaymentAttempts, routingHints, Some(routeParams))
payFSM ! payment
}
case None =>
log.debug("relaying trampoline payment to next trampoline node")
val payFSM = spawnOutgoingPayFSM(paymentCfg, multiPart = true)
val paymentSecret = randomBytes32 // we generate a new secret to protect against probing attacks
val payment = SendMultiPartPayment(paymentHash, paymentSecret, payloadOut.outgoingNodeId, payloadOut.amountToForward, payloadOut.outgoingCltv, nodeParams.maxPaymentAttempts, routeParams = Some(routeParams), additionalTlvs = Seq(OnionTlv.TrampolineOnion(packetOut)))
payFSM ! payment
}
paymentId
}
private def rejectHtlc(htlcId: Long, channelId: ByteVector32, amount: MilliSatoshi, failure: Option[FailureMessage] = None): Unit = {
val failureMessage = failure.getOrElse(IncorrectOrUnknownPaymentDetails(amount, nodeParams.currentBlockHeight))
commandBuffer ! CommandBuffer.CommandSend(channelId, CMD_FAIL_HTLC(htlcId, Right(failureMessage), commit = true))
}
private def rejectPayment(upstream: Upstream.TrampolineRelayed, failure: Option[FailureMessage] = None): Unit =
upstream.adds.foreach(add => rejectHtlc(add.id, add.channelId, upstream.amountIn, failure))
private def fulfillPayment(upstream: Upstream.TrampolineRelayed, paymentPreimage: ByteVector32): Unit = upstream.adds.foreach(add => {
val cmdFulfill = CMD_FULFILL_HTLC(add.id, paymentPreimage, commit = true)
commandBuffer ! CommandBuffer.CommandSend(add.channelId, cmdFulfill)
})
override def mdc(currentMessage: Any): MDC = {
val paymentHash_opt = currentMessage match {
case IncomingPacket.NodeRelayPacket(add, _, _, _) => Some(add.paymentHash)
case MultiPartPaymentFSM.MultiPartHtlcFailed(paymentHash, _, _) => Some(paymentHash)
case MultiPartPaymentFSM.MultiPartHtlcSucceeded(paymentHash, _) => Some(paymentHash)
case MultiPartPaymentFSM.ExtraHtlcReceived(paymentHash, _, _) => Some(paymentHash)
case PaymentFailed(_, paymentHash, _, _) => Some(paymentHash)
case PaymentSent(_, paymentHash, _, _) => Some(paymentHash)
case _ => None
}
Logs.mdc(category_opt = Some(Logs.LogCategory.PAYMENT), paymentHash_opt = paymentHash_opt)
}
}
object NodeRelayer {
def props(nodeParams: NodeParams, relayer: ActorRef, router: ActorRef, commandBuffer: ActorRef, register: ActorRef) = Props(classOf[NodeRelayer], nodeParams, relayer, router, commandBuffer, register)
/**
* We start by aggregating an incoming HTLC set. Once we received the whole set, we will compute a route to the next
* trampoline node and forward the payment.
*
* @param htlcs received incoming HTLCs for this set.
* @param secret all incoming HTLCs in this set must have the same secret to protect against probing / fee theft.
* @param nextPayload relay instructions (should be identical across HTLCs in this set).
* @param nextPacket trampoline onion to relay to the next trampoline node.
* @param handler actor handling the aggregation of the incoming HTLC set.
*/
case class PendingRelay(htlcs: Queue[UpdateAddHtlc], secret: ByteVector32, nextPayload: Onion.NodeRelayPayload, nextPacket: OnionRoutingPacket, handler: ActorRef)
/**
* Once the payment is forwarded, we're waiting for fail/fulfill responses from downstream nodes.
*
* @param upstream complete HTLC set received.
* @param nextPayload relay instructions.
*/
case class PendingResult(upstream: Upstream.TrampolineRelayed, nextPayload: Onion.NodeRelayPayload)
def validateRelay(nodeParams: NodeParams, upstream: Upstream.TrampolineRelayed, payloadOut: Onion.NodeRelayPayload): Option[FailureMessage] = {
val fee = nodeFee(nodeParams.feeBase, nodeParams.feeProportionalMillionth, payloadOut.amountToForward)
if (upstream.amountIn - payloadOut.amountToForward < fee) {
// TODO: @t-bast: should be a TrampolineFeeInsufficient(upstream.amountIn, myLatestNodeUpdate)
Some(IncorrectOrUnknownPaymentDetails(upstream.amountIn, nodeParams.currentBlockHeight))
} else if (upstream.expiryIn - payloadOut.outgoingCltv < nodeParams.expiryDeltaBlocks) {
// TODO: @t-bast: should be a TrampolineExpiryTooSoon(myLatestNodeUpdate)
Some(IncorrectOrUnknownPaymentDetails(upstream.amountIn, nodeParams.currentBlockHeight))
} else {
None
}
}
/** Compute route params that honor our fee and cltv requirements. */
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(
maxFeeBase = routeMaxFee,
routeMaxCltv = routeMaxCltv,
maxFeePct = 0 // we disable percent-based max fee calculation, we're only interested in collecting our node fee
)
}
}

View File

@ -0,0 +1,302 @@
/*
* Copyright 2019 ACINQ SAS
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package fr.acinq.eclair.payment.relay
import akka.Done
import akka.actor.{Actor, ActorLogging, ActorRef, Props}
import akka.event.LoggingAdapter
import fr.acinq.bitcoin.ByteVector32
import fr.acinq.bitcoin.Crypto.PrivateKey
import fr.acinq.eclair.channel.Helpers.Closing
import fr.acinq.eclair.channel._
import fr.acinq.eclair.db._
import fr.acinq.eclair.payment.{IncomingPacket, PaymentFailed, PaymentSent}
import fr.acinq.eclair.transactions.{IN, OUT}
import fr.acinq.eclair.wire.{TemporaryNodeFailure, UpdateAddHtlc}
import fr.acinq.eclair.{LongToBtcAmount, NodeParams}
import scodec.bits.ByteVector
import scala.concurrent.Promise
import scala.util.Try
/**
* Created by t-bast on 21/11/2019.
*/
/**
* If we have stopped eclair while it was handling HTLCs, it is possible that we are in a state were incoming HTLCs were
* committed by both sides, but we didn't have time to send and/or sign corresponding HTLCs to the downstream node.
* It's also possible that we partially forwarded a payment (if MPP was used downstream): we have lost the intermediate
* state necessary to retry that payment, so we need to wait for the partial HTLC set sent downstream to either fail or
* fulfill (and forward the result upstream).
*
* If we were sending a payment (no downstream HTLCs) when we stopped eclair, we might have sent only a portion of the
* payment (because of multi-part): we have lost the intermediate state necessary to retry that payment, so we need to
* wait for the partial HTLC set sent downstream to either fail or fulfill the payment in our DB.
*/
class PostRestartHtlcCleaner(nodeParams: NodeParams, commandBuffer: ActorRef, initialized: Option[Promise[Done]] = None) extends Actor with ActorLogging {
import PostRestartHtlcCleaner._
// we pass these to helpers classes so that they have the logging context
implicit def implicitLog: LoggingAdapter = log
context.system.eventStream.subscribe(self, classOf[ChannelStateChanged])
val brokenHtlcs = {
// Check if channels that are still in CLOSING state have actually been closed. This can happen when the app is
// stopped just after a channel state has transitioned to CLOSED and before it has effectively been removed.
// Closed channels will be removed, other channels will be restored.
val channels = nodeParams.db.channels.listLocalChannels().filter(c => Closing.isClosed(c, None).isEmpty)
cleanupRelayDb(channels, nodeParams.db.pendingRelay)
checkBrokenHtlcs(channels, nodeParams.db.payments, nodeParams.privateKey, nodeParams.globalFeatures)
}
override def receive: Receive = main(brokenHtlcs)
// Once we've loaded the channels and identified broken HTLCs, we let other components know they can proceed.
Try(initialized.map(_.success(Done)))
def main(brokenHtlcs: BrokenHtlcs): Receive = {
// When channels are restarted we immediately fail the incoming HTLCs that weren't relayed.
case ChannelStateChanged(channel, _, _, WAIT_FOR_INIT_INTERNAL | OFFLINE | SYNCING, NORMAL | SHUTDOWN | CLOSING, data: HasCommitments) =>
val acked = brokenHtlcs.notRelayed
.filter(_.add.channelId == data.channelId) // only consider htlcs related to this channel
.filter {
case IncomingHtlc(htlc, preimage) if Commitments.getHtlcCrossSigned(data.commitments, IN, htlc.id).isDefined =>
// this htlc is cross signed in the current commitment, we can settle it
preimage match {
case Some(preimage) =>
log.info(s"fulfilling broken htlc=$htlc")
channel ! CMD_FULFILL_HTLC(htlc.id, preimage, commit = true)
case None =>
log.info(s"failing not relayed htlc=$htlc")
channel ! CMD_FAIL_HTLC(htlc.id, Right(TemporaryNodeFailure), commit = true)
}
false // the channel may very well be disconnected before we sign (=ack) the fail/fulfill, so we keep it for now
case _ =>
true // the htlc has already been settled, we can forget about it now
}
acked.foreach(htlc => log.info(s"forgetting htlc id=${htlc.add.id} channelId=${htlc.add.channelId}"))
context become main(brokenHtlcs.copy(notRelayed = brokenHtlcs.notRelayed diff acked))
case Relayer.ForwardFulfill(fulfill, to, add) => handleDownstreamFulfill(brokenHtlcs, to, add, fulfill.paymentPreimage)
case Relayer.ForwardFail(_, to, add) => handleDownstreamFailure(brokenHtlcs, to, add)
case Relayer.ForwardFailMalformed(_, to, add) => handleDownstreamFailure(brokenHtlcs, to, add)
case ack: CommandBuffer.CommandAck => commandBuffer forward ack
case "ok" => // ignoring responses from channels
}
private def handleDownstreamFulfill(brokenHtlcs: BrokenHtlcs, origin: Origin, fulfilledHtlc: UpdateAddHtlc, paymentPreimage: ByteVector32): Unit =
brokenHtlcs.relayedOut.get(origin) match {
case Some(relayedOut) => origin match {
case Origin.Local(id, _) =>
val feesPaid = 0.msat // fees are unknown since we lost the reference to the payment
nodeParams.db.payments.updateOutgoingPayment(PaymentSent(id, fulfilledHtlc.paymentHash, paymentPreimage, PaymentSent.PartialPayment(id, fulfilledHtlc.amountMsat, feesPaid, fulfilledHtlc.channelId, None) :: Nil))
// If all downstream HTLCs are now resolved, we can emit the payment event.
nodeParams.db.payments.getOutgoingPayment(id).foreach(p => {
val payments = nodeParams.db.payments.listOutgoingPayments(p.parentId)
if (!payments.exists(p => p.status == OutgoingPaymentStatus.Pending)) {
val succeeded = payments.collect {
case OutgoingPayment(id, _, _, _, amount, _, _, _, OutgoingPaymentStatus.Succeeded(_, feesPaid, _, completedAt)) =>
PaymentSent.PartialPayment(id, amount, feesPaid, ByteVector32.Zeroes, None, completedAt)
}
val sent = PaymentSent(p.parentId, fulfilledHtlc.paymentHash, paymentPreimage, succeeded)
log.info(s"payment id=${sent.id} paymentHash=${sent.paymentHash} successfully sent (amount=${sent.amount})")
context.system.eventStream.publish(sent)
}
})
// There can never be more than one pending downstream HTLC for a given local origin (a multi-part payment is
// instead spread across multiple local origins) so we can now forget this origin.
context become main(brokenHtlcs.copy(relayedOut = brokenHtlcs.relayedOut - origin))
case Origin.TrampolineRelayed(origins, _) =>
// We fulfill upstream as soon as we have the payment preimage available.
if (!brokenHtlcs.settledUpstream.contains(origin)) {
log.info(s"received preimage for paymentHash=${fulfilledHtlc.paymentHash}: fulfilling ${origins.length} HTLCs upstream")
origins.foreach { case (channelId, htlcId) =>
commandBuffer ! CommandBuffer.CommandSend(channelId, CMD_FULFILL_HTLC(htlcId, paymentPreimage, commit = true))
}
}
val relayedOut1 = relayedOut diff Seq((fulfilledHtlc.channelId, fulfilledHtlc.id))
if (relayedOut1.isEmpty) {
log.info(s"payment with paymentHash=${fulfilledHtlc.paymentHash} successfully relayed")
// We could emit a TrampolinePaymentRelayed event but that requires more book-keeping on incoming HTLCs.
// It seems low priority so isn't done at the moment but can be added when we feel we need it.
context become main(brokenHtlcs.copy(relayedOut = brokenHtlcs.relayedOut - origin, settledUpstream = brokenHtlcs.settledUpstream - origin))
} else {
context become main(brokenHtlcs.copy(relayedOut = brokenHtlcs.relayedOut + (origin -> relayedOut1), settledUpstream = brokenHtlcs.settledUpstream + origin))
}
case _: Origin.Relayed =>
log.error(s"unsupported origin: ${origin.getClass.getSimpleName}")
}
case None =>
log.error(s"received fulfill with unknown origin $origin for htlcId=${fulfilledHtlc.id}, channelId=${fulfilledHtlc.channelId}: cannot forward upstream")
}
private def handleDownstreamFailure(brokenHtlcs: BrokenHtlcs, origin: Origin, failedHtlc: UpdateAddHtlc): Unit =
brokenHtlcs.relayedOut.get(origin) match {
case Some(relayedOut) =>
// If this is a local payment, we need to update the DB:
origin match {
case Origin.Local(id, _) => nodeParams.db.payments.updateOutgoingPayment(PaymentFailed(id, failedHtlc.paymentHash, Nil))
case _ =>
}
val relayedOut1 = relayedOut diff Seq((failedHtlc.channelId, failedHtlc.id))
// This was the last downstream HTLC we were waiting for.
if (relayedOut1.isEmpty) {
// If we haven't already settled upstream, we can fail now.
if (!brokenHtlcs.settledUpstream.contains(origin)) {
origin match {
case Origin.Local(id, _) => nodeParams.db.payments.getOutgoingPayment(id).foreach(p => {
val payments = nodeParams.db.payments.listOutgoingPayments(p.parentId)
if (payments.forall(_.status.isInstanceOf[OutgoingPaymentStatus.Failed])) {
log.warning(s"payment failed for paymentHash=${failedHtlc.paymentHash}")
context.system.eventStream.publish(PaymentFailed(p.parentId, failedHtlc.paymentHash, Nil))
}
})
case Origin.TrampolineRelayed(origins, _) =>
log.warning(s"payment failed for paymentHash=${failedHtlc.paymentHash}: failing ${origins.length} upstream HTLCs")
origins.foreach { case (channelId, htlcId) =>
// We don't bother decrypting the downstream failure to forward a more meaningful error upstream, it's
// very likely that it won't be actionable anyway because of our node restart.
commandBuffer ! CommandBuffer.CommandSend(channelId, CMD_FAIL_HTLC(htlcId, Right(TemporaryNodeFailure), commit = true))
}
case _: Origin.Relayed =>
log.error(s"unsupported origin: ${origin.getClass.getSimpleName}")
}
}
// We can forget about this payment since it has been fully settled downstream and upstream.
context become main(brokenHtlcs.copy(relayedOut = brokenHtlcs.relayedOut - origin, settledUpstream = brokenHtlcs.settledUpstream - origin))
} else {
context become main(brokenHtlcs.copy(relayedOut = brokenHtlcs.relayedOut + (origin -> relayedOut1)))
}
case None =>
log.error(s"received failure with unknown origin $origin for htlcId=${failedHtlc.id}, channelId=${failedHtlc.channelId}")
}
}
object PostRestartHtlcCleaner {
def props(nodeParams: NodeParams, commandBuffer: ActorRef, initialized: Option[Promise[Done]] = None) = Props(classOf[PostRestartHtlcCleaner], nodeParams, commandBuffer, initialized)
/**
* @param add incoming HTLC that was committed upstream.
* @param preimage payment preimage if the payment succeeded downstream.
*/
case class IncomingHtlc(add: UpdateAddHtlc, preimage: Option[ByteVector32])
/**
* Payments that may be in a broken state after a restart.
*
* @param notRelayed incoming HTLCs that were committed upstream but not relayed downstream.
* @param relayedOut outgoing HTLC sets that may have been incompletely sent and need to be watched.
* @param settledUpstream upstream payments that have already been settled (failed or fulfilled) by this actor.
*/
case class BrokenHtlcs(notRelayed: Seq[IncomingHtlc], relayedOut: Map[Origin, Seq[(ByteVector32, Long)]], settledUpstream: Set[Origin])
/** Returns true if the given HTLC matches the given origin. */
private def matchesOrigin(htlcIn: UpdateAddHtlc, origin: Origin): Boolean = origin match {
case _: Origin.Local => false
case Origin.Relayed(originChannelId, originHtlcId, _, _) => originChannelId == htlcIn.channelId && originHtlcId == htlcIn.id
case Origin.TrampolineRelayed(origins, _) => origins.exists {
case (originChannelId, originHtlcId) => originChannelId == htlcIn.channelId && originHtlcId == htlcIn.id
}
}
/**
* When we restart while we're receiving a payment, we need to look at the DB to find out whether the payment
* succeeded or not (which may have triggered external downstream components to treat the payment as received and
* ship some physical goods to a customer).
*/
private def shouldFulfill(finalPacket: IncomingPacket.FinalPacket, paymentsDb: IncomingPaymentsDb): Option[ByteVector32] =
paymentsDb.getIncomingPayment(finalPacket.add.paymentHash) match {
case Some(IncomingPayment(_, preimage, _, IncomingPaymentStatus.Received(_, _))) => Some(preimage)
case _ => None
}
/**
* If we do nothing after a restart, incoming HTLCs that were committed upstream but not relayed will eventually
* expire and we won't lose money, but the channel will get closed, which is a major inconvenience. We want to detect
* this and fast-fail those HTLCs and thus preserve channels.
*
* Outgoing HTLC sets that are still pending may either succeed or fail: we need to watch them to properly forward the
* result upstream to preserve channels.
*/
private def checkBrokenHtlcs(channels: Seq[HasCommitments], paymentsDb: IncomingPaymentsDb, privateKey: PrivateKey, features: ByteVector)(implicit log: LoggingAdapter): BrokenHtlcs = {
// We are interested in incoming HTLCs, that have been *cross-signed* (otherwise they wouldn't have been relayed).
// They signed it first, so the HTLC will first appear in our commitment tx, and later on in their commitment when
// we subsequently sign it. That's why we need to look in *their* commitment with direction=OUT.
val htlcsIn = channels
.flatMap(_.commitments.remoteCommit.spec.htlcs)
.filter(_.direction == OUT)
.map(_.add)
.map(IncomingPacket.decrypt(_, privateKey, features))
.collect {
// When we're not the final recipient, we'll only consider HTLCs that aren't relayed downstream, so no need to look for a preimage.
case Right(IncomingPacket.ChannelRelayPacket(add, _, _)) => IncomingHtlc(add, None)
case Right(IncomingPacket.NodeRelayPacket(add, _, _, _)) => IncomingHtlc(add, None)
// When we're the final recipient, we want to know if we want to fulfill or fail.
case Right(p@IncomingPacket.FinalPacket(add, _)) => IncomingHtlc(add, shouldFulfill(p, paymentsDb))
}
// We group relayed outgoing HTLCs by their origin.
val relayedOut = channels
.flatMap(c => c.commitments.originChannels.map { case (outgoingHtlcId, origin) => (origin, c.channelId, outgoingHtlcId) })
.groupBy { case (origin, _, _) => origin }
.mapValues(_.map { case (_, channelId, htlcId) => (channelId, htlcId) })
val notRelayed = htlcsIn.filterNot(htlcIn => relayedOut.keys.exists(origin => matchesOrigin(htlcIn.add, origin)))
log.info(s"htlcsIn=${htlcsIn.length} notRelayed=${notRelayed.length} relayedOut=${relayedOut.values.flatten.size}")
BrokenHtlcs(notRelayed, relayedOut, Set.empty)
}
/**
* We store [[CMD_FULFILL_HTLC]]/[[CMD_FAIL_HTLC]]/[[CMD_FAIL_MALFORMED_HTLC]] in a database
* (see [[fr.acinq.eclair.payment.relay.CommandBuffer]]) because we don't want to lose preimages, or to forget to fail
* incoming htlcs, which would lead to unwanted channel closings.
*
* Because of the way our watcher works, in a scenario where a downstream channel has gone to the blockchain, it may
* send several times the same command, and the upstream channel may have disappeared in the meantime.
*
* That's why we need to periodically clean up the pending relay db.
*/
private def cleanupRelayDb(channels: Seq[HasCommitments], relayDb: PendingRelayDb)(implicit log: LoggingAdapter): Unit = {
// We are interested in incoming HTLCs, that have been *cross-signed* (otherwise they wouldn't have been relayed).
// If the HTLC is not in their commitment, it means that we have already fulfilled/failed it and that we can remove
// the command from the pending relay db.
val channel2Htlc: Set[(ByteVector32, Long)] =
channels
.flatMap(_.commitments.remoteCommit.spec.htlcs)
.filter(_.direction == OUT)
.map(htlc => (htlc.add.channelId, htlc.add.id))
.toSet
val pendingRelay: Set[(ByteVector32, Long)] = relayDb.listPendingRelay()
val toClean = pendingRelay -- channel2Htlc
toClean.foreach {
case (channelId, htlcId) =>
log.info(s"cleaning up channelId=$channelId htlcId=$htlcId from relay db")
relayDb.removePendingRelay(channelId, htlcId)
}
}
}

View File

@ -18,20 +18,21 @@ package fr.acinq.eclair.payment.relay
import java.util.UUID
import akka.Done
import akka.actor.{Actor, ActorRef, DiagnosticActorLogging, Props, Status}
import akka.event.Logging.MDC
import akka.event.LoggingAdapter
import fr.acinq.bitcoin.ByteVector32
import fr.acinq.bitcoin.Crypto.PublicKey
import fr.acinq.eclair.channel._
import fr.acinq.eclair.db.{OutgoingPayment, OutgoingPaymentStatus}
import fr.acinq.eclair.payment._
import fr.acinq.eclair.router.Announcements
import fr.acinq.eclair.wire._
import fr.acinq.eclair.{Logs, LongToBtcAmount, MilliSatoshi, NodeParams, ShortChannelId}
import fr.acinq.eclair.{Logs, MilliSatoshi, NodeParams, ShortChannelId}
import grizzled.slf4j.Logging
import scala.collection.mutable
import scala.concurrent.Promise
// @formatter:off
sealed trait Origin
@ -40,7 +41,13 @@ object Origin {
case class Local(id: UUID, sender: Option[ActorRef]) extends Origin // we don't persist reference to local actors
/** Our node forwarded a single incoming HTLC to an outgoing channel. */
case class Relayed(originChannelId: ByteVector32, originHtlcId: Long, amountIn: MilliSatoshi, amountOut: MilliSatoshi) extends Origin
// TODO: @t-bast: add TrampolineRelayed
/**
* Our node forwarded an incoming HTLC set to a remote outgoing node (potentially producing multiple downstream HTLCs).
*
* @param origins origin channelIds and htlcIds.
* @param paymentSender actor sending the outgoing HTLC (if we haven't restarted and lost the reference).
*/
case class TrampolineRelayed(origins: List[(ByteVector32, Long)], paymentSender: Option[ActorRef]) extends Origin
}
// @formatter:on
@ -57,7 +64,7 @@ object Origin {
* It also receives channel HTLC events (fulfill / failed) and relays those to the appropriate handlers.
* It also maintains an up-to-date view of local channel balances.
*/
class Relayer(nodeParams: NodeParams, register: ActorRef, commandBuffer: ActorRef, paymentHandler: ActorRef) extends Actor with DiagnosticActorLogging {
class Relayer(nodeParams: NodeParams, router: ActorRef, register: ActorRef, commandBuffer: ActorRef, paymentHandler: ActorRef, initialized: Option[Promise[Done]] = None) extends Actor with DiagnosticActorLogging {
import Relayer._
@ -69,7 +76,9 @@ class Relayer(nodeParams: NodeParams, register: ActorRef, commandBuffer: ActorRe
context.system.eventStream.subscribe(self, classOf[AvailableBalanceChanged])
context.system.eventStream.subscribe(self, classOf[ShortChannelIdAssigned])
private val postRestartCleaner = context.actorOf(PostRestartHtlcCleaner.props(nodeParams, commandBuffer, initialized))
private val channelRelayer = context.actorOf(ChannelRelayer.props(nodeParams, self, register, commandBuffer))
private val nodeRelayer = context.actorOf(NodeRelayer.props(nodeParams, self, router, commandBuffer, register))
override def receive: Receive = main(Map.empty, new mutable.HashMap[PublicKey, mutable.Set[ShortChannelId]] with mutable.MultiMap[PublicKey, ShortChannelId])
@ -124,9 +133,7 @@ class Relayer(nodeParams: NodeParams, register: ActorRef, commandBuffer: ActorRe
log.warning(s"rejecting htlc #${add.id} from channelId=${add.channelId} to nodeId=${r.innerPayload.outgoingNodeId} reason=trampoline disabled")
commandBuffer ! CommandBuffer.CommandSend(add.channelId, CMD_FAIL_HTLC(add.id, Right(RequiredNodeFeatureMissing), commit = true))
} else {
// TODO: @t-bast: relay trampoline payload instead of rejecting.
log.warning(s"rejecting htlc #${add.id} from channelId=${add.channelId} to nodeId=${r.innerPayload.outgoingNodeId} reason=trampoline not implemented yet")
commandBuffer ! CommandBuffer.CommandSend(add.channelId, CMD_FAIL_HTLC(add.id, Right(RequiredNodeFeatureMissing), commit = true))
nodeRelayer forward r
}
case Left(badOnion: BadOnion) =>
log.warning(s"couldn't parse onion: reason=${badOnion.message}")
@ -140,49 +147,46 @@ class Relayer(nodeParams: NodeParams, register: ActorRef, commandBuffer: ActorRe
}
case Status.Failure(addFailed: AddHtlcFailed) =>
import addFailed.paymentHash
addFailed.origin match {
case Origin.Local(id, None) =>
handleLocalPaymentAfterRestart(PaymentFailed(id, paymentHash, Nil))
case Origin.Local(_, Some(sender)) =>
sender ! Status.Failure(addFailed)
case _: Origin.Relayed =>
channelRelayer forward Status.Failure(addFailed)
case Origin.Local(id, None) => log.error(s"received unexpected add failed with no sender (paymentId=$id)")
case Origin.Local(_, Some(sender)) => sender ! Status.Failure(addFailed)
case _: Origin.Relayed => channelRelayer forward Status.Failure(addFailed)
case Origin.TrampolineRelayed(htlcs, None) => log.error(s"received unexpected add failed with no sender (upstream=${htlcs.mkString(", ")}")
case Origin.TrampolineRelayed(_, Some(paymentSender)) => paymentSender ! Status.Failure(addFailed)
}
case ForwardFulfill(fulfill, to, add) =>
case ff@ForwardFulfill(fulfill, to, add) =>
to match {
case Origin.Local(id, None) =>
val feesPaid = 0.msat // fees are unknown since we lost the reference to the payment
handleLocalPaymentAfterRestart(PaymentSent(id, add.paymentHash, fulfill.paymentPreimage, Seq(PaymentSent.PartialPayment(id, add.amountMsat, feesPaid, add.channelId, None))))
case Origin.Local(_, Some(sender)) =>
sender ! fulfill
case Origin.Local(_, None) => postRestartCleaner forward ff
case Origin.Local(_, Some(sender)) => sender ! fulfill
case Origin.Relayed(originChannelId, originHtlcId, amountIn, amountOut) =>
val cmd = CMD_FULFILL_HTLC(originHtlcId, fulfill.paymentPreimage, commit = true)
commandBuffer ! CommandBuffer.CommandSend(originChannelId, cmd)
context.system.eventStream.publish(PaymentRelayed(amountIn, amountOut, add.paymentHash, fromChannelId = originChannelId, toChannelId = fulfill.channelId))
context.system.eventStream.publish(ChannelPaymentRelayed(amountIn, amountOut, add.paymentHash, originChannelId, fulfill.channelId))
case Origin.TrampolineRelayed(_, None) => postRestartCleaner forward ff
case Origin.TrampolineRelayed(_, Some(paymentSender)) => paymentSender ! fulfill
}
case ForwardFail(fail, to, add) =>
case ff@ForwardFail(fail, to, _) =>
to match {
case Origin.Local(id, None) =>
handleLocalPaymentAfterRestart(PaymentFailed(id, add.paymentHash, Nil))
case Origin.Local(_, Some(sender)) =>
sender ! fail
case Origin.Local(_, None) => postRestartCleaner forward ff
case Origin.Local(_, Some(sender)) => sender ! fail
case Origin.Relayed(originChannelId, originHtlcId, _, _) =>
val cmd = CMD_FAIL_HTLC(originHtlcId, Left(fail.reason), commit = true)
commandBuffer ! CommandBuffer.CommandSend(originChannelId, cmd)
case Origin.TrampolineRelayed(_, None) => postRestartCleaner forward ff
case Origin.TrampolineRelayed(_, Some(paymentSender)) => paymentSender ! fail
}
case ForwardFailMalformed(fail, to, add) =>
case ff@ForwardFailMalformed(fail, to, _) =>
to match {
case Origin.Local(id, None) =>
handleLocalPaymentAfterRestart(PaymentFailed(id, add.paymentHash, Nil))
case Origin.Local(_, Some(sender)) =>
sender ! fail
case Origin.Local(_, None) => postRestartCleaner forward ff
case Origin.Local(_, Some(sender)) => sender ! fail
case Origin.Relayed(originChannelId, originHtlcId, _, _) =>
val cmd = CMD_FAIL_MALFORMED_HTLC(originHtlcId, fail.onionHash, fail.failureCode, commit = true)
commandBuffer ! CommandBuffer.CommandSend(originChannelId, cmd)
case Origin.TrampolineRelayed(_, None) => postRestartCleaner forward ff
case Origin.TrampolineRelayed(_, Some(paymentSender)) => paymentSender ! fail
}
case ack: CommandBuffer.CommandAck => commandBuffer forward ack
@ -190,37 +194,6 @@ class Relayer(nodeParams: NodeParams, register: ActorRef, commandBuffer: ActorRe
case "ok" => () // ignoring responses from channels
}
/**
* It may happen that we sent a payment and then re-started before the payment completed.
* When we receive the HTLC fulfill/fail associated to that payment, the payment FSM that generated them doesn't exist
* anymore so we need to reconcile the database.
*/
def handleLocalPaymentAfterRestart(paymentResult: PaymentEvent): Unit = paymentResult match {
case e: PaymentFailed =>
nodeParams.db.payments.updateOutgoingPayment(e)
// Since payments can be multi-part, we only emit the payment failed event once all child payments have failed.
nodeParams.db.payments.getOutgoingPayment(e.id).foreach(p => {
val payments = nodeParams.db.payments.listOutgoingPayments(p.parentId)
if (payments.forall(_.status.isInstanceOf[OutgoingPaymentStatus.Failed])) {
context.system.eventStream.publish(PaymentFailed(p.parentId, e.paymentHash, Nil))
}
})
case e: PaymentSent =>
nodeParams.db.payments.updateOutgoingPayment(e)
// Since payments can be multi-part, we only emit the payment sent event once all child payments have settled.
nodeParams.db.payments.getOutgoingPayment(e.id).foreach(p => {
val payments = nodeParams.db.payments.listOutgoingPayments(p.parentId)
if (!payments.exists(p => p.status == OutgoingPaymentStatus.Pending)) {
val succeeded = payments.collect {
case OutgoingPayment(id, _, _, _, amount, _, _, _, OutgoingPaymentStatus.Succeeded(_, feesPaid, _, completedAt)) =>
PaymentSent.PartialPayment(id, amount, feesPaid, ByteVector32.Zeroes, None, completedAt)
}
context.system.eventStream.publish(PaymentSent(p.parentId, e.paymentHash, e.paymentPreimage, succeeded))
}
})
case _ =>
}
override def mdc(currentMessage: Any): MDC = {
val paymentHash_opt = currentMessage match {
case ForwardAdd(add, _) => Some(add.paymentHash)
@ -237,7 +210,8 @@ class Relayer(nodeParams: NodeParams, register: ActorRef, commandBuffer: ActorRe
object Relayer extends Logging {
def props(nodeParams: NodeParams, register: ActorRef, commandBuffer: ActorRef, paymentHandler: ActorRef) = Props(classOf[Relayer], nodeParams, register, commandBuffer, paymentHandler)
def props(nodeParams: NodeParams, router: ActorRef, register: ActorRef, commandBuffer: ActorRef, paymentHandler: ActorRef, initialized: Option[Promise[Done]] = None) =
Props(classOf[Relayer], nodeParams, router, register, commandBuffer, paymentHandler, initialized)
type ChannelUpdates = Map[ShortChannelId, OutgoingChannel]
type NodeChannels = mutable.HashMap[PublicKey, mutable.Set[ShortChannelId]] with mutable.MultiMap[PublicKey, ShortChannelId]

View File

@ -22,7 +22,7 @@ import akka.actor.{ActorRef, FSM, Props}
import akka.event.Logging.MDC
import fr.acinq.bitcoin.ByteVector32
import fr.acinq.bitcoin.Crypto.PublicKey
import fr.acinq.eclair.channel.Commitments
import fr.acinq.eclair.channel.{Commitments, Upstream}
import fr.acinq.eclair.crypto.Sphinx
import fr.acinq.eclair.payment.PaymentRequest.ExtraHop
import fr.acinq.eclair.payment.PaymentSent.PartialPayment
@ -94,7 +94,8 @@ class MultiPartPaymentLifecycle(nodeParams: NodeParams, cfg: SendPaymentConfig,
val pending = setFees(d.request.routeParams, payments, payments.size)
Kamon.runWithContextEntry(parentPaymentIdKey, cfg.parentId) {
Kamon.runWithSpan(span, finishSpan = true) {
pending.foreach { case (childId, payment) => spawnChildPaymentFsm(childId) ! payment }
pending.headOption.foreach { case (childId, payment) => spawnChildPaymentFsm(childId, includeTrampolineFees = true) ! payment }
pending.tail.foreach { case (childId, payment) => spawnChildPaymentFsm(childId, includeTrampolineFees = false) ! payment }
}
}
goto(PAYMENT_IN_PROGRESS) using PaymentProgress(d.sender, d.request, d.networkStats, channels.length, 0 msat, d.request.maxAttempts - 1, pending, Set.empty, Nil)
@ -150,7 +151,7 @@ class MultiPartPaymentLifecycle(nodeParams: NodeParams, cfg: SendPaymentConfig,
goto(PAYMENT_ABORTED) using PaymentAborted(d.sender, d.request, d.failures :+ LocalFailure(BalanceTooLow), d.pending.keySet)
} else {
val pending = setFees(d.request.routeParams, payments, payments.size + d.pending.size)
pending.foreach { case (childId, payment) => spawnChildPaymentFsm(childId) ! payment }
pending.foreach { case (childId, payment) => spawnChildPaymentFsm(childId, includeTrampolineFees = false) ! payment }
goto(PAYMENT_IN_PROGRESS) using d.copy(toSend = 0 msat, remainingAttempts = d.remainingAttempts - 1, pending = d.pending ++ pending, channelsCount = channels.length)
}
@ -224,8 +225,17 @@ class MultiPartPaymentLifecycle(nodeParams: NodeParams, cfg: SendPaymentConfig,
}
}
def spawnChildPaymentFsm(childId: UUID): ActorRef = {
val childCfg = cfg.copy(id = childId, publishEvent = false)
def spawnChildPaymentFsm(childId: UUID, includeTrampolineFees: Boolean): ActorRef = {
val upstream = cfg.upstream match {
case Upstream.Local(_) => Upstream.Local(childId)
case _ => cfg.upstream
}
// We attach the trampoline fees to the first child in order to account for them in the DB.
// This is hackish and won't work if the first child payment fails and is retried, but it's okay-ish for an MVP.
// We will update the DB schema to contain accurate Trampoline reporting, which will fix that in the future.
// TODO: @t-bast: fix that once the DB schema is updated
val trampolineData = if (includeTrampolineFees) cfg.trampolineData else cfg.trampolineData.map(_.copy(trampolineFees = 0 msat))
val childCfg = cfg.copy(id = childId, publishEvent = false, upstream = upstream, trampolineData = trampolineData)
context.actorOf(PaymentLifecycle.props(nodeParams, childCfg, router, register))
}

View File

@ -21,7 +21,7 @@ import java.util.UUID
import akka.actor.{Actor, ActorLogging, ActorRef, Props}
import fr.acinq.bitcoin.ByteVector32
import fr.acinq.bitcoin.Crypto.PublicKey
import fr.acinq.eclair.channel.Channel
import fr.acinq.eclair.channel.{Channel, Upstream}
import fr.acinq.eclair.crypto.Sphinx
import fr.acinq.eclair.payment.PaymentRequest.ExtraHop
import fr.acinq.eclair.payment.send.MultiPartPaymentLifecycle.SendMultiPartPayment
@ -43,7 +43,7 @@ class PaymentInitiator(nodeParams: NodeParams, router: ActorRef, relayer: ActorR
case r: SendPaymentRequest =>
val paymentId = UUID.randomUUID()
sender ! paymentId
val paymentCfg = SendPaymentConfig(paymentId, paymentId, r.externalId, r.paymentHash, r.targetNodeId, r.paymentRequest, storeInDb = true, publishEvent = true)
val paymentCfg = SendPaymentConfig(paymentId, paymentId, r.externalId, r.paymentHash, r.targetNodeId, Upstream.Local(paymentId), r.paymentRequest, storeInDb = true, publishEvent = true)
val finalExpiry = r.finalExpiry(nodeParams.currentBlockHeight)
r.paymentRequest match {
case Some(invoice) if !invoice.features.supported =>
@ -70,7 +70,7 @@ class PaymentInitiator(nodeParams: NodeParams, router: ActorRef, relayer: ActorR
if (!r.paymentRequest.features.allowTrampoline && r.paymentRequest.amount.isEmpty) {
sender ! PaymentFailed(paymentId, r.paymentRequest.paymentHash, LocalFailure(new IllegalArgumentException("cannot pay a 0-value invoice via trampoline-to-legacy (trampoline may steal funds)")) :: Nil)
} else {
val paymentCfg = SendPaymentConfig(paymentId, paymentId, None, r.paymentRequest.paymentHash, r.trampolineNodeId, Some(r.paymentRequest), storeInDb = true, publishEvent = true)
val paymentCfg = SendPaymentConfig(paymentId, paymentId, None, r.paymentRequest.paymentHash, r.trampolineNodeId, Upstream.Local(paymentId), Some(r.paymentRequest), storeInDb = true, publishEvent = true, Some(r))
val finalPayload = if (r.paymentRequest.features.allowMultiPart) {
Onion.createMultiPartPayload(r.finalAmount, r.finalAmount, r.finalExpiry(nodeParams.currentBlockHeight), r.paymentRequest.paymentSecret.get)
} else {
@ -139,8 +139,11 @@ object PaymentInitiator {
externalId: Option[String],
paymentHash: ByteVector32,
targetNodeId: PublicKey,
upstream: Upstream,
paymentRequest: Option[PaymentRequest],
storeInDb: Boolean, // e.g. for trampoline we don't want to store in the DB when we're relaying payments
publishEvent: Boolean)
publishEvent: Boolean,
// TODO: @t-bast: this is a very awkward work-around to get accurate data in the DB: fix this once we update the DB schema
trampolineData: Option[SendTrampolinePaymentRequest] = None)
}

View File

@ -71,7 +71,9 @@ class PaymentLifecycle(nodeParams: NodeParams, cfg: SendPaymentConfig, router: A
val send = SendPayment(c.paymentHash, c.hops.last, c.finalPayload, maxAttempts = 1)
router ! FinalizeRoute(c.hops)
if (cfg.storeInDb) {
paymentsDb.addOutgoingPayment(OutgoingPayment(id, cfg.parentId, cfg.externalId, cfg.paymentHash, c.finalPayload.amount, cfg.targetNodeId, Platform.currentTime, cfg.paymentRequest, OutgoingPaymentStatus.Pending))
val targetNodeId = cfg.trampolineData.map(_.paymentRequest.nodeId).getOrElse(cfg.targetNodeId)
val finalAmount = c.finalPayload.amount - cfg.trampolineData.map(_.trampolineFees).getOrElse(0 msat)
paymentsDb.addOutgoingPayment(OutgoingPayment(id, cfg.parentId, cfg.externalId, cfg.paymentHash, finalAmount, targetNodeId, Platform.currentTime, cfg.paymentRequest, OutgoingPaymentStatus.Pending))
}
goto(WAITING_FOR_ROUTE) using WaitingForRoute(sender, send, failures = Nil)
@ -89,7 +91,9 @@ class PaymentLifecycle(nodeParams: NodeParams, cfg: SendPaymentConfig, router: A
router ! RouteRequest(c.getRouteRequestStart(nodeParams), c.targetNodeId, c.finalPayload.amount, c.assistedRoutes, routeParams = c.routeParams, ignoreNodes = ignoredNodes)
}
if (cfg.storeInDb) {
paymentsDb.addOutgoingPayment(OutgoingPayment(id, cfg.parentId, cfg.externalId, cfg.paymentHash, c.finalPayload.amount, cfg.targetNodeId, Platform.currentTime, cfg.paymentRequest, OutgoingPaymentStatus.Pending))
val targetNodeId = cfg.trampolineData.map(_.paymentRequest.nodeId).getOrElse(cfg.targetNodeId)
val finalAmount = c.finalPayload.amount - cfg.trampolineData.map(_.trampolineFees).getOrElse(0 msat)
paymentsDb.addOutgoingPayment(OutgoingPayment(id, cfg.parentId, cfg.externalId, cfg.paymentHash, finalAmount, targetNodeId, Platform.currentTime, cfg.paymentRequest, OutgoingPaymentStatus.Pending))
}
goto(WAITING_FOR_ROUTE) using WaitingForRoute(sender, c, failures = Nil)
}
@ -99,7 +103,7 @@ class PaymentLifecycle(nodeParams: NodeParams, cfg: SendPaymentConfig, router: A
val hops = c.routePrefix ++ routeHops
log.info(s"route found: attempt=${failures.size + 1}/${c.maxAttempts} route=${hops.map(_.nextNodeId).mkString("->")} channels=${hops.map(_.lastUpdate.shortChannelId).mkString("->")}")
val firstHop = hops.head
val (cmd, sharedSecrets) = OutgoingPacket.buildCommand(id, c.paymentHash, hops, c.finalPayload)
val (cmd, sharedSecrets) = OutgoingPacket.buildCommand(cfg.upstream, c.paymentHash, hops, c.finalPayload)
register ! Register.ForwardShortId(firstHop.lastUpdate.shortChannelId, cmd)
goto(WAITING_FOR_PAYMENT_COMPLETE) using WaitingForComplete(s, c, cmd, failures, sharedSecrets, ignoreNodes, ignoreChannels, hops)
@ -112,7 +116,8 @@ class PaymentLifecycle(nodeParams: NodeParams, cfg: SendPaymentConfig, router: A
case Event("ok", _) => stay
case Event(fulfill: UpdateFulfillHtlc, WaitingForComplete(s, c, cmd, _, _, _, _, route)) =>
val p = PartialPayment(id, c.finalPayload.amount, cmd.amount - c.finalPayload.amount, fulfill.channelId, Some(route))
val trampolineFees = cfg.trampolineData.map(_.trampolineFees).getOrElse(0 msat)
val p = PartialPayment(id, c.finalPayload.amount - trampolineFees, cmd.amount - c.finalPayload.amount + trampolineFees, fulfill.channelId, Some(route))
onSuccess(s, PaymentSent(id, c.paymentHash, fulfill.paymentPreimage, p :: Nil))
myStop()

View File

@ -193,10 +193,16 @@ object ChannelCodecs extends Logging {
// this is for backward compatibility to handle legacy payments that didn't have identifiers
val UNKNOWN_UUID = UUID.fromString("00000000-0000-0000-0000-000000000000")
val trampolineRelayedCodec: Codec[Origin.TrampolineRelayed] = (
listOfN(uint16, bytes32 ~ int64) ::
("sender" | provide(Option.empty[ActorRef]))
).as[Origin.TrampolineRelayed]
val originCodec: Codec[Origin] = discriminated[Origin].by(uint16)
.typecase(0x03, localCodec) // backward compatible
.typecase(0x01, provide(Origin.Local(UNKNOWN_UUID, None)))
.typecase(0x02, relayedCodec)
.typecase(0x04, trampolineRelayedCodec)
val originsListCodec: Codec[List[(Long, Origin)]] = listOfN(uint16, int64 ~ originCodec)

View File

@ -271,7 +271,7 @@ object Onion {
/** Create a trampoline inner payload instructing the trampoline node to relay via a non-trampoline payment. */
def createNodeRelayToNonTrampolinePayload(amount: MilliSatoshi, totalAmount: MilliSatoshi, expiry: CltvExpiry, targetNodeId: PublicKey, invoice: PaymentRequest): NodeRelayPayload = {
val tlvs = Seq[OnionTlv](AmountToForward(amount), OutgoingCltv(expiry), OutgoingNodeId(targetNodeId), InvoiceFeatures(invoice.features.bitmask.bytes), InvoiceRoutingInfo(invoice.routingInfo.toList.map(_.toList)))
val tlvs = Seq[OnionTlv](AmountToForward(amount), OutgoingCltv(expiry), OutgoingNodeId(targetNodeId), InvoiceFeatures(invoice.features.toByteVector), InvoiceRoutingInfo(invoice.routingInfo.toList.map(_.toList)))
val tlvs2 = invoice.paymentSecret.map(s => tlvs :+ PaymentData(s, totalAmount)).getOrElse(tlvs)
NodeRelayPayload(TlvStream(tlvs2))
}

View File

@ -32,6 +32,10 @@ class CltvExpirySpec extends FunSuite with ParallelTestExecution {
assert(d + 5 === CltvExpiryDelta(566))
assert(d + CltvExpiryDelta(5) === CltvExpiryDelta(566))
// subtract
assert(d - CltvExpiryDelta(5) === CltvExpiryDelta(556))
assert(d - CltvExpiryDelta(562) === CltvExpiryDelta(-1))
// compare
assert(d <= CltvExpiryDelta(561))
assert(d < CltvExpiryDelta(562))

View File

@ -60,8 +60,8 @@ class FuzzySpec extends TestkitBaseClass with StateTestsHelperMethods with Loggi
val commandBufferB = system.actorOf(Props(new TestCommandBuffer(Bob.nodeParams, registerB)))
val paymentHandlerA = system.actorOf(Props(new PaymentHandler(Alice.nodeParams, commandBufferA)))
val paymentHandlerB = system.actorOf(Props(new PaymentHandler(Bob.nodeParams, commandBufferB)))
val relayerA = system.actorOf(Relayer.props(Alice.nodeParams, registerA, commandBufferA, paymentHandlerA))
val relayerB = system.actorOf(Relayer.props(Bob.nodeParams, registerB, commandBufferB, paymentHandlerB))
val relayerA = system.actorOf(Relayer.props(Alice.nodeParams, TestProbe().ref, registerA, commandBufferA, paymentHandlerA))
val relayerB = system.actorOf(Relayer.props(Bob.nodeParams, TestProbe().ref, registerB, commandBufferB, paymentHandlerB))
val router = TestProbe()
val wallet = new TestWallet
val alice: TestFSMRef[State, Data, Channel] = TestFSMRef(new Channel(Alice.nodeParams, wallet, Bob.nodeParams.nodeId, alice2blockchain.ref, router.ref, relayerA))
@ -121,7 +121,7 @@ class FuzzySpec extends TestkitBaseClass with StateTestsHelperMethods with Loggi
// allow overpaying (no more than 2 times the required amount)
val amount = requiredAmount + Random.nextInt(requiredAmount.toLong.toInt).msat
val expiry = (Channel.MIN_CLTV_EXPIRY_DELTA + 1).toCltvExpiry(blockHeight = 400000)
OutgoingPacket.buildCommand(UUID.randomUUID(), paymentHash, ChannelHop(null, dest, null) :: Nil, FinalLegacyPayload(amount, expiry))._1
OutgoingPacket.buildCommand(Upstream.Local(UUID.randomUUID()), paymentHash, ChannelHop(null, dest, null) :: Nil, FinalLegacyPayload(amount, expiry))._1
}
def initiatePaymentOrStop(remaining: Int): Unit =

View File

@ -34,7 +34,6 @@ import org.scalatest.FunSuite
import scala.concurrent.duration._
import scala.util.Random
class ThroughputSpec extends FunSuite {
ignore("throughput") {
implicit val system = ActorSystem("test")
@ -67,8 +66,8 @@ class ThroughputSpec extends FunSuite {
val registerB = TestProbe()
val commandBufferA = system.actorOf(Props(new CommandBuffer(Alice.nodeParams, registerA.ref)))
val commandBufferB = system.actorOf(Props(new CommandBuffer(Bob.nodeParams, registerB.ref)))
val relayerA = system.actorOf(Relayer.props(Alice.nodeParams, registerA.ref, commandBufferA, paymentHandler))
val relayerB = system.actorOf(Relayer.props(Bob.nodeParams, registerB.ref, commandBufferB, paymentHandler))
val relayerA = system.actorOf(Relayer.props(Alice.nodeParams, TestProbe().ref, registerA.ref, commandBufferA, paymentHandler))
val relayerB = system.actorOf(Relayer.props(Bob.nodeParams, TestProbe().ref, registerB.ref, commandBufferB, paymentHandler))
val wallet = new TestWallet
val alice = system.actorOf(Channel.props(Alice.nodeParams, wallet, Bob.nodeParams.nodeId, blockchain, ???, relayerA, None), "a")
val bob = system.actorOf(Channel.props(Bob.nodeParams, wallet, Alice.nodeParams.nodeId, blockchain, ???, relayerB, None), "b")

View File

@ -114,7 +114,7 @@ trait StateTestsHelperMethods extends TestKitBase with fixture.TestSuite with Pa
val payment_preimage: ByteVector32 = randomBytes32
val payment_hash: ByteVector32 = Crypto.sha256(payment_preimage)
val expiry = CltvExpiryDelta(144).toCltvExpiry(currentBlockHeight)
val cmd = OutgoingPacket.buildCommand(UUID.randomUUID, payment_hash, ChannelHop(null, destination, null) :: Nil, FinalLegacyPayload(amount, expiry))._1.copy(commit = false)
val cmd = OutgoingPacket.buildCommand(Upstream.Local(UUID.randomUUID), payment_hash, ChannelHop(null, destination, null) :: Nil, FinalLegacyPayload(amount, expiry))._1.copy(commit = false)
(payment_preimage, cmd)
}

View File

@ -114,6 +114,27 @@ class NormalStateSpec extends TestkitBaseClass with StateTestsHelperMethods {
)))
}
test("recv CMD_ADD_HTLC (trampoline relayed htlc)") { f =>
import f._
val initialState = alice.stateData.asInstanceOf[DATA_NORMAL]
val sender = TestProbe()
val h = randomBytes32
val originHtlc1 = UpdateAddHtlc(randomBytes32, 47, 30000000 msat, h, CltvExpiryDelta(144).toCltvExpiry(currentBlockHeight), TestConstants.emptyOnionPacket)
val originHtlc2 = UpdateAddHtlc(randomBytes32, 32, 20000000 msat, h, CltvExpiryDelta(160).toCltvExpiry(currentBlockHeight), TestConstants.emptyOnionPacket)
val upstream = Upstream.TrampolineRelayed(originHtlc1 :: originHtlc2 :: Nil)
val cmd = CMD_ADD_HTLC(originHtlc1.amountMsat + originHtlc2.amountMsat - 10000.msat, h, originHtlc2.cltvExpiry - CltvExpiryDelta(7), TestConstants.emptyOnionPacket, upstream)
sender.send(alice, cmd)
sender.expectMsg("ok")
val htlc = alice2bob.expectMsgType[UpdateAddHtlc]
assert(htlc.id == 0 && htlc.paymentHash == h)
awaitCond(alice.stateData == initialState.copy(
commitments = initialState.commitments.copy(
localNextHtlcId = 1,
localChanges = initialState.commitments.localChanges.copy(proposed = htlc :: Nil),
originChannels = Map(0L -> Origin.TrampolineRelayed((originHtlc1.channelId, originHtlc1.id) :: (originHtlc2.channelId, originHtlc2.id) :: Nil, Some(sender.ref)))
)))
}
test("recv CMD_ADD_HTLC (expiry too small)") { f =>
import f._
val sender = TestProbe()
@ -887,7 +908,6 @@ class NormalStateSpec extends TestkitBaseClass with StateTestsHelperMethods {
// now bob will forward the htlc downstream
val forward = relayerB.expectMsgType[ForwardAdd]
assert(forward.add === htlc)
}
test("recv RevokeAndAck (multiple htlcs in both directions)") { f =>

View File

@ -59,7 +59,7 @@ class ShutdownStateSpec extends TestkitBaseClass with StateTestsHelperMethods {
val h1 = Crypto.sha256(r1)
val amount1 = 300000000 msat
val expiry1 = CltvExpiryDelta(144).toCltvExpiry(currentBlockHeight)
val cmd1 = OutgoingPacket.buildCommand(UUID.randomUUID, h1, ChannelHop(null, TestConstants.Bob.nodeParams.nodeId, null) :: Nil, FinalLegacyPayload(amount1, expiry1))._1.copy(commit = false)
val cmd1 = OutgoingPacket.buildCommand(Upstream.Local(UUID.randomUUID), h1, ChannelHop(null, TestConstants.Bob.nodeParams.nodeId, null) :: Nil, FinalLegacyPayload(amount1, expiry1))._1.copy(commit = false)
sender.send(alice, cmd1)
sender.expectMsg("ok")
val htlc1 = alice2bob.expectMsgType[UpdateAddHtlc]
@ -69,7 +69,7 @@ class ShutdownStateSpec extends TestkitBaseClass with StateTestsHelperMethods {
val h2 = Crypto.sha256(r2)
val amount2 = 200000000 msat
val expiry2 = CltvExpiryDelta(144).toCltvExpiry(currentBlockHeight)
val cmd2 = OutgoingPacket.buildCommand(UUID.randomUUID, h2, ChannelHop(null, TestConstants.Bob.nodeParams.nodeId, null) :: Nil, FinalLegacyPayload(amount2, expiry2))._1.copy(commit = false)
val cmd2 = OutgoingPacket.buildCommand(Upstream.Local(UUID.randomUUID), h2, ChannelHop(null, TestConstants.Bob.nodeParams.nodeId, null) :: Nil, FinalLegacyPayload(amount2, expiry2))._1.copy(commit = false)
sender.send(alice, cmd2)
sender.expectMsg("ok")
val htlc2 = alice2bob.expectMsgType[UpdateAddHtlc]

View File

@ -48,17 +48,20 @@ class SqliteAuditDbSpec extends FunSuite {
val pp2a = PaymentReceived.PartialPayment(42000 msat, randomBytes32)
val pp2b = PaymentReceived.PartialPayment(42100 msat, randomBytes32)
val e2 = PaymentReceived(randomBytes32, pp2a :: pp2b :: Nil)
val e3 = PaymentRelayed(42000 msat, 1000 msat, randomBytes32, randomBytes32, randomBytes32)
val e3 = ChannelPaymentRelayed(42000 msat, 1000 msat, randomBytes32, randomBytes32, randomBytes32)
val e4 = NetworkFeePaid(null, randomKey.publicKey, randomBytes32, Transaction(0, Seq.empty, Seq.empty, 0), 42 sat, "mutual")
val pp5a = PaymentSent.PartialPayment(UUID.randomUUID(), 42000 msat, 1000 msat, randomBytes32, None, timestamp = 0)
val pp5b = PaymentSent.PartialPayment(UUID.randomUUID(), 42100 msat, 900 msat, randomBytes32, None, timestamp = 1)
val e5 = PaymentSent(ChannelCodecs.UNKNOWN_UUID, randomBytes32, randomBytes32, pp5a :: pp5b :: Nil)
val pp6 = PaymentSent.PartialPayment(UUID.randomUUID(), 42000 msat, 1000 msat, randomBytes32, None, timestamp = (Platform.currentTime.milliseconds + 10.minutes).toMillis)
val e6 = PaymentSent(ChannelCodecs.UNKNOWN_UUID, randomBytes32, randomBytes32, pp6 :: Nil)
val e7 = AvailableBalanceChanged(null, randomBytes32, ShortChannelId(500000, 42, 1), 456123000 msat, ChannelCodecsSpec.commitments)
val e7 = AvailableBalanceChanged(null, randomBytes32, ShortChannelId(500000, 42, 1), 456123000 msat, ChannelCodecsSpec.normal.commitments)
val e8 = ChannelLifecycleEvent(randomBytes32, randomKey.publicKey, 456123000 sat, isFunder = true, isPrivate = false, "mutual")
val e9 = ChannelErrorOccurred(null, randomBytes32, randomKey.publicKey, null, LocalError(new RuntimeException("oops")), isFatal = true)
val e10 = ChannelErrorOccurred(null, randomBytes32, randomKey.publicKey, null, RemoteError(wire.Error(randomBytes32, "remote oops")), isFatal = true)
val e11 = TrampolinePaymentRelayed(42000 msat, 40000 msat, randomBytes32, randomKey.publicKey, Seq(randomBytes32), Seq(randomBytes32))
// TrampolinePaymentRelayed events are converted to ChannelPaymentRelayed events for now. We need to udpate the DB schema to fix this.
val e11bis = ChannelPaymentRelayed(42000 msat, 40000 msat, e11.paymentHash, e11.fromChannelIds.head, e11.toChannelIds.head)
db.add(e1)
db.add(e2)
@ -70,11 +73,12 @@ class SqliteAuditDbSpec extends FunSuite {
db.add(e8)
db.add(e9)
db.add(e10)
db.add(e11)
assert(db.listSent(from = 0L, to = (Platform.currentTime.milliseconds + 15.minute).toMillis).toSet === Set(e1, e5.copy(id = pp5a.id, parts = pp5a :: Nil), e5.copy(id = pp5b.id, parts = pp5b :: Nil), e6.copy(id = pp6.id)))
assert(db.listSent(from = 100000L, to = (Platform.currentTime.milliseconds + 1.minute).toMillis).toList === List(e1))
assert(db.listReceived(from = 0L, to = (Platform.currentTime.milliseconds + 1.minute).toMillis).toList === List(e2.copy(parts = pp2a :: Nil), e2.copy(parts = pp2b :: Nil)))
assert(db.listRelayed(from = 0L, to = (Platform.currentTime.milliseconds + 1.minute).toMillis).toList === List(e3))
assert(db.listRelayed(from = 0L, to = (Platform.currentTime.milliseconds + 1.minute).toMillis).toList === List(e3, e11bis))
assert(db.listNetworkFees(from = 0L, to = (Platform.currentTime.milliseconds + 1.minute).toMillis).size === 1)
assert(db.listNetworkFees(from = 0L, to = (Platform.currentTime.milliseconds + 1.minute).toMillis).head.txType === "mutual")
}
@ -91,10 +95,10 @@ class SqliteAuditDbSpec extends FunSuite {
val c2 = randomBytes32
val c3 = randomBytes32
db.add(PaymentRelayed(46000 msat, 44000 msat, randomBytes32, randomBytes32, c1))
db.add(PaymentRelayed(41000 msat, 40000 msat, randomBytes32, randomBytes32, c1))
db.add(PaymentRelayed(43000 msat, 42000 msat, randomBytes32, randomBytes32, c1))
db.add(PaymentRelayed(42000 msat, 40000 msat, randomBytes32, randomBytes32, c2))
db.add(ChannelPaymentRelayed(46000 msat, 44000 msat, randomBytes32, randomBytes32, c1))
db.add(ChannelPaymentRelayed(41000 msat, 40000 msat, randomBytes32, randomBytes32, c1))
db.add(ChannelPaymentRelayed(43000 msat, 42000 msat, randomBytes32, randomBytes32, c1))
db.add(ChannelPaymentRelayed(42000 msat, 40000 msat, randomBytes32, randomBytes32, c2))
db.add(NetworkFeePaid(null, n1, c1, Transaction(0, Seq.empty, Seq.empty, 0), 100 sat, "funding"))
db.add(NetworkFeePaid(null, n2, c2, Transaction(0, Seq.empty, Seq.empty, 0), 200 sat, "funding"))

View File

@ -35,11 +35,13 @@ import fr.acinq.eclair.crypto.Sphinx.DecryptedFailurePacket
import fr.acinq.eclair.db.{IncomingPayment, IncomingPaymentStatus, OutgoingPaymentStatus}
import fr.acinq.eclair.io.Peer
import fr.acinq.eclair.io.Peer.{Disconnect, PeerRoutingMessage}
import fr.acinq.eclair.payment.PaymentRequest.ExtraHop
import fr.acinq.eclair.payment._
import fr.acinq.eclair.payment.receive.MultiPartHandler.ReceivePayment
import fr.acinq.eclair.payment.receive.{ForwardHandler, PaymentHandler}
import fr.acinq.eclair.payment.relay.Relayer
import fr.acinq.eclair.payment.relay.Relayer.{GetOutgoingChannels, OutgoingChannels}
import fr.acinq.eclair.payment.send.PaymentInitiator.SendPaymentRequest
import fr.acinq.eclair.payment.send.PaymentInitiator.{SendPaymentRequest, SendTrampolinePaymentRequest}
import fr.acinq.eclair.payment.send.PaymentLifecycle.{State => _}
import fr.acinq.eclair.router.Graph.WeightRatios
import fr.acinq.eclair.router.Router.ROUTE_MAX_LENGTH
@ -55,6 +57,7 @@ import org.scalatest.{BeforeAndAfterAll, FunSuiteLike}
import scodec.bits.ByteVector
import scala.collection.JavaConversions._
import scala.compat.Platform
import scala.concurrent.Await
import scala.concurrent.ExecutionContext.Implicits.global
import scala.concurrent.duration._
@ -145,16 +148,16 @@ class IntegrationSpec extends TestKit(ActorSystem("test")) with BitcoindService
test("starting eclair nodes") {
import collection.JavaConversions._
instantiateEclairNode("A", ConfigFactory.parseMap(Map("eclair.node-alias" -> "A", "eclair.expiry-delta-blocks" -> 130, "eclair.server.port" -> 29730, "eclair.api.port" -> 28080, "eclair.channel-flags" -> 0)).withFallback(commonConfig)) // A's channels are private
instantiateEclairNode("B", ConfigFactory.parseMap(Map("eclair.node-alias" -> "B", "eclair.expiry-delta-blocks" -> 131, "eclair.server.port" -> 29731, "eclair.api.port" -> 28081)).withFallback(commonConfig))
instantiateEclairNode("C", ConfigFactory.parseMap(Map("eclair.node-alias" -> "C", "eclair.expiry-delta-blocks" -> 132, "eclair.server.port" -> 29732, "eclair.api.port" -> 28082)).withFallback(commonConfig))
instantiateEclairNode("D", ConfigFactory.parseMap(Map("eclair.node-alias" -> "D", "eclair.expiry-delta-blocks" -> 133, "eclair.server.port" -> 29733, "eclair.api.port" -> 28083)).withFallback(commonConfig))
instantiateEclairNode("B", ConfigFactory.parseMap(Map("eclair.node-alias" -> "B", "eclair.expiry-delta-blocks" -> 131, "eclair.server.port" -> 29731, "eclair.api.port" -> 28081, "eclair.trampoline-payments-enable" -> true)).withFallback(commonConfig))
instantiateEclairNode("C", ConfigFactory.parseMap(Map("eclair.node-alias" -> "C", "eclair.expiry-delta-blocks" -> 132, "eclair.server.port" -> 29732, "eclair.api.port" -> 28082, "eclair.trampoline-payments-enable" -> true, "eclair.max-payment-attempts" -> 15)).withFallback(commonConfig))
instantiateEclairNode("D", ConfigFactory.parseMap(Map("eclair.node-alias" -> "D", "eclair.expiry-delta-blocks" -> 133, "eclair.server.port" -> 29733, "eclair.api.port" -> 28083, "eclair.trampoline-payments-enable" -> true)).withFallback(commonConfig))
instantiateEclairNode("E", ConfigFactory.parseMap(Map("eclair.node-alias" -> "E", "eclair.expiry-delta-blocks" -> 134, "eclair.server.port" -> 29734, "eclair.api.port" -> 28084)).withFallback(commonConfig))
instantiateEclairNode("F1", ConfigFactory.parseMap(Map("eclair.node-alias" -> "F1", "eclair.expiry-delta-blocks" -> 135, "eclair.server.port" -> 29735, "eclair.api.port" -> 28085)).withFallback(commonConfig))
instantiateEclairNode("F2", ConfigFactory.parseMap(Map("eclair.node-alias" -> "F2", "eclair.expiry-delta-blocks" -> 136, "eclair.server.port" -> 29736, "eclair.api.port" -> 28086)).withFallback(commonConfig))
instantiateEclairNode("F3", ConfigFactory.parseMap(Map("eclair.node-alias" -> "F3", "eclair.expiry-delta-blocks" -> 137, "eclair.server.port" -> 29737, "eclair.api.port" -> 28087)).withFallback(commonConfig))
instantiateEclairNode("F3", ConfigFactory.parseMap(Map("eclair.node-alias" -> "F3", "eclair.expiry-delta-blocks" -> 137, "eclair.server.port" -> 29737, "eclair.api.port" -> 28087, "eclair.trampoline-payments-enable" -> true)).withFallback(commonConfig))
instantiateEclairNode("F4", ConfigFactory.parseMap(Map("eclair.node-alias" -> "F4", "eclair.expiry-delta-blocks" -> 138, "eclair.server.port" -> 29738, "eclair.api.port" -> 28088)).withFallback(commonConfig))
instantiateEclairNode("F5", ConfigFactory.parseMap(Map("eclair.node-alias" -> "F5", "eclair.expiry-delta-blocks" -> 139, "eclair.server.port" -> 29739, "eclair.api.port" -> 28089)).withFallback(commonConfig))
instantiateEclairNode("G", ConfigFactory.parseMap(Map("eclair.node-alias" -> "G", "eclair.expiry-delta-blocks" -> 140, "eclair.server.port" -> 29740, "eclair.api.port" -> 28090, "eclair.fee-base-msat" -> 1010, "eclair.fee-proportional-millionths" -> 102)).withFallback(commonConfig))
instantiateEclairNode("G", ConfigFactory.parseMap(Map("eclair.node-alias" -> "G", "eclair.expiry-delta-blocks" -> 140, "eclair.server.port" -> 29740, "eclair.api.port" -> 28090, "eclair.fee-base-msat" -> 1010, "eclair.fee-proportional-millionths" -> 102, "eclair.trampoline-payments-enable" -> true)).withFallback(commonConfig))
// by default C has a normal payment handler, but this can be overriden in tests
val paymentHandlerC = nodes("C").system.actorOf(PaymentHandler.props(nodes("C").nodeParams, nodes("C").commandBuffer))
@ -463,6 +466,7 @@ class IntegrationSpec extends TestKit(ActorSystem("test")) with BitcoindService
assert(paymentParts.forall(p => p.parentId != p.id))
assert(paymentParts.forall(p => p.status.asInstanceOf[OutgoingPaymentStatus.Succeeded].feesPaid > 0.msat))
awaitCond(nodes("D").nodeParams.db.payments.getIncomingPayment(pr.paymentHash).exists(_.status.isInstanceOf[IncomingPaymentStatus.Received]))
val Some(IncomingPayment(_, _, _, IncomingPaymentStatus.Received(receivedAmount, _))) = nodes("D").nodeParams.db.payments.getIncomingPayment(pr.paymentHash)
assert(receivedAmount === amount)
}
@ -519,6 +523,7 @@ class IntegrationSpec extends TestKit(ActorSystem("test")) with BitcoindService
assert(paymentParts.forall(p => p.parentId != p.id))
assert(paymentParts.forall(p => p.status.asInstanceOf[OutgoingPaymentStatus.Succeeded].feesPaid == 0.msat))
awaitCond(nodes("C").nodeParams.db.payments.getIncomingPayment(pr.paymentHash).exists(_.status.isInstanceOf[IncomingPaymentStatus.Received]))
val Some(IncomingPayment(_, _, _, IncomingPaymentStatus.Received(receivedAmount, _))) = nodes("C").nodeParams.db.payments.getIncomingPayment(pr.paymentHash)
assert(receivedAmount === amount)
}
@ -549,6 +554,161 @@ class IntegrationSpec extends TestKit(ActorSystem("test")) with BitcoindService
assert(math.abs((canSend - canSend2).toLong) < 50000000)
}
test("send a trampoline payment B->F3 (via trampoline G)") {
val start = Platform.currentTime
val sender = TestProbe()
val amount = 4000000000L.msat
sender.send(nodes("F3").paymentHandler, ReceivePayment(Some(amount), "like trampoline much?", allowMultiPart = true))
val pr = sender.expectMsgType[PaymentRequest](15 seconds)
assert(pr.features.allowMultiPart)
assert(pr.features.allowTrampoline)
val payment = SendTrampolinePaymentRequest(amount, 1000000 msat, pr, nodes("G").nodeParams.nodeId, trampolineExpiryDelta = CltvExpiryDelta(288))
sender.send(nodes("B").paymentInitiator, payment)
val paymentId = sender.expectMsgType[UUID](30 seconds)
val paymentSent = sender.expectMsgType[PaymentSent](30 seconds)
assert(paymentSent.id === paymentId)
assert(paymentSent.paymentHash === pr.paymentHash)
assert(paymentSent.amount === amount)
assert(paymentSent.feesPaid === payment.trampolineFees)
awaitCond(nodes("F3").nodeParams.db.payments.getIncomingPayment(pr.paymentHash).exists(_.status.isInstanceOf[IncomingPaymentStatus.Received]))
val Some(IncomingPayment(_, _, _, IncomingPaymentStatus.Received(receivedAmount, _))) = nodes("F3").nodeParams.db.payments.getIncomingPayment(pr.paymentHash)
assert(receivedAmount === amount)
awaitCond(nodes("G").nodeParams.db.audit.listRelayed(start, Platform.currentTime).exists(_.paymentHash == pr.paymentHash))
val relayed = nodes("G").nodeParams.db.audit.listRelayed(start, Platform.currentTime).filter(_.paymentHash == pr.paymentHash).head
assert(relayed.amountIn - relayed.amountOut === payment.trampolineFees)
// TODO: @t-bast: validate trampoline route data once implemented
val outgoingSuccess = nodes("B").nodeParams.db.payments.listOutgoingPayments(paymentId).filter(p => p.status.isInstanceOf[OutgoingPaymentStatus.Succeeded])
assert(outgoingSuccess.forall(p => p.targetNodeId == nodes("F3").nodeParams.nodeId))
assert(outgoingSuccess.map(_.amount).sum === amount)
assert(outgoingSuccess.map(_.status.asInstanceOf[OutgoingPaymentStatus.Succeeded].feesPaid).sum === payment.trampolineFees)
}
test("send a trampoline payment D->B (via trampoline C)") {
val start = Platform.currentTime
val sender = TestProbe()
val amount = 2500000000L.msat
sender.send(nodes("B").paymentHandler, ReceivePayment(Some(amount), "trampoline-MPP is so #reckless", allowMultiPart = true))
val pr = sender.expectMsgType[PaymentRequest](15 seconds)
assert(pr.features.allowMultiPart)
assert(pr.features.allowTrampoline)
val payment = SendTrampolinePaymentRequest(amount, 300000 msat, pr, nodes("C").nodeParams.nodeId, trampolineExpiryDelta = CltvExpiryDelta(144))
sender.send(nodes("D").paymentInitiator, payment)
val paymentId = sender.expectMsgType[UUID](30 seconds)
val paymentSent = sender.expectMsgType[PaymentSent](30 seconds)
assert(paymentSent.id === paymentId)
assert(paymentSent.paymentHash === pr.paymentHash)
assert(paymentSent.amount === amount)
assert(paymentSent.feesPaid === payment.trampolineFees)
awaitCond(nodes("B").nodeParams.db.payments.getIncomingPayment(pr.paymentHash).exists(_.status.isInstanceOf[IncomingPaymentStatus.Received]))
val Some(IncomingPayment(_, _, _, IncomingPaymentStatus.Received(receivedAmount, _))) = nodes("B").nodeParams.db.payments.getIncomingPayment(pr.paymentHash)
assert(receivedAmount === amount)
awaitCond(nodes("C").nodeParams.db.audit.listRelayed(start, Platform.currentTime).exists(_.paymentHash == pr.paymentHash))
val relayed = nodes("C").nodeParams.db.audit.listRelayed(start, Platform.currentTime).filter(_.paymentHash == pr.paymentHash).head
assert(relayed.amountIn - relayed.amountOut === payment.trampolineFees)
// TODO: @t-bast: validate trampoline route data once implemented
val outgoingSuccess = nodes("D").nodeParams.db.payments.listOutgoingPayments(paymentId).filter(p => p.status.isInstanceOf[OutgoingPaymentStatus.Succeeded])
assert(outgoingSuccess.forall(p => p.targetNodeId == nodes("B").nodeParams.nodeId))
assert(outgoingSuccess.map(_.amount).sum === amount)
assert(outgoingSuccess.map(_.status.asInstanceOf[OutgoingPaymentStatus.Succeeded].feesPaid).sum === payment.trampolineFees)
}
test("send a trampoline payment F3->A (via trampoline C, non-trampoline recipient)") {
// The A -> B channel is not announced.
val start = Platform.currentTime
val sender = TestProbe()
sender.send(nodes("B").relayer, Relayer.GetOutgoingChannels())
val channelUpdate_ba = sender.expectMsgType[Relayer.OutgoingChannels].channels.filter(c => c.nextNodeId == nodes("A").nodeParams.nodeId).head.channelUpdate
val routingHints = List(List(ExtraHop(nodes("B").nodeParams.nodeId, channelUpdate_ba.shortChannelId, channelUpdate_ba.feeBaseMsat, channelUpdate_ba.feeProportionalMillionths, channelUpdate_ba.cltvExpiryDelta)))
val amount = 3000000000L.msat
sender.send(nodes("A").paymentHandler, ReceivePayment(Some(amount), "trampoline to non-trampoline is so #vintage", allowMultiPart = true, extraHops = routingHints))
val pr = sender.expectMsgType[PaymentRequest](15 seconds)
assert(pr.features.allowMultiPart)
assert(!pr.features.allowTrampoline)
val payment = SendTrampolinePaymentRequest(amount, 1000000 msat, pr, nodes("C").nodeParams.nodeId, trampolineExpiryDelta = CltvExpiryDelta(432))
sender.send(nodes("F3").paymentInitiator, payment)
val paymentId = sender.expectMsgType[UUID](30 seconds)
val paymentSent = sender.expectMsgType[PaymentSent](30 seconds)
assert(paymentSent.id === paymentId)
assert(paymentSent.paymentHash === pr.paymentHash)
assert(paymentSent.amount === amount)
assert(paymentSent.feesPaid === payment.trampolineFees)
awaitCond(nodes("A").nodeParams.db.payments.getIncomingPayment(pr.paymentHash).exists(_.status.isInstanceOf[IncomingPaymentStatus.Received]))
val Some(IncomingPayment(_, _, _, IncomingPaymentStatus.Received(receivedAmount, _))) = nodes("A").nodeParams.db.payments.getIncomingPayment(pr.paymentHash)
assert(receivedAmount === amount)
awaitCond(nodes("C").nodeParams.db.audit.listRelayed(start, Platform.currentTime).exists(_.paymentHash == pr.paymentHash))
val relayed = nodes("C").nodeParams.db.audit.listRelayed(start, Platform.currentTime).filter(_.paymentHash == pr.paymentHash).head
assert(relayed.amountIn - relayed.amountOut === payment.trampolineFees)
// TODO: @t-bast: validate trampoline route data once implemented
val outgoingSuccess = nodes("F3").nodeParams.db.payments.listOutgoingPayments(paymentId).filter(p => p.status.isInstanceOf[OutgoingPaymentStatus.Succeeded])
assert(outgoingSuccess.forall(p => p.targetNodeId == nodes("A").nodeParams.nodeId))
assert(outgoingSuccess.map(_.amount).sum === amount)
assert(outgoingSuccess.map(_.status.asInstanceOf[OutgoingPaymentStatus.Succeeded].feesPaid).sum === payment.trampolineFees)
}
test("send a trampoline payment B->D (temporary local failure at trampoline)") {
val sender = TestProbe()
// We put most of the capacity C <-> D on D's side.
sender.send(nodes("D").paymentHandler, ReceivePayment(Some(8000000000L msat), "plz send everything", allowMultiPart = true))
val pr1 = sender.expectMsgType[PaymentRequest](15 seconds)
sender.send(nodes("C").paymentInitiator, SendPaymentRequest(8000000000L msat, pr1.paymentHash, nodes("D").nodeParams.nodeId, 3, paymentRequest = Some(pr1)))
sender.expectMsgType[UUID](30 seconds)
sender.expectMsgType[PaymentSent](30 seconds)
// Now we try to send more than C's outgoing capacity to D.
val amount = 2000000000L.msat
sender.send(nodes("D").paymentHandler, ReceivePayment(Some(amount), "I iz Satoshi", allowMultiPart = true))
val pr = sender.expectMsgType[PaymentRequest](15 seconds)
assert(pr.features.allowMultiPart)
assert(pr.features.allowTrampoline)
val payment = SendTrampolinePaymentRequest(amount, 250000 msat, pr, nodes("C").nodeParams.nodeId, trampolineExpiryDelta = CltvExpiryDelta(144))
sender.send(nodes("B").paymentInitiator, payment)
val paymentId = sender.expectMsgType[UUID](30 seconds)
val paymentFailed = sender.expectMsgType[PaymentFailed](30 seconds)
assert(paymentFailed.id === paymentId)
assert(paymentFailed.paymentHash === pr.paymentHash)
assert(nodes("D").nodeParams.db.payments.getIncomingPayment(pr.paymentHash).get.status === IncomingPaymentStatus.Pending)
val outgoingPayments = nodes("B").nodeParams.db.payments.listOutgoingPayments(paymentId)
assert(outgoingPayments.nonEmpty)
assert(outgoingPayments.forall(p => p.status.isInstanceOf[OutgoingPaymentStatus.Failed]))
}
test("send a trampoline payment A->D (temporary remote failure at trampoline)") {
val sender = TestProbe()
val amount = 2000000000L.msat // B can forward to C, but C doesn't have that much outgoing capacity to D
sender.send(nodes("D").paymentHandler, ReceivePayment(Some(amount), "I iz not Satoshi", allowMultiPart = true))
val pr = sender.expectMsgType[PaymentRequest](15 seconds)
assert(pr.features.allowMultiPart)
assert(pr.features.allowTrampoline)
val payment = SendTrampolinePaymentRequest(amount, 450000 msat, pr, nodes("B").nodeParams.nodeId, trampolineExpiryDelta = CltvExpiryDelta(288))
sender.send(nodes("A").paymentInitiator, payment)
val paymentId = sender.expectMsgType[UUID](30 seconds)
val paymentFailed = sender.expectMsgType[PaymentFailed](30 seconds)
assert(paymentFailed.id === paymentId)
assert(paymentFailed.paymentHash === pr.paymentHash)
assert(nodes("D").nodeParams.db.payments.getIncomingPayment(pr.paymentHash).get.status === IncomingPaymentStatus.Pending)
val outgoingPayments = nodes("A").nodeParams.db.payments.listOutgoingPayments(paymentId)
assert(outgoingPayments.nonEmpty)
assert(outgoingPayments.forall(p => p.status.isInstanceOf[OutgoingPaymentStatus.Failed]))
}
/**
* We currently use p2pkh script Helpers.getFinalScriptPubKey
*/
@ -989,4 +1149,29 @@ class IntegrationSpec extends TestKit(ActorSystem("test")) with BitcoindService
}, max = 120 seconds, interval = 1 second)
}
/** Handy way to check what the channel balances are before adding new tests. */
def debugChannelBalances(): Unit = {
val sender = TestProbe()
sender.send(nodes("B").relayer, Relayer.GetOutgoingChannels())
sender.send(nodes("C").relayer, Relayer.GetOutgoingChannels())
logger.info(s"A -> ${nodes("A").nodeParams.nodeId}")
logger.info(s"B -> ${nodes("B").nodeParams.nodeId}")
logger.info(s"C -> ${nodes("C").nodeParams.nodeId}")
logger.info(s"D -> ${nodes("D").nodeParams.nodeId}")
logger.info(s"E -> ${nodes("E").nodeParams.nodeId}")
logger.info(s"F1 -> ${nodes("F1").nodeParams.nodeId}")
logger.info(s"F2 -> ${nodes("F2").nodeParams.nodeId}")
logger.info(s"F3 -> ${nodes("F3").nodeParams.nodeId}")
logger.info(s"F4 -> ${nodes("F4").nodeParams.nodeId}")
logger.info(s"F5 -> ${nodes("F5").nodeParams.nodeId}")
logger.info(s"G -> ${nodes("G").nodeParams.nodeId}")
val channels1 = sender.expectMsgType[Relayer.OutgoingChannels]
val channels2 = sender.expectMsgType[Relayer.OutgoingChannels]
logger.info(channels1.channels.map(_.toUsableBalance))
logger.info(channels2.channels.map(_.toUsableBalance))
}
}

View File

@ -1,79 +0,0 @@
/*
* Copyright 2019 ACINQ SAS
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package fr.acinq.eclair.io
import akka.actor.{ActorSystem, Props}
import akka.testkit.{TestKit, TestProbe}
import fr.acinq.bitcoin.Crypto
import fr.acinq.eclair.channel._
import fr.acinq.eclair.wire.{ChannelCodecsSpec, TemporaryNodeFailure, UpdateAddHtlc}
import fr.acinq.eclair.{CltvExpiry, LongToBtcAmount, TestConstants, randomBytes32}
import org.scalatest.FunSuiteLike
import scala.concurrent.duration._
/**
* Created by PM on 27/01/2017.
*/
class HtlcReaperSpec extends TestKit(ActorSystem("test")) with FunSuiteLike {
test("init and cleanup") {
val data = ChannelCodecsSpec.normal
val preimage = randomBytes32
val paymentHash = Crypto.sha256(preimage)
// assuming that data has incoming htlcs 0, 1 and 2, we don't care about the amount/payment_hash/onion fields except for fulfilled HTLCs
val add0 = UpdateAddHtlc(data.channelId, 0, 20000 msat, randomBytes32, CltvExpiry(100), TestConstants.emptyOnionPacket)
val add1 = UpdateAddHtlc(data.channelId, 1, 30000 msat, randomBytes32, CltvExpiry(100), TestConstants.emptyOnionPacket)
val add2 = UpdateAddHtlc(data.channelId, 2, 40000 msat, paymentHash, CltvExpiry(100), TestConstants.emptyOnionPacket)
// unrelated htlc
val add99 = UpdateAddHtlc(randomBytes32, 0, 12345678 msat, randomBytes32, CltvExpiry(100), TestConstants.emptyOnionPacket)
val brokenHtlcs = Seq((add0, None), (add1, None), (add2, Some(preimage)), (add99, None))
val brokenHtlcKiller = system.actorOf(Props[HtlcReaper], name = "htlc-reaper")
brokenHtlcKiller ! brokenHtlcs
val sender = TestProbe()
val channel = TestProbe()
// channel goes to NORMAL state
sender.send(brokenHtlcKiller, ChannelStateChanged(channel.ref, system.deadLetters, data.commitments.remoteParams.nodeId, OFFLINE, NORMAL, data))
channel.expectMsg(CMD_FAIL_HTLC(add0.id, Right(TemporaryNodeFailure), commit = true))
channel.expectMsg(CMD_FAIL_HTLC(add1.id, Right(TemporaryNodeFailure), commit = true))
channel.expectMsg(CMD_FULFILL_HTLC(add2.id, preimage, commit = true))
channel.expectNoMsg(100 millis)
// lets'assume that channel was disconnected before having signed the updates, and gets connected again:
sender.send(brokenHtlcKiller, ChannelStateChanged(channel.ref, system.deadLetters, data.commitments.remoteParams.nodeId, OFFLINE, NORMAL, data))
channel.expectMsg(CMD_FAIL_HTLC(add0.id, Right(TemporaryNodeFailure), commit = true))
channel.expectMsg(CMD_FAIL_HTLC(add1.id, Right(TemporaryNodeFailure), commit = true))
channel.expectMsg(CMD_FULFILL_HTLC(add2.id, preimage, commit = true))
channel.expectNoMsg(100 millis)
// let's now assume that the channel gets reconnected, and it had the time to settle the htlcs
val data1 = data.copy(commitments = data.commitments.copy(localCommit = data.commitments.localCommit.copy(spec = data.commitments.localCommit.spec.copy(htlcs = Set.empty))))
sender.send(brokenHtlcKiller, ChannelStateChanged(channel.ref, system.deadLetters, data.commitments.remoteParams.nodeId, OFFLINE, NORMAL, data1))
channel.expectNoMsg(100 millis)
// reaper has cleaned up htlc, so next time it won't fail them anymore, even if we artificially submit the former state
sender.send(brokenHtlcKiller, ChannelStateChanged(channel.ref, system.deadLetters, data.commitments.remoteParams.nodeId, OFFLINE, NORMAL, data))
channel.expectNoMsg(100 millis)
}
}

View File

@ -9,7 +9,7 @@ import fr.acinq.bitcoin.Crypto.PublicKey
import fr.acinq.eclair.TestConstants._
import fr.acinq.eclair.blockchain.TestWallet
import fr.acinq.eclair.db._
import fr.acinq.eclair.wire.{ChannelCodecsSpec, Color, NodeAddress, NodeAnnouncement}
import fr.acinq.eclair.wire._
import org.mockito.scalatest.IdiomaticMockito
import org.scalatest.FunSuiteLike
import scodec.bits._
@ -17,7 +17,6 @@ import scodec.bits._
class SwitchboardSpec extends TestKit(ActorSystem("test")) with FunSuiteLike with IdiomaticMockito {
test("on initialization create peers and send Reconnect to them") {
val mockNetworkDb = mock[NetworkDb]
val nodeParams = Alice.nodeParams.copy(
db = new Databases {
@ -27,6 +26,7 @@ class SwitchboardSpec extends TestKit(ActorSystem("test")) with FunSuiteLike wit
override val peers: PeersDb = Alice.nodeParams.db.peers
override val payments: PaymentsDb = Alice.nodeParams.db.payments
override val pendingRelay: PendingRelayDb = Alice.nodeParams.db.pendingRelay
override def backup(file: File): Unit = ()
}
)
@ -42,7 +42,7 @@ class SwitchboardSpec extends TestKit(ActorSystem("test")) with FunSuiteLike wit
// mock the call that will be done by the peer once it receives Peer.Reconnect
mockNetworkDb.getNode(remoteNodeId) returns Some(
NodeAnnouncement(ByteVector64.Zeroes, ByteVector.empty, 0, remoteNodeId, Color(0,0,0), "alias", List(NodeAddress.fromParts("127.0.0.1", 9735).get))
NodeAnnouncement(ByteVector64.Zeroes, ByteVector.empty, 0, remoteNodeId, Color(0, 0, 0), "alias", List(NodeAddress.fromParts("127.0.0.1", 9735).get))
)
// add a channel to the db
@ -68,6 +68,7 @@ class SwitchboardSpec extends TestKit(ActorSystem("test")) with FunSuiteLike wit
override val peers: PeersDb = Alice.nodeParams.db.peers
override val payments: PaymentsDb = Alice.nodeParams.db.payments
override val pendingRelay: PendingRelayDb = Alice.nodeParams.db.pendingRelay
override def backup(file: File): Unit = ()
}
)
@ -83,7 +84,7 @@ class SwitchboardSpec extends TestKit(ActorSystem("test")) with FunSuiteLike wit
// mock the call that will be done by the peer once it receives Peer.Connect(remoteNodeId)
mockNetworkDb.getNode(remoteNodeId) returns Some(
NodeAnnouncement(ByteVector64.Zeroes, ByteVector.empty, 0, remoteNodeId, Color(0,0,0), "alias", List(NodeAddress.fromParts("127.0.0.1", 9735).get))
NodeAnnouncement(ByteVector64.Zeroes, ByteVector.empty, 0, remoteNodeId, Color(0, 0, 0), "alias", List(NodeAddress.fromParts("127.0.0.1", 9735).get))
)
val switchboard = system.actorOf(Switchboard.props(nodeParams, authenticator.ref, watcher.ref, router.ref, relayer.ref, paymentHandler.ref, wallet))
@ -94,4 +95,5 @@ class SwitchboardSpec extends TestKit(ActorSystem("test")) with FunSuiteLike wit
// assert that the peer called `networkDb.getNode` - because it received a Peer.Connect(remoteNodeId, None)
awaitAssert(mockNetworkDb.getNode(remoteNodeId).wasCalled(once))
}
}

View File

@ -26,7 +26,7 @@ import fr.acinq.eclair.TestConstants.TestFeeEstimator
import fr.acinq.eclair._
import fr.acinq.eclair.blockchain.fee.FeeratesPerKw
import fr.acinq.eclair.channel.Helpers.Funding
import fr.acinq.eclair.channel.{ChannelFlags, Commitments}
import fr.acinq.eclair.channel.{ChannelFlags, Commitments, Upstream}
import fr.acinq.eclair.crypto.Sphinx
import fr.acinq.eclair.payment.PaymentSent.PartialPayment
import fr.acinq.eclair.payment.relay.Relayer.{GetOutgoingChannels, OutgoingChannel, OutgoingChannels}
@ -63,12 +63,12 @@ class MultiPartPaymentLifecycleSpec extends TestKit(ActorSystem("test")) with fi
override def withFixture(test: OneArgTest): Outcome = {
val id = UUID.randomUUID()
val cfg = SendPaymentConfig(id, id, Some("42"), paymentHash, b, None, storeInDb = true, publishEvent = true)
val cfg = SendPaymentConfig(id, id, Some("42"), paymentHash, b, Upstream.Local(id), None, storeInDb = true, publishEvent = true)
val nodeParams = TestConstants.Alice.nodeParams
nodeParams.onChainFeeConf.feeEstimator.asInstanceOf[TestFeeEstimator].setFeerate(FeeratesPerKw.single(500))
val (childPayFsm, router, relayer, sender, eventListener) = (TestProbe(), TestProbe(), TestProbe(), TestProbe(), TestProbe())
class TestMultiPartPaymentLifecycle extends MultiPartPaymentLifecycle(nodeParams, cfg, relayer.ref, router.ref, TestProbe().ref) {
override def spawnChildPaymentFsm(childId: UUID): ActorRef = childPayFsm.ref
override def spawnChildPaymentFsm(childId: UUID, includeTrampolineFees: Boolean): ActorRef = childPayFsm.ref
}
val paymentHandler = TestFSMRef(new TestMultiPartPaymentLifecycle().asInstanceOf[MultiPartPaymentLifecycle])
system.eventStream.subscribe(eventListener.ref, classOf[PaymentEvent])

View File

@ -0,0 +1,386 @@
/*
* Copyright 2019 ACINQ SAS
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package fr.acinq.eclair.payment
import java.util.UUID
import akka.actor.ActorRef
import akka.testkit.{TestActorRef, TestProbe}
import fr.acinq.bitcoin.{Block, Crypto}
import fr.acinq.eclair.Features._
import fr.acinq.eclair.channel.{CMD_FAIL_HTLC, CMD_FULFILL_HTLC, Upstream}
import fr.acinq.eclair.crypto.Sphinx
import fr.acinq.eclair.payment.PaymentRequest.{ExtraHop, Features}
import fr.acinq.eclair.payment.receive.MultiPartPaymentFSM
import fr.acinq.eclair.payment.relay.{CommandBuffer, NodeRelayer}
import fr.acinq.eclair.payment.send.MultiPartPaymentLifecycle.SendMultiPartPayment
import fr.acinq.eclair.payment.send.PaymentInitiator.SendPaymentConfig
import fr.acinq.eclair.payment.send.PaymentLifecycle.SendPayment
import fr.acinq.eclair.wire._
import fr.acinq.eclair.{CltvExpiry, CltvExpiryDelta, LongToBtcAmount, MilliSatoshi, NodeParams, ShortChannelId, TestConstants, TestkitBaseClass, nodeFee, randomBytes, randomBytes32, randomKey}
import org.scalatest.Outcome
import scodec.bits.HexStringSyntax
import scala.collection.immutable.Queue
import scala.concurrent.duration._
import scala.util.Random
/**
* Created by t-bast on 10/10/2019.
*/
class NodeRelayerSpec extends TestkitBaseClass {
import NodeRelayerSpec._
case class FixtureParam(nodeParams: NodeParams, nodeRelayer: TestActorRef[NodeRelayer], relayer: TestProbe, outgoingPayFSM: TestProbe, commandBuffer: TestProbe, eventListener: TestProbe)
override def withFixture(test: OneArgTest): Outcome = {
within(30 seconds) {
val nodeParams = TestConstants.Bob.nodeParams
val outgoingPayFSM = TestProbe()
val (relayer, router, commandBuffer, register, eventListener) = (TestProbe(), TestProbe(), TestProbe(), TestProbe(), TestProbe())
system.eventStream.subscribe(eventListener.ref, classOf[PaymentEvent])
class TestNodeRelayer extends NodeRelayer(nodeParams, relayer.ref, router.ref, commandBuffer.ref, register.ref) {
override def spawnOutgoingPayFSM(cfg: SendPaymentConfig, multiPart: Boolean): ActorRef = {
outgoingPayFSM.ref ! cfg
outgoingPayFSM.ref
}
}
val nodeRelayer = TestActorRef(new TestNodeRelayer().asInstanceOf[NodeRelayer])
withFixture(test.toNoArgTest(FixtureParam(nodeParams, nodeRelayer, relayer, outgoingPayFSM, commandBuffer, eventListener)))
}
}
test("fail to relay when incoming multi-part payment times out") { f =>
import f._
// Receive a partial upstream multi-part payment.
incomingMultiPart.dropRight(1).foreach(incoming => relayer.send(nodeRelayer, incoming))
val sender = TestProbe()
val parts = incomingMultiPart.dropRight(1).map(i => MultiPartPaymentFSM.PendingPayment(i.add.id, PaymentReceived.PartialPayment(i.add.amountMsat, i.add.channelId)))
sender.send(nodeRelayer, MultiPartPaymentFSM.MultiPartHtlcFailed(paymentHash, PaymentTimeout, Queue(parts: _*)))
incomingMultiPart.dropRight(1).foreach(p => commandBuffer.expectMsg(CommandBuffer.CommandSend(p.add.channelId, CMD_FAIL_HTLC(p.add.id, Right(PaymentTimeout), commit = true))))
sender.expectNoMsg(100 millis)
outgoingPayFSM.expectNoMsg(100 millis)
}
test("fail all extraneous multi-part incoming HTLCs") { f =>
import f._
val sender = TestProbe()
val partial = MultiPartPaymentFSM.PendingPayment(15, PaymentReceived.PartialPayment(100 msat, randomBytes32))
sender.send(nodeRelayer, MultiPartPaymentFSM.ExtraHtlcReceived(paymentHash, partial, Some(InvalidRealm)))
commandBuffer.expectMsg(CommandBuffer.CommandSend(partial.payment.fromChannelId, CMD_FAIL_HTLC(partial.htlcId, Right(InvalidRealm), commit = true)))
sender.expectNoMsg(100 millis)
outgoingPayFSM.expectNoMsg(100 millis)
}
test("fail to relay an incoming payment without payment secret") { f =>
import f._
val p = createValidIncomingPacket(2000000 msat, 2000000 msat, CltvExpiry(500000), outgoingAmount, outgoingExpiry).copy(
outerPayload = Onion.createSinglePartPayload(2000000 msat, CltvExpiry(500000)) // missing outer payment secret
)
relayer.send(nodeRelayer, p)
val failure = IncorrectOrUnknownPaymentDetails(2000000 msat, nodeParams.currentBlockHeight)
commandBuffer.expectMsg(CommandBuffer.CommandSend(p.add.channelId, CMD_FAIL_HTLC(p.add.id, Right(failure), commit = true)))
outgoingPayFSM.expectNoMsg(100 millis)
}
test("fail to relay when incoming payment secrets don't match") { f =>
import f._
val p1 = createValidIncomingPacket(2000000 msat, 3000000 msat, CltvExpiry(500000), 2500000 msat, outgoingExpiry)
val p2 = createValidIncomingPacket(1000000 msat, 3000000 msat, CltvExpiry(500000), 2500000 msat, outgoingExpiry).copy(
outerPayload = Onion.createMultiPartPayload(1000000 msat, 3000000 msat, CltvExpiry(500000), randomBytes32)
)
relayer.send(nodeRelayer, p1)
relayer.send(nodeRelayer, p2)
val failure = IncorrectOrUnknownPaymentDetails(1000000 msat, nodeParams.currentBlockHeight)
commandBuffer.expectMsg(CommandBuffer.CommandSend(p2.add.channelId, CMD_FAIL_HTLC(p2.add.id, Right(failure), commit = true)))
commandBuffer.expectNoMsg(100 millis)
outgoingPayFSM.expectNoMsg(100 millis)
}
test("fail to relay when expiry is too soon (single-part)") { f =>
import f._
val expiryIn = CltvExpiry(500000) // not ok (delta = 100)
val expiryOut = CltvExpiry(499900)
val p = createValidIncomingPacket(2000000 msat, 2000000 msat, expiryIn, 1000000 msat, expiryOut)
relayer.send(nodeRelayer, p)
// TODO: @t-bast: should be an Expiry failure
val failure = IncorrectOrUnknownPaymentDetails(2000000 msat, nodeParams.currentBlockHeight)
commandBuffer.expectMsg(CommandBuffer.CommandSend(p.add.channelId, CMD_FAIL_HTLC(p.add.id, Right(failure), commit = true)))
commandBuffer.expectNoMsg(100 millis)
outgoingPayFSM.expectNoMsg(100 millis)
}
test("fail to relay when expiry is too soon (multi-part)") { f =>
import f._
val expiryIn1 = CltvExpiry(510000) // ok
val expiryIn2 = CltvExpiry(500000) // not ok (delta = 100)
val expiryOut = CltvExpiry(499900)
val p = Seq(
createValidIncomingPacket(2000000 msat, 3000000 msat, expiryIn1, 2500000 msat, expiryOut),
createValidIncomingPacket(1000000 msat, 3000000 msat, expiryIn2, 2500000 msat, expiryOut)
)
p.foreach(p => relayer.send(nodeRelayer, p))
// TODO: @t-bast: should be an Expiry failure
val failure = IncorrectOrUnknownPaymentDetails(3000000 msat, nodeParams.currentBlockHeight)
p.foreach(p => commandBuffer.expectMsg(CommandBuffer.CommandSend(p.add.channelId, CMD_FAIL_HTLC(p.add.id, Right(failure), commit = true))))
commandBuffer.expectNoMsg(100 millis)
outgoingPayFSM.expectNoMsg(100 millis)
}
test("fail to relay when fees are insufficient (single-part)") { f =>
import f._
val p = createValidIncomingPacket(2000000 msat, 2000000 msat, CltvExpiry(500000), 1999000 msat, CltvExpiry(490000))
relayer.send(nodeRelayer, p)
// TODO: @t-bast: should be a Fee failure
val failure = IncorrectOrUnknownPaymentDetails(2000000 msat, nodeParams.currentBlockHeight)
commandBuffer.expectMsg(CommandBuffer.CommandSend(p.add.channelId, CMD_FAIL_HTLC(p.add.id, Right(failure), commit = true)))
commandBuffer.expectNoMsg(100 millis)
outgoingPayFSM.expectNoMsg(100 millis)
}
test("fail to relay when fees are insufficient (multi-part)") { f =>
import f._
val p = Seq(
createValidIncomingPacket(2000000 msat, 3000000 msat, CltvExpiry(500000), 2999000 msat, CltvExpiry(400000)),
createValidIncomingPacket(1000000 msat, 3000000 msat, CltvExpiry(500000), 2999000 msat, CltvExpiry(400000))
)
p.foreach(p => relayer.send(nodeRelayer, p))
// TODO: @t-bast: should be a Fee failure
val failure = IncorrectOrUnknownPaymentDetails(3000000 msat, nodeParams.currentBlockHeight)
p.foreach(p => commandBuffer.expectMsg(CommandBuffer.CommandSend(p.add.channelId, CMD_FAIL_HTLC(p.add.id, Right(failure), commit = true))))
commandBuffer.expectNoMsg(100 millis)
outgoingPayFSM.expectNoMsg(100 millis)
}
test("fail to relay because of downstream failures") { f =>
import f._
// Receive an upstream multi-part payment.
incomingMultiPart.foreach(p => relayer.send(nodeRelayer, p))
val outgoingPaymentId = outgoingPayFSM.expectMsgType[SendPaymentConfig].id
outgoingPayFSM.expectMsgType[SendMultiPartPayment]
outgoingPayFSM.send(nodeRelayer, PaymentFailed(outgoingPaymentId, paymentHash, Nil))
incomingMultiPart.foreach(p => commandBuffer.expectMsg(CommandBuffer.CommandSend(p.add.channelId, CMD_FAIL_HTLC(p.add.id, Right(IncorrectOrUnknownPaymentDetails(incomingAmount, nodeParams.currentBlockHeight)), commit = true))))
commandBuffer.expectNoMsg(100 millis)
eventListener.expectNoMsg(100 millis)
}
test("compute route params") { f =>
import f._
relayer.send(nodeRelayer, incomingSinglePart)
outgoingPayFSM.expectMsgType[SendPaymentConfig]
val routeParams = outgoingPayFSM.expectMsgType[SendMultiPartPayment].routeParams.get
val fee = nodeFee(nodeParams.feeBase, nodeParams.feeProportionalMillionth, outgoingAmount)
assert(routeParams.maxFeePct === 0) // should be disabled
assert(routeParams.maxFeeBase === incomingAmount - outgoingAmount - fee) // we collect our fee and then use what remains for the rest of the route
assert(routeParams.routeMaxCltv === incomingSinglePart.add.cltvExpiry - outgoingExpiry - nodeParams.expiryDeltaBlocks) // we apply our cltv delta
}
test("relay incoming multi-part payment") { f =>
import f._
// Receive an upstream multi-part payment.
incomingMultiPart.dropRight(1).foreach(incoming => relayer.send(nodeRelayer, incoming))
outgoingPayFSM.expectNoMsg(100 millis) // we should NOT trigger a downstream payment before we received a complete upstream payment
relayer.send(nodeRelayer, incomingMultiPart.last)
val outgoingCfg = outgoingPayFSM.expectMsgType[SendPaymentConfig]
validateOutgoingCfg(outgoingCfg, Upstream.TrampolineRelayed(incomingMultiPart.map(_.add)))
val outgoingPayment = outgoingPayFSM.expectMsgType[SendMultiPartPayment]
validateOutgoingPayment(outgoingPayment)
outgoingPayFSM.send(nodeRelayer, createSuccessEvent(outgoingCfg.id))
incomingMultiPart.foreach(p => commandBuffer.expectMsg(CommandBuffer.CommandSend(p.add.channelId, CMD_FULFILL_HTLC(p.add.id, paymentPreimage, commit = true))))
val relayEvent = eventListener.expectMsgType[TrampolinePaymentRelayed]
validateRelayEvent(relayEvent)
assert(relayEvent.fromChannelIds.toSet === incomingMultiPart.map(_.add.channelId).toSet)
assert(relayEvent.toChannelIds.nonEmpty)
commandBuffer.expectNoMsg(100 millis)
}
test("relay incoming single-part payment") { f =>
import f._
// Receive an upstream single-part payment.
relayer.send(nodeRelayer, incomingSinglePart)
val outgoingCfg = outgoingPayFSM.expectMsgType[SendPaymentConfig]
validateOutgoingCfg(outgoingCfg, Upstream.TrampolineRelayed(incomingSinglePart.add :: Nil))
val outgoingPayment = outgoingPayFSM.expectMsgType[SendMultiPartPayment]
validateOutgoingPayment(outgoingPayment)
outgoingPayFSM.send(nodeRelayer, createSuccessEvent(outgoingCfg.id))
val incomingAdd = incomingSinglePart.add
commandBuffer.expectMsg(CommandBuffer.CommandSend(incomingAdd.channelId, CMD_FULFILL_HTLC(incomingAdd.id, paymentPreimage, commit = true)))
val relayEvent = eventListener.expectMsgType[TrampolinePaymentRelayed]
validateRelayEvent(relayEvent)
assert(relayEvent.fromChannelIds === Seq(incomingSinglePart.add.channelId))
assert(relayEvent.toChannelIds.nonEmpty)
commandBuffer.expectNoMsg(100 millis)
}
test("relay to non-trampoline recipient supporting multi-part") { f =>
import f._
// Receive an upstream multi-part payment.
val hints = List(List(ExtraHop(outgoingNodeId, ShortChannelId(42), feeBase = 10 msat, feeProportionalMillionths = 1, cltvExpiryDelta = CltvExpiryDelta(12))))
val pr = PaymentRequest(Block.LivenetGenesisBlock.hash, Some(outgoingAmount * 3), paymentHash, randomKey, "Some invoice", extraHops = hints, features = Some(Features(BASIC_MULTI_PART_PAYMENT_OPTIONAL, PAYMENT_SECRET_MANDATORY)))
incomingMultiPart.foreach(incoming => relayer.send(nodeRelayer, incoming.copy(innerPayload = Onion.createNodeRelayToNonTrampolinePayload(
incoming.innerPayload.amountToForward, outgoingAmount * 3, outgoingExpiry, outgoingNodeId, pr
))))
val outgoingCfg = outgoingPayFSM.expectMsgType[SendPaymentConfig]
validateOutgoingCfg(outgoingCfg, Upstream.TrampolineRelayed(incomingMultiPart.map(_.add)))
val outgoingPayment = outgoingPayFSM.expectMsgType[SendMultiPartPayment]
assert(outgoingPayment.paymentHash === paymentHash)
assert(outgoingPayment.paymentSecret === pr.paymentSecret.get) // we should use the provided secret
assert(outgoingPayment.totalAmount === outgoingAmount)
assert(outgoingPayment.finalExpiry === outgoingExpiry)
assert(outgoingPayment.targetNodeId === outgoingNodeId)
assert(outgoingPayment.additionalTlvs === Nil)
assert(outgoingPayment.routeParams.isDefined)
assert(outgoingPayment.assistedRoutes === hints)
outgoingPayFSM.send(nodeRelayer, createSuccessEvent(outgoingCfg.id))
incomingMultiPart.foreach(p => commandBuffer.expectMsg(CommandBuffer.CommandSend(p.add.channelId, CMD_FULFILL_HTLC(p.add.id, paymentPreimage, commit = true))))
val relayEvent = eventListener.expectMsgType[TrampolinePaymentRelayed]
validateRelayEvent(relayEvent)
assert(relayEvent.fromChannelIds === incomingMultiPart.map(_.add.channelId))
assert(relayEvent.toChannelIds.nonEmpty)
commandBuffer.expectNoMsg(100 millis)
}
test("relay to non-trampoline recipient without multi-part") { f =>
import f._
// Receive an upstream multi-part payment.
val hints = List(List(ExtraHop(outgoingNodeId, ShortChannelId(42), feeBase = 10 msat, feeProportionalMillionths = 1, cltvExpiryDelta = CltvExpiryDelta(12))))
val pr = PaymentRequest(Block.LivenetGenesisBlock.hash, Some(outgoingAmount), paymentHash, randomKey, "Some invoice", extraHops = hints, features = Some(Features()))
incomingMultiPart.foreach(incoming => relayer.send(nodeRelayer, incoming.copy(innerPayload = Onion.createNodeRelayToNonTrampolinePayload(
incoming.innerPayload.amountToForward, incoming.innerPayload.amountToForward, outgoingExpiry, outgoingNodeId, pr
))))
val outgoingCfg = outgoingPayFSM.expectMsgType[SendPaymentConfig]
validateOutgoingCfg(outgoingCfg, Upstream.TrampolineRelayed(incomingMultiPart.map(_.add)))
val outgoingPayment = outgoingPayFSM.expectMsgType[SendPayment]
assert(outgoingPayment.paymentHash === paymentHash)
assert(outgoingPayment.routePrefix === Nil)
assert(outgoingPayment.finalPayload.amount === outgoingAmount)
assert(outgoingPayment.finalPayload.expiry === outgoingExpiry)
assert(outgoingPayment.targetNodeId === outgoingNodeId)
assert(outgoingPayment.routeParams.isDefined)
assert(outgoingPayment.assistedRoutes === hints)
outgoingPayFSM.send(nodeRelayer, createSuccessEvent(outgoingCfg.id))
incomingMultiPart.foreach(p => commandBuffer.expectMsg(CommandBuffer.CommandSend(p.add.channelId, CMD_FULFILL_HTLC(p.add.id, paymentPreimage, commit = true))))
val relayEvent = eventListener.expectMsgType[TrampolinePaymentRelayed]
validateRelayEvent(relayEvent)
assert(relayEvent.fromChannelIds === incomingMultiPart.map(_.add.channelId))
assert(relayEvent.toChannelIds.length === 1)
commandBuffer.expectNoMsg(100 millis)
}
def validateOutgoingCfg(outgoingCfg: SendPaymentConfig, upstream: Upstream): Unit = {
assert(!outgoingCfg.publishEvent)
assert(!outgoingCfg.storeInDb)
assert(outgoingCfg.paymentHash === paymentHash)
assert(outgoingCfg.paymentRequest === None)
assert(outgoingCfg.targetNodeId === outgoingNodeId)
assert(outgoingCfg.upstream === upstream)
}
def validateOutgoingPayment(outgoingPayment: SendMultiPartPayment): Unit = {
assert(outgoingPayment.paymentHash === paymentHash)
assert(outgoingPayment.paymentSecret !== incomingSecret) // we should generate a new outgoing secret
assert(outgoingPayment.totalAmount === outgoingAmount)
assert(outgoingPayment.finalExpiry === outgoingExpiry)
assert(outgoingPayment.targetNodeId === outgoingNodeId)
assert(outgoingPayment.additionalTlvs === Seq(OnionTlv.TrampolineOnion(nextTrampolinePacket)))
assert(outgoingPayment.routeParams.isDefined)
assert(outgoingPayment.assistedRoutes === Nil)
}
def validateRelayEvent(e: TrampolinePaymentRelayed): Unit = {
assert(e.amountIn === incomingAmount)
assert(e.amountOut === outgoingAmount)
assert(e.paymentHash === paymentHash)
assert(e.toNodeId === outgoingNodeId)
}
}
object NodeRelayerSpec {
val paymentPreimage = randomBytes32
val paymentHash = Crypto.sha256(paymentPreimage)
// This is the result of decrypting the incoming trampoline onion packet.
// It should be forwarded to the next trampoline node.
val nextTrampolinePacket = OnionRoutingPacket(0, hex"02eec7245d6b7d2ccb30380bfbe2a3648cd7a942653f5aa340edcea1f283686619", randomBytes(Sphinx.TrampolinePacket.PayloadLength), randomBytes32)
val outgoingAmount = 4000000 msat
val outgoingExpiry = CltvExpiry(490000)
val outgoingNodeId = randomKey.publicKey
val incomingAmount = 5000000 msat
val incomingSecret = randomBytes32
val incomingMultiPart = Seq(
createValidIncomingPacket(2000000 msat, incomingAmount, CltvExpiry(500000), outgoingAmount, outgoingExpiry),
createValidIncomingPacket(2000000 msat, incomingAmount, CltvExpiry(499999), outgoingAmount, outgoingExpiry),
createValidIncomingPacket(1000000 msat, incomingAmount, CltvExpiry(499999), outgoingAmount, outgoingExpiry)
)
val incomingSinglePart =
createValidIncomingPacket(incomingAmount, incomingAmount, CltvExpiry(500000), outgoingAmount, outgoingExpiry)
def createSuccessEvent(id: UUID): PaymentSent =
PaymentSent(id, paymentHash, paymentPreimage, Seq(PaymentSent.PartialPayment(id, outgoingAmount, 10 msat, randomBytes32, None)))
def createValidIncomingPacket(amountIn: MilliSatoshi, totalAmountIn: MilliSatoshi, expiryIn: CltvExpiry, amountOut: MilliSatoshi, expiryOut: CltvExpiry): IncomingPacket.NodeRelayPacket = {
val outerPayload = if (amountIn == totalAmountIn) {
Onion.createSinglePartPayload(amountIn, expiryIn, Some(incomingSecret))
} else {
Onion.createMultiPartPayload(amountIn, totalAmountIn, expiryIn, incomingSecret)
}
IncomingPacket.NodeRelayPacket(
UpdateAddHtlc(randomBytes32, Random.nextInt(100), amountIn, paymentHash, expiryIn, TestConstants.emptyOnionPacket),
outerPayload,
Onion.createNodeRelayPayload(amountOut, expiryOut, outgoingNodeId),
nextTrampolinePacket)
}
}

View File

@ -22,7 +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.channel.Channel
import fr.acinq.eclair.channel.{Channel, Upstream}
import fr.acinq.eclair.crypto.Sphinx
import fr.acinq.eclair.payment.PaymentPacketSpec._
import fr.acinq.eclair.payment.PaymentRequest.{ExtraHop, Features}
@ -35,6 +35,7 @@ import fr.acinq.eclair.wire.Onion.FinalLegacyPayload
import fr.acinq.eclair.wire.{OnionCodecs, OnionTlv}
import fr.acinq.eclair.{CltvExpiryDelta, LongToBtcAmount, NodeParams, TestConstants, randomKey}
import org.scalatest.{Outcome, fixture}
import scodec.bits.HexStringSyntax
import scala.concurrent.duration._
@ -81,7 +82,7 @@ class PaymentInitiatorSpec extends TestKit(ActorSystem("test")) with fixture.Fun
import f._
sender.send(initiator, SendPaymentRequest(finalAmount, paymentHash, c, 1, predefinedRoute = Seq(a, b, c)))
val paymentId = sender.expectMsgType[UUID]
payFsm.expectMsg(SendPaymentConfig(paymentId, paymentId, None, paymentHash, c, None, storeInDb = true, publishEvent = true))
payFsm.expectMsg(SendPaymentConfig(paymentId, paymentId, None, paymentHash, c, Upstream.Local(paymentId), None, storeInDb = true, publishEvent = true))
payFsm.expectMsg(SendPaymentToRoute(paymentHash, Seq(a, b, c), FinalLegacyPayload(finalAmount, Channel.MIN_CLTV_EXPIRY_DELTA.toCltvExpiry(nodeParams.currentBlockHeight + 1))))
}
@ -91,12 +92,12 @@ class PaymentInitiatorSpec extends TestKit(ActorSystem("test")) with fixture.Fun
val routeParams = RouteParams(randomize = true, 15 msat, 1.5, 5, CltvExpiryDelta(561), None)
sender.send(initiator, SendPaymentRequest(finalAmount, paymentHash, c, 1, CltvExpiryDelta(42), assistedRoutes = hints, routeParams = Some(routeParams)))
val id1 = sender.expectMsgType[UUID]
payFsm.expectMsg(SendPaymentConfig(id1, id1, None, paymentHash, c, None, storeInDb = true, publishEvent = true))
payFsm.expectMsg(SendPaymentConfig(id1, id1, None, paymentHash, c, Upstream.Local(id1), None, storeInDb = true, publishEvent = true))
payFsm.expectMsg(SendPayment(paymentHash, c, FinalLegacyPayload(finalAmount, CltvExpiryDelta(42).toCltvExpiry(nodeParams.currentBlockHeight + 1)), 1, hints, Some(routeParams)))
sender.send(initiator, SendPaymentRequest(finalAmount, paymentHash, e, 3))
val id2 = sender.expectMsgType[UUID]
payFsm.expectMsg(SendPaymentConfig(id2, id2, None, paymentHash, e, None, storeInDb = true, publishEvent = true))
payFsm.expectMsg(SendPaymentConfig(id2, id2, None, paymentHash, e, Upstream.Local(id2), None, storeInDb = true, publishEvent = true))
payFsm.expectMsg(SendPayment(paymentHash, e, FinalLegacyPayload(finalAmount, Channel.MIN_CLTV_EXPIRY_DELTA.toCltvExpiry(nodeParams.currentBlockHeight + 1)), 3))
}
@ -106,7 +107,7 @@ class PaymentInitiatorSpec extends TestKit(ActorSystem("test")) with fixture.Fun
val req = SendPaymentRequest(finalAmount + 100.msat, paymentHash, c, 1, CltvExpiryDelta(42), Some(pr))
sender.send(initiator, req)
val id = sender.expectMsgType[UUID]
multiPartPayFsm.expectMsg(SendPaymentConfig(id, id, None, paymentHash, c, Some(pr), storeInDb = true, publishEvent = true))
multiPartPayFsm.expectMsg(SendPaymentConfig(id, id, None, paymentHash, c, Upstream.Local(id), Some(pr), storeInDb = true, publishEvent = true))
multiPartPayFsm.expectMsg(SendMultiPartPayment(paymentHash, pr.paymentSecret.get, c, finalAmount + 100.msat, req.finalExpiry(nodeParams.currentBlockHeight), 1))
}
@ -116,7 +117,7 @@ class PaymentInitiatorSpec extends TestKit(ActorSystem("test")) with fixture.Fun
val req = SendPaymentRequest(finalAmount / 2, paymentHash, c, 1, paymentRequest = Some(pr), predefinedRoute = Seq(a, b, c))
sender.send(initiator, req)
val id = sender.expectMsgType[UUID]
payFsm.expectMsg(SendPaymentConfig(id, id, None, paymentHash, c, Some(pr), storeInDb = true, publishEvent = true))
payFsm.expectMsg(SendPaymentConfig(id, id, None, paymentHash, c, Upstream.Local(id), Some(pr), storeInDb = true, publishEvent = true))
val msg = payFsm.expectMsgType[SendPaymentToRoute]
assert(msg.paymentHash === paymentHash)
assert(msg.hops === Seq(a, b, c))
@ -194,7 +195,7 @@ class PaymentInitiatorSpec extends TestKit(ActorSystem("test")) with fixture.Fun
assert(trampolinePayload.outgoingCltv.toLong === currentBlockCount + 9 + 1)
assert(trampolinePayload.outgoingNodeId === c)
assert(trampolinePayload.paymentSecret === pr.paymentSecret)
assert(trampolinePayload.invoiceFeatures === Some(pr.features.bitmask.bytes))
assert(trampolinePayload.invoiceFeatures === Some(hex"8000")) // PAYMENT_SECRET_OPTIONAL
}
test("reject trampoline to legacy payment for 0-value invoice") { f =>

View File

@ -26,13 +26,13 @@ import fr.acinq.bitcoin.{Block, ByteVector32, Crypto, Transaction, TxOut}
import fr.acinq.eclair._
import fr.acinq.eclair.blockchain.{UtxoStatus, ValidateRequest, ValidateResult, WatchSpentBasic}
import fr.acinq.eclair.channel.Register.ForwardShortId
import fr.acinq.eclair.channel.{AddHtlcFailed, Channel, ChannelUnavailable}
import fr.acinq.eclair.channel.{AddHtlcFailed, Channel, ChannelUnavailable, Upstream}
import fr.acinq.eclair.crypto.Sphinx
import fr.acinq.eclair.db.{OutgoingPayment, OutgoingPaymentStatus}
import fr.acinq.eclair.io.Peer.PeerRoutingMessage
import fr.acinq.eclair.payment.relay.Origin.Local
import fr.acinq.eclair.payment.PaymentRequest.ExtraHop
import fr.acinq.eclair.payment.PaymentSent.PartialPayment
import fr.acinq.eclair.payment.relay.Origin.Local
import fr.acinq.eclair.payment.send.PaymentInitiator.{SendPaymentConfig, SendPaymentRequest}
import fr.acinq.eclair.payment.send.PaymentLifecycle
import fr.acinq.eclair.payment.send.PaymentLifecycle._
@ -70,7 +70,7 @@ class PaymentLifecycleSpec extends BaseRouterSpec {
def createPaymentLifecycle(storeInDb: Boolean = true, publishEvent: Boolean = true): PaymentFixture = {
val id = UUID.randomUUID()
val nodeParams = TestConstants.Alice.nodeParams.copy(keyManager = testKeyManager)
val cfg = SendPaymentConfig(id, id, Some(defaultExternalId), defaultPaymentHash, d, defaultPaymentRequest.paymentRequest, storeInDb, publishEvent)
val cfg = SendPaymentConfig(id, id, Some(defaultExternalId), defaultPaymentHash, d, Upstream.Local(id), defaultPaymentRequest.paymentRequest, storeInDb, publishEvent)
val (routerForwarder, register, sender, monitor, eventListener) = (TestProbe(), TestProbe(), TestProbe(), TestProbe(), TestProbe())
val paymentFSM = TestFSMRef(new PaymentLifecycle(nodeParams, cfg, routerForwarder.ref, register.ref))
paymentFSM ! SubscribeTransitionCallBack(monitor.ref)

View File

@ -22,7 +22,7 @@ import fr.acinq.bitcoin.Crypto.PublicKey
import fr.acinq.bitcoin.DeterministicWallet.ExtendedPrivateKey
import fr.acinq.bitcoin.{Block, ByteVector32, Crypto, DeterministicWallet}
import fr.acinq.eclair.Features._
import fr.acinq.eclair.channel.{Channel, ChannelVersion, Commitments}
import fr.acinq.eclair.channel.{Channel, ChannelVersion, Commitments, Upstream}
import fr.acinq.eclair.crypto.Sphinx
import fr.acinq.eclair.payment.IncomingPacket.{ChannelRelayPacket, FinalPacket, NodeRelayPacket, decrypt}
import fr.acinq.eclair.payment.OutgoingPacket._
@ -34,7 +34,7 @@ import fr.acinq.eclair.wire._
import fr.acinq.eclair.{CltvExpiry, CltvExpiryDelta, LongToBtcAmount, MilliSatoshi, ShortChannelId, TestConstants, UInt64, nodeFee, randomBytes32, randomKey}
import org.scalatest.{BeforeAndAfterAll, FunSuite}
import scodec.Attempt
import scodec.bits.ByteVector
import scodec.bits.{ByteVector, HexStringSyntax}
/**
* Created by PM on 31/05/2016.
@ -121,7 +121,7 @@ class PaymentPacketSpec extends FunSuite with BeforeAndAfterAll {
}
test("build a command including the onion") {
val (add, _) = buildCommand(UUID.randomUUID, paymentHash, hops, FinalLegacyPayload(finalAmount, finalExpiry))
val (add, _) = buildCommand(Upstream.Local(UUID.randomUUID), paymentHash, hops, FinalLegacyPayload(finalAmount, finalExpiry))
assert(add.amount > finalAmount)
assert(add.cltvExpiry === finalExpiry + channelUpdate_de.cltvExpiryDelta + channelUpdate_cd.cltvExpiryDelta + channelUpdate_bc.cltvExpiryDelta)
assert(add.paymentHash === paymentHash)
@ -132,7 +132,7 @@ class PaymentPacketSpec extends FunSuite with BeforeAndAfterAll {
}
test("build a command with no hops") {
val (add, _) = buildCommand(UUID.randomUUID(), paymentHash, hops.take(1), FinalLegacyPayload(finalAmount, finalExpiry))
val (add, _) = buildCommand(Upstream.Local(UUID.randomUUID()), paymentHash, hops.take(1), FinalLegacyPayload(finalAmount, finalExpiry))
assert(add.amount === finalAmount)
assert(add.cltvExpiry === finalExpiry)
assert(add.paymentHash === paymentHash)
@ -255,7 +255,7 @@ class PaymentPacketSpec extends FunSuite with BeforeAndAfterAll {
assert(inner_d.outgoingNodeId === e)
assert(inner_d.totalAmount === finalAmount)
assert(inner_d.paymentSecret === invoice.paymentSecret)
assert(inner_d.invoiceFeatures === Some(invoiceFeatures.bitmask.bytes))
assert(inner_d.invoiceFeatures === Some(hex"028000")) // PAYMENT_SECRET_OPTIONAL, BASIC_MULTI_PART_PAYMENT_OPTIONAL
assert(inner_d.invoiceRoutingInfo === Some(routingHints))
}

View File

@ -337,6 +337,23 @@ class PaymentRequestSpec extends FunSuite {
}
}
test("feature bits to minimally-encoded feature bytes") {
val testCases = Seq(
(bin" 0010000100000101", hex" 2105"),
(bin" 1010000100000101", hex" a105"),
(bin" 11000000000000110", hex"018006"),
(bin" 01000000000000110", hex" 8006"),
(bin" 001000000000000000", hex" 8000"),
(bin" 101000000000000000", hex"028000"),
(bin"0101110000000000110", hex"02e006"),
(bin"1001110000000000110", hex"04e006")
)
for ((bitmask, featureBytes) <- testCases) {
assert(Features(bitmask).toByteVector === featureBytes)
}
}
test("payment secret") {
val pr = PaymentRequest(Block.LivenetGenesisBlock.hash, Some(123 msat), ByteVector32.One, priv, "Some invoice")
assert(pr.paymentSecret.isDefined)

View File

@ -0,0 +1,484 @@
/*
* Copyright 2019 ACINQ SAS
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package fr.acinq.eclair.payment
import java.util.UUID
import akka.actor.ActorRef
import akka.testkit.TestProbe
import fr.acinq.bitcoin.{Block, ByteVector32, Crypto}
import fr.acinq.eclair.channel._
import fr.acinq.eclair.db.{OutgoingPayment, OutgoingPaymentStatus}
import fr.acinq.eclair.payment.OutgoingPacket.buildCommand
import fr.acinq.eclair.payment.PaymentPacketSpec._
import fr.acinq.eclair.payment.relay.Relayer.{ForwardFail, ForwardFulfill}
import fr.acinq.eclair.payment.relay.{CommandBuffer, Origin, Relayer}
import fr.acinq.eclair.router.ChannelHop
import fr.acinq.eclair.transactions.{DirectedHtlc, Direction, IN, OUT}
import fr.acinq.eclair.wire.Onion.FinalLegacyPayload
import fr.acinq.eclair.wire._
import fr.acinq.eclair.{CltvExpiry, LongToBtcAmount, NodeParams, TestConstants, TestkitBaseClass, randomBytes32}
import org.scalatest.Outcome
import scodec.bits.ByteVector
import scala.concurrent.duration._
/**
* Created by t-bast on 21/11/2019.
*/
class PostRestartHtlcCleanerSpec extends TestkitBaseClass {
import PostRestartHtlcCleanerSpec._
case class FixtureParam(nodeParams: NodeParams, commandBuffer: TestProbe, sender: TestProbe, eventListener: TestProbe) {
def createRelayer(): ActorRef = {
system.actorOf(Relayer.props(nodeParams, TestProbe().ref, TestProbe().ref, commandBuffer.ref, TestProbe().ref))
}
}
override def withFixture(test: OneArgTest): Outcome = {
within(30 seconds) {
val nodeParams = TestConstants.Bob.nodeParams
val commandBuffer = TestProbe()
val eventListener = TestProbe()
system.eventStream.subscribe(eventListener.ref, classOf[PaymentEvent])
withFixture(test.toNoArgTest(FixtureParam(nodeParams, commandBuffer, TestProbe(), eventListener)))
}
}
test("clean up upstream HTLCs that weren't relayed downstream") { f =>
import f._
// We simulate the following state:
// (channel AB1)
// +-<-<- 2, 3 -<-<-<-<-<-+
// +->->- 0, 1, 4, 5 ->->-+
// | |
// A B ---> relayed (AB1, 0), (AB1, 5), (AB2, 2)
// | |
// +->->- 0, 2, 4 ->->->-+
// +-<-<- 1, 3 -<-<-<-<-<-+
// (channel AB2)
val relayedPaymentHash = randomBytes32
val relayed = Origin.Relayed(channelId_ab_1, 5, 10 msat, 10 msat)
val trampolineRelayedPaymentHash = randomBytes32
val trampolineRelayed = Origin.TrampolineRelayed((channelId_ab_1, 0L) :: (channelId_ab_2, 2L) :: Nil, None)
val htlc_ab_1 = Seq(
buildHtlc(0, channelId_ab_1, trampolineRelayedPaymentHash, IN),
buildHtlc(1, channelId_ab_1, randomBytes32, IN), // not relayed
buildHtlc(2, channelId_ab_1, randomBytes32, OUT),
buildHtlc(3, channelId_ab_1, randomBytes32, OUT),
buildHtlc(4, channelId_ab_1, randomBytes32, IN), // not relayed
buildHtlc(5, channelId_ab_1, relayedPaymentHash, IN)
)
val htlc_ab_2 = Seq(
buildHtlc(0, channelId_ab_2, randomBytes32, IN), // not relayed
buildHtlc(1, channelId_ab_2, randomBytes32, OUT),
buildHtlc(2, channelId_ab_2, trampolineRelayedPaymentHash, IN),
buildHtlc(3, channelId_ab_2, randomBytes32, OUT),
buildHtlc(4, channelId_ab_2, randomBytes32, IN) // not relayed
)
val channels = Seq(
ChannelCodecsSpec.makeChannelDataNormal(htlc_ab_1, Map(51L -> relayed, 1L -> trampolineRelayed)),
ChannelCodecsSpec.makeChannelDataNormal(htlc_ab_2, Map(4L -> trampolineRelayed))
)
// Prepare channels state before restart.
channels.foreach(c => nodeParams.db.channels.addOrUpdateChannel(c))
val channel = TestProbe()
f.createRelayer()
commandBuffer.expectNoMsg(100 millis) // nothing should happen while channels are still offline.
// channel 1 goes to NORMAL state:
system.eventStream.publish(ChannelStateChanged(channel.ref, system.deadLetters, a, OFFLINE, NORMAL, channels.head))
val fails_ab_1 = channel.expectMsgType[CMD_FAIL_HTLC] :: channel.expectMsgType[CMD_FAIL_HTLC] :: Nil
assert(fails_ab_1.toSet === Set(CMD_FAIL_HTLC(1, Right(TemporaryNodeFailure), commit = true), CMD_FAIL_HTLC(4, Right(TemporaryNodeFailure), commit = true)))
channel.expectNoMsg(100 millis)
// channel 2 goes to NORMAL state:
system.eventStream.publish(ChannelStateChanged(channel.ref, system.deadLetters, a, OFFLINE, NORMAL, channels(1)))
val fails_ab_2 = channel.expectMsgType[CMD_FAIL_HTLC] :: channel.expectMsgType[CMD_FAIL_HTLC] :: Nil
assert(fails_ab_2.toSet === Set(CMD_FAIL_HTLC(0, Right(TemporaryNodeFailure), commit = true), CMD_FAIL_HTLC(4, Right(TemporaryNodeFailure), commit = true)))
channel.expectNoMsg(100 millis)
// let's assume that channel 1 was disconnected before having signed the fails, and gets connected again:
system.eventStream.publish(ChannelStateChanged(channel.ref, system.deadLetters, a, OFFLINE, NORMAL, channels.head))
val fails_ab_1_bis = channel.expectMsgType[CMD_FAIL_HTLC] :: channel.expectMsgType[CMD_FAIL_HTLC] :: Nil
assert(fails_ab_1_bis.toSet === Set(CMD_FAIL_HTLC(1, Right(TemporaryNodeFailure), commit = true), CMD_FAIL_HTLC(4, Right(TemporaryNodeFailure), commit = true)))
channel.expectNoMsg(100 millis)
// let's now assume that channel 1 gets reconnected, and it had the time to fail the htlcs:
val data1 = channels.head.copy(commitments = channels.head.commitments.copy(localCommit = channels.head.commitments.localCommit.copy(spec = channels.head.commitments.localCommit.spec.copy(htlcs = Set.empty))))
system.eventStream.publish(ChannelStateChanged(channel.ref, system.deadLetters, a, OFFLINE, NORMAL, data1))
channel.expectNoMsg(100 millis)
// post-restart cleaner has cleaned up the htlcs, so next time it won't fail them anymore, even if we artificially submit the former state:
system.eventStream.publish(ChannelStateChanged(channel.ref, system.deadLetters, a, OFFLINE, NORMAL, channels.head))
channel.expectNoMsg(100 millis)
}
test("clean up upstream HTLCs for which we're the final recipient") { f =>
import f._
val preimage = randomBytes32
val paymentHash = Crypto.sha256(preimage)
val invoice = PaymentRequest(Block.TestnetGenesisBlock.hash, Some(500 msat), paymentHash, TestConstants.Bob.keyManager.nodeKey.privateKey, "Some invoice")
nodeParams.db.payments.addIncomingPayment(invoice, preimage)
nodeParams.db.payments.receiveIncomingPayment(paymentHash, 5000 msat)
val htlc_ab_1 = Seq(
buildFinalHtlc(0, channelId_ab_1, randomBytes32),
buildFinalHtlc(3, channelId_ab_1, paymentHash),
buildFinalHtlc(5, channelId_ab_1, paymentHash),
buildFinalHtlc(7, channelId_ab_1, randomBytes32)
)
val htlc_ab_2 = Seq(
buildFinalHtlc(1, channelId_ab_2, randomBytes32),
buildFinalHtlc(3, channelId_ab_2, randomBytes32),
buildFinalHtlc(4, channelId_ab_2, paymentHash),
buildFinalHtlc(9, channelId_ab_2, randomBytes32)
)
val channels = Seq(
ChannelCodecsSpec.makeChannelDataNormal(htlc_ab_1, Map.empty),
ChannelCodecsSpec.makeChannelDataNormal(htlc_ab_2, Map.empty)
)
// Prepare channels state before restart.
channels.foreach(c => nodeParams.db.channels.addOrUpdateChannel(c))
val channel = TestProbe()
f.createRelayer()
commandBuffer.expectNoMsg(100 millis) // nothing should happen while channels are still offline.
// channel 1 goes to NORMAL state:
system.eventStream.publish(ChannelStateChanged(channel.ref, system.deadLetters, a, OFFLINE, NORMAL, channels.head))
val expected1 = Set(
CMD_FAIL_HTLC(0, Right(TemporaryNodeFailure), commit = true),
CMD_FULFILL_HTLC(3, preimage, commit = true),
CMD_FULFILL_HTLC(5, preimage, commit = true),
CMD_FAIL_HTLC(7, Right(TemporaryNodeFailure), commit = true)
)
val received1 = expected1.map(_ => channel.expectMsgType[Command])
assert(received1 === expected1)
channel.expectNoMsg(100 millis)
// channel 2 goes to NORMAL state:
system.eventStream.publish(ChannelStateChanged(channel.ref, system.deadLetters, a, OFFLINE, NORMAL, channels(1)))
val expected2 = Set(
CMD_FAIL_HTLC(1, Right(TemporaryNodeFailure), commit = true),
CMD_FAIL_HTLC(3, Right(TemporaryNodeFailure), commit = true),
CMD_FULFILL_HTLC(4, preimage, commit = true),
CMD_FAIL_HTLC(9, Right(TemporaryNodeFailure), commit = true)
)
val received2 = expected2.map(_ => channel.expectMsgType[Command])
assert(received2 === expected2)
channel.expectNoMsg(100 millis)
// let's assume that channel 1 was disconnected before having signed the updates, and gets connected again:
system.eventStream.publish(ChannelStateChanged(channel.ref, system.deadLetters, a, OFFLINE, NORMAL, channels.head))
val received3 = expected1.map(_ => channel.expectMsgType[Command])
assert(received3 === expected1)
channel.expectNoMsg(100 millis)
// let's now assume that channel 1 gets reconnected, and it had the time to sign the htlcs:
val data1 = channels.head.copy(commitments = channels.head.commitments.copy(localCommit = channels.head.commitments.localCommit.copy(spec = channels.head.commitments.localCommit.spec.copy(htlcs = Set.empty))))
system.eventStream.publish(ChannelStateChanged(channel.ref, system.deadLetters, a, OFFLINE, NORMAL, data1))
channel.expectNoMsg(100 millis)
// post-restart cleaner has cleaned up the htlcs, so next time it won't fail them anymore, even if we artificially submit the former state:
system.eventStream.publish(ChannelStateChanged(channel.ref, system.deadLetters, a, OFFLINE, NORMAL, channels.head))
channel.expectNoMsg(100 millis)
}
test("handle a local payment htlc-fail") { f =>
import f._
val testCase = setupLocalPayments(nodeParams)
val relayer = createRelayer()
commandBuffer.expectNoMsg(100 millis)
sender.send(relayer, testCase.fails(1))
eventListener.expectNoMsg(100 millis)
// This is a multi-part payment, the second part is still pending.
assert(nodeParams.db.payments.getOutgoingPayment(testCase.childIds(2)).get.status === OutgoingPaymentStatus.Pending)
sender.send(relayer, testCase.fails(2))
val e1 = eventListener.expectMsgType[PaymentFailed]
assert(e1.id === testCase.parentId)
assert(e1.paymentHash === paymentHash2)
assert(nodeParams.db.payments.getOutgoingPayment(testCase.childIds(1)).get.status.isInstanceOf[OutgoingPaymentStatus.Failed])
assert(nodeParams.db.payments.getOutgoingPayment(testCase.childIds(2)).get.status.isInstanceOf[OutgoingPaymentStatus.Failed])
assert(nodeParams.db.payments.getOutgoingPayment(testCase.childIds.head).get.status === OutgoingPaymentStatus.Pending)
sender.send(relayer, testCase.fails.head)
val e2 = eventListener.expectMsgType[PaymentFailed]
assert(e2.id === testCase.childIds.head)
assert(e2.paymentHash === paymentHash1)
assert(nodeParams.db.payments.getOutgoingPayment(testCase.childIds.head).get.status.isInstanceOf[OutgoingPaymentStatus.Failed])
commandBuffer.expectNoMsg(100 millis)
}
test("handle a local payment htlc-fulfill") { f =>
import f._
val testCase = setupLocalPayments(nodeParams)
val relayer = f.createRelayer()
commandBuffer.expectNoMsg(100 millis)
sender.send(relayer, testCase.fulfills(1))
eventListener.expectNoMsg(100 millis)
// This is a multi-part payment, the second part is still pending.
assert(nodeParams.db.payments.getOutgoingPayment(testCase.childIds(2)).get.status === OutgoingPaymentStatus.Pending)
sender.send(relayer, testCase.fulfills(2))
val e1 = eventListener.expectMsgType[PaymentSent]
assert(e1.id === testCase.parentId)
assert(e1.paymentPreimage === preimage2)
assert(e1.paymentHash === paymentHash2)
assert(e1.parts.length === 2)
assert(e1.amount === 2834.msat)
assert(nodeParams.db.payments.getOutgoingPayment(testCase.childIds(1)).get.status.isInstanceOf[OutgoingPaymentStatus.Succeeded])
assert(nodeParams.db.payments.getOutgoingPayment(testCase.childIds(2)).get.status.isInstanceOf[OutgoingPaymentStatus.Succeeded])
assert(nodeParams.db.payments.getOutgoingPayment(testCase.childIds.head).get.status === OutgoingPaymentStatus.Pending)
sender.send(relayer, testCase.fulfills.head)
val e2 = eventListener.expectMsgType[PaymentSent]
assert(e2.id === testCase.childIds.head)
assert(e2.paymentPreimage === preimage1)
assert(e2.paymentHash === paymentHash1)
assert(e2.parts.length === 1)
assert(e2.amount === 561.msat)
assert(nodeParams.db.payments.getOutgoingPayment(testCase.childIds.head).get.status.isInstanceOf[OutgoingPaymentStatus.Succeeded])
commandBuffer.expectNoMsg(100 millis)
}
test("handle a trampoline relay htlc-fail") { f =>
import f._
val testCase = setupTrampolinePayments(nodeParams)
val relayer = f.createRelayer()
commandBuffer.expectNoMsg(100 millis)
// This downstream HTLC has two upstream HTLCs.
sender.send(relayer, buildForwardFail(testCase.downstream_1_1, testCase.upstream_1))
val fails = commandBuffer.expectMsgType[CommandBuffer.CommandSend] :: commandBuffer.expectMsgType[CommandBuffer.CommandSend] :: Nil
assert(fails.toSet === testCase.upstream_1.origins.map {
case (channelId, htlcId) => CommandBuffer.CommandSend(channelId, CMD_FAIL_HTLC(htlcId, Right(TemporaryNodeFailure), commit = true))
}.toSet)
sender.send(relayer, buildForwardFail(testCase.downstream_1_1, testCase.upstream_1))
commandBuffer.expectNoMsg(100 millis) // a duplicate failure should be ignored
sender.send(relayer, buildForwardFail(testCase.downstream_2_1, testCase.upstream_2))
sender.send(relayer, buildForwardFail(testCase.downstream_2_2, testCase.upstream_2))
commandBuffer.expectNoMsg(100 millis) // there is still a third downstream payment pending
sender.send(relayer, buildForwardFail(testCase.downstream_2_3, testCase.upstream_2))
commandBuffer.expectMsg(testCase.upstream_2.origins.map {
case (channelId, htlcId) => CommandBuffer.CommandSend(channelId, CMD_FAIL_HTLC(htlcId, Right(TemporaryNodeFailure), commit = true))
}.head)
commandBuffer.expectNoMsg(100 millis)
eventListener.expectNoMsg(100 millis)
}
test("handle a trampoline relay htlc-fulfill") { f =>
import f._
val testCase = setupTrampolinePayments(nodeParams)
val relayer = f.createRelayer()
commandBuffer.expectNoMsg(100 millis)
// This downstream HTLC has two upstream HTLCs.
sender.send(relayer, buildForwardFulfill(testCase.downstream_1_1, testCase.upstream_1, preimage1))
val fails = commandBuffer.expectMsgType[CommandBuffer.CommandSend] :: commandBuffer.expectMsgType[CommandBuffer.CommandSend] :: Nil
assert(fails.toSet === testCase.upstream_1.origins.map {
case (channelId, htlcId) => CommandBuffer.CommandSend(channelId, CMD_FULFILL_HTLC(htlcId, preimage1, commit = true))
}.toSet)
sender.send(relayer, buildForwardFulfill(testCase.downstream_1_1, testCase.upstream_1, preimage1))
commandBuffer.expectNoMsg(100 millis) // a duplicate fulfill should be ignored
// This payment has 3 downstream HTLCs, but we should fulfill upstream as soon as we receive the preimage.
sender.send(relayer, buildForwardFulfill(testCase.downstream_2_1, testCase.upstream_2, preimage2))
commandBuffer.expectMsg(testCase.upstream_2.origins.map {
case (channelId, htlcId) => CommandBuffer.CommandSend(channelId, CMD_FULFILL_HTLC(htlcId, preimage2, commit = true))
}.head)
sender.send(relayer, buildForwardFulfill(testCase.downstream_2_2, testCase.upstream_2, preimage2))
sender.send(relayer, buildForwardFulfill(testCase.downstream_2_3, testCase.upstream_2, preimage2))
commandBuffer.expectNoMsg(100 millis) // the payment has already been fulfilled upstream
eventListener.expectNoMsg(100 millis)
}
test("handle a trampoline relay htlc-fail followed by htlc-fulfill") { f =>
import f._
val testCase = setupTrampolinePayments(nodeParams)
val relayer = f.createRelayer()
commandBuffer.expectNoMsg(100 millis)
sender.send(relayer, buildForwardFail(testCase.downstream_2_1, testCase.upstream_2))
sender.send(relayer, buildForwardFulfill(testCase.downstream_2_2, testCase.upstream_2, preimage2))
commandBuffer.expectMsg(testCase.upstream_2.origins.map {
case (channelId, htlcId) => CommandBuffer.CommandSend(channelId, CMD_FULFILL_HTLC(htlcId, preimage2, commit = true))
}.head)
sender.send(relayer, buildForwardFail(testCase.downstream_2_3, testCase.upstream_2))
commandBuffer.expectNoMsg(100 millis) // the payment has already been fulfilled upstream
eventListener.expectNoMsg(100 millis)
}
}
object PostRestartHtlcCleanerSpec {
val channelId_ab_1 = randomBytes32
val channelId_ab_2 = randomBytes32
val channelId_bc_1 = randomBytes32
val channelId_bc_2 = randomBytes32
val channelId_bc_3 = randomBytes32
val (preimage1, preimage2) = (randomBytes32, randomBytes32)
val (paymentHash1, paymentHash2) = (Crypto.sha256(preimage1), Crypto.sha256(preimage2))
def buildHtlc(htlcId: Long, channelId: ByteVector32, paymentHash: ByteVector32, direction: Direction): DirectedHtlc = {
val (cmd, _) = buildCommand(Upstream.Local(UUID.randomUUID()), paymentHash, hops, FinalLegacyPayload(finalAmount, finalExpiry))
val add = UpdateAddHtlc(channelId, htlcId, cmd.amount, cmd.paymentHash, cmd.cltvExpiry, cmd.onion)
DirectedHtlc(direction, add)
}
def buildFinalHtlc(htlcId: Long, channelId: ByteVector32, paymentHash: ByteVector32): DirectedHtlc = {
val (cmd, _) = buildCommand(Upstream.Local(UUID.randomUUID()), paymentHash, ChannelHop(a, TestConstants.Bob.nodeParams.nodeId, channelUpdate_ab) :: Nil, FinalLegacyPayload(finalAmount, finalExpiry))
val add = UpdateAddHtlc(channelId, htlcId, cmd.amount, cmd.paymentHash, cmd.cltvExpiry, cmd.onion)
DirectedHtlc(IN, add)
}
def buildForwardFail(add: UpdateAddHtlc, origin: Origin) =
ForwardFail(UpdateFailHtlc(add.channelId, add.id, ByteVector.empty), origin, add)
def buildForwardFulfill(add: UpdateAddHtlc, origin: Origin, preimage: ByteVector32) =
ForwardFulfill(UpdateFulfillHtlc(add.channelId, add.id, preimage), origin, add)
case class LocalPaymentTest(parentId: UUID, childIds: Seq[UUID], fails: Seq[ForwardFail], fulfills: Seq[ForwardFulfill])
/**
* We setup two outgoing payments:
* - the first one is a single-part payment
* - the second one is a multi-part payment split between two child payments
*/
def setupLocalPayments(nodeParams: NodeParams): LocalPaymentTest = {
val parentId = UUID.randomUUID()
val (id1, id2, id3) = (UUID.randomUUID(), UUID.randomUUID(), UUID.randomUUID())
val add1 = UpdateAddHtlc(channelId_bc_1, 72, 561 msat, paymentHash1, CltvExpiry(4200), onionRoutingPacket = TestConstants.emptyOnionPacket)
val origin1 = Origin.Local(id1, None)
val add2 = UpdateAddHtlc(channelId_bc_1, 75, 1105 msat, paymentHash2, CltvExpiry(4250), onionRoutingPacket = TestConstants.emptyOnionPacket)
val origin2 = Origin.Local(id2, None)
val add3 = UpdateAddHtlc(channelId_bc_1, 78, 1729 msat, paymentHash2, CltvExpiry(4300), onionRoutingPacket = TestConstants.emptyOnionPacket)
val origin3 = Origin.Local(id3, None)
// Prepare channels and payment state before restart.
nodeParams.db.payments.addOutgoingPayment(OutgoingPayment(id1, id1, None, paymentHash1, add1.amountMsat, c, 0, None, OutgoingPaymentStatus.Pending))
nodeParams.db.payments.addOutgoingPayment(OutgoingPayment(id2, parentId, None, paymentHash2, add2.amountMsat, c, 0, None, OutgoingPaymentStatus.Pending))
nodeParams.db.payments.addOutgoingPayment(OutgoingPayment(id3, parentId, None, paymentHash2, add3.amountMsat, c, 0, None, OutgoingPaymentStatus.Pending))
nodeParams.db.channels.addOrUpdateChannel(ChannelCodecsSpec.makeChannelDataNormal(
Seq(add1, add2, add3).map(add => DirectedHtlc(OUT, add)),
Map(add1.id -> origin1, add2.id -> origin2, add3.id -> origin3))
)
val fails = Seq(buildForwardFail(add1, origin1), buildForwardFail(add2, origin2), buildForwardFail(add3, origin3))
val fulfills = Seq(buildForwardFulfill(add1, origin1, preimage1), buildForwardFulfill(add2, origin2, preimage2), buildForwardFulfill(add3, origin3, preimage2))
LocalPaymentTest(parentId, Seq(id1, id2, id3), fails, fulfills)
}
case class TrampolinePaymentTest(upstream_1: Origin.TrampolineRelayed,
downstream_1_1: UpdateAddHtlc,
upstream_2: Origin.TrampolineRelayed,
downstream_2_1: UpdateAddHtlc,
downstream_2_2: UpdateAddHtlc,
downstream_2_3: UpdateAddHtlc)
/**
* We setup two trampoline relayed payments:
* - the first one has 2 upstream HTLCs and 1 downstream HTLC
* - the second one has 1 upstream HTLC and 3 downstream HTLCs
*/
def setupTrampolinePayments(nodeParams: NodeParams): TrampolinePaymentTest = {
// Upstream HTLCs.
val htlc_ab_1 = Seq(
buildHtlc(0, channelId_ab_1, paymentHash1, IN),
buildHtlc(2, channelId_ab_1, randomBytes32, OUT), // ignored
buildHtlc(3, channelId_ab_1, randomBytes32, OUT), // ignored
buildHtlc(5, channelId_ab_1, paymentHash2, IN)
)
val htlc_ab_2 = Seq(
buildHtlc(1, channelId_ab_2, randomBytes32, OUT), // ignored
buildHtlc(7, channelId_ab_2, paymentHash1, IN),
buildHtlc(9, channelId_ab_2, randomBytes32, OUT) // ignored
)
val origin_1 = Origin.TrampolineRelayed((channelId_ab_1, 0L) :: (channelId_ab_2, 7L) :: Nil, None)
val origin_2 = Origin.TrampolineRelayed((channelId_ab_1, 5L) :: Nil, None)
// Downstream HTLCs.
val htlc_bc_1 = Seq(
buildHtlc(1, channelId_bc_1, randomBytes32, IN), // ignored
buildHtlc(6, channelId_bc_1, paymentHash1, OUT),
buildHtlc(8, channelId_bc_1, paymentHash2, OUT)
)
val htlc_bc_2 = Seq(
buildHtlc(0, channelId_bc_2, randomBytes32, IN), // ignored
buildHtlc(1, channelId_bc_2, paymentHash2, OUT)
)
val htlc_bc_3 = Seq(
buildHtlc(3, channelId_bc_3, randomBytes32, IN), // ignored
buildHtlc(4, channelId_bc_3, paymentHash2, OUT),
buildHtlc(5, channelId_bc_3, randomBytes32, IN) // ignored
)
val downstream_1_1 = UpdateAddHtlc(channelId_bc_1, 6L, finalAmount, paymentHash1, finalExpiry, TestConstants.emptyOnionPacket)
val downstream_2_1 = UpdateAddHtlc(channelId_bc_1, 8L, finalAmount, paymentHash2, finalExpiry, TestConstants.emptyOnionPacket)
val downstream_2_2 = UpdateAddHtlc(channelId_bc_2, 1L, finalAmount, paymentHash2, finalExpiry, TestConstants.emptyOnionPacket)
val downstream_2_3 = UpdateAddHtlc(channelId_bc_3, 4L, finalAmount, paymentHash2, finalExpiry, TestConstants.emptyOnionPacket)
val data_ab_1 = ChannelCodecsSpec.makeChannelDataNormal(htlc_ab_1, Map.empty)
val data_ab_2 = ChannelCodecsSpec.makeChannelDataNormal(htlc_ab_2, Map.empty)
val data_bc_1 = ChannelCodecsSpec.makeChannelDataNormal(htlc_bc_1, Map(6L -> origin_1, 8L -> origin_2))
val data_bc_2 = ChannelCodecsSpec.makeChannelDataNormal(htlc_bc_2, Map(1L -> origin_2))
val data_bc_3 = ChannelCodecsSpec.makeChannelDataNormal(htlc_bc_3, Map(4L -> origin_2))
// Prepare channels state before restart.
nodeParams.db.channels.addOrUpdateChannel(data_ab_1)
nodeParams.db.channels.addOrUpdateChannel(data_ab_2)
nodeParams.db.channels.addOrUpdateChannel(data_bc_1)
nodeParams.db.channels.addOrUpdateChannel(data_bc_2)
nodeParams.db.channels.addOrUpdateChannel(data_bc_3)
TrampolinePaymentTest(origin_1, downstream_1_1, origin_2, downstream_2_1, downstream_2_2, downstream_2_3)
}
}

View File

@ -20,16 +20,15 @@ import java.util.UUID
import akka.actor.{ActorRef, Props, Status}
import akka.testkit.TestProbe
import fr.acinq.bitcoin.{ByteVector32, Crypto}
import fr.acinq.bitcoin.ByteVector32
import fr.acinq.eclair.channel._
import fr.acinq.eclair.crypto.Sphinx
import fr.acinq.eclair.db.{OutgoingPayment, OutgoingPaymentStatus}
import fr.acinq.eclair.payment.IncomingPacket.FinalPacket
import fr.acinq.eclair.payment.OutgoingPacket.{buildCommand, buildOnion, buildPacket}
import fr.acinq.eclair.payment.relay.Origin._
import fr.acinq.eclair.payment.relay.Relayer._
import fr.acinq.eclair.payment.relay.{CommandBuffer, Origin, Relayer}
import fr.acinq.eclair.router.{Announcements, ChannelHop, NodeHop}
import fr.acinq.eclair.payment.relay.{CommandBuffer, Relayer}
import fr.acinq.eclair.router._
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}
@ -46,17 +45,17 @@ class RelayerSpec extends TestkitBaseClass {
import PaymentPacketSpec._
case class FixtureParam(nodeParams: NodeParams, relayer: ActorRef, register: TestProbe, paymentHandler: TestProbe, sender: TestProbe)
case class FixtureParam(nodeParams: NodeParams, relayer: ActorRef, router: TestProbe, register: TestProbe, paymentHandler: TestProbe, sender: TestProbe)
override def withFixture(test: OneArgTest): Outcome = {
within(30 seconds) {
val nodeParams = TestConstants.Bob.nodeParams
val register = TestProbe()
val (router, register) = (TestProbe(), TestProbe())
val commandBuffer = system.actorOf(Props(new CommandBuffer(nodeParams, register.ref)))
val paymentHandler = TestProbe()
// we are node B in the route A -> B -> C -> ....
val relayer = system.actorOf(Relayer.props(nodeParams, register.ref, commandBuffer, paymentHandler.ref))
withFixture(test.toNoArgTest(FixtureParam(nodeParams, relayer, register, paymentHandler, TestProbe())))
val relayer = system.actorOf(Relayer.props(nodeParams, router.ref, register.ref, commandBuffer, paymentHandler.ref))
withFixture(test.toNoArgTest(FixtureParam(nodeParams, relayer, router, register, paymentHandler, TestProbe())))
}
}
@ -67,7 +66,7 @@ class RelayerSpec extends TestkitBaseClass {
import f._
// we use this to build a valid onion
val (cmd, _) = buildCommand(UUID.randomUUID(), paymentHash, hops, FinalLegacyPayload(finalAmount, finalExpiry))
val (cmd, _) = buildCommand(Upstream.Local(UUID.randomUUID()), paymentHash, hops, FinalLegacyPayload(finalAmount, finalExpiry))
// and then manually build an htlc
val add_ab = UpdateAddHtlc(channelId = channelId_ab, id = 123456, cmd.amount, cmd.paymentHash, cmd.cltvExpiry, cmd.onion)
relayer ! LocalChannelUpdate(null, channelId_bc, channelUpdate_bc.shortChannelId, c, None, channelUpdate_bc, makeCommitments(channelId_bc))
@ -116,7 +115,7 @@ class RelayerSpec extends TestkitBaseClass {
import f._
// we use this to build a valid onion
val (cmd, _) = buildCommand(UUID.randomUUID(), paymentHash, hops, FinalLegacyPayload(finalAmount, finalExpiry))
val (cmd, _) = buildCommand(Upstream.Local(UUID.randomUUID()), paymentHash, hops, FinalLegacyPayload(finalAmount, finalExpiry))
// and then manually build an htlc
val add_ab = UpdateAddHtlc(channelId = channelId_ab, id = 123456, cmd.amount, cmd.paymentHash, cmd.cltvExpiry, cmd.onion)
@ -156,10 +155,59 @@ class RelayerSpec extends TestkitBaseClass {
paymentHandler.expectNoMsg(50 millis)
}
test("relay a trampoline htlc-add with retries") { f =>
import f._
// we tell the relayer about channel B-C
relayer ! LocalChannelUpdate(null, channelId_bc, channelUpdate_bc.shortChannelId, c, None, channelUpdate_bc, makeCommitments(channelId_bc))
val totalAmount = finalAmount * 3 // we simulate a payment split between multiple trampoline routes.
val trampolineHops = NodeHop(a, b, channelUpdate_ab.cltvExpiryDelta, 0 msat) :: NodeHop(b, c, nodeParams.expiryDeltaBlocks, nodeFee(nodeParams.feeBase, nodeParams.feeProportionalMillionth, finalAmount)) :: Nil
val (trampolineAmount, trampolineExpiry, trampolineOnion) = buildPacket(Sphinx.TrampolinePacket)(paymentHash, trampolineHops, Onion.createMultiPartPayload(finalAmount, totalAmount, finalExpiry, paymentSecret))
// A sends a multi-part payment to trampoline node B
val secret_ab = randomBytes32
val (cmd1, _) = buildCommand(Upstream.Local(UUID.randomUUID()), paymentHash, ChannelHop(a, b, channelUpdate_ab) :: Nil, Onion.createTrampolinePayload(trampolineAmount - 10000000.msat, trampolineAmount, trampolineExpiry, secret_ab, trampolineOnion.packet))
val add_ab1 = UpdateAddHtlc(channelId_ab, 561, cmd1.amount, cmd1.paymentHash, cmd1.cltvExpiry, cmd1.onion)
val (cmd2, _) = buildCommand(Upstream.Local(UUID.randomUUID()), paymentHash, ChannelHop(a, b, channelUpdate_ab) :: Nil, Onion.createTrampolinePayload(10000000.msat, trampolineAmount, trampolineExpiry, secret_ab, trampolineOnion.packet))
val add_ab2 = UpdateAddHtlc(channelId_ab, 565, cmd2.amount, cmd2.paymentHash, cmd2.cltvExpiry, cmd2.onion)
sender.send(relayer, ForwardAdd(add_ab1))
sender.send(relayer, ForwardAdd(add_ab2))
// A multi-part payment FSM should start to relay the payment.
router.expectMsg(GetNetworkStats)
router.send(router.lastSender, GetNetworkStatsResponse(None))
router.expectMsg(TickComputeNetworkStats)
// first try
val fwd1 = register.expectMsgType[Register.ForwardShortId[CMD_ADD_HTLC]]
assert(fwd1.shortChannelId === channelUpdate_bc.shortChannelId)
assert(fwd1.message.upstream.asInstanceOf[Upstream.TrampolineRelayed].adds === Seq(add_ab1, add_ab2))
// channel returns an error
val origin1 = TrampolineRelayed((channelId_ab, 561L) :: (channelId_ab, 565L) :: Nil, Some(register.lastSender))
sender.send(relayer, Status.Failure(AddHtlcFailed(channelId_bc, paymentHash, HtlcValueTooHighInFlight(channelId_bc, UInt64(1000000000L), 1516977616L msat), origin1, Some(channelUpdate_bc), originalCommand = Some(fwd1.message))))
// second try
val fwd2 = register.expectMsgType[Register.ForwardShortId[CMD_ADD_HTLC]]
assert(fwd2.shortChannelId === channelUpdate_bc.shortChannelId)
assert(fwd2.message.upstream.asInstanceOf[Upstream.TrampolineRelayed].adds === Seq(add_ab1, add_ab2))
// the downstream HTLC is successfully fulfilled
val origin2 = TrampolineRelayed((channelId_ab, 561L) :: (channelId_ab, 565L) :: Nil, Some(register.lastSender))
val add_bc = UpdateAddHtlc(channelId_bc, 72, cmd1.amount + cmd2.amount, paymentHash, cmd1.cltvExpiry, onionRoutingPacket = TestConstants.emptyOnionPacket)
val fulfill_ba = UpdateFulfillHtlc(channelId_bc, 72, paymentPreimage)
sender.send(relayer, ForwardFulfill(fulfill_ba, origin2, add_bc))
// it should trigger a fulfill on the upstream HTLCs
register.expectMsg(Register.Forward(channelId_ab, CMD_FULFILL_HTLC(561, paymentPreimage, commit = true)))
register.expectMsg(Register.Forward(channelId_ab, CMD_FULFILL_HTLC(565, paymentPreimage, commit = true)))
}
test("relay an htlc-add at the final node to the payment handler") { f =>
import f._
val (cmd, _) = buildCommand(UUID.randomUUID(), paymentHash, hops.take(1), FinalLegacyPayload(finalAmount, finalExpiry))
val (cmd, _) = buildCommand(Upstream.Local(UUID.randomUUID()), paymentHash, hops.take(1), FinalLegacyPayload(finalAmount, finalExpiry))
val add_ab = UpdateAddHtlc(channelId = channelId_ab, id = 123456, cmd.amount, cmd.paymentHash, cmd.cltvExpiry, cmd.onion)
sender.send(relayer, ForwardAdd(add_ab))
@ -181,7 +229,7 @@ class RelayerSpec extends TestkitBaseClass {
assert(trampolineAmount === finalAmount)
assert(trampolineExpiry === finalExpiry)
val (cmd, _) = buildCommand(UUID.randomUUID(), paymentHash, ChannelHop(a, b, channelUpdate_ab) :: Nil, Onion.createTrampolinePayload(trampolineAmount, trampolineAmount, trampolineExpiry, randomBytes32, trampolineOnion.packet))
val (cmd, _) = buildCommand(Upstream.Local(UUID.randomUUID()), paymentHash, ChannelHop(a, b, channelUpdate_ab) :: Nil, Onion.createTrampolinePayload(trampolineAmount, trampolineAmount, trampolineExpiry, randomBytes32, trampolineOnion.packet))
assert(cmd.amount === finalAmount)
assert(cmd.cltvExpiry === finalExpiry)
@ -203,7 +251,7 @@ class RelayerSpec extends TestkitBaseClass {
import f._
// we use this to build a valid onion
val (cmd, _) = buildCommand(UUID.randomUUID(), paymentHash, hops, FinalLegacyPayload(finalAmount, finalExpiry))
val (cmd, _) = buildCommand(Upstream.Local(UUID.randomUUID()), paymentHash, hops, FinalLegacyPayload(finalAmount, finalExpiry))
// and then manually build an htlc
val add_ab = UpdateAddHtlc(channelId = channelId_ab, id = 123456, cmd.amount, cmd.paymentHash, cmd.cltvExpiry, cmd.onion)
@ -222,7 +270,7 @@ class RelayerSpec extends TestkitBaseClass {
import f._
// we use this to build a valid onion
val (cmd, _) = buildCommand(UUID.randomUUID(), paymentHash, hops, FinalLegacyPayload(finalAmount, finalExpiry))
val (cmd, _) = buildCommand(Upstream.Local(UUID.randomUUID()), paymentHash, hops, FinalLegacyPayload(finalAmount, finalExpiry))
// and then manually build an htlc
val add_ab = UpdateAddHtlc(channelId = channelId_ab, id = 123456, cmd.amount, cmd.paymentHash, cmd.cltvExpiry, cmd.onion)
relayer ! LocalChannelUpdate(null, channelId_bc, channelUpdate_bc.shortChannelId, c, None, channelUpdate_bc, makeCommitments(channelId_bc))
@ -248,7 +296,7 @@ class RelayerSpec extends TestkitBaseClass {
import f._
// check that payments are sent properly
val (cmd, _) = buildCommand(UUID.randomUUID(), paymentHash, hops, FinalLegacyPayload(finalAmount, finalExpiry))
val (cmd, _) = buildCommand(Upstream.Local(UUID.randomUUID()), paymentHash, hops, FinalLegacyPayload(finalAmount, finalExpiry))
val add_ab = UpdateAddHtlc(channelId = channelId_ab, id = 123456, cmd.amount, cmd.paymentHash, cmd.cltvExpiry, cmd.onion)
relayer ! LocalChannelUpdate(null, channelId_bc, channelUpdate_bc.shortChannelId, c, None, channelUpdate_bc, makeCommitments(channelId_bc))
@ -264,7 +312,7 @@ class RelayerSpec extends TestkitBaseClass {
// now tell the relayer that the channel is down and try again
relayer ! LocalChannelDown(sender.ref, channelId = channelId_bc, shortChannelId = channelUpdate_bc.shortChannelId, remoteNodeId = TestConstants.Bob.nodeParams.nodeId)
val (cmd1, _) = buildCommand(UUID.randomUUID(), randomBytes32, hops, FinalLegacyPayload(finalAmount, finalExpiry))
val (cmd1, _) = buildCommand(Upstream.Local(UUID.randomUUID()), randomBytes32, hops, FinalLegacyPayload(finalAmount, finalExpiry))
val add_ab1 = UpdateAddHtlc(channelId = channelId_ab, id = 123456, cmd1.amount, cmd1.paymentHash, cmd1.cltvExpiry, cmd1.onion)
sender.send(relayer, ForwardAdd(add_ab1))
@ -280,7 +328,7 @@ class RelayerSpec extends TestkitBaseClass {
import f._
// we use this to build a valid onion
val (cmd, _) = buildCommand(UUID.randomUUID(), paymentHash, hops, FinalLegacyPayload(finalAmount, finalExpiry))
val (cmd, _) = buildCommand(Upstream.Local(UUID.randomUUID()), paymentHash, hops, FinalLegacyPayload(finalAmount, finalExpiry))
// and then manually build an htlc
val add_ab = UpdateAddHtlc(channelId = channelId_ab, id = 123456, cmd.amount, cmd.paymentHash, cmd.cltvExpiry, cmd.onion)
val channelUpdate_bc_disabled = channelUpdate_bc.copy(channelFlags = Announcements.makeChannelFlags(Announcements.isNode1(channelUpdate_bc.channelFlags), enable = false))
@ -300,7 +348,7 @@ class RelayerSpec extends TestkitBaseClass {
import f._
// we use this to build a valid onion
val (cmd, _) = buildCommand(UUID.randomUUID(), paymentHash, hops, FinalLegacyPayload(finalAmount, finalExpiry))
val (cmd, _) = buildCommand(Upstream.Local(UUID.randomUUID()), paymentHash, hops, FinalLegacyPayload(finalAmount, finalExpiry))
// and then manually build an htlc with an invalid onion (hmac)
val add_ab = UpdateAddHtlc(channelId = channelId_ab, id = 123456, cmd.amount, cmd.paymentHash, cmd.cltvExpiry, cmd.onion.copy(hmac = cmd.onion.hmac.reverse))
relayer ! LocalChannelUpdate(null, channelId_bc, channelUpdate_bc.shortChannelId, c, None, channelUpdate_bc, makeCommitments(channelId_bc))
@ -321,12 +369,12 @@ class RelayerSpec extends TestkitBaseClass {
val nodeParams = TestConstants.Bob.nodeParams.copy(enableTrampolinePayment = false)
val commandBuffer = system.actorOf(Props(new CommandBuffer(nodeParams, register.ref)))
val relayer = system.actorOf(Relayer.props(nodeParams, register.ref, commandBuffer, paymentHandler.ref))
val relayer = system.actorOf(Relayer.props(nodeParams, router.ref, register.ref, commandBuffer, paymentHandler.ref))
// we use this to build a valid trampoline onion inside a normal onion
val trampolineHops = NodeHop(a, b, channelUpdate_ab.cltvExpiryDelta, 0 msat) :: NodeHop(b, c, channelUpdate_bc.cltvExpiryDelta, fee_b) :: Nil
val (trampolineAmount, trampolineExpiry, trampolineOnion) = buildPacket(Sphinx.TrampolinePacket)(paymentHash, trampolineHops, Onion.createSinglePartPayload(finalAmount, finalExpiry))
val (cmd, _) = buildCommand(UUID.randomUUID(), paymentHash, ChannelHop(a, b, channelUpdate_ab) :: Nil, Onion.createTrampolinePayload(trampolineAmount, trampolineAmount, trampolineExpiry, randomBytes32, trampolineOnion.packet))
val (cmd, _) = buildCommand(Upstream.Local(UUID.randomUUID()), paymentHash, ChannelHop(a, b, channelUpdate_ab) :: Nil, Onion.createTrampolinePayload(trampolineAmount, trampolineAmount, trampolineExpiry, randomBytes32, trampolineOnion.packet))
// and then manually build an htlc
val add_ab = UpdateAddHtlc(channelId = channelId_ab, id = 123456, cmd.amount, cmd.paymentHash, cmd.cltvExpiry, cmd.onion)
@ -348,7 +396,7 @@ class RelayerSpec extends TestkitBaseClass {
// we use this to build a valid onion
val finalPayload = FinalLegacyPayload(channelUpdate_bc.htlcMinimumMsat - (1 msat), finalExpiry)
val zeroFeeHops = hops.map(hop => hop.copy(lastUpdate = hop.lastUpdate.copy(feeBaseMsat = 0 msat, feeProportionalMillionths = 0)))
val (cmd, _) = buildCommand(UUID.randomUUID(), paymentHash, zeroFeeHops, finalPayload)
val (cmd, _) = buildCommand(Upstream.Local(UUID.randomUUID()), paymentHash, zeroFeeHops, finalPayload)
// and then manually build an htlc
val add_ab = UpdateAddHtlc(channelId = channelId_ab, id = 123456, cmd.amount, cmd.paymentHash, cmd.cltvExpiry, cmd.onion)
relayer ! LocalChannelUpdate(null, channelId_bc, channelUpdate_bc.shortChannelId, c, None, channelUpdate_bc, makeCommitments(channelId_bc))
@ -367,7 +415,7 @@ class RelayerSpec extends TestkitBaseClass {
import f._
val hops1 = hops.updated(1, hops(1).copy(lastUpdate = hops(1).lastUpdate.copy(cltvExpiryDelta = CltvExpiryDelta(0))))
val (cmd, _) = buildCommand(UUID.randomUUID(), paymentHash, hops1, FinalLegacyPayload(finalAmount, finalExpiry))
val (cmd, _) = buildCommand(Upstream.Local(UUID.randomUUID()), paymentHash, hops1, FinalLegacyPayload(finalAmount, finalExpiry))
// and then manually build an htlc
val add_ab = UpdateAddHtlc(channelId = channelId_ab, id = 123456, cmd.amount, cmd.paymentHash, cmd.cltvExpiry, cmd.onion)
relayer ! LocalChannelUpdate(null, channelId_bc, channelUpdate_bc.shortChannelId, c, None, channelUpdate_bc, makeCommitments(channelId_bc))
@ -386,7 +434,7 @@ class RelayerSpec extends TestkitBaseClass {
import f._
val hops1 = hops.updated(1, hops(1).copy(lastUpdate = hops(1).lastUpdate.copy(feeBaseMsat = hops(1).lastUpdate.feeBaseMsat / 2)))
val (cmd, _) = buildCommand(UUID.randomUUID(), paymentHash, hops1, FinalLegacyPayload(finalAmount, finalExpiry))
val (cmd, _) = buildCommand(Upstream.Local(UUID.randomUUID()), paymentHash, hops1, FinalLegacyPayload(finalAmount, finalExpiry))
// and then manually build an htlc
val add_ab = UpdateAddHtlc(channelId = channelId_ab, id = 123456, cmd.amount, cmd.paymentHash, cmd.cltvExpiry, cmd.onion)
relayer ! LocalChannelUpdate(null, channelId_bc, channelUpdate_bc.shortChannelId, c, None, channelUpdate_bc, makeCommitments(channelId_bc))
@ -437,65 +485,31 @@ class RelayerSpec extends TestkitBaseClass {
// we build a fake htlc for the downstream channel
val add_bc = UpdateAddHtlc(channelId = channelId_bc, id = 72, amountMsat = 10000000 msat, paymentHash = ByteVector32.Zeroes, CltvExpiry(4200), onionRoutingPacket = TestConstants.emptyOnionPacket)
val fulfill_ba = UpdateFulfillHtlc(channelId = channelId_bc, id = 42, paymentPreimage = ByteVector32.Zeroes)
val origin = Relayed(channelId_ab, 150, 11000000 msat, 10000000 msat)
val fulfill_ba = UpdateFulfillHtlc(channelId = channelId_bc, id = 72, paymentPreimage = ByteVector32.Zeroes)
val origin = Relayed(channelId_ab, 42, 11000000 msat, 10000000 msat)
sender.send(relayer, ForwardFulfill(fulfill_ba, origin, add_bc))
val fwd = register.expectMsgType[Register.Forward[CMD_FULFILL_HTLC]]
assert(fwd.channelId === origin.originChannelId)
assert(fwd.message.id === origin.originHtlcId)
val paymentRelayed = eventListener.expectMsgType[PaymentRelayed]
assert(paymentRelayed.copy(timestamp = 0) === PaymentRelayed(origin.amountIn, origin.amountOut, add_bc.paymentHash, channelId_ab, channelId_bc, timestamp = 0))
val paymentRelayed = eventListener.expectMsgType[ChannelPaymentRelayed]
assert(paymentRelayed.copy(timestamp = 0) === ChannelPaymentRelayed(origin.amountIn, origin.amountOut, add_bc.paymentHash, channelId_ab, channelId_bc, timestamp = 0))
}
test("handle an htlc-fulfill after restart") { f =>
test("relay a trampoline htlc-fulfill") { f =>
import f._
val eventListener = TestProbe()
system.eventStream.subscribe(eventListener.ref, classOf[PaymentEvent])
val parentId = UUID.randomUUID()
val (id1, id2, id3) = (UUID.randomUUID(), UUID.randomUUID(), UUID.randomUUID())
val (preimage1, preimage2) = (randomBytes32, randomBytes32)
val (paymentHash1, paymentHash2) = (Crypto.sha256(preimage1), Crypto.sha256(preimage2))
val payFSM = TestProbe()
val add1 = UpdateAddHtlc(channelId_bc, 72, 561 msat, paymentHash1, CltvExpiry(4200), onionRoutingPacket = TestConstants.emptyOnionPacket)
val fulfill1 = UpdateFulfillHtlc(channelId_bc, 72, preimage1)
val origin1 = Origin.Local(id1, None)
val add2 = UpdateAddHtlc(channelId_bc, 75, 1105 msat, paymentHash1, CltvExpiry(4250), onionRoutingPacket = TestConstants.emptyOnionPacket)
val fulfill2 = UpdateFulfillHtlc(channelId_bc, 75, preimage1)
val origin2 = Origin.Local(id2, None)
val add3 = UpdateAddHtlc(channelId_bc, 78, 1729 msat, paymentHash2, CltvExpiry(4300), onionRoutingPacket = TestConstants.emptyOnionPacket)
val fulfill3 = UpdateFulfillHtlc(channelId_bc, 78, preimage2)
val origin3 = Origin.Local(id3, None)
// we build a fake htlc for the downstream channel
val add_bc = UpdateAddHtlc(channelId = channelId_bc, id = 72, amountMsat = 10000000 msat, paymentHash = ByteVector32.Zeroes, CltvExpiry(4200), onionRoutingPacket = TestConstants.emptyOnionPacket)
val fulfill_ba = UpdateFulfillHtlc(channelId = channelId_bc, id = 72, paymentPreimage = ByteVector32.Zeroes)
val origin = TrampolineRelayed(List((channelId_ab, 42), (randomBytes32, 7)), Some(payFSM.ref))
sender.send(relayer, ForwardFulfill(fulfill_ba, origin, add_bc))
nodeParams.db.payments.addOutgoingPayment(OutgoingPayment(id1, parentId, None, paymentHash1, add1.amountMsat, c, 0, None, OutgoingPaymentStatus.Pending))
nodeParams.db.payments.addOutgoingPayment(OutgoingPayment(id2, parentId, None, paymentHash1, add2.amountMsat, c, 0, None, OutgoingPaymentStatus.Pending))
nodeParams.db.payments.addOutgoingPayment(OutgoingPayment(id3, id3, None, paymentHash2, add3.amountMsat, c, 0, None, OutgoingPaymentStatus.Pending))
sender.send(relayer, ForwardFulfill(fulfill1, origin1, add1))
eventListener.expectNoMsg(100 millis)
assert(nodeParams.db.payments.getOutgoingPayment(id2).get.status === OutgoingPaymentStatus.Pending)
sender.send(relayer, ForwardFulfill(fulfill2, origin2, add2))
val e1 = eventListener.expectMsgType[PaymentSent]
assert(e1.id === parentId)
assert(e1.paymentPreimage === preimage1)
assert(e1.paymentHash === paymentHash1)
assert(e1.parts.length === 2)
assert(e1.amount === 1666.msat)
assert(nodeParams.db.payments.getOutgoingPayment(id1).get.status.isInstanceOf[OutgoingPaymentStatus.Succeeded])
assert(nodeParams.db.payments.getOutgoingPayment(id2).get.status.isInstanceOf[OutgoingPaymentStatus.Succeeded])
assert(nodeParams.db.payments.getOutgoingPayment(id3).get.status === OutgoingPaymentStatus.Pending)
sender.send(relayer, ForwardFulfill(fulfill3, origin3, add3))
val e2 = eventListener.expectMsgType[PaymentSent]
assert(e2.id === id3)
assert(e2.paymentPreimage === preimage2)
assert(e2.paymentHash === paymentHash2)
assert(e2.parts.length === 1)
assert(e2.amount === 1729.msat)
assert(nodeParams.db.payments.getOutgoingPayment(id3).get.status.isInstanceOf[OutgoingPaymentStatus.Succeeded])
// we forward to the FSM responsible for the payment.
payFSM.expectMsg(fulfill_ba)
}
test("relay an htlc-fail") { f =>
@ -503,8 +517,8 @@ class RelayerSpec extends TestkitBaseClass {
// we build a fake htlc for the downstream channel
val add_bc = UpdateAddHtlc(channelId = channelId_bc, id = 72, amountMsat = 10000000 msat, paymentHash = ByteVector32.Zeroes, CltvExpiry(4200), onionRoutingPacket = TestConstants.emptyOnionPacket)
val fail_ba = UpdateFailHtlc(channelId = channelId_bc, id = 42, reason = Sphinx.FailurePacket.create(ByteVector32(ByteVector.fill(32)(1)), TemporaryChannelFailure(channelUpdate_cd)))
val origin = Relayed(channelId_ab, 150, 11000000 msat, 10000000 msat)
val fail_ba = UpdateFailHtlc(channelId = channelId_bc, id = 72, reason = Sphinx.FailurePacket.create(ByteVector32(ByteVector.fill(32)(1)), TemporaryChannelFailure(channelUpdate_cd)))
val origin = Relayed(channelId_ab, 42, 11000000 msat, 10000000 msat)
sender.send(relayer, ForwardFail(fail_ba, origin, add_bc))
val fwd = register.expectMsgType[Register.Forward[CMD_FAIL_HTLC]]
@ -512,46 +526,19 @@ class RelayerSpec extends TestkitBaseClass {
assert(fwd.message.id === origin.originHtlcId)
}
test("handle an htlc-fail after restart") { f =>
test("relay a trampoline htlc-fail") { f =>
import f._
val eventListener = TestProbe()
system.eventStream.subscribe(eventListener.ref, classOf[PaymentEvent])
val parentId = UUID.randomUUID()
val (id1, id2, id3) = (UUID.randomUUID(), UUID.randomUUID(), UUID.randomUUID())
val (paymentHash1, paymentHash2) = (randomBytes32, randomBytes32)
val payFSM = TestProbe()
val add1 = UpdateAddHtlc(channelId_bc, 72, 561 msat, paymentHash1, CltvExpiry(4200), onionRoutingPacket = TestConstants.emptyOnionPacket)
val fail1 = UpdateFailHtlc(channelId_bc, 72, ByteVector.empty)
val origin1 = Origin.Local(id1, None)
val add2 = UpdateAddHtlc(channelId_bc, 75, 1105 msat, paymentHash1, CltvExpiry(4250), onionRoutingPacket = TestConstants.emptyOnionPacket)
val fail2 = UpdateFailHtlc(channelId_bc, 75, ByteVector.empty)
val origin2 = Origin.Local(id2, None)
val add3 = UpdateAddHtlc(channelId_bc, 78, 1729 msat, paymentHash2, CltvExpiry(4300), onionRoutingPacket = TestConstants.emptyOnionPacket)
val fail3 = UpdateFailHtlc(channelId_bc, 78, ByteVector.empty)
val origin3 = Origin.Local(id3, None)
// we build a fake htlc for the downstream channel
val add_bc = UpdateAddHtlc(channelId = channelId_bc, id = 72, amountMsat = 10000000 msat, paymentHash = ByteVector32.Zeroes, CltvExpiry(4200), onionRoutingPacket = TestConstants.emptyOnionPacket)
val fail_ba = UpdateFailHtlc(channelId = channelId_bc, id = 72, reason = Sphinx.FailurePacket.create(ByteVector32(ByteVector.fill(32)(1)), TemporaryChannelFailure(channelUpdate_cd)))
val origin = TrampolineRelayed(List((channelId_ab, 42), (randomBytes32, 7)), Some(payFSM.ref))
sender.send(relayer, ForwardFail(fail_ba, origin, add_bc))
nodeParams.db.payments.addOutgoingPayment(OutgoingPayment(id1, parentId, None, paymentHash1, add1.amountMsat, c, 0, None, OutgoingPaymentStatus.Pending))
nodeParams.db.payments.addOutgoingPayment(OutgoingPayment(id2, parentId, None, paymentHash1, add2.amountMsat, c, 0, None, OutgoingPaymentStatus.Pending))
nodeParams.db.payments.addOutgoingPayment(OutgoingPayment(id3, id3, None, paymentHash2, add3.amountMsat, c, 0, None, OutgoingPaymentStatus.Pending))
sender.send(relayer, ForwardFail(fail1, origin1, add1))
eventListener.expectNoMsg(100 millis)
assert(nodeParams.db.payments.getOutgoingPayment(id2).get.status === OutgoingPaymentStatus.Pending)
sender.send(relayer, ForwardFail(fail2, origin2, add2))
val e1 = eventListener.expectMsgType[PaymentFailed]
assert(e1.id === parentId)
assert(e1.paymentHash === paymentHash1)
assert(nodeParams.db.payments.getOutgoingPayment(id1).get.status.isInstanceOf[OutgoingPaymentStatus.Failed])
assert(nodeParams.db.payments.getOutgoingPayment(id2).get.status.isInstanceOf[OutgoingPaymentStatus.Failed])
assert(nodeParams.db.payments.getOutgoingPayment(id3).get.status === OutgoingPaymentStatus.Pending)
sender.send(relayer, ForwardFail(fail3, origin3, add3))
val e2 = eventListener.expectMsgType[PaymentFailed]
assert(e2.id === id3)
assert(e2.paymentHash === paymentHash2)
assert(nodeParams.db.payments.getOutgoingPayment(id3).get.status.isInstanceOf[OutgoingPaymentStatus.Failed])
// we forward to the FSM responsible for the payment to trigger the retry mechanism.
payFSM.expectMsg(fail_ba)
}
test("get outgoing channels") { f =>
@ -611,53 +598,4 @@ class RelayerSpec extends TestkitBaseClass {
assert(channels8.head.channelUpdate.shortChannelId === ShortChannelId(42))
}
test("replay pending commands after restart") { f =>
import f._
val channel = TestProbe()
val channelData = ChannelCodecsSpec.normal
val channelId = channelData.commitments.channelId
val remoteNodeId = channelData.commitments.remoteParams.nodeId
val (preimage1, preimage2) = (randomBytes32, randomBytes32)
val onionHash = randomBytes32
nodeParams.db.pendingRelay.addPendingRelay(channelId, CMD_FULFILL_HTLC(1, preimage1, commit = true))
nodeParams.db.pendingRelay.addPendingRelay(channelId, CMD_FULFILL_HTLC(100, preimage2, commit = true))
nodeParams.db.pendingRelay.addPendingRelay(channelId, CMD_FAIL_HTLC(101, Right(TemporaryNodeFailure), commit = true))
nodeParams.db.pendingRelay.addPendingRelay(channelId, CMD_FAIL_MALFORMED_HTLC(102, onionHash, 0x4001, commit = true))
// Channel comes online: we should replay pending commands.
system.eventStream.publish(ChannelStateChanged(channel.ref, system.deadLetters, remoteNodeId, OFFLINE, NORMAL, channelData))
val expected = Set(
CMD_FULFILL_HTLC(1, preimage1),
CMD_FULFILL_HTLC(100, preimage2),
CMD_FAIL_HTLC(101, Right(TemporaryNodeFailure)),
CMD_FAIL_MALFORMED_HTLC(102, onionHash, 0x4001)
)
val received = expected.map(_ => channel.expectMsgType[Command])
assert(received === expected)
channel.expectMsg(CMD_SIGN) // we should then ask to sign all the updates.
channel.expectNoMsg(100 millis)
// 3 of the 4 HTLCs were ack-ed (maybe the peer disconnected/reconnected again).
channel.send(relayer, CommandBuffer.CommandAck(channelId, 1))
channel.send(relayer, CommandBuffer.CommandAck(channelId, 100))
channel.send(relayer, CommandBuffer.CommandAck(channelId, 101))
// Once ack-ed, these commands should be removed from the DB.
awaitCond(nodeParams.db.pendingRelay.listPendingRelay(channelId).length === 1)
// The last failure wasn't ack-ed, so it's re-sent.
system.eventStream.publish(ChannelStateChanged(channel.ref, system.deadLetters, remoteNodeId, SYNCING, NORMAL, channelData))
channel.expectMsg(CMD_FAIL_MALFORMED_HTLC(102, onionHash, 0x4001))
channel.expectMsg(CMD_SIGN)
channel.expectNoMsg(100 millis)
channel.send(relayer, CommandBuffer.CommandAck(channelId, 102))
awaitCond(nodeParams.db.pendingRelay.listPendingRelay(channelId).isEmpty)
system.eventStream.publish(ChannelStateChanged(channel.ref, system.deadLetters, remoteNodeId, OFFLINE, NORMAL, channelData))
channel.expectNoMsg(100 millis)
}
}

View File

@ -27,7 +27,8 @@ import fr.acinq.bitcoin.{Block, ByteVector32, ByteVector64, Crypto, Deterministi
import fr.acinq.eclair.channel.Helpers.Funding
import fr.acinq.eclair.channel._
import fr.acinq.eclair.crypto.{LocalKeyManager, ShaChain}
import fr.acinq.eclair.payment.relay.Origin.{Local, Relayed}
import fr.acinq.eclair.payment.relay.Origin
import fr.acinq.eclair.payment.relay.Origin.{Local, Relayed, TrampolineRelayed}
import fr.acinq.eclair.router.Announcements
import fr.acinq.eclair.transactions.Transactions.{CommitTx, InputInfo, TransactionWithInputInfo}
import fr.acinq.eclair.transactions._
@ -174,18 +175,23 @@ class ChannelCodecsSpec extends FunSuite {
test("encode/decode origin") {
val id = UUID.randomUUID()
assert(originCodec.decodeValue(originCodec.encode(Local(id, Some(ActorSystem("test").deadLetters))).require).require === Local(id, None))
// TODO: add backward compatibility check
assert(originCodec.decodeValue(hex"0001 0123456789abcdef0123456789abcdef".bits).require === Local(UNKNOWN_UUID, None))
val relayed = Relayed(randomBytes32, 4324, 12000000 msat, 11000000 msat)
assert(originCodec.decodeValue(originCodec.encode(relayed).require).require === relayed)
val trampolineRelayed = TrampolineRelayed((randomBytes32, 1L) :: (randomBytes32, 1L) :: (randomBytes32, 2L) :: Nil, None)
assert(originCodec.decodeValue(originCodec.encode(trampolineRelayed).require).require === trampolineRelayed)
}
test("encode/decode map of origins") {
val map = Map(
1L -> Local(UUID.randomUUID(), None),
42L -> Relayed(randomBytes32, 4324, 12000000 msat, 11000000 msat),
43L -> TrampolineRelayed((randomBytes32, 17L) :: (randomBytes32, 21L) :: (randomBytes32, 21L) :: Nil, None),
130L -> Relayed(randomBytes32, -45, 13000000 msat, 12000000 msat),
140L -> TrampolineRelayed((randomBytes32, 0L) :: Nil, None),
1000L -> Relayed(randomBytes32, 10, 14000000 msat, 13000000 msat),
-32L -> Relayed(randomBytes32, 54, 15000000 msat, 14000000 msat),
-54L -> TrampolineRelayed((randomBytes32, 1L) :: (randomBytes32, 2L) :: Nil, None),
-4L -> Local(UUID.randomUUID(), None))
assert(originsMapCodec.decodeValue(originsMapCodec.encode(map).require).require === map)
}
@ -391,22 +397,27 @@ object ChannelCodecsSpec {
DirectedHtlc(IN, UpdateAddHtlc(ByteVector32.Zeroes, 2, 4000000 msat, Crypto.sha256(paymentPreimages(4)), CltvExpiry(504), TestConstants.emptyOnionPacket))
)
val fundingTx = Transaction.read("0200000001adbb20ea41a8423ea937e76e8151636bf6093b70eaff942930d20576600521fd000000006b48304502210090587b6201e166ad6af0227d3036a9454223d49a1f11839c1a362184340ef0240220577f7cd5cca78719405cbf1de7414ac027f0239ef6e214c90fcaab0454d84b3b012103535b32d5eb0a6ed0982a0479bbadc9868d9836f6ba94dd5a63be16d875069184ffffffff028096980000000000220020c015c4a6be010e21657068fc2e6a9d02b27ebe4d490a25846f7237f104d1a3cd20256d29010000001600143ca33c2e4446f4a305f23c80df8ad1afdcf652f900000000")
val fundingAmount = fundingTx.txOut(0).amount
val commitmentInput = Funding.makeFundingInputInfo(fundingTx.hash, 0, fundingAmount, keyManager.fundingPublicKey(localParams.fundingKeyPath).publicKey, remoteParams.fundingPubKey)
val normal = makeChannelDataNormal(htlcs, Map(42L -> Local(UUID.randomUUID, None), 15000L -> Relayed(ByteVector32(ByteVector.fill(32)(42)), 43, 11000000 msat, 10000000 msat)))
val localCommit = LocalCommit(0, CommitmentSpec(htlcs.toSet, 1500, 50000000 msat, 70000000 msat), PublishableTxs(CommitTx(commitmentInput, Transaction(2, Nil, Nil, 0)), Nil))
val remoteCommit = RemoteCommit(0, CommitmentSpec(htlcs.map(htlc => htlc.copy(direction = htlc.direction.opposite)).toSet, 1500, 50000 msat, 700000 msat), ByteVector32(hex"0303030303030303030303030303030303030303030303030303030303030303"), PrivateKey(ByteVector.fill(32)(4)).publicKey)
val commitments = Commitments(ChannelVersion.STANDARD, localParams, remoteParams, channelFlags = 0x01.toByte, localCommit, remoteCommit, LocalChanges(Nil, Nil, Nil), RemoteChanges(Nil, Nil, Nil),
localNextHtlcId = 32L,
remoteNextHtlcId = 4L,
originChannels = Map(42L -> Local(UUID.randomUUID, None), 15000L -> Relayed(ByteVector32(ByteVector.fill(32)(42)), 43, 11000000 msat, 10000000 msat)),
remoteNextCommitInfo = Right(randomKey.publicKey),
commitInput = commitmentInput, remotePerCommitmentSecrets = ShaChain.init, channelId = ByteVector32.Zeroes)
def makeChannelDataNormal(htlcs: Seq[DirectedHtlc], origins: Map[Long, Origin]): DATA_NORMAL = {
val channelUpdate = Announcements.makeChannelUpdate(ByteVector32(ByteVector.fill(32)(1)), randomKey, randomKey.publicKey, ShortChannelId(142553), CltvExpiryDelta(42), 15 msat, 575 msat, 53, Channel.MAX_FUNDING.toMilliSatoshi)
val fundingTx = Transaction.read("0200000001adbb20ea41a8423ea937e76e8151636bf6093b70eaff942930d20576600521fd000000006b48304502210090587b6201e166ad6af0227d3036a9454223d49a1f11839c1a362184340ef0240220577f7cd5cca78719405cbf1de7414ac027f0239ef6e214c90fcaab0454d84b3b012103535b32d5eb0a6ed0982a0479bbadc9868d9836f6ba94dd5a63be16d875069184ffffffff028096980000000000220020c015c4a6be010e21657068fc2e6a9d02b27ebe4d490a25846f7237f104d1a3cd20256d29010000001600143ca33c2e4446f4a305f23c80df8ad1afdcf652f900000000")
val fundingAmount = fundingTx.txOut.head.amount
val commitmentInput = Funding.makeFundingInputInfo(fundingTx.hash, 0, fundingAmount, keyManager.fundingPublicKey(localParams.fundingKeyPath).publicKey, remoteParams.fundingPubKey)
val channelUpdate = Announcements.makeChannelUpdate(ByteVector32(ByteVector.fill(32)(1)), randomKey, randomKey.publicKey, ShortChannelId(142553), CltvExpiryDelta(42), 15 msat, 575 msat, 53, Channel.MAX_FUNDING.toMilliSatoshi)
val localCommit = LocalCommit(0, CommitmentSpec(htlcs.toSet, 1500, 50000000 msat, 70000000 msat), PublishableTxs(CommitTx(commitmentInput, Transaction(2, Nil, Nil, 0)), Nil))
val remoteCommit = RemoteCommit(0, CommitmentSpec(htlcs.map(htlc => htlc.copy(direction = htlc.direction.opposite)).toSet, 1500, 50000 msat, 700000 msat), ByteVector32(hex"0303030303030303030303030303030303030303030303030303030303030303"), PrivateKey(ByteVector.fill(32)(4)).publicKey)
val commitments = Commitments(ChannelVersion.STANDARD, localParams, remoteParams, channelFlags = 0x01.toByte, localCommit, remoteCommit, LocalChanges(Nil, Nil, Nil), RemoteChanges(Nil, Nil, Nil),
localNextHtlcId = 32L,
remoteNextHtlcId = 4L,
originChannels = origins,
remoteNextCommitInfo = Right(randomKey.publicKey),
commitInput = commitmentInput,
remotePerCommitmentSecrets = ShaChain.init,
channelId = htlcs.headOption.map(_.add.channelId).getOrElse(ByteVector32.Zeroes))
val normal = DATA_NORMAL(commitments, ShortChannelId(42), true, None, channelUpdate, None, None)
DATA_NORMAL(commitments, ShortChannelId(42), buried = true, None, channelUpdate, None, None)
}
object JsonSupport {

View File

@ -263,7 +263,7 @@ object CustomTypeHints {
val paymentEvent = CustomTypeHints(Map(
classOf[PaymentSent] -> "payment-sent",
classOf[PaymentRelayed] -> "payment-relayed",
classOf[ChannelPaymentRelayed] -> "payment-relayed",
classOf[PaymentReceived] -> "payment-received",
classOf[PaymentSettlingOnChain] -> "payment-settling-onchain",
classOf[PaymentFailed] -> "payment-failed"

View File

@ -221,6 +221,13 @@ trait Service extends ExtraDirectives with Logging {
case _ => reject(MalformedFormFieldRejection("invoice", "The invoice must have an amount or you need to specify one using the field 'amountMsat'"))
}
} ~
// TODO: @t-bast: remove this API once stabilized: should re-work the payment APIs to integrate Trampoline nicely
path("sendtotrampoline") {
formFields(invoiceFormParam, "trampolineId".as[PublicKey], "trampolineFeesMsat".as[MilliSatoshi], "trampolineExpiryDelta".as[Int]) {
(invoice, trampolineId, trampolineFees, trampolineExpiryDelta) =>
complete(eclairApi.sendToTrampoline(invoice, trampolineId, trampolineFees, CltvExpiryDelta(trampolineExpiryDelta)))
}
} ~
path("sendtonode") {
formFields(amountMsatFormParam, paymentHashFormParam, nodeIdFormParam, "maxAttempts".as[Int].?, "feeThresholdSat".as[Satoshi].?, "maxFeePct".as[Double].?, "externalId".?) {
(amountMsat, paymentHash, nodeId, maxAttempts_opt, feeThresholdSat_opt, maxFeePct_opt, externalId_opt) =>

View File

@ -480,7 +480,7 @@ class ApiServiceSpec extends FunSuite with ScalatestRouteTest with IdiomaticMock
system.eventStream.publish(ps)
wsClient.expectMessage(expectedSerializedPs)
val prel = PaymentRelayed(amountIn = 21 msat, amountOut = 20 msat, paymentHash = ByteVector32.Zeroes, fromChannelId = ByteVector32.Zeroes, ByteVector32.One, timestamp = 1553784963659L)
val prel = ChannelPaymentRelayed(amountIn = 21 msat, amountOut = 20 msat, paymentHash = ByteVector32.Zeroes, fromChannelId = ByteVector32.Zeroes, ByteVector32.One, timestamp = 1553784963659L)
val expectedSerializedPrel = """{"type":"payment-relayed","amountIn":21,"amountOut":20,"paymentHash":"0000000000000000000000000000000000000000000000000000000000000000","fromChannelId":"0000000000000000000000000000000000000000000000000000000000000000","toChannelId":"0100000000000000000000000000000000000000000000000000000000000000","timestamp":1553784963659}"""
assert(serialization.write(prel) === expectedSerializedPrel)
system.eventStream.publish(prel)