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:
parent
2d95168749
commit
611f0cfebe
@ -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
|
||||
|
@ -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)
|
||||
|
@ -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))
|
||||
|
@ -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
|
||||
|
@ -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
|
||||
|
@ -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) {
|
||||
|
@ -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"),
|
||||
|
@ -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)
|
||||
}
|
||||
|
||||
}
|
||||
|
@ -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)
|
||||
|
@ -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")
|
||||
|
@ -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
|
||||
}
|
||||
|
||||
}
|
||||
|
@ -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 {
|
||||
|
@ -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
|
||||
}
|
||||
|
@ -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
|
||||
)
|
||||
}
|
||||
|
||||
}
|
@ -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)
|
||||
}
|
||||
}
|
||||
|
||||
}
|
@ -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]
|
||||
|
@ -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))
|
||||
}
|
||||
|
||||
|
@ -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)
|
||||
|
||||
}
|
||||
|
@ -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()
|
||||
|
||||
|
@ -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)
|
||||
|
||||
|
@ -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))
|
||||
}
|
||||
|
@ -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))
|
||||
|
@ -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 =
|
||||
|
@ -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")
|
||||
|
@ -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)
|
||||
}
|
||||
|
||||
|
@ -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 =>
|
||||
|
@ -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]
|
||||
|
@ -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"))
|
||||
|
@ -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))
|
||||
}
|
||||
|
||||
}
|
||||
|
@ -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)
|
||||
}
|
||||
|
||||
}
|
@ -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))
|
||||
}
|
||||
|
||||
}
|
||||
|
@ -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])
|
||||
|
@ -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)
|
||||
}
|
||||
|
||||
}
|
@ -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 =>
|
||||
|
@ -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)
|
||||
|
@ -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))
|
||||
}
|
||||
|
||||
|
@ -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)
|
||||
|
@ -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)
|
||||
}
|
||||
|
||||
}
|
@ -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)
|
||||
}
|
||||
|
||||
}
|
||||
|
@ -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 {
|
||||
|
||||
|
@ -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"
|
||||
|
@ -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) =>
|
||||
|
@ -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)
|
||||
|
Loading…
Reference in New Issue
Block a user