1
0
mirror of https://github.com/ACINQ/eclair.git synced 2024-11-19 01:43:22 +01:00

Improve Origin and Upstream (#2872)

We move the `Upstream` trait closer to the `Origin`, and make it more
obvious than a hot `Origin` is:

- an `Upstream` referencing the upstream HTLCs
- an actor requesting the outgoing payment

We also improve the cold trampoline relay class to record the incoming
HTLC amount, which we previously didn't bother encoding but is useful to
compute the fees collected during relay. To ensure backwards-compat, it
is set to `0 msat` for pending HTLCs. It will only affect HTLCs that
were pending during the upgrade, which is acceptable.
This commit is contained in:
Bastien Teinturier 2024-06-27 16:28:15 +02:00 committed by GitHub
parent c53b32c781
commit 791edf78b6
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
39 changed files with 478 additions and 400 deletions

View File

@ -56,7 +56,7 @@ trait CustomCommitmentsPlugin extends PluginParams {
* result upstream to preserve channels. If you have non-standard HTLCs that may be in this situation, they should be
* returned by this method.
*/
def getHtlcsRelayedOut(htlcsIn: Seq[IncomingHtlc], nodeParams: NodeParams, log: LoggingAdapter): Map[Origin, Set[(ByteVector32, Long)]]
def getHtlcsRelayedOut(htlcsIn: Seq[IncomingHtlc], nodeParams: NodeParams, log: LoggingAdapter): Map[Origin.Cold, Set[(ByteVector32, Long)]]
}
// @formatter:off

View File

@ -132,7 +132,7 @@ object CheckBalance {
// and succeed if they were sent from this node
val htlcOut = localCommit.spec.htlcs.collect(outgoing)
.filterNot(htlc => htlcsOutOnChain.contains(htlc.id)) // we filter the htlc that already pay us on-chain
.filterNot(htlc => originChannels.get(htlc.id).exists(_.isInstanceOf[Origin.Local]))
.filterNot(htlc => originChannels.get(htlc.id).exists(_.upstream.isInstanceOf[Upstream.Local]))
.filterNot(htlc => remoteHasPreimage(changes, htlc.id))
.sumAmount
// all claim txs have possibly been published

View File

@ -24,11 +24,10 @@ import fr.acinq.eclair.channel.LocalFundingStatus.DualFundedUnconfirmedFundingTx
import fr.acinq.eclair.channel.fund.InteractiveTxBuilder._
import fr.acinq.eclair.channel.fund.{InteractiveTxBuilder, InteractiveTxSigningSession}
import fr.acinq.eclair.io.Peer
import fr.acinq.eclair.payment.OutgoingPaymentPacket.Upstream
import fr.acinq.eclair.transactions.CommitmentSpec
import fr.acinq.eclair.transactions.Transactions._
import fr.acinq.eclair.wire.protocol.{ChannelAnnouncement, ChannelReady, ChannelReestablish, ChannelUpdate, ClosingSigned, CommitSig, FailureMessage, FundingCreated, FundingSigned, Init, OnionRoutingPacket, OpenChannel, OpenDualFundedChannel, Shutdown, SpliceInit, Stfu, TxSignatures, UpdateAddHtlc, UpdateFailHtlc, UpdateFailMalformedHtlc, UpdateFulfillHtlc}
import fr.acinq.eclair.{Alias, BlockHeight, CltvExpiry, CltvExpiryDelta, Features, InitFeature, MilliSatoshi, MilliSatoshiLong, RealShortChannelId, UInt64}
import fr.acinq.eclair.{Alias, BlockHeight, CltvExpiry, CltvExpiryDelta, Features, InitFeature, MilliSatoshi, MilliSatoshiLong, RealShortChannelId, TimestampMilli, UInt64}
import scodec.bits.ByteVector
import java.util.UUID
@ -134,51 +133,58 @@ case class INPUT_RESTORED(data: PersistentChannelData)
"Y8888P" "Y88888P" 888 888 888 888 d88P 888 888 Y888 8888888P" "Y8888P"
*/
/** Detailed upstream parent(s) of a payment in the HTLC chain. */
sealed trait Upstream { def amountIn: MilliSatoshi }
object Upstream {
/** We haven't restarted and have full information about the upstream parent(s). */
sealed trait Hot extends Upstream
object Hot {
/** Our node is forwarding a single incoming HTLC. */
case class Channel(add: UpdateAddHtlc, receivedAt: TimestampMilli) extends Hot {
override val amountIn: MilliSatoshi = add.amountMsat
val expiryIn: CltvExpiry = add.cltvExpiry
}
/** Our node is forwarding a payment based on a set of HTLCs from potentially multiple upstream channels. */
case class Trampoline(received: Seq[Channel]) extends Hot {
override val amountIn: MilliSatoshi = received.map(_.add.amountMsat).sum
// We must use the lowest expiry of the incoming HTLC set.
val expiryIn: CltvExpiry = received.map(_.add.cltvExpiry).min
val receivedAt: TimestampMilli = received.map(_.receivedAt).max
}
}
/** We have restarted and stored limited information about the upstream parent(s). */
sealed trait Cold extends Upstream
object Cold {
def apply(hot: Hot): Cold = hot match {
case Local(id) => Local(id)
case Hot.Channel(add, _) => Cold.Channel(add.channelId, add.id, add.amountMsat)
case Hot.Trampoline(received) => Cold.Trampoline(received.map(r => Cold.Channel(r.add.channelId, r.add.id, r.add.amountMsat)).toList)
}
/** Our node is forwarding a single incoming HTLC. */
case class Channel(originChannelId: ByteVector32, originHtlcId: Long, amountIn: MilliSatoshi) extends Cold
/** Our node is forwarding a payment based on a set of HTLCs from potentially multiple upstream channels. */
case class Trampoline(originHtlcs: List[Channel]) extends Cold { override val amountIn: MilliSatoshi = originHtlcs.map(_.amountIn).sum }
}
/** Our node is the origin of the payment: there are no matching upstream HTLCs. */
case class Local(id: UUID) extends Hot with Cold { override val amountIn: MilliSatoshi = 0 msat }
}
/**
* Origin of a payment, answering both questions:
* - what actor in the app sent that htlc? (Origin.replyTo)
* - what are the upstream parent(s) of this payment in the htlc chain?
* - what actor in the app sent that htlc and is waiting for its result?
* - what are the upstream parent(s) of this payment in the htlc chain?
*/
sealed trait Origin
sealed trait Origin { def upstream: Upstream }
object Origin {
/** We haven't restarted since we sent the payment downstream: the origin actor is known. */
sealed trait Hot extends Origin { def replyTo: ActorRef }
case class Hot(replyTo: ActorRef, upstream: Upstream.Hot) extends Origin
/** We have restarted after the payment was sent, we have limited info and the origin actor doesn't exist anymore. */
sealed trait Cold extends Origin
/** Our node is the origin of the payment. */
sealed trait Local extends Origin { def id: UUID }
case class LocalHot(replyTo: ActorRef, id: UUID) extends Local with Hot
case class LocalCold(id: UUID) extends Local with Cold
/** Our node forwarded a single incoming HTLC to an outgoing channel. */
sealed trait ChannelRelayed extends Origin {
def originChannelId: ByteVector32
def originHtlcId: Long
def amountIn: MilliSatoshi
def amountOut: MilliSatoshi
}
case class ChannelRelayedHot(replyTo: ActorRef, add: UpdateAddHtlc, override val amountOut: MilliSatoshi) extends ChannelRelayed with Hot {
override def originChannelId: ByteVector32 = add.channelId
override def originHtlcId: Long = add.id
override def amountIn: MilliSatoshi = add.amountMsat
}
case class ChannelRelayedCold(originChannelId: ByteVector32, originHtlcId: Long, amountIn: MilliSatoshi, amountOut: MilliSatoshi) extends ChannelRelayed with Cold
/** Our node forwarded an incoming HTLC set to a remote outgoing node (potentially producing multiple downstream HTLCs).*/
sealed trait TrampolineRelayed extends Origin { def htlcs: List[(ByteVector32, Long)] }
case class TrampolineRelayedHot(replyTo: ActorRef, adds: Seq[UpdateAddHtlc]) extends TrampolineRelayed with Hot {
override def htlcs: List[(ByteVector32, Long)] = adds.map(u => (u.channelId, u.id)).toList
val amountIn: MilliSatoshi = adds.map(_.amountMsat).sum
val expiryIn: CltvExpiry = adds.map(_.cltvExpiry).min
}
case class TrampolineRelayedCold(override val htlcs: List[(ByteVector32, Long)]) extends TrampolineRelayed with Cold
object Hot {
def apply(replyTo: ActorRef, upstream: Upstream): Hot = upstream match {
case u: Upstream.Local => Origin.LocalHot(replyTo, u.id)
case u: Upstream.Trampoline => Origin.TrampolineRelayedHot(replyTo, u.adds.map(_.add))
}
case class Cold(upstream: Upstream.Cold) extends Origin
object Cold {
def apply(hot: Hot): Cold = Cold(Upstream.Cold(hot.upstream))
}
}

View File

@ -1781,7 +1781,7 @@ class Channel(val nodeParams: NodeParams, val wallet: OnChainChannelFunder with
// for our outgoing payments, let's send events if we know that they will settle on chain
Closing
.onChainOutgoingHtlcs(d.commitments.latest.localCommit, d.commitments.latest.remoteCommit, d.commitments.latest.nextRemoteCommit_opt.map(_.commit), tx)
.map(add => (add, d.commitments.originChannels.get(add.id).collect { case o: Origin.Local => o.id })) // we resolve the payment id if this was a local payment
.map(add => (add, d.commitments.originChannels.get(add.id).map(_.upstream).collect { case Upstream.Local(id) => id })) // we resolve the payment id if this was a local payment
.collect { case (add, Some(id)) => context.system.eventStream.publish(PaymentSettlingOnChain(id, amount = add.amountMsat, add.paymentHash)) }
// then let's see if any of the possible close scenarios can be considered done
val closingType_opt = Closing.isClosed(d1, Some(tx))

View File

@ -514,17 +514,37 @@ object ChannelEventSerializer extends MinimalSerializer({
})
object OriginSerializer extends MinimalSerializer({
case o: Origin.Local => JObject(JField("paymentId", JString(o.id.toString)))
case o: Origin.ChannelRelayed => JObject(
JField("channelId", JString(o.originChannelId.toHex)),
JField("htlcId", JLong(o.originHtlcId)),
)
case o: Origin.TrampolineRelayed => JArray(o.htlcs.map {
case (channelId, htlcId) => JObject(
JField("channelId", JString(channelId.toHex)),
JField("htlcId", JLong(htlcId)),
case o: Origin => o.upstream match {
case u: Upstream.Local => JObject(JField("paymentId", JString(u.id.toString)))
case u: Upstream.Hot.Channel => JObject(
JField("channelId", JString(u.add.channelId.toHex)),
JField("htlcId", JLong(u.add.id)),
JField("amount", JLong(u.add.amountMsat.toLong)),
JField("expiry", JLong(u.add.cltvExpiry.toLong)),
JField("receivedAt", JLong(u.receivedAt.toLong)),
)
})
case u: Upstream.Hot.Trampoline => JArray(u.received.map { htlc =>
JObject(
JField("channelId", JString(htlc.add.channelId.toHex)),
JField("htlcId", JLong(htlc.add.id)),
JField("amount", JLong(htlc.add.amountMsat.toLong)),
JField("expiry", JLong(htlc.add.cltvExpiry.toLong)),
JField("receivedAt", JLong(htlc.receivedAt.toLong)),
)
}.toList)
case o: Upstream.Cold.Channel => JObject(
JField("channelId", JString(o.originChannelId.toHex)),
JField("htlcId", JLong(o.originHtlcId)),
JField("amount", JLong(o.amountIn.toLong)),
)
case o: Upstream.Cold.Trampoline => JArray(o.originHtlcs.map { htlc =>
JObject(
JField("channelId", JString(htlc.originChannelId.toHex)),
JField("htlcId", JLong(htlc.originHtlcId)),
JField("amount", JLong(htlc.amountIn.toLong)),
)
}.toList)
}
})
// @formatter:off

View File

@ -16,7 +16,6 @@
package fr.acinq.eclair.payment
import akka.actor.ActorRef
import fr.acinq.bitcoin.scalacompat.ByteVector32
import fr.acinq.bitcoin.scalacompat.Crypto.{PrivateKey, PublicKey}
import fr.acinq.eclair.channel.{CMD_ADD_HTLC, CMD_FAIL_HTLC, CannotExtractSharedSecret, Origin}
@ -26,11 +25,10 @@ import fr.acinq.eclair.router.Router.Route
import fr.acinq.eclair.wire.protocol.OnionPaymentPayloadTlv.OutgoingBlindedPaths
import fr.acinq.eclair.wire.protocol.PaymentOnion.{FinalPayload, IntermediatePayload, PerHopPayload}
import fr.acinq.eclair.wire.protocol._
import fr.acinq.eclair.{CltvExpiry, CltvExpiryDelta, Feature, Features, MilliSatoshi, ShortChannelId, TimestampMilli, UInt64, randomKey}
import fr.acinq.eclair.{CltvExpiry, CltvExpiryDelta, Feature, Features, MilliSatoshi, ShortChannelId, UInt64, randomKey}
import scodec.bits.ByteVector
import scodec.{Attempt, DecodeResult}
import java.util.UUID
import scala.util.{Failure, Success}
/**
@ -263,17 +261,6 @@ object OutgoingPaymentPacket {
case class MissingTrampolineHop(trampolineNodeId: PublicKey) extends OutgoingPaymentError { override def getMessage: String = s"expected route to trampoline node $trampolineNodeId" }
case class MissingBlindedHop(introductionNodeIds: Set[PublicKey]) extends OutgoingPaymentError { override def getMessage: String = s"expected blinded route using one of the following introduction nodes: ${introductionNodeIds.mkString(", ")}" }
case object EmptyRoute extends OutgoingPaymentError { override def getMessage: String = "route cannot be empty" }
sealed trait Upstream
object Upstream {
case class Local(id: UUID) extends Upstream
case class Trampoline(adds: Seq[ReceivedHtlc]) extends Upstream {
val amountIn: MilliSatoshi = adds.map(_.add.amountMsat).sum
val expiryIn: CltvExpiry = adds.map(_.add.cltvExpiry).min
}
case class ReceivedHtlc(add: UpdateAddHtlc, receivedAt: TimestampMilli)
}
// @formatter:on
/**
@ -298,12 +285,12 @@ object OutgoingPaymentPacket {
}
/** Build the command to add an HTLC for the given recipient using the provided route. */
def buildOutgoingPayment(replyTo: ActorRef, upstream: Upstream, paymentHash: ByteVector32, route: Route, recipient: Recipient): Either[OutgoingPaymentError, OutgoingPaymentPacket] = {
def buildOutgoingPayment(origin: Origin.Hot, paymentHash: ByteVector32, route: Route, recipient: Recipient): Either[OutgoingPaymentError, OutgoingPaymentPacket] = {
for {
payment <- recipient.buildPayloads(paymentHash, route)
onion <- buildOnion(payment.payloads, paymentHash, Some(PaymentOnionCodecs.paymentOnionPayloadLength)) // BOLT 2 requires that associatedData == paymentHash
} yield {
val cmd = CMD_ADD_HTLC(replyTo, payment.amount, paymentHash, payment.expiry, onion.packet, payment.outerBlinding_opt, Origin.Hot(replyTo, upstream), commit = true)
val cmd = CMD_ADD_HTLC(origin.replyTo, payment.amount, paymentHash, payment.expiry, onion.packet, payment.outerBlinding_opt, origin, commit = true)
OutgoingPaymentPacket(cmd, route.hops.head.shortChannelId, onion.sharedSecrets)
}
}

View File

@ -112,6 +112,8 @@ class ChannelRelay private(nodeParams: NodeParams,
private val forwardFailureAdapter = context.messageAdapter[Register.ForwardFailure[CMD_ADD_HTLC]](WrappedForwardFailure)
private val addResponseAdapter = context.messageAdapter[CommandResponse[CMD_ADD_HTLC]](WrappedAddResponse)
private val upstream = Upstream.Hot.Channel(r.add.removeUnknownTlvs(), startedAt)
private case class PreviouslyTried(channelId: ByteVector32, failure: RES_ADD_FAILED[ChannelException])
def relay(previousFailures: Seq[PreviouslyTried]): Behavior[Command] = {
@ -136,13 +138,13 @@ class ChannelRelay private(nodeParams: NodeParams,
def waitForAddResponse(selectedChannelId: ByteVector32, previousFailures: Seq[PreviouslyTried]): Behavior[Command] =
Behaviors.receiveMessagePartial {
case WrappedForwardFailure(Register.ForwardFailure(Register.Forward(_, channelId, CMD_ADD_HTLC(_, _, _, _, _, _, o: Origin.ChannelRelayedHot, _)))) =>
context.log.warn(s"couldn't resolve downstream channel $channelId, failing htlc #${o.add.id}")
val cmdFail = CMD_FAIL_HTLC(o.add.id, Right(UnknownNextPeer()), commit = true)
case WrappedForwardFailure(Register.ForwardFailure(Register.Forward(_, channelId, _))) =>
context.log.warn(s"couldn't resolve downstream channel $channelId, failing htlc #${upstream.add.id}")
val cmdFail = CMD_FAIL_HTLC(upstream.add.id, Right(UnknownNextPeer()), commit = true)
Metrics.recordPaymentRelayFailed(Tags.FailureType(cmdFail), Tags.RelayType.Channel)
safeSendAndStop(o.add.channelId, cmdFail)
safeSendAndStop(upstream.add.channelId, cmdFail)
case WrappedAddResponse(addFailed@RES_ADD_FAILED(CMD_ADD_HTLC(_, _, _, _, _, _, _: Origin.ChannelRelayedHot, _), _, _)) =>
case WrappedAddResponse(addFailed: RES_ADD_FAILED[_]) =>
context.log.info("attempt failed with reason={}", addFailed.t.getClass.getSimpleName)
context.self ! DoRelay
relay(previousFailures :+ PreviouslyTried(selectedChannelId, addFailed))
@ -154,19 +156,19 @@ class ChannelRelay private(nodeParams: NodeParams,
def waitForAddSettled(): Behavior[Command] =
Behaviors.receiveMessagePartial {
case WrappedAddResponse(RES_ADD_SETTLED(o: Origin.ChannelRelayedHot, htlc, fulfill: HtlcResult.Fulfill)) =>
case WrappedAddResponse(RES_ADD_SETTLED(_, htlc, fulfill: HtlcResult.Fulfill)) =>
context.log.debug("relaying fulfill to upstream")
val cmd = CMD_FULFILL_HTLC(o.originHtlcId, fulfill.paymentPreimage, commit = true)
context.system.eventStream ! EventStream.Publish(ChannelPaymentRelayed(o.amountIn, o.amountOut, htlc.paymentHash, o.originChannelId, htlc.channelId, startedAt, TimestampMilli.now()))
val cmd = CMD_FULFILL_HTLC(upstream.add.id, fulfill.paymentPreimage, commit = true)
context.system.eventStream ! EventStream.Publish(ChannelPaymentRelayed(upstream.amountIn, htlc.amountMsat, htlc.paymentHash, upstream.add.channelId, htlc.channelId, startedAt, TimestampMilli.now()))
recordRelayDuration(isSuccess = true)
safeSendAndStop(o.originChannelId, cmd)
safeSendAndStop(upstream.add.channelId, cmd)
case WrappedAddResponse(RES_ADD_SETTLED(o: Origin.ChannelRelayedHot, _, fail: HtlcResult.Fail)) =>
case WrappedAddResponse(RES_ADD_SETTLED(_, _, fail: HtlcResult.Fail)) =>
context.log.debug("relaying fail to upstream")
Metrics.recordPaymentRelayFailed(Tags.FailureType.Remote, Tags.RelayType.Channel)
val cmd = translateRelayFailure(o.originHtlcId, fail)
val cmd = translateRelayFailure(upstream.add.id, fail)
recordRelayDuration(isSuccess = false)
safeSendAndStop(o.originChannelId, cmd)
safeSendAndStop(upstream.add.channelId, cmd)
}
def safeSendAndStop(channelId: ByteVector32, cmd: channel.HtlcSettlementCommand): Behavior[Command] = {
@ -305,7 +307,7 @@ class ChannelRelay private(nodeParams: NodeParams,
outgoingChannel_opt.flatMap(_.prevChannelUpdate).forall(c => r.relayFeeMsat < nodeFee(c.relayFees, r.amountToForward))) =>
RelayFailure(CMD_FAIL_HTLC(r.add.id, Right(FeeInsufficient(r.add.amountMsat, Some(c.channelUpdate))), commit = true))
case Some(c: OutgoingChannel) =>
val origin = Origin.ChannelRelayedHot(addResponseAdapter.toClassic, r.add, r.amountToForward)
val origin = Origin.Hot(addResponseAdapter.toClassic, upstream)
val nextBlindingKey_opt = r.payload match {
case payload: IntermediatePayload.ChannelRelay.Blinded => Some(payload.nextBlinding)
case _: IntermediatePayload.ChannelRelay.Standard => None

View File

@ -23,11 +23,10 @@ import akka.actor.typed.scaladsl.{ActorContext, Behaviors}
import akka.actor.{ActorRef, typed}
import com.softwaremill.quicklens.ModifyPimp
import fr.acinq.bitcoin.scalacompat.ByteVector32
import fr.acinq.eclair.channel.{CMD_FAIL_HTLC, CMD_FULFILL_HTLC}
import fr.acinq.eclair.channel.{CMD_FAIL_HTLC, CMD_FULFILL_HTLC, Upstream}
import fr.acinq.eclair.db.PendingCommandsDb
import fr.acinq.eclair.payment.IncomingPaymentPacket.NodeRelayPacket
import fr.acinq.eclair.payment.Monitoring.{Metrics, Tags}
import fr.acinq.eclair.payment.OutgoingPaymentPacket.Upstream
import fr.acinq.eclair.payment._
import fr.acinq.eclair.payment.receive.MultiPartPaymentFSM
import fr.acinq.eclair.payment.receive.MultiPartPaymentFSM.HtlcPart
@ -113,7 +112,7 @@ object NodeRelay {
}
}
private def validateRelay(nodeParams: NodeParams, upstream: Upstream.Trampoline, payloadOut: IntermediatePayload.NodeRelay): Option[FailureMessage] = {
private def validateRelay(nodeParams: NodeParams, upstream: Upstream.Hot.Trampoline, payloadOut: IntermediatePayload.NodeRelay): Option[FailureMessage] = {
val fee = nodeFee(nodeParams.relayParams.minTrampolineFees, payloadOut.amountToForward)
if (upstream.amountIn - payloadOut.amountToForward < fee) {
Some(TrampolineFeeInsufficient())
@ -150,7 +149,7 @@ object NodeRelay {
* This helper method translates relaying errors (returned by the downstream nodes) to a BOLT 4 standard error that we
* should return upstream.
*/
private def translateError(nodeParams: NodeParams, failures: Seq[PaymentFailure], upstream: Upstream.Trampoline, nextPayload: IntermediatePayload.NodeRelay): Option[FailureMessage] = {
private def translateError(nodeParams: NodeParams, failures: Seq[PaymentFailure], upstream: Upstream.Hot.Trampoline, nextPayload: IntermediatePayload.NodeRelay): Option[FailureMessage] = {
val routeNotFound = failures.collectFirst { case f@LocalFailure(_, _, RouteNotFound) => f }.nonEmpty
val routingFeeHigh = upstream.amountIn - nextPayload.amountToForward >= nodeFee(nodeParams.relayParams.minTrampolineFees, nextPayload.amountToForward) * 5
failures match {
@ -202,13 +201,13 @@ class NodeRelay private(nodeParams: NodeParams,
* @param nextPacket_opt trampoline onion to relay to the next trampoline node.
* @param handler actor handling the aggregation of the incoming HTLC set.
*/
private def receiving(htlcs: Queue[Upstream.ReceivedHtlc], nextPayload: IntermediatePayload.NodeRelay, nextPacket_opt: Option[OnionRoutingPacket], handler: ActorRef): Behavior[Command] =
private def receiving(htlcs: Queue[Upstream.Hot.Channel], nextPayload: IntermediatePayload.NodeRelay, nextPacket_opt: Option[OnionRoutingPacket], handler: ActorRef): Behavior[Command] =
Behaviors.receiveMessagePartial {
case Relay(packet: IncomingPaymentPacket.NodeRelayPacket) =>
require(packet.outerPayload.paymentSecret == paymentSecret, "payment secret mismatch")
context.log.debug("forwarding incoming htlc #{} from channel {} to the payment FSM", packet.add.id, packet.add.channelId)
handler ! MultiPartPaymentFSM.HtlcPart(packet.outerPayload.totalAmount, packet.add)
receiving(htlcs :+ Upstream.ReceivedHtlc(packet.add, TimestampMilli.now()), nextPayload, nextPacket_opt, handler)
receiving(htlcs :+ Upstream.Hot.Channel(packet.add.removeUnknownTlvs(), TimestampMilli.now()), nextPayload, nextPacket_opt, handler)
case WrappedMultiPartPaymentFailed(MultiPartPaymentFSM.MultiPartPaymentFailed(_, failure, parts)) =>
context.log.warn("could not complete incoming multi-part payment (parts={} paidAmount={} failure={})", parts.size, parts.map(_.amount).sum, failure)
Metrics.recordPaymentRelayFailed(failure.getClass.getSimpleName, Tags.RelayType.Trampoline)
@ -216,7 +215,7 @@ class NodeRelay private(nodeParams: NodeParams,
stopping()
case WrappedMultiPartPaymentSucceeded(MultiPartPaymentFSM.MultiPartPaymentSucceeded(_, parts)) =>
context.log.info("completed incoming multi-part payment with parts={} paidAmount={}", parts.size, parts.map(_.amount).sum)
val upstream = Upstream.Trampoline(htlcs)
val upstream = Upstream.Hot.Trampoline(htlcs)
validateRelay(nodeParams, upstream, nextPayload) match {
case Some(failure) =>
context.log.warn(s"rejecting trampoline payment reason=$failure")
@ -233,7 +232,7 @@ class NodeRelay private(nodeParams: NodeParams,
}
}
private def waitForTrigger(upstream: Upstream.Trampoline, nextPayload: IntermediatePayload.NodeRelay.Standard, nextPacket_opt: Option[OnionRoutingPacket]): Behavior[Command] = {
private def waitForTrigger(upstream: Upstream.Hot.Trampoline, nextPayload: IntermediatePayload.NodeRelay.Standard, nextPacket_opt: Option[OnionRoutingPacket]): Behavior[Command] = {
context.log.info(s"waiting for async payment to trigger before relaying trampoline payment (amountIn=${upstream.amountIn} expiryIn=${upstream.expiryIn} amountOut=${nextPayload.amountToForward} expiryOut=${nextPayload.outgoingCltv}, asyncPaymentsParams=${nodeParams.relayParams.asyncPaymentsParams})")
val timeoutBlock = nodeParams.currentBlockHeight + nodeParams.relayParams.asyncPaymentsParams.holdTimeoutBlocks
val safetyBlock = (upstream.expiryIn - nodeParams.relayParams.asyncPaymentsParams.cancelSafetyBeforeTimeout).blockHeight
@ -257,7 +256,7 @@ class NodeRelay private(nodeParams: NodeParams,
}
}
private def doSend(upstream: Upstream.Trampoline, nextPayload: IntermediatePayload.NodeRelay, nextPacket_opt: Option[OnionRoutingPacket]): Behavior[Command] = {
private def doSend(upstream: Upstream.Hot.Trampoline, nextPayload: IntermediatePayload.NodeRelay, nextPacket_opt: Option[OnionRoutingPacket]): Behavior[Command] = {
context.log.debug(s"relaying trampoline payment (amountIn=${upstream.amountIn} expiryIn=${upstream.expiryIn} amountOut=${nextPayload.amountToForward} expiryOut=${nextPayload.outgoingCltv})")
relay(upstream, nextPayload, nextPacket_opt)
}
@ -269,7 +268,7 @@ class NodeRelay private(nodeParams: NodeParams,
* @param nextPayload relay instructions.
* @param fulfilledUpstream true if we already fulfilled the payment upstream.
*/
private def sending(upstream: Upstream.Trampoline, nextPayload: IntermediatePayload.NodeRelay, startedAt: TimestampMilli, fulfilledUpstream: Boolean): Behavior[Command] =
private def sending(upstream: Upstream.Hot.Trampoline, nextPayload: IntermediatePayload.NodeRelay, startedAt: TimestampMilli, fulfilledUpstream: Boolean): Behavior[Command] =
Behaviors.receiveMessagePartial {
rejectExtraHtlcPartialFunction orElse {
// this is the fulfill that arrives from downstream channels
@ -316,7 +315,7 @@ class NodeRelay private(nodeParams: NodeParams,
context.messageAdapter[PaymentFailed](WrappedPaymentFailed)
}.toClassic
private def relay(upstream: Upstream.Trampoline, payloadOut: IntermediatePayload.NodeRelay, packetOut_opt: Option[OnionRoutingPacket]): Behavior[Command] = {
private def relay(upstream: Upstream.Hot.Trampoline, payloadOut: IntermediatePayload.NodeRelay, packetOut_opt: Option[OnionRoutingPacket]): Behavior[Command] = {
val displayNodeId = payloadOut match {
case payloadOut: IntermediatePayload.NodeRelay.Standard => payloadOut.outgoingNodeId
case _: IntermediatePayload.NodeRelay.ToBlindedPaths => randomKey().publicKey
@ -345,7 +344,7 @@ class NodeRelay private(nodeParams: NodeParams,
}
}
private def relayToRecipient(upstream: Upstream.Trampoline,
private def relayToRecipient(upstream: Upstream.Hot.Trampoline,
payloadOut: IntermediatePayload.NodeRelay,
recipient: Recipient,
paymentCfg: SendPaymentConfig,
@ -366,7 +365,7 @@ class NodeRelay private(nodeParams: NodeParams,
* Blinded paths in Bolt 12 invoices may encode the introduction node with an scid and a direction: we need to resolve
* that to a nodeId in order to reach that introduction node and use the blinded path.
*/
private def waitForResolvedPaths(upstream: Upstream.Trampoline,
private def waitForResolvedPaths(upstream: Upstream.Hot.Trampoline,
payloadOut: IntermediatePayload.NodeRelay.ToBlindedPaths,
paymentCfg: SendPaymentConfig,
routeParams: RouteParams): Behavior[Command] =
@ -408,22 +407,22 @@ class NodeRelay private(nodeParams: NodeParams,
PendingCommandsDb.safeSend(register, nodeParams.db.pendingCommands, channelId, cmd)
}
private def rejectPayment(upstream: Upstream.Trampoline, failure: Option[FailureMessage]): Unit = {
private def rejectPayment(upstream: Upstream.Hot.Trampoline, failure: Option[FailureMessage]): Unit = {
Metrics.recordPaymentRelayFailed(failure.map(_.getClass.getSimpleName).getOrElse("Unknown"), Tags.RelayType.Trampoline)
upstream.adds.foreach(r => rejectHtlc(r.add.id, r.add.channelId, upstream.amountIn, failure))
upstream.received.foreach(r => rejectHtlc(r.add.id, r.add.channelId, upstream.amountIn, failure))
}
private def fulfillPayment(upstream: Upstream.Trampoline, paymentPreimage: ByteVector32): Unit = upstream.adds.foreach(r => {
private def fulfillPayment(upstream: Upstream.Hot.Trampoline, paymentPreimage: ByteVector32): Unit = upstream.received.foreach(r => {
val cmd = CMD_FULFILL_HTLC(r.add.id, paymentPreimage, commit = true)
PendingCommandsDb.safeSend(register, nodeParams.db.pendingCommands, r.add.channelId, cmd)
})
private def success(upstream: Upstream.Trampoline, fulfilledUpstream: Boolean, paymentSent: PaymentSent): Unit = {
private def success(upstream: Upstream.Hot.Trampoline, fulfilledUpstream: Boolean, paymentSent: PaymentSent): Unit = {
// We may have already fulfilled upstream, but we can now emit an accurate relayed event and clean-up resources.
if (!fulfilledUpstream) {
fulfillPayment(upstream, paymentSent.paymentPreimage)
}
val incoming = upstream.adds.map(r => PaymentRelayed.IncomingPart(r.add.amountMsat, r.add.channelId, r.receivedAt))
val incoming = upstream.received.map(r => PaymentRelayed.IncomingPart(r.add.amountMsat, r.add.channelId, r.receivedAt))
val outgoing = paymentSent.parts.map(part => PaymentRelayed.OutgoingPart(part.amountWithFees, part.toChannelId, part.timestamp))
context.system.eventStream ! EventStream.Publish(TrampolinePaymentRelayed(paymentHash, incoming, outgoing, paymentSent.recipientNodeId, paymentSent.recipientAmount))
}

View File

@ -70,8 +70,8 @@ class PostRestartHtlcCleaner(nodeParams: NodeParams, register: ActorRef, initial
val channels = listLocalChannels(init.channels)
val nonStandardIncomingHtlcs: Seq[IncomingHtlc] = nodeParams.pluginParams.collect { case p: CustomCommitmentsPlugin => p.getIncomingHtlcs(nodeParams, log) }.flatten
val htlcsIn: Seq[IncomingHtlc] = getIncomingHtlcs(channels, nodeParams.db.payments, nodeParams.privateKey, nodeParams.features) ++ nonStandardIncomingHtlcs
val nonStandardRelayedOutHtlcs: Map[Origin, Set[(ByteVector32, Long)]] = nodeParams.pluginParams.collect { case p: CustomCommitmentsPlugin => p.getHtlcsRelayedOut(htlcsIn, nodeParams, log) }.flatten.toMap
val relayedOut: Map[Origin, Set[(ByteVector32, Long)]] = getHtlcsRelayedOut(channels, htlcsIn) ++ nonStandardRelayedOutHtlcs
val nonStandardRelayedOutHtlcs: Map[Origin.Cold, Set[(ByteVector32, Long)]] = nodeParams.pluginParams.collect { case p: CustomCommitmentsPlugin => p.getHtlcsRelayedOut(htlcsIn, nodeParams, log) }.flatten.toMap
val relayedOut: Map[Origin.Cold, Set[(ByteVector32, Long)]] = getHtlcsRelayedOut(channels, htlcsIn) ++ nonStandardRelayedOutHtlcs
val settledHtlcs: Set[(ByteVector32, Long)] = nodeParams.db.pendingCommands.listSettlementCommands().map { case (channelId, cmd) => (channelId, cmd.id) }.toSet
val notRelayed = htlcsIn.filterNot(htlcIn => {
@ -162,8 +162,8 @@ class PostRestartHtlcCleaner(nodeParams: NodeParams, register: ActorRef, initial
private def handleDownstreamFulfill(brokenHtlcs: BrokenHtlcs, origin: Origin.Cold, fulfilledHtlc: UpdateAddHtlc, paymentPreimage: ByteVector32): Unit =
brokenHtlcs.relayedOut.get(origin) match {
case Some(relayedOut) => origin match {
case Origin.LocalCold(id) =>
case Some(relayedOut) => origin.upstream match {
case Upstream.Local(id) =>
val feesPaid = 0.msat // fees are unknown since we lost the reference to the payment
nodeParams.db.payments.getOutgoingPayment(id) match {
case Some(p) =>
@ -192,23 +192,21 @@ class PostRestartHtlcCleaner(nodeParams: NodeParams, register: ActorRef, initial
// instead spread across multiple local origins) so we can now forget this origin.
Metrics.PendingRelayedOut.decrement()
context become main(brokenHtlcs.copy(relayedOut = brokenHtlcs.relayedOut - origin))
case Origin.ChannelRelayedCold(originChannelId, originHtlcId, amountIn, amountOut) =>
case Upstream.Cold.Channel(originChannelId, originHtlcId, amountIn) =>
log.info(s"received preimage for paymentHash=${fulfilledHtlc.paymentHash}: fulfilling 1 HTLC upstream")
if (relayedOut != Set((fulfilledHtlc.channelId, fulfilledHtlc.id))) {
log.error(s"unexpected channel relay downstream HTLCs: expected (${fulfilledHtlc.channelId},${fulfilledHtlc.id}), found $relayedOut")
}
PendingCommandsDb.safeSend(register, nodeParams.db.pendingCommands, originChannelId, CMD_FULFILL_HTLC(originHtlcId, paymentPreimage, commit = true))
// We don't know when we received this HTLC so we just pretend that we received it just now.
context.system.eventStream.publish(ChannelPaymentRelayed(amountIn, amountOut, fulfilledHtlc.paymentHash, originChannelId, fulfilledHtlc.channelId, TimestampMilli.now(), TimestampMilli.now()))
context.system.eventStream.publish(ChannelPaymentRelayed(amountIn, fulfilledHtlc.amountMsat, fulfilledHtlc.paymentHash, originChannelId, fulfilledHtlc.channelId, TimestampMilli.now(), TimestampMilli.now()))
Metrics.PendingRelayedOut.decrement()
context become main(brokenHtlcs.copy(relayedOut = brokenHtlcs.relayedOut - origin))
case Origin.TrampolineRelayedCold(origins) =>
case Upstream.Cold.Trampoline(originHtlcs) =>
// 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) =>
log.info(s"received preimage for paymentHash=${fulfilledHtlc.paymentHash}: fulfilling ${originHtlcs.length} HTLCs upstream")
originHtlcs.foreach { case Upstream.Cold.Channel(channelId, htlcId, _) =>
Metrics.Resolved.withTag(Tags.Success, value = true).withTag(Metrics.Relayed, value = true).increment()
PendingCommandsDb.safeSend(register, nodeParams.db.pendingCommands, channelId, CMD_FULFILL_HTLC(htlcId, paymentPreimage, commit = true))
}
@ -233,8 +231,8 @@ class PostRestartHtlcCleaner(nodeParams: NodeParams, register: ActorRef, initial
brokenHtlcs.relayedOut.get(origin) match {
case Some(relayedOut) =>
// If this is a local payment, we need to update the DB:
origin match {
case Origin.LocalCold(id) => nodeParams.db.payments.updateOutgoingPayment(PaymentFailed(id, failedHtlc.paymentHash, Nil))
origin.upstream match {
case Upstream.Local(id) => nodeParams.db.payments.updateOutgoingPayment(PaymentFailed(id, failedHtlc.paymentHash, Nil))
case _ =>
}
val relayedOut1 = relayedOut diff Set((failedHtlc.channelId, failedHtlc.id))
@ -242,15 +240,15 @@ class PostRestartHtlcCleaner(nodeParams: NodeParams, register: ActorRef, initial
if (relayedOut1.isEmpty) {
// If we haven't already settled upstream, we can fail now.
if (!brokenHtlcs.settledUpstream.contains(origin)) {
origin match {
case Origin.LocalCold(id) => nodeParams.db.payments.getOutgoingPayment(id).foreach(p => {
origin.upstream match {
case Upstream.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.ChannelRelayedCold(originChannelId, originHtlcId, _, _) =>
case Upstream.Cold.Channel(originChannelId, originHtlcId, _) =>
log.warning(s"payment failed for paymentHash=${failedHtlc.paymentHash}: failing 1 HTLC upstream")
Metrics.Resolved.withTag(Tags.Success, value = false).withTag(Metrics.Relayed, value = true).increment()
val cmd = failedHtlc.blinding_opt match {
@ -264,9 +262,9 @@ class PostRestartHtlcCleaner(nodeParams: NodeParams, register: ActorRef, initial
ChannelRelay.translateRelayFailure(originHtlcId, fail)
}
PendingCommandsDb.safeSend(register, nodeParams.db.pendingCommands, originChannelId, cmd)
case Origin.TrampolineRelayedCold(origins) =>
log.warning(s"payment failed for paymentHash=${failedHtlc.paymentHash}: failing ${origins.length} HTLCs upstream")
origins.foreach { case (channelId, htlcId) =>
case Upstream.Cold.Trampoline(originHtlcs) =>
log.warning(s"payment failed for paymentHash=${failedHtlc.paymentHash}: failing ${originHtlcs.length} HTLCs upstream")
originHtlcs.foreach { case Upstream.Cold.Channel(channelId, htlcId, _) =>
Metrics.Resolved.withTag(Tags.Success, value = false).withTag(Metrics.Relayed, value = true).increment()
// 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.
@ -332,15 +330,13 @@ object PostRestartHtlcCleaner {
* @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, Set[(ByteVector32, Long)]], settledUpstream: Set[Origin])
case class BrokenHtlcs(notRelayed: Seq[IncomingHtlc], relayedOut: Map[Origin.Cold, Set[(ByteVector32, Long)]], settledUpstream: Set[Origin.Cold])
/** 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 o: Origin.ChannelRelayed => o.originChannelId == htlcIn.channelId && o.originHtlcId == htlcIn.id
case o: Origin.TrampolineRelayed => o.htlcs.exists {
case (originChannelId, originHtlcId) => originChannelId == htlcIn.channelId && originHtlcId == htlcIn.id
}
private def matchesOrigin(htlcIn: UpdateAddHtlc, origin: Origin.Cold): Boolean = origin.upstream match {
case _: Upstream.Local => false
case o: Upstream.Cold.Channel => o.originChannelId == htlcIn.channelId && o.originHtlcId == htlcIn.id
case o: Upstream.Cold.Trampoline => o.originHtlcs.exists(h => h.originChannelId == htlcIn.channelId && h.originHtlcId == htlcIn.id)
}
/**
@ -379,7 +375,7 @@ object PostRestartHtlcCleaner {
private def isPendingUpstream(channelId: ByteVector32, htlcId: Long, htlcsIn: Seq[IncomingHtlc]): Boolean =
htlcsIn.exists(htlc => htlc.add.channelId == channelId && htlc.add.id == htlcId)
private def groupByOrigin(htlcsOut: Seq[(Origin, ByteVector32, Long)], htlcsIn: Seq[IncomingHtlc]): Map[Origin, Set[(ByteVector32, Long)]] =
private def groupByOrigin(htlcsOut: Seq[(Origin.Cold, ByteVector32, Long)], htlcsIn: Seq[IncomingHtlc]): Map[Origin.Cold, Set[(ByteVector32, Long)]] =
htlcsOut
.groupBy { case (origin, _, _) => origin }
.view
@ -389,14 +385,14 @@ object PostRestartHtlcCleaner {
// channel is closing (e.g. due to an HTLC timeout) because cooperatively failing the HTLC downstream will be
// instant whereas the uncooperative close of the downstream channel will take time.
.filterKeys {
case _: Origin.Local => true
case o: Origin.ChannelRelayed => isPendingUpstream(o.originChannelId, o.originHtlcId, htlcsIn)
case o: Origin.TrampolineRelayed => o.htlcs.exists { case (channelId, htlcId) => isPendingUpstream(channelId, htlcId, htlcsIn) }
case Origin.Cold(_: Upstream.Local) => true
case Origin.Cold(o: Upstream.Cold.Channel) => isPendingUpstream(o.originChannelId, o.originHtlcId, htlcsIn)
case Origin.Cold(o: Upstream.Cold.Trampoline) => o.originHtlcs.exists(h => isPendingUpstream(h.originChannelId, h.originHtlcId, htlcsIn))
}
.toMap
/** @return pending outgoing HTLCs, grouped by their upstream origin. */
private def getHtlcsRelayedOut(channels: Seq[PersistentChannelData], htlcsIn: Seq[IncomingHtlc])(implicit log: LoggingAdapter): Map[Origin, Set[(ByteVector32, Long)]] = {
private def getHtlcsRelayedOut(channels: Seq[PersistentChannelData], htlcsIn: Seq[IncomingHtlc])(implicit log: LoggingAdapter): Map[Origin.Cold, Set[(ByteVector32, Long)]] = {
val htlcsOut = channels
.collect { case c: ChannelDataWithCommitments => c }
.flatMap { c =>
@ -428,7 +424,9 @@ object PostRestartHtlcCleaner {
overriddenHtlcs ++ timedOutHtlcs
case _ => Set.empty
}
c.commitments.originChannels.collect { case (outgoingHtlcId, origin) if !htlcsToIgnore.contains(outgoingHtlcId) => (origin, c.channelId, outgoingHtlcId) }
c.commitments.originChannels.collect {
case (outgoingHtlcId, origin: Origin.Cold) if !htlcsToIgnore.contains(outgoingHtlcId) => (origin, c.channelId, outgoingHtlcId)
}
}
groupByOrigin(htlcsOut, htlcsIn)
}

View File

@ -19,10 +19,9 @@ package fr.acinq.eclair.payment.send
import akka.actor.{ActorRef, FSM, Props, Status}
import akka.event.Logging.MDC
import fr.acinq.bitcoin.scalacompat.ByteVector32
import fr.acinq.eclair.channel.{HtlcOverriddenByLocalCommit, HtlcsTimedoutDownstream, HtlcsWillTimeoutUpstream}
import fr.acinq.eclair.channel.{HtlcOverriddenByLocalCommit, HtlcsTimedoutDownstream, HtlcsWillTimeoutUpstream, Upstream}
import fr.acinq.eclair.db.{OutgoingPayment, OutgoingPaymentStatus}
import fr.acinq.eclair.payment.Monitoring.{Metrics, Tags}
import fr.acinq.eclair.payment.OutgoingPaymentPacket.Upstream
import fr.acinq.eclair.payment.PaymentSent.PartialPayment
import fr.acinq.eclair.payment._
import fr.acinq.eclair.payment.send.PaymentInitiator.SendPaymentConfig
@ -248,7 +247,8 @@ class MultiPartPaymentLifecycle(nodeParams: NodeParams, cfg: SendPaymentConfig,
case Right(paymentSent) =>
val localFees = cfg.upstream match {
case _: Upstream.Local => 0.msat // no local fees when we are the origin of the payment
case _: Upstream.Trampoline =>
case u: Upstream.Hot.Channel => u.amountIn - paymentSent.amountWithFees
case _: Upstream.Hot.Trampoline =>
// in case of a relayed payment, we need to take into account the fee of the first channels
paymentSent.parts.collect {
// NB: the route attribute will always be defined here

View File

@ -19,10 +19,10 @@ package fr.acinq.eclair.payment.send
import akka.actor.{Actor, ActorContext, ActorLogging, ActorRef, Props}
import fr.acinq.bitcoin.scalacompat.Crypto.{PrivateKey, PublicKey}
import fr.acinq.bitcoin.scalacompat.{ByteVector32, Crypto}
import fr.acinq.eclair.channel.Upstream
import fr.acinq.eclair.channel.fsm.Channel
import fr.acinq.eclair.crypto.Sphinx
import fr.acinq.eclair.db.PaymentType
import fr.acinq.eclair.payment.OutgoingPaymentPacket.Upstream
import fr.acinq.eclair.payment._
import fr.acinq.eclair.payment.send.BlindedPathsResolver.ResolvedPath
import fr.acinq.eclair.payment.send.PaymentError._
@ -190,7 +190,7 @@ class PaymentInitiator(nodeParams: NodeParams, outgoingPaymentFactory: PaymentIn
TrampolineRecipient(r.invoice, r.recipientAmount, finalExpiry, trampolineHop, trampolineSecret)
}
private def sendTrampolinePayment(paymentId: UUID, r: SendTrampolinePayment, trampolineFees: MilliSatoshi, trampolineExpiryDelta: CltvExpiryDelta) = {
private def sendTrampolinePayment(paymentId: UUID, r: SendTrampolinePayment, trampolineFees: MilliSatoshi, trampolineExpiryDelta: CltvExpiryDelta): Unit = {
val trampolineHop = NodeHop(r.trampolineNodeId, r.recipientNodeId, trampolineExpiryDelta, trampolineFees)
val paymentCfg = SendPaymentConfig(paymentId, paymentId, None, r.paymentHash, r.recipientNodeId, Upstream.Local(paymentId), Some(r.invoice), None, storeInDb = true, publishEvent = false, recordPathFindingMetrics = true)
val recipient = buildTrampolineRecipient(r, trampolineHop)
@ -401,7 +401,7 @@ object PaymentInitiator {
externalId: Option[String],
paymentHash: ByteVector32,
recipientNodeId: PublicKey,
upstream: Upstream,
upstream: Upstream.Hot,
invoice: Option[Invoice],
payerKey_opt: Option[PrivateKey],
storeInDb: Boolean, // e.g. for trampoline we don't want to store in the DB when we're relaying payments

View File

@ -27,7 +27,6 @@ import fr.acinq.eclair.crypto.{Sphinx, TransportHandler}
import fr.acinq.eclair.db.{OutgoingPayment, OutgoingPaymentStatus}
import fr.acinq.eclair.payment.Invoice.ExtraEdge
import fr.acinq.eclair.payment.Monitoring.{Metrics, Tags}
import fr.acinq.eclair.payment.OutgoingPaymentPacket.Upstream
import fr.acinq.eclair.payment.PaymentSent.PartialPayment
import fr.acinq.eclair.payment._
import fr.acinq.eclair.payment.send.PaymentInitiator.SendPaymentConfig
@ -76,7 +75,7 @@ class PaymentLifecycle(nodeParams: NodeParams, cfg: SendPaymentConfig, router: A
when(WAITING_FOR_ROUTE) {
case Event(RouteResponse(route +: _), WaitingForRoute(request, failures, ignore)) =>
log.info(s"route found: attempt=${failures.size + 1}/${request.maxAttempts} route=${route.printNodes()} channels=${route.printChannels()}")
OutgoingPaymentPacket.buildOutgoingPayment(self, cfg.upstream, paymentHash, route, request.recipient) match {
OutgoingPaymentPacket.buildOutgoingPayment(Origin.Hot(self, cfg.upstream), paymentHash, route, request.recipient) match {
case Right(payment) =>
register ! Register.ForwardShortId(self.toTyped[Register.ForwardShortIdFailure[CMD_ADD_HTLC]], payment.outgoingChannel, payment.cmd)
goto(WAITING_FOR_PAYMENT_COMPLETE) using WaitingForComplete(request, payment.cmd, failures, payment.sharedSecrets, ignore, route)
@ -387,7 +386,8 @@ class PaymentLifecycle(nodeParams: NodeParams, cfg: SendPaymentConfig, router: A
case Right(paymentSent) =>
val localFees = cfg.upstream match {
case _: Upstream.Local => 0.msat // no local fees when we are the origin of the payment
case _: Upstream.Trampoline =>
case u: Upstream.Hot.Channel => u.amountIn - paymentSent.amountWithFees
case _: Upstream.Hot.Trampoline =>
// in case of a relayed payment, we need to take into account the fee of the first channels
paymentSent.parts.collect {
// NB: the route attribute will always be defined here

View File

@ -29,7 +29,7 @@ import fr.acinq.eclair.wire.internal.channel.version0.ChannelTypes0.{HtlcTxAndSi
import fr.acinq.eclair.wire.protocol.CommonCodecs._
import fr.acinq.eclair.wire.protocol.LightningMessageCodecs.{channelAnnouncementCodec, channelUpdateCodec, combinedFeaturesCodec}
import fr.acinq.eclair.wire.protocol._
import fr.acinq.eclair.{Alias, BlockHeight, TimestampSecond}
import fr.acinq.eclair.{Alias, BlockHeight, MilliSatoshiLong, TimestampSecond}
import scodec.Codec
import scodec.bits.{BitVector, ByteVector}
import scodec.codecs._
@ -231,30 +231,37 @@ private[channel] object ChannelCodecs0 {
("sentAfterLocalCommitIndex" | uint64overflow) ::
("reSignAsap" | ignore(1))).as[ChannelTypes0.WaitingForRevocation].decodeOnly
val localColdCodec: Codec[Origin.LocalCold] = ("id" | uuid).as[Origin.LocalCold]
val upstreamLocalCodec: Codec[Upstream.Local] = ("id" | uuid).as[Upstream.Local]
val localCodec: Codec[Origin.Local] = localColdCodec.xmap[Origin.Local](o => o: Origin.Local, o => Origin.LocalCold(o.id))
val relayedColdCodec: Codec[Origin.ChannelRelayedCold] = (
val upstreamChannelCodec: Codec[Upstream.Cold.Channel] = (
("originChannelId" | bytes32) ::
("originHtlcId" | int64) ::
("amountIn" | millisatoshi) ::
("amountOut" | millisatoshi)).as[Origin.ChannelRelayedCold]
("amountOut" | ignore(64))).as[Upstream.Cold.Channel]
val relayedCodec: Codec[Origin.ChannelRelayed] = relayedColdCodec.xmap[Origin.ChannelRelayed](o => o: Origin.ChannelRelayed, o => Origin.ChannelRelayedCold(o.originChannelId, o.originHtlcId, o.amountIn, o.amountOut))
val upstreamChannelWithoutAmountCodec: Codec[Upstream.Cold.Channel] = (
("originChannelId" | bytes32) ::
("originHtlcId" | int64) ::
("amountIn" | provide(0 msat))).as[Upstream.Cold.Channel]
val trampolineRelayedColdCodec: Codec[Origin.TrampolineRelayedCold] = listOfN(uint16, bytes32 ~ int64).as[Origin.TrampolineRelayedCold]
val trampolineRelayedCodec: Codec[Origin.TrampolineRelayed] = trampolineRelayedColdCodec.xmap[Origin.TrampolineRelayed](o => o: Origin.TrampolineRelayed, o => Origin.TrampolineRelayedCold(o.htlcs))
val upstreamTrampolineCodec: Codec[Upstream.Cold.Trampoline] = listOfN(uint16, upstreamChannelWithoutAmountCodec).as[Upstream.Cold.Trampoline]
// this is for backward compatibility to handle legacy payments that didn't have identifiers
val UNKNOWN_UUID: UUID = UUID.fromString("00000000-0000-0000-0000-000000000000")
val originCodec: Codec[Origin] = discriminated[Origin].by(uint16)
.typecase(0x03, localCodec) // backward compatible
.typecase(0x01, provide(Origin.LocalCold(UNKNOWN_UUID)))
.typecase(0x02, relayedCodec)
.typecase(0x04, trampolineRelayedCodec)
val coldUpstreamCodec: Codec[Upstream.Cold] = discriminated[Upstream.Cold].by(uint16)
.typecase(0x03, upstreamLocalCodec) // backward compatible
.typecase(0x01, provide(Upstream.Local(UNKNOWN_UUID)))
.typecase(0x02, upstreamChannelCodec)
.typecase(0x04, upstreamTrampolineCodec)
val originCodec: Codec[Origin] = coldUpstreamCodec.xmap[Origin](
upstream => Origin.Cold(upstream),
{
case Origin.Hot(_, upstream) => Upstream.Cold(upstream)
case Origin.Cold(upstream) => upstream
}
)
val originsListCodec: Codec[List[(Long, Origin)]] = listOfN(uint16, int64 ~ originCodec)

View File

@ -30,7 +30,7 @@ import fr.acinq.eclair.wire.internal.channel.version0.ChannelTypes0.{HtlcTxAndSi
import fr.acinq.eclair.wire.protocol.CommonCodecs._
import fr.acinq.eclair.wire.protocol.LightningMessageCodecs._
import fr.acinq.eclair.wire.protocol._
import fr.acinq.eclair.{Alias, BlockHeight}
import fr.acinq.eclair.{Alias, BlockHeight, MilliSatoshiLong}
import scodec.bits.ByteVector
import scodec.codecs._
import scodec.{Attempt, Codec}
@ -156,26 +156,33 @@ private[channel] object ChannelCodecs1 {
("sentAfterLocalCommitIndex" | uint64overflow) ::
("reSignAsap" | ignore(8))).as[ChannelTypes0.WaitingForRevocation]
val localColdCodec: Codec[Origin.LocalCold] = ("id" | uuid).as[Origin.LocalCold]
val upstreamLocalCodec: Codec[Upstream.Local] = ("id" | uuid).as[Upstream.Local]
val localCodec: Codec[Origin.Local] = localColdCodec.xmap[Origin.Local](o => o: Origin.Local, o => Origin.LocalCold(o.id))
val relayedColdCodec: Codec[Origin.ChannelRelayedCold] = (
val upstreamChannelCodec: Codec[Upstream.Cold.Channel] = (
("originChannelId" | bytes32) ::
("originHtlcId" | int64) ::
("amountIn" | millisatoshi) ::
("amountOut" | millisatoshi)).as[Origin.ChannelRelayedCold]
("amountOut" | ignore(64))).as[Upstream.Cold.Channel]
val relayedCodec: Codec[Origin.ChannelRelayed] = relayedColdCodec.xmap[Origin.ChannelRelayed](o => o: Origin.ChannelRelayed, o => Origin.ChannelRelayedCold(o.originChannelId, o.originHtlcId, o.amountIn, o.amountOut))
val upstreamChannelWithoutAmountCodec: Codec[Upstream.Cold.Channel] = (
("originChannelId" | bytes32) ::
("originHtlcId" | int64) ::
("amountIn" | provide(0 msat))).as[Upstream.Cold.Channel]
val trampolineRelayedColdCodec: Codec[Origin.TrampolineRelayedCold] = listOfN(uint16, bytes32 ~ int64).as[Origin.TrampolineRelayedCold]
val upstreamTrampolineCodec: Codec[Upstream.Cold.Trampoline] = listOfN(uint16, upstreamChannelWithoutAmountCodec).as[Upstream.Cold.Trampoline]
val trampolineRelayedCodec: Codec[Origin.TrampolineRelayed] = trampolineRelayedColdCodec.xmap[Origin.TrampolineRelayed](o => o: Origin.TrampolineRelayed, o => Origin.TrampolineRelayedCold(o.htlcs))
val coldUpstreamCodec: Codec[Upstream.Cold] = discriminated[Upstream.Cold].by(uint16)
.typecase(0x02, upstreamChannelCodec)
.typecase(0x03, upstreamLocalCodec)
.typecase(0x04, upstreamTrampolineCodec)
val originCodec: Codec[Origin] = discriminated[Origin].by(uint16)
.typecase(0x02, relayedCodec)
.typecase(0x03, localCodec)
.typecase(0x04, trampolineRelayedCodec)
val originCodec: Codec[Origin] = coldUpstreamCodec.xmap[Origin](
upstream => Origin.Cold(upstream),
{
case Origin.Hot(_, upstream) => Upstream.Cold(upstream)
case Origin.Cold(upstream) => upstream
}
)
def mapCodec[K, V](keyCodec: Codec[K], valueCodec: Codec[V]): Codec[Map[K, V]] = listOfN(uint16, keyCodec ~ valueCodec).xmap(_.toMap, _.toList)

View File

@ -30,7 +30,7 @@ import fr.acinq.eclair.wire.internal.channel.version0.ChannelTypes0.{HtlcTxAndSi
import fr.acinq.eclair.wire.protocol.CommonCodecs._
import fr.acinq.eclair.wire.protocol.LightningMessageCodecs._
import fr.acinq.eclair.wire.protocol._
import fr.acinq.eclair.{Alias, BlockHeight}
import fr.acinq.eclair.{Alias, BlockHeight, MilliSatoshiLong}
import scodec.bits.ByteVector
import scodec.codecs._
import scodec.{Attempt, Codec}
@ -183,26 +183,33 @@ private[channel] object ChannelCodecs2 {
("sentAfterLocalCommitIndex" | uint64overflow) ::
("reSignAsap" | ignore(8))).as[ChannelTypes0.WaitingForRevocation]
val localColdCodec: Codec[Origin.LocalCold] = ("id" | uuid).as[Origin.LocalCold]
val upstreamLocalCodec: Codec[Upstream.Local] = ("id" | uuid).as[Upstream.Local]
val localCodec: Codec[Origin.Local] = localColdCodec.xmap[Origin.Local](o => o: Origin.Local, o => Origin.LocalCold(o.id))
val relayedColdCodec: Codec[Origin.ChannelRelayedCold] = (
val upstreamChannelCodec: Codec[Upstream.Cold.Channel] = (
("originChannelId" | bytes32) ::
("originHtlcId" | int64) ::
("amountIn" | millisatoshi) ::
("amountOut" | millisatoshi)).as[Origin.ChannelRelayedCold]
("amountOut" | ignore(64))).as[Upstream.Cold.Channel]
val relayedCodec: Codec[Origin.ChannelRelayed] = relayedColdCodec.xmap[Origin.ChannelRelayed](o => o: Origin.ChannelRelayed, o => Origin.ChannelRelayedCold(o.originChannelId, o.originHtlcId, o.amountIn, o.amountOut))
val upstreamChannelWithoutAmountCodec: Codec[Upstream.Cold.Channel] = (
("originChannelId" | bytes32) ::
("originHtlcId" | int64) ::
("amountIn" | provide(0 msat))).as[Upstream.Cold.Channel]
val trampolineRelayedColdCodec: Codec[Origin.TrampolineRelayedCold] = listOfN(uint16, bytes32 ~ int64).as[Origin.TrampolineRelayedCold]
val upstreamTrampolineCodec: Codec[Upstream.Cold.Trampoline] = listOfN(uint16, upstreamChannelWithoutAmountCodec).as[Upstream.Cold.Trampoline]
val trampolineRelayedCodec: Codec[Origin.TrampolineRelayed] = trampolineRelayedColdCodec.xmap[Origin.TrampolineRelayed](o => o: Origin.TrampolineRelayed, o => Origin.TrampolineRelayedCold(o.htlcs))
val coldUpstreamCodec: Codec[Upstream.Cold] = discriminated[Upstream.Cold].by(uint16)
.typecase(0x02, upstreamChannelCodec)
.typecase(0x03, upstreamLocalCodec)
.typecase(0x04, upstreamTrampolineCodec)
val originCodec: Codec[Origin] = discriminated[Origin].by(uint16)
.typecase(0x02, relayedCodec)
.typecase(0x03, localCodec)
.typecase(0x04, trampolineRelayedCodec)
val originCodec: Codec[Origin] = coldUpstreamCodec.xmap[Origin](
upstream => Origin.Cold(upstream),
{
case Origin.Hot(_, upstream) => Upstream.Cold(upstream)
case Origin.Cold(upstream) => upstream
}
)
def mapCodec[K, V](keyCodec: Codec[K], valueCodec: Codec[V]): Codec[Map[K, V]] = listOfN(uint16, keyCodec ~ valueCodec).xmap(_.toMap, _.toList)

View File

@ -30,7 +30,7 @@ import fr.acinq.eclair.wire.internal.channel.version0.ChannelTypes0
import fr.acinq.eclair.wire.protocol.CommonCodecs._
import fr.acinq.eclair.wire.protocol.LightningMessageCodecs._
import fr.acinq.eclair.wire.protocol.UpdateMessage
import fr.acinq.eclair.{Alias, BlockHeight, FeatureSupport, Features, PermanentChannelFeature}
import fr.acinq.eclair.{Alias, BlockHeight, FeatureSupport, Features, MilliSatoshiLong, PermanentChannelFeature}
import scodec.bits.{BitVector, ByteVector}
import scodec.codecs._
import scodec.{Attempt, Codec, Err}
@ -123,7 +123,7 @@ private[channel] object ChannelCodecs3 {
("amount" | satoshi) ::
("scriptPubKey" | lengthDelimited(bytes))).as[OutputInfo]
private val defaultConfirmationTarget: Codec[ConfirmationTarget.Absolute] = provide(ConfirmationTarget.Absolute(BlockHeight(0)))
private val defaultConfirmationTarget: Codec[ConfirmationTarget.Absolute] = provide(ConfirmationTarget.Absolute(BlockHeight(0)))
private val blockHeightConfirmationTarget: Codec[ConfirmationTarget.Absolute] = blockHeight.map(ConfirmationTarget.Absolute).decodeOnly
val commitTxCodec: Codec[CommitTx] = (("inputInfo" | inputInfoCodec) :: ("tx" | txCodec)).as[CommitTx]
@ -235,26 +235,33 @@ private[channel] object ChannelCodecs3 {
("sentAfterLocalCommitIndex" | uint64overflow) ::
("reSignAsap" | ignore(8))).as[ChannelTypes3.WaitingForRevocation]
val localColdCodec: Codec[Origin.LocalCold] = ("id" | uuid).as[Origin.LocalCold]
val upstreamLocalCodec: Codec[Upstream.Local] = ("id" | uuid).as[Upstream.Local]
val localCodec: Codec[Origin.Local] = localColdCodec.xmap[Origin.Local](o => o: Origin.Local, o => Origin.LocalCold(o.id))
val relayedColdCodec: Codec[Origin.ChannelRelayedCold] = (
val upstreamChannelCodec: Codec[Upstream.Cold.Channel] = (
("originChannelId" | bytes32) ::
("originHtlcId" | int64) ::
("amountIn" | millisatoshi) ::
("amountOut" | millisatoshi)).as[Origin.ChannelRelayedCold]
("amountOut" | ignore(64))).as[Upstream.Cold.Channel]
val relayedCodec: Codec[Origin.ChannelRelayed] = relayedColdCodec.xmap[Origin.ChannelRelayed](o => o: Origin.ChannelRelayed, o => Origin.ChannelRelayedCold(o.originChannelId, o.originHtlcId, o.amountIn, o.amountOut))
val upstreamChannelWithoutAmountCodec: Codec[Upstream.Cold.Channel] = (
("originChannelId" | bytes32) ::
("originHtlcId" | int64) ::
("amountIn" | provide(0 msat))).as[Upstream.Cold.Channel]
val trampolineRelayedColdCodec: Codec[Origin.TrampolineRelayedCold] = listOfN(uint16, bytes32 ~ int64).as[Origin.TrampolineRelayedCold]
val upstreamTrampolineCodec: Codec[Upstream.Cold.Trampoline] = listOfN(uint16, upstreamChannelWithoutAmountCodec).as[Upstream.Cold.Trampoline]
val trampolineRelayedCodec: Codec[Origin.TrampolineRelayed] = trampolineRelayedColdCodec.xmap[Origin.TrampolineRelayed](o => o: Origin.TrampolineRelayed, o => Origin.TrampolineRelayedCold(o.htlcs))
val coldUpstreamCodec: Codec[Upstream.Cold] = discriminated[Upstream.Cold].by(uint16)
.typecase(0x02, upstreamChannelCodec)
.typecase(0x03, upstreamLocalCodec)
.typecase(0x04, upstreamTrampolineCodec)
val originCodec: Codec[Origin] = discriminated[Origin].by(uint16)
.typecase(0x02, relayedCodec)
.typecase(0x03, localCodec)
.typecase(0x04, trampolineRelayedCodec)
val originCodec: Codec[Origin] = coldUpstreamCodec.xmap[Origin](
upstream => Origin.Cold(upstream),
{
case Origin.Hot(_, upstream) => Upstream.Cold(upstream)
case Origin.Cold(upstream) => upstream
}
)
def mapCodec[K, V](keyCodec: Codec[K], valueCodec: Codec[V]): Codec[Map[K, V]] = listOfN(uint16, keyCodec ~ valueCodec).xmap(_.toMap, _.toList)

View File

@ -199,26 +199,43 @@ private[channel] object ChannelCodecs4 {
("acked" | listOfN(uint16, updateMessageCodec)) ::
("signed" | listOfN(uint16, updateMessageCodec))).as[RemoteChanges]
val localColdCodec: Codec[Origin.LocalCold] = ("id" | uuid).as[Origin.LocalCold]
val upstreamLocalCodec: Codec[Upstream.Local] = ("id" | uuid).as[Upstream.Local]
val localCodec: Codec[Origin.Local] = localColdCodec.xmap[Origin.Local](o => o: Origin.Local, o => Origin.LocalCold(o.id))
val upstreamChannelCodec: Codec[Upstream.Cold.Channel] = (
("originChannelId" | bytes32) ::
("originHtlcId" | int64) ::
("amountIn" | millisatoshi)).as[Upstream.Cold.Channel]
val relayedColdCodec: Codec[Origin.ChannelRelayedCold] = (
val legacyUpstreamChannelCodec: Codec[Upstream.Cold.Channel] = (
("originChannelId" | bytes32) ::
("originHtlcId" | int64) ::
("amountIn" | millisatoshi) ::
("amountOut" | millisatoshi)).as[Origin.ChannelRelayedCold]
("amountOut" | ignore(64))).as[Upstream.Cold.Channel]
val relayedCodec: Codec[Origin.ChannelRelayed] = relayedColdCodec.xmap[Origin.ChannelRelayed](o => o: Origin.ChannelRelayed, o => Origin.ChannelRelayedCold(o.originChannelId, o.originHtlcId, o.amountIn, o.amountOut))
val upstreamChannelWithoutAmountCodec: Codec[Upstream.Cold.Channel] = (
("originChannelId" | bytes32) ::
("originHtlcId" | int64) ::
("amountIn" | provide(0 msat))).as[Upstream.Cold.Channel]
val trampolineRelayedColdCodec: Codec[Origin.TrampolineRelayedCold] = listOfN(uint16, bytes32 ~ int64).as[Origin.TrampolineRelayedCold]
val legacyUpstreamTrampolineCodec: Codec[Upstream.Cold.Trampoline] = listOfN(uint16, upstreamChannelWithoutAmountCodec).as[Upstream.Cold.Trampoline]
val trampolineRelayedCodec: Codec[Origin.TrampolineRelayed] = trampolineRelayedColdCodec.xmap[Origin.TrampolineRelayed](o => o: Origin.TrampolineRelayed, o => Origin.TrampolineRelayedCold(o.htlcs))
val upstreamTrampolineCodec: Codec[Upstream.Cold.Trampoline] = listOfN(uint16, upstreamChannelCodec).as[Upstream.Cold.Trampoline]
val originCodec: Codec[Origin] = discriminated[Origin].by(uint16)
.typecase(0x02, relayedCodec)
.typecase(0x03, localCodec)
.typecase(0x04, trampolineRelayedCodec)
val coldUpstreamCodec: Codec[Upstream.Cold] = discriminated[Upstream.Cold].by(uint16)
// NB: order matters!
.typecase(0x06, upstreamChannelCodec)
.typecase(0x05, upstreamTrampolineCodec)
.typecase(0x04, legacyUpstreamTrampolineCodec)
.typecase(0x03, upstreamLocalCodec)
.typecase(0x02, legacyUpstreamChannelCodec)
val originCodec: Codec[Origin] = coldUpstreamCodec.xmap[Origin](
upstream => Origin.Cold(upstream),
{
case Origin.Hot(_, upstream) => Upstream.Cold(upstream)
case Origin.Cold(upstream) => upstream
}
)
def mapCodec[K, V](keyCodec: Codec[K], valueCodec: Codec[V]): Codec[Map[K, V]] = listOfN(uint16, keyCodec ~ valueCodec).xmap(_.toMap, _.toList)

View File

@ -353,6 +353,9 @@ case class UpdateAddHtlc(channelId: ByteVector32,
onionRoutingPacket: OnionRoutingPacket,
tlvStream: TlvStream[UpdateAddHtlcTlv]) extends HtlcMessage with UpdateMessage with HasChannelId {
val blinding_opt: Option[PublicKey] = tlvStream.get[UpdateAddHtlcTlv.BlindingPoint].map(_.publicKey)
/** When storing in our DB, we avoid wasting storage with unknown data. */
def removeUnknownTlvs(): UpdateAddHtlc = this.copy(tlvStream = tlvStream.copy(unknown = Set.empty))
}
object UpdateAddHtlc {

View File

@ -16,12 +16,13 @@
package fr.acinq.eclair
import akka.actor.ActorRef
import fr.acinq.bitcoin.scalacompat.{Block, ByteVector32, Satoshi, SatoshiLong}
import fr.acinq.eclair.FeatureSupport.{Mandatory, Optional}
import fr.acinq.eclair.Features._
import fr.acinq.eclair.blockchain.fee._
import fr.acinq.eclair.channel.fsm.Channel.{ChannelConf, RemoteRbfLimits, UnhandledExceptionStrategy}
import fr.acinq.eclair.channel.{ChannelFlags, LocalParams}
import fr.acinq.eclair.channel.{ChannelFlags, LocalParams, Origin, Upstream}
import fr.acinq.eclair.crypto.keymanager.{LocalChannelKeyManager, LocalNodeKeyManager}
import fr.acinq.eclair.db.RevokedHtlcInfoCleaner
import fr.acinq.eclair.io.MessageRelay.RelayAll
@ -52,6 +53,7 @@ object TestConstants {
val feeratePerKw: FeeratePerKw = FeeratePerKw(10_000 sat)
val anchorOutputsFeeratePerKw: FeeratePerKw = FeeratePerKw(2_500 sat)
val emptyOnionPacket: OnionRoutingPacket = OnionRoutingPacket(0, ByteVector.fill(33)(0), ByteVector.fill(1300)(0), ByteVector32.Zeroes)
val emptyOrigin = Origin.Hot(ActorRef.noSender, Upstream.Local(UUID.randomUUID()))
case object TestFeature extends Feature with InitFeature with NodeFeature {
val rfcName = "test_feature"

View File

@ -61,9 +61,7 @@ object TestDatabases {
override def addOrUpdateChannel(data: PersistentChannelData): Unit = {
def freeze1(input: Origin): Origin = input match {
case h: Origin.LocalHot => Origin.LocalCold(h.id)
case h: Origin.ChannelRelayedHot => Origin.ChannelRelayedCold(h.originChannelId, h.originHtlcId, h.amountIn, h.amountOut)
case h: Origin.TrampolineRelayedHot => Origin.TrampolineRelayedCold(h.htlcs)
case h: Origin.Hot => Origin.Cold(h)
case c: Origin.Cold => c
}

View File

@ -10,10 +10,9 @@ import fr.acinq.eclair.blockchain.bitcoind.rpc.BitcoinCoreClient
import fr.acinq.eclair.channel.Helpers.Closing.{CurrentRemoteClose, LocalClose}
import fr.acinq.eclair.channel.publish.TxPublisher.{PublishFinalTx, PublishReplaceableTx}
import fr.acinq.eclair.channel.states.ChannelStateTestsBase
import fr.acinq.eclair.channel.{CLOSING, CMD_SIGN, DATA_CLOSING, DATA_NORMAL}
import fr.acinq.eclair.channel.{CLOSING, CMD_SIGN, DATA_CLOSING, DATA_NORMAL, Upstream}
import fr.acinq.eclair.db.jdbc.JdbcUtils.ExtendedResultSet._
import fr.acinq.eclair.db.pg.PgUtils.using
import fr.acinq.eclair.payment.OutgoingPaymentPacket.Upstream
import fr.acinq.eclair.wire.internal.channel.ChannelCodecs.channelDataCodec
import fr.acinq.eclair.wire.protocol.{CommitSig, Error, RevokeAndAck, TlvStream, UpdateAddHtlc, UpdateAddHtlcTlv}
import fr.acinq.eclair.{BlockHeight, CltvExpiry, CltvExpiryDelta, MilliSatoshiLong, TestConstants, TestKitBaseClass, TimestampMilli, ToMilliSatoshiConversion, randomBytes32}
@ -171,7 +170,7 @@ class CheckBalanceSpec extends TestKitBaseClass with FixtureAnyFunSuiteLike with
val (ra2, htlca2) = addHtlc(100000000 msat, alice, bob, alice2bob, bob2alice)
val (_, htlca3) = addHtlc(10000 msat, alice, bob, alice2bob, bob2alice)
// for this one we set a non-local upstream to simulate a relayed payment
val (_, htlca4) = addHtlc(30000000 msat, CltvExpiryDelta(144), alice, bob, alice2bob, bob2alice, upstream = Upstream.Trampoline(Upstream.ReceivedHtlc(UpdateAddHtlc(randomBytes32(), 42, 30003000 msat, randomBytes32(), CltvExpiry(144), TestConstants.emptyOnionPacket, TlvStream.empty[UpdateAddHtlcTlv]), TimestampMilli(1687345927000L)) :: Nil), replyTo = TestProbe().ref)
val (_, htlca4) = addHtlc(30000000 msat, CltvExpiryDelta(144), alice, bob, alice2bob, bob2alice, upstream = Upstream.Hot.Trampoline(Upstream.Hot.Channel(UpdateAddHtlc(randomBytes32(), 42, 30003000 msat, randomBytes32(), CltvExpiry(144), TestConstants.emptyOnionPacket, TlvStream.empty[UpdateAddHtlcTlv]), TimestampMilli(1687345927000L)) :: Nil), replyTo = TestProbe().ref)
val (rb1, htlcb1) = addHtlc(50000000 msat, bob, alice, bob2alice, alice2bob)
val (_, _) = addHtlc(55000000 msat, bob, alice, bob2alice, alice2bob)
crossSign(alice, bob, alice2bob, bob2alice)

View File

@ -28,7 +28,6 @@ import fr.acinq.eclair.channel.fsm.Channel
import fr.acinq.eclair.channel.publish.TxPublisher
import fr.acinq.eclair.channel.states.ChannelStateTestsBase
import fr.acinq.eclair.channel.states.ChannelStateTestsBase.FakeTxPublisherFactory
import fr.acinq.eclair.payment.OutgoingPaymentPacket.Upstream
import fr.acinq.eclair.payment._
import fr.acinq.eclair.payment.receive.MultiPartHandler.ReceiveStandardPayment
import fr.acinq.eclair.payment.receive.PaymentHandler
@ -120,7 +119,7 @@ class FuzzySpec extends TestKitBaseClass with FixtureAnyFunSuiteLike with Channe
// 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(currentBlockHeight = BlockHeight(400000))
val Right(payment) = OutgoingPaymentPacket.buildOutgoingPayment(self, Upstream.Local(UUID.randomUUID()), invoice.paymentHash, makeSingleHopRoute(amount, invoice.nodeId), ClearRecipient(invoice, amount, expiry, Set.empty))
val Right(payment) = OutgoingPaymentPacket.buildOutgoingPayment(localOrigin(self), invoice.paymentHash, makeSingleHopRoute(amount, invoice.nodeId), ClearRecipient(invoice, amount, expiry, Set.empty))
payment.cmd
}

View File

@ -68,14 +68,14 @@ class HelpersSpec extends TestKitBaseClass with AnyFunSuiteLike with ChannelStat
awaitCond(bob.stateName == NORMAL)
// We have two identical HTLCs (MPP):
val (_, htlca1a) = addHtlc(15_000_000 msat, alice, bob, alice2bob, bob2alice)
val aliceMppCmd = CMD_ADD_HTLC(TestProbe().ref, 15_000_000 msat, htlca1a.paymentHash, htlca1a.cltvExpiry, htlca1a.onionRoutingPacket, None, Origin.LocalHot(TestProbe().ref, UUID.randomUUID()))
val aliceMppCmd = CMD_ADD_HTLC(TestProbe().ref, 15_000_000 msat, htlca1a.paymentHash, htlca1a.cltvExpiry, htlca1a.onionRoutingPacket, None, Origin.Hot(TestProbe().ref, Upstream.Local(UUID.randomUUID())))
val htlca1b = addHtlc(aliceMppCmd, alice, bob, alice2bob, bob2alice)
val (ra2, htlca2) = addHtlc(16_000_000 msat, alice, bob, alice2bob, bob2alice)
addHtlc(500_000 msat, alice, bob, alice2bob, bob2alice) // below dust
crossSign(alice, bob, alice2bob, bob2alice)
// We have two identical HTLCs (MPP):
val (_, htlcb1a) = addHtlc(17_000_000 msat, bob, alice, bob2alice, alice2bob)
val bobMppCmd = CMD_ADD_HTLC(TestProbe().ref, 17_000_000 msat, htlcb1a.paymentHash, htlcb1a.cltvExpiry, htlcb1a.onionRoutingPacket, None, Origin.LocalHot(TestProbe().ref, UUID.randomUUID()))
val bobMppCmd = CMD_ADD_HTLC(TestProbe().ref, 17_000_000 msat, htlcb1a.paymentHash, htlcb1a.cltvExpiry, htlcb1a.onionRoutingPacket, None, Origin.Hot(TestProbe().ref, Upstream.Local(UUID.randomUUID())))
val htlcb1b = addHtlc(bobMppCmd, bob, alice, bob2alice, alice2bob)
val (rb2, htlcb2) = addHtlc(18_000_000 msat, bob, alice, bob2alice, alice2bob)
addHtlc(400_000 msat, bob, alice, bob2alice, alice2bob) // below dust

View File

@ -33,7 +33,6 @@ import fr.acinq.eclair.channel.publish.TxPublisher
import fr.acinq.eclair.channel.publish.TxPublisher.PublishReplaceableTx
import fr.acinq.eclair.channel.states.ChannelStateTestsBase.FakeTxPublisherFactory
import fr.acinq.eclair.channel._
import fr.acinq.eclair.payment.OutgoingPaymentPacket.Upstream
import fr.acinq.eclair.payment.send.SpontaneousRecipient
import fr.acinq.eclair.payment.{Invoice, OutgoingPaymentPacket}
import fr.acinq.eclair.router.Router.{ChannelHop, HopRelayParams, Route}
@ -371,7 +370,7 @@ trait ChannelStateTestsBase extends Assertions with Eventually {
fundingTx
}
def localOrigin(replyTo: ActorRef): Origin.LocalHot = Origin.LocalHot(replyTo, UUID.randomUUID())
def localOrigin(replyTo: ActorRef): Origin.Hot = Origin.Hot(replyTo, Upstream.Local(UUID.randomUUID()))
def makeCmdAdd(amount: MilliSatoshi, destination: PublicKey, currentBlockHeight: BlockHeight): (ByteVector32, CMD_ADD_HTLC) = {
makeCmdAdd(amount, CltvExpiryDelta(144), destination, randomBytes32(), currentBlockHeight, Upstream.Local(UUID.randomUUID()))
@ -381,15 +380,15 @@ trait ChannelStateTestsBase extends Assertions with Eventually {
makeCmdAdd(amount, destination, currentBlockHeight, paymentPreimage, Upstream.Local(UUID.randomUUID()))
}
def makeCmdAdd(amount: MilliSatoshi, destination: PublicKey, currentBlockHeight: BlockHeight, paymentPreimage: ByteVector32, upstream: Upstream): (ByteVector32, CMD_ADD_HTLC) = {
def makeCmdAdd(amount: MilliSatoshi, destination: PublicKey, currentBlockHeight: BlockHeight, paymentPreimage: ByteVector32, upstream: Upstream.Hot): (ByteVector32, CMD_ADD_HTLC) = {
makeCmdAdd(amount, CltvExpiryDelta(144), destination, paymentPreimage, currentBlockHeight, upstream)
}
def makeCmdAdd(amount: MilliSatoshi, cltvExpiryDelta: CltvExpiryDelta, destination: PublicKey, paymentPreimage: ByteVector32, currentBlockHeight: BlockHeight, upstream: Upstream, replyTo: ActorRef = TestProbe().ref): (ByteVector32, CMD_ADD_HTLC) = {
def makeCmdAdd(amount: MilliSatoshi, cltvExpiryDelta: CltvExpiryDelta, destination: PublicKey, paymentPreimage: ByteVector32, currentBlockHeight: BlockHeight, upstream: Upstream.Hot, replyTo: ActorRef = TestProbe().ref): (ByteVector32, CMD_ADD_HTLC) = {
val paymentHash = Crypto.sha256(paymentPreimage)
val expiry = cltvExpiryDelta.toCltvExpiry(currentBlockHeight)
val recipient = SpontaneousRecipient(destination, amount, expiry, paymentPreimage)
val Right(payment) = OutgoingPaymentPacket.buildOutgoingPayment(replyTo, upstream, paymentHash, makeSingleHopRoute(amount, destination), recipient)
val Right(payment) = OutgoingPaymentPacket.buildOutgoingPayment(Origin.Hot(replyTo, upstream), paymentHash, makeSingleHopRoute(amount, destination), recipient)
(paymentPreimage, payment.cmd.copy(commit = false))
}
@ -406,7 +405,7 @@ trait ChannelStateTestsBase extends Assertions with Eventually {
addHtlc(amount, CltvExpiryDelta(144), s, r, s2r, r2s, replyTo)
}
def addHtlc(amount: MilliSatoshi, cltvExpiryDelta: CltvExpiryDelta, s: TestFSMRef[ChannelState, ChannelData, Channel], r: TestFSMRef[ChannelState, ChannelData, Channel], s2r: TestProbe, r2s: TestProbe, replyTo: ActorRef = TestProbe().ref, upstream: Upstream = Upstream.Local(UUID.randomUUID())): (ByteVector32, UpdateAddHtlc) = {
def addHtlc(amount: MilliSatoshi, cltvExpiryDelta: CltvExpiryDelta, s: TestFSMRef[ChannelState, ChannelData, Channel], r: TestFSMRef[ChannelState, ChannelData, Channel], s2r: TestProbe, r2s: TestProbe, replyTo: ActorRef = TestProbe().ref, upstream: Upstream.Hot = Upstream.Local(UUID.randomUUID())): (ByteVector32, UpdateAddHtlc) = {
val currentBlockHeight = s.underlyingActor.nodeParams.currentBlockHeight
val (payment_preimage, cmd) = makeCmdAdd(amount, cltvExpiryDelta, r.underlyingActor.nodeParams.nodeId, randomBytes32(), currentBlockHeight, upstream, replyTo)
val htlc = addHtlc(cmd, s, r, s2r, r2s)

View File

@ -116,8 +116,8 @@ class NormalStateSpec extends TestKitBaseClass with FixtureAnyFunSuiteLike with
val sender = TestProbe()
val h = randomBytes32()
val originHtlc = UpdateAddHtlc(channelId = randomBytes32(), id = 5656, amountMsat = 50000000 msat, cltvExpiry = CltvExpiryDelta(144).toCltvExpiry(currentBlockHeight), paymentHash = h, onionRoutingPacket = TestConstants.emptyOnionPacket, blinding_opt = None)
val origin = Origin.ChannelRelayedHot(sender.ref, originHtlc, originHtlc.amountMsat)
val cmd = CMD_ADD_HTLC(sender.ref, originHtlc.amountMsat - 10000.msat, h, originHtlc.cltvExpiry - CltvExpiryDelta(7), TestConstants.emptyOnionPacket, None, origin)
val origin = Origin.Hot(sender.ref, Upstream.Hot.Channel(originHtlc, TimestampMilli.now()))
val cmd = CMD_ADD_HTLC(sender.ref, originHtlc.amountMsat - 10_000.msat, h, originHtlc.cltvExpiry - CltvExpiryDelta(7), TestConstants.emptyOnionPacket, None, origin)
alice ! cmd
sender.expectMsgType[RES_SUCCESS[CMD_ADD_HTLC]]
val htlc = alice2bob.expectMsgType[UpdateAddHtlc]
@ -135,7 +135,7 @@ class NormalStateSpec extends TestKitBaseClass with FixtureAnyFunSuiteLike with
val h = randomBytes32()
val originHtlc1 = UpdateAddHtlc(randomBytes32(), 47, 30000000 msat, h, CltvExpiryDelta(144).toCltvExpiry(currentBlockHeight), TestConstants.emptyOnionPacket, None)
val originHtlc2 = UpdateAddHtlc(randomBytes32(), 32, 20000000 msat, h, CltvExpiryDelta(160).toCltvExpiry(currentBlockHeight), TestConstants.emptyOnionPacket, None)
val origin = Origin.TrampolineRelayedHot(sender.ref, originHtlc1 :: originHtlc2 :: Nil)
val origin = Origin.Hot(sender.ref, Upstream.Hot.Trampoline(Seq(originHtlc1, originHtlc2).map(htlc => Upstream.Hot.Channel(htlc, TimestampMilli.now()))))
val cmd = CMD_ADD_HTLC(sender.ref, originHtlc1.amountMsat + originHtlc2.amountMsat - 10000.msat, h, originHtlc2.cltvExpiry - CltvExpiryDelta(7), TestConstants.emptyOnionPacket, None, origin)
alice ! cmd
sender.expectMsgType[RES_SUCCESS[CMD_ADD_HTLC]]

View File

@ -28,7 +28,6 @@ import fr.acinq.eclair.channel._
import fr.acinq.eclair.channel.publish.TxPublisher.{PublishFinalTx, PublishReplaceableTx, PublishTx}
import fr.acinq.eclair.channel.states.ChannelStateTestsBase.PimpTestFSM
import fr.acinq.eclair.channel.states.{ChannelStateTestsBase, ChannelStateTestsTags}
import fr.acinq.eclair.payment.OutgoingPaymentPacket.Upstream
import fr.acinq.eclair.payment._
import fr.acinq.eclair.payment.relay.Relayer._
import fr.acinq.eclair.payment.send.SpontaneousRecipient
@ -38,7 +37,6 @@ import org.scalatest.funsuite.FixtureAnyFunSuiteLike
import org.scalatest.{Outcome, Tag}
import scodec.bits.ByteVector
import java.util.UUID
import scala.concurrent.duration._
/**
@ -61,7 +59,7 @@ class ShutdownStateSpec extends TestKitBaseClass with FixtureAnyFunSuiteLike wit
// alice sends an HTLC to bob
val h1 = Crypto.sha256(r1)
val recipient1 = SpontaneousRecipient(TestConstants.Bob.nodeParams.nodeId, 300_000_000 msat, CltvExpiryDelta(144).toCltvExpiry(currentBlockHeight), r1)
val Right(cmd1) = OutgoingPaymentPacket.buildOutgoingPayment(sender.ref, Upstream.Local(UUID.randomUUID), h1, makeSingleHopRoute(recipient1.totalAmount, recipient1.nodeId), recipient1).map(_.cmd.copy(commit = false))
val Right(cmd1) = OutgoingPaymentPacket.buildOutgoingPayment(localOrigin(sender.ref), h1, makeSingleHopRoute(recipient1.totalAmount, recipient1.nodeId), recipient1).map(_.cmd.copy(commit = false))
alice ! cmd1
sender.expectMsgType[RES_SUCCESS[CMD_ADD_HTLC]]
val htlc1 = alice2bob.expectMsgType[UpdateAddHtlc]
@ -70,7 +68,7 @@ class ShutdownStateSpec extends TestKitBaseClass with FixtureAnyFunSuiteLike wit
// alice sends another HTLC to bob
val h2 = Crypto.sha256(r2)
val recipient2 = SpontaneousRecipient(TestConstants.Bob.nodeParams.nodeId, 200_000_000 msat, CltvExpiryDelta(144).toCltvExpiry(currentBlockHeight), r2)
val Right(cmd2) = OutgoingPaymentPacket.buildOutgoingPayment(sender.ref, Upstream.Local(UUID.randomUUID), h2, makeSingleHopRoute(recipient2.totalAmount, recipient2.nodeId), recipient2).map(_.cmd.copy(commit = false))
val Right(cmd2) = OutgoingPaymentPacket.buildOutgoingPayment(localOrigin(sender.ref), h2, makeSingleHopRoute(recipient2.totalAmount, recipient2.nodeId), recipient2).map(_.cmd.copy(commit = false))
alice ! cmd2
sender.expectMsgType[RES_SUCCESS[CMD_ADD_HTLC]]
val htlc2 = alice2bob.expectMsgType[UpdateAddHtlc]

View File

@ -57,7 +57,7 @@ class SynchronizationPipe(latch: CountDownLatch) extends Actor with ActorLogging
(script: @unchecked) match {
case offer(x, amount, rhash) :: rest =>
resolve(x) ! CMD_ADD_HTLC(self, MilliSatoshi(amount.toInt), ByteVector32.fromValidHex(rhash), CltvExpiry(144), TestConstants.emptyOnionPacket, None, Origin.LocalHot(self, UUID.randomUUID()))
resolve(x) ! CMD_ADD_HTLC(self, MilliSatoshi(amount.toInt), ByteVector32.fromValidHex(rhash), CltvExpiry(144), TestConstants.emptyOnionPacket, None, Origin.Hot(self, Upstream.Local(UUID.randomUUID())))
exec(rest, a, b)
case fulfill(x, id, r) :: rest =>
resolve(x) ! CMD_FULFILL_HTLC(id.toInt, ByteVector32.fromValidHex(r))

View File

@ -262,16 +262,20 @@ class JsonSerializersSpec extends TestKitBaseClass with AnyFunSuiteLike with Mat
}
test("HTLC origin serialization") {
val localOrigin = Origin.LocalCold(UUID.fromString("11111111-1111-1111-1111-111111111111"))
val localOrigin = Origin.Cold(Upstream.Local(UUID.fromString("11111111-1111-1111-1111-111111111111")))
val expectedLocalOrigin = """{"paymentId":"11111111-1111-1111-1111-111111111111"}"""
JsonSerializers.serialization.write(localOrigin)(org.json4s.DefaultFormats + OriginSerializer) shouldBe expectedLocalOrigin
val channelOrigin = Origin.ChannelRelayedCold(ByteVector32(hex"345b2b05ec046ffe0c14d3b61838c79980713ad1cf8ae7a45c172ce90c9c0b9f"), 7, 500 msat, 400 msat)
val expectedChannelOrigin = """{"channelId":"345b2b05ec046ffe0c14d3b61838c79980713ad1cf8ae7a45c172ce90c9c0b9f","htlcId":7}"""
val channelOrigin = Origin.Cold(Upstream.Cold.Channel(ByteVector32(hex"345b2b05ec046ffe0c14d3b61838c79980713ad1cf8ae7a45c172ce90c9c0b9f"), 7, 500 msat))
val expectedChannelOrigin = """{"channelId":"345b2b05ec046ffe0c14d3b61838c79980713ad1cf8ae7a45c172ce90c9c0b9f","htlcId":7,"amount":500}"""
JsonSerializers.serialization.write(channelOrigin)(org.json4s.DefaultFormats + OriginSerializer) shouldBe expectedChannelOrigin
val trampolineOrigin = Origin.TrampolineRelayedCold((ByteVector32(hex"9fcd45bbaa09c60c991ac0425704163c3f3d2d683c789fa409455b9c97792692"), 3L) :: (ByteVector32(hex"70685ca81a8e4d4d01beec5781f4cc924684072ae52c507f8ebe9daf0caaab7b"), 7L) :: Nil)
val expectedTrampolineOrigin = """[{"channelId":"9fcd45bbaa09c60c991ac0425704163c3f3d2d683c789fa409455b9c97792692","htlcId":3},{"channelId":"70685ca81a8e4d4d01beec5781f4cc924684072ae52c507f8ebe9daf0caaab7b","htlcId":7}]"""
val relayedHtlcs = List(
Upstream.Cold.Channel(ByteVector32(hex"9fcd45bbaa09c60c991ac0425704163c3f3d2d683c789fa409455b9c97792692"), 3, 600 msat),
Upstream.Cold.Channel(ByteVector32(hex"70685ca81a8e4d4d01beec5781f4cc924684072ae52c507f8ebe9daf0caaab7b"), 7, 500 msat),
)
val trampolineOrigin = Origin.Cold(Upstream.Cold.Trampoline(relayedHtlcs))
val expectedTrampolineOrigin = """[{"channelId":"9fcd45bbaa09c60c991ac0425704163c3f3d2d683c789fa409455b9c97792692","htlcId":3,"amount":600},{"channelId":"70685ca81a8e4d4d01beec5781f4cc924684072ae52c507f8ebe9daf0caaab7b","htlcId":7,"amount":500}]"""
JsonSerializers.serialization.write(trampolineOrigin)(org.json4s.DefaultFormats + OriginSerializer) shouldBe expectedTrampolineOrigin
}

View File

@ -21,11 +21,10 @@ import akka.testkit.{TestFSMRef, TestProbe}
import fr.acinq.bitcoin.scalacompat.Crypto.PublicKey
import fr.acinq.bitcoin.scalacompat.{Block, ByteVector32, Crypto, SatoshiLong}
import fr.acinq.eclair._
import fr.acinq.eclair.channel.{ChannelUnavailable, HtlcsTimedoutDownstream, RemoteCannotAffordFeesForNewHtlc}
import fr.acinq.eclair.channel.{ChannelUnavailable, HtlcsTimedoutDownstream, RemoteCannotAffordFeesForNewHtlc, Upstream}
import fr.acinq.eclair.crypto.Sphinx
import fr.acinq.eclair.db.{FailureSummary, FailureType, OutgoingPaymentStatus}
import fr.acinq.eclair.payment.Invoice.ExtraEdge
import fr.acinq.eclair.payment.OutgoingPaymentPacket.Upstream
import fr.acinq.eclair.payment.relay.Relayer.RelayFees
import fr.acinq.eclair.payment.send.MultiPartPaymentLifecycle._
import fr.acinq.eclair.payment.send.PaymentError.RetryExhausted

View File

@ -23,10 +23,10 @@ import fr.acinq.bitcoin.scalacompat.Crypto.PrivateKey
import fr.acinq.eclair.FeatureSupport.{Mandatory, Optional}
import fr.acinq.eclair.Features._
import fr.acinq.eclair.UInt64.Conversions._
import fr.acinq.eclair.channel.Upstream
import fr.acinq.eclair.channel.fsm.Channel
import fr.acinq.eclair.crypto.Sphinx
import fr.acinq.eclair.payment.Bolt11Invoice.ExtraHop
import fr.acinq.eclair.payment.OutgoingPaymentPacket.Upstream
import fr.acinq.eclair.payment.PaymentPacketSpec._
import fr.acinq.eclair.payment.PaymentSent.PartialPayment
import fr.acinq.eclair.payment.send.BlindedPathsResolver.{FullBlindedRoute, ResolvedPath}

View File

@ -32,7 +32,6 @@ import fr.acinq.eclair.crypto.Sphinx
import fr.acinq.eclair.db.{OutgoingPayment, OutgoingPaymentStatus, PaymentType}
import fr.acinq.eclair.io.Peer.PeerRoutingMessage
import fr.acinq.eclair.payment.Invoice.ExtraEdge
import fr.acinq.eclair.payment.OutgoingPaymentPacket.Upstream
import fr.acinq.eclair.payment.PaymentSent.PartialPayment
import fr.acinq.eclair.payment.relay.Relayer.RelayFees
import fr.acinq.eclair.payment.send.PaymentInitiator.SendPaymentConfig
@ -61,7 +60,7 @@ class PaymentLifecycleSpec extends BaseRouterSpec {
val defaultRouteExpiry = CltvExpiry(100_000)
val defaultPaymentPreimage = randomBytes32()
val defaultPaymentHash = Crypto.sha256(defaultPaymentPreimage)
val defaultOrigin = Origin.LocalCold(UUID.randomUUID())
val defaultOrigin = Origin.Cold(Upstream.Local(UUID.randomUUID()))
val defaultExternalId = UUID.randomUUID().toString
val defaultInvoice = Bolt11Invoice(Block.RegtestGenesisBlock.hash, None, defaultPaymentHash, priv_d, Left("test"), Channel.MIN_CLTV_EXPIRY_DELTA)
val defaultRecipient = ClearRecipient(defaultInvoice, defaultAmountMsat, defaultExpiry, Set.empty)

View File

@ -43,7 +43,6 @@ import org.scalatest.BeforeAndAfterAll
import org.scalatest.funsuite.AnyFunSuite
import scodec.bits.{ByteVector, HexStringSyntax}
import java.util.UUID
import scala.concurrent.duration._
import scala.util.Success
@ -69,7 +68,7 @@ class PaymentPacketSpec extends AnyFunSuite with BeforeAndAfterAll {
def testBuildOutgoingPayment(): Unit = {
val recipient = ClearRecipient(e, Features.empty, finalAmount, finalExpiry, paymentSecret)
val Right(payment) = buildOutgoingPayment(ActorRef.noSender, Upstream.Local(UUID.randomUUID()), paymentHash, Route(finalAmount, hops, None), recipient)
val Right(payment) = buildOutgoingPayment(TestConstants.emptyOrigin, paymentHash, Route(finalAmount, hops, None), recipient)
assert(payment.outgoingChannel == channelUpdate_ab.shortChannelId)
assert(payment.cmd.amount == amount_ab)
assert(payment.cmd.cltvExpiry == expiry_ab)
@ -127,7 +126,7 @@ class PaymentPacketSpec extends AnyFunSuite with BeforeAndAfterAll {
test("build outgoing payment for direct peer") {
val recipient = ClearRecipient(b, Features.empty, finalAmount, finalExpiry, paymentSecret, paymentMetadata_opt = Some(paymentMetadata))
val route = Route(finalAmount, hops.take(1), None)
val Right(payment) = buildOutgoingPayment(ActorRef.noSender, Upstream.Local(UUID.randomUUID()), paymentHash, route, recipient)
val Right(payment) = buildOutgoingPayment(TestConstants.emptyOrigin, paymentHash, route, recipient)
assert(payment.cmd.amount == finalAmount)
assert(payment.cmd.cltvExpiry == finalExpiry)
assert(payment.cmd.paymentHash == paymentHash)
@ -148,7 +147,7 @@ class PaymentPacketSpec extends AnyFunSuite with BeforeAndAfterAll {
test("build outgoing payment with greater amount and expiry") {
val recipient = ClearRecipient(b, Features.empty, finalAmount, finalExpiry, paymentSecret, paymentMetadata_opt = Some(paymentMetadata))
val route = Route(finalAmount, hops.take(1), None)
val Right(payment) = buildOutgoingPayment(ActorRef.noSender, Upstream.Local(UUID.randomUUID()), paymentHash, route, recipient)
val Right(payment) = buildOutgoingPayment(TestConstants.emptyOrigin, paymentHash, route, recipient)
// let's peel the onion
val add_b = UpdateAddHtlc(randomBytes32(), 0, finalAmount + 100.msat, paymentHash, finalExpiry + CltvExpiryDelta(6), payment.cmd.onion, None)
@ -165,7 +164,7 @@ class PaymentPacketSpec extends AnyFunSuite with BeforeAndAfterAll {
assert(recipient.extraEdges.length == 1)
assert(recipient.extraEdges.head.sourceNodeId == c)
assert(recipient.extraEdges.head.targetNodeId == invoice.nodeId)
val Right(payment) = buildOutgoingPayment(ActorRef.noSender, Upstream.Local(UUID.randomUUID()), paymentHash, route, recipient)
val Right(payment) = buildOutgoingPayment(TestConstants.emptyOrigin, paymentHash, route, recipient)
assert(payment.outgoingChannel == channelUpdate_ab.shortChannelId)
assert(payment.cmd.amount >= amount_ab)
assert(payment.cmd.cltvExpiry == expiry_ab)
@ -228,7 +227,7 @@ class PaymentPacketSpec extends AnyFunSuite with BeforeAndAfterAll {
})
val recipient = BlindedRecipient(invoice, resolvedPaths, amount_bc, expiry_bc, Set.empty)
val hops = Seq(channelHopFromUpdate(a, b, channelUpdate_ab), channelHopFromUpdate(b, c, channelUpdate_bc))
val Right(payment) = buildOutgoingPayment(ActorRef.noSender, Upstream.Local(UUID.randomUUID()), paymentHash, Route(amount_bc, hops, Some(recipient.blindedHops.head)), recipient)
val Right(payment) = buildOutgoingPayment(TestConstants.emptyOrigin, paymentHash, Route(amount_bc, hops, Some(recipient.blindedHops.head)), recipient)
assert(payment.outgoingChannel == channelUpdate_ab.shortChannelId)
assert(payment.cmd.amount == amount_ab)
assert(payment.cmd.cltvExpiry == expiry_ab)
@ -289,7 +288,7 @@ class PaymentPacketSpec extends AnyFunSuite with BeforeAndAfterAll {
val recipient = TrampolineRecipient(invoice, finalAmount, finalExpiry, trampolineHop, randomBytes32())
assert(recipient.trampolineAmount == amount_bc)
assert(recipient.trampolineExpiry == expiry_bc)
val Right(payment) = buildOutgoingPayment(ActorRef.noSender, Upstream.Local(UUID.randomUUID()), paymentHash, Route(recipient.trampolineAmount, trampolineChannelHops, Some(trampolineHop)), recipient)
val Right(payment) = buildOutgoingPayment(TestConstants.emptyOrigin, paymentHash, Route(recipient.trampolineAmount, trampolineChannelHops, Some(trampolineHop)), recipient)
assert(payment.outgoingChannel == channelUpdate_ab.shortChannelId)
assert(payment.cmd.amount == amount_ab)
assert(payment.cmd.cltvExpiry == expiry_ab)
@ -316,7 +315,7 @@ class PaymentPacketSpec extends AnyFunSuite with BeforeAndAfterAll {
// c forwards the trampoline payment to e through d.
val recipient_e = ClearRecipient(e, Features.empty, inner_c.amountToForward, inner_c.outgoingCltv, randomBytes32(), nextTrampolineOnion_opt = Some(trampolinePacket_e))
val Right(payment_e) = buildOutgoingPayment(ActorRef.noSender, Upstream.Trampoline(Seq(Upstream.ReceivedHtlc(add_c, TimestampMilli(1687345927000L)))), paymentHash, Route(inner_c.amountToForward, afterTrampolineChannelHops, None), recipient_e)
val Right(payment_e) = buildOutgoingPayment(Origin.Hot(ActorRef.noSender, Upstream.Hot.Trampoline(Seq(Upstream.Hot.Channel(add_c, TimestampMilli(1687345927000L))))), paymentHash, Route(inner_c.amountToForward, afterTrampolineChannelHops, None), recipient_e)
assert(payment_e.outgoingChannel == channelUpdate_cd.shortChannelId)
assert(payment_e.cmd.amount == amount_cd)
assert(payment_e.cmd.cltvExpiry == expiry_cd)
@ -342,7 +341,7 @@ class PaymentPacketSpec extends AnyFunSuite with BeforeAndAfterAll {
val recipient = TrampolineRecipient(invoice, finalAmount, finalExpiry, trampolineHop, randomBytes32())
assert(recipient.trampolineAmount == amount_bc)
assert(recipient.trampolineExpiry == expiry_bc)
val Right(payment) = buildOutgoingPayment(ActorRef.noSender, Upstream.Local(UUID.randomUUID()), paymentHash, Route(recipient.trampolineAmount, trampolineChannelHops, Some(trampolineHop)), recipient)
val Right(payment) = buildOutgoingPayment(TestConstants.emptyOrigin, paymentHash, Route(recipient.trampolineAmount, trampolineChannelHops, Some(trampolineHop)), recipient)
assert(payment.outgoingChannel == channelUpdate_ab.shortChannelId)
assert(payment.cmd.amount == amount_ab)
assert(payment.cmd.cltvExpiry == expiry_ab)
@ -368,7 +367,7 @@ class PaymentPacketSpec extends AnyFunSuite with BeforeAndAfterAll {
// c forwards the trampoline payment to e through d.
val recipient_e = ClearRecipient(e, Features.empty, inner_c.amountToForward, inner_c.outgoingCltv, inner_c.paymentSecret.get, invoice.extraEdges, inner_c.paymentMetadata)
val Right(payment_e) = buildOutgoingPayment(ActorRef.noSender, Upstream.Trampoline(Seq(Upstream.ReceivedHtlc(add_c, TimestampMilli(1687345927000L)))), paymentHash, Route(inner_c.amountToForward, afterTrampolineChannelHops, None), recipient_e)
val Right(payment_e) = buildOutgoingPayment(Origin.Hot(ActorRef.noSender, Upstream.Hot.Trampoline(Seq(Upstream.Hot.Channel(add_c, TimestampMilli(1687345927000L))))), paymentHash, Route(inner_c.amountToForward, afterTrampolineChannelHops, None), recipient_e)
assert(payment_e.outgoingChannel == channelUpdate_cd.shortChannelId)
assert(payment_e.cmd.amount == amount_cd)
assert(payment_e.cmd.cltvExpiry == expiry_cd)
@ -394,7 +393,7 @@ class PaymentPacketSpec extends AnyFunSuite with BeforeAndAfterAll {
val invoiceFeatures = Features[Bolt11Feature](VariableLengthOnion -> Mandatory, PaymentSecret -> Mandatory, BasicMultiPartPayment -> Optional)
val invoice = Bolt11Invoice(Block.RegtestGenesisBlock.hash, Some(finalAmount), paymentHash, priv_e.privateKey, Left("#reckless"), CltvExpiryDelta(18), extraHops = routingHints, features = invoiceFeatures, paymentMetadata = Some(paymentMetadata))
val recipient = TrampolineRecipient(invoice, finalAmount, finalExpiry, trampolineHop, randomBytes32())
val Right(payment) = buildOutgoingPayment(ActorRef.noSender, Upstream.Local(UUID.randomUUID()), paymentHash, Route(recipient.trampolineAmount, trampolineChannelHops, Some(trampolineHop)), recipient)
val Right(payment) = buildOutgoingPayment(TestConstants.emptyOrigin, paymentHash, Route(recipient.trampolineAmount, trampolineChannelHops, Some(trampolineHop)), recipient)
assert(payment.outgoingChannel == channelUpdate_ab.shortChannelId)
val add_b = UpdateAddHtlc(randomBytes32(), 1, payment.cmd.amount, paymentHash, payment.cmd.cltvExpiry, payment.cmd.onion, None)
@ -409,7 +408,7 @@ class PaymentPacketSpec extends AnyFunSuite with BeforeAndAfterAll {
// c forwards the trampoline payment to e through d.
val recipient_e = ClearRecipient(e, Features.empty, inner_c.amountToForward, inner_c.outgoingCltv, inner_c.paymentSecret.get, invoice.extraEdges, inner_c.paymentMetadata)
val Right(payment_e) = buildOutgoingPayment(ActorRef.noSender, Upstream.Trampoline(Seq(Upstream.ReceivedHtlc(add_c, TimestampMilli(1687345927000L)))), paymentHash, Route(inner_c.amountToForward, afterTrampolineChannelHops, None), recipient_e)
val Right(payment_e) = buildOutgoingPayment(Origin.Hot(ActorRef.noSender, Upstream.Hot.Trampoline(Seq(Upstream.Hot.Channel(add_c, TimestampMilli(1687345927000L))))), paymentHash, Route(inner_c.amountToForward, afterTrampolineChannelHops, None), recipient_e)
assert(payment_e.outgoingChannel == channelUpdate_cd.shortChannelId)
val add_d = UpdateAddHtlc(randomBytes32(), 3, payment_e.cmd.amount, paymentHash, payment_e.cmd.cltvExpiry, payment_e.cmd.onion, None)
val Right(ChannelRelayPacket(add_d2, payload_d, packet_e)) = decrypt(add_d, priv_d.privateKey, Features.empty)
@ -425,7 +424,7 @@ class PaymentPacketSpec extends AnyFunSuite with BeforeAndAfterAll {
test("fail to build outgoing payment with invalid route") {
val recipient = ClearRecipient(e, Features.empty, finalAmount, finalExpiry, paymentSecret)
val route = Route(finalAmount, hops.dropRight(1), None) // route doesn't reach e
val Left(failure) = buildOutgoingPayment(ActorRef.noSender, Upstream.Local(UUID.randomUUID()), paymentHash, route, recipient)
val Left(failure) = buildOutgoingPayment(TestConstants.emptyOrigin, paymentHash, route, recipient)
assert(failure == InvalidRouteRecipient(e, d))
}
@ -434,21 +433,21 @@ class PaymentPacketSpec extends AnyFunSuite with BeforeAndAfterAll {
val invoice = Bolt11Invoice(Block.RegtestGenesisBlock.hash, None, paymentHash, priv_e.privateKey, Left("invoice"), CltvExpiryDelta(6), paymentSecret = paymentSecret, features = invoiceFeatures)
val recipient = TrampolineRecipient(invoice, finalAmount, finalExpiry, trampolineHop, randomBytes32())
val route = Route(finalAmount, trampolineChannelHops, None) // missing trampoline hop
val Left(failure) = buildOutgoingPayment(ActorRef.noSender, Upstream.Local(UUID.randomUUID()), paymentHash, route, recipient)
val Left(failure) = buildOutgoingPayment(TestConstants.emptyOrigin, paymentHash, route, recipient)
assert(failure == MissingTrampolineHop(c))
}
test("fail to build outgoing blinded payment with invalid route") {
val (_, route, recipient) = longBlindedHops(hex"deadbeef")
assert(buildOutgoingPayment(ActorRef.noSender, Upstream.Local(UUID.randomUUID()), paymentHash, route, recipient).isRight)
assert(buildOutgoingPayment(TestConstants.emptyOrigin, paymentHash, route, recipient).isRight)
val routeMissingBlindedHop = route.copy(finalHop_opt = None)
val Left(failure) = buildOutgoingPayment(ActorRef.noSender, Upstream.Local(UUID.randomUUID()), paymentHash, routeMissingBlindedHop, recipient)
val Left(failure) = buildOutgoingPayment(TestConstants.emptyOrigin, paymentHash, routeMissingBlindedHop, recipient)
assert(failure == MissingBlindedHop(Set(c)))
}
test("fail to decrypt when the onion is invalid") {
val recipient = ClearRecipient(e, Features.empty, finalAmount, finalExpiry, paymentSecret)
val Right(payment) = buildOutgoingPayment(ActorRef.noSender, Upstream.Local(UUID.randomUUID()), paymentHash, Route(finalAmount, hops, None), recipient)
val Right(payment) = buildOutgoingPayment(TestConstants.emptyOrigin, paymentHash, Route(finalAmount, hops, None), recipient)
val add = UpdateAddHtlc(randomBytes32(), 1, payment.cmd.amount, paymentHash, payment.cmd.cltvExpiry, payment.cmd.onion.copy(payload = payment.cmd.onion.payload.reverse), None)
val Left(failure) = decrypt(add, priv_b.privateKey, Features.empty)
assert(failure.isInstanceOf[InvalidOnionHmac])
@ -458,7 +457,7 @@ class PaymentPacketSpec extends AnyFunSuite with BeforeAndAfterAll {
val invoiceFeatures = Features[Bolt11Feature](VariableLengthOnion -> Mandatory, PaymentSecret -> Mandatory, BasicMultiPartPayment -> Optional, PaymentMetadata -> Optional, TrampolinePaymentPrototype -> Optional)
val invoice = Bolt11Invoice(Block.RegtestGenesisBlock.hash, None, paymentHash, priv_e.privateKey, Left("invoice"), CltvExpiryDelta(6), paymentSecret = paymentSecret, features = invoiceFeatures, paymentMetadata = Some(hex"010203"))
val recipient = TrampolineRecipient(invoice, finalAmount, finalExpiry, trampolineHop, randomBytes32())
val Right(payment) = buildOutgoingPayment(ActorRef.noSender, Upstream.Local(UUID.randomUUID()), paymentHash, Route(recipient.trampolineAmount, trampolineChannelHops, Some(trampolineHop)), recipient)
val Right(payment) = buildOutgoingPayment(TestConstants.emptyOrigin, paymentHash, Route(recipient.trampolineAmount, trampolineChannelHops, Some(trampolineHop)), recipient)
val add_b = UpdateAddHtlc(randomBytes32(), 1, payment.cmd.amount, paymentHash, payment.cmd.cltvExpiry, payment.cmd.onion, None)
val Right(ChannelRelayPacket(_, _, packet_c)) = decrypt(add_b, priv_b.privateKey, Features.empty)
@ -468,7 +467,7 @@ class PaymentPacketSpec extends AnyFunSuite with BeforeAndAfterAll {
// c forwards an invalid trampoline onion to e through d.
val recipient_e = ClearRecipient(e, Features.empty, inner_c.amountToForward, inner_c.outgoingCltv, randomBytes32(), nextTrampolineOnion_opt = Some(trampolinePacket_e.copy(payload = trampolinePacket_e.payload.reverse)))
val Right(payment_e) = buildOutgoingPayment(ActorRef.noSender, Upstream.Trampoline(Seq(Upstream.ReceivedHtlc(add_c, TimestampMilli(1687345927000L)))), paymentHash, Route(inner_c.amountToForward, afterTrampolineChannelHops, None), recipient_e)
val Right(payment_e) = buildOutgoingPayment(Origin.Hot(ActorRef.noSender, Upstream.Hot.Trampoline(Seq(Upstream.Hot.Channel(add_c, TimestampMilli(1687345927000L))))), paymentHash, Route(inner_c.amountToForward, afterTrampolineChannelHops, None), recipient_e)
assert(payment_e.outgoingChannel == channelUpdate_cd.shortChannelId)
val add_d = UpdateAddHtlc(randomBytes32(), 3, payment_e.cmd.amount, paymentHash, payment_e.cmd.cltvExpiry, payment_e.cmd.onion, None)
val Right(ChannelRelayPacket(_, _, packet_e)) = decrypt(add_d, priv_d.privateKey, Features.empty)
@ -480,7 +479,7 @@ class PaymentPacketSpec extends AnyFunSuite with BeforeAndAfterAll {
test("fail to decrypt when payment hash doesn't match associated data") {
val recipient = ClearRecipient(e, Features.empty, finalAmount, finalExpiry, paymentSecret)
val Right(payment) = buildOutgoingPayment(ActorRef.noSender, Upstream.Local(UUID.randomUUID()), paymentHash.reverse, Route(finalAmount, hops, None), recipient)
val Right(payment) = buildOutgoingPayment(TestConstants.emptyOrigin, paymentHash.reverse, Route(finalAmount, hops, None), recipient)
val add = UpdateAddHtlc(randomBytes32(), 1, payment.cmd.amount, paymentHash, payment.cmd.cltvExpiry, payment.cmd.onion, None)
val Left(failure) = decrypt(add, priv_b.privateKey, Features.empty)
assert(failure.isInstanceOf[InvalidOnionHmac])
@ -504,7 +503,7 @@ class PaymentPacketSpec extends AnyFunSuite with BeforeAndAfterAll {
val route = Route(amount_bc, Seq(channelHopFromUpdate(a, b, channelUpdate_ab)), Some(recipient.blindedHops.head))
(route, recipient)
}
val Right(payment) = buildOutgoingPayment(ActorRef.noSender, Upstream.Local(UUID.randomUUID()), paymentHash, route, recipient)
val Right(payment) = buildOutgoingPayment(TestConstants.emptyOrigin, paymentHash, route, recipient)
assert(payment.outgoingChannel == channelUpdate_ab.shortChannelId)
assert(payment.cmd.amount == amount_bc + fee_b)
@ -515,7 +514,7 @@ class PaymentPacketSpec extends AnyFunSuite with BeforeAndAfterAll {
test("fail to decrypt blinded payment when route blinding is disabled") {
val (route, recipient) = shortBlindedHops()
val Right(payment) = buildOutgoingPayment(ActorRef.noSender, Upstream.Local(UUID.randomUUID()), paymentHash, route, recipient)
val Right(payment) = buildOutgoingPayment(TestConstants.emptyOrigin, paymentHash, route, recipient)
val add_d = UpdateAddHtlc(randomBytes32(), 0, payment.cmd.amount, payment.cmd.paymentHash, payment.cmd.cltvExpiry, payment.cmd.onion, payment.cmd.nextBlindingKey_opt)
val Left(failure) = decrypt(add_d, priv_d.privateKey, Features.empty) // d doesn't support route blinding
assert(failure == InvalidOnionPayload(UInt64(10), 0))
@ -524,7 +523,7 @@ class PaymentPacketSpec extends AnyFunSuite with BeforeAndAfterAll {
test("fail to decrypt at the final node when amount has been modified by next-to-last node") {
val recipient = ClearRecipient(b, Features.empty, finalAmount, finalExpiry, paymentSecret)
val route = Route(finalAmount, hops.take(1), None)
val Right(payment) = buildOutgoingPayment(ActorRef.noSender, Upstream.Local(UUID.randomUUID()), paymentHash, route, recipient)
val Right(payment) = buildOutgoingPayment(TestConstants.emptyOrigin, paymentHash, route, recipient)
val add = UpdateAddHtlc(randomBytes32(), 1, payment.cmd.amount - 100.msat, paymentHash, payment.cmd.cltvExpiry, payment.cmd.onion, None)
val Left(failure) = decrypt(add, priv_b.privateKey, Features.empty)
assert(failure == FinalIncorrectHtlcAmount(payment.cmd.amount - 100.msat))
@ -533,7 +532,7 @@ class PaymentPacketSpec extends AnyFunSuite with BeforeAndAfterAll {
test("fail to decrypt at the final node when expiry has been modified by next-to-last node") {
val recipient = ClearRecipient(b, Features.empty, finalAmount, finalExpiry, paymentSecret)
val route = Route(finalAmount, hops.take(1), None)
val Right(payment) = buildOutgoingPayment(ActorRef.noSender, Upstream.Local(UUID.randomUUID()), paymentHash, route, recipient)
val Right(payment) = buildOutgoingPayment(TestConstants.emptyOrigin, paymentHash, route, recipient)
val add = UpdateAddHtlc(randomBytes32(), 1, payment.cmd.amount, paymentHash, payment.cmd.cltvExpiry - CltvExpiryDelta(12), payment.cmd.onion, None)
val Left(failure) = decrypt(add, priv_b.privateKey, Features.empty)
assert(failure == FinalIncorrectCltvExpiry(payment.cmd.cltvExpiry - CltvExpiryDelta(12)))
@ -541,7 +540,7 @@ class PaymentPacketSpec extends AnyFunSuite with BeforeAndAfterAll {
test("fail to decrypt blinded payment at the final node when amount is too low") {
val (route, recipient) = shortBlindedHops()
val Right(payment) = buildOutgoingPayment(ActorRef.noSender, Upstream.Local(UUID.randomUUID()), paymentHash, route, recipient)
val Right(payment) = buildOutgoingPayment(TestConstants.emptyOrigin, paymentHash, route, recipient)
assert(payment.outgoingChannel == channelUpdate_cd.shortChannelId)
assert(payment.cmd.amount == amount_cd)
@ -561,7 +560,7 @@ class PaymentPacketSpec extends AnyFunSuite with BeforeAndAfterAll {
test("fail to decrypt blinded payment at the final node when expiry is too low") {
val (route, recipient) = shortBlindedHops()
val Right(payment) = buildOutgoingPayment(ActorRef.noSender, Upstream.Local(UUID.randomUUID()), paymentHash, route, recipient)
val Right(payment) = buildOutgoingPayment(TestConstants.emptyOrigin, paymentHash, route, recipient)
assert(payment.outgoingChannel == channelUpdate_cd.shortChannelId)
assert(payment.cmd.cltvExpiry == expiry_cd)
@ -584,7 +583,7 @@ class PaymentPacketSpec extends AnyFunSuite with BeforeAndAfterAll {
test("fail to decrypt blinded payment at intermediate node when expiry is too high") {
val routeExpiry = expiry_de - channelUpdate_de.cltvExpiryDelta
val (route, recipient) = shortBlindedHops(routeExpiry)
val Right(payment) = buildOutgoingPayment(ActorRef.noSender, Upstream.Local(UUID.randomUUID()), paymentHash, route, recipient)
val Right(payment) = buildOutgoingPayment(TestConstants.emptyOrigin, paymentHash, route, recipient)
assert(payment.outgoingChannel == channelUpdate_cd.shortChannelId)
assert(payment.cmd.cltvExpiry > expiry_de)
@ -603,7 +602,7 @@ class PaymentPacketSpec extends AnyFunSuite with BeforeAndAfterAll {
val invoiceFeatures = Features[Bolt11Feature](VariableLengthOnion -> Mandatory, PaymentSecret -> Mandatory, BasicMultiPartPayment -> Optional, TrampolinePaymentPrototype -> Optional)
val invoice = Bolt11Invoice(Block.RegtestGenesisBlock.hash, None, paymentHash, priv_e.privateKey, Left("invoice"), CltvExpiryDelta(6), paymentSecret = paymentSecret, features = invoiceFeatures)
val recipient = TrampolineRecipient(invoice, finalAmount, finalExpiry, trampolineHop, randomBytes32())
val Right(payment) = buildOutgoingPayment(ActorRef.noSender, Upstream.Local(UUID.randomUUID()), paymentHash, Route(recipient.trampolineAmount, trampolineChannelHops, Some(trampolineHop)), recipient)
val Right(payment) = buildOutgoingPayment(TestConstants.emptyOrigin, paymentHash, Route(recipient.trampolineAmount, trampolineChannelHops, Some(trampolineHop)), recipient)
val add_b = UpdateAddHtlc(randomBytes32(), 1, payment.cmd.amount, paymentHash, payment.cmd.cltvExpiry, payment.cmd.onion, None)
val Right(ChannelRelayPacket(_, _, packet_c)) = decrypt(add_b, priv_b.privateKey, Features.empty)
@ -618,7 +617,7 @@ class PaymentPacketSpec extends AnyFunSuite with BeforeAndAfterAll {
// c forwards an invalid amount to e through (the outer total amount doesn't match the inner amount).
val invalidTotalAmount = inner_c.amountToForward - 1.msat
val recipient_e = ClearRecipient(e, Features.empty, invalidTotalAmount, inner_c.outgoingCltv, randomBytes32(), nextTrampolineOnion_opt = Some(trampolinePacket_e))
val Right(payment_e) = buildOutgoingPayment(ActorRef.noSender, Upstream.Trampoline(Seq(Upstream.ReceivedHtlc(add_c, TimestampMilli(1687345927000L)))), paymentHash, Route(invalidTotalAmount, afterTrampolineChannelHops, None), recipient_e)
val Right(payment_e) = buildOutgoingPayment(Origin.Hot(ActorRef.noSender, Upstream.Hot.Trampoline(Seq(Upstream.Hot.Channel(add_c, TimestampMilli(1687345927000L))))), paymentHash, Route(invalidTotalAmount, afterTrampolineChannelHops, None), recipient_e)
val add_d = UpdateAddHtlc(randomBytes32(), 3, payment_e.cmd.amount, paymentHash, payment_e.cmd.cltvExpiry, payment_e.cmd.onion, None)
val Right(ChannelRelayPacket(_, payload_d, packet_e)) = decrypt(add_d, priv_d.privateKey, Features.empty)
@ -634,7 +633,7 @@ class PaymentPacketSpec extends AnyFunSuite with BeforeAndAfterAll {
// c forwards an invalid amount to e through (the outer expiry doesn't match the inner expiry).
val invalidExpiry = inner_c.outgoingCltv - CltvExpiryDelta(12)
val recipient_e = ClearRecipient(e, Features.empty, inner_c.amountToForward, invalidExpiry, randomBytes32(), nextTrampolineOnion_opt = Some(trampolinePacket_e))
val Right(payment_e) = buildOutgoingPayment(ActorRef.noSender, Upstream.Trampoline(Seq(Upstream.ReceivedHtlc(add_c, TimestampMilli(1687345927000L)))), paymentHash, Route(inner_c.amountToForward, afterTrampolineChannelHops, None), recipient_e)
val Right(payment_e) = buildOutgoingPayment(Origin.Hot(ActorRef.noSender, Upstream.Hot.Trampoline(Seq(Upstream.Hot.Channel(add_c, TimestampMilli(1687345927000L))))), paymentHash, Route(inner_c.amountToForward, afterTrampolineChannelHops, None), recipient_e)
val add_d = UpdateAddHtlc(randomBytes32(), 3, payment_e.cmd.amount, paymentHash, payment_e.cmd.cltvExpiry, payment_e.cmd.onion, None)
val Right(ChannelRelayPacket(_, payload_d, packet_e)) = decrypt(add_d, priv_d.privateKey, Features.empty)
@ -661,7 +660,7 @@ class PaymentPacketSpec extends AnyFunSuite with BeforeAndAfterAll {
test("build htlc failure onion") {
// a -> b -> c -> d -> e
val recipient = ClearRecipient(e, Features.empty, finalAmount, finalExpiry, paymentSecret)
val Right(payment) = buildOutgoingPayment(ActorRef.noSender, Upstream.Local(UUID.randomUUID()), paymentHash, Route(finalAmount, hops, None), recipient)
val Right(payment) = buildOutgoingPayment(TestConstants.emptyOrigin, paymentHash, Route(finalAmount, hops, None), recipient)
val add_b = UpdateAddHtlc(randomBytes32(), 0, amount_ab, paymentHash, expiry_ab, payment.cmd.onion, None)
val Right(ChannelRelayPacket(_, _, packet_c)) = decrypt(add_b, priv_b.privateKey, Features.empty)
val add_c = UpdateAddHtlc(randomBytes32(), 1, amount_bc, paymentHash, expiry_bc, packet_c, None)
@ -690,7 +689,7 @@ class PaymentPacketSpec extends AnyFunSuite with BeforeAndAfterAll {
test("build htlc failure onion (blinded payment)") {
// a -> b -> c -> d -> e, blinded after c
val (_, route, recipient) = longBlindedHops(hex"0451")
val Right(payment) = buildOutgoingPayment(ActorRef.noSender, Upstream.Local(UUID.randomUUID()), paymentHash, route, recipient)
val Right(payment) = buildOutgoingPayment(TestConstants.emptyOrigin, paymentHash, route, recipient)
val add_b = UpdateAddHtlc(randomBytes32(), 0, payment.cmd.amount, payment.cmd.paymentHash, payment.cmd.cltvExpiry, payment.cmd.onion, payment.cmd.nextBlindingKey_opt)
val Right(ChannelRelayPacket(_, _, packet_c)) = decrypt(add_b, priv_b.privateKey, Features.empty)
val add_c = UpdateAddHtlc(randomBytes32(), 1, amount_bc, paymentHash, expiry_bc, packet_c, None)
@ -797,7 +796,7 @@ object PaymentPacketSpec {
val blindedRoute = BlindedRouteCreation.createBlindedRouteWithoutHops(b, hex"deadbeef", 1.msat, routeExpiry).route
val finalPayload = NodePayload(blindedRoute.introductionNode.blindedPublicKey, OutgoingBlindedPerHopPayload.createFinalPayload(finalAmount, finalAmount, finalExpiry, blindedRoute.introductionNode.encryptedPayload))
val onion = buildOnion(Seq(finalPayload), paymentHash, Some(PaymentOnionCodecs.paymentOnionPayloadLength)).toOption.get // BOLT 2 requires that associatedData == paymentHash
val cmd = CMD_ADD_HTLC(ActorRef.noSender, finalAmount, paymentHash, finalExpiry, onion.packet, Some(blindedRoute.blindingKey), Origin.Hot(ActorRef.noSender, Upstream.Local(UUID.randomUUID())), commit = true)
val cmd = CMD_ADD_HTLC(ActorRef.noSender, finalAmount, paymentHash, finalExpiry, onion.packet, Some(blindedRoute.blindingKey), TestConstants.emptyOrigin, commit = true)
Right(OutgoingPaymentPacket(cmd, channelUpdate_ab.shortChannelId, onion.sharedSecrets))
}

View File

@ -28,7 +28,7 @@ import fr.acinq.eclair.channel.Helpers.Closing
import fr.acinq.eclair.channel._
import fr.acinq.eclair.channel.states.ChannelStateTestsBase
import fr.acinq.eclair.db.{OutgoingPayment, OutgoingPaymentStatus, PaymentType}
import fr.acinq.eclair.payment.OutgoingPaymentPacket.{Upstream, buildOutgoingPayment}
import fr.acinq.eclair.payment.OutgoingPaymentPacket.buildOutgoingPayment
import fr.acinq.eclair.payment.PaymentPacketSpec._
import fr.acinq.eclair.payment.relay.{PostRestartHtlcCleaner, Relayer}
import fr.acinq.eclair.payment.send.SpontaneousRecipient
@ -89,9 +89,9 @@ class PostRestartHtlcCleanerSpec extends TestKitBaseClass with FixtureAnyFunSuit
// (channel AB2)
val relayedPaymentHash = randomBytes32()
val relayed = Origin.ChannelRelayedCold(channelId_ab_1, 5, 10 msat, 10 msat)
val relayed = Origin.Cold(Upstream.Cold.Channel(channelId_ab_1, 5, 10 msat))
val trampolineRelayedPaymentHash = randomBytes32()
val trampolineRelayed = Origin.TrampolineRelayedCold((channelId_ab_1, 0L) :: (channelId_ab_2, 2L) :: Nil)
val trampolineRelayed = Origin.Cold(Upstream.Cold.Trampoline(Upstream.Cold.Channel(channelId_ab_1, 0, 1000 msat) :: Upstream.Cold.Channel(channelId_ab_2, 2, 2500 msat) :: Nil))
val htlc_ab_1 = Seq(
buildHtlcIn(0, channelId_ab_1, trampolineRelayedPaymentHash),
@ -307,8 +307,8 @@ class PostRestartHtlcCleanerSpec extends TestKitBaseClass with FixtureAnyFunSuit
val brokenHtlcs = probe.expectMsgType[PostRestartHtlcCleaner.BrokenHtlcs]
assert(brokenHtlcs.notRelayed.map(htlc => (htlc.add.id, htlc.add.channelId)).toSet == testCase.notRelayed)
assert(brokenHtlcs.relayedOut == Map(
testCase.origin_1 -> Set(testCase.downstream_1_1).map(htlc => (htlc.channelId, htlc.id)),
testCase.origin_2 -> Set(testCase.downstream_2_1, testCase.downstream_2_2, testCase.downstream_2_3).map(htlc => (htlc.channelId, htlc.id))
Origin.Cold(testCase.upstream_1) -> Set(testCase.downstream_1_1).map(htlc => (htlc.channelId, htlc.id)),
Origin.Cold(testCase.upstream_2) -> Set(testCase.downstream_2_1, testCase.downstream_2_2, testCase.downstream_2_3).map(htlc => (htlc.channelId, htlc.id))
))
}
@ -337,9 +337,9 @@ class PostRestartHtlcCleanerSpec extends TestKitBaseClass with FixtureAnyFunSuit
val htlc_upstream_1 = Seq(buildHtlcIn(0, channelId_ab_1, paymentHash1), buildHtlcIn(5, channelId_ab_1, paymentHash2))
val htlc_upstream_2 = Seq(buildHtlcIn(7, channelId_ab_2, paymentHash1), buildHtlcIn(9, channelId_ab_2, paymentHash2))
val htlc_upstream_3 = Seq(buildHtlcIn(11, randomBytes32(), paymentHash3))
val upstream_1 = Upstream.Trampoline(Upstream.ReceivedHtlc(htlc_upstream_1.head.add, TimestampMilli(1687345927000L)) :: Upstream.ReceivedHtlc(htlc_upstream_2.head.add, TimestampMilli(1687345967000L)) :: Nil)
val upstream_2 = Upstream.Trampoline(Upstream.ReceivedHtlc(htlc_upstream_1(1).add, TimestampMilli(1687345902000L)) :: Upstream.ReceivedHtlc(htlc_upstream_2(1).add, TimestampMilli(1687345999000L)) :: Nil)
val upstream_3 = Upstream.Trampoline(Upstream.ReceivedHtlc(htlc_upstream_3.head.add, TimestampMilli(1687345980000L)) :: Nil)
val upstream_1 = Upstream.Hot.Trampoline(Upstream.Hot.Channel(htlc_upstream_1.head.add, TimestampMilli(1687345927000L)) :: Upstream.Hot.Channel(htlc_upstream_2.head.add, TimestampMilli(1687345967000L)) :: Nil)
val upstream_2 = Upstream.Hot.Trampoline(Upstream.Hot.Channel(htlc_upstream_1(1).add, TimestampMilli(1687345902000L)) :: Upstream.Hot.Channel(htlc_upstream_2(1).add, TimestampMilli(1687345999000L)) :: Nil)
val upstream_3 = Upstream.Hot.Trampoline(Upstream.Hot.Channel(htlc_upstream_3.head.add, TimestampMilli(1687345980000L)) :: Nil)
val data_upstream_1 = ChannelCodecsSpec.makeChannelDataNormal(htlc_upstream_1, Map.empty)
val data_upstream_2 = ChannelCodecsSpec.makeChannelDataNormal(htlc_upstream_2, Map.empty)
val data_upstream_3 = ChannelCodecsSpec.makeChannelDataNormal(htlc_upstream_3, Map.empty)
@ -416,7 +416,7 @@ class PostRestartHtlcCleanerSpec extends TestKitBaseClass with FixtureAnyFunSuit
channel_upstream_2.expectNoMessage(100 millis)
// Payment 2 should fulfill once we receive the preimage.
val origin_2 = Origin.TrampolineRelayedCold(upstream_2.adds.map(r => (r.add.channelId, r.add.id)).toList)
val origin_2 = Origin.Cold(Upstream.Cold(upstream_2))
sender.send(relayer, RES_ADD_SETTLED(origin_2, htlc_2_2, HtlcResult.OnChainFulfill(preimage2)))
register.expectMsgAllOf(
Register.Forward(replyTo = null, channelId_ab_1, CMD_FULFILL_HTLC(5, preimage2, commit = true)),
@ -465,10 +465,10 @@ class PostRestartHtlcCleanerSpec extends TestKitBaseClass with FixtureAnyFunSuit
buildHtlcOut(5, channelId_bc_1, paymentHash2),
)
val origins: Map[Long, Origin] = Map(
2L -> Origin.TrampolineRelayedCold((channelId_ab_1, 1L) :: (channelId_ab_1, 2L) :: Nil),
3L -> Origin.TrampolineRelayedCold((channelId_ab_1, 1L) :: (channelId_ab_1, 2L) :: Nil),
4L -> Origin.TrampolineRelayedCold((channelId_ab_1, 1L) :: (channelId_ab_1, 2L) :: Nil),
5L -> Origin.ChannelRelayedCold(channelId_ab_1, 4, 550 msat, 500 msat),
2L -> Origin.Cold(Upstream.Cold.Trampoline(Upstream.Cold.Channel(channelId_ab_1, 1, 500 msat) :: Upstream.Cold.Channel(channelId_ab_1, 2, 200 msat) :: Nil)),
3L -> Origin.Cold(Upstream.Cold.Trampoline(Upstream.Cold.Channel(channelId_ab_1, 1, 500 msat) :: Upstream.Cold.Channel(channelId_ab_1, 2, 200 msat) :: Nil)),
4L -> Origin.Cold(Upstream.Cold.Trampoline(Upstream.Cold.Channel(channelId_ab_1, 1, 500 msat) :: Upstream.Cold.Channel(channelId_ab_1, 2, 200 msat) :: Nil)),
5L -> Origin.Cold(Upstream.Cold.Channel(channelId_ab_1, 4, 550 msat)),
)
val downstreamChannel = {
val normal = ChannelCodecsSpec.makeChannelDataNormal(htlc_bc, origins)
@ -500,10 +500,10 @@ class PostRestartHtlcCleanerSpec extends TestKitBaseClass with FixtureAnyFunSuit
relayer ! PostRestartHtlcCleaner.Init(testCase.channels)
register.expectNoMessage(100 millis)
sender.send(relayer, buildForwardFail(testCase.downstream, testCase.origin))
sender.send(relayer, buildForwardFail(testCase.downstream, testCase.upstream))
register.expectMsgType[Register.Forward[CMD_FAIL_HTLC]]
sender.send(relayer, buildForwardFail(testCase.downstream, testCase.origin))
sender.send(relayer, buildForwardFail(testCase.downstream, testCase.upstream))
register.expectNoMessage(100 millis) // the payment has already been failed upstream
eventListener.expectNoMessage(100 millis)
}
@ -512,17 +512,17 @@ class PostRestartHtlcCleanerSpec extends TestKitBaseClass with FixtureAnyFunSuit
import f._
val htlc_ab = buildHtlcIn(0, channelId_ab_1, paymentHash1, blinded = true)
val origin = Origin.ChannelRelayedCold(htlc_ab.add.channelId, htlc_ab.add.id, htlc_ab.add.amountMsat, htlc_ab.add.amountMsat - 100.msat)
val upstream = Upstream.Cold.Channel(htlc_ab.add.channelId, htlc_ab.add.id, htlc_ab.add.amountMsat)
val htlc_bc = buildHtlcOut(6, channelId_bc_1, paymentHash1, blinded = true)
val data_ab = ChannelCodecsSpec.makeChannelDataNormal(Seq(htlc_ab), Map.empty)
val data_bc = ChannelCodecsSpec.makeChannelDataNormal(Seq(htlc_bc), Map(6L -> origin))
val data_bc = ChannelCodecsSpec.makeChannelDataNormal(Seq(htlc_bc), Map(6L -> Origin.Cold(upstream)))
val channels = List(data_ab, data_bc)
val (relayer, _) = f.createRelayer(nodeParams)
relayer ! PostRestartHtlcCleaner.Init(channels)
register.expectNoMessage(100 millis)
sender.send(relayer, buildForwardFail(htlc_bc.add, origin))
sender.send(relayer, buildForwardFail(htlc_bc.add, upstream))
val cmd = register.expectMsgType[Register.Forward[CMD_FAIL_MALFORMED_HTLC]]
assert(cmd.message == CMD_FAIL_MALFORMED_HTLC(htlc_ab.add.id, ByteVector32.Zeroes, FailureMessageCodecs.BADONION | FailureMessageCodecs.PERM | 24, commit = true))
}
@ -535,11 +535,11 @@ class PostRestartHtlcCleanerSpec extends TestKitBaseClass with FixtureAnyFunSuit
relayer ! PostRestartHtlcCleaner.Init(testCase.channels)
register.expectNoMessage(100 millis)
sender.send(relayer, buildForwardFulfill(testCase.downstream, testCase.origin, preimage1))
register.expectMsg(Register.Forward(null, testCase.origin.originChannelId, CMD_FULFILL_HTLC(testCase.origin.originHtlcId, preimage1, commit = true)))
sender.send(relayer, buildForwardFulfill(testCase.downstream, testCase.upstream, preimage1))
register.expectMsg(Register.Forward(null, testCase.upstream.originChannelId, CMD_FULFILL_HTLC(testCase.upstream.originHtlcId, preimage1, commit = true)))
eventListener.expectMsgType[ChannelPaymentRelayed]
sender.send(relayer, buildForwardFulfill(testCase.downstream, testCase.origin, preimage1))
sender.send(relayer, buildForwardFulfill(testCase.downstream, testCase.upstream, preimage1))
register.expectNoMessage(100 millis) // the payment has already been fulfilled upstream
eventListener.expectNoMessage(100 millis)
}
@ -553,22 +553,22 @@ class PostRestartHtlcCleanerSpec extends TestKitBaseClass with FixtureAnyFunSuit
register.expectNoMessage(100 millis)
// This downstream HTLC has two upstream HTLCs.
sender.send(relayer, buildForwardFail(testCase.downstream_1_1, testCase.origin_1))
sender.send(relayer, buildForwardFail(testCase.downstream_1_1, testCase.upstream_1))
val fails = register.expectMsgType[Register.Forward[CMD_FAIL_HTLC]] :: register.expectMsgType[Register.Forward[CMD_FAIL_HTLC]] :: Nil
assert(fails.toSet == testCase.origin_1.htlcs.map {
case (channelId, htlcId) => Register.Forward(null, channelId, CMD_FAIL_HTLC(htlcId, Right(TemporaryNodeFailure()), commit = true))
assert(fails.toSet == testCase.upstream_1.originHtlcs.map {
case Upstream.Cold.Channel(channelId, htlcId, _) => Register.Forward(null, channelId, CMD_FAIL_HTLC(htlcId, Right(TemporaryNodeFailure()), commit = true))
}.toSet)
sender.send(relayer, buildForwardFail(testCase.downstream_1_1, testCase.origin_1))
sender.send(relayer, buildForwardFail(testCase.downstream_1_1, testCase.upstream_1))
register.expectNoMessage(100 millis) // a duplicate failure should be ignored
sender.send(relayer, buildForwardOnChainFail(testCase.downstream_2_1, testCase.origin_2))
sender.send(relayer, buildForwardFail(testCase.downstream_2_2, testCase.origin_2))
sender.send(relayer, buildForwardOnChainFail(testCase.downstream_2_1, testCase.upstream_2))
sender.send(relayer, buildForwardFail(testCase.downstream_2_2, testCase.upstream_2))
register.expectNoMessage(100 millis) // there is still a third downstream payment pending
sender.send(relayer, buildForwardFail(testCase.downstream_2_3, testCase.origin_2))
register.expectMsg(testCase.origin_2.htlcs.map {
case (channelId, htlcId) => Register.Forward(null, channelId, CMD_FAIL_HTLC(htlcId, Right(TemporaryNodeFailure()), commit = true))
sender.send(relayer, buildForwardFail(testCase.downstream_2_3, testCase.upstream_2))
register.expectMsg(testCase.upstream_2.originHtlcs.map {
case Upstream.Cold.Channel(channelId, htlcId, _) => Register.Forward(null, channelId, CMD_FAIL_HTLC(htlcId, Right(TemporaryNodeFailure()), commit = true))
}.head)
register.expectNoMessage(100 millis)
@ -584,23 +584,23 @@ class PostRestartHtlcCleanerSpec extends TestKitBaseClass with FixtureAnyFunSuit
register.expectNoMessage(100 millis)
// This downstream HTLC has two upstream HTLCs.
sender.send(relayer, buildForwardFulfill(testCase.downstream_1_1, testCase.origin_1, preimage1))
sender.send(relayer, buildForwardFulfill(testCase.downstream_1_1, testCase.upstream_1, preimage1))
val fulfills = register.expectMsgType[Register.Forward[CMD_FULFILL_HTLC]] :: register.expectMsgType[Register.Forward[CMD_FULFILL_HTLC]] :: Nil
assert(fulfills.toSet == testCase.origin_1.htlcs.map {
case (channelId, htlcId) => Register.Forward(null, channelId, CMD_FULFILL_HTLC(htlcId, preimage1, commit = true))
assert(fulfills.toSet == testCase.upstream_1.originHtlcs.map {
case Upstream.Cold.Channel(channelId, htlcId, _) => Register.Forward(null, channelId, CMD_FULFILL_HTLC(htlcId, preimage1, commit = true))
}.toSet)
sender.send(relayer, buildForwardFulfill(testCase.downstream_1_1, testCase.origin_1, preimage1))
sender.send(relayer, buildForwardFulfill(testCase.downstream_1_1, testCase.upstream_1, preimage1))
register.expectNoMessage(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.origin_2, preimage2))
register.expectMsg(testCase.origin_2.htlcs.map {
case (channelId, htlcId) => Register.Forward(null, channelId, CMD_FULFILL_HTLC(htlcId, preimage2, commit = true))
sender.send(relayer, buildForwardFulfill(testCase.downstream_2_1, testCase.upstream_2, preimage2))
register.expectMsg(testCase.upstream_2.originHtlcs.map {
case Upstream.Cold.Channel(channelId, htlcId, _) => Register.Forward(null, channelId, CMD_FULFILL_HTLC(htlcId, preimage2, commit = true))
}.head)
sender.send(relayer, buildForwardFulfill(testCase.downstream_2_2, testCase.origin_2, preimage2))
sender.send(relayer, buildForwardFulfill(testCase.downstream_2_3, testCase.origin_2, preimage2))
sender.send(relayer, buildForwardFulfill(testCase.downstream_2_2, testCase.upstream_2, preimage2))
sender.send(relayer, buildForwardFulfill(testCase.downstream_2_3, testCase.upstream_2, preimage2))
register.expectNoMessage(100 millis) // the payment has already been fulfilled upstream
eventListener.expectNoMessage(100 millis)
}
@ -613,13 +613,13 @@ class PostRestartHtlcCleanerSpec extends TestKitBaseClass with FixtureAnyFunSuit
relayer ! PostRestartHtlcCleaner.Init(testCase.channels)
register.expectNoMessage(100 millis)
sender.send(relayer, buildForwardFail(testCase.downstream_2_1, testCase.origin_2))
sender.send(relayer, buildForwardFulfill(testCase.downstream_2_2, testCase.origin_2, preimage2))
register.expectMsg(testCase.origin_2.htlcs.map {
case (channelId, htlcId) => Register.Forward(null, channelId, CMD_FULFILL_HTLC(htlcId, preimage2, commit = true))
sender.send(relayer, buildForwardFail(testCase.downstream_2_1, testCase.upstream_2))
sender.send(relayer, buildForwardFulfill(testCase.downstream_2_2, testCase.upstream_2, preimage2))
register.expectMsg(testCase.upstream_2.originHtlcs.map {
case Upstream.Cold.Channel(channelId, htlcId, _) => Register.Forward(null, channelId, CMD_FULFILL_HTLC(htlcId, preimage2, commit = true))
}.head)
sender.send(relayer, buildForwardFail(testCase.downstream_2_3, testCase.origin_2))
sender.send(relayer, buildForwardFail(testCase.downstream_2_3, testCase.upstream_2))
register.expectNoMessage(100 millis) // the payment has already been fulfilled upstream
eventListener.expectNoMessage(100 millis)
}
@ -629,7 +629,7 @@ class PostRestartHtlcCleanerSpec extends TestKitBaseClass with FixtureAnyFunSuit
val relayedPaymentHash = randomBytes32()
val trampolineRelayedPaymentHash = randomBytes32()
val trampolineRelayed = Origin.TrampolineRelayedCold((channelId_ab_2, 0L) :: Nil)
val trampolineRelayed = Origin.Cold(Upstream.Cold.Trampoline(Upstream.Cold.Channel(channelId_ab_2, 0, 1 msat) :: Nil))
val relayedHtlcIn = buildHtlcIn(0L, channelId_ab_2, trampolineRelayedPaymentHash)
val nonRelayedHtlcIn = buildHtlcIn(1L, channelId_ab_2, relayedPaymentHash)
@ -637,7 +637,7 @@ class PostRestartHtlcCleanerSpec extends TestKitBaseClass with FixtureAnyFunSuit
val pluginParams = new CustomCommitmentsPlugin {
override def name = "test with outgoing HTLC to remote"
override def getIncomingHtlcs(np: NodeParams, log: LoggingAdapter): Seq[PostRestartHtlcCleaner.IncomingHtlc] = List.empty
override def getHtlcsRelayedOut(htlcsIn: Seq[PostRestartHtlcCleaner.IncomingHtlc], np: NodeParams, log: LoggingAdapter): Map[Origin, Set[(ByteVector32, Long)]] = Map(trampolineRelayed -> Set((channelId_ab_1, 10L)))
override def getHtlcsRelayedOut(htlcsIn: Seq[PostRestartHtlcCleaner.IncomingHtlc], np: NodeParams, log: LoggingAdapter): Map[Origin.Cold, Set[(ByteVector32, Long)]] = Map(trampolineRelayed -> Set((channelId_ab_1, 10L)))
}
// @formatter:on
@ -665,7 +665,7 @@ class PostRestartHtlcCleanerSpec extends TestKitBaseClass with FixtureAnyFunSuit
val pluginParams = new CustomCommitmentsPlugin {
override def name = "test with incoming HTLC from remote"
override def getIncomingHtlcs(np: NodeParams, log: LoggingAdapter): Seq[PostRestartHtlcCleaner.IncomingHtlc] = List(PostRestartHtlcCleaner.IncomingHtlc(relayedHtlc1In.add, None))
override def getHtlcsRelayedOut(htlcsIn: Seq[PostRestartHtlcCleaner.IncomingHtlc], np: NodeParams, log: LoggingAdapter): Map[Origin, Set[(ByteVector32, Long)]] = Map.empty
override def getHtlcsRelayedOut(htlcsIn: Seq[PostRestartHtlcCleaner.IncomingHtlc], np: NodeParams, log: LoggingAdapter): Map[Origin.Cold, Set[(ByteVector32, Long)]] = Map.empty
}
// @formatter:on
@ -700,7 +700,7 @@ object PostRestartHtlcCleanerSpec {
buildOutgoingBlindedPaymentAB(paymentHash)
} else {
val (route, recipient) = (Route(finalAmount, hops, None), SpontaneousRecipient(e, finalAmount, finalExpiry, randomBytes32()))
buildOutgoingPayment(ActorRef.noSender, Upstream.Local(UUID.randomUUID()), paymentHash, route, recipient)
buildOutgoingPayment(TestConstants.emptyOrigin, paymentHash, route, recipient)
}
UpdateAddHtlc(channelId, htlcId, payment.cmd.amount, paymentHash, payment.cmd.cltvExpiry, payment.cmd.onion, payment.cmd.nextBlindingKey_opt)
}
@ -710,18 +710,18 @@ object PostRestartHtlcCleanerSpec {
def buildHtlcOut(htlcId: Long, channelId: ByteVector32, paymentHash: ByteVector32, blinded: Boolean = false): DirectedHtlc = OutgoingHtlc(buildHtlc(htlcId, channelId, paymentHash, blinded))
def buildFinalHtlc(htlcId: Long, channelId: ByteVector32, paymentHash: ByteVector32): DirectedHtlc = {
val Right(payment) = buildOutgoingPayment(ActorRef.noSender, Upstream.Local(UUID.randomUUID()), paymentHash, Route(finalAmount, Seq(channelHopFromUpdate(a, b, channelUpdate_ab)), None), SpontaneousRecipient(b, finalAmount, finalExpiry, randomBytes32()))
val Right(payment) = buildOutgoingPayment(TestConstants.emptyOrigin, paymentHash, Route(finalAmount, Seq(channelHopFromUpdate(a, b, channelUpdate_ab)), None), SpontaneousRecipient(b, finalAmount, finalExpiry, randomBytes32()))
IncomingHtlc(UpdateAddHtlc(channelId, htlcId, payment.cmd.amount, paymentHash, payment.cmd.cltvExpiry, payment.cmd.onion, None))
}
def buildForwardFail(add: UpdateAddHtlc, origin: Origin.Cold): RES_ADD_SETTLED[Origin.Cold, HtlcResult.Fail] =
RES_ADD_SETTLED(origin, add, HtlcResult.RemoteFail(UpdateFailHtlc(add.channelId, add.id, ByteVector.empty)))
def buildForwardFail(add: UpdateAddHtlc, upstream: Upstream.Cold): RES_ADD_SETTLED[Origin.Cold, HtlcResult.Fail] =
RES_ADD_SETTLED(Origin.Cold(upstream), add, HtlcResult.RemoteFail(UpdateFailHtlc(add.channelId, add.id, ByteVector.empty)))
def buildForwardOnChainFail(add: UpdateAddHtlc, origin: Origin.Cold): RES_ADD_SETTLED[Origin.Cold, HtlcResult.Fail] =
RES_ADD_SETTLED(origin, add, HtlcResult.OnChainFail(HtlcsTimedoutDownstream(add.channelId, Set(add))))
def buildForwardOnChainFail(add: UpdateAddHtlc, upstream: Upstream.Cold): RES_ADD_SETTLED[Origin.Cold, HtlcResult.Fail] =
RES_ADD_SETTLED(Origin.Cold(upstream), add, HtlcResult.OnChainFail(HtlcsTimedoutDownstream(add.channelId, Set(add))))
def buildForwardFulfill(add: UpdateAddHtlc, origin: Origin.Cold, preimage: ByteVector32): RES_ADD_SETTLED[Origin.Cold, HtlcResult.Fulfill] =
RES_ADD_SETTLED(origin, add, HtlcResult.RemoteFulfill(UpdateFulfillHtlc(add.channelId, add.id, preimage)))
def buildForwardFulfill(add: UpdateAddHtlc, upstream: Upstream.Cold, preimage: ByteVector32): RES_ADD_SETTLED[Origin.Cold, HtlcResult.Fulfill] =
RES_ADD_SETTLED(Origin.Cold(upstream), add, HtlcResult.RemoteFulfill(UpdateFulfillHtlc(add.channelId, add.id, preimage)))
case class LocalPaymentTest(channel: PersistentChannelData, parentId: UUID, childIds: Seq[UUID], fails: Seq[RES_ADD_SETTLED[Origin.Cold, HtlcResult.Fail]], fulfills: Seq[RES_ADD_SETTLED[Origin.Cold, HtlcResult.Fulfill]])
@ -735,11 +735,11 @@ object PostRestartHtlcCleanerSpec {
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, blinding_opt = None)
val origin1 = Origin.LocalCold(id1)
val origin1 = Origin.Cold(Upstream.Local(id1))
val add2 = UpdateAddHtlc(channelId_bc_1, 75, 1105 msat, paymentHash2, CltvExpiry(4250), onionRoutingPacket = TestConstants.emptyOnionPacket, blinding_opt = None)
val origin2 = Origin.LocalCold(id2)
val origin2 = Origin.Cold(Upstream.Local(id2))
val add3 = UpdateAddHtlc(channelId_bc_1, 78, 1729 msat, paymentHash2, CltvExpiry(4300), onionRoutingPacket = TestConstants.emptyOnionPacket, blinding_opt = None)
val origin3 = Origin.LocalCold(id3)
val origin3 = Origin.Cold(Upstream.Local(id3))
// Prepare channels and payment state before restart.
nodeParams.db.payments.addOutgoingPayment(OutgoingPayment(id1, id1, None, paymentHash1, PaymentType.Standard, add1.amountMsat, add1.amountMsat, c, 0 unixms, None, None, OutgoingPaymentStatus.Pending))
@ -750,12 +750,12 @@ object PostRestartHtlcCleanerSpec {
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))
val fails = Seq(buildForwardFail(add1, origin1.upstream), buildForwardFail(add2, origin2.upstream), buildForwardFail(add3, origin3.upstream))
val fulfills = Seq(buildForwardFulfill(add1, origin1.upstream, preimage1), buildForwardFulfill(add2, origin2.upstream, preimage2), buildForwardFulfill(add3, origin3.upstream, preimage2))
LocalPaymentTest(channel, parentId, Seq(id1, id2, id3), fails, fulfills)
}
case class ChannelRelayedPaymentTest(channels: Seq[PersistentChannelData], origin: Origin.ChannelRelayedCold, downstream: UpdateAddHtlc, notRelayed: Set[(Long, ByteVector32)])
case class ChannelRelayedPaymentTest(channels: Seq[PersistentChannelData], upstream: Upstream.Cold.Channel, downstream: UpdateAddHtlc, notRelayed: Set[(Long, ByteVector32)])
def setupChannelRelayedPayments(): ChannelRelayedPaymentTest = {
// Upstream HTLCs.
@ -763,26 +763,26 @@ object PostRestartHtlcCleanerSpec {
buildHtlcIn(0, channelId_ab_1, paymentHash1)
)
val origin_1 = Origin.ChannelRelayedCold(htlc_ab_1.head.add.channelId, htlc_ab_1.head.add.id, htlc_ab_1.head.add.amountMsat, htlc_ab_1.head.add.amountMsat - 100.msat)
val upstream_1 = Upstream.Cold.Channel(htlc_ab_1.head.add.channelId, htlc_ab_1.head.add.id, htlc_ab_1.head.add.amountMsat)
val htlc_bc_1 = Seq(
buildHtlcOut(6, channelId_bc_1, paymentHash1)
)
val data_ab_1 = ChannelCodecsSpec.makeChannelDataNormal(htlc_ab_1, Map.empty)
val data_bc_1 = ChannelCodecsSpec.makeChannelDataNormal(htlc_bc_1, Map(6L -> origin_1))
val data_bc_1 = ChannelCodecsSpec.makeChannelDataNormal(htlc_bc_1, Map(6L -> Origin.Cold(upstream_1)))
val channels = List(data_ab_1, data_bc_1)
val downstream_1 = htlc_bc_1.head.add
ChannelRelayedPaymentTest(channels, origin_1, downstream_1, Set.empty)
ChannelRelayedPaymentTest(channels, upstream_1, downstream_1, Set.empty)
}
case class TrampolinePaymentTest(channels: Seq[PersistentChannelData],
origin_1: Origin.TrampolineRelayedCold,
upstream_1: Upstream.Cold.Trampoline,
downstream_1_1: UpdateAddHtlc,
origin_2: Origin.TrampolineRelayedCold,
upstream_2: Upstream.Cold.Trampoline,
downstream_2_1: UpdateAddHtlc,
downstream_2_2: UpdateAddHtlc,
downstream_2_3: UpdateAddHtlc,
@ -814,12 +814,12 @@ object PostRestartHtlcCleanerSpec {
buildHtlcOut(9, channelId_ab_2, randomBytes32()) // ignored
)
val origin_1 = Origin.TrampolineRelayedCold((channelId_ab_1, 0L) :: (channelId_ab_2, 7L) :: Nil)
val origin_2 = Origin.TrampolineRelayedCold((channelId_ab_1, 5L) :: Nil)
val upstream_1 = Upstream.Cold.Trampoline(Upstream.Cold.Channel(channelId_ab_1, 0, 500 msat) :: Upstream.Cold.Channel(channelId_ab_2, 7, 250 msat) :: Nil)
val upstream_2 = Upstream.Cold.Trampoline(Upstream.Cold.Channel(channelId_ab_1, 5, 500 msat) :: Nil)
// The following two origins reference upstream HTLCs that have already been settled.
// They should be ignored by the post-restart clean-up.
val origin_3 = Origin.TrampolineRelayedCold((channelId_ab_1, 57L) :: Nil)
val origin_4 = Origin.ChannelRelayedCold(channelId_ab_2, 57, 150 msat, 100 msat)
val upstream_3 = Upstream.Cold.Trampoline(Upstream.Cold.Channel(channelId_ab_1, 57, 100 msat) :: Nil)
val upstream_4 = Upstream.Cold.Channel(channelId_ab_2, 57, 150 msat)
// Downstream HTLCs.
val htlc_bc_1 = Seq(
@ -854,15 +854,15 @@ object PostRestartHtlcCleanerSpec {
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))
val data_bc_4 = ChannelCodecsSpec.makeChannelDataNormal(htlc_bc_4, Map(5L -> origin_3))
val data_bc_5 = ChannelCodecsSpec.makeChannelDataNormal(htlc_bc_5, Map(2L -> origin_3, 4L -> origin_4))
val data_bc_1 = ChannelCodecsSpec.makeChannelDataNormal(htlc_bc_1, Map(6L -> Origin.Cold(upstream_1), 8L -> Origin.Cold(upstream_2)))
val data_bc_2 = ChannelCodecsSpec.makeChannelDataNormal(htlc_bc_2, Map(1L -> Origin.Cold(upstream_2)))
val data_bc_3 = ChannelCodecsSpec.makeChannelDataNormal(htlc_bc_3, Map(4L -> Origin.Cold(upstream_2)))
val data_bc_4 = ChannelCodecsSpec.makeChannelDataNormal(htlc_bc_4, Map(5L -> Origin.Cold(upstream_3)))
val data_bc_5 = ChannelCodecsSpec.makeChannelDataNormal(htlc_bc_5, Map(2L -> Origin.Cold(upstream_3), 4L -> Origin.Cold(upstream_4)))
val channels = List(data_ab_1, data_ab_2, data_bc_1, data_bc_2, data_bc_3, data_bc_4, data_bc_5)
TrampolinePaymentTest(channels, origin_1, downstream_1_1, origin_2, downstream_2_1, downstream_2_2, downstream_2_3, notRelayed)
TrampolinePaymentTest(channels, upstream_1, downstream_1_1, upstream_2, downstream_2_1, downstream_2_2, downstream_2_3, notRelayed)
}
/**

View File

@ -73,9 +73,6 @@ class ChannelRelayerSpec extends ScalaTestWithActorTestKit(ConfigFactory.load("a
inside(fwd.message) { case add: CMD_ADD_HTLC =>
assert(add.amount == outAmount)
assert(add.cltvExpiry == outExpiry)
inside(add.origin) { case o: Origin.ChannelRelayedHot =>
assert(o.amountOut == outAmount)
}
}
assert(fwd.channelId == channelId)
fwd

View File

@ -29,12 +29,11 @@ import fr.acinq.bitcoin.scalacompat.{Block, BlockHash, ByteVector32, Crypto}
import fr.acinq.eclair.FeatureSupport.{Mandatory, Optional}
import fr.acinq.eclair.Features.{AsyncPaymentPrototype, BasicMultiPartPayment, PaymentSecret, VariableLengthOnion}
import fr.acinq.eclair.EncodedNodeId.ShortChannelIdDir
import fr.acinq.eclair.channel.{CMD_FAIL_HTLC, CMD_FULFILL_HTLC, Register}
import fr.acinq.eclair.channel.{CMD_FAIL_HTLC, CMD_FULFILL_HTLC, Register, Upstream}
import fr.acinq.eclair.crypto.Sphinx
import fr.acinq.eclair.payment.Bolt11Invoice.ExtraHop
import fr.acinq.eclair.payment.IncomingPaymentPacket.{RelayToBlindedPathsPacket, RelayToTrampolinePacket}
import fr.acinq.eclair.payment.Invoice.ExtraEdge
import fr.acinq.eclair.payment.OutgoingPaymentPacket.Upstream
import fr.acinq.eclair.payment._
import fr.acinq.eclair.payment.relay.AsyncPaymentTriggerer.{AsyncPaymentCanceled, AsyncPaymentTimeout, AsyncPaymentTriggered, Watch}
import fr.acinq.eclair.payment.relay.NodeRelayer.PaymentKey
@ -249,7 +248,7 @@ class NodeRelayerSpec extends ScalaTestWithActorTestKit(ConfigFactory.load("appl
incomingMultiPart.foreach(incoming => nodeRelayer ! NodeRelay.Relay(incoming))
val outgoingCfg = mockPayFSM.expectMessageType[SendPaymentConfig]
validateOutgoingCfg(outgoingCfg, Upstream.Trampoline(incomingMultiPart.map(p => Upstream.ReceivedHtlc(p.add, TimestampMilli.now()))))
validateOutgoingCfg(outgoingCfg, Upstream.Hot.Trampoline(incomingMultiPart.map(p => Upstream.Hot.Channel(p.add, TimestampMilli.now()))))
val outgoingPayment = mockPayFSM.expectMessageType[SendMultiPartPayment]
validateOutgoingPayment(outgoingPayment)
@ -378,7 +377,7 @@ class NodeRelayerSpec extends ScalaTestWithActorTestKit(ConfigFactory.load("appl
// upstream payment relayed
val outgoingCfg = mockPayFSM.expectMessageType[SendPaymentConfig]
validateOutgoingCfg(outgoingCfg, Upstream.Trampoline(incomingAsyncPayment.map(p => Upstream.ReceivedHtlc(p.add, TimestampMilli.now()))))
validateOutgoingCfg(outgoingCfg, Upstream.Hot.Trampoline(incomingAsyncPayment.map(p => Upstream.Hot.Channel(p.add, TimestampMilli.now()))))
val outgoingPayment = mockPayFSM.expectMessageType[SendMultiPartPayment]
validateOutgoingPayment(outgoingPayment)
// those are adapters for pay-fsm messages
@ -455,7 +454,7 @@ class NodeRelayerSpec extends ScalaTestWithActorTestKit(ConfigFactory.load("appl
// upstream payment relayed
val outgoingCfg = mockPayFSM.expectMessageType[SendPaymentConfig]
validateOutgoingCfg(outgoingCfg, Upstream.Trampoline(incomingAsyncPayment.map(p => Upstream.ReceivedHtlc(p.add, TimestampMilli.now()))))
validateOutgoingCfg(outgoingCfg, Upstream.Hot.Trampoline(incomingAsyncPayment.map(p => Upstream.Hot.Channel(p.add, TimestampMilli.now()))))
val outgoingPayment = mockPayFSM.expectMessageType[SendMultiPartPayment]
validateOutgoingPayment(outgoingPayment)
// those are adapters for pay-fsm messages
@ -663,7 +662,7 @@ class NodeRelayerSpec extends ScalaTestWithActorTestKit(ConfigFactory.load("appl
nodeRelayer ! NodeRelay.Relay(incomingMultiPart.last)
val outgoingCfg = mockPayFSM.expectMessageType[SendPaymentConfig]
validateOutgoingCfg(outgoingCfg, Upstream.Trampoline(incomingMultiPart.map(p => Upstream.ReceivedHtlc(p.add, TimestampMilli.now()))))
validateOutgoingCfg(outgoingCfg, Upstream.Hot.Trampoline(incomingMultiPart.map(p => Upstream.Hot.Channel(p.add, TimestampMilli.now()))))
val outgoingPayment = mockPayFSM.expectMessageType[SendMultiPartPayment]
validateOutgoingPayment(outgoingPayment)
// those are adapters for pay-fsm messages
@ -699,7 +698,7 @@ class NodeRelayerSpec extends ScalaTestWithActorTestKit(ConfigFactory.load("appl
nodeRelayer ! NodeRelay.Relay(incomingSinglePart)
val outgoingCfg = mockPayFSM.expectMessageType[SendPaymentConfig]
validateOutgoingCfg(outgoingCfg, Upstream.Trampoline(Upstream.ReceivedHtlc(incomingSinglePart.add, TimestampMilli.now()) :: Nil))
validateOutgoingCfg(outgoingCfg, Upstream.Hot.Trampoline(Upstream.Hot.Channel(incomingSinglePart.add, TimestampMilli.now()) :: Nil))
val outgoingPayment = mockPayFSM.expectMessageType[SendMultiPartPayment]
validateOutgoingPayment(outgoingPayment)
// those are adapters for pay-fsm messages
@ -734,7 +733,7 @@ class NodeRelayerSpec extends ScalaTestWithActorTestKit(ConfigFactory.load("appl
incomingPayments.foreach(incoming => nodeRelayer ! NodeRelay.Relay(incoming))
val outgoingCfg = mockPayFSM.expectMessageType[SendPaymentConfig]
validateOutgoingCfg(outgoingCfg, Upstream.Trampoline(incomingMultiPart.map(p => Upstream.ReceivedHtlc(p.add, TimestampMilli.now()))))
validateOutgoingCfg(outgoingCfg, Upstream.Hot.Trampoline(incomingMultiPart.map(p => Upstream.Hot.Channel(p.add, TimestampMilli.now()))))
val outgoingPayment = mockPayFSM.expectMessageType[SendMultiPartPayment]
assert(outgoingPayment.recipient.nodeId == outgoingNodeId)
assert(outgoingPayment.recipient.totalAmount == outgoingAmount)
@ -778,7 +777,7 @@ class NodeRelayerSpec extends ScalaTestWithActorTestKit(ConfigFactory.load("appl
incomingPayments.foreach(incoming => nodeRelayer ! NodeRelay.Relay(incoming))
val outgoingCfg = mockPayFSM.expectMessageType[SendPaymentConfig]
validateOutgoingCfg(outgoingCfg, Upstream.Trampoline(incomingMultiPart.map(p => Upstream.ReceivedHtlc(p.add, TimestampMilli.now()))))
validateOutgoingCfg(outgoingCfg, Upstream.Hot.Trampoline(incomingMultiPart.map(p => Upstream.Hot.Channel(p.add, TimestampMilli.now()))))
val outgoingPayment = mockPayFSM.expectMessageType[SendPaymentToNode]
assert(outgoingPayment.recipient.nodeId == outgoingNodeId)
assert(outgoingPayment.amount == outgoingAmount)
@ -847,7 +846,7 @@ class NodeRelayerSpec extends ScalaTestWithActorTestKit(ConfigFactory.load("appl
incomingPayments.foreach(incoming => nodeRelayer ! NodeRelay.Relay(incoming))
val outgoingCfg = mockPayFSM.expectMessageType[SendPaymentConfig]
validateOutgoingCfg(outgoingCfg, Upstream.Trampoline(incomingMultiPart.map(p => Upstream.ReceivedHtlc(p.add, TimestampMilli.now()))), ignoreNodeId = true)
validateOutgoingCfg(outgoingCfg, Upstream.Hot.Trampoline(incomingMultiPart.map(p => Upstream.Hot.Channel(p.add, TimestampMilli.now()))), ignoreNodeId = true)
val outgoingPayment = mockPayFSM.expectMessageType[SendPaymentToNode]
assert(outgoingPayment.amount == outgoingAmount)
assert(outgoingPayment.recipient.expiry == outgoingExpiry)
@ -886,7 +885,7 @@ class NodeRelayerSpec extends ScalaTestWithActorTestKit(ConfigFactory.load("appl
incomingPayments.foreach(incoming => nodeRelayer ! NodeRelay.Relay(incoming))
val outgoingCfg = mockPayFSM.expectMessageType[SendPaymentConfig]
validateOutgoingCfg(outgoingCfg, Upstream.Trampoline(incomingMultiPart.map(p => Upstream.ReceivedHtlc(p.add, TimestampMilli.now()))), ignoreNodeId = true)
validateOutgoingCfg(outgoingCfg, Upstream.Hot.Trampoline(incomingMultiPart.map(p => Upstream.Hot.Channel(p.add, TimestampMilli.now()))), ignoreNodeId = true)
val outgoingPayment = mockPayFSM.expectMessageType[SendMultiPartPayment]
assert(outgoingPayment.recipient.totalAmount == outgoingAmount)
assert(outgoingPayment.recipient.expiry == outgoingExpiry)
@ -933,7 +932,7 @@ class NodeRelayerSpec extends ScalaTestWithActorTestKit(ConfigFactory.load("appl
getNodeId.replyTo ! Some(outgoingNodeId)
val outgoingCfg = mockPayFSM.expectMessageType[SendPaymentConfig]
validateOutgoingCfg(outgoingCfg, Upstream.Trampoline(incomingMultiPart.map(p => Upstream.ReceivedHtlc(p.add, TimestampMilli.now()))), ignoreNodeId = true)
validateOutgoingCfg(outgoingCfg, Upstream.Hot.Trampoline(incomingMultiPart.map(p => Upstream.Hot.Channel(p.add, TimestampMilli.now()))), ignoreNodeId = true)
val outgoingPayment = mockPayFSM.expectMessageType[SendPaymentToNode]
assert(outgoingPayment.amount == outgoingAmount)
assert(outgoingPayment.recipient.expiry == outgoingExpiry)
@ -995,8 +994,7 @@ class NodeRelayerSpec extends ScalaTestWithActorTestKit(ConfigFactory.load("appl
assert(outgoingCfg.invoice.isEmpty)
assert(ignoreNodeId || outgoingCfg.recipientNodeId == outgoingNodeId)
(outgoingCfg.upstream, upstream) match {
case (Upstream.Trampoline(adds1), Upstream.Trampoline(adds2)) =>
assert(adds1.map(_.add) == adds2.map(_.add))
case (Upstream.Hot.Trampoline(adds1), Upstream.Hot.Trampoline(adds2)) => assert(adds1.map(_.add) == adds2.map(_.add))
case _ => assert(outgoingCfg.upstream == upstream)
}
}

View File

@ -16,7 +16,6 @@
package fr.acinq.eclair.payment.relay
import akka.actor.ActorRef
import akka.actor.testkit.typed.scaladsl.{ScalaTestWithActorTestKit, TestProbe}
import akka.actor.typed.eventstream.EventStream
import akka.actor.typed.scaladsl.Behaviors
@ -30,7 +29,7 @@ import fr.acinq.eclair.channel._
import fr.acinq.eclair.crypto.Sphinx
import fr.acinq.eclair.payment.Bolt11Invoice
import fr.acinq.eclair.payment.IncomingPaymentPacket.FinalPacket
import fr.acinq.eclair.payment.OutgoingPaymentPacket.{NodePayload, Upstream, buildOnion, buildOutgoingPayment}
import fr.acinq.eclair.payment.OutgoingPaymentPacket.{NodePayload, buildOnion, buildOutgoingPayment}
import fr.acinq.eclair.payment.PaymentPacketSpec._
import fr.acinq.eclair.payment.relay.Relayer._
import fr.acinq.eclair.payment.send.{ClearRecipient, TrampolineRecipient}
@ -43,7 +42,6 @@ import org.scalatest.funsuite.FixtureAnyFunSuiteLike
import org.scalatest.{Outcome, Tag}
import scodec.bits.HexStringSyntax
import java.util.UUID
import scala.concurrent.duration.DurationInt
class RelayerSpec extends ScalaTestWithActorTestKit(ConfigFactory.load("application")) with FixtureAnyFunSuiteLike {
@ -92,7 +90,7 @@ class RelayerSpec extends ScalaTestWithActorTestKit(ConfigFactory.load("applicat
}
// we use this to build a valid onion
val Right(payment) = buildOutgoingPayment(ActorRef.noSender, Upstream.Local(UUID.randomUUID()), paymentHash, Route(finalAmount, hops, None), ClearRecipient(e, Features.empty, finalAmount, finalExpiry, paymentSecret))
val Right(payment) = buildOutgoingPayment(TestConstants.emptyOrigin, paymentHash, Route(finalAmount, hops, None), ClearRecipient(e, Features.empty, finalAmount, finalExpiry, paymentSecret))
// and then manually build an htlc
val add_ab = UpdateAddHtlc(randomBytes32(), 123456, payment.cmd.amount, payment.cmd.paymentHash, payment.cmd.cltvExpiry, payment.cmd.onion, None)
relayer ! RelayForward(add_ab)
@ -102,7 +100,7 @@ class RelayerSpec extends ScalaTestWithActorTestKit(ConfigFactory.load("applicat
test("relay an htlc-add at the final node to the payment handler") { f =>
import f._
val Right(payment) = buildOutgoingPayment(ActorRef.noSender, Upstream.Local(UUID.randomUUID()), paymentHash, Route(finalAmount, hops.take(1), None), ClearRecipient(b, Features.empty, finalAmount, finalExpiry, paymentSecret))
val Right(payment) = buildOutgoingPayment(TestConstants.emptyOrigin, paymentHash, Route(finalAmount, hops.take(1), None), ClearRecipient(b, Features.empty, finalAmount, finalExpiry, paymentSecret))
val add_ab = UpdateAddHtlc(channelId_ab, 123456, payment.cmd.amount, payment.cmd.paymentHash, payment.cmd.cltvExpiry, payment.cmd.onion, None)
relayer ! RelayForward(add_ab)
@ -121,7 +119,7 @@ class RelayerSpec extends ScalaTestWithActorTestKit(ConfigFactory.load("applicat
val finalTrampolinePayload = NodePayload(b, FinalPayload.Standard.createPayload(finalAmount, totalAmount, finalExpiry, paymentSecret))
val Right(trampolineOnion) = buildOnion(Seq(finalTrampolinePayload), paymentHash, None)
val recipient = ClearRecipient(b, nodeParams.features.invoiceFeatures(), finalAmount, finalExpiry, randomBytes32(), nextTrampolineOnion_opt = Some(trampolineOnion.packet))
val Right(payment) = buildOutgoingPayment(ActorRef.noSender, Upstream.Local(UUID.randomUUID()), paymentHash, Route(finalAmount, Seq(channelHopFromUpdate(priv_a.publicKey, b, channelUpdate_ab)), None), recipient)
val Right(payment) = buildOutgoingPayment(TestConstants.emptyOrigin, paymentHash, Route(finalAmount, Seq(channelHopFromUpdate(priv_a.publicKey, b, channelUpdate_ab)), None), recipient)
assert(payment.cmd.amount == finalAmount)
assert(payment.cmd.cltvExpiry == finalExpiry)
val add_ab = UpdateAddHtlc(channelId_ab, 123456, payment.cmd.amount, payment.cmd.paymentHash, payment.cmd.cltvExpiry, payment.cmd.onion, None)
@ -142,7 +140,7 @@ class RelayerSpec extends ScalaTestWithActorTestKit(ConfigFactory.load("applicat
import f._
// we use this to build a valid onion
val Right(payment) = buildOutgoingPayment(ActorRef.noSender, Upstream.Local(UUID.randomUUID()), paymentHash, Route(finalAmount, hops, None), ClearRecipient(e, Features.empty, finalAmount, finalExpiry, paymentSecret))
val Right(payment) = buildOutgoingPayment(TestConstants.emptyOrigin, paymentHash, Route(finalAmount, hops, None), ClearRecipient(e, Features.empty, finalAmount, finalExpiry, paymentSecret))
// and then manually build an htlc with an invalid onion (hmac)
val add_ab = UpdateAddHtlc(channelId_ab, 123456, payment.cmd.amount, payment.cmd.paymentHash, payment.cmd.cltvExpiry, payment.cmd.onion.copy(hmac = payment.cmd.onion.hmac.reverse), None)
relayer ! RelayForward(add_ab)
@ -162,7 +160,7 @@ class RelayerSpec extends ScalaTestWithActorTestKit(ConfigFactory.load("applicat
val routeExpiry = CltvExpiry(nodeParams.currentBlockHeight - 10)
val (_, blindedHop, recipient) = blindedRouteFromHops(finalAmount, finalExpiry, Seq(channelHopFromUpdate(b, c, channelUpdate_bc)), routeExpiry, paymentPreimage, hex"deadbeef")
val route = Route(finalAmount, Seq(channelHopFromUpdate(priv_a.publicKey, b, channelUpdate_ab)), Some(blindedHop))
val Right(payment) = buildOutgoingPayment(ActorRef.noSender, Upstream.Local(UUID.randomUUID()), paymentHash, route, recipient)
val Right(payment) = buildOutgoingPayment(TestConstants.emptyOrigin, paymentHash, route, recipient)
val add_ab = UpdateAddHtlc(channelId_ab, 0, payment.cmd.amount, payment.cmd.paymentHash, payment.cmd.cltvExpiry, payment.cmd.onion, payment.cmd.nextBlindingKey_opt)
relayer ! RelayForward(add_ab)
@ -198,7 +196,7 @@ class RelayerSpec extends ScalaTestWithActorTestKit(ConfigFactory.load("applicat
val invoice = Bolt11Invoice(Block.RegtestGenesisBlock.hash, None, paymentHash, priv_c.privateKey, Left("invoice"), CltvExpiryDelta(6), paymentSecret = paymentSecret, features = invoiceFeatures)
val trampolineHop = NodeHop(b, c, channelUpdate_bc.cltvExpiryDelta, fee_b)
val recipient = TrampolineRecipient(invoice, finalAmount, finalExpiry, trampolineHop, randomBytes32())
val Right(payment) = buildOutgoingPayment(ActorRef.noSender, Upstream.Local(UUID.randomUUID()), paymentHash, Route(recipient.trampolineAmount, Seq(channelHopFromUpdate(priv_a.publicKey, b, channelUpdate_ab)), Some(trampolineHop)), recipient)
val Right(payment) = buildOutgoingPayment(TestConstants.emptyOrigin, paymentHash, Route(recipient.trampolineAmount, Seq(channelHopFromUpdate(priv_a.publicKey, b, channelUpdate_ab)), Some(trampolineHop)), recipient)
// and then manually build an htlc
val add_ab = UpdateAddHtlc(channelId_ab, 123456, payment.cmd.amount, payment.cmd.paymentHash, payment.cmd.cltvExpiry, payment.cmd.onion, None)
@ -217,8 +215,8 @@ class RelayerSpec extends ScalaTestWithActorTestKit(ConfigFactory.load("applicat
val replyTo = TestProbe[Any]()
val add_ab = UpdateAddHtlc(channelId = channelId_ab, id = 42, amountMsat = 11000000 msat, paymentHash = ByteVector32.Zeroes, CltvExpiry(4200), TestConstants.emptyOnionPacket, None)
val add_bc = UpdateAddHtlc(channelId_bc, 72, 1000 msat, paymentHash, CltvExpiry(1), TestConstants.emptyOnionPacket, None)
val channelOrigin = Origin.ChannelRelayedHot(replyTo.ref.toClassic, add_ab, 1000 msat)
val trampolineOrigin = Origin.TrampolineRelayedHot(replyTo.ref.toClassic, Seq(add_ab))
val channelOrigin = Origin.Hot(replyTo.ref.toClassic, Upstream.Hot.Channel(add_ab, TimestampMilli.now()))
val trampolineOrigin = Origin.Hot(replyTo.ref.toClassic, Upstream.Hot.Trampoline(Seq(Upstream.Hot.Channel(add_ab, TimestampMilli.now()))))
val addSettled = Seq(
RES_ADD_SETTLED(channelOrigin, add_bc, HtlcResult.OnChainFulfill(randomBytes32())),

View File

@ -298,7 +298,13 @@ object ChannelCodecsSpec {
IncomingHtlc(UpdateAddHtlc(ByteVector32.Zeroes, 2, 4000000 msat, Crypto.sha256(paymentPreimages(4)), CltvExpiry(504), TestConstants.emptyOnionPacket, None))
)
val normal: DATA_NORMAL = makeChannelDataNormal(htlcs, Map(42L -> Origin.LocalCold(UUID.randomUUID), 15000L -> Origin.ChannelRelayedCold(ByteVector32(ByteVector.fill(32)(42)), 43, 11000000 msat, 10000000 msat)))
val normal: DATA_NORMAL = {
val origins = Map(
42L -> Origin.Cold(Upstream.Local(UUID.randomUUID)),
15000L -> Origin.Cold(Upstream.Cold.Channel(ByteVector32(ByteVector.fill(32)(42)), 43, 11_000_000 msat))
)
makeChannelDataNormal(htlcs, origins)
}
def makeChannelDataNormal(htlcs: Seq[DirectedHtlc], origins: Map[Long, Origin]): DATA_NORMAL = {
val channelUpdate = Announcements.makeChannelUpdate(BlockHash(ByteVector32(ByteVector.fill(32)(1))), randomKey(), randomKey().publicKey, ShortChannelId(142553), CltvExpiryDelta(42), 15 msat, 575 msat, 53, Channel.MAX_FUNDING_WITHOUT_WUMBO.toMilliSatoshi)

View File

@ -6,12 +6,12 @@ import fr.acinq.bitcoin.scalacompat.DeterministicWallet.KeyPath
import fr.acinq.bitcoin.scalacompat.{OutPoint, SatoshiLong}
import fr.acinq.eclair.TestUtils.randomTxId
import fr.acinq.eclair.blockchain.fee.FeeratePerKw
import fr.acinq.eclair.channel.Origin
import fr.acinq.eclair.channel.{Origin, Upstream}
import fr.acinq.eclair.transactions.{CommitmentSpec, DirectedHtlc, IncomingHtlc, OutgoingHtlc}
import fr.acinq.eclair.wire.internal.channel.version0.ChannelTypes0.ChannelVersion
import fr.acinq.eclair.wire.internal.channel.version1.ChannelCodecs1.Codecs._
import fr.acinq.eclair.wire.protocol.UpdateAddHtlc
import fr.acinq.eclair.{CltvExpiry, MilliSatoshi, MilliSatoshiLong, TestConstants, randomBytes32}
import fr.acinq.eclair.{CltvExpiry, MilliSatoshi, MilliSatoshiLong, TestConstants, TimestampMilli, randomBytes32}
import org.scalatest.funsuite.AnyFunSuite
import scodec.bits._
import scodec.{Attempt, DecodeResult}
@ -117,14 +117,14 @@ class ChannelCodecs1Spec extends AnyFunSuite {
test("encode/decode origin") {
val replyTo = TestProbe("replyTo")(ActorSystem("system")).ref
val localHot = Origin.LocalHot(replyTo, UUID.randomUUID())
val localCold = Origin.LocalCold(localHot.id)
val localHot = Origin.Hot(replyTo, Upstream.Local(UUID.randomUUID()))
val localCold = Origin.Cold(localHot)
assert(originCodec.decodeValue(originCodec.encode(localHot).require).require == localCold)
assert(originCodec.decodeValue(originCodec.encode(localCold).require).require == localCold)
val add = UpdateAddHtlc(randomBytes32(), 4324, 11000000 msat, randomBytes32(), CltvExpiry(400000), TestConstants.emptyOnionPacket, None)
val relayedHot = Origin.ChannelRelayedHot(replyTo, add, 11000000 msat)
val relayedCold = Origin.ChannelRelayedCold(add.channelId, add.id, add.amountMsat, relayedHot.amountOut)
val relayedHot = Origin.Hot(replyTo, Upstream.Hot.Channel(add, TimestampMilli(0)))
val relayedCold = Origin.Cold(Upstream.Cold.Channel(add.channelId, add.id, add.amountMsat))
assert(originCodec.decodeValue(originCodec.encode(relayedHot).require).require == relayedCold)
assert(originCodec.decodeValue(originCodec.encode(relayedCold).require).require == relayedCold)
@ -133,23 +133,25 @@ class ChannelCodecs1Spec extends AnyFunSuite {
UpdateAddHtlc(randomBytes32(), 1L, 2000 msat, randomBytes32(), CltvExpiry(400000), TestConstants.emptyOnionPacket, None),
UpdateAddHtlc(randomBytes32(), 2L, 3000 msat, randomBytes32(), CltvExpiry(400000), TestConstants.emptyOnionPacket, None),
)
val trampolineRelayedHot = Origin.TrampolineRelayedHot(replyTo, adds)
val trampolineRelayedCold = Origin.TrampolineRelayedCold(trampolineRelayedHot.htlcs)
assert(originCodec.decodeValue(originCodec.encode(trampolineRelayedHot).require).require == trampolineRelayedCold)
assert(originCodec.decodeValue(originCodec.encode(trampolineRelayedCold).require).require == trampolineRelayedCold)
val trampolineRelayedHot = Origin.Hot(replyTo, Upstream.Hot.Trampoline(adds.map(add => Upstream.Hot.Channel(add, TimestampMilli(0)))))
// We didn't encode the incoming HTLC amount.
val trampolineRelayed = Origin.Cold(Upstream.Cold.Trampoline(adds.map(add => Upstream.Cold.Channel(add.channelId, add.id, 0 msat)).toList))
assert(originCodec.decodeValue(originCodec.encode(trampolineRelayedHot).require).require == trampolineRelayed)
assert(originCodec.decodeValue(originCodec.encode(trampolineRelayed).require).require == trampolineRelayed)
}
test("encode/decode map of origins") {
val map = Map(
1L -> Origin.LocalCold(UUID.randomUUID()),
42L -> Origin.ChannelRelayedCold(randomBytes32(), 4324, 12000000 msat, 11000000 msat),
43L -> Origin.TrampolineRelayedCold((randomBytes32(), 17L) :: (randomBytes32(), 21L) :: (randomBytes32(), 21L) :: Nil),
130L -> Origin.ChannelRelayedCold(randomBytes32(), -45, 13000000 msat, 12000000 msat),
140L -> Origin.TrampolineRelayedCold((randomBytes32(), 0L) :: Nil),
1000L -> Origin.ChannelRelayedCold(randomBytes32(), 10, 14000000 msat, 13000000 msat),
-32L -> Origin.ChannelRelayedCold(randomBytes32(), 54, 15000000 msat, 14000000 msat),
-54L -> Origin.TrampolineRelayedCold((randomBytes32(), 1L) :: (randomBytes32(), 2L) :: Nil),
-4L -> Origin.LocalCold(UUID.randomUUID()))
1L -> Origin.Cold(Upstream.Local(UUID.randomUUID())),
42L -> Origin.Cold(Upstream.Cold.Channel(randomBytes32(), 4324, 12_000_000 msat)),
43L -> Origin.Cold(Upstream.Cold.Trampoline(Upstream.Cold.Channel(randomBytes32(), 17, 0 msat) :: Upstream.Cold.Channel(randomBytes32(), 21, 0 msat) :: Upstream.Cold.Channel(randomBytes32(), 21, 0 msat) :: Nil)),
130L -> Origin.Cold(Upstream.Cold.Channel(randomBytes32(), -45, 13_000_000 msat)),
140L -> Origin.Cold(Upstream.Cold.Trampoline(Upstream.Cold.Channel(randomBytes32(), 0, 0 msat) :: Nil)),
1000L -> Origin.Cold(Upstream.Cold.Channel(randomBytes32(), 10, 14_000_000 msat)),
-32L -> Origin.Cold(Upstream.Cold.Channel(randomBytes32(), 54, 15_000_000 msat)),
-54L -> Origin.Cold(Upstream.Cold.Trampoline(Upstream.Cold.Channel(randomBytes32(), 1, 0 msat) :: Upstream.Cold.Channel(randomBytes32(), 2, 0 msat) :: Nil)),
-4L -> Origin.Cold(Upstream.Local(UUID.randomUUID()))
)
assert(originsMapCodec.decodeValue(originsMapCodec.encode(map).require).require == map)
}

View File

@ -201,4 +201,25 @@ class ChannelCodecs4Spec extends AnyFunSuite {
assert(!nonInitiatorDecoded.commitments.params.localParams.paysCommitTxFees)
}
test("encode/decode cold origins") {
val origins = Map(
13L -> Origin.Cold(Upstream.Cold.Channel(randomBytes32(), 2765, 1863 msat)),
27L -> Origin.Cold(Upstream.Cold.Trampoline(Upstream.Cold.Channel(randomBytes32(), 541, 6500 msat) :: Nil)),
28L -> Origin.Cold(Upstream.Cold.Trampoline(Upstream.Cold.Channel(randomBytes32(), 0, 0 msat) :: Upstream.Cold.Channel(randomBytes32(), 6778, 250_000_001 msat) :: Nil)),
)
val encoded = originsMapCodec.encode(origins).require
val decoded = originsMapCodec.decode(encoded).require.value
assert(decoded == origins)
}
test("decode origin backwards compatibility") {
// The following values were encoded with eclair v0.10.0.
val channelRelayed = Origin.Cold(Upstream.Cold.Channel(ByteVector32(hex"fe99ac49738d44ff8b73d8e5da01e868584a2071326320e8f51fe8bdbbe84c64"), 1561, 1721 msat))
val channelRelayedBin = hex"0002fe99ac49738d44ff8b73d8e5da01e868584a2071326320e8f51fe8bdbbe84c64000000000000061900000000000006b900000000000005dc"
assert(originCodec.decode(channelRelayedBin.bits).require.value == channelRelayed)
val trampolineRelayed = Origin.Cold(Upstream.Cold.Trampoline(Upstream.Cold.Channel(ByteVector32(hex"19a3e2f5b1d747e6e59cec57d927c068c976eab0b914a1bf66aaacaa0917d49d"), 17, 0 msat) :: Upstream.Cold.Channel(ByteVector32(hex"4fbf1090bf27e4ef3af8b784f8e0e62dd2fc836775131d6e58400a68ec8fcf2c"), 21, 0 msat) :: Nil))
val trampolineRelayedBin = hex"0004000219a3e2f5b1d747e6e59cec57d927c068c976eab0b914a1bf66aaacaa0917d49d00000000000000114fbf1090bf27e4ef3af8b784f8e0e62dd2fc836775131d6e58400a68ec8fcf2c0000000000000015"
assert(originCodec.decode(trampolineRelayedBin.bits).require.value == trampolineRelayed)
}
}