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:
parent
c53b32c781
commit
791edf78b6
@ -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
|
||||
|
@ -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
|
||||
|
@ -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))
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -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))
|
||||
|
@ -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
|
||||
|
@ -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)
|
||||
}
|
||||
}
|
||||
|
@ -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
|
||||
|
@ -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))
|
||||
}
|
||||
|
@ -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)
|
||||
}
|
||||
|
@ -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
|
||||
|
@ -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
|
||||
|
@ -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
|
||||
|
@ -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)
|
||||
|
||||
|
@ -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)
|
||||
|
||||
|
@ -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)
|
||||
|
||||
|
@ -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)
|
||||
|
||||
|
@ -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)
|
||||
|
||||
|
@ -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 {
|
||||
|
@ -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"
|
||||
|
@ -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
|
||||
}
|
||||
|
||||
|
@ -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)
|
||||
|
@ -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
|
||||
}
|
||||
|
||||
|
@ -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
|
||||
|
@ -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)
|
||||
|
@ -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]]
|
||||
|
@ -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]
|
||||
|
@ -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))
|
||||
|
@ -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
|
||||
}
|
||||
|
||||
|
@ -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
|
||||
|
@ -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}
|
||||
|
@ -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)
|
||||
|
@ -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))
|
||||
}
|
||||
|
||||
|
@ -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)
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -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
|
||||
|
@ -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)
|
||||
}
|
||||
}
|
||||
|
@ -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())),
|
||||
|
@ -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)
|
||||
|
@ -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)
|
||||
}
|
||||
|
||||
|
@ -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)
|
||||
}
|
||||
|
||||
}
|
||||
|
Loading…
Reference in New Issue
Block a user