Add duration information to payment events (#3241)
What changed, and why it matters
This commit is a refactoring and observability improvement for the Eclair Lightning node. It adds start/end timestamps to payment events and failures, renames some timestamp fields for consistency, and updates how payment hashes are derived from preimages. There is no direct security vulnerability visible in the diff; the changes are primarily for monitoring, metrics, and database/event-stream hygiene. A few places now use the current time instead of a previously stored request time when recording relay settlement, which is a behavioral change but not an obvious exploit.
No immediate security action is required. Reviewers should verify that the timestamp changes do not break accounting, audit, or downstream consumers that relied on the old `timestamp` field. The TODOs about pending DB schema changes should be tracked to completion so that `startedAt` is persisted correctly rather than duplicated from `settledAt`.
Security signals we found
Refactoring of payment event data model with new timestamp fields
Payment hash now derived from preimage instead of passed separately
Removal of WaitingToRelayPayment from JSON type hints
ChannelRelay settlement timestamp changed from request-received time to current time
No DB schema migration in this commit; TODOs note future schema update
Evidence from the diff
The commit refactors payment event classes in PaymentEvents.scala to track startedAt/settledAt (and receivedAt/failedAt) instead of a single timestamp. It updates constructors across the payment send/receive/relay lifecycle, DB serializers (Postgres and SQLite), JSON serializers, and tests. Notable changes: PaymentSent no longer stores paymentHash as a constructor field but derives it via Crypto.sha256(paymentPreimage); PaymentSettlingOnChain gains a channelId and loses the PaymentEvent trait; WaitingToRelayPayment is removed from type hints; ChannelRelay now uses TimestampMilli.now() for the downstream settled time rather than r.receivedAt. The DB schema is explicitly not updated yet (noted in TODOs), so audit DB reads duplicate settledAt into startedAt.
Changed components
eclair-core payment event modelPostgres and SQLite audit/payments databasesJSON serializers for payment eventsPayment send lifecycle (single-part, multi-part, trampoline)Payment receive and relay componentsWeakEntropyPool event subscriptionInspect captured patch +407 / −355
diff --git a/eclair-core/src/main/scala/fr/acinq/eclair/channel/fsm/Channel.scala b/eclair-core/src/main/scala/fr/acinq/eclair/channel/fsm/Channel.scala
index f3e10e0..c6e138a 100644
--- a/eclair-core/src/main/scala/fr/acinq/eclair/channel/fsm/Channel.scala
+++ b/eclair-core/src/main/scala/fr/acinq/eclair/channel/fsm/Channel.scala
@@ -2276,7 +2276,7 @@ class Channel(val nodeParams: NodeParams, val channelKeys: ChannelKeys, val wall
Closing
.onChainOutgoingHtlcs(d.commitments.latest.localCommit, d.commitments.latest.remoteCommit, d.commitments.latest.nextRemoteCommit_opt, tx)
.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)) }
+ .collect { case (add, Some(id)) => context.system.eventStream.publish(PaymentSettlingOnChain(id, d.channelId, add.amountMsat, add.paymentHash)) }
// finally, if one of the unilateral closes is done, we move to CLOSED state, otherwise we stay()
Closing.isClosed(d1, Some(tx)) match {
case Some(closingType) =>
diff --git a/eclair-core/src/main/scala/fr/acinq/eclair/crypto/WeakEntropyPool.scala b/eclair-core/src/main/scala/fr/acinq/eclair/crypto/WeakEntropyPool.scala
index cd34acc..303b3ef 100644
--- a/eclair-core/src/main/scala/fr/acinq/eclair/crypto/WeakEntropyPool.scala
+++ b/eclair-core/src/main/scala/fr/acinq/eclair/crypto/WeakEntropyPool.scala
@@ -57,7 +57,7 @@ object WeakEntropyPool {
def apply(collector: EntropyCollector): Behavior[Command] = {
Behaviors.setup { context =>
context.system.eventStream ! EventStream.Subscribe(context.messageAdapter[NewBlock](e => WrappedNewBlock(e.blockId)))
- context.system.eventStream ! EventStream.Subscribe(context.messageAdapter[ChannelPaymentRelayed](e => WrappedPaymentRelayed(e.paymentHash, e.timestamp)))
+ context.system.eventStream ! EventStream.Subscribe(context.messageAdapter[ChannelPaymentRelayed](e => WrappedPaymentRelayed(e.paymentHash, e.settledAt)))
context.system.eventStream ! EventStream.Subscribe(context.messageAdapter[PeerConnected](e => WrappedPeerConnected(e.nodeId)))
context.system.eventStream ! EventStream.Subscribe(context.messageAdapter[NodeUpdated](e => WrappedNodeUpdated(e.ann.signature)))
context.system.eventStream ! EventStream.Subscribe(context.messageAdapter[ChannelSignatureReceived](e => WrappedChannelSignature(e.commitments.latest.localCommit.remoteSig)))
diff --git a/eclair-core/src/main/scala/fr/acinq/eclair/db/PaymentsDb.scala b/eclair-core/src/main/scala/fr/acinq/eclair/db/PaymentsDb.scala
index 53ce18f..6e0747b 100644
--- a/eclair-core/src/main/scala/fr/acinq/eclair/db/PaymentsDb.scala
+++ b/eclair-core/src/main/scala/fr/acinq/eclair/db/PaymentsDb.scala
@@ -249,8 +249,8 @@ object FailureType extends Enumeration {
object FailureSummary {
def apply(f: PaymentFailure): FailureSummary = f match {
case LocalFailure(_, route, t) => FailureSummary(FailureType.LOCAL, t.getMessage, route.map(h => HopSummary(h)).toList, route.headOption.map(_.nodeId))
- case RemoteFailure(_, route, e) => FailureSummary(FailureType.REMOTE, e.failureMessage.message, route.map(h => HopSummary(h)).toList, Some(e.originNode))
- case UnreadableRemoteFailure(_, route, _, _) => FailureSummary(FailureType.UNREADABLE_REMOTE, "could not decrypt failure onion", route.map(h => HopSummary(h)).toList, None)
+ case RemoteFailure(_, route, e, _, _) => FailureSummary(FailureType.REMOTE, e.failureMessage.message, route.map(h => HopSummary(h)).toList, Some(e.originNode))
+ case UnreadableRemoteFailure(_, route, _, _, _, _) => FailureSummary(FailureType.UNREADABLE_REMOTE, "could not decrypt failure onion", route.map(h => HopSummary(h)).toList, None)
}
}
diff --git a/eclair-core/src/main/scala/fr/acinq/eclair/db/pg/PgAuditDb.scala b/eclair-core/src/main/scala/fr/acinq/eclair/db/pg/PgAuditDb.scala
index 8eceb18..3623a0c 100644
--- a/eclair-core/src/main/scala/fr/acinq/eclair/db/pg/PgAuditDb.scala
+++ b/eclair-core/src/main/scala/fr/acinq/eclair/db/pg/PgAuditDb.scala
@@ -215,7 +215,7 @@ class PgAuditDb(implicit ds: DataSource) extends AuditDb with Logging {
statement.setString(7, e.paymentPreimage.toHex)
statement.setString(8, e.recipientNodeId.value.toHex)
statement.setString(9, p.toChannelId.toHex)
- statement.setTimestamp(10, p.timestamp.toSqlTimestamp)
+ statement.setTimestamp(10, p.settledAt.toSqlTimestamp)
statement.addBatch()
})
statement.executeBatch()
@@ -230,7 +230,7 @@ class PgAuditDb(implicit ds: DataSource) extends AuditDb with Logging {
statement.setLong(1, p.amount.toLong)
statement.setString(2, e.paymentHash.toHex)
statement.setString(3, p.fromChannelId.toHex)
- statement.setTimestamp(4, p.timestamp.toSqlTimestamp)
+ statement.setTimestamp(4, p.receivedAt.toSqlTimestamp)
statement.addBatch()
})
statement.executeBatch()
@@ -251,7 +251,7 @@ class PgAuditDb(implicit ds: DataSource) extends AuditDb with Logging {
statement.setString(1, e.paymentHash.toHex)
statement.setLong(2, nextTrampolineAmount.toLong)
statement.setString(3, nextTrampolineNodeId.value.toHex)
- statement.setTimestamp(4, e.timestamp.toSqlTimestamp)
+ statement.setTimestamp(4, e.settledAt.toSqlTimestamp)
statement.executeUpdate()
}
// trampoline relayed payments do MPP aggregation and may have M inputs and N outputs
@@ -362,20 +362,22 @@ class PgAuditDb(implicit ds: DataSource) extends AuditDb with Logging {
MilliSatoshi(rs.getLong("fees_msat")),
rs.getByteVector32FromHex("to_channel_id"),
None, // we don't store the route in the audit DB
- TimestampMilli.fromSqlTimestamp(rs.getTimestamp("timestamp")))
+ // TODO: store startedAt when updating the DB schema instead of duplicating settledAt.
+ startedAt = TimestampMilli.fromSqlTimestamp(rs.getTimestamp("timestamp")),
+ settledAt = TimestampMilli.fromSqlTimestamp(rs.getTimestamp("timestamp")))
val sent = sentByParentId.get(parentId) match {
case Some(s) => s.copy(parts = s.parts :+ part)
case None => PaymentSent(
parentId,
- rs.getByteVector32FromHex("payment_hash"),
rs.getByteVector32FromHex("payment_preimage"),
MilliSatoshi(rs.getLong("recipient_amount_msat")),
PublicKey(rs.getByteVectorFromHex("recipient_node_id")),
Seq(part),
- None)
+ None,
+ part.startedAt)
}
sentByParentId + (parentId -> sent)
- }.values.toSeq.sortBy(_.timestamp)
+ }.values.toSeq.sortBy(_.settledAt)
paginated_opt match {
case Some(paginated) => result.slice(paginated.skip, paginated.skip + paginated.count)
case None => result
@@ -400,7 +402,7 @@ class PgAuditDb(implicit ds: DataSource) extends AuditDb with Logging {
case None => PaymentReceived(paymentHash, Seq(part))
}
receivedByHash + (paymentHash -> received)
- }.values.toSeq.sortBy(_.timestamp)
+ }.values.toSeq.sortBy(_.settledAt)
paginated_opt match {
case Some(paginated) => result.slice(paginated.skip, paginated.skip + paginated.count)
case None => result
@@ -454,7 +456,7 @@ class PgAuditDb(implicit ds: DataSource) extends AuditDb with Logging {
Seq(OnTheFlyFundingPaymentRelayed(paymentHash, incoming, outgoing))
case _ => Nil
}
- }.toSeq.sortBy(_.timestamp)
+ }.toSeq.sortBy(_.settledAt)
paginated_opt match {
case Some(paginated) => result.slice(paginated.skip, paginated.skip + paginated.count)
case None => result
diff --git a/eclair-core/src/main/scala/fr/acinq/eclair/db/pg/PgPaymentsDb.scala b/eclair-core/src/main/scala/fr/acinq/eclair/db/pg/PgPaymentsDb.scala
index 27af66e..2be4ddc 100644
--- a/eclair-core/src/main/scala/fr/acinq/eclair/db/pg/PgPaymentsDb.scala
+++ b/eclair-core/src/main/scala/fr/acinq/eclair/db/pg/PgPaymentsDb.scala
@@ -140,7 +140,7 @@ class PgPaymentsDb(implicit ds: DataSource, lock: PgLock) extends PaymentsDb wit
withLock { pg =>
using(pg.prepareStatement("UPDATE payments.sent SET (completed_at, payment_preimage, fees_msat, payment_route) = (?, ?, ?, ?) WHERE id = ? AND completed_at IS NULL")) { statement =>
paymentResult.parts.foreach(p => {
- statement.setTimestamp(1, p.timestamp.toSqlTimestamp)
+ statement.setTimestamp(1, p.settledAt.toSqlTimestamp)
statement.setString(2, paymentResult.paymentPreimage.toHex)
statement.setLong(3, p.feesPaid.toLong)
statement.setBytes(4, encodeRoute(p.route.getOrElse(Nil).map(h => HopSummary(h)).toList))
@@ -155,7 +155,7 @@ class PgPaymentsDb(implicit ds: DataSource, lock: PgLock) extends PaymentsDb wit
override def updateOutgoingPayment(paymentResult: PaymentFailed): Unit = withMetrics("payments/update-outgoing-failed", DbBackends.Postgres) {
withLock { pg =>
using(pg.prepareStatement("UPDATE payments.sent SET (completed_at, failures) = (?, ?) WHERE id = ? AND completed_at IS NULL")) { statement =>
- statement.setTimestamp(1, paymentResult.timestamp.toSqlTimestamp)
+ statement.setTimestamp(1, paymentResult.settledAt.toSqlTimestamp)
statement.setBytes(2, encodeFailures(paymentResult.failures.map(f => FailureSummary(f)).toList))
statement.setString(3, paymentResult.id.toString)
if (statement.executeUpdate() == 0) throw new IllegalArgumentException(s"Tried to mark an outgoing payment as failed but already in final status (id=${paymentResult.id})")
diff --git a/eclair-core/src/main/scala/fr/acinq/eclair/db/sqlite/SqliteAuditDb.scala b/eclair-core/src/main/scala/fr/acinq/eclair/db/sqlite/SqliteAuditDb.scala
index e5cce5b..b69d2d9 100644
--- a/eclair-core/src/main/scala/fr/acinq/eclair/db/sqlite/SqliteAuditDb.scala
+++ b/eclair-core/src/main/scala/fr/acinq/eclair/db/sqlite/SqliteAuditDb.scala
@@ -207,7 +207,7 @@ class SqliteAuditDb(val sqlite: Connection) extends AuditDb with Logging {
statement.setBytes(7, e.paymentPreimage.toArray)
statement.setBytes(8, e.recipientNodeId.value.toArray)
statement.setBytes(9, p.toChannelId.toArray)
- statement.setLong(10, p.timestamp.toLong)
+ statement.setLong(10, p.settledAt.toLong)
statement.addBatch()
})
statement.executeBatch()
@@ -220,7 +220,7 @@ class SqliteAuditDb(val sqlite: Connection) extends AuditDb with Logging {
statement.setLong(1, p.amount.toLong)
statement.setBytes(2, e.paymentHash.toArray)
statement.setBytes(3, p.fromChannelId.toArray)
- statement.setLong(4, p.timestamp.toLong)
+ statement.setLong(4, p.receivedAt.toLong)
statement.addBatch()
})
statement.executeBatch()
@@ -239,7 +239,7 @@ class SqliteAuditDb(val sqlite: Connection) extends AuditDb with Logging {
statement.setBytes(1, e.paymentHash.toArray)
statement.setLong(2, nextTrampolineAmount.toLong)
statement.setBytes(3, nextTrampolineNodeId.value.toArray)
- statement.setLong(4, e.timestamp.toLong)
+ statement.setLong(4, e.settledAt.toLong)
statement.executeUpdate()
}
// trampoline relayed payments do MPP aggregation and may have M inputs and N outputs
@@ -336,20 +336,22 @@ class SqliteAuditDb(val sqlite: Connection) extends AuditDb with Logging {
MilliSatoshi(rs.getLong("fees_msat")),
rs.getByteVector32("to_channel_id"),
None, // we don't store the route in the audit DB
- TimestampMilli(rs.getLong("timestamp")))
+ // TODO: store startedAt when updating the DB schema instead of duplicating settledAt.
+ startedAt = TimestampMilli(rs.getLong("timestamp")),
+ settledAt = TimestampMilli(rs.getLong("timestamp")))
val sent = sentByParentId.get(parentId) match {
case Some(s) => s.copy(parts = s.parts :+ part)
case None => PaymentSent(
parentId,
- rs.getByteVector32("payment_hash"),
rs.getByteVector32("payment_preimage"),
MilliSatoshi(rs.getLong("recipient_amount_msat")),
PublicKey(rs.getByteVector("recipient_node_id")),
Seq(part),
- None)
+ None,
+ part.startedAt)
}
sentByParentId + (parentId -> sent)
- }.values.toSeq.sortBy(_.timestamp)
+ }.values.toSeq.sortBy(_.settledAt)
paginated_opt match {
case Some(paginated) => result.slice(paginated.skip, paginated.skip + paginated.count)
case None => result
@@ -372,7 +374,7 @@ class SqliteAuditDb(val sqlite: Connection) extends AuditDb with Logging {
case None => PaymentReceived(paymentHash, Seq(part))
}
receivedByHash + (paymentHash -> received)
- }.values.toSeq.sortBy(_.timestamp)
+ }.values.toSeq.sortBy(_.settledAt)
paginated_opt match {
case Some(paginated) => result.slice(paginated.skip, paginated.skip + paginated.count)
case None => result
@@ -425,7 +427,7 @@ class SqliteAuditDb(val sqlite: Connection) extends AuditDb with Logging {
Seq(OnTheFlyFundingPaymentRelayed(paymentHash, incoming, outgoing))
case _ => Nil
}
- }.toSeq.sortBy(_.timestamp)
+ }.toSeq.sortBy(_.settledAt)
paginated_opt match {
case Some(paginated) => result.slice(paginated.skip, paginated.skip + paginated.count)
case None => result
diff --git a/eclair-core/src/main/scala/fr/acinq/eclair/db/sqlite/SqlitePaymentsDb.scala b/eclair-core/src/main/scala/fr/acinq/eclair/db/sqlite/SqlitePaymentsDb.scala
index d080083..27c0587 100644
--- a/eclair-core/src/main/scala/fr/acinq/eclair/db/sqlite/SqlitePaymentsDb.scala
+++ b/eclair-core/src/main/scala/fr/acinq/eclair/db/sqlite/SqlitePaymentsDb.scala
@@ -168,7 +168,7 @@ class SqlitePaymentsDb(val sqlite: Connection) extends PaymentsDb with Logging {
override def updateOutgoingPayment(paymentResult: PaymentSent): Unit = withMetrics("payments/update-outgoing-sent", DbBackends.Sqlite) {
using(sqlite.prepareStatement("UPDATE sent_payments SET (completed_at, payment_preimage, fees_msat, payment_route) = (?, ?, ?, ?) WHERE id = ? AND completed_at IS NULL")) { statement =>
paymentResult.parts.foreach(p => {
- statement.setLong(1, p.timestamp.toLong)
+ statement.setLong(1, p.settledAt.toLong)
statement.setBytes(2, paymentResult.paymentPreimage.toArray)
statement.setLong(3, p.feesPaid.toLong)
statement.setBytes(4, encodeRoute(p.route.getOrElse(Nil).map(h => HopSummary(h)).toList))
@@ -181,7 +181,7 @@ class SqlitePaymentsDb(val sqlite: Connection) extends PaymentsDb with Logging {
override def updateOutgoingPayment(paymentResult: PaymentFailed): Unit = withMetrics("payments/update-outgoing-failed", DbBackends.Sqlite) {
using(sqlite.prepareStatement("UPDATE sent_payments SET (completed_at, failures) = (?, ?) WHERE id = ? AND completed_at IS NULL")) { statement =>
- statement.setLong(1, paymentResult.timestamp.toLong)
+ statement.setLong(1, paymentResult.settledAt.toLong)
statement.setBytes(2, encodeFailures(paymentResult.failures.map(f => FailureSummary(f)).toList))
statement.setString(3, paymentResult.id.toString)
if (statement.executeUpdate() == 0) throw new IllegalArgumentException(s"Tried to mark an outgoing payment as failed but already in final status (id=${paymentResult.id})")
diff --git a/eclair-core/src/main/scala/fr/acinq/eclair/json/JsonSerializers.scala b/eclair-core/src/main/scala/fr/acinq/eclair/json/JsonSerializers.scala
index f37d66e..d81e9b4 100644
--- a/eclair-core/src/main/scala/fr/acinq/eclair/json/JsonSerializers.scala
+++ b/eclair-core/src/main/scala/fr/acinq/eclair/json/JsonSerializers.scala
@@ -375,7 +375,7 @@ object PaymentFailedSummarySerializer extends ConvertClassSerializer[PaymentFail
val route = f.route.map(_.nodeId) ++ f.route.lastOption.map(_.nextNodeId)
val message = f match {
case LocalFailure(_, _, t) => t.getMessage
- case RemoteFailure(_, _, Sphinx.DecryptedFailurePacket(origin, _, failureMessage)) => s"$origin returned: ${failureMessage.message}"
+ case RemoteFailure(_, _, Sphinx.DecryptedFailurePacket(origin, _, failureMessage), _, _) => s"$origin returned: ${failureMessage.message}"
case _: UnreadableRemoteFailure => "unreadable remote failure"
}
PaymentFailureSummaryJson(f.amount, route, message)
@@ -383,6 +383,9 @@ object PaymentFailedSummarySerializer extends ConvertClassSerializer[PaymentFail
))
// @formatter:on
+private case class PaymentSentJson(id: UUID, paymentHash: ByteVector32, paymentPreimage: ByteVector32, recipientAmount: MilliSatoshi, recipientNodeId: PublicKey, parts: Seq[PaymentSent.PartialPayment], fees: MilliSatoshi, startedAt: TimestampMilli, settledAt: TimestampMilli)
+object PaymentSentSerializer extends ConvertClassSerializer[PaymentSent](p => PaymentSentJson(p.id, p.paymentHash, p.paymentPreimage, p.recipientAmount, p.recipientNodeId, p.parts, p.feesPaid, p.startedAt, p.settledAt))
+
object ThrowableSerializer extends MinimalSerializer({
case t: Throwable if t.getMessage != null => JString(t.getMessage)
case t: Throwable => JString(t.getClass.getSimpleName)
@@ -680,10 +683,11 @@ object CustomTypeHints {
val paymentEvent: CustomTypeHints = CustomTypeHints(Map(
classOf[PaymentSent] -> "payment-sent",
+ classOf[PaymentSentJson] -> "payment-sent",
classOf[ChannelPaymentRelayed] -> "payment-relayed",
classOf[TrampolinePaymentRelayed] -> "trampoline-payment-relayed",
+ classOf[OnTheFlyFundingPaymentRelayed] -> "on-the-fly-funding-payment-relayed",
classOf[PaymentReceived] -> "payment-received",
- classOf[PaymentSettlingOnChain] -> "payment-settling-onchain",
classOf[PaymentFailed] -> "payment-failed",
))
@@ -795,6 +799,7 @@ object JsonSerializers {
GlobalBalanceSerializer +
PeerInfoSerializer +
PaymentFailedSummarySerializer +
+ PaymentSentSerializer +
OnionMessageReceivedSerializer +
ShortIdAliasesSerializer +
FundingTxStatusSerializer +
diff --git a/eclair-core/src/main/scala/fr/acinq/eclair/payment/Monitoring.scala b/eclair-core/src/main/scala/fr/acinq/eclair/payment/Monitoring.scala
index 3ffa7e0..5ccba39 100644
--- a/eclair-core/src/main/scala/fr/acinq/eclair/payment/Monitoring.scala
+++ b/eclair-core/src/main/scala/fr/acinq/eclair/payment/Monitoring.scala
@@ -135,7 +135,7 @@ object Monitoring {
def apply(pf: PaymentFailure): String = pf match {
case LocalFailure(_, _, t) => t.getClass.getSimpleName
- case RemoteFailure(_, _, e) => e.failureMessage.getClass.getSimpleName
+ case RemoteFailure(_, _, e, _, _) => e.failureMessage.getClass.getSimpleName
case _: UnreadableRemoteFailure => "UnreadableRemoteFailure"
}
}
diff --git a/eclair-core/src/main/scala/fr/acinq/eclair/payment/PaymentEvents.scala b/eclair-core/src/main/scala/fr/acinq/eclair/payment/PaymentEvents.scala
index eec22e5..e1a9477 100644
--- a/eclair-core/src/main/scala/fr/acinq/eclair/payment/PaymentEvents.scala
+++ b/eclair-core/src/main/scala/fr/acinq/eclair/payment/PaymentEvents.scala
@@ -16,8 +16,8 @@
package fr.acinq.eclair.payment
-import fr.acinq.bitcoin.scalacompat.ByteVector32
import fr.acinq.bitcoin.scalacompat.Crypto.PublicKey
+import fr.acinq.bitcoin.scalacompat.{ByteVector32, Crypto}
import fr.acinq.eclair.crypto.Sphinx
import fr.acinq.eclair.crypto.Sphinx.HoldTime
import fr.acinq.eclair.payment.Invoice.ExtraEdge
@@ -39,8 +39,12 @@ import scala.util.{Failure, Success, Try}
*/
sealed trait PaymentEvent {
- val paymentHash: ByteVector32
- val timestamp: TimestampMilli
+ // @formatter:off
+ def paymentHash: ByteVector32
+ def startedAt: TimestampMilli
+ def settledAt: TimestampMilli
+ def duration: FiniteDuration = settledAt - startedAt
+ // @formatter:on
}
/**
@@ -48,18 +52,18 @@ sealed trait PaymentEvent {
*
* @param id id of the whole payment attempt (if using multi-part, there will be multiple parts,
* each with a different id).
- * @param paymentHash payment hash.
* @param paymentPreimage payment preimage (proof of payment).
* @param recipientAmount amount that has been received by the final recipient.
* @param recipientNodeId id of the final recipient.
* @param parts child payments (actual outgoing HTLCs).
* @param remainingAttribution_opt for relayed trampoline payments, the attribution data that needs to be sent upstream
*/
-case class PaymentSent(id: UUID, paymentHash: ByteVector32, paymentPreimage: ByteVector32, recipientAmount: MilliSatoshi, recipientNodeId: PublicKey, parts: Seq[PaymentSent.PartialPayment], remainingAttribution_opt: Option[ByteVector]) extends PaymentEvent {
+case class PaymentSent(id: UUID, paymentPreimage: ByteVector32, recipientAmount: MilliSatoshi, recipientNodeId: PublicKey, parts: Seq[PaymentSent.PartialPayment], remainingAttribution_opt: Option[ByteVector], startedAt: TimestampMilli) extends PaymentEvent {
require(parts.nonEmpty, "must have at least one payment part")
+ val paymentHash: ByteVector32 = Crypto.sha256(paymentPreimage)
val amountWithFees: MilliSatoshi = parts.map(_.amountWithFees).sum
val feesPaid: MilliSatoshi = amountWithFees - recipientAmount // overall fees for this payment
- val timestamp: TimestampMilli = parts.map(_.timestamp).min // we use min here because we receive the proof of payment as soon as the first partial payment is fulfilled
+ val settledAt: TimestampMilli = parts.map(_.settledAt).max
}
object PaymentSent {
@@ -72,42 +76,40 @@ object PaymentSent {
* @param feesPaid fees paid to route to the target node.
* @param toChannelId id of the channel used.
* @param route payment route used.
- * @param timestamp absolute time in milli-seconds since UNIX epoch when the payment was fulfilled.
+ * @param startedAt absolute time in milliseconds since UNIX epoch when the payment was started.
+ * @param settledAt absolute time in milliseconds since UNIX epoch when the payment was fulfilled.
*/
- case class PartialPayment(id: UUID, amount: MilliSatoshi, feesPaid: MilliSatoshi, toChannelId: ByteVector32, route: Option[Seq[Hop]], timestamp: TimestampMilli = TimestampMilli.now()) {
+ case class PartialPayment(id: UUID, amount: MilliSatoshi, feesPaid: MilliSatoshi, toChannelId: ByteVector32, route: Option[Seq[Hop]], startedAt: TimestampMilli, settledAt: TimestampMilli) {
require(route.isEmpty || route.get.nonEmpty, "route must be None or contain at least one hop")
val amountWithFees: MilliSatoshi = amount + feesPaid
+ val duration: FiniteDuration = settledAt - startedAt
}
}
-case class PaymentFailed(id: UUID, paymentHash: ByteVector32, failures: Seq[PaymentFailure], timestamp: TimestampMilli = TimestampMilli.now()) extends PaymentEvent
+case class PaymentFailed(id: UUID, paymentHash: ByteVector32, failures: Seq[PaymentFailure], startedAt: TimestampMilli, settledAt: TimestampMilli = TimestampMilli.now()) extends PaymentEvent
sealed trait PaymentRelayed extends PaymentEvent {
val amountIn: MilliSatoshi
val amountOut: MilliSatoshi
- val receivedAt: TimestampMilli
+ val startedAt: TimestampMilli
val settledAt: TimestampMilli
}
-case class ChannelPaymentRelayed(amountIn: MilliSatoshi, amountOut: MilliSatoshi, paymentHash: ByteVector32, fromChannelId: ByteVector32, toChannelId: ByteVector32, receivedAt: TimestampMilli, settledAt: TimestampMilli) extends PaymentRelayed {
- override val timestamp: TimestampMilli = settledAt
-}
+case class ChannelPaymentRelayed(amountIn: MilliSatoshi, amountOut: MilliSatoshi, paymentHash: ByteVector32, fromChannelId: ByteVector32, toChannelId: ByteVector32, startedAt: TimestampMilli, settledAt: TimestampMilli) extends PaymentRelayed
case class TrampolinePaymentRelayed(paymentHash: ByteVector32, incoming: PaymentRelayed.Incoming, outgoing: PaymentRelayed.Outgoing, nextTrampolineNodeId: PublicKey, nextTrampolineAmount: MilliSatoshi) extends PaymentRelayed {
override val amountIn: MilliSatoshi = incoming.map(_.amount).sum
override val amountOut: MilliSatoshi = outgoing.map(_.amount).sum
- override val receivedAt: TimestampMilli = incoming.map(_.receivedAt).minOption.getOrElse(TimestampMilli.now())
+ override val startedAt: TimestampMilli = incoming.map(_.receivedAt).minOption.getOrElse(TimestampMilli.now())
override val settledAt: TimestampMilli = outgoing.map(_.settledAt).maxOption.getOrElse(TimestampMilli.now())
- override val timestamp: TimestampMilli = settledAt
}
case class OnTheFlyFundingPaymentRelayed(paymentHash: ByteVector32, incoming: PaymentRelayed.Incoming, outgoing: PaymentRelayed.Outgoing) extends PaymentRelayed {
override val amountIn: MilliSatoshi = incoming.map(_.amount).sum
override val amountOut: MilliSatoshi = outgoing.map(_.amount).sum
- override val receivedAt: TimestampMilli = incoming.map(_.receivedAt).minOption.getOrElse(TimestampMilli.now())
+ override val startedAt: TimestampMilli = incoming.map(_.receivedAt).minOption.getOrElse(TimestampMilli.now())
override val settledAt: TimestampMilli = outgoing.map(_.settledAt).maxOption.getOrElse(TimestampMilli.now())
- override val timestamp: TimestampMilli = settledAt
}
object PaymentRelayed {
@@ -123,20 +125,19 @@ object PaymentRelayed {
case class PaymentReceived(paymentHash: ByteVector32, parts: Seq[PaymentReceived.PartialPayment]) extends PaymentEvent {
require(parts.nonEmpty, "must have at least one payment part")
val amount: MilliSatoshi = parts.map(_.amount).sum
- val timestamp: TimestampMilli = parts.map(_.timestamp).max // we use max here because we fulfill the payment only once we received all the parts
+ val startedAt: TimestampMilli = parts.map(_.receivedAt).min // we use min here (when we receive the first payment part)
+ val settledAt: TimestampMilli = parts.map(_.receivedAt).max // we use max here because we fulfill the payment only once we received all the parts
}
object PaymentReceived {
- case class PartialPayment(amount: MilliSatoshi, fromChannelId: ByteVector32, timestamp: TimestampMilli = TimestampMilli.now())
+ case class PartialPayment(amount: MilliSatoshi, fromChannelId: ByteVector32, receivedAt: TimestampMilli = TimestampMilli.now())
}
case class PaymentMetadataReceived(paymentHash: ByteVector32, paymentMetadata: ByteVector)
-case class PaymentSettlingOnChain(id: UUID, amount: MilliSatoshi, paymentHash: ByteVector32, timestamp: TimestampMilli = TimestampMilli.now()) extends PaymentEvent
-
-case class WaitingToRelayPayment(remoteNodeId: PublicKey, paymentHash: ByteVector32, timestamp: TimestampMilli = TimestampMilli.now()) extends PaymentEvent
+case class PaymentSettlingOnChain(id: UUID, channelId: ByteVector32, amount: MilliSatoshi, paymentHash: ByteVector32, timestamp: TimestampMilli = TimestampMilli.now())
sealed trait PaymentFailure {
// @formatter:off
@@ -149,10 +150,14 @@ sealed trait PaymentFailure {
case class LocalFailure(amount: MilliSatoshi, route: Seq[Hop], t: Throwable) extends PaymentFailure
/** A remote node failed the payment and we were able to decrypt the onion failure packet. */
-case class RemoteFailure(amount: MilliSatoshi, route: Seq[Hop], e: Sphinx.DecryptedFailurePacket) extends PaymentFailure
+case class RemoteFailure(amount: MilliSatoshi, route: Seq[Hop], e: Sphinx.DecryptedFailurePacket, startedAt: TimestampMilli, failedAt: TimestampMilli) extends PaymentFailure {
+ val duration: FiniteDuration = failedAt - startedAt
+}
/** A remote node failed the payment but we couldn't decrypt the failure (e.g. a malicious node tampered with the message). */
-case class UnreadableRemoteFailure(amount: MilliSatoshi, route: Seq[Hop], e: Sphinx.CannotDecryptFailurePacket, holdTimes: Seq[HoldTime]) extends PaymentFailure
+case class UnreadableRemoteFailure(amount: MilliSatoshi, route: Seq[Hop], e: Sphinx.CannotDecryptFailurePacket, startedAt: TimestampMilli, failedAt: TimestampMilli, holdTimes: Seq[HoldTime]) extends PaymentFailure {
+ val duration: FiniteDuration = failedAt - startedAt
+}
object PaymentFailure {
@@ -194,7 +199,7 @@ object PaymentFailure {
*/
def hasAlreadyFailedOnce(nodeId: PublicKey, failures: Seq[PaymentFailure]): Boolean =
failures
- .collectFirst { case RemoteFailure(_, _, Sphinx.DecryptedFailurePacket(origin, _, u: Update)) if origin == nodeId => u.update_opt }
+ .collectFirst { case RemoteFailure(_, _, Sphinx.DecryptedFailurePacket(origin, _, u: Update), _, _) if origin == nodeId => u.update_opt }
.isDefined
/** Ignore the channel outgoing from the given nodeId in the given route. */
@@ -213,12 +218,12 @@ object PaymentFailure {
/** Update the set of nodes and channels to ignore in retries depending on the failure we received. */
def updateIgnored(failure: PaymentFailure, ignore: Ignore): Ignore = failure match {
- case RemoteFailure(_, hops, Sphinx.DecryptedFailurePacket(nodeId, _, _)) if nodeId == hops.last.nextNodeId =>
+ case RemoteFailure(_, hops, Sphinx.DecryptedFailurePacket(nodeId, _, _), _, _) if nodeId == hops.last.nextNodeId =>
// The failure came from the final recipient: the payment should be aborted without penalizing anyone in the route.
ignore
- case RemoteFailure(_, _, Sphinx.DecryptedFailurePacket(nodeId, _, _: Node)) =>
+ case RemoteFailure(_, _, Sphinx.DecryptedFailurePacket(nodeId, _, _: Node), _, _) =>
ignore + nodeId
- case RemoteFailure(_, hops, Sphinx.DecryptedFailurePacket(nodeId, _, failureMessage: Update)) =>
+ case RemoteFailure(_, hops, Sphinx.DecryptedFailurePacket(nodeId, _, failureMessage: Update), _, _) =>
if (failureMessage.update_opt.forall(update => Announcements.checkSig(update, nodeId))) {
val shouldIgnore = failureMessage match {
case _: TemporaryChannelFailure => true
@@ -235,9 +240,9 @@ object PaymentFailure {
// This node is fishy, it gave us a bad channel update signature, so let's filter it out.
ignore + nodeId
}
- case RemoteFailure(_, hops, Sphinx.DecryptedFailurePacket(nodeId, _, _)) =>
+ case RemoteFailure(_, hops, Sphinx.DecryptedFailurePacket(nodeId, _, _), _, _) =>
ignoreNodeOutgoingEdge(nodeId, hops, ignore)
- case UnreadableRemoteFailure(_, hops, _, holdTimes) =>
+ case UnreadableRemoteFailure(_, hops, _, _, _, holdTimes) =>
// TODO: Once everyone supports attributable errors, we should only exclude two nodes: the last for which we have attribution data and the next one.
// We don't know which node is sending garbage, let's blacklist all nodes except:
// - the nodes that returned attribution data (except the last one)
@@ -267,7 +272,7 @@ object PaymentFailure {
// We're only interested in the last channel update received per channel.
val updates = failures.foldLeft(Map.empty[ShortChannelId, ChannelUpdate]) {
case (current, failure) => failure match {
- case RemoteFailure(_, _, Sphinx.DecryptedFailurePacket(_, _, f: Update)) => f.update_opt match {
+ case RemoteFailure(_, _, Sphinx.DecryptedFailurePacket(_, _, f: Update), _, _) => f.update_opt match {
case Some(update) => current.updated(update.shortChannelId, update)
case None => current
}
diff --git a/eclair-core/src/main/scala/fr/acinq/eclair/payment/receive/MultiPartHandler.scala b/eclair-core/src/main/scala/fr/acinq/eclair/payment/receive/MultiPartHandler.scala
index 6822aa7..b837838 100644
--- a/eclair-core/src/main/scala/fr/acinq/eclair/payment/receive/MultiPartHandler.scala
+++ b/eclair-core/src/main/scala/fr/acinq/eclair/payment/receive/MultiPartHandler.scala
@@ -187,8 +187,8 @@ class MultiPartHandler(nodeParams: NodeParams, register: ActorRef, db: IncomingP
// NB: this case shouldn't happen unless the sender violated the spec, so it's ok that we take a slightly more
// expensive code path by fetching the preimage from DB.
case p: MultiPartPaymentFSM.HtlcPart => db.getIncomingPayment(paymentHash).foreach(record => {
- val received = PaymentReceived(paymentHash, PaymentReceived.PartialPayment(p.amount, p.htlc.channelId) :: Nil)
- if (db.receiveIncomingPayment(paymentHash, p.amount, received.timestamp)) {
+ val received = PaymentReceived(paymentHash, PaymentReceived.PartialPayment(p.amount, p.htlc.channelId, p.receivedAt) :: Nil)
+ if (db.receiveIncomingPayment(paymentHash, p.amount, received.settledAt)) {
val attribution = FulfillAttributionData(htlcReceivedAt = p.receivedAt, trampolineReceivedAt_opt = None, downstreamAttribution_opt = None)
PendingCommandsDb.safeSend(register, nodeParams.db.pendingCommands, p.htlc.channelId, CMD_FULFILL_HTLC(p.htlc.id, record.paymentPreimage, Some(attribution), commit = true))
ctx.system.eventStream.publish(received)
@@ -207,17 +207,17 @@ class MultiPartHandler(nodeParams: NodeParams, register: ActorRef, db: IncomingP
Logs.withMdc(log)(Logs.mdc(paymentHash_opt = Some(paymentHash))) {
log.debug("fulfilling payment for amount={}", parts.map(_.amount).sum)
val received = PaymentReceived(paymentHash, parts.flatMap {
- case p: MultiPartPaymentFSM.HtlcPart => Some(PaymentReceived.PartialPayment(p.amount, p.htlc.channelId))
+ case p: MultiPartPaymentFSM.HtlcPart => Some(PaymentReceived.PartialPayment(p.amount, p.htlc.channelId, p.receivedAt))
case _: MultiPartPaymentFSM.RecipientBlindedPathFeePart => None
})
val recordedInDb = payment match {
// Incoming offer payments are not stored in the database until they have been paid.
case IncomingBlindedPayment(invoice, preimage, paymentType, _, _) =>
- db.receiveIncomingOfferPayment(invoice, preimage, received.amount, received.timestamp, paymentType)
+ db.receiveIncomingOfferPayment(invoice, preimage, received.amount, received.settledAt, paymentType)
true
// Incoming standard payments are already stored and need to be marked as received.
case _: IncomingStandardPayment =>
- db.receiveIncomingPayment(paymentHash, received.amount, received.timestamp)
+ db.receiveIncomingPayment(paymentHash, received.amount, received.settledAt)
}
if (recordedInDb) {
parts.collect {
diff --git a/eclair-core/src/main/scala/fr/acinq/eclair/payment/relay/ChannelRelay.scala b/eclair-core/src/main/scala/fr/acinq/eclair/payment/relay/ChannelRelay.scala
index bdae4a2..167f477 100644
--- a/eclair-core/src/main/scala/fr/acinq/eclair/payment/relay/ChannelRelay.scala
+++ b/eclair-core/src/main/scala/fr/acinq/eclair/payment/relay/ChannelRelay.scala
@@ -245,7 +245,8 @@ class ChannelRelay private(nodeParams: NodeParams,
private def waitForAddSettled(): Behavior[Command] =
Behaviors.receiveMessagePartial {
case WrappedAddResponse(RES_ADD_SETTLED(_, htlc, fulfill: HtlcResult.Fulfill)) =>
- context.log.info("relaying fulfill to upstream, receivedAt={}, endedAt={}, confidence={}, originNode={}, outgoingChannel={}", upstream.receivedAt, r.receivedAt, reputationScore.outgoingConfidence, upstream.receivedFrom, htlc.channelId)
+ val now = TimestampMilli.now()
+ context.log.info("relaying fulfill to upstream, receivedAt={}, endedAt={}, confidence={}, originNode={}, outgoingChannel={}", upstream.receivedAt, now, reputationScore.outgoingConfidence, upstream.receivedFrom, htlc.channelId)
Metrics.relayFulfill(reputationScore.outgoingConfidence)
val downstreamAttribution_opt = fulfill match {
case HtlcResult.RemoteFulfill(fulfill) => fulfill.attribution_opt
@@ -253,12 +254,12 @@ class ChannelRelay private(nodeParams: NodeParams,
}
val attribution = FulfillAttributionData(htlcReceivedAt = upstream.receivedAt, trampolineReceivedAt_opt = None, downstreamAttribution_opt = downstreamAttribution_opt)
val cmd = CMD_FULFILL_HTLC(upstream.add.id, fulfill.paymentPreimage, Some(attribution), commit = true)
- context.system.eventStream ! EventStream.Publish(ChannelPaymentRelayed(upstream.amountIn, htlc.amountMsat, htlc.paymentHash, upstream.add.channelId, htlc.channelId, upstream.receivedAt, r.receivedAt))
+ context.system.eventStream ! EventStream.Publish(ChannelPaymentRelayed(upstream.amountIn, htlc.amountMsat, htlc.paymentHash, upstream.add.channelId, htlc.channelId, upstream.receivedAt, now))
recordRelayDuration(isSuccess = true)
safeSendAndStop(upstream.add.channelId, cmd)
-
case WrappedAddResponse(RES_ADD_SETTLED(_, htlc, fail: HtlcResult.Fail)) =>
- context.log.info("relaying fail to upstream, receivedAt={}, endedAt={}, confidence={}, originNode={}, outgoingChannel={}", upstream.receivedAt, r.receivedAt, reputationScore.outgoingConfidence, upstream.receivedFrom, htlc.channelId)
+ val now = TimestampMilli.now()
+ context.log.info("relaying fail to upstream, receivedAt={}, endedAt={}, confidence={}, originNode={}, outgoingChannel={}", upstream.receivedAt, now, reputationScore.outgoingConfidence, upstream.receivedFrom, htlc.channelId)
Metrics.relayFail(reputationScore.outgoingConfidence)
Metrics.recordPaymentRelayFailed(Tags.FailureType.Remote, Tags.RelayType.Channel)
val cmd = translateRelayFailure(upstream.add.id, fail, Some(upstream.receivedAt))
diff --git a/eclair-core/src/main/scala/fr/acinq/eclair/payment/relay/NodeRelay.scala b/eclair-core/src/main/scala/fr/acinq/eclair/payment/relay/NodeRelay.scala
index 3b8c451..5e12162 100644
--- a/eclair-core/src/main/scala/fr/acinq/eclair/payment/relay/NodeRelay.scala
+++ b/eclair-core/src/main/scala/fr/acinq/eclair/payment/relay/NodeRelay.scala
@@ -184,12 +184,12 @@ object NodeRelay {
case _ =>
// Otherwise, we try to find a downstream error that we could decrypt.
val outgoingNodeFailure = nextPayload match {
- case nextPayload: IntermediatePayload.NodeRelay.Standard => failures.collectFirst { case RemoteFailure(_, _, e) if e.originNode == nextPayload.outgoingNodeId => e.failureMessage }
- case nextPayload: IntermediatePayload.NodeRelay.ToNonTrampoline => failures.collectFirst { case RemoteFailure(_, _, e) if e.originNode == nextPayload.outgoingNodeId => e.failureMessage }
+ case nextPayload: IntermediatePayload.NodeRelay.Standard => failures.collectFirst { case f: RemoteFailure if f.e.originNode == nextPayload.outgoingNodeId => f.e.failureMessage }
+ case nextPayload: IntermediatePayload.NodeRelay.ToNonTrampoline => failures.collectFirst { case f: RemoteFailure if f.e.originNode == nextPayload.outgoingNodeId => f.e.failureMessage }
// When using blinded paths, we will never get a failure from the final node (for privacy reasons).
case _: IntermediatePayload.NodeRelay.ToBlindedPaths => None
}
- val otherNodeFailure = failures.collectFirst { case RemoteFailure(_, _, e) => e.failureMessage }
+ val otherNodeFailure = failures.collectFirst { case f: RemoteFailure => f.e.failureMessage }
val failure = outgoingNodeFailure.getOrElse(otherNodeFailure.getOrElse(TemporaryNodeFailure()))
Some(failure)
}
@@ -405,13 +405,13 @@ class NodeRelay private(nodeParams: NodeParams,
context.log.warn("trampoline payment failed downstream but was fulfilled upstream")
recordRelayDuration(startedAt, isSuccess = true)
stopping()
- case WrappedPaymentFailed(PaymentFailed(_, _, failures, _)) =>
+ case WrappedPaymentFailed(e) =>
walletNodeId_opt match {
- case Some(walletNodeId) if shouldAttemptOnTheFlyFunding(nodeParams, recipientFeatures_opt, failures)(context) =>
+ case Some(walletNodeId) if shouldAttemptOnTheFlyFunding(nodeParams, recipientFeatures_opt, e.failures)(context) =>
context.log.info("trampoline payment failed, attempting on-the-fly funding")
- attemptOnTheFlyFunding(upstream, walletNodeId, recipient, nextPayload, failures, startedAt)
+ attemptOnTheFlyFunding(upstream, walletNodeId, recipient, nextPayload, e.failures, startedAt)
case _ =>
- rejectPayment(upstream, translateError(nodeParams, failures, upstream, nextPayload))
+ rejectPayment(upstream, translateError(nodeParams, e.failures, upstream, nextPayload))
recordRelayDuration(startedAt, isSuccess = false)
stopping()
}
@@ -516,7 +516,7 @@ class NodeRelay private(nodeParams: NodeParams,
fulfillPayment(upstream, paymentSent.paymentPreimage, paymentSent.remainingAttribution_opt)
}
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))
+ val outgoing = paymentSent.parts.map(part => PaymentRelayed.OutgoingPart(part.amountWithFees, part.toChannelId, part.settledAt))
context.system.eventStream ! EventStream.Publish(TrampolinePaymentRelayed(paymentHash, incoming, outgoing, paymentSent.recipientNodeId, paymentSent.recipientAmount))
}
diff --git a/eclair-core/src/main/scala/fr/acinq/eclair/payment/relay/PostRestartHtlcCleaner.scala b/eclair-core/src/main/scala/fr/acinq/eclair/payment/relay/PostRestartHtlcCleaner.scala
index 8c51b41..cdc8fb1 100644
--- a/eclair-core/src/main/scala/fr/acinq/eclair/payment/relay/PostRestartHtlcCleaner.scala
+++ b/eclair-core/src/main/scala/fr/acinq/eclair/payment/relay/PostRestartHtlcCleaner.scala
@@ -178,15 +178,15 @@ class PostRestartHtlcCleaner(nodeParams: NodeParams, register: ActorRef, initial
val feesPaid = 0.msat // fees are unknown since we lost the reference to the payment
nodeParams.db.payments.getOutgoingPayment(id) match {
case Some(p) =>
- nodeParams.db.payments.updateOutgoingPayment(PaymentSent(p.parentId, fulfilledHtlc.paymentHash, paymentPreimage, p.recipientAmount, p.recipientNodeId, PaymentSent.PartialPayment(id, fulfilledHtlc.amountMsat, feesPaid, fulfilledHtlc.channelId, None) :: Nil, None))
+ nodeParams.db.payments.updateOutgoingPayment(PaymentSent(p.parentId, paymentPreimage, p.recipientAmount, p.recipientNodeId, PaymentSent.PartialPayment(id, fulfilledHtlc.amountMsat, feesPaid, fulfilledHtlc.channelId, None, startedAt = p.createdAt, settledAt = TimestampMilli.now()) :: Nil, None, p.createdAt))
// If all downstream HTLCs are now resolved, we can emit the payment event.
val payments = nodeParams.db.payments.listOutgoingPayments(p.parentId)
if (!payments.exists(p => p.status == OutgoingPaymentStatus.Pending)) {
val succeeded = payments.collect {
- case OutgoingPayment(id, _, _, _, _, amount, _, _, _, _, _, OutgoingPaymentStatus.Succeeded(_, feesPaid, _, completedAt)) =>
- PaymentSent.PartialPayment(id, amount, feesPaid, ByteVector32.Zeroes, None, completedAt)
+ case OutgoingPayment(id, _, _, _, _, amount, _, _, createdAt, _, _, OutgoingPaymentStatus.Succeeded(_, feesPaid, _, completedAt)) =>
+ PaymentSent.PartialPayment(id, amount, feesPaid, ByteVector32.Zeroes, None, createdAt, completedAt)
}
- val sent = PaymentSent(p.parentId, fulfilledHtlc.paymentHash, paymentPreimage, p.recipientAmount, p.recipientNodeId, succeeded, None)
+ val sent = PaymentSent(p.parentId, paymentPreimage, p.recipientAmount, p.recipientNodeId, succeeded, None, p.createdAt)
log.info(s"payment id=${sent.id} paymentHash=${sent.paymentHash} successfully sent (amount=${sent.recipientAmount})")
context.system.eventStream.publish(sent)
}
@@ -196,8 +196,9 @@ class PostRestartHtlcCleaner(nodeParams: NodeParams, register: ActorRef, initial
// dummy values in the DB (to make sure we store the preimage) but we don't emit an event.
val dummyFinalAmount = fulfilledHtlc.amountMsat
val dummyNodeId = nodeParams.nodeId
- nodeParams.db.payments.addOutgoingPayment(OutgoingPayment(id, id, None, fulfilledHtlc.paymentHash, PaymentType.Standard, fulfilledHtlc.amountMsat, dummyFinalAmount, dummyNodeId, TimestampMilli.now(), None, None, OutgoingPaymentStatus.Pending))
- nodeParams.db.payments.updateOutgoingPayment(PaymentSent(id, fulfilledHtlc.paymentHash, paymentPreimage, dummyFinalAmount, dummyNodeId, PaymentSent.PartialPayment(id, fulfilledHtlc.amountMsat, feesPaid, fulfilledHtlc.channelId, None) :: Nil, None))
+ val now = TimestampMilli.now()
+ nodeParams.db.payments.addOutgoingPayment(OutgoingPayment(id, id, None, fulfilledHtlc.paymentHash, PaymentType.Standard, fulfilledHtlc.amountMsat, dummyFinalAmount, dummyNodeId, now, None, None, OutgoingPaymentStatus.Pending))
+ nodeParams.db.payments.updateOutgoingPayment(PaymentSent(id, paymentPreimage, dummyFinalAmount, dummyNodeId, PaymentSent.PartialPayment(id, fulfilledHtlc.amountMsat, feesPaid, fulfilledHtlc.channelId, None, startedAt = now, settledAt = now) :: Nil, None, startedAt = now))
}
// There can never be more than one pending downstream HTLC for a given local origin (a multi-part payment is
// instead spread across multiple local origins) so we can now forget this origin.
@@ -243,7 +244,7 @@ class PostRestartHtlcCleaner(nodeParams: NodeParams, register: ActorRef, initial
case Some(relayedOut) =>
// If this is a local payment, we need to update the DB:
origin.upstream match {
- case Upstream.Local(id) => nodeParams.db.payments.updateOutgoingPayment(PaymentFailed(id, failedHtlc.paymentHash, Nil))
+ case Upstream.Local(id) => nodeParams.db.payments.updateOutgoingPayment(PaymentFailed(id, failedHtlc.paymentHash, Nil, startedAt = TimestampMilli.now(), settledAt = TimestampMilli.now()))
case _ =>
}
val relayedOut1 = relayedOut diff Set((failedHtlc.channelId, failedHtlc.id))
@@ -256,7 +257,7 @@ class PostRestartHtlcCleaner(nodeParams: NodeParams, register: ActorRef, initial
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))
+ context.system.eventStream.publish(PaymentFailed(p.parentId, failedHtlc.paymentHash, Nil, p.createdAt, settledAt = TimestampMilli.now()))
}
})
case Upstream.Cold.Channel(originChannelId, originHtlcId, _) =>
diff --git a/eclair-core/src/main/scala/fr/acinq/eclair/payment/send/Autoprobe.scala b/eclair-core/src/main/scala/fr/acinq/eclair/payment/send/Autoprobe.scala
index d45e650..e7f0e21 100644
--- a/eclair-core/src/main/scala/fr/acinq/eclair/payment/send/Autoprobe.scala
+++ b/eclair-core/src/main/scala/fr/acinq/eclair/payment/send/Autoprobe.scala
@@ -76,7 +76,7 @@ class Autoprobe(nodeParams: NodeParams, router: ActorRef, paymentInitiator: Acto
case paymentResult: PaymentEvent =>
paymentResult match {
- case PaymentFailed(_, _, _ :+ RemoteFailure(_, _, DecryptedFailurePacket(targetNodeId, _, _: IncorrectOrUnknownPaymentDetails)), _) =>
+ case PaymentFailed(_, _, _ :+ RemoteFailure(_, _, DecryptedFailurePacket(targetNodeId, _, _: IncorrectOrUnknownPaymentDetails), _, _), _, _) =>
log.info(s"payment probe successful to node=$targetNodeId")
case _ =>
log.info(s"payment probe failed with paymentResult=$paymentResult")
diff --git a/eclair-core/src/main/scala/fr/acinq/eclair/payment/send/MultiPartPaymentLifecycle.scala b/eclair-core/src/main/scala/fr/acinq/eclair/payment/send/MultiPartPaymentLifecycle.scala
index b486e22..a25cf1b 100644
--- a/eclair-core/src/main/scala/fr/acinq/eclair/payment/send/MultiPartPaymentLifecycle.scala
+++ b/eclair-core/src/main/scala/fr/acinq/eclair/payment/send/MultiPartPaymentLifecycle.scala
@@ -17,7 +17,7 @@
package fr.acinq.eclair.payment.send
import akka.actor.typed.scaladsl.adapter.ClassicActorRefOps
-import akka.actor.{ActorRef, FSM, Props, Status}
+import akka.actor.{ActorRef, FSM, Props}
import akka.event.Logging.MDC
import fr.acinq.bitcoin.scalacompat.ByteVector32
import fr.acinq.eclair.channel.{HtlcOverriddenByLocalCommit, HtlcsTimedoutDownstream, HtlcsWillTimeoutUpstream, Upstream}
@@ -49,9 +49,9 @@ class MultiPartPaymentLifecycle(nodeParams: NodeParams, cfg: SendPaymentConfig,
require(cfg.id == cfg.parentId, "multi-part payment cannot have a parent payment")
- val id = cfg.id
- val paymentHash = cfg.paymentHash
- val start = TimestampMilli.now()
+ val id: UUID = cfg.id
+ val paymentHash: ByteVector32 = cfg.paymentHash
+ val start: TimestampMilli = TimestampMilli.now()
private var retriedFailedChannels = false
startWith(WAIT_FOR_PAYMENT_REQUEST, WaitingForRequest)
@@ -101,7 +101,7 @@ class MultiPartPaymentLifecycle(nodeParams: NodeParams, cfg: SendPaymentConfig,
// payment, which may be confusing for users.
val dummyPayment = OutgoingPayment(id, cfg.parentId, cfg.externalId, paymentHash, cfg.paymentType, d.request.recipient.totalAmount, d.request.recipient.totalAmount, d.request.recipient.nodeId, TimestampMilli.now(), cfg.invoice, cfg.payerKey_opt, OutgoingPaymentStatus.Pending)
nodeParams.db.payments.addOutgoingPayment(dummyPayment)
- nodeParams.db.payments.updateOutgoingPayment(PaymentFailed(id, paymentHash, failure :: Nil))
+ nodeParams.db.payments.updateOutgoingPayment(PaymentFailed(id, paymentHash, failure :: Nil, startedAt = start, settledAt = TimestampMilli.now()))
}
gotoAbortedOrStop(PaymentAborted(d.request, d.failures :+ failure, d.pending.keySet))
}
@@ -154,7 +154,7 @@ class MultiPartPaymentLifecycle(nodeParams: NodeParams, cfg: SendPaymentConfig,
val failures = d.failures ++ pf.failures
val pending = d.pending - pf.id
if (pending.isEmpty) {
- myStop(d.request, Left(PaymentFailed(id, paymentHash, failures)))
+ myStop(d.request, Left(PaymentFailed(id, paymentHash, failures, startedAt = start, settledAt = TimestampMilli.now())))
} else {
stay() using d.copy(failures = failures, pending = pending)
}
@@ -176,7 +176,7 @@ class MultiPartPaymentLifecycle(nodeParams: NodeParams, cfg: SendPaymentConfig,
val parts = d.parts ++ ps.parts
val pending = d.pending - ps.parts.head.id
if (pending.isEmpty) {
- myStop(d.request, Right(cfg.createPaymentSent(d.request.recipient, d.preimage, parts, d.remainingAttribution_opt)))
+ myStop(d.request, Right(cfg.createPaymentSent(d.request.recipient, d.preimage, parts, d.remainingAttribution_opt, start)))
} else {
stay() using d.copy(parts = parts, pending = pending)
}
@@ -187,7 +187,7 @@ class MultiPartPaymentLifecycle(nodeParams: NodeParams, cfg: SendPaymentConfig,
log.warning(s"payment succeeded but partial payment failed (id=${pf.id})")
val pending = d.pending - pf.id
if (pending.isEmpty) {
- myStop(d.request, Right(cfg.createPaymentSent(d.request.recipient, d.preimage, d.parts, d.remainingAttribution_opt)))
+ myStop(d.request, Right(cfg.createPaymentSent(d.request.recipient, d.preimage, d.parts, d.remainingAttribution_opt, start)))
} else {
stay() using d.copy(pending = pending)
}
@@ -207,7 +207,7 @@ class MultiPartPaymentLifecycle(nodeParams: NodeParams, cfg: SendPaymentConfig,
private def gotoAbortedOrStop(d: PaymentAborted): State = {
if (d.pending.isEmpty) {
- myStop(d.request, Left(PaymentFailed(id, paymentHash, d.failures)))
+ myStop(d.request, Left(PaymentFailed(id, paymentHash, d.failures, startedAt = start, settledAt = TimestampMilli.now())))
} else
goto(PAYMENT_ABORTED) using d
}
@@ -217,7 +217,7 @@ class MultiPartPaymentLifecycle(nodeParams: NodeParams, cfg: SendPaymentConfig,
d.request.replyTo ! PreimageReceived(paymentHash, d.preimage, d.remainingAttribution_opt)
}
if (d.pending.isEmpty) {
- myStop(d.request, Right(cfg.createPaymentSent(d.request.recipient, d.preimage, d.parts, d.remainingAttribution_opt)))
+ myStop(d.request, Right(cfg.createPaymentSent(d.request.recipient, d.preimage, d.parts, d.remainingAttribution_opt, start)))
} else
goto(PAYMENT_SUCCEEDED) using d
}
@@ -255,7 +255,7 @@ class MultiPartPaymentLifecycle(nodeParams: NodeParams, cfg: SendPaymentConfig,
// 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
- case p@PartialPayment(_, _, _, _, Some(route), _) => route.head.fee(p.amountWithFees)
+ case p@PartialPayment(_, _, _, _, Some(route), _, _) => route.head.fee(p.amountWithFees)
}.sum
}
paymentSent.feesPaid + localFees
diff --git a/eclair-core/src/main/scala/fr/acinq/eclair/payment/send/PaymentInitiator.scala b/eclair-core/src/main/scala/fr/acinq/eclair/payment/send/PaymentInitiator.scala
index 3a7170e..6fdb12d 100644
--- a/eclair-core/src/main/scala/fr/acinq/eclair/payment/send/PaymentInitiator.scala
+++ b/eclair-core/src/main/scala/fr/acinq/eclair/payment/send/PaymentInitiator.scala
@@ -28,7 +28,7 @@ import fr.acinq.eclair.payment.send.BlindedPathsResolver.ResolvedPath
import fr.acinq.eclair.payment.send.PaymentError._
import fr.acinq.eclair.router.Router._
import fr.acinq.eclair.wire.protocol._
-import fr.acinq.eclair.{CltvExpiry, CltvExpiryDelta, Features, MilliSatoshi, MilliSatoshiLong, NodeParams}
+import fr.acinq.eclair.{CltvExpiry, CltvExpiryDelta, Features, MilliSatoshi, MilliSatoshiLong, NodeParams, TimestampMilli}
import scodec.bits.ByteVector
import java.util.UUID
@@ -57,7 +57,7 @@ class PaymentInitiator(nodeParams: NodeParams, outgoingPaymentFactory: PaymentIn
case invoice: Bolt12Invoice => BlindedRecipient(invoice, r.resolvedPaths, r.recipientAmount, finalExpiry, r.userCustomTlvs)
}
if (!nodeParams.features.invoiceFeatures().areSupported(recipient.features)) {
- replyTo ! PaymentFailed(paymentId, r.paymentHash, LocalFailure(r.recipientAmount, Nil, UnsupportedFeatures(recipient.features)) :: Nil)
+ replyTo ! PaymentFailed(paymentId, r.paymentHash, LocalFailure(r.recipientAmount, Nil, UnsupportedFeatures(recipient.features)) :: Nil, startedAt = TimestampMilli.now(), settledAt = TimestampMilli.now())
} else if (Features.canUseFeature(nodeParams.features.invoiceFeatures(), recipient.features, Features.BasicMultiPartPayment)) {
val fsm = outgoingPaymentFactory.spawnOutgoingMultiPartPayment(context, paymentCfg, publishPreimage = !r.blockUntilComplete)
fsm ! MultiPartPaymentLifecycle.SendMultiPartPayment(self, recipient, r.maxAttempts, r.routeParams)
@@ -84,7 +84,7 @@ class PaymentInitiator(nodeParams: NodeParams, outgoingPaymentFactory: PaymentIn
r.replyTo ! paymentId
}
if (r.invoice.amount_opt.isEmpty) {
- r.replyTo ! PaymentFailed(paymentId, r.paymentHash, LocalFailure(r.recipientAmount, Nil, new IllegalArgumentException("test trampoline payments must not use amount-less invoices")) :: Nil)
+ r.replyTo ! PaymentFailed(paymentId, r.paymentHash, LocalFailure(r.recipientAmount, Nil, new IllegalArgumentException("test trampoline payments must not use amount-less invoices")) :: Nil, startedAt = TimestampMilli.now(), settledAt = TimestampMilli.now())
} else {
log.info(s"sending trampoline payment with trampolineNodeId=${r.trampolineNodeId} and invoice=${r.invoice.toString}")
val fsm = outgoingPaymentFactory.spawnOutgoingTrampolinePayment(context)
@@ -96,7 +96,7 @@ class PaymentInitiator(nodeParams: NodeParams, outgoingPaymentFactory: PaymentIn
val paymentId = UUID.randomUUID()
val parentPaymentId = r.parentId.getOrElse(UUID.randomUUID())
if (!nodeParams.features.invoiceFeatures().areSupported(r.invoice.features)) {
- sender() ! PaymentFailed(paymentId, r.paymentHash, LocalFailure(r.recipientAmount, Nil, UnsupportedFeatures(r.invoice.features)) :: Nil)
+ sender() ! PaymentFailed(paymentId, r.paymentHash, LocalFailure(r.recipientAmount, Nil, UnsupportedFeatures(r.invoice.features)) :: Nil, startedAt = TimestampMilli.now(), settledAt = TimestampMilli.now())
} else {
sender() ! SendPaymentToRouteResponse(paymentId, parentPaymentId)
val paymentCfg = SendPaymentConfig(paymentId, parentPaymentId, r.externalId, r.paymentHash, r.recipientNodeId, Upstream.Local(paymentId), Some(r.invoice), None, storeInDb = true, publishEvent = true, recordPathFindingMetrics = false, accountable = false)
@@ -227,7 +227,7 @@ object PaymentInitiator {
trampolineNodeId: PublicKey,
routeParams: RouteParams,
blockUntilComplete: Boolean = false) extends SendRequestedPayment {
- override val recipientAmount = invoice.amount_opt.getOrElse(0 msat)
+ override val recipientAmount: MilliSatoshi = invoice.amount_opt.getOrElse(0 msat)
}
/**
@@ -270,7 +270,7 @@ object PaymentInitiator {
routeParams: RouteParams,
userCustomTlvs: Set[GenericTlv] = Set.empty,
recordPathFindingMetrics: Boolean = false) {
- val paymentHash = Crypto.sha256(paymentPreimage)
+ val paymentHash: ByteVector32 = Crypto.sha256(paymentPreimage)
}
/**
@@ -331,12 +331,14 @@ object PaymentInitiator {
recordPathFindingMetrics: Boolean,
accountable: Boolean) {
val paymentContext: PaymentContext = PaymentContext(id, parentId, paymentHash)
- val paymentType = invoice match {
+ val paymentType: String = invoice match {
case Some(_: Bolt12Invoice) => PaymentType.Blinded
case _ => PaymentType.Standard
}
- def createPaymentSent(recipient: Recipient, preimage: ByteVector32, parts: Seq[PaymentSent.PartialPayment], remainingAttribution_opt: Option[ByteVector]) = PaymentSent(parentId, paymentHash, preimage, recipient.totalAmount, recipient.nodeId, parts, remainingAttribution_opt)
+ def createPaymentSent(recipient: Recipient, preimage: ByteVector32, parts: Seq[PaymentSent.PartialPayment], remainingAttribution_opt: Option[ByteVector], startedAt: TimestampMilli): PaymentSent = {
+ PaymentSent(parentId, preimage, recipient.totalAmount, recipient.nodeId, parts, remainingAttribution_opt, startedAt)
+ }
}
}
diff --git a/eclair-core/src/main/scala/fr/acinq/eclair/payment/send/PaymentLifecycle.scala b/eclair-core/src/main/scala/fr/acinq/eclair/payment/send/PaymentLifecycle.scala
index 2de7489..f571a01 100644
--- a/eclair-core/src/main/scala/fr/acinq/eclair/payment/send/PaymentLifecycle.scala
+++ b/eclair-core/src/main/scala/fr/acinq/eclair/payment/send/PaymentLifecycle.scala
@@ -86,7 +86,7 @@ class PaymentLifecycle(nodeParams: NodeParams, cfg: SendPaymentConfig, router: A
case Event(PaymentRouteNotFound(t), WaitingForRoute(request, failures, _)) =>
Metrics.PaymentError.withTag(Tags.Failure, Tags.FailureType(LocalFailure(request.amount, Nil, t))).increment()
- myStop(request, Left(PaymentFailed(id, paymentHash, failures :+ LocalFailure(request.amount, Nil, t))))
+ myStop(request, Left(PaymentFailed(id, paymentHash, failures :+ LocalFailure(request.amount, Nil, t), startedAt = start, settledAt = TimestampMilli.now())))
}
when(WAITING_FOR_CONFIDENCE) {
@@ -94,11 +94,11 @@ class PaymentLifecycle(nodeParams: NodeParams, cfg: SendPaymentConfig, router: A
OutgoingPaymentPacket.buildOutgoingPayment(Origin.Hot(self, cfg.upstream), paymentHash, route, request.recipient, score) 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)
+ goto(WAITING_FOR_PAYMENT_COMPLETE) using WaitingForComplete(request, payment.cmd, failures, payment.sharedSecrets, ignore, route, sentAt = TimestampMilli.now())
case Left(error) =>
log.warning("cannot send outgoing payment: {}", error.getMessage)
Metrics.PaymentError.withTag(Tags.Failure, Tags.FailureType(LocalFailure(request.amount, route.fullRoute, error))).increment()
- myStop(request, Left(PaymentFailed(id, paymentHash, failures :+ LocalFailure(request.amount, route.fullRoute, error))))
+ myStop(request, Left(PaymentFailed(id, paymentHash, failures :+ LocalFailure(request.amount, route.fullRoute, error), startedAt = start, settledAt = TimestampMilli.now())))
}
}
@@ -114,7 +114,7 @@ class PaymentLifecycle(nodeParams: NodeParams, cfg: SendPaymentConfig, router: A
case Event(RES_ADD_SETTLED(_, htlc, fulfill: HtlcResult.Fulfill), d: WaitingForComplete) =>
router ! Router.RouteDidRelay(d.route)
Metrics.PaymentAttempt.withTag(Tags.MultiPart, value = false).record(d.failures.size + 1)
- val p = PartialPayment(id, d.request.amount, d.cmd.amount - d.request.amount, htlc.channelId, Some(d.route.fullRoute))
+ val p = PartialPayment(id, d.request.amount, d.cmd.amount - d.request.amount, htlc.channelId, Some(d.route.fullRoute), startedAt = d.sentAt, settledAt = TimestampMilli.now())
val remainingAttribution_opt = fulfill match {
case HtlcResult.RemoteFulfill(updateFulfill) =>
updateFulfill.attribution_opt match {
@@ -128,7 +128,7 @@ class PaymentLifecycle(nodeParams: NodeParams, cfg: SendPaymentConfig, router: A
}
case _: HtlcResult.OnChainFulfill => None
}
- myStop(d.request, Right(cfg.createPaymentSent(d.recipient, fulfill.paymentPreimage, p :: Nil, remainingAttribution_opt)))
+ myStop(d.request, Right(cfg.createPaymentSent(d.recipient, fulfill.paymentPreimage, p :: Nil, remainingAttribution_opt, start)))
case Event(RES_ADD_SETTLED(_, _, fail: HtlcResult.Fail), d: WaitingForComplete) =>
fail match {
@@ -184,22 +184,23 @@ class PaymentLifecycle(nodeParams: NodeParams, cfg: SendPaymentConfig, router: A
log.info(s"received an error message from local, trying to use a different channel (failure=${t.getMessage})")
retry(localFailure, d)
} else {
- myStop(d.request, Left(PaymentFailed(id, paymentHash, d.failures :+ localFailure)))
+ myStop(d.request, Left(PaymentFailed(id, paymentHash, d.failures :+ localFailure, startedAt = start, settledAt = TimestampMilli.now())))
}
}
private def handleRemoteFail(d: WaitingForComplete, fail: UpdateFailHtlc) = {
import d._
+ val now = TimestampMilli.now()
val htlcFailure = Sphinx.FailurePacket.decrypt(fail.reason, fail.attribution_opt, sharedSecrets)
if (htlcFailure.holdTimes.nonEmpty) {
context.system.eventStream.publish(Router.ReportedHoldTimes(htlcFailure.holdTimes))
}
((htlcFailure.failure match {
case success@Right(e) =>
- Metrics.PaymentError.withTag(Tags.Failure, Tags.FailureType(RemoteFailure(request.amount, Nil, e))).increment()
+ Metrics.PaymentError.withTag(Tags.Failure, Tags.FailureType(RemoteFailure(request.amount, Nil, e, startedAt = d.sentAt, failedAt = now))).increment()
success
case failure@Left(e) =>
- Metrics.PaymentError.withTag(Tags.Failure, Tags.FailureType(UnreadableRemoteFailure(request.amount, Nil, e, htlcFailure.holdTimes))).increment()
+ Metrics.PaymentError.withTag(Tags.Failure, Tags.FailureType(UnreadableRemoteFailure(request.amount, Nil, e, startedAt = d.sentAt, failedAt = now, htlcFailure.holdTimes))).increment()
failure
}) match {
case res@Right(Sphinx.DecryptedFailurePacket(_, index, failureMessage)) =>
@@ -227,11 +228,11 @@ class PaymentLifecycle(nodeParams: NodeParams, cfg: SendPaymentConfig, router: A
case Right(e@Sphinx.DecryptedFailurePacket(nodeId, _, failureMessage)) if nodeId == recipient.nodeId =>
// if destination node returns an error, we fail the payment immediately
log.warning(s"received an error message from target nodeId=$nodeId, failing the payment (failure=$failureMessage)")
- myStop(request, Left(PaymentFailed(id, paymentHash, failures :+ RemoteFailure(request.amount, route.fullRoute, e))))
+ myStop(request, Left(PaymentFailed(id, paymentHash, failures :+ RemoteFailure(request.amount, route.fullRoute, e, startedAt = d.sentAt, failedAt = now), startedAt = start, settledAt = now)))
case Right(e@Sphinx.DecryptedFailurePacket(nodeId, _, failureMessage)) if route.finalHop_opt.collect { case h: NodeHop if h.nodeId == nodeId => h }.nonEmpty =>
// if trampoline node returns an error, we fail the payment immediately
log.warning(s"received an error message from trampoline nodeId=$nodeId, failing the payment (failure=$failureMessage)")
- myStop(request, Left(PaymentFailed(id, paymentHash, failures :+ RemoteFailure(request.amount, route.fullRoute, e))))
+ myStop(request, Left(PaymentFailed(id, paymentHash, failures :+ RemoteFailure(request.amount, route.fullRoute, e, startedAt = d.sentAt, failedAt = now), startedAt = start, settledAt = now)))
case res if failures.size + 1 >= request.maxAttempts =>
// otherwise we never try more than maxAttempts, no matter the kind of error returned
val failure = res match {
@@ -241,24 +242,24 @@ class PaymentLifecycle(nodeParams: NodeParams, cfg: SendPaymentConfig, router: A
case failureMessage: Update => handleUpdate(nodeId, failureMessage, d)
case _ =>
}
- RemoteFailure(request.amount, route.fullRoute, e)
+ RemoteFailure(request.amount, route.fullRoute, e, startedAt = d.sentAt, failedAt = now)
case Left(e@Sphinx.CannotDecryptFailurePacket(unwrapped, _)) =>
log.warning(s"cannot parse returned error ${fail.reason.toHex} with sharedSecrets=$sharedSecrets: unwrapped=$unwrapped")
- UnreadableRemoteFailure(request.amount, route.fullRoute, e, htlcFailure.holdTimes)
+ UnreadableRemoteFailure(request.amount, route.fullRoute, e, startedAt = d.sentAt, failedAt = now, htlcFailure.holdTimes)
}
log.warning(s"too many failed attempts, failing the payment")
- myStop(request, Left(PaymentFailed(id, paymentHash, failures :+ failure)))
+ myStop(request, Left(PaymentFailed(id, paymentHash, failures :+ failure, startedAt = start, settledAt = now)))
case Left(e@Sphinx.CannotDecryptFailurePacket(unwrapped, _)) =>
log.warning(s"cannot parse returned error: unwrapped=$unwrapped, route=${route.printNodes()}")
- val failure = UnreadableRemoteFailure(request.amount, route.fullRoute, e, htlcFailure.holdTimes)
+ val failure = UnreadableRemoteFailure(request.amount, route.fullRoute, e, startedAt = d.sentAt, failedAt = now, htlcFailure.holdTimes)
retry(failure, d)
case Right(e@Sphinx.DecryptedFailurePacket(nodeId, _, failureMessage: Node)) =>
log.info(s"received 'Node' type error message from nodeId=$nodeId, trying to route around it (failure=$failureMessage)")
- val failure = RemoteFailure(request.amount, route.fullRoute, e)
+ val failure = RemoteFailure(request.amount, route.fullRoute, e, startedAt = d.sentAt, failedAt = now)
retry(failure, d)
case Right(e@Sphinx.DecryptedFailurePacket(nodeId, _, failureMessage: Update)) =>
log.info(s"received 'Update' type error message from nodeId=$nodeId, retrying payment (failure=$failureMessage)")
- val failure = RemoteFailure(request.amount, route.fullRoute, e)
+ val failure = RemoteFailure(request.amount, route.fullRoute, e, startedAt = d.sentAt, failedAt = now)
if (failureMessage.update_opt.forall(update => Announcements.checkSig(update, nodeId))) {
val recipient1 = handleUpdate(nodeId, failureMessage, d)
val ignore1 = PaymentFailure.updateIgnored(failure, ignore)
@@ -286,7 +287,7 @@ class PaymentLifecycle(nodeParams: NodeParams, cfg: SendPaymentConfig, router: A
case Right(e@Sphinx.DecryptedFailurePacket(nodeId, _, _: InvalidOnionBlinding)) =>
// there was a failure inside the blinded route we used: we cannot know why it failed, so let's ignore it.
log.info(s"received an error coming from nodeId=$nodeId inside the blinded route, retrying with different blinded routes")
- val failure = RemoteFailure(request.amount, route.fullRoute, e)
+ val failure = RemoteFailure(request.amount, route.fullRoute, e, startedAt = d.sentAt, failedAt = now)
val ignore1 = PaymentFailure.updateIgnored(failure, ignore)
request match {
case _: SendPaymentToRoute =>
@@ -298,7 +299,7 @@ class PaymentLifecycle(nodeParams: NodeParams, cfg: SendPaymentConfig, router: A
}
case Right(e@Sphinx.DecryptedFailurePacket(nodeId, _, failureMessage)) =>
log.info(s"received an error message from nodeId=$nodeId, trying to use a different channel (failure=$failureMessage)")
- val failure = RemoteFailure(request.amount, route.fullRoute, e)
+ val failure = RemoteFailure(request.amount, route.fullRoute, e, startedAt = d.sentAt, failedAt = now)
retry(failure, d)
}
}
@@ -417,7 +418,7 @@ class PaymentLifecycle(nodeParams: NodeParams, cfg: SendPaymentConfig, router: A
// 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
- case p@PartialPayment(_, _, _, _, Some(route), _) => route.head.fee(p.amountWithFees)
+ case p@PartialPayment(_, _, _, _, Some(route), _, _) => route.head.fee(p.amountWithFees)
}.sum
}
paymentSent.feesPaid + localFees
@@ -504,7 +505,7 @@ object PaymentLifecycle {
case object WaitingForRequest extends Data
case class WaitingForRoute(request: SendPayment, failures: Seq[PaymentFailure], ignore: Ignore) extends Data
case class WaitingForConfidence(request: SendPayment, failures: Seq[PaymentFailure], ignore: Ignore, route: Route) extends Data
- case class WaitingForComplete(request: SendPayment, cmd: CMD_ADD_HTLC, failures: Seq[PaymentFailure], sharedSecrets: Seq[Sphinx.SharedSecret], ignore: Ignore, route: Route) extends Data {
+ case class WaitingForComplete(request: SendPayment, cmd: CMD_ADD_HTLC, failures: Seq[PaymentFailure], sharedSecrets: Seq[Sphinx.SharedSecret], ignore: Ignore, route: Route, sentAt: TimestampMilli) extends Data {
val recipient: Recipient = request.recipient
}
diff --git a/eclair-core/src/main/scala/fr/acinq/eclair/payment/send/TrampolinePaymentLifecycle.scala b/eclair-core/src/main/scala/fr/acinq/eclair/payment/send/TrampolinePaymentLifecycle.scala
index 465b611..22cc576 100644
--- a/eclair-core/src/main/scala/fr/acinq/eclair/payment/send/TrampolinePaymentLifecycle.scala
+++ b/eclair-core/src/main/scala/fr/acinq/eclair/payment/send/TrampolinePaymentLifecycle.scala
@@ -33,7 +33,7 @@ import fr.acinq.eclair.reputation.Reputation
import fr.acinq.eclair.router.Router
import fr.acinq.eclair.router.Router.RouteParams
import fr.acinq.eclair.wire.protocol.{PaymentOnion, PaymentOnionCodecs}
-import fr.acinq.eclair.{CltvExpiry, CltvExpiryDelta, Features, Logs, MilliSatoshi, NodeParams, randomBytes32}
+import fr.acinq.eclair.{CltvExpiry, CltvExpiryDelta, Features, Logs, MilliSatoshi, NodeParams, TimestampMilli, randomBytes32}
import java.util.UUID
@@ -113,7 +113,7 @@ object TrampolinePaymentLifecycle {
val add = CMD_ADD_HTLC(addHtlcAdapter.toClassic, outgoing.trampolineAmount, paymentHash, outgoing.trampolineExpiry, outgoing.onion.packet, None, Reputation.Score.max(accountable = false), None, origin, commit = true)
channelInfo.channel ! add
val channelId = channelInfo.data.asInstanceOf[DATA_NORMAL].channelId
- val part = PartialPayment(cmd.paymentId, amount, computeFees(amount, attemptNumber), channelId, None)
+ val part = PartialPayment(cmd.paymentId, amount, computeFees(amount, attemptNumber), channelId, None, startedAt = TimestampMilli.now(), settledAt = TimestampMilli.now()) // we will update settledAt below
waitForSettlement(part, outgoing.onion.sharedSecrets, outgoing.trampolineOnion.sharedSecrets)
}
@@ -137,7 +137,7 @@ object TrampolinePaymentLifecycle {
}
case _: HtlcResult.OnChainFulfill => Nil
}
- parent ! HtlcSettled(fulfill, part, holdTimes)
+ parent ! HtlcSettled(fulfill, part.copy(settledAt = TimestampMilli.now()), holdTimes)
Behaviors.stopped
case fail: HtlcResult.Fail =>
val holdTimes = fail match {
@@ -145,7 +145,7 @@ object TrampolinePaymentLifecycle {
Sphinx.FailurePacket.decrypt(updateFail.reason, updateFail.attribution_opt, outerOnionSecrets).holdTimes
case _ => Nil
}
- parent ! HtlcSettled(fail, part, holdTimes)
+ parent ! HtlcSettled(fail, part.copy(settledAt = TimestampMilli.now()), holdTimes)
Behaviors.stopped
}
}
@@ -161,6 +161,7 @@ class TrampolinePaymentLifecycle private(nodeParams: NodeParams,
import TrampolinePayment._
import TrampolinePaymentLifecycle._
+ private val startedAt = TimestampMilli.now()
private val paymentHash = cmd.invoice.paymentHash
private val totalAmount = cmd.invoice.amount_opt.get
@@ -174,7 +175,7 @@ class TrampolinePaymentLifecycle private(nodeParams: NodeParams,
Behaviors.receiveMessagePartial {
case TrampolinePeerNotFound(nodeId) =>
context.log.warn("could not send trampoline payment: we don't have channels with trampoline node {}", nodeId)
- cmd.replyTo ! PaymentFailed(cmd.paymentId, paymentHash, LocalFailure(totalAmount, Nil, new RuntimeException("no channels with trampoline node")) :: Nil)
+ cmd.replyTo ! PaymentFailed(cmd.paymentId, paymentHash, LocalFailure(totalAmount, Nil, new RuntimeException("no channels with trampoline node")) :: Nil, startedAt, settledAt = TimestampMilli.now())
Behaviors.stopped
case WrappedPeerChannels(channels) =>
sendPayment(channels, attemptNumber)
@@ -194,7 +195,7 @@ class TrampolinePaymentLifecycle private(nodeParams: NodeParams,
val expiry = CltvExpiry(nodeParams.currentBlockHeight) + CltvExpiryDelta(36)
if (filtered.isEmpty) {
context.log.warn("no usable channel with trampoline node {}", cmd.trampolineNodeId)
- cmd.replyTo ! PaymentFailed(cmd.paymentId, paymentHash, LocalFailure(totalAmount, Nil, new RuntimeException("no usable channel with trampoline node")) :: Nil)
+ cmd.replyTo ! PaymentFailed(cmd.paymentId, paymentHash, LocalFailure(totalAmount, Nil, new RuntimeException("no usable channel with trampoline node")) :: Nil, startedAt, settledAt = TimestampMilli.now())
Behaviors.stopped
} else {
val amount1 = totalAmount / 2
@@ -220,7 +221,7 @@ class TrampolinePaymentLifecycle private(nodeParams: NodeParams,
waitForSettlement(remaining - 1, attemptNumber, fulfilledParts)
} else {
context.log.warn("trampoline payment failed")
- cmd.replyTo ! PaymentFailed(cmd.paymentId, paymentHash, LocalFailure(totalAmount, Nil, failure) :: Nil)
+ cmd.replyTo ! PaymentFailed(cmd.paymentId, paymentHash, LocalFailure(totalAmount, Nil, failure) :: Nil, startedAt, settledAt = TimestampMilli.now())
Behaviors.stopped
}
case HtlcSettled(result: HtlcResult, part, holdTimes) =>
@@ -235,7 +236,7 @@ class TrampolinePaymentLifecycle private(nodeParams: NodeParams,
waitForSettlement(remaining - 1, attemptNumber, part +: fulfilledParts)
} else {
context.log.info("trampoline payment succeeded")
- cmd.replyTo ! PaymentSent(cmd.paymentId, paymentHash, fulfill.paymentPreimage, totalAmount, cmd.invoice.nodeId, part +: fulfilledParts, None)
+ cmd.replyTo ! PaymentSent(cmd.paymentId, fulfill.paymentPreimage, totalAmount, cmd.invoice.nodeId, part +: fulfilledParts, None, startedAt)
Behaviors.stopped
}
case fail: HtlcResult.Fail =>
@@ -254,11 +255,11 @@ class TrampolinePaymentLifecycle private(nodeParams: NodeParams,
val nextFees = computeFees(totalAmount, attemptNumber)
if (attemptNumber > 3) {
context.log.warn("cannot retry trampoline payment: retries exceeded")
- cmd.replyTo ! PaymentFailed(cmd.paymentId, paymentHash, LocalFailure(totalAmount, Nil, new RuntimeException("maximum trampoline retries exceeded")) :: Nil)
+ cmd.replyTo ! PaymentFailed(cmd.paymentId, paymentHash, LocalFailure(totalAmount, Nil, new RuntimeException("maximum trampoline retries exceeded")) :: Nil, startedAt, settledAt = TimestampMilli.now())
Behaviors.stopped
} else if (cmd.routeParams.getMaxFee(totalAmount) < nextFees) {
context.log.warn("cannot retry trampoline payment: maximum fees exceeded ({} > {})", nextFees, cmd.routeParams.getMaxFee(totalAmount))
- cmd.replyTo ! PaymentFailed(cmd.paymentId, paymentHash, LocalFailure(totalAmount, Nil, new RuntimeException("maximum trampoline fees exceeded")) :: Nil)
+ cmd.replyTo ! PaymentFailed(cmd.paymentId, paymentHash, LocalFailure(totalAmount, Nil, new RuntimeException("maximum trampoline fees exceeded")) :: Nil, startedAt, settledAt = TimestampMilli.now())
Behaviors.stopped
} else {
context.log.info("retrying trampoline payment with fees={}", nextFees)
diff --git a/eclair-core/src/test/scala/fr/acinq/eclair/EclairImplSpec.scala b/eclair-core/src/test/scala/fr/acinq/eclair/EclairImplSpec.scala
index 11de170..918e44d 100644
--- a/eclair-core/src/test/scala/fr/acinq/eclair/EclairImplSpec.scala
+++ b/eclair-core/src/test/scala/fr/acinq/eclair/EclairImplSpec.scala
@@ -581,9 +581,10 @@ class EclairImplSpec extends TestKitBaseClass with FixtureAnyFunSuiteLike with I
import f._
val eclair = new EclairImpl(kit)
+ val startedAt = TimestampMilli.now()
// A first payment has been sent out and is currently pending.
- val pendingPayment1 = OutgoingPayment(UUID.randomUUID(), UUID.randomUUID(), None, randomBytes32(), "test", 500 msat, 750 msat, randomKey().publicKey, TimestampMilli.now(), None, None, OutgoingPaymentStatus.Pending)
+ val pendingPayment1 = OutgoingPayment(UUID.randomUUID(), UUID.randomUUID(), None, randomBytes32(), "test", 500 msat, 750 msat, randomKey().publicKey, startedAt, None, None, OutgoingPaymentStatus.Pending)
kit.nodeParams.db.payments.addOutgoingPayment(pendingPayment1)
eclair.sentInfo(PaymentIdentifier.PaymentUUID(pendingPayment1.parentId)).pipeTo(sender.ref)
sender.expectMsg(Seq(pendingPayment1))
@@ -611,9 +612,9 @@ class EclairImplSpec extends TestKitBaseClass with FixtureAnyFunSuiteLike with I
// A third payment is fully settled in the DB and not being retried.
val failedAt = TimestampMilli.now()
- val failedPayment = OutgoingPayment(UUID.randomUUID(), UUID.randomUUID(), None, spontaneousPayment.paymentHash, "test", 700 msat, 900 msat, randomKey().publicKey, TimestampMilli.now(), None, None, OutgoingPaymentStatus.Failed(Nil, failedAt))
+ val failedPayment = OutgoingPayment(UUID.randomUUID(), UUID.randomUUID(), None, spontaneousPayment.paymentHash, "test", 700 msat, 900 msat, randomKey().publicKey, startedAt, None, None, OutgoingPaymentStatus.Failed(Nil, failedAt))
kit.nodeParams.db.payments.addOutgoingPayment(failedPayment.copy(status = OutgoingPaymentStatus.Pending))
- kit.nodeParams.db.payments.updateOutgoingPayment(PaymentFailed(failedPayment.id, failedPayment.paymentHash, Nil, failedAt))
+ kit.nodeParams.db.payments.updateOutgoingPayment(PaymentFailed(failedPayment.id, failedPayment.paymentHash, Nil, startedAt, failedAt))
eclair.sentInfo(PaymentIdentifier.PaymentUUID(failedPayment.parentId)).pipeTo(sender.ref)
paymentInitiator.expectMsg(GetPayment(PaymentIdentifier.PaymentUUID(failedPayment.parentId)))
paymentInitiator.reply(NoPendingPayment(PaymentIdentifier.PaymentUUID(failedPayment.parentId)))
diff --git a/eclair-core/src/test/scala/fr/acinq/eclair/db/AuditDbSpec.scala b/eclair-core/src/test/scala/fr/acinq/eclair/db/AuditDbSpec.scala
index 94a095e..88bdd53 100644
--- a/eclair-core/src/test/scala/fr/acinq/eclair/db/AuditDbSpec.scala
+++ b/eclair-core/src/test/scala/fr/acinq/eclair/db/AuditDbSpec.scala
@@ -63,7 +63,7 @@ class AuditDbSpec extends AnyFunSuite {
val db = dbs.audit
val now = TimestampMilli.now()
- val e1 = PaymentSent(ZERO_UUID, randomBytes32(), randomBytes32(), 40000 msat, randomKey().publicKey, PaymentSent.PartialPayment(ZERO_UUID, 42000 msat, 1000 msat, randomBytes32(), None) :: Nil, None)
+ val e1 = PaymentSent(ZERO_UUID, randomBytes32(), 40000 msat, randomKey().publicKey, PaymentSent.PartialPayment(ZERO_UUID, 42000 msat, 1000 msat, randomBytes32(), None, now, now) :: Nil, None, now)
val pp2a = PaymentReceived.PartialPayment(42000 msat, randomBytes32())
val pp2b = PaymentReceived.PartialPayment(42100 msat, randomBytes32())
val e2 = PaymentReceived(randomBytes32(), pp2a :: pp2b :: Nil)
@@ -71,11 +71,11 @@ class AuditDbSpec extends AnyFunSuite {
val e4a = TransactionPublished(randomBytes32(), randomKey().publicKey, Transaction(0, Seq.empty, Seq.empty, 0), 42 sat, "mutual")
val e4b = TransactionConfirmed(e4a.channelId, e4a.remoteNodeId, e4a.tx)
val e4c = TransactionConfirmed(randomBytes32(), randomKey().publicKey, Transaction(2, Nil, TxOut(500 sat, hex"1234") :: Nil, 0))
- val pp5a = PaymentSent.PartialPayment(UUID.randomUUID(), 42000 msat, 1000 msat, randomBytes32(), None, timestamp = 0 unixms)
- val pp5b = PaymentSent.PartialPayment(UUID.randomUUID(), 42100 msat, 900 msat, randomBytes32(), None, timestamp = 1 unixms)
- val e5 = PaymentSent(UUID.randomUUID(), randomBytes32(), randomBytes32(), 84100 msat, randomKey().publicKey, pp5a :: pp5b :: Nil, None)
- val pp6 = PaymentSent.PartialPayment(UUID.randomUUID(), 42000 msat, 1000 msat, randomBytes32(), None, timestamp = now + 10.minutes)
- val e6 = PaymentSent(UUID.randomUUID(), randomBytes32(), randomBytes32(), 42000 msat, randomKey().publicKey, pp6 :: Nil, None)
+ val pp5a = PaymentSent.PartialPayment(UUID.randomUUID(), 42000 msat, 1000 msat, randomBytes32(), None, startedAt = 0 unixms, settledAt = 0 unixms)
+ val pp5b = PaymentSent.PartialPayment(UUID.randomUUID(), 42100 msat, 900 msat, randomBytes32(), None, startedAt = 1 unixms, settledAt = 1 unixms)
+ val e5 = PaymentSent(UUID.randomUUID(), randomBytes32(), 84100 msat, randomKey().publicKey, pp5a :: pp5b :: Nil, None, startedAt = 0 unixms)
+ val pp6 = PaymentSent.PartialPayment(UUID.randomUUID(), 42000 msat, 1000 msat, randomBytes32(), None, startedAt = now + 10.minutes, settledAt = now + 10.minutes)
+ val e6 = PaymentSent(UUID.randomUUID(), randomBytes32(), 42000 msat, randomKey().publicKey, pp6 :: Nil, None, startedAt = now + 10.minutes)
val e7 = ChannelEvent(randomBytes32(), randomKey().publicKey, randomTxId(), 456123000 sat, isChannelOpener = true, isPrivate = false, ChannelEvent.EventType.Closed(MutualClose(null)))
val e10 = TrampolinePaymentRelayed(randomBytes32(),
Seq(
diff --git a/eclair-core/src/test/scala/fr/acinq/eclair/db/PaymentsDbSpec.scala b/eclair-core/src/test/scala/fr/acinq/eclair/db/PaymentsDbSpec.scala
index ba41fe6..238e6a2 100644
--- a/eclair-core/src/test/scala/fr/acinq/eclair/db/PaymentsDbSpec.scala
+++ b/eclair-core/src/test/scala/fr/acinq/eclair/db/PaymentsDbSpec.scala
@@ -200,9 +200,9 @@ class PaymentsDbSpec extends AnyFunSuite {
val ps6 = OutgoingPayment(UUID.randomUUID(), UUID.randomUUID(), Some("3"), randomBytes32(), PaymentType.Standard, 789 msat, 789 msat, bob, 1250 unixms, None, None, OutgoingPaymentStatus.Failed(Nil, 1300 unixms))
db.addOutgoingPayment(ps4)
db.addOutgoingPayment(ps5.copy(status = OutgoingPaymentStatus.Pending))
- db.updateOutgoingPayment(PaymentSent(ps5.parentId, ps5.paymentHash, preimage1, ps5.amount, ps5.recipientNodeId, Seq(PaymentSent.PartialPayment(ps5.id, ps5.amount, 42 msat, randomBytes32(), None, 1180 unixms)), None))
+ db.updateOutgoingPayment(PaymentSent(ps5.parentId, preimage1, ps5.amount, ps5.recipientNodeId, Seq(PaymentSent.PartialPayment(ps5.id, ps5.amount, 42 msat, randomBytes32(), None, 1000 unixms, 1180 unixms)), None, 900 unixms))
db.addOutgoingPayment(ps6.copy(status = OutgoingPaymentStatus.Pending))
- db.updateOutgoingPayment(PaymentFailed(ps6.id, ps6.paymentHash, Nil, 1300 unixms))
+ db.updateOutgoingPayment(PaymentFailed(ps6.id, ps6.paymentHash, Nil, 1100 unixms, 1300 unixms))
assert(db.listOutgoingPayments(1 unixms, 2000 unixms) == Seq(ps1, ps2, ps3, ps4, ps5, ps6))
assert(db.listIncomingPayments(1 unixms, TimestampMilli.now(), None) == Seq(pr1, pr2, pr3))
@@ -764,20 +764,20 @@ class PaymentsDbSpec extends AnyFunSuite {
db.addOutgoingPayment(s3)
db.addOutgoingPayment(s4)
- db.updateOutgoingPayment(PaymentFailed(s3.id, s3.paymentHash, Nil, 310 unixms))
+ db.updateOutgoingPayment(PaymentFailed(s3.id, s3.paymentHash, Nil, 10 unixms, 310 unixms))
val ss3 = s3.copy(status = OutgoingPaymentStatus.Failed(Nil, 310 unixms))
assert(db.getOutgoingPayment(s3.id).contains(ss3))
- db.updateOutgoingPayment(PaymentFailed(s4.id, s4.paymentHash, Seq(LocalFailure(s4.amount, Seq(hop_ab), new RuntimeException("woops")), RemoteFailure(s4.amount, Seq(hop_ab, hop_bc), Sphinx.DecryptedFailurePacket(carol, 2, UnknownNextPeer()))), 320 unixms))
+ db.updateOutgoingPayment(PaymentFailed(s4.id, s4.paymentHash, Seq(LocalFailure(s4.amount, Seq(hop_ab), new RuntimeException("woops")), RemoteFailure(s4.amount, Seq(hop_ab, hop_bc), Sphinx.DecryptedFailurePacket(carol, 2, UnknownNextPeer()), startedAt = 10 unixms, failedAt = 320 unixms)), startedAt = 100 unixms, settledAt = 320 unixms))
val ss4 = s4.copy(status = OutgoingPaymentStatus.Failed(Seq(FailureSummary(FailureType.LOCAL, "woops", List(HopSummary(alice, bob, Some(ShortChannelId(42)))), Some(alice)), FailureSummary(FailureType.REMOTE, "processing node does not know the next peer in the route", List(HopSummary(alice, bob, Some(ShortChannelId(42))), HopSummary(bob, carol, None)), Some(carol))), 320 unixms))
assert(db.getOutgoingPayment(s4.id).contains(ss4))
// can't update again once it's in a final state
- assertThrows[IllegalArgumentException](db.updateOutgoingPayment(PaymentSent(parentId, s3.paymentHash, preimage1, s3.recipientAmount, s3.recipientNodeId, Seq(PaymentSent.PartialPayment(s3.id, s3.amount, 42 msat, randomBytes32(), None)), None)))
+ assertThrows[IllegalArgumentException](db.updateOutgoingPayment(PaymentSent(parentId, preimage1, s3.recipientAmount, s3.recipientNodeId, Seq(PaymentSent.PartialPayment(s3.id, s3.amount, 42 msat, randomBytes32(), None, startedAt = 100 unixms, settledAt = 500 unixms)), None, 100 unixms)))
- val paymentSent = PaymentSent(parentId, paymentHash1, preimage1, 600 msat, carol, Seq(
- PaymentSent.PartialPayment(s1.id, s1.amount, 15 msat, randomBytes32(), None, 400 unixms),
- PaymentSent.PartialPayment(s2.id, s2.amount, 20 msat, randomBytes32(), Some(Seq(hop_ab, hop_bc)), 410 unixms)
- ), None)
+ val paymentSent = PaymentSent(parentId, preimage1, 600 msat, carol, Seq(
+ PaymentSent.PartialPayment(s1.id, s1.amount, 15 msat, randomBytes32(), None, startedAt = 200 unixms, settledAt = 400 unixms),
+ PaymentSent.PartialPayment(s2.id, s2.amount, 20 msat, randomBytes32(), Some(Seq(hop_ab, hop_bc)), startedAt = 210 unixms, settledAt = 410 unixms)
+ ), None, startedAt = 100 unixms)
val ss1 = s1.copy(status = OutgoingPaymentStatus.Succeeded(preimage1, 15 msat, Nil, 400 unixms))
val ss2 = s2.copy(status = OutgoingPaymentStatus.Succeeded(preimage1, 20 msat, Seq(HopSummary(alice, bob, Some(ShortChannelId(42))), HopSummary(bob, carol, None)), 410 unixms))
db.updateOutgoingPayment(paymentSent)
@@ -786,7 +786,7 @@ class PaymentsDbSpec extends AnyFunSuite {
assert(db.listOutgoingPayments(parentId) == Seq(ss1, ss2, ss3, ss4))
// can't update again once it's in a final state
- assertThrows[IllegalArgumentException](db.updateOutgoingPayment(PaymentFailed(s1.id, s1.paymentHash, Nil)))
+ assertThrows[IllegalArgumentException](db.updateOutgoingPayment(PaymentFailed(s1.id, s1.paymentHash, Nil, startedAt = 200 unixms, settledAt = 400 unixms)))
}
}
diff --git a/eclair-core/src/test/scala/fr/acinq/eclair/integration/PaymentIntegrationSpec.scala b/eclair-core/src/test/scala/fr/acinq/eclair/integration/PaymentIntegrationSpec.scala
index 0d8e890..cad805f 100644
--- a/eclair-core/src/test/scala/fr/acinq/eclair/integration/PaymentIntegrationSpec.scala
+++ b/eclair-core/src/test/scala/fr/acinq/eclair/integration/PaymentIntegrationSpec.scala
@@ -48,7 +48,7 @@ import fr.acinq.eclair.router.Router.{ChannelHop, GossipDecision, PublicChannel}
import fr.acinq.eclair.router.{Announcements, AnnouncementsBatchValidationSpec, Router}
import fr.acinq.eclair.wire.protocol.OfferTypes.{Offer, OfferPaths}
import fr.acinq.eclair.wire.protocol.{ChannelAnnouncement, ChannelUpdate, IncorrectOrUnknownPaymentDetails}
-import fr.acinq.eclair.{CltvExpiryDelta, EclairImpl, EncodedNodeId, Features, Kit, MilliSatoshiLong, ShortChannelId, TimestampMilli, randomBytes32, randomKey}
+import fr.acinq.eclair.{CltvExpiryDelta, EclairImpl, EncodedNodeId, Features, Kit, MilliSatoshiLong, ShortChannelId, TimestampMilli,TimestampMilliLong, randomBytes32, randomKey}
import org.json4s.JsonAST.{JString, JValue}
import scodec.bits.{ByteVector, HexStringSyntax}
@@ -369,6 +369,7 @@ class PaymentIntegrationSpec extends IntegrationSpec {
assert(paymentSent.feesPaid > 0.msat, paymentSent)
assert(paymentSent.parts.forall(p => p.id != paymentSent.id), paymentSent)
assert(paymentSent.parts.forall(p => p.route.isDefined), paymentSent)
+ assert(paymentSent.startedAt >= start)
val paymentParts = nodes("B").nodeParams.db.payments.listOutgoingPayments(paymentId).filter(_.status.isInstanceOf[OutgoingPaymentStatus.Succeeded])
assert(paymentParts.length == paymentSent.parts.length, paymentParts)
@@ -380,7 +381,14 @@ class PaymentIntegrationSpec extends IntegrationSpec {
awaitCond(nodes("B").nodeParams.db.audit.listSent(start, TimestampMilli.now()).nonEmpty)
val sent = nodes("B").nodeParams.db.audit.listSent(start, TimestampMilli.now())
assert(sent.length == 1, sent)
- assert(sent.head.copy(parts = sent.head.parts.sortBy(_.timestamp)) == paymentSent.copy(parts = paymentSent.parts.map(_.copy(route = None)).sortBy(_.timestamp), remainingAttribution_opt = None), sent)
+ val paymentSent1 = paymentSent.copy(
+ // We don't store the route in the DB, and don't store the startedAt timestamp yet (we set it to the same value as settledAt).
+ parts = paymentSent.parts.map(p => p.copy(route = None, startedAt = p.settledAt)).sortBy(_.settledAt),
+ // We don't store attribution data in the DB.
+ remainingAttribution_opt = None,
+ startedAt = 0 unixms,
+ )
+ assert(sent.head.copy(parts = sent.head.parts.sortBy(_.settledAt), startedAt = 0 unixms) == paymentSent1)
awaitCond(nodes("D").nodeParams.db.payments.getIncomingPayment(invoice.paymentHash).exists(_.status.isInstanceOf[IncomingPaymentStatus.Received]))
val Some(IncomingStandardPayment(_, _, _, _, IncomingPaymentStatus.Received(receivedAmount, _))) = nodes("D").nodeParams.db.payments.getIncomingPayment(invoice.paymentHash)
diff --git a/eclair-core/src/test/scala/fr/acinq/eclair/integration/basic/zeroconf/ZeroConfAliasIntegrationSpec.scala b/eclair-core/src/test/scala/fr/acinq/eclair/integration/basic/zeroconf/ZeroConfAliasIntegrationSpec.scala
index 43e337d..bf25698 100644
--- a/eclair-core/src/test/scala/fr/acinq/eclair/integration/basic/zeroconf/ZeroConfAliasIntegrationSpec.scala
+++ b/eclair-core/src/test/scala/fr/acinq/eclair/integration/basic/zeroconf/ZeroConfAliasIntegrationSpec.scala
@@ -88,8 +88,8 @@ class ZeroConfAliasIntegrationSpec extends FixtureSpec with IntegrationPatience
val result = sendPayment(alice, carol, 100_000 msat, hints = List(createBobToCarolTestHint(f, useHint, overrideHintScid_opt))) match {
case Left(paymentFailed) =>
Left(PaymentFailure.transformForUser(paymentFailed.failures).last match {
- case LocalFailure(_, _, t) => Left(t)
- case RemoteFailure(_, _, e) => Right(e.failureMessage)
+ case f: LocalFailure => Left(f.t)
+ case f: RemoteFailure => Right(f.e.failureMessage)
case _: UnreadableRemoteFailure => fail("received unreadable remote failure")
})
case Right(_) => Right(Ok)
@@ -277,7 +277,7 @@ class ZeroConfAliasIntegrationSpec extends FixtureSpec with IntegrationPatience
// The channel update returned in failures doesn't leak the real scid.
val failure = sendFailingPayment(alice, carol, 40_000_000 msat, hints = List(List(carolHint)))
- val failureWithChannelUpdate = failure.failures.collect { case RemoteFailure(_, _, Sphinx.DecryptedFailurePacket(_, _, f: Update)) => f }
+ val failureWithChannelUpdate = failure.failures.collect { case RemoteFailure(_, _, Sphinx.DecryptedFailurePacket(_, _, f: Update), _, _) => f }
assert(failureWithChannelUpdate.length == 1)
assert(failureWithChannelUpdate.head.update_opt.map(_.shortChannelId).contains(bobAlias))
}
diff --git a/eclair-core/src/test/scala/fr/acinq/eclair/json/JsonSerializersSpec.scala b/eclair-core/src/test/scala/fr/acinq/eclair/json/JsonSerializersSpec.scala
index eab686c..857eb29 100644
--- a/eclair-core/src/test/scala/fr/acinq/eclair/json/JsonSerializersSpec.scala
+++ b/eclair-core/src/test/scala/fr/acinq/eclair/json/JsonSerializersSpec.scala
@@ -30,7 +30,7 @@ import fr.acinq.eclair.crypto.{ShaChain, Sphinx}
import fr.acinq.eclair.db.OfferData
import fr.acinq.eclair.io.Peer
import fr.acinq.eclair.io.Peer.PeerInfo
-import fr.acinq.eclair.payment.{Invoice, PaymentSettlingOnChain}
+import fr.acinq.eclair.payment.{ChannelPaymentRelayed, Invoice}
import fr.acinq.eclair.transactions.Transactions._
import fr.acinq.eclair.transactions.{CommitmentSpec, IncomingHtlc, OutgoingHtlc, Transactions}
import fr.acinq.eclair.wire.internal.channel.ChannelCodecs
@@ -404,8 +404,8 @@ class JsonSerializersSpec extends TestKitBaseClass with AnyFunSuiteLike with Mat
}
test("type hints") {
- val e1 = PaymentSettlingOnChain(UUID.randomUUID, 42 msat, randomBytes32())
- assert(JsonSerializers.serialization.writePretty(e1)(JsonSerializers.formats).contains("\"type\" : \"payment-settling-onchain\""))
+ val e1 = ChannelPaymentRelayed(110 msat, 100 msat, randomBytes32(), randomBytes32(), randomBytes32(), 100 unixms, 150 unixms)
+ assert(JsonSerializers.serialization.writePretty(e1)(JsonSerializers.formats).contains("\"type\" : \"payment-relayed\""))
}
test("transaction serializer") {
diff --git a/eclair-core/src/test/scala/fr/acinq/eclair/payment/MultiPartHandlerSpec.scala b/eclair-core/src/test/scala/fr/acinq/eclair/payment/MultiPartHandlerSpec.scala
index 3d435b8..0557333 100644
--- a/eclair-core/src/test/scala/fr/acinq/eclair/payment/MultiPartHandlerSpec.scala
+++ b/eclair-core/src/test/scala/fr/acinq/eclair/payment/MultiPartHandlerSpec.scala
@@ -22,7 +22,7 @@ import fr.acinq.bitcoin.scalacompat.{Block, ByteVector32, Crypto}
import fr.acinq.eclair.FeatureSupport.{Mandatory, Optional}
import fr.acinq.eclair.Features.{KeySend, _}
import fr.acinq.eclair.TestConstants.Alice
-import fr.acinq.eclair.channel.{CMD_FAIL_HTLC, CMD_FULFILL_HTLC, FailureAttributionData, FulfillAttributionData, Register}
+import fr.acinq.eclair.channel._
import fr.acinq.eclair.db.{IncomingBlindedPayment, IncomingPaymentStatus, IncomingStandardPayment, PaymentType}
import fr.acinq.eclair.payment.Bolt11Invoice.ExtraHop
import fr.acinq.eclair.payment.PaymentReceived.PartialPayment
@@ -30,8 +30,6 @@ import fr.acinq.eclair.payment.offer.OfferManager
import fr.acinq.eclair.payment.receive.MultiPartHandler._
import fr.acinq.eclair.payment.receive.MultiPartPaymentFSM.HtlcPart
import fr.acinq.eclair.payment.receive.{MultiPartPaymentFSM, PaymentHandler}
-import fr.acinq.eclair.payment.relay.Relayer.RelayFees
-import fr.acinq.eclair.reputation.Reputation
import fr.acinq.eclair.router.BlindedRouteCreation.aggregatePaymentInfo
import fr.acinq.eclair.router.Router
import fr.acinq.eclair.router.Router.ChannelHop
@@ -40,7 +38,7 @@ import fr.acinq.eclair.wire.protocol.OnionPaymentPayloadTlv._
import fr.acinq.eclair.wire.protocol.PaymentOnion.FinalPayload
import fr.acinq.eclair.wire.protocol.RouteBlindingEncryptedDataTlv.{PathId, PaymentConstraints}
import fr.acinq.eclair.wire.protocol._
-import fr.acinq.eclair.{CltvExpiry, CltvExpiryDelta, EncodedNodeId, Feature, Features, MilliSatoshi, MilliSatoshiLong, NodeParams, ShortChannelId, TestConstants, TestKitBaseClass, TimestampMilli, TimestampMilliLong, randomBytes, randomBytes32, randomKey}
+import fr.acinq.eclair.{CltvExpiry, CltvExpiryDelta, EncodedNodeId, Feature, Features, MilliSatoshi, MilliSatoshiLong, NodeParams, ShortChannelId, TestConstants, TestKitBaseClass, TimestampMilli, randomBytes, randomBytes32, randomKey}
import org.scalatest.Outcome
import org.scalatest.funsuite.FixtureAnyFunSuiteLike
import scodec.bits.{ByteVector, HexStringSyntax}
@@ -97,16 +95,17 @@ class MultiPartHandlerSpec extends TestKitBaseClass with FixtureAnyFunSuiteLike
}
}
- def createBlindedPacket(amount: MilliSatoshi, paymentHash: ByteVector32, expiry: CltvExpiry, finalExpiry: CltvExpiry, pathId: ByteVector): IncomingPaymentPacket.FinalPacket = {
+ def createBlindedPacket(amount: MilliSatoshi, paymentHash: ByteVector32, expiry: CltvExpiry, finalExpiry: CltvExpiry, pathId: ByteVector, receivedAt: TimestampMilli = TimestampMilli.now()): IncomingPaymentPacket.FinalPacket = {
val add = UpdateAddHtlc(ByteVector32.One, 0, amount, paymentHash, expiry, TestConstants.emptyOnionPacket, Some(randomKey().publicKey), accountable = false, None)
val payload = FinalPayload.Blinded(TlvStream(AmountToForward(amount), TotalAmount(amount), OutgoingCltv(finalExpiry), EncryptedRecipientData(hex"deadbeef")), TlvStream(PathId(pathId), PaymentConstraints(CltvExpiry(500_000), 1 msat)))
- IncomingPaymentPacket.FinalPacket(add, payload, TimestampMilli.now())
+ IncomingPaymentPacket.FinalPacket(add, payload, receivedAt)
}
test("PaymentHandler should reply with a fulfill/fail, emit a PaymentReceived and add payment in DB") { f =>
import f._
val amountMsat = 42000 msat
+ val receivedAt = TimestampMilli.now()
{
sender.send(handlerWithoutMpp, ReceiveStandardPayment(sender.ref, Some(amountMsat), Left("1 coffee")))
@@ -118,14 +117,14 @@ class MultiPartHandlerSpec extends TestKitBaseClass with FixtureAnyFunSuiteLike
assert(Crypto.sha256(incoming.get.paymentPreimage) == invoice.paymentHash)
val add = UpdateAddHtlc(ByteVector32.One, 1, amountMsat, invoice.paymentHash, defaultExpiry, TestConstants.emptyOnionPacket, None, accountable = false, None)
- sender.send(handlerWithoutMpp, IncomingPaymentPacket.FinalPacket(add, FinalPayload.Standard.createPayload(add.amountMsat, add.amountMsat, add.cltvExpiry, invoice.paymentSecret, invoice.paymentMetadata, upgradeAccountability = false), TimestampMilli.now()))
+ sender.send(handlerWithoutMpp, IncomingPaymentPacket.FinalPacket(add, FinalPayload.Standard.createPayload(add.amountMsat, add.amountMsat, add.cltvExpiry, invoice.paymentSecret, invoice.paymentMetadata, upgradeAccountability = false), receivedAt))
assert(register.expectMsgType[Register.Forward[CMD_FULFILL_HTLC]].message.id == add.id)
val paymentReceived = eventListener.expectMsgType[PaymentReceived]
- assert(paymentReceived.copy(parts = paymentReceived.parts.map(_.copy(timestamp = 0 unixms))) == PaymentReceived(add.paymentHash, PartialPayment(amountMsat, add.channelId, timestamp = 0 unixms) :: Nil))
+ assert(paymentReceived == PaymentReceived(add.paymentHash, PartialPayment(amountMsat, add.channelId, receivedAt) :: Nil))
val received = nodeParams.db.payments.getIncomingPayment(invoice.paymentHash)
assert(received.isDefined && received.get.status.isInstanceOf[IncomingPaymentStatus.Received])
- assert(received.get.status.asInstanceOf[IncomingPaymentStatus.Received].copy(receivedAt = 0 unixms) == IncomingPaymentStatus.Received(amountMsat, 0 unixms))
+ assert(received.get.status.asInstanceOf[IncomingPaymentStatus.Received] == IncomingPaymentStatus.Received(amountMsat, receivedAt))
sender.expectNoMessage(50 millis)
}
@@ -134,14 +133,14 @@ class MultiPartHandlerSpec extends TestKitBaseClass with FixtureAnyFunSuiteLike
val invoice = sender.expectMsgType[Bolt11Invoice]
val add = UpdateAddHtlc(ByteVector32.One, 1, 75_000 msat, invoice.paymentHash, defaultExpiry + CltvExpiryDelta(12), TestConstants.emptyOnionPacket, None, accountable = false, None)
- sender.send(handlerWithoutMpp, IncomingPaymentPacket.FinalPacket(add, FinalPayload.Standard.createPayload(70_000 msat, 70_000 msat, defaultExpiry, invoice.paymentSecret, invoice.paymentMetadata, upgradeAccountability = false), TimestampMilli.now()))
+ sender.send(handlerWithoutMpp, IncomingPaymentPacket.FinalPacket(add, FinalPayload.Standard.createPayload(70_000 msat, 70_000 msat, defaultExpiry, invoice.paymentSecret, invoice.paymentMetadata, upgradeAccountability = false), receivedAt))
assert(register.expectMsgType[Register.Forward[CMD_FULFILL_HTLC]].message.id == add.id)
val paymentReceived = eventListener.expectMsgType[PaymentReceived]
- assert(paymentReceived.copy(parts = paymentReceived.parts.map(_.copy(timestamp = 0 unixms))) == PaymentReceived(add.paymentHash, PartialPayment(add.amountMsat, add.channelId, timestamp = 0 unixms) :: Nil))
+ assert(paymentReceived == PaymentReceived(add.paymentHash, PartialPayment(add.amountMsat, add.channelId, receivedAt) :: Nil))
val received = nodeParams.db.payments.getIncomingPayment(invoice.paymentHash)
assert(received.isDefined && received.get.status.isInstanceOf[IncomingPaymentStatus.Received])
- assert(received.get.status.asInstanceOf[IncomingPaymentStatus.Received].copy(receivedAt = 0 unixms) == IncomingPaymentStatus.Received(add.amountMsat, 0 unixms))
+ assert(received.get.status.asInstanceOf[IncomingPaymentStatus.Received] == IncomingPaymentStatus.Received(add.amountMsat, receivedAt))
sender.expectNoMessage(50 millis)
}
@@ -152,14 +151,14 @@ class MultiPartHandlerSpec extends TestKitBaseClass with FixtureAnyFunSuiteLike
assert(nodeParams.db.payments.getIncomingPayment(invoice.paymentHash).get.status == IncomingPaymentStatus.Pending)
val add = UpdateAddHtlc(ByteVector32.One, 2, amountMsat, invoice.paymentHash, defaultExpiry, TestConstants.emptyOnionPacket, None, accountable = false, None)
- sender.send(handlerWithMpp, IncomingPaymentPacket.FinalPacket(add, FinalPayload.Standard.createPayload(add.amountMsat, add.amountMsat, add.cltvExpiry, invoice.paymentSecret, invoice.paymentMetadata, upgradeAccountability = false), TimestampMilli.now()))
+ sender.send(handlerWithMpp, IncomingPaymentPacket.FinalPacket(add, FinalPayload.Standard.createPayload(add.amountMsat, add.amountMsat, add.cltvExpiry, invoice.paymentSecret, invoice.paymentMetadata, upgradeAccountability = false), receivedAt))
assert(register.expectMsgType[Register.Forward[CMD_FULFILL_HTLC]].message.id == add.id)
val paymentReceived = eventListener.expectMsgType[PaymentReceived]
- assert(paymentReceived.copy(parts = paymentReceived.parts.map(_.copy(timestamp = 0 unixms))) == PaymentReceived(add.paymentHash, PartialPayment(amountMsat, add.channelId, timestamp = 0 unixms) :: Nil))
+ assert(paymentReceived == PaymentReceived(add.paymentHash, PartialPayment(amountMsat, add.channelId, receivedAt) :: Nil))
val received = nodeParams.db.payments.getIncomingPayment(invoice.paymentHash)
assert(received.isDefined && received.get.status.isInstanceOf[IncomingPaymentStatus.Received])
- assert(received.get.status.asInstanceOf[IncomingPaymentStatus.Received].copy(receivedAt = 0 unixms) == IncomingPaymentStatus.Received(amountMsat, 0 unixms))
+ assert(received.get.status.asInstanceOf[IncomingPaymentStatus.Received] == IncomingPaymentStatus.Received(amountMsat, receivedAt))
sender.expectNoMessage(50 millis)
}
@@ -176,20 +175,20 @@ class MultiPartHandlerSpec extends TestKitBaseClass with FixtureAnyFunSuiteLike
// Offer invoices shouldn't be stored in the DB until we receive a payment for it.
assert(nodeParams.db.payments.getIncomingPayment(invoice.paymentHash).isEmpty)
- val finalPacket = createBlindedPacket(amountMsat, invoice.paymentHash, defaultExpiry, CltvExpiry(nodeParams.currentBlockHeight), pathId)
+ val finalPacket = createBlindedPacket(amountMsat, invoice.paymentHash, defaultExpiry, CltvExpiry(nodeParams.currentBlockHeight), pathId, receivedAt)
sender.send(handlerWithRouteBlinding, finalPacket)
val receivePayment = offerManager.expectMsgType[OfferManager.ReceivePayment]
assert(receivePayment.paymentHash == invoice.paymentHash)
assert(receivePayment.payload.pathId == pathId.bytes)
- val payment = IncomingBlindedPayment(MinimalBolt12Invoice(invoice.records), preimage, PaymentType.Blinded, TimestampMilli.now(), IncomingPaymentStatus.Pending)
+ val payment = IncomingBlindedPayment(MinimalBolt12Invoice(invoice.records), preimage, PaymentType.Blinded, receivedAt, IncomingPaymentStatus.Pending)
receivePayment.replyTo ! GetIncomingPaymentActor.ProcessPayment(payment, 0 msat)
assert(register.expectMsgType[Register.Forward[CMD_FULFILL_HTLC]].message.id == finalPacket.add.id)
val paymentReceived = eventListener.expectMsgType[PaymentReceived]
- assert(paymentReceived.copy(parts = paymentReceived.parts.map(_.copy(timestamp = 0 unixms))) == PaymentReceived(finalPacket.add.paymentHash, PartialPayment(amountMsat, finalPacket.add.channelId, timestamp = 0 unixms) :: Nil))
+ assert(paymentReceived == PaymentReceived(finalPacket.add.paymentHash, PartialPayment(amountMsat, finalPacket.add.channelId, receivedAt) :: Nil))
val received = nodeParams.db.payments.getIncomingPayment(invoice.paymentHash)
assert(received.isDefined && received.get.status.isInstanceOf[IncomingPaymentStatus.Received])
- assert(received.get.status.asInstanceOf[IncomingPaymentStatus.Received].copy(receivedAt = 0 unixms) == IncomingPaymentStatus.Received(amountMsat, 0 unixms))
+ assert(received.get.status.asInstanceOf[IncomingPaymentStatus.Received] == IncomingPaymentStatus.Received(amountMsat, receivedAt))
sender.expectNoMessage(50 millis)
}
@@ -199,7 +198,7 @@ class MultiPartHandlerSpec extends TestKitBaseClass with FixtureAnyFunSuiteLike
assert(nodeParams.db.payments.getIncomingPayment(invoice.paymentHash).get.status == IncomingPaymentStatus.Pending)
val add = UpdateAddHtlc(ByteVector32.One, 0, amountMsat, invoice.paymentHash, CltvExpiryDelta(3).toCltvExpiry(nodeParams.currentBlockHeight), TestConstants.emptyOnionPacket, None, accountable = false, None)
- sender.send(handlerWithMpp, IncomingPaymentPacket.FinalPacket(add, FinalPayload.Standard.createPayload(add.amountMsat, add.amountMsat, add.cltvExpiry, invoice.paymentSecret, invoice.paymentMetadata, upgradeAccountability = false), TimestampMilli.now()))
+ sender.send(handlerWithMpp, IncomingPaymentPacket.FinalPacket(add, FinalPayload.Standard.createPayload(add.amountMsat, add.amountMsat, add.cltvExpiry, invoice.paymentSecret, invoice.paymentMetadata, upgradeAccountability = false), receivedAt))
val cmd = register.expectMsgType[Register.Forward[CMD_FAIL_HTLC]].message
assert(cmd.reason == FailureReason.LocalFailure(IncorrectOrUnknownPaymentDetails(amountMsat, nodeParams.currentBlockHeight)))
assert(nodeParams.db.payments.getIncomingPayment(invoice.paymentHash).get.status == IncomingPaymentStatus.Pending)
@@ -608,7 +607,7 @@ class MultiPartHandlerSpec extends TestKitBaseClass with FixtureAnyFunSuiteLike
)
val paymentReceived = f.eventListener.expectMsgType[PaymentReceived]
- assert(paymentReceived.parts.map(_.copy(timestamp = 0 unixms)).toSet == Set(PartialPayment(800 msat, ByteVector32.One, 0 unixms), PartialPayment(200 msat, ByteVector32.Zeroes, 0 unixms)))
+ assert(paymentReceived.parts.toSet == Set(PartialPayment(800 msat, ByteVector32.One, receivedAt1), PartialPayment(200 msat, ByteVector32.Zeroes, receivedAt3)))
val received = nodeParams.db.payments.getIncomingPayment(invoice.paymentHash)
assert(received.isDefined && received.get.status.isInstanceOf[IncomingPaymentStatus.Received])
assert(received.get.status.asInstanceOf[IncomingPaymentStatus.Received].amount == 1000.msat)
@@ -650,7 +649,7 @@ class MultiPartHandlerSpec extends TestKitBaseClass with FixtureAnyFunSuiteLike
)
val paymentReceived = f.eventListener.expectMsgType[PaymentReceived]
- assert(paymentReceived.parts.map(_.copy(timestamp = 0 unixms)).toSet == Set(PartialPayment(1100 msat, add1.channelId, 0 unixms), PartialPayment(500 msat, add2.channelId, 0 unixms)))
+ assert(paymentReceived.parts.toSet == Set(PartialPayment(1100 msat, add1.channelId, receivedAt1), PartialPayment(500 msat, add2.channelId, receivedAt2)))
val received = nodeParams.db.payments.getIncomingPayment(invoice.paymentHash)
assert(received.isDefined && received.get.status.isInstanceOf[IncomingPaymentStatus.Received])
assert(received.get.status.asInstanceOf[IncomingPaymentStatus.Received].amount == 1600.msat)
@@ -690,7 +689,7 @@ class MultiPartHandlerSpec extends TestKitBaseClass with FixtureAnyFunSuiteLike
val paymentReceived = f.eventListener.expectMsgType[PaymentReceived]
assert(paymentReceived.paymentHash == invoice.paymentHash)
- assert(paymentReceived.parts.map(_.copy(timestamp = 0 unixms)).toSet == Set(PartialPayment(300 msat, ByteVector32.One, 0 unixms), PartialPayment(700 msat, ByteVector32.Zeroes, 0 unixms)))
+ assert(paymentReceived.parts.toSet == Set(PartialPayment(300 msat, ByteVector32.One, receivedAt2), PartialPayment(700 msat, ByteVector32.Zeroes, receivedAt3)))
val received = nodeParams.db.payments.getIncomingPayment(invoice.paymentHash)
assert(received.isDefined && received.get.status.isInstanceOf[IncomingPaymentStatus.Received])
assert(received.get.status.asInstanceOf[IncomingPaymentStatus.Received].amount == 1000.msat)
@@ -712,14 +711,15 @@ class MultiPartHandlerSpec extends TestKitBaseClass with FixtureAnyFunSuiteLike
assert(nodeParams.db.payments.getIncomingPayment(paymentHash).isEmpty)
val add = UpdateAddHtlc(ByteVector32.One, 0, amountMsat, paymentHash, defaultExpiry, TestConstants.emptyOnionPacket, None, accountable = false, None)
- sender.send(handlerWithKeySend, IncomingPaymentPacket.FinalPacket(add, payload, TimestampMilli.now()))
+ val receivedAt = TimestampMilli.now()
+ sender.send(handlerWithKeySend, IncomingPaymentPacket.FinalPacket(add, payload, receivedAt))
register.expectMsgType[Register.Forward[CMD_FULFILL_HTLC]]
val paymentReceived = eventListener.expectMsgType[PaymentReceived]
- assert(paymentReceived.copy(parts = paymentReceived.parts.map(_.copy(timestamp = 0 unixms))) == PaymentReceived(add.paymentHash, PartialPayment(amountMsat, add.channelId, timestamp = 0 unixms) :: Nil))
+ assert(paymentReceived == PaymentReceived(add.paymentHash, PartialPayment(amountMsat, add.channelId, receivedAt) :: Nil))
val received = nodeParams.db.payments.getIncomingPayment(paymentHash)
assert(received.isDefined && received.get.status.isInstanceOf[IncomingPaymentStatus.Received])
- assert(received.get.status.asInstanceOf[IncomingPaymentStatus.Received].copy(receivedAt = 0 unixms) == IncomingPaymentStatus.Received(amountMsat, 0 unixms))
+ assert(received.get.status.asInstanceOf[IncomingPaymentStatus.Received] == IncomingPaymentStatus.Received(amountMsat, receivedAt))
}
test("PaymentHandler should handle single-part KeySend payment without payment secret") { f =>
@@ -733,14 +733,15 @@ class MultiPartHandlerSpec extends TestKitBaseClass with FixtureAnyFunSuiteLike
assert(nodeParams.db.payments.getIncomingPayment(paymentHash).isEmpty)
val add = UpdateAddHtlc(ByteVector32.One, 0, amountMsat, paymentHash, defaultExpiry, TestConstants.emptyOnionPacket, None, accountable = false, None)
- sender.send(handlerWithKeySend, IncomingPaymentPacket.FinalPacket(add, payload, TimestampMilli.now()))
+ val receivedAt = TimestampMilli.now()
+ sender.send(handlerWithKeySend, IncomingPaymentPacket.FinalPacket(add, payload, receivedAt))
register.expectMsgType[Register.Forward[CMD_FULFILL_HTLC]]
val paymentReceived = eventListener.expectMsgType[PaymentReceived]
- assert(paymentReceived.copy(parts = paymentReceived.parts.map(_.copy(timestamp = 0 unixms))) == PaymentReceived(add.paymentHash, PartialPayment(amountMsat, add.channelId, timestamp = 0 unixms) :: Nil))
+ assert(paymentReceived == PaymentReceived(add.paymentHash, PartialPayment(amountMsat, add.channelId, receivedAt) :: Nil))
val received = nodeParams.db.payments.getIncomingPayment(paymentHash)
assert(received.isDefined && received.get.status.isInstanceOf[IncomingPaymentStatus.Received])
- assert(received.get.status.asInstanceOf[IncomingPaymentStatus.Received].copy(receivedAt = 0 unixms) == IncomingPaymentStatus.Received(amountMsat, 0 unixms))
+ assert(received.get.status.asInstanceOf[IncomingPaymentStatus.Received] == IncomingPaymentStatus.Received(amountMsat, receivedAt))
}
test("PaymentHandler should reject KeySend payment when feature is disabled") { f =>
diff --git a/eclair-core/src/test/scala/fr/acinq/eclair/payment/MultiPartPaymentLifecycleSpec.scala b/eclair-core/src/test/scala/fr/acinq/eclair/payment/MultiPartPaymentLifecycleSpec.scala
index f19487b..d7b636d 100644
--- a/eclair-core/src/test/scala/fr/acinq/eclair/payment/MultiPartPaymentLifecycleSpec.scala
+++ b/eclair-core/src/test/scala/fr/acinq/eclair/payment/MultiPartPaymentLifecycleSpec.scala
@@ -194,7 +194,7 @@ class MultiPartPaymentLifecycleSpec extends TestKitBaseClass with FixtureAnyFunS
childPayFsm.expectNoMessage(100 millis)
val childId = payFsm.stateData.asInstanceOf[PaymentProgress].pending.keys.head
- childPayFsm.send(payFsm, PaymentFailed(childId, paymentHash, Seq(RemoteFailure(failingRoute.amount, failingRoute.fullRoute, Sphinx.DecryptedFailurePacket(b, 1, PermanentChannelFailure())))))
+ childPayFsm.send(payFsm, PaymentFailed(childId, paymentHash, Seq(RemoteFailure(failingRoute.amount, failingRoute.fullRoute, Sphinx.DecryptedFailurePacket(b, 1, PermanentChannelFailure()), 100 unixms, 200 unixms)), 50 unixms, 200 unixms))
// We retry ignoring the failing channel.
expectRouteRequest(router, nodeParams.nodeId, clearRecipient, routeParams.copy(randomize = true), allowMultiPart = true, ignore = Ignore(Set.empty, Set(ChannelDesc(channelId_be, b, e))), paymentContext = Some(cfg.paymentContext))
router.send(payFsm, RouteResponse(Seq(Route(400_000 msat, hop_ac_1 :: hop_ce :: Nil, None), Route(600_000 msat, hop_ad :: hop_de :: Nil, None))))
@@ -225,12 +225,12 @@ class MultiPartPaymentLifecycleSpec extends TestKitBaseClass with FixtureAnyFunS
childPayFsm.expectNoMessage(100 millis)
val (failedId1, failedRoute1) :: (failedId2, failedRoute2) :: Nil = payFsm.stateData.asInstanceOf[PaymentProgress].pending.toSeq
- childPayFsm.send(payFsm, PaymentFailed(failedId1, paymentHash, Seq(RemoteFailure(failedRoute1.amount, failedRoute1.hops, Sphinx.DecryptedFailurePacket(b, 1, TemporaryNodeFailure())))))
+ childPayFsm.send(payFsm, PaymentFailed(failedId1, paymentHash, Seq(RemoteFailure(failedRoute1.amount, failedRoute1.hops, Sphinx.DecryptedFailurePacket(b, 1, TemporaryNodeFailure()), 500 unixms, 550 unixms)), 500 unixms, 600 unixms))
// When we retry, we ignore the failing node and we let the router know about the remaining pending route.
expectRouteRequest(router, nodeParams.nodeId, clearRecipient, routeParams.copy(randomize = true), Ignore(Set(b), Set.empty), pendingPayments = Seq(failedRoute2), allowMultiPart = true, paymentContext = Some(cfg.paymentContext))
// The second part fails while we're still waiting for new routes.
- childPayFsm.send(payFsm, PaymentFailed(failedId2, paymentHash, Seq(RemoteFailure(failedRoute2.amount, failedRoute2.hops, Sphinx.DecryptedFailurePacket(b, 1, TemporaryNodeFailure())))))
+ childPayFsm.send(payFsm, PaymentFailed(failedId2, paymentHash, Seq(RemoteFailure(failedRoute2.amount, failedRoute2.hops, Sphinx.DecryptedFailurePacket(b, 1, TemporaryNodeFailure()), 300 unixms, 300 unixms)), 100 unixms, 300 unixms))
// We receive a response to our first request, but it's now obsolete: we re-sent a new route request that takes into
// account the latest failures.
router.send(payFsm, RouteResponse(Seq(Route(failedRoute1.amount, hop_ac_1 :: hop_ce :: Nil, None))))
@@ -265,7 +265,7 @@ class MultiPartPaymentLifecycleSpec extends TestKitBaseClass with FixtureAnyFunS
childPayFsm.expectNoMessage(100 millis)
val (failedId, failedRoute) :: Nil = payFsm.stateData.asInstanceOf[PaymentProgress].pending.toSeq
- childPayFsm.send(payFsm, PaymentFailed(failedId, paymentHash, Seq(LocalFailure(failedRoute.amount, failedRoute.fullRoute, RemoteCannotAffordFeesForNewHtlc(randomBytes32(), finalAmount, 15 sat, 0 sat, 15 sat)))))
+ childPayFsm.send(payFsm, PaymentFailed(failedId, paymentHash, Seq(LocalFailure(failedRoute.amount, failedRoute.fullRoute, RemoteCannotAffordFeesForNewHtlc(randomBytes32(), finalAmount, 15 sat, 0 sat, 15 sat))), 0 unixms, 750 unixms))
// We retry without the failing channel.
expectRouteRequest(router,
@@ -290,7 +290,7 @@ class MultiPartPaymentLifecycleSpec extends TestKitBaseClass with FixtureAnyFunS
childPayFsm.expectNoMessage(100 millis)
val (failedId, failedRoute) :: (_, pendingRoute) :: Nil = payFsm.stateData.asInstanceOf[PaymentProgress].pending.toSeq
- childPayFsm.send(payFsm, PaymentFailed(failedId, paymentHash, Seq(LocalFailure(failedRoute.amount, failedRoute.fullRoute, ChannelUnavailable(randomBytes32())))))
+ childPayFsm.send(payFsm, PaymentFailed(failedId, paymentHash, Seq(LocalFailure(failedRoute.amount, failedRoute.fullRoute, ChannelUnavailable(randomBytes32()))), 100 unixms, 250 unixms))
// If the router doesn't find routes, we will retry without ignoring the channel: it may work with a different split
// of the amount to send.
@@ -342,7 +342,7 @@ class MultiPartPaymentLifecycleSpec extends TestKitBaseClass with FixtureAnyFunS
// B changed his fees and expiry after the invoice was issued.
val channelUpdate = channelUpdate_be.copy(feeBaseMsat = 250 msat, feeProportionalMillionths = 150, cltvExpiryDelta = CltvExpiryDelta(24))
val childId = payFsm.stateData.asInstanceOf[PaymentProgress].pending.keys.head
- childPayFsm.send(payFsm, PaymentFailed(childId, paymentHash, Seq(RemoteFailure(route.amount, route.fullRoute, Sphinx.DecryptedFailurePacket(b, 1, FeeInsufficient(finalAmount, Some(channelUpdate)))))))
+ childPayFsm.send(payFsm, PaymentFailed(childId, paymentHash, Seq(RemoteFailure(route.amount, route.fullRoute, Sphinx.DecryptedFailurePacket(b, 1, FeeInsufficient(finalAmount, Some(channelUpdate))), 100 unixms, 250 unixms)), 100 unixms, 250 unixms))
// We update the routing hints accordingly before requesting a new route.
val extraEdge1 = extraEdge.copy(feeBase = 250 msat, feeProportionalMillionths = 150, cltvExpiryDelta = CltvExpiryDelta(24))
assert(router.expectMsgType[RouteRequest].target.extraEdges == Seq(extraEdge1))
@@ -366,7 +366,7 @@ class MultiPartPaymentLifecycleSpec extends TestKitBaseClass with FixtureAnyFunS
// NB: we need a channel update with a valid signature, otherwise we'll ignore the node instead of this specific channel.
val channelUpdate = Announcements.makeChannelUpdate(channelUpdate_be.chainHash, priv_b, e, channelUpdate_be.shortChannelId, channelUpdate_be.cltvExpiryDelta, channelUpdate_be.htlcMinimumMsat, channelUpdate_be.feeBaseMsat, channelUpdate_be.feeProportionalMillionths, channelUpdate_be.htlcMaximumMsat)
val childId = payFsm.stateData.asInstanceOf[PaymentProgress].pending.keys.head
- childPayFsm.send(payFsm, PaymentFailed(childId, paymentHash, Seq(RemoteFailure(route.amount, route.fullRoute, Sphinx.DecryptedFailurePacket(b, 1, TemporaryChannelFailure(Some(channelUpdate)))))))
+ childPayFsm.send(payFsm, PaymentFailed(childId, paymentHash, Seq(RemoteFailure(route.amount, route.fullRoute, Sphinx.DecryptedFailurePacket(b, 1, TemporaryChannelFailure(Some(channelUpdate))), 100 unixms, 250 unixms)), 100 unixms, 250 unixms))
// We update the routing hints accordingly before requesting a new route and ignore the channel.
val routeRequest = router.expectMsgType[RouteRequest]
assert(routeRequest.target.extraEdges == Seq(extraEdge))
@@ -387,7 +387,7 @@ class MultiPartPaymentLifecycleSpec extends TestKitBaseClass with FixtureAnyFunS
// The blinded route fails to relay the payment.
val childId = payFsm.stateData.asInstanceOf[PaymentProgress].pending.keys.head
- childPayFsm.send(payFsm, PaymentFailed(childId, paymentHash, Seq(RemoteFailure(route.amount, route.fullRoute, Sphinx.DecryptedFailurePacket(b, 1, InvalidOnionBlinding(randomBytes32()))))))
+ childPayFsm.send(payFsm, PaymentFailed(childId, paymentHash, Seq(RemoteFailure(route.amount, route.fullRoute, Sphinx.DecryptedFailurePacket(b, 1, InvalidOnionBlinding(randomBytes32())), 100 unixms, 250 unixms)), 100 unixms, 250 unixms))
// We retry and ignore that blinded route.
val routeRequest = router.expectMsgType[RouteRequest]
assert(routeRequest.ignore.channels.map(_.shortChannelId) == Set(hop_be.dummyId))
@@ -407,8 +407,8 @@ class MultiPartPaymentLifecycleSpec extends TestKitBaseClass with FixtureAnyFunS
{
val failures = Seq(
LocalFailure(finalAmount, Nil, ChannelUnavailable(randomBytes32())),
- RemoteFailure(finalAmount, Nil, Sphinx.DecryptedFailurePacket(b, 2, FeeInsufficient(100 msat, Some(makeChannelUpdate(ShortChannelId(2), 15 msat, 150, CltvExpiryDelta(48)))))),
- UnreadableRemoteFailure(finalAmount, Nil, Sphinx.CannotDecryptFailurePacket(randomBytes(292), None), Nil)
+ RemoteFailure(finalAmount, Nil, Sphinx.DecryptedFailurePacket(b, 2, FeeInsufficient(100 msat, Some(makeChannelUpdate(ShortChannelId(2), 15 msat, 150, CltvExpiryDelta(48))))), 100 unixms, 250 unixms),
+ UnreadableRemoteFailure(finalAmount, Nil, Sphinx.CannotDecryptFailurePacket(randomBytes(292), None), 100 unixms, 250 unixms, Nil)
)
val extraEdges1 = Seq(
ExtraEdge(a, b, ShortChannelId(1), 10 msat, 0, CltvExpiryDelta(12), 1 msat, None),
@@ -419,10 +419,10 @@ class MultiPartPaymentLifecycleSpec extends TestKitBaseClass with FixtureAnyFunS
}
{
val failures = Seq(
- RemoteFailure(finalAmount, Nil, Sphinx.DecryptedFailurePacket(a, 1, FeeInsufficient(100 msat, Some(makeChannelUpdate(ShortChannelId(1), 20 msat, 20, CltvExpiryDelta(20)))))),
- RemoteFailure(finalAmount, Nil, Sphinx.DecryptedFailurePacket(b, 2, FeeInsufficient(100 msat, Some(makeChannelUpdate(ShortChannelId(2), 21 msat, 21, CltvExpiryDelta(21)))))),
- RemoteFailure(finalAmount, Nil, Sphinx.DecryptedFailurePacket(a, 1, FeeInsufficient(100 msat, Some(makeChannelUpdate(ShortChannelId(3), 22 msat, 22, CltvExpiryDelta(22)))))),
- RemoteFailure(finalAmount, Nil, Sphinx.DecryptedFailurePacket(a, 1, FeeInsufficient(100 msat, Some(makeChannelUpdate(ShortChannelId(1), 23 msat, 23, CltvExpiryDelta(23)))))),
+ RemoteFailure(finalAmount, Nil, Sphinx.DecryptedFailurePacket(a, 1, FeeInsufficient(100 msat, Some(makeChannelUpdate(ShortChannelId(1), 20 msat, 20, CltvExpiryDelta(20))))), 100 unixms, 250 unixms),
+ RemoteFailure(finalAmount, Nil, Sphinx.DecryptedFailurePacket(b, 2, FeeInsufficient(100 msat, Some(makeChannelUpdate(ShortChannelId(2), 21 msat, 21, CltvExpiryDelta(21))))), 100 unixms, 250 unixms),
+ RemoteFailure(finalAmount, Nil, Sphinx.DecryptedFailurePacket(a, 1, FeeInsufficient(100 msat, Some(makeChannelUpdate(ShortChannelId(3), 22 msat, 22, CltvExpiryDelta(22))))), 100 unixms, 250 unixms),
+ RemoteFailure(finalAmount, Nil, Sphinx.DecryptedFailurePacket(a, 1, FeeInsufficient(100 msat, Some(makeChannelUpdate(ShortChannelId(1), 23 msat, 23, CltvExpiryDelta(23))))), 100 unixms, 250 unixms),
)
val extraEdges1 = Seq(
ExtraEdge(a, b, ShortChannelId(1), 23 msat, 23, CltvExpiryDelta(23), defaultChannelUpdate.htlcMinimumMsat, Some(defaultChannelUpdate.htlcMaximumMsat)),
@@ -444,14 +444,14 @@ class MultiPartPaymentLifecycleSpec extends TestKitBaseClass with FixtureAnyFunS
childPayFsm.expectMsgType[SendPaymentToRoute]
val (failedId1, failedRoute1) = payFsm.stateData.asInstanceOf[PaymentProgress].pending.head
- childPayFsm.send(payFsm, PaymentFailed(failedId1, paymentHash, Seq(UnreadableRemoteFailure(failedRoute1.amount, failedRoute1.hops, Sphinx.CannotDecryptFailurePacket(randomBytes(292), None), Nil))))
+ childPayFsm.send(payFsm, PaymentFailed(failedId1, paymentHash, Seq(UnreadableRemoteFailure(failedRoute1.amount, failedRoute1.hops, Sphinx.CannotDecryptFailurePacket(randomBytes(292), None), 100 unixms, 250 unixms, Nil)), 100 unixms, 250 unixms))
router.expectMsgType[RouteRequest]
router.send(payFsm, RouteResponse(Seq(Route(500_000 msat, hop_ad :: hop_de :: Nil, None))))
childPayFsm.expectMsgType[SendPaymentToRoute]
assert(!payFsm.stateData.asInstanceOf[PaymentProgress].pending.contains(failedId1))
val (failedId2, failedRoute2) = payFsm.stateData.asInstanceOf[PaymentProgress].pending.head
- val result = abortAfterFailure(f, PaymentFailed(failedId2, paymentHash, Seq(UnreadableRemoteFailure(failedRoute2.amount, failedRoute2.hops, Sphinx.CannotDecryptFailurePacket(randomBytes(292), None), Nil))))
+ val result = abortAfterFailure(f, PaymentFailed(failedId2, paymentHash, Seq(UnreadableRemoteFailure(failedRoute2.amount, failedRoute2.hops, Sphinx.CannotDecryptFailurePacket(randomBytes(292), None), 100 unixms, 250 unixms, Nil)), 100 unixms, 250 unixms))
assert(result.failures.length >= 3)
assert(result.failures.contains(LocalFailure(finalAmount, Nil, RetryExhausted)))
@@ -503,7 +503,7 @@ class MultiPartPaymentLifecycleSpec extends TestKitBaseClass with FixtureAnyFunS
childPayFsm.expectMsgType[SendPaymentToRoute]
val (failedId, failedRoute) = payFsm.stateData.asInstanceOf[PaymentProgress].pending.head
- val result = abortAfterFailure(f, PaymentFailed(failedId, paymentHash, Seq(RemoteFailure(failedRoute.amount, failedRoute.fullRoute, Sphinx.DecryptedFailurePacket(e, 2, IncorrectOrUnknownPaymentDetails(600_000 msat, BlockHeight(0)))))))
+ val result = abortAfterFailure(f, PaymentFailed(failedId, paymentHash, Seq(RemoteFailure(failedRoute.amount, failedRoute.fullRoute, Sphinx.DecryptedFailurePacket(e, 2, IncorrectOrUnknownPaymentDetails(600_000 msat, BlockHeight(0))), 100 unixms, 250 unixms)), 100 unixms, 250 unixms))
assert(result.failures.length == 1)
val metrics = metricsListener.expectMsgType[PathFindingExperimentMetrics]
@@ -524,7 +524,7 @@ class MultiPartPaymentLifecycleSpec extends TestKitBaseClass with FixtureAnyFunS
childPayFsm.expectMsgType[SendPaymentToRoute]
val (failedId, failedRoute) = payFsm.stateData.asInstanceOf[PaymentProgress].pending.head
- val result = abortAfterFailure(f, PaymentFailed(failedId, paymentHash, Seq(LocalFailure(failedRoute.amount, failedRoute.fullRoute, HtlcsTimedoutDownstream(channelId = ByteVector32.One, htlcs = Set.empty)))))
+ val result = abortAfterFailure(f, PaymentFailed(failedId, paymentHash, Seq(LocalFailure(failedRoute.amount, failedRoute.fullRoute, HtlcsTimedoutDownstream(channelId = ByteVector32.One, htlcs = Set.empty))), 100 unixms, 250 unixms))
assert(result.failures.length == 1)
}
@@ -539,10 +539,10 @@ class MultiPartPaymentLifecycleSpec extends TestKitBaseClass with FixtureAnyFunS
childPayFsm.expectMsgType[SendPaymentToRoute]
val (failedId1, failedRoute1) :: (failedId2, failedRoute2) :: Nil = payFsm.stateData.asInstanceOf[PaymentProgress].pending.toSeq
- childPayFsm.send(payFsm, PaymentFailed(failedId1, paymentHash, Seq(UnreadableRemoteFailure(failedRoute1.amount, failedRoute1.hops, Sphinx.CannotDecryptFailurePacket(randomBytes(292), None), Nil))))
+ childPayFsm.send(payFsm, PaymentFailed(failedId1, paymentHash, Seq(UnreadableRemoteFailure(failedRoute1.amount, failedRoute1.hops, Sphinx.CannotDecryptFailurePacket(randomBytes(292), None), 100 unixms, 250 unixms, Nil)), 100 unixms, 250 unixms))
router.expectMsgType[RouteRequest]
- val result = abortAfterFailure(f, PaymentFailed(failedId2, paymentHash, Seq(RemoteFailure(failedRoute2.amount, failedRoute2.hops, Sphinx.DecryptedFailurePacket(e, 2, PaymentTimeout())))))
+ val result = abortAfterFailure(f, PaymentFailed(failedId2, paymentHash, Seq(RemoteFailure(failedRoute2.amount, failedRoute2.hops, Sphinx.DecryptedFailurePacket(e, 2, PaymentTimeout()), 100 unixms, 250 unixms)), 100 unixms, 250 unixms))
assert(result.failures.length == 2)
}
@@ -556,8 +556,8 @@ class MultiPartPaymentLifecycleSpec extends TestKitBaseClass with FixtureAnyFunS
childPayFsm.expectMsgType[SendPaymentToRoute]
childPayFsm.expectMsgType[SendPaymentToRoute]
- val (failedId, failedRoute) :: (successId, successRoute) :: Nil = payFsm.stateData.asInstanceOf[PaymentProgress].pending.toSeq
- childPayFsm.send(payFsm, PaymentFailed(failedId, paymentHash, Seq(UnreadableRemoteFailure(failedRoute.amount, failedRoute.fullRoute, Sphinx.CannotDecryptFailurePacket(randomBytes(292), None), Nil))))
+ val (failedId, failedRoute) :: (successId, _) :: Nil = payFsm.stateData.asInstanceOf[PaymentProgress].pending.toSeq
+ childPayFsm.send(payFsm, PaymentFailed(failedId, paymentHash, Seq(UnreadableRemoteFailure(failedRoute.amount, failedRoute.fullRoute, Sphinx.CannotDecryptFailurePacket(randomBytes(292), None), 100 unixms, 250 unixms, Nil)), 100 unixms, 250 unixms))
router.expectMsgType[RouteRequest]
val result = fulfillPendingPayments(f, 1, e, finalAmount)
@@ -576,11 +576,11 @@ class MultiPartPaymentLifecycleSpec extends TestKitBaseClass with FixtureAnyFunS
childPayFsm.expectMsgType[SendPaymentToRoute]
val (failedId, failedRoute) :: (successId, successRoute) :: Nil = payFsm.stateData.asInstanceOf[PaymentProgress].pending.toSeq
- childPayFsm.send(payFsm, PaymentFailed(failedId, paymentHash, Seq(RemoteFailure(failedRoute.amount, failedRoute.fullRoute, Sphinx.DecryptedFailurePacket(e, 2, PaymentTimeout())))))
+ childPayFsm.send(payFsm, PaymentFailed(failedId, paymentHash, Seq(RemoteFailure(failedRoute.amount, failedRoute.fullRoute, Sphinx.DecryptedFailurePacket(e, 2, PaymentTimeout()), 100 unixms, 250 unixms)), 100 unixms, 250 unixms))
awaitCond(payFsm.stateName == PAYMENT_ABORTED)
sender.watch(payFsm)
- childPayFsm.send(payFsm, PaymentSent(cfg.id, paymentHash, paymentPreimage, finalAmount, e, Seq(PaymentSent.PartialPayment(successId, successRoute.amount, successRoute.channelFee(false), randomBytes32(), Some(successRoute.fullRoute))), None))
+ childPayFsm.send(payFsm, PaymentSent(cfg.id, paymentPreimage, finalAmount, e, Seq(PaymentSent.PartialPayment(successId, successRoute.amount, successRoute.channelFee(false), randomBytes32(), Some(successRoute.fullRoute), 100 unixms, 250 unixms)), None, 75 unixms))
sender.expectMsg(PreimageReceived(paymentHash, paymentPreimage, None))
val result = sender.expectMsgType[PaymentSent]
assert(result.id == cfg.id)
@@ -608,12 +608,12 @@ class MultiPartPaymentLifecycleSpec extends TestKitBaseClass with FixtureAnyFunS
childPayFsm.expectMsgType[SendPaymentToRoute]
val (childId, route) :: (failedId, failedRoute) :: Nil = payFsm.stateData.asInstanceOf[PaymentProgress].pending.toSeq
- childPayFsm.send(payFsm, PaymentSent(cfg.id, paymentHash, paymentPreimage, finalAmount, e, Seq(PaymentSent.PartialPayment(childId, route.amount, route.channelFee(false), randomBytes32(), Some(route.fullRoute))), None))
+ childPayFsm.send(payFsm, PaymentSent(cfg.id, paymentPreimage, finalAmount, e, Seq(PaymentSent.PartialPayment(childId, route.amount, route.channelFee(false), randomBytes32(), Some(route.fullRoute), 100 unixms, 250 unixms)), None, 75 unixms))
sender.expectMsg(PreimageReceived(paymentHash, paymentPreimage, None))
awaitCond(payFsm.stateName == PAYMENT_SUCCEEDED)
sender.watch(payFsm)
- childPayFsm.send(payFsm, PaymentFailed(failedId, paymentHash, Seq(RemoteFailure(failedRoute.amount, failedRoute.fullRoute, Sphinx.DecryptedFailurePacket(e, 2, PaymentTimeout())))))
+ childPayFsm.send(payFsm, PaymentFailed(failedId, paymentHash, Seq(RemoteFailure(failedRoute.amount, failedRoute.fullRoute, Sphinx.DecryptedFailurePacket(e, 2, PaymentTimeout()), 100 unixms, 250 unixms)), 100 unixms, 250 unixms))
val result = sender.expectMsgType[PaymentSent]
assert(result.parts.length == 1 && result.parts.head.id == childId)
assert(result.amountWithFees < finalAmount) // we got the preimage without paying the full amount
@@ -632,9 +632,9 @@ class MultiPartPaymentLifecycleSpec extends TestKitBaseClass with FixtureAnyFunS
assert(pending.size == childCount)
val partialPayments = pending.map {
- case (childId, route) => PaymentSent.PartialPayment(childId, route.amount, route.channelFee(false) + route.blindedFee, randomBytes32(), Some(route.fullRoute))
+ case (childId, route) => PaymentSent.PartialPayment(childId, route.amount, route.channelFee(false) + route.blindedFee, randomBytes32(), Some(route.fullRoute), 100 unixms, 250 unixms)
}
- partialPayments.foreach(pp => childPayFsm.send(payFsm, PaymentSent(cfg.id, paymentHash, paymentPreimage, finalAmount, e, Seq(pp), None)))
+ partialPayments.foreach(pp => childPayFsm.send(payFsm, PaymentSent(cfg.id, paymentPreimage, finalAmount, e, Seq(pp), None, 100 unixms)))
sender.expectMsg(PreimageReceived(paymentHash, paymentPreimage, None))
val result = sender.expectMsgType[PaymentSent]
assert(result.id == cfg.id)
@@ -663,7 +663,7 @@ class MultiPartPaymentLifecycleSpec extends TestKitBaseClass with FixtureAnyFunS
assert(payFsm.stateData.asInstanceOf[PaymentAborted].pending.size == pending.size - 1)
// Fail all remaining child payments.
payFsm.stateData.asInstanceOf[PaymentAborted].pending.foreach(childId =>
- childPayFsm.send(payFsm, PaymentFailed(childId, paymentHash, Seq(RemoteFailure(pending(childId).amount, hop_ab_1 :: hop_be :: Nil, Sphinx.DecryptedFailurePacket(e, 2, PaymentTimeout())))))
+ childPayFsm.send(payFsm, PaymentFailed(childId, paymentHash, Seq(RemoteFailure(pending(childId).amount, hop_ab_1 :: hop_be :: Nil, Sphinx.DecryptedFailurePacket(e, 2, PaymentTimeout()), 100 unixms, 250 unixms)), 100 unixms, 250 unixms))
)
}
@@ -684,12 +684,12 @@ class MultiPartPaymentLifecycleSpec extends TestKitBaseClass with FixtureAnyFunS
object MultiPartPaymentLifecycleSpec {
- val paymentPreimage = randomBytes32()
- val paymentHash = Crypto.sha256(paymentPreimage)
- val expiry = CltvExpiry(1105)
- val blindedRouteExpiry = CltvExpiry(500_000)
- val finalAmount = 1_000_000 msat
- val routeParams = PathFindingConf(
+ val paymentPreimage: ByteVector32 = randomBytes32()
+ val paymentHash: ByteVector32 = Crypto.sha256(paymentPreimage)
+ val expiry: CltvExpiry = CltvExpiry(1105)
+ val blindedRouteExpiry: CltvExpiry = CltvExpiry(500_000)
+ val finalAmount: MilliSatoshi = 1_000_000 msat
+ val routeParams: RouteParams = PathFindingConf(
randomize = false,
boundaries = SearchBoundaries(
15_000 msat,
@@ -723,25 +723,25 @@ object MultiPartPaymentLifecycleSpec {
val channelId_ad = ShortChannelId(21)
val channelId_de = ShortChannelId(22)
val defaultChannelUpdate = ChannelUpdate(randomBytes64(), Block.RegtestGenesisBlock.hash, ShortChannelId(0), 0 unixsec, ChannelUpdate.MessageFlags(dontForward = false), ChannelUpdate.ChannelFlags.DUMMY, CltvExpiryDelta(12), 1 msat, 100 msat, 0, 2_000_000 msat)
- val channelUpdate_ab_1 = defaultChannelUpdate.copy(shortChannelId = channelId_ab_1)
- val channelUpdate_ab_2 = defaultChannelUpdate.copy(shortChannelId = channelId_ab_2)
- val channelUpdate_be = defaultChannelUpdate.copy(shortChannelId = channelId_be)
- val channelUpdate_ac_1 = defaultChannelUpdate.copy(shortChannelId = channelId_ac_1)
- val channelUpdate_ac_2 = defaultChannelUpdate.copy(shortChannelId = channelId_ac_2)
- val channelUpdate_ce = defaultChannelUpdate.copy(shortChannelId = channelId_ce)
- val channelUpdate_ad = defaultChannelUpdate.copy(shortChannelId = channelId_ad)
- val channelUpdate_de = defaultChannelUpdate.copy(shortChannelId = channelId_de)
-
- val hop_ab_1 = channelHopFromUpdate(a, b, channelUpdate_ab_1)
- val hop_ab_2 = channelHopFromUpdate(a, b, channelUpdate_ab_2)
- val hop_be = channelHopFromUpdate(b, e, channelUpdate_be)
- val hop_ac_1 = channelHopFromUpdate(a, c, channelUpdate_ac_1)
- val hop_ac_2 = channelHopFromUpdate(a, c, channelUpdate_ac_2)
- val hop_ce = channelHopFromUpdate(c, e, channelUpdate_ce)
- val hop_ad = channelHopFromUpdate(a, d, channelUpdate_ad)
- val hop_de = channelHopFromUpdate(d, e, channelUpdate_de)
-
- val recipientFeatures = Features(
+ val channelUpdate_ab_1: ChannelUpdate = defaultChannelUpdate.copy(shortChannelId = channelId_ab_1)
+ val channelUpdate_ab_2: ChannelUpdate = defaultChannelUpdate.copy(shortChannelId = channelId_ab_2)
+ val channelUpdate_be: ChannelUpdate = defaultChannelUpdate.copy(shortChannelId = channelId_be)
+ val channelUpdate_ac_1: ChannelUpdate = defaultChannelUpdate.copy(shortChannelId = channelId_ac_1)
+ val channelUpdate_ac_2: ChannelUpdate = defaultChannelUpdate.copy(shortChannelId = channelId_ac_2)
+ val channelUpdate_ce: ChannelUpdate = defaultChannelUpdate.copy(shortChannelId = channelId_ce)
+ val channelUpdate_ad: ChannelUpdate = defaultChannelUpdate.copy(shortChannelId = channelId_ad)
+ val channelUpdate_de: ChannelUpdate = defaultChannelUpdate.copy(shortChannelId = channelId_de)
+
+ val hop_ab_1: ChannelHop = channelHopFromUpdate(a, b, channelUpdate_ab_1)
+ val hop_ab_2: ChannelHop = channelHopFromUpdate(a, b, channelUpdate_ab_2)
+ val hop_be: ChannelHop = channelHopFromUpdate(b, e, channelUpdate_be)
+ val hop_ac_1: ChannelHop = channelHopFromUpdate(a, c, channelUpdate_ac_1)
+ val hop_ac_2: ChannelHop = channelHopFromUpdate(a, c, channelUpdate_ac_2)
+ val hop_ce: ChannelHop = channelHopFromUpdate(c, e, channelUpdate_ce)
+ val hop_ad: ChannelHop = channelHopFromUpdate(a, d, channelUpdate_ad)
+ val hop_de: ChannelHop = channelHopFromUpdate(d, e, channelUpdate_de)
+
+ val recipientFeatures: Features[InvoiceFeature] = Features(
Features.VariableLengthOnion -> FeatureSupport.Mandatory,
Features.PaymentSecret -> FeatureSupport.Mandatory,
Features.BasicMultiPartPayment -> FeatureSupport.Optional,
diff --git a/eclair-core/src/test/scala/fr/acinq/eclair/payment/PaymentInitiatorSpec.scala b/eclair-core/src/test/scala/fr/acinq/eclair/payment/PaymentInitiatorSpec.scala
index 93cddfa..4e43828 100644
--- a/eclair-core/src/test/scala/fr/acinq/eclair/payment/PaymentInitiatorSpec.scala
+++ b/eclair-core/src/test/scala/fr/acinq/eclair/payment/PaymentInitiatorSpec.scala
@@ -38,7 +38,7 @@ import fr.acinq.eclair.router.BlindedRouteCreation
import fr.acinq.eclair.router.Router._
import fr.acinq.eclair.wire.protocol.OfferTypes.{InvoiceRequest, Offer}
import fr.acinq.eclair.wire.protocol._
-import fr.acinq.eclair.{Bolt11Feature, Bolt12Feature, CltvExpiry, CltvExpiryDelta, EncodedNodeId, Feature, Features, MilliSatoshiLong, NodeParams, PaymentFinalExpiryConf, TestConstants, TestKitBaseClass, TimestampSecond, UnknownFeature, randomBytes32, randomKey}
+import fr.acinq.eclair.{Bolt11Feature, Bolt12Feature, CltvExpiry, CltvExpiryDelta, EncodedNodeId, Feature, Features, MilliSatoshiLong, NodeParams, PaymentFinalExpiryConf, TestConstants, TestKitBaseClass, TimestampMilliLong, TimestampSecond, UnknownFeature, randomBytes32, randomKey}
import org.scalatest.funsuite.FixtureAnyFunSuiteLike
import org.scalatest.{Outcome, Tag}
import scodec.bits.{ByteVector, HexStringSyntax}
@@ -183,7 +183,7 @@ class PaymentInitiatorSpec extends TestKitBaseClass with FixtureAnyFunSuiteLike
sender.send(initiator, GetPayment(PaymentIdentifier.PaymentHash(invoice.paymentHash)))
sender.expectMsg(PaymentIsPending(payment.paymentId, invoice.paymentHash, PendingPaymentToRoute(sender.ref, request)))
- val pf = PaymentFailed(payment.paymentId, invoice.paymentHash, Nil)
+ val pf = PaymentFailed(payment.paymentId, invoice.paymentHash, Nil, 100 unixms, 200 unixms)
payFsm.send(initiator, pf)
sender.expectMsg(pf)
eventListener.expectNoMessage(100 millis)
@@ -208,7 +208,7 @@ class PaymentInitiatorSpec extends TestKitBaseClass with FixtureAnyFunSuiteLike
sender.send(initiator, GetPayment(PaymentIdentifier.PaymentHash(invoice.paymentHash)))
sender.expectMsg(PaymentIsPending(id, invoice.paymentHash, PendingPaymentToNode(sender.ref, req)))
- val pf = PaymentFailed(id, invoice.paymentHash, Nil)
+ val pf = PaymentFailed(id, invoice.paymentHash, Nil, 0 unixms, 150 unixms)
payFsm.send(initiator, pf)
sender.expectMsg(pf)
eventListener.expectNoMessage(100 millis)
@@ -231,7 +231,7 @@ class PaymentInitiatorSpec extends TestKitBaseClass with FixtureAnyFunSuiteLike
sender.send(initiator, GetPayment(PaymentIdentifier.PaymentHash(invoice.paymentHash)))
sender.expectMsg(PaymentIsPending(id, invoice.paymentHash, PendingPaymentToNode(sender.ref, req)))
- val ps = PaymentSent(id, invoice.paymentHash, randomBytes32(), finalAmount, priv_c.publicKey, Seq(PartialPayment(UUID.randomUUID(), finalAmount, 0 msat, randomBytes32(), None)), None)
+ val ps = PaymentSent(id, paymentPreimage, finalAmount, priv_c.publicKey, Seq(PartialPayment(UUID.randomUUID(), finalAmount, 0 msat, randomBytes32(), None, 100 unixms, 200 unixms)), None, 80 unixms)
payFsm.send(initiator, ps)
sender.expectMsg(ps)
eventListener.expectNoMessage(100 millis)
@@ -275,7 +275,7 @@ class PaymentInitiatorSpec extends TestKitBaseClass with FixtureAnyFunSuiteLike
sender.send(initiator, GetPayment(PaymentIdentifier.PaymentHash(invoice.paymentHash)))
sender.expectMsg(PaymentIsPending(payment.paymentId, invoice.paymentHash, PendingPaymentToRoute(sender.ref, req)))
- val pf = PaymentFailed(payment.paymentId, invoice.paymentHash, Nil)
+ val pf = PaymentFailed(payment.paymentId, invoice.paymentHash, Nil, 100 unixms, 200 unixms)
payFsm.send(initiator, pf)
sender.expectMsg(pf)
eventListener.expectNoMessage(100 millis)
@@ -317,7 +317,7 @@ class PaymentInitiatorSpec extends TestKitBaseClass with FixtureAnyFunSuiteLike
sender.send(initiator, GetPayment(PaymentIdentifier.PaymentHash(invoice.paymentHash)))
sender.expectMsg(PaymentIsPending(id, invoice.paymentHash, PendingPaymentToNode(sender.ref, req)))
- val pf = PaymentFailed(id, invoice.paymentHash, Nil)
+ val pf = PaymentFailed(id, invoice.paymentHash, Nil, 100 unixms, 200 unixms)
payFsm.send(initiator, pf)
sender.expectMsg(pf)
eventListener.expectNoMessage(100 millis)
@@ -350,7 +350,7 @@ class PaymentInitiatorSpec extends TestKitBaseClass with FixtureAnyFunSuiteLike
sender.send(initiator, GetPayment(PaymentIdentifier.PaymentHash(invoice.paymentHash)))
sender.expectMsg(PaymentIsPending(id, invoice.paymentHash, PendingPaymentToNode(sender.ref, req)))
- val ps = PaymentSent(id, invoice.paymentHash, paymentPreimage, finalAmount, invoice.nodeId, Seq(PartialPayment(UUID.randomUUID(), finalAmount, 0 msat, randomBytes32(), None)), None)
+ val ps = PaymentSent(id, paymentPreimage, finalAmount, invoice.nodeId, Seq(PartialPayment(UUID.randomUUID(), finalAmount, 0 msat, randomBytes32(), None, 100 unixms, 200 unixms)), None, 100 unixms)
payFsm.send(initiator, ps)
sender.expectMsg(ps)
eventListener.expectNoMessage(100 millis)
diff --git a/eclair-core/src/test/scala/fr/acinq/eclair/payment/PaymentLifecycleSpec.scala b/eclair-core/src/test/scala/fr/acinq/eclair/payment/PaymentLifecycleSpec.scala
index 9afcae0..c3003ea 100644
--- a/eclair-core/src/test/scala/fr/acinq/eclair/payment/PaymentLifecycleSpec.scala
+++ b/eclair-core/src/test/scala/fr/acinq/eclair/payment/PaymentLifecycleSpec.scala
@@ -59,16 +59,16 @@ import scala.concurrent.duration._
class PaymentLifecycleSpec extends BaseRouterSpec {
- val defaultAmountMsat = 142_000_000 msat
- val defaultExpiry = Channel.MIN_CLTV_EXPIRY_DELTA.toCltvExpiry(BlockHeight(40_000))
- val defaultRouteExpiry = CltvExpiry(100_000)
- val defaultPaymentPreimage = randomBytes32()
- val defaultPaymentHash = Crypto.sha256(defaultPaymentPreimage)
- 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)
- val defaultRouteParams = TestConstants.Alice.nodeParams.routerConf.pathFindingExperimentConf.getRandomConf().getDefaultRouteParams
+ val defaultAmountMsat: MilliSatoshi = 142_000_000 msat
+ val defaultExpiry: CltvExpiry = Channel.MIN_CLTV_EXPIRY_DELTA.toCltvExpiry(BlockHeight(40_000))
+ val defaultRouteExpiry: CltvExpiry = CltvExpiry(100_000)
+ val defaultPaymentPreimage: ByteVector32 = randomBytes32()
+ val defaultPaymentHash: ByteVector32 = Crypto.sha256(defaultPaymentPreimage)
+ val defaultOrigin: Origin = Origin.Cold(Upstream.Local(UUID.randomUUID()))
+ val defaultExternalId: String = UUID.randomUUID().toString
+ val defaultInvoice: Bolt11Invoice = Bolt11Invoice(Block.RegtestGenesisBlock.hash, None, defaultPaymentHash, priv_d, Left("test"), Channel.MIN_CLTV_EXPIRY_DELTA)
+ val defaultRecipient: Recipient = ClearRecipient(defaultInvoice, defaultAmountMsat, defaultExpiry, Set.empty)
+ val defaultRouteParams: RouteParams = TestConstants.Alice.nodeParams.routerConf.pathFindingExperimentConf.getRandomConf().getDefaultRouteParams
def expectRouteRequest(probe: TestProbe, source: PublicKey, cfg: SendPaymentConfig, target: Recipient = defaultRecipient, ignore: Ignore = Ignore.empty): Unit = {
val request = probe.expectMsgType[RouteRequest]
@@ -106,7 +106,7 @@ class PaymentLifecycleSpec extends BaseRouterSpec {
PaymentFixture(cfg, nodeParams, paymentFSM, routerForwarder, register, sender, monitor, eventListener, metricsListener)
}
- def addCompleted(result: HtlcResult) = {
+ def addCompleted(result: HtlcResult): RES_ADD_SETTLED[Origin, HtlcResult] = {
RES_ADD_SETTLED(
origin = defaultOrigin,
htlc = UpdateAddHtlc(ByteVector32.Zeroes, 0, defaultAmountMsat, defaultPaymentHash, defaultExpiry, TestConstants.emptyOnionPacket, None, accountable = false, None),
@@ -357,7 +357,7 @@ class PaymentLifecycleSpec extends BaseRouterSpec {
val WaitingForRoute(_, Nil, _) = paymentFSM.stateData
routerForwarder.forward(routerFixture.router)
awaitCond(paymentFSM.stateName == WAITING_FOR_PAYMENT_COMPLETE)
- val WaitingForComplete(_, cmd1, Nil, _, ignore1, route) = paymentFSM.stateData
+ val WaitingForComplete(_, cmd1, Nil, _, ignore1, route, _) = paymentFSM.stateData
assert(ignore1.nodes.isEmpty)
register.expectMsg(ForwardShortId(paymentFSM.toTyped, scid_ab, cmd1))
@@ -369,7 +369,7 @@ class PaymentLifecycleSpec extends BaseRouterSpec {
// let's simulate a response by the router with another route
sender.send(paymentFSM, RouteResponse(route :: Nil))
awaitCond(paymentFSM.stateName == WAITING_FOR_PAYMENT_COMPLETE)
- val WaitingForComplete(_, cmd2, _, _, ignore2, _) = paymentFSM.stateData
+ val WaitingForComplete(_, cmd2, _, _, ignore2, _, _) = paymentFSM.stateData
assert(ignore2.nodes == Set(c))
// and reply a 2nd time with an unparsable failure
register.expectMsg(ForwardShortId(paymentFSM.toTyped, scid_ab, cmd2))
@@ -408,7 +408,7 @@ class PaymentLifecycleSpec extends BaseRouterSpec {
routerForwarder.forward(routerFixture.router)
awaitCond(paymentFSM.stateName == WAITING_FOR_PAYMENT_COMPLETE)
- val WaitingForComplete(_, cmd1, Nil, _, _, _) = paymentFSM.stateData
+ val WaitingForComplete(_, cmd1, Nil, _, _, _, _) = paymentFSM.stateData
register.expectMsg(ForwardShortId(paymentFSM.toTyped, scid_ab, cmd1))
sender.send(paymentFSM, RES_ADD_FAILED(cmd1, ChannelUnavailable(ByteVector32.Zeroes), None))
@@ -450,7 +450,7 @@ class PaymentLifecycleSpec extends BaseRouterSpec {
expectRouteRequest(routerForwarder, a, cfg)
routerForwarder.forward(routerFixture.router)
awaitCond(paymentFSM.stateName == WAITING_FOR_PAYMENT_COMPLETE)
- val WaitingForComplete(_, cmd1, Nil, _, _, _) = paymentFSM.stateData
+ val WaitingForComplete(_, cmd1, Nil, _, _, _, _) = paymentFSM.stateData
register.expectMsg(ForwardShortId(paymentFSM.toTyped, scid_ab, cmd1))
sender.send(paymentFSM, addCompleted(HtlcResult.RemoteFailMalformed(UpdateFailMalformedHtlc(ByteVector32.Zeroes, 0, randomBytes32(), FailureMessageCodecs.BADONION))))
@@ -473,7 +473,7 @@ class PaymentLifecycleSpec extends BaseRouterSpec {
expectRouteRequest(routerForwarder, a, cfg)
routerForwarder.forward(routerFixture.router)
awaitCond(paymentFSM.stateName == WAITING_FOR_PAYMENT_COMPLETE)
- val WaitingForComplete(_, cmd1, Nil, _, _, _) = paymentFSM.stateData
+ val WaitingForComplete(_, cmd1, Nil, _, _, _, _) = paymentFSM.stateData
register.expectMsg(ForwardShortId(paymentFSM.toTyped, scid_ab, cmd1))
sender.send(paymentFSM, addCompleted(HtlcResult.OnChainFail(HtlcsTimedoutDownstream(randomBytes32(), Set.empty))))
@@ -496,7 +496,7 @@ class PaymentLifecycleSpec extends BaseRouterSpec {
expectRouteRequest(routerForwarder, a, cfg)
routerForwarder.forward(routerFixture.router)
awaitCond(paymentFSM.stateName == WAITING_FOR_PAYMENT_COMPLETE)
- val WaitingForComplete(_, cmd1, Nil, _, _, _) = paymentFSM.stateData
+ val WaitingForComplete(_, cmd1, Nil, _, _, _, _) = paymentFSM.stateData
register.expectMsg(ForwardShortId(paymentFSM.toTyped, scid_ab, cmd1))
val update_bc_disabled = update_bc.copy(channelFlags = ChannelUpdate.ChannelFlags(isNode1 = true, isEnabled = false))
@@ -518,8 +518,9 @@ class PaymentLifecycleSpec extends BaseRouterSpec {
expectRouteRequest(routerForwarder, a, cfg)
routerForwarder.forward(routerFixture.router)
awaitCond(paymentFSM.stateName == WAITING_FOR_PAYMENT_COMPLETE)
- val WaitingForComplete(_, cmd1, Nil, sharedSecrets1, _, route) = paymentFSM.stateData
+ val WaitingForComplete(_, cmd1, Nil, sharedSecrets1, _, route, sentAt) = paymentFSM.stateData
+ val failedAt = TimestampMilli.now()
register.expectMsg(ForwardShortId(paymentFSM.toTyped, scid_ab, cmd1))
val failure = TemporaryChannelFailure(Some(update_bc))
sender.send(paymentFSM, addCompleted(HtlcResult.RemoteFail(UpdateFailHtlc(ByteVector32.Zeroes, 0, SphinxSpec.createAndWrap(sharedSecrets1.head.secret, failure)))))
@@ -532,7 +533,13 @@ class PaymentLifecycleSpec extends BaseRouterSpec {
expectRouteRequest(routerForwarder, a, cfg, ignore = Ignore(Set.empty, Set(ChannelDesc(update_bc.shortChannelId, b, c))))
routerForwarder.forward(routerFixture.router)
// we allow 2 tries, so we send a 2nd request to the router
- assert(sender.expectMsgType[PaymentFailed].failures == RemoteFailure(route.amount, route.hops, Sphinx.DecryptedFailurePacket(b, 1, failure)) :: LocalFailure(defaultAmountMsat, Nil, RouteNotFound) :: Nil)
+ val pf = sender.expectMsgType[PaymentFailed]
+ assert(pf.failures.size == 2)
+ assert(pf.failures.head.isInstanceOf[RemoteFailure])
+ assert(pf.failures.head.asInstanceOf[RemoteFailure].failedAt >= failedAt)
+ assert(pf.failures.head.asInstanceOf[RemoteFailure].copy(failedAt = failedAt) == RemoteFailure(route.amount, route.hops, Sphinx.DecryptedFailurePacket(b, 1, failure), sentAt, failedAt))
+ assert(pf.failures.last == LocalFailure(defaultAmountMsat, Nil, RouteNotFound))
+ assert(pf.settledAt >= failedAt)
}
test("payment failed (Update)") { routerFixture =>
@@ -548,12 +555,13 @@ class PaymentLifecycleSpec extends BaseRouterSpec {
expectRouteRequest(routerForwarder, a, cfg)
routerForwarder.forward(routerFixture.router)
awaitCond(paymentFSM.stateName == WAITING_FOR_PAYMENT_COMPLETE)
- val WaitingForComplete(_, cmd1, Nil, sharedSecrets1, _, route1) = paymentFSM.stateData
+ val WaitingForComplete(_, cmd1, Nil, sharedSecrets1, _, route1, sentAt1) = paymentFSM.stateData
register.expectMsg(ForwardShortId(paymentFSM.toTyped, scid_ab, cmd1))
// we change the cltv expiry
val channelUpdate_bc_modified = makeChannelUpdate(Block.RegtestGenesisBlock.hash, priv_b, c, scid_bc, CltvExpiryDelta(42), htlcMinimumMsat = update_bc.htlcMinimumMsat, feeBaseMsat = update_bc.feeBaseMsat, feeProportionalMillionths = update_bc.feeProportionalMillionths, htlcMaximumMsat = update_bc.htlcMaximumMsat)
val failure = IncorrectCltvExpiry(CltvExpiry(5), Some(channelUpdate_bc_modified))
+ val failedAt1 = TimestampMilli.now()
// and node replies with a failure containing a new channel update
sender.send(paymentFSM, addCompleted(HtlcResult.RemoteFail(UpdateFailHtlc(ByteVector32.Zeroes, 0, SphinxSpec.createAndWrap(sharedSecrets1.head.secret, failure)))))
@@ -563,12 +571,13 @@ class PaymentLifecycleSpec extends BaseRouterSpec {
// router answers with a new route, taking into account the new update
awaitCond(paymentFSM.stateName == WAITING_FOR_PAYMENT_COMPLETE)
- val WaitingForComplete(_, cmd2, _, sharedSecrets2, _, route2) = paymentFSM.stateData
+ val WaitingForComplete(_, cmd2, _, sharedSecrets2, _, route2, sentAt2) = paymentFSM.stateData
register.expectMsg(ForwardShortId(paymentFSM.toTyped, scid_ab, cmd2))
// we change the cltv expiry one more time
val channelUpdate_bc_modified_2 = makeChannelUpdate(Block.RegtestGenesisBlock.hash, priv_b, c, scid_bc, CltvExpiryDelta(43), htlcMinimumMsat = update_bc.htlcMinimumMsat, feeBaseMsat = update_bc.feeBaseMsat, feeProportionalMillionths = update_bc.feeProportionalMillionths, htlcMaximumMsat = update_bc.htlcMaximumMsat)
val failure2 = IncorrectCltvExpiry(CltvExpiry(5), Some(channelUpdate_bc_modified_2))
+ val failedAt2 = TimestampMilli.now()
// and node replies with a failure containing a new channel update
sender.send(paymentFSM, addCompleted(HtlcResult.RemoteFail(UpdateFailHtlc(ByteVector32.Zeroes, 0, SphinxSpec.createAndWrap(sharedSecrets2.head.secret, failure2)))))
@@ -580,7 +589,12 @@ class PaymentLifecycleSpec extends BaseRouterSpec {
routerForwarder.forward(routerFixture.router)
// this time the router can't find a route: game over
- assert(sender.expectMsgType[PaymentFailed].failures == RemoteFailure(route1.amount, route1.hops, Sphinx.DecryptedFailurePacket(b, 1, failure)) :: RemoteFailure(route2.amount, route2.hops, Sphinx.DecryptedFailurePacket(b, 1, failure2)) :: LocalFailure(defaultAmountMsat, Nil, RouteNotFound) :: Nil)
+ val pf = sender.expectMsgType[PaymentFailed]
+ assert(pf.failures.size == 3)
+ assert(pf.failures.take(2).forall(_.isInstanceOf[RemoteFailure]))
+ assert(pf.failures(0).asInstanceOf[RemoteFailure].copy(failedAt = failedAt1) == RemoteFailure(route1.amount, route1.hops, Sphinx.DecryptedFailurePacket(b, 1, failure), sentAt1, failedAt1))
+ assert(pf.failures(1).asInstanceOf[RemoteFailure].copy(failedAt = failedAt2) == RemoteFailure(route2.amount, route2.hops, Sphinx.DecryptedFailurePacket(b, 1, failure2), sentAt2, failedAt2))
+ assert(pf.failures.last == LocalFailure(defaultAmountMsat, Nil, RouteNotFound))
awaitCond(nodeParams.db.payments.getOutgoingPayment(id).exists(_.status.isInstanceOf[OutgoingPaymentStatus.Failed]))
routerForwarder.expectNoMessage(100 millis)
@@ -595,7 +609,7 @@ class PaymentLifecycleSpec extends BaseRouterSpec {
expectRouteRequest(routerForwarder, a, cfg)
routerForwarder.forward(routerFixture.router)
awaitCond(paymentFSM.stateName == WAITING_FOR_PAYMENT_COMPLETE)
- val WaitingForComplete(_, cmd1, Nil, sharedSecrets1, _, _) = paymentFSM.stateData
+ val WaitingForComplete(_, cmd1, Nil, sharedSecrets1, _, _, _) = paymentFSM.stateData
register.expectMsg(ForwardShortId(paymentFSM.toTyped, scid_ab, cmd1))
// the node replies with a temporary failure containing the same update as the one we already have (likely a balance issue)
@@ -627,7 +641,7 @@ class PaymentLifecycleSpec extends BaseRouterSpec {
expectRouteRequest(routerForwarder, a, cfg, target = recipient)
routerForwarder.forward(routerFixture.router)
awaitCond(paymentFSM.stateName == WAITING_FOR_PAYMENT_COMPLETE)
- val WaitingForComplete(_, cmd1, Nil, sharedSecrets1, _, _) = paymentFSM.stateData
+ val WaitingForComplete(_, cmd1, Nil, sharedSecrets1, _, _, _) = paymentFSM.stateData
register.expectMsg(ForwardShortId(paymentFSM.toTyped, scid_ab, cmd1))
// we change the cltv expiry
@@ -646,7 +660,7 @@ class PaymentLifecycleSpec extends BaseRouterSpec {
// router answers with a new route, taking into account the new update
awaitCond(paymentFSM.stateName == WAITING_FOR_PAYMENT_COMPLETE)
- val WaitingForComplete(_, cmd2, _, _, _, _) = paymentFSM.stateData
+ val WaitingForComplete(_, cmd2, _, _, _, _, _) = paymentFSM.stateData
register.expectMsg(ForwardShortId(paymentFSM.toTyped, scid_ab, cmd2))
assert(cmd2.cltvExpiry > cmd1.cltvExpiry)
}
@@ -667,7 +681,7 @@ class PaymentLifecycleSpec extends BaseRouterSpec {
expectRouteRequest(routerForwarder, a, cfg, target = recipient)
routerForwarder.forward(routerFixture.router)
awaitCond(paymentFSM.stateName == WAITING_FOR_PAYMENT_COMPLETE)
- val WaitingForComplete(_, cmd1, Nil, sharedSecrets1, _, _) = paymentFSM.stateData
+ val WaitingForComplete(_, cmd1, Nil, sharedSecrets1, _, _, _) = paymentFSM.stateData
register.expectMsg(ForwardShortId(paymentFSM.toTyped, scid_ab, cmd1))
// we disable the channel
@@ -693,9 +707,10 @@ class PaymentLifecycleSpec extends BaseRouterSpec {
expectRouteRequest(routerForwarder, a, cfg)
routerForwarder.forward(router)
awaitCond(paymentFSM.stateName == WAITING_FOR_PAYMENT_COMPLETE)
- val WaitingForComplete(_, cmd1, Nil, sharedSecrets1, _, route1) = paymentFSM.stateData
+ val WaitingForComplete(_, cmd1, Nil, sharedSecrets1, _, route1, sentAt1) = paymentFSM.stateData
register.expectMsg(ForwardShortId(paymentFSM.toTyped, scid_ab, cmd1))
+ val failedAt = TimestampMilli.now()
sender.send(paymentFSM, addCompleted(HtlcResult.RemoteFail(UpdateFailHtlc(ByteVector32.Zeroes, 0, SphinxSpec.createAndWrap(sharedSecrets1.head.secret, failure)))))
// payment lifecycle forwards the embedded channelUpdate to the router
@@ -704,7 +719,11 @@ class PaymentLifecycleSpec extends BaseRouterSpec {
routerForwarder.forward(router)
// we allow 2 tries, so we send a 2nd request to the router, which won't find another route
- assert(sender.expectMsgType[PaymentFailed].failures == RemoteFailure(route1.amount, route1.hops, Sphinx.DecryptedFailurePacket(b, 1, failure)) :: LocalFailure(defaultAmountMsat, Nil, RouteNotFound) :: Nil)
+ val pf = sender.expectMsgType[PaymentFailed]
+ assert(pf.failures.size == 2)
+ assert(pf.failures.head.isInstanceOf[RemoteFailure])
+ assert(pf.failures.head.asInstanceOf[RemoteFailure].copy(failedAt = failedAt) == RemoteFailure(route1.amount, route1.hops, Sphinx.DecryptedFailurePacket(b, 1, failure), sentAt1, failedAt))
+ assert(pf.failures.last == LocalFailure(defaultAmountMsat, Nil, RouteNotFound))
awaitCond(nodeParams.db.payments.getOutgoingPayment(id).exists(_.status.isInstanceOf[OutgoingPaymentStatus.Failed]))
}
@@ -730,7 +749,7 @@ class PaymentLifecycleSpec extends BaseRouterSpec {
routerForwarder.expectMsgType[RouteRequest]
routerForwarder.forward(routerFixture.router)
awaitCond(paymentFSM.stateName == WAITING_FOR_PAYMENT_COMPLETE)
- val WaitingForComplete(_, cmd1, Nil, sharedSecrets, _, route) = paymentFSM.stateData
+ val WaitingForComplete(_, cmd1, Nil, sharedSecrets, _, route, sentAt) = paymentFSM.stateData
register.expectMsg(ForwardShortId(paymentFSM.toTyped, scid_ab, cmd1))
// The payment fails inside the blinded route: the introduction node sends back an error.
@@ -746,7 +765,10 @@ class PaymentLifecycleSpec extends BaseRouterSpec {
// Without the blinded route, the router cannot find a route to the recipient.
val failed = sender.expectMsgType[PaymentFailed]
- assert(failed.failures == Seq(RemoteFailure(defaultAmountMsat, route.hops ++ Seq(blindedHop), Sphinx.DecryptedFailurePacket(b, 1, failure)), LocalFailure(defaultAmountMsat, Nil, RouteNotFound)))
+ assert(failed.failures.size == 2)
+ assert(failed.failures.head.isInstanceOf[RemoteFailure])
+ assert(failed.failures.head.asInstanceOf[RemoteFailure].copy(failedAt = 0 unixms) == RemoteFailure(defaultAmountMsat, route.hops ++ Seq(blindedHop), Sphinx.DecryptedFailurePacket(b, 1, failure), sentAt, 0 unixms))
+ assert(failed.failures.last == LocalFailure(defaultAmountMsat, Nil, RouteNotFound))
awaitCond(nodeParams.db.payments.getOutgoingPayment(cfg.id).exists(_.status.isInstanceOf[OutgoingPaymentStatus.Failed]))
}
@@ -824,7 +846,7 @@ class PaymentLifecycleSpec extends BaseRouterSpec {
sender.send(paymentFSM, addCompleted(HtlcResult.OnChainFulfill(defaultPaymentPreimage)))
val paymentOK = sender.expectMsgType[PaymentSent]
- val PaymentSent(_, _, paymentOK.paymentPreimage, finalAmount, _, PartialPayment(_, partAmount, fee, ByteVector32.Zeroes, _, _) :: Nil, _) = eventListener.expectMsgType[PaymentSent]
+ val PaymentSent(_, paymentOK.paymentPreimage, finalAmount, _, PartialPayment(_, partAmount, fee, ByteVector32.Zeroes, _, _, _) :: Nil, _, _) = eventListener.expectMsgType[PaymentSent]
assert(partAmount == request.amount)
assert(finalAmount == defaultAmountMsat)
@@ -878,14 +900,14 @@ class PaymentLifecycleSpec extends BaseRouterSpec {
test("filter errors properly") { () =>
val failures = Seq(
LocalFailure(defaultAmountMsat, Nil, RouteNotFound),
- RemoteFailure(defaultAmountMsat, channelHopFromUpdate(a, b, update_ab) :: Nil, Sphinx.DecryptedFailurePacket(b, 1, TemporaryNodeFailure())),
+ RemoteFailure(defaultAmountMsat, channelHopFromUpdate(a, b, update_ab) :: Nil, Sphinx.DecryptedFailurePacket(b, 1, TemporaryNodeFailure()), 150 unixms, 300 unixms),
LocalFailure(defaultAmountMsat, channelHopFromUpdate(a, b, update_ab) :: Nil, ChannelUnavailable(ByteVector32.Zeroes)),
LocalFailure(defaultAmountMsat, Nil, RouteNotFound)
)
val filtered = PaymentFailure.transformForUser(failures)
val expected = Seq(
LocalFailure(defaultAmountMsat, Nil, RouteNotFound),
- RemoteFailure(defaultAmountMsat, channelHopFromUpdate(a, b, update_ab) :: Nil, Sphinx.DecryptedFailurePacket(b, 1, TemporaryNodeFailure())),
+ RemoteFailure(defaultAmountMsat, channelHopFromUpdate(a, b, update_ab) :: Nil, Sphinx.DecryptedFailurePacket(b, 1, TemporaryNodeFailure()), 150 unixms, 300 unixms),
LocalFailure(defaultAmountMsat, channelHopFromUpdate(a, b, update_ab) :: Nil, ChannelUnavailable(ByteVector32.Zeroes))
)
assert(filtered == expected)
@@ -903,22 +925,22 @@ class PaymentLifecycleSpec extends BaseRouterSpec {
(LocalFailure(defaultAmountMsat, NodeHop(a, b, CltvExpiryDelta(144), 0 msat) :: NodeHop(b, c, CltvExpiryDelta(144), 0 msat) :: Nil, RouteNotFound), Set.empty, Set.empty),
(LocalFailure(defaultAmountMsat, route_abcd, new RuntimeException("fatal")), Set.empty, Set(ChannelDesc(scid_ab, a, b))),
// remote failure from final recipient -> all intermediate nodes behaved correctly
- (RemoteFailure(defaultAmountMsat, route_abcd, Sphinx.DecryptedFailurePacket(d, 3, IncorrectOrUnknownPaymentDetails(100 msat, BlockHeight(42)))), Set.empty, Set.empty),
+ (RemoteFailure(defaultAmountMsat, route_abcd, Sphinx.DecryptedFailurePacket(d, 3, IncorrectOrUnknownPaymentDetails(100 msat, BlockHeight(42))), 150 unixms, 300 unixms), Set.empty, Set.empty),
// remote failures from intermediate nodes -> depending on the failure, ignore either the failing node or its outgoing channel
- (RemoteFailure(defaultAmountMsat, route_abcd, Sphinx.DecryptedFailurePacket(b, 1, PermanentNodeFailure())), Set(b), Set.empty),
- (RemoteFailure(defaultAmountMsat, route_abcd, Sphinx.DecryptedFailurePacket(c, 2, TemporaryNodeFailure())), Set(c), Set.empty),
- (RemoteFailure(defaultAmountMsat, route_abcd, Sphinx.DecryptedFailurePacket(b, 1, PermanentChannelFailure())), Set.empty, Set(ChannelDesc(scid_bc, b, c))),
- (RemoteFailure(defaultAmountMsat, route_abcd, Sphinx.DecryptedFailurePacket(c, 2, UnknownNextPeer())), Set.empty, Set(ChannelDesc(scid_cd, c, d))),
- (RemoteFailure(defaultAmountMsat, route_abcd, Sphinx.DecryptedFailurePacket(b, 1, FeeInsufficient(100 msat, Some(update_bc)))), Set.empty, Set.empty),
- (RemoteFailure(defaultAmountMsat, blindedRoute_abc, Sphinx.DecryptedFailurePacket(b, 1, InvalidOnionBlinding(randomBytes32()))), Set.empty, Set(ChannelDesc(blindedHop_bc.dummyId, blindedHop_bc.nodeId, blindedHop_bc.nextNodeId))),
- (RemoteFailure(defaultAmountMsat, blindedRoute_abc, Sphinx.DecryptedFailurePacket(blindedHop_bc.resolved.route.blindedNodeIds(1), 2, InvalidOnionBlinding(randomBytes32()))), Set.empty, Set(ChannelDesc(blindedHop_bc.dummyId, blindedHop_bc.nodeId, blindedHop_bc.nextNodeId))),
+ (RemoteFailure(defaultAmountMsat, route_abcd, Sphinx.DecryptedFailurePacket(b, 1, PermanentNodeFailure()), 150 unixms, 300 unixms), Set(b), Set.empty),
+ (RemoteFailure(defaultAmountMsat, route_abcd, Sphinx.DecryptedFailurePacket(c, 2, TemporaryNodeFailure()), 150 unixms, 300 unixms), Set(c), Set.empty),
+ (RemoteFailure(defaultAmountMsat, route_abcd, Sphinx.DecryptedFailurePacket(b, 1, PermanentChannelFailure()), 150 unixms, 300 unixms), Set.empty, Set(ChannelDesc(scid_bc, b, c))),
+ (RemoteFailure(defaultAmountMsat, route_abcd, Sphinx.DecryptedFailurePacket(c, 2, UnknownNextPeer()), 150 unixms, 300 unixms), Set.empty, Set(ChannelDesc(scid_cd, c, d))),
+ (RemoteFailure(defaultAmountMsat, route_abcd, Sphinx.DecryptedFailurePacket(b, 1, FeeInsufficient(100 msat, Some(update_bc))), 150 unixms, 300 unixms), Set.empty, Set.empty),
+ (RemoteFailure(defaultAmountMsat, blindedRoute_abc, Sphinx.DecryptedFailurePacket(b, 1, InvalidOnionBlinding(randomBytes32())), 150 unixms, 300 unixms), Set.empty, Set(ChannelDesc(blindedHop_bc.dummyId, blindedHop_bc.nodeId, blindedHop_bc.nextNodeId))),
+ (RemoteFailure(defaultAmountMsat, blindedRoute_abc, Sphinx.DecryptedFailurePacket(blindedHop_bc.resolved.route.blindedNodeIds(1), 2, InvalidOnionBlinding(randomBytes32())), 150 unixms, 300 unixms), Set.empty, Set(ChannelDesc(blindedHop_bc.dummyId, blindedHop_bc.nodeId, blindedHop_bc.nextNodeId))),
// unreadable remote failures -> blacklist all nodes except our direct peer, the final recipient, the last hop or nodes relaying attribution data
- (UnreadableRemoteFailure(defaultAmountMsat, channelHopFromUpdate(a, b, update_ab) :: Nil, Sphinx.CannotDecryptFailurePacket(ByteVector.empty, None), Nil), Set.empty, Set.empty),
- (UnreadableRemoteFailure(defaultAmountMsat, channelHopFromUpdate(a, b, update_ab) :: channelHopFromUpdate(b, c, update_bc) :: channelHopFromUpdate(c, d, update_cd) :: Nil, Sphinx.CannotDecryptFailurePacket(ByteVector.empty, None), Nil), Set(c), Set.empty),
- (UnreadableRemoteFailure(defaultAmountMsat, channelHopFromUpdate(a, b, update_ab) :: channelHopFromUpdate(b, c, update_bc) :: channelHopFromUpdate(c, d, update_cd) :: channelHopFromUpdate(d, e, update_de) :: Nil, Sphinx.CannotDecryptFailurePacket(ByteVector.empty, None), Nil), Set(c, d), Set.empty),
- (UnreadableRemoteFailure(defaultAmountMsat, channelHopFromUpdate(a, b, update_ab) :: channelHopFromUpdate(b, c, update_bc) :: channelHopFromUpdate(c, d, update_cd) :: channelHopFromUpdate(d, e, update_de) :: Nil, Sphinx.CannotDecryptFailurePacket(ByteVector.empty, None), Seq(HoldTime(100 millis, b), HoldTime(90 millis, c), HoldTime(80 millis, d))), Set(d), Set.empty),
- (UnreadableRemoteFailure(defaultAmountMsat, channelHopFromUpdate(a, b, update_ab) :: channelHopFromUpdate(b, c, update_bc) :: channelHopFromUpdate(c, d, update_cd) :: NodeHop(d, e, CltvExpiryDelta(24), 0 msat) :: Nil, Sphinx.CannotDecryptFailurePacket(ByteVector.empty, None), Nil), Set(c), Set.empty),
- (UnreadableRemoteFailure(defaultAmountMsat, channelHopFromUpdate(a, b, update_ab) :: channelHopFromUpdate(b, c, update_bc) :: channelHopFromUpdate(c, d, update_cd) :: blindedHop_de :: Nil, Sphinx.CannotDecryptFailurePacket(ByteVector.empty, None), Nil), Set(c), Set.empty),
+ (UnreadableRemoteFailure(defaultAmountMsat, channelHopFromUpdate(a, b, update_ab) :: Nil, Sphinx.CannotDecryptFailurePacket(ByteVector.empty, None), 150 unixms, 300 unixms, Nil), Set.empty, Set.empty),
+ (UnreadableRemoteFailure(defaultAmountMsat, channelHopFromUpdate(a, b, update_ab) :: channelHopFromUpdate(b, c, update_bc) :: channelHopFromUpdate(c, d, update_cd) :: Nil, Sphinx.CannotDecryptFailurePacket(ByteVector.empty, None), 150 unixms, 300 unixms, Nil), Set(c), Set.empty),
+ (UnreadableRemoteFailure(defaultAmountMsat, channelHopFromUpdate(a, b, update_ab) :: channelHopFromUpdate(b, c, update_bc) :: channelHopFromUpdate(c, d, update_cd) :: channelHopFromUpdate(d, e, update_de) :: Nil, Sphinx.CannotDecryptFailurePacket(ByteVector.empty, None), 150 unixms, 300 unixms, Nil), Set(c, d), Set.empty),
+ (UnreadableRemoteFailure(defaultAmountMsat, channelHopFromUpdate(a, b, update_ab) :: channelHopFromUpdate(b, c, update_bc) :: channelHopFromUpdate(c, d, update_cd) :: channelHopFromUpdate(d, e, update_de) :: Nil, Sphinx.CannotDecryptFailurePacket(ByteVector.empty, None), 150 unixms, 300 unixms, Seq(HoldTime(100 millis, b), HoldTime(90 millis, c), HoldTime(80 millis, d))), Set(d), Set.empty),
+ (UnreadableRemoteFailure(defaultAmountMsat, channelHopFromUpdate(a, b, update_ab) :: channelHopFromUpdate(b, c, update_bc) :: channelHopFromUpdate(c, d, update_cd) :: NodeHop(d, e, CltvExpiryDelta(24), 0 msat) :: Nil, Sphinx.CannotDecryptFailurePacket(ByteVector.empty, None), 150 unixms, 300 unixms, Nil), Set(c), Set.empty),
+ (UnreadableRemoteFailure(defaultAmountMsat, channelHopFromUpdate(a, b, update_ab) :: channelHopFromUpdate(b, c, update_bc) :: channelHopFromUpdate(c, d, update_cd) :: blindedHop_de :: Nil, Sphinx.CannotDecryptFailurePacket(ByteVector.empty, None), 150 unixms, 300 unixms, Nil), Set(c), Set.empty),
)
for ((failure, expectedNodes, expectedChannels) <- testCases) {
@@ -928,8 +950,8 @@ class PaymentLifecycleSpec extends BaseRouterSpec {
}
val failures = Seq(
- RemoteFailure(defaultAmountMsat, route_abcd, Sphinx.DecryptedFailurePacket(c, 2, TemporaryNodeFailure())),
- RemoteFailure(defaultAmountMsat, route_abcd, Sphinx.DecryptedFailurePacket(b, 1, UnknownNextPeer())),
+ RemoteFailure(defaultAmountMsat, route_abcd, Sphinx.DecryptedFailurePacket(c, 2, TemporaryNodeFailure()), 150 unixms, 300 unixms),
+ RemoteFailure(defaultAmountMsat, route_abcd, Sphinx.DecryptedFailurePacket(b, 1, UnknownNextPeer()), 150 unixms, 300 unixms),
LocalFailure(defaultAmountMsat, route_abcd, new RuntimeException("fatal"))
)
val ignore = PaymentFailure.updateIgnored(failures, Ignore.empty)
@@ -973,7 +995,7 @@ class PaymentLifecycleSpec extends BaseRouterSpec {
val Transition(_, WAITING_FOR_ROUTE, WAITING_FOR_CONFIDENCE) = monitor.expectMsgClass(classOf[Transition[_]])
val Transition(_, WAITING_FOR_CONFIDENCE, WAITING_FOR_PAYMENT_COMPLETE) = monitor.expectMsgClass(classOf[Transition[_]])
- val WaitingForComplete(_, _, Nil, sharedSecrets1, _, _) = paymentFSM.stateData
+ val WaitingForComplete(_, _, Nil, sharedSecrets1, _, _, _) = paymentFSM.stateData
awaitCond(nodeParams.db.payments.getOutgoingPayment(id).exists(_.status == OutgoingPaymentStatus.Pending))
val Some(outgoing) = nodeParams.db.payments.getOutgoingPayment(id)
assert(outgoing.copy(createdAt = 0 unixms) == OutgoingPayment(id, parentId, Some(defaultExternalId), defaultPaymentHash, PaymentType.Standard, defaultAmountMsat, defaultAmountMsat, d, 0 unixms, Some(defaultInvoice), None, OutgoingPaymentStatus.Pending))
diff --git a/eclair-core/src/test/scala/fr/acinq/eclair/payment/relay/ChannelRelayerSpec.scala b/eclair-core/src/test/scala/fr/acinq/eclair/payment/relay/ChannelRelayerSpec.scala
index 898c02b..1603964 100644
--- a/eclair-core/src/test/scala/fr/acinq/eclair/payment/relay/ChannelRelayerSpec.scala
+++ b/eclair-core/src/test/scala/fr/acinq/eclair/payment/relay/ChannelRelayerSpec.scala
@@ -595,7 +595,7 @@ class ChannelRelayerSpec extends ScalaTestWithActorTestKit(ConfigFactory.load("a
{
val payload = ChannelRelay.Standard(ShortChannelId(12345), 998900 msat, CltvExpiry(60), upgradeAccountability = false)
- val r = createValidIncomingPacket(payload, 1000000 msat, CltvExpiry(70), accountableIn = false)
+ val r = createValidIncomingPacket(payload, 1000000 msat, CltvExpiry(70))
channelRelayer ! Relay(r, TestConstants.Alice.nodeParams.nodeId, 0.1)
receiveConfidence(Reputation.Score(1.0, accountable = false))
// select the channel to the same node, with the lowest capacity and balance but still high enough to handle the payment
@@ -616,7 +616,7 @@ class ChannelRelayerSpec extends ScalaTestWithActorTestKit(ConfigFactory.load("a
{
// higher amount payment (have to increased incoming htlc amount for fees to be sufficient)
val payload = ChannelRelay.Standard(ShortChannelId(12345), 50000000 msat, CltvExpiry(60), upgradeAccountability = false)
- val r = createValidIncomingPacket(payload, 60000000 msat, CltvExpiry(70), accountableIn = false)
+ val r = createValidIncomingPacket(payload, 60000000 msat, CltvExpiry(70))
channelRelayer ! Relay(r, TestConstants.Alice.nodeParams.nodeId, 0.1)
receiveConfidence(Reputation.Score(1.0, accountable = false))
expectFwdAdd(register, channelUpdates(ShortChannelId(11111)).channelId, r.amountToForward, r.outgoingCltv, outAccountable = false).message
@@ -624,7 +624,7 @@ class ChannelRelayerSpec extends ScalaTestWithActorTestKit(ConfigFactory.load("a
{
// lower amount payment
val payload = ChannelRelay.Standard(ShortChannelId(12345), 1000 msat, CltvExpiry(60), upgradeAccountability = false)
- val r = createValidIncomingPacket(payload, 60000000 msat, CltvExpiry(70), accountableIn = false)
+ val r = createValidIncomingPacket(payload, 60000000 msat, CltvExpiry(70))
channelRelayer ! Relay(r, TestConstants.Alice.nodeParams.nodeId, 0.1)
receiveConfidence(Reputation.Score(1.0, accountable = false))
expectFwdAdd(register, channelUpdates(ShortChannelId(33333)).channelId, r.amountToForward, r.outgoingCltv, outAccountable = false).message
@@ -701,7 +701,7 @@ class ChannelRelayerSpec extends ScalaTestWithActorTestKit(ConfigFactory.load("a
Seq(true, false).foreach { isIntroduction =>
testCases.foreach { htlcResult =>
- val r = createValidIncomingPacket(createBlindedPayload(Right(u.channelUpdate.shortChannelId), u.channelUpdate, isIntroduction), outgoingAmount + u.channelUpdate.feeBaseMsat, outgoingExpiry + u.channelUpdate.cltvExpiryDelta, accountableIn = false)
+ val r = createValidIncomingPacket(createBlindedPayload(Right(u.channelUpdate.shortChannelId), u.channelUpdate, isIntroduction), outgoingAmount + u.channelUpdate.feeBaseMsat, outgoingExpiry + u.channelUpdate.cltvExpiryDelta)
channelRelayer ! WrappedLocalChannelUpdate(u)
channelRelayer ! Relay(r, TestConstants.Alice.nodeParams.nodeId, 0.1)
receiveConfidence(Reputation.Score(1.0, accountable = false))
@@ -734,7 +734,7 @@ class ChannelRelayerSpec extends ScalaTestWithActorTestKit(ConfigFactory.load("a
val channelId1 = channelIds(realScid1)
val payload = ChannelRelay.Standard(realScid1, outgoingAmount, outgoingExpiry, upgradeAccountability = false)
- val r = createValidIncomingPacket(payload, accountableIn = false)
+ val r = createValidIncomingPacket(payload)
val u = createLocalUpdate(channelId1)
val downstream_htlc = UpdateAddHtlc(channelId1, 7, outgoingAmount, paymentHash, outgoingExpiry, emptyOnionPacket, None, accountable = false, None)
@@ -746,6 +746,7 @@ class ChannelRelayerSpec extends ScalaTestWithActorTestKit(ConfigFactory.load("a
)
testCases.foreach { testCase =>
+ val now = TimestampMilli.now()
channelRelayer ! WrappedLocalChannelUpdate(u)
channelRelayer ! Relay(r, TestConstants.Alice.nodeParams.nodeId, 0.1)
receiveConfidence(Reputation.Score(1.0, accountable = false))
@@ -760,7 +761,13 @@ class ChannelRelayerSpec extends ScalaTestWithActorTestKit(ConfigFactory.load("a
assert(fwd2.message.r == paymentPreimage)
val paymentRelayed = eventListener.expectMessageType[ChannelPaymentRelayed]
- assert(paymentRelayed.copy(receivedAt = 0 unixms, settledAt = 0 unixms) == ChannelPaymentRelayed(r.add.amountMsat, r.amountToForward, r.add.paymentHash, r.add.channelId, channelId1, receivedAt = 0 unixms, settledAt = 0 unixms))
+ assert(paymentRelayed.paymentHash == r.add.paymentHash)
+ assert(paymentRelayed.amountIn == r.add.amountMsat)
+ assert(paymentRelayed.fromChannelId == r.add.channelId)
+ assert(paymentRelayed.amountOut == r.amountToForward)
+ assert(paymentRelayed.toChannelId == channelId1)
+ assert(paymentRelayed.startedAt == r.receivedAt)
+ assert(paymentRelayed.settledAt >= now)
}
}
@@ -772,7 +779,7 @@ class ChannelRelayerSpec extends ScalaTestWithActorTestKit(ConfigFactory.load("a
channelRelayer ! WrappedLocalChannelUpdate(u)
val payload = ChannelRelay.Standard(realScid1, outgoingAmount, outgoingExpiry, upgradeAccountability = false)
- val r = createValidIncomingPacket(payload, accountableIn = false)
+ val r = createValidIncomingPacket(payload)
channelRelayer ! Relay(r, TestConstants.Alice.nodeParams.nodeId, 0.1)
receiveConfidence(Reputation.Score(1.0, accountable = false))
val fwd1 = expectFwdAdd(register, channelIds(realScid1), outgoingAmount, outgoingExpiry, outAccountable = false)
@@ -886,14 +893,14 @@ object ChannelRelayerSpec {
ChannelRelay.Blinded(tlvs, PaymentRelayData(blindedTlvs), randomKey().publicKey)
}
- def createValidIncomingPacket(payload: IntermediatePayload.ChannelRelay, amountIn: MilliSatoshi = 11_000_000 msat, expiryIn: CltvExpiry = CltvExpiry(400_100), accountableIn: Boolean = false): IncomingPaymentPacket.ChannelRelayPacket = {
+ def createValidIncomingPacket(payload: IntermediatePayload.ChannelRelay, amountIn: MilliSatoshi = 11_000_000 msat, expiryIn: CltvExpiry = CltvExpiry(400_100), accountableIn: Boolean = false, receivedAt: TimestampMilli = TimestampMilli.now()): IncomingPaymentPacket.ChannelRelayPacket = {
val nextPathKey_opt = payload match {
case p: ChannelRelay.Blinded => Some(UpdateAddHtlcTlv.PathKey(p.nextPathKey))
case _: ChannelRelay.Standard => None
}
val tlvs = TlvStream(Set[Option[UpdateAddHtlcTlv]](nextPathKey_opt, if (accountableIn) Some(UpdateAddHtlcTlv.Accountable()) else None).flatten)
val add_ab = UpdateAddHtlc(channelId = randomBytes32(), id = 123456, amountIn, paymentHash, expiryIn, emptyOnionPacket, tlvs)
- ChannelRelayPacket(add_ab, payload, emptyOnionPacket, TimestampMilli.now())
+ ChannelRelayPacket(add_ab, payload, emptyOnionPacket, receivedAt)
}
def createAliases(channelId: ByteVector32): ShortIdAliases = {
diff --git a/eclair-core/src/test/scala/fr/acinq/eclair/payment/relay/NodeRelayerSpec.scala b/eclair-core/src/test/scala/fr/acinq/eclair/payment/relay/NodeRelayerSpec.scala
index ebca25c..9d37bd3 100644
--- a/eclair-core/src/test/scala/fr/acinq/eclair/payment/relay/NodeRelayerSpec.scala
+++ b/eclair-core/src/test/scala/fr/acinq/eclair/payment/relay/NodeRelayerSpec.scala
@@ -49,7 +49,7 @@ import fr.acinq.eclair.wire.protocol.PaymentOnion.{FinalPayload, IntermediatePay
import fr.acinq.eclair.wire.protocol.RouteBlindingEncryptedDataCodecs.blindedRouteDataCodec
import fr.acinq.eclair.wire.protocol.RouteBlindingEncryptedDataTlv.{AllowedFeatures, PathId, PaymentConstraints}
import fr.acinq.eclair.wire.protocol._
-import fr.acinq.eclair.{Alias, BlockHeight, Bolt11Feature, Bolt12Feature, CltvExpiry, CltvExpiryDelta, EncodedNodeId, FeatureSupport, Features, MilliSatoshi, MilliSatoshiLong, NodeParams, RealShortChannelId, ShortChannelId, TestConstants, TimestampMilli, UInt64, randomBytes32, randomKey}
+import fr.acinq.eclair.{Alias, BlockHeight, Bolt11Feature, Bolt12Feature, CltvExpiry, CltvExpiryDelta, EncodedNodeId, FeatureSupport, Features, MilliSatoshi, MilliSatoshiLong, NodeParams, RealShortChannelId, ShortChannelId, TestConstants, TimestampMilli, TimestampMilliLong, UInt64, randomBytes32, randomKey}
import org.scalatest.funsuite.FixtureAnyFunSuiteLike
import org.scalatest.{Outcome, Tag}
import scodec.bits.{ByteVector, HexStringSyntax}
@@ -485,7 +485,7 @@ class NodeRelayerSpec extends ScalaTestWithActorTestKit(ConfigFactory.load("appl
val nodeRelayerAdapters = mockPayFSM.expectMessageType[SendMultiPartPayment].replyTo
// The proposed fees are low, so we ask the sender to raise them.
- nodeRelayerAdapters ! PaymentFailed(relayId, paymentHash, LocalFailure(outgoingAmount, Nil, BalanceTooLow) :: Nil)
+ nodeRelayerAdapters ! PaymentFailed(relayId, paymentHash, LocalFailure(outgoingAmount, Nil, BalanceTooLow) :: Nil, 100 unixms, 150 unixms)
incomingMultiPart.foreach { p =>
val fwd = register.expectMessageType[Register.Forward[CMD_FAIL_HTLC]]
assert(fwd.channelId == p.add.channelId)
@@ -537,8 +537,8 @@ class NodeRelayerSpec extends ScalaTestWithActorTestKit(ConfigFactory.load("appl
router.expectMessageType[RouteRequest]
// If we're having a hard time finding routes, raising the fee/cltv will likely help.
- val failures = LocalFailure(outgoingAmount, Nil, RouteNotFound) :: RemoteFailure(outgoingAmount, Nil, Sphinx.DecryptedFailurePacket(outgoingNodeId, 1, PermanentNodeFailure())) :: LocalFailure(outgoingAmount, Nil, RouteNotFound) :: Nil
- payFSM ! PaymentFailed(relayId, incomingMultiPart.head.add.paymentHash, failures)
+ val failures = LocalFailure(outgoingAmount, Nil, RouteNotFound) :: RemoteFailure(outgoingAmount, Nil, Sphinx.DecryptedFailurePacket(outgoingNodeId, 1, PermanentNodeFailure()), 100 unixms, 150 unixms) :: LocalFailure(outgoingAmount, Nil, RouteNotFound) :: Nil
+ payFSM ! PaymentFailed(relayId, incomingMultiPart.head.add.paymentHash, failures, 100 unixms, 150 unixms)
incomingMultiPart.foreach { p =>
val fwd = register.expectMessageType[Register.Forward[CMD_FAIL_HTLC]]
@@ -562,8 +562,8 @@ class NodeRelayerSpec extends ScalaTestWithActorTestKit(ConfigFactory.load("appl
val payFSM = mockPayFSM.expectMessageType[akka.actor.ActorRef]
router.expectMessageType[RouteRequest]
- val failures = RemoteFailure(outgoingAmount, Nil, Sphinx.DecryptedFailurePacket(outgoingNodeId, 1, FinalIncorrectHtlcAmount(42 msat))) :: UnreadableRemoteFailure(outgoingAmount, Nil, Sphinx.CannotDecryptFailurePacket(ByteVector.empty, None), Nil) :: Nil
- payFSM ! PaymentFailed(relayId, incomingMultiPart.head.add.paymentHash, failures)
+ val failures = RemoteFailure(outgoingAmount, Nil, Sphinx.DecryptedFailurePacket(outgoingNodeId, 1, FinalIncorrectHtlcAmount(42 msat)), 100 unixms, 150 unixms) :: UnreadableRemoteFailure(outgoingAmount, Nil, Sphinx.CannotDecryptFailurePacket(ByteVector.empty, None), 100 unixms, 150 unixms, Nil) :: Nil
+ payFSM ! PaymentFailed(relayId, incomingMultiPart.head.add.paymentHash, failures, 100 unixms, 150 unixms)
incomingMultiPart.foreach { p =>
val fwd = register.expectMessageType[Register.Forward[CMD_FAIL_HTLC]]
@@ -668,7 +668,7 @@ class NodeRelayerSpec extends ScalaTestWithActorTestKit(ConfigFactory.load("appl
// The corresponding commands conflict with the previous fulfill and are ignored.
val downstreamHtlc = UpdateAddHtlc(randomBytes32(), 7, outgoingAmount, paymentHash, outgoingExpiry, TestConstants.emptyOnionPacket, None, accountable = false, None)
val failure = LocalFailure(outgoingAmount, Nil, HtlcOverriddenByLocalCommit(randomBytes32(), downstreamHtlc))
- nodeRelayerAdapters ! PaymentFailed(relayId, incomingMultiPart.head.add.paymentHash, Seq(failure))
+ nodeRelayerAdapters ! PaymentFailed(relayId, incomingMultiPart.head.add.paymentHash, Seq(failure), 100 unixms, 150 unixms)
eventListener.expectNoMessage(100 millis) // the payment didn't succeed, but didn't fail either, so we just ignore it
parent.expectMessageType[NodeRelayer.RelayComplete]
register.expectNoMessage(100 millis)
@@ -738,7 +738,7 @@ class NodeRelayerSpec extends ScalaTestWithActorTestKit(ConfigFactory.load("appl
validateOutgoingPayment(outgoingPayment)
// The outgoing payment fails because we don't have enough balance: we trigger on-the-fly funding.
- outgoingPayment.replyTo ! PaymentFailed(relayId, paymentHash, LocalFailure(outgoingAmount, Nil, BalanceTooLow) :: Nil)
+ outgoingPayment.replyTo ! PaymentFailed(relayId, paymentHash, LocalFailure(outgoingAmount, Nil, BalanceTooLow) :: Nil, 100 unixms, 150 unixms)
val fwd = register.expectMessageType[Register.ForwardNodeId[Peer.ProposeOnTheFlyFunding]]
assert(fwd.nodeId == outgoingNodeId)
assert(fwd.message.nextPathKey_opt.isEmpty)
@@ -784,7 +784,7 @@ class NodeRelayerSpec extends ScalaTestWithActorTestKit(ConfigFactory.load("appl
validateOutgoingPayment(outgoingPayment)
// The outgoing payment fails, but it's not a liquidity issue.
- outgoingPayment.replyTo ! PaymentFailed(relayId, paymentHash, RemoteFailure(outgoingAmount, Nil, Sphinx.DecryptedFailurePacket(outgoingNodeId, 1, TemporaryNodeFailure())) :: Nil)
+ outgoingPayment.replyTo ! PaymentFailed(relayId, paymentHash, RemoteFailure(outgoingAmount, Nil, Sphinx.DecryptedFailurePacket(outgoingNodeId, 1, TemporaryNodeFailure()), 100 unixms, 150 unixms) :: Nil, 100 unixms, 150 unixms)
incomingMultiPart.foreach { p =>
val fwd = register.expectMessageType[Register.Forward[CMD_FAIL_HTLC]]
assert(fwd.channelId == p.add.channelId)
@@ -1039,7 +1039,7 @@ class NodeRelayerSpec extends ScalaTestWithActorTestKit(ConfigFactory.load("appl
val outgoingPayment = mockPayFSM.expectMessageType[SendMultiPartPayment]
// The outgoing payment fails because we don't have enough balance: we trigger on-the-fly funding.
- outgoingPayment.replyTo ! PaymentFailed(relayId, paymentHash, LocalFailure(outgoingAmount, Nil, BalanceTooLow) :: Nil)
+ outgoingPayment.replyTo ! PaymentFailed(relayId, paymentHash, LocalFailure(outgoingAmount, Nil, BalanceTooLow) :: Nil, 100 unixms, 150 unixms)
val fwd = register.expectMessageType[Register.ForwardNodeId[Peer.ProposeOnTheFlyFunding]]
assert(fwd.nodeId == outgoingNodeId)
assert(fwd.message.nextPathKey_opt.nonEmpty)
@@ -1078,7 +1078,7 @@ class NodeRelayerSpec extends ScalaTestWithActorTestKit(ConfigFactory.load("appl
val outgoingPayment = mockPayFSM.expectMessageType[SendMultiPartPayment]
// The outgoing payment fails because we don't have enough balance: we trigger on-the-fly funding, but can't reach our peer.
- outgoingPayment.replyTo ! PaymentFailed(relayId, paymentHash, LocalFailure(outgoingAmount, Nil, BalanceTooLow) :: Nil)
+ outgoingPayment.replyTo ! PaymentFailed(relayId, paymentHash, LocalFailure(outgoingAmount, Nil, BalanceTooLow) :: Nil, 100 unixms, 110 unixms)
val fwd = register.expectMessageType[Register.ForwardNodeId[Peer.ProposeOnTheFlyFunding]]
fwd.message.replyTo ! Peer.ProposeOnTheFlyFundingResponse.NotAvailable("peer disconnected")
// We fail the payments immediately since the recipient isn't available.
@@ -1185,25 +1185,25 @@ class NodeRelayerSpec extends ScalaTestWithActorTestKit(ConfigFactory.load("appl
object NodeRelayerSpec {
- val relayId = UUID.randomUUID()
+ val relayId: UUID = UUID.randomUUID()
- val paymentPreimage = randomBytes32()
- val paymentHash = Crypto.sha256(paymentPreimage)
- val paymentSecret = randomBytes32()
+ val paymentPreimage: ByteVector32 = randomBytes32()
+ val paymentHash: ByteVector32 = Crypto.sha256(paymentPreimage)
+ val paymentSecret: ByteVector32 = randomBytes32()
- val outgoingAmount = 40_000_000 msat
- val outgoingExpiry = CltvExpiry(490000)
- val outgoingNodeKey = randomKey()
- val outgoingNodeId = outgoingNodeKey.publicKey
+ val outgoingAmount: MilliSatoshi = 40_000_000 msat
+ val outgoingExpiry: CltvExpiry = CltvExpiry(490000)
+ val outgoingNodeKey: PrivateKey = randomKey()
+ val outgoingNodeId: PublicKey = outgoingNodeKey.publicKey
- val incomingAmount = 41_000_000 msat
- val incomingSecret = randomBytes32()
- val incomingMultiPart = Seq(
- createValidIncomingPacket(15_000_000 msat, incomingAmount, CltvExpiry(500000), outgoingAmount, outgoingExpiry, TimestampMilli(1000), accountableIn = false),
- createValidIncomingPacket(15_000_000 msat, incomingAmount, CltvExpiry(499999), outgoingAmount, outgoingExpiry, TimestampMilli(2000), accountableIn = false),
- createValidIncomingPacket(11_000_000 msat, incomingAmount, CltvExpiry(499999), outgoingAmount, outgoingExpiry, TimestampMilli(3000), accountableIn = false)
+ val incomingAmount: MilliSatoshi = 41_000_000 msat
+ val incomingSecret: ByteVector32 = randomBytes32()
+ val incomingMultiPart: Seq[RelayToTrampolinePacket] = Seq(
+ createValidIncomingPacket(15_000_000 msat, incomingAmount, CltvExpiry(500000), outgoingAmount, outgoingExpiry, TimestampMilli(1000)),
+ createValidIncomingPacket(15_000_000 msat, incomingAmount, CltvExpiry(499999), outgoingAmount, outgoingExpiry, TimestampMilli(2000)),
+ createValidIncomingPacket(11_000_000 msat, incomingAmount, CltvExpiry(499999), outgoingAmount, outgoingExpiry, TimestampMilli(3000))
)
- val incomingSinglePart = createValidIncomingPacket(incomingAmount, incomingAmount, CltvExpiry(500000), outgoingAmount, outgoingExpiry, TimestampMilli(5000))
+ val incomingSinglePart: RelayToTrampolinePacket = createValidIncomingPacket(incomingAmount, incomingAmount, CltvExpiry(500000), outgoingAmount, outgoingExpiry, TimestampMilli(5000))
val incomingAsyncPayment: Seq[RelayToTrampolinePacket] = incomingMultiPart.map(p => p.copy(innerPayload = IntermediatePayload.NodeRelay.Standard.createNodeRelayForAsyncPayment(p.innerPayload.amountToForward, p.innerPayload.outgoingCltv, outgoingNodeId, upgradeAccountability = false)))
def asyncTimeoutHeight(nodeParams: NodeParams): BlockHeight =
@@ -1213,7 +1213,7 @@ object NodeRelayerSpec {
(paymentPackets.map(_.outerPayload.expiry).min - nodeParams.relayParams.asyncPaymentsParams.cancelSafetyBeforeTimeout).blockHeight
def createSuccessEvent(): PaymentSent =
- PaymentSent(relayId, paymentHash, paymentPreimage, outgoingAmount, outgoingNodeId, Seq(PaymentSent.PartialPayment(UUID.randomUUID(), outgoingAmount, 10 msat, randomBytes32(), None)), None)
+ PaymentSent(relayId, paymentPreimage, outgoingAmount, outgoingNodeId, Seq(PaymentSent.PartialPayment(UUID.randomUUID(), outgoingAmount, 10 msat, randomBytes32(), None, 0 unixms, 50 unixms)), None, 0 unixms)
def createTrampolinePacket(amount: MilliSatoshi, expiry: CltvExpiry): OnionRoutingPacket = {
val payload = NodePayload(outgoingNodeId, FinalPayload.Standard.createPayload(amount, amount, expiry, paymentSecret, upgradeAccountability = false))
diff --git a/eclair-node/src/test/scala/fr/acinq/eclair/api/ApiServiceSpec.scala b/eclair-node/src/test/scala/fr/acinq/eclair/api/ApiServiceSpec.scala
index f71e03e..50e8cc5 100644
--- a/eclair-node/src/test/scala/fr/acinq/eclair/api/ApiServiceSpec.scala
+++ b/eclair-node/src/test/scala/fr/acinq/eclair/api/ApiServiceSpec.scala
@@ -623,7 +623,7 @@ class ApiServiceSpec extends AnyFunSuite with ScalatestRouteTest with IdiomaticM
val mockService = new MockService(eclair)
val uuid = UUID.fromString("487da196-a4dc-4b1e-92b4-3e5e905e9f3f")
- val paymentSent = PaymentSent(uuid, ByteVector32.Zeroes, ByteVector32.One, 25 msat, aliceNodeId, Seq(PaymentSent.PartialPayment(uuid, 21 msat, 1 msat, ByteVector32.Zeroes, None, TimestampMilli(1553784337711L))), None)
+ val paymentSent = PaymentSent(uuid, ByteVector32.One, 25 msat, aliceNodeId, Seq(PaymentSent.PartialPayment(uuid, 28 msat, 1 msat, ByteVector32.Zeroes, None, TimestampMilli(1553784337650L), TimestampMilli(1553784337711L))), None, TimestampMilli(1553784337120L))
eclair.sendBlocking(any, any, any, any, any, any, any)(any[Timeout]).returns(Future.successful(paymentSent))
Post("/payinvoice", FormData("invoice" -> invoice, "blocking" -> "true").toEntity) ~>
addCredentials(BasicHttpCredentials("", mockApi().password)) ~>
@@ -632,11 +632,11 @@ class ApiServiceSpec extends AnyFunSuite with ScalatestRouteTest with IdiomaticM
assert(handled)
assert(status == OK)
val response = entityAs[String]
- val expected = """{"type":"payment-sent","id":"487da196-a4dc-4b1e-92b4-3e5e905e9f3f","paymentHash":"0000000000000000000000000000000000000000000000000000000000000000","paymentPreimage":"0100000000000000000000000000000000000000000000000000000000000000","recipientAmount":25,"recipientNodeId":"03af0ed6052cf28d670665549bc86f4b721c9fdb309d40c58f5811f63966e005d0","parts":[{"id":"487da196-a4dc-4b1e-92b4-3e5e905e9f3f","amount":21,"feesPaid":1,"toChannelId":"0000000000000000000000000000000000000000000000000000000000000000","timestamp":{"iso":"2019-03-28T14:45:37.711Z","unix":1553784337}}]}"""
+ val expected = """{"type":"payment-sent","id":"487da196-a4dc-4b1e-92b4-3e5e905e9f3f","paymentHash":"01d0fabd251fcbbe2b93b4b927b26ad2a1a99077152e45ded1e678afa45dbec5","paymentPreimage":"0100000000000000000000000000000000000000000000000000000000000000","recipientAmount":25,"recipientNodeId":"03af0ed6052cf28d670665549bc86f4b721c9fdb309d40c58f5811f63966e005d0","parts":[{"id":"487da196-a4dc-4b1e-92b4-3e5e905e9f3f","amount":28,"feesPaid":1,"toChannelId":"0000000000000000000000000000000000000000000000000000000000000000","startedAt":{"iso":"2019-03-28T14:45:37.650Z","unix":1553784337},"settledAt":{"iso":"2019-03-28T14:45:37.711Z","unix":1553784337}}],"fees":4,"startedAt":{"iso":"2019-03-28T14:45:37.120Z","unix":1553784337},"settledAt":{"iso":"2019-03-28T14:45:37.711Z","unix":1553784337}}"""
assert(response == expected)
}
- val paymentFailed = PaymentFailed(uuid, ByteVector32.Zeroes, failures = Seq.empty, timestamp = TimestampMilli(1553784963659L))
+ val paymentFailed = PaymentFailed(uuid, ByteVector32.Zeroes, failures = Seq.empty, startedAt = TimestampMilli(1553784963507L), settledAt = TimestampMilli(1553784963659L))
eclair.sendBlocking(any, any, any, any, any, any, any)(any[Timeout]).returns(Future.successful(paymentFailed))
Post("/payinvoice", FormData("invoice" -> invoice, "blocking" -> "true").toEntity) ~>
addCredentials(BasicHttpCredentials("", mockApi().password)) ~>
@@ -645,7 +645,7 @@ class ApiServiceSpec extends AnyFunSuite with ScalatestRouteTest with IdiomaticM
assert(handled)
assert(status == OK)
val response = entityAs[String]
- val expected = """{"type":"payment-failed","id":"487da196-a4dc-4b1e-92b4-3e5e905e9f3f","paymentHash":"0000000000000000000000000000000000000000000000000000000000000000","failures":[],"timestamp":{"iso":"2019-03-28T14:56:03.659Z","unix":1553784963}}"""
+ val expected = """{"type":"payment-failed","id":"487da196-a4dc-4b1e-92b4-3e5e905e9f3f","paymentHash":"0000000000000000000000000000000000000000000000000000000000000000","failures":[],"startedAt":{"iso":"2019-03-28T14:56:03.507Z","unix":1553784963},"settledAt":{"iso":"2019-03-28T14:56:03.659Z","unix":1553784963}}"""
assert(response == expected)
}
}
@@ -1109,7 +1109,6 @@ class ApiServiceSpec extends AnyFunSuite with ScalatestRouteTest with IdiomaticM
assert(status == OK)
eclair.audit(123456 unixsec, 654321 unixsec, Some(Paginated(count = 1, skip = 2)))(any[Timeout]).wasCalled(once)
}
-
}
test("the websocket should return typed objects") {
@@ -1122,20 +1121,20 @@ class ApiServiceSpec extends AnyFunSuite with ScalatestRouteTest with IdiomaticM
addCredentials(BasicHttpCredentials("", mockApi().password)) ~>
mockService.webSocket ~>
check {
- val pf = PaymentFailed(fixedUUID, ByteVector32.Zeroes, failures = Seq.empty, timestamp = TimestampMilli(1553784963659L))
- val expectedSerializedPf = """{"type":"payment-failed","id":"487da196-a4dc-4b1e-92b4-3e5e905e9f3f","paymentHash":"0000000000000000000000000000000000000000000000000000000000000000","failures":[],"timestamp":{"iso":"2019-03-28T14:56:03.659Z","unix":1553784963}}"""
+ val pf = PaymentFailed(fixedUUID, ByteVector32.Zeroes, failures = Seq.empty, startedAt = TimestampMilli(1553784963221L), settledAt = TimestampMilli(1553784963659L))
+ val expectedSerializedPf = """{"type":"payment-failed","id":"487da196-a4dc-4b1e-92b4-3e5e905e9f3f","paymentHash":"0000000000000000000000000000000000000000000000000000000000000000","failures":[],"startedAt":{"iso":"2019-03-28T14:56:03.221Z","unix":1553784963},"settledAt":{"iso":"2019-03-28T14:56:03.659Z","unix":1553784963}}"""
assert(serialization.write(pf) == expectedSerializedPf)
system.eventStream.publish(pf)
wsClient.expectMessage(expectedSerializedPf)
- val ps = PaymentSent(fixedUUID, ByteVector32.Zeroes, ByteVector32.One, 25 msat, aliceNodeId, Seq(PaymentSent.PartialPayment(fixedUUID, 21 msat, 1 msat, ByteVector32.Zeroes, None, TimestampMilli(1553784337711L))), None)
- val expectedSerializedPs = """{"type":"payment-sent","id":"487da196-a4dc-4b1e-92b4-3e5e905e9f3f","paymentHash":"0000000000000000000000000000000000000000000000000000000000000000","paymentPreimage":"0100000000000000000000000000000000000000000000000000000000000000","recipientAmount":25,"recipientNodeId":"03af0ed6052cf28d670665549bc86f4b721c9fdb309d40c58f5811f63966e005d0","parts":[{"id":"487da196-a4dc-4b1e-92b4-3e5e905e9f3f","amount":21,"feesPaid":1,"toChannelId":"0000000000000000000000000000000000000000000000000000000000000000","timestamp":{"iso":"2019-03-28T14:45:37.711Z","unix":1553784337}}]}"""
+ val ps = PaymentSent(fixedUUID, ByteVector32.One, 25 msat, aliceNodeId, Seq(PaymentSent.PartialPayment(fixedUUID, 28 msat, 1 msat, ByteVector32.Zeroes, None, startedAt = TimestampMilli(1553784337539L), settledAt = TimestampMilli(1553784337711L))), None, startedAt = TimestampMilli(1553784337073L))
+ val expectedSerializedPs = """{"type":"payment-sent","id":"487da196-a4dc-4b1e-92b4-3e5e905e9f3f","paymentHash":"01d0fabd251fcbbe2b93b4b927b26ad2a1a99077152e45ded1e678afa45dbec5","paymentPreimage":"0100000000000000000000000000000000000000000000000000000000000000","recipientAmount":25,"recipientNodeId":"03af0ed6052cf28d670665549bc86f4b721c9fdb309d40c58f5811f63966e005d0","parts":[{"id":"487da196-a4dc-4b1e-92b4-3e5e905e9f3f","amount":28,"feesPaid":1,"toChannelId":"0000000000000000000000000000000000000000000000000000000000000000","startedAt":{"iso":"2019-03-28T14:45:37.539Z","unix":1553784337},"settledAt":{"iso":"2019-03-28T14:45:37.711Z","unix":1553784337}}],"fees":4,"startedAt":{"iso":"2019-03-28T14:45:37.073Z","unix":1553784337},"settledAt":{"iso":"2019-03-28T14:45:37.711Z","unix":1553784337}}"""
assert(serialization.write(ps) == expectedSerializedPs)
system.eventStream.publish(ps)
wsClient.expectMessage(expectedSerializedPs)
val prel = ChannelPaymentRelayed(21 msat, 20 msat, ByteVector32.Zeroes, ByteVector32.Zeroes, ByteVector32.One, TimestampMilli(1553784961048L), TimestampMilli(1553784963659L))
- val expectedSerializedPrel = """{"type":"payment-relayed","amountIn":21,"amountOut":20,"paymentHash":"0000000000000000000000000000000000000000000000000000000000000000","fromChannelId":"0000000000000000000000000000000000000000000000000000000000000000","toChannelId":"0100000000000000000000000000000000000000000000000000000000000000","receivedAt":{"iso":"2019-03-28T14:56:01.048Z","unix":1553784961},"settledAt":{"iso":"2019-03-28T14:56:03.659Z","unix":1553784963}}"""
+ val expectedSerializedPrel = """{"type":"payment-relayed","amountIn":21,"amountOut":20,"paymentHash":"0000000000000000000000000000000000000000000000000000000000000000","fromChannelId":"0000000000000000000000000000000000000000000000000000000000000000","toChannelId":"0100000000000000000000000000000000000000000000000000000000000000","startedAt":{"iso":"2019-03-28T14:56:01.048Z","unix":1553784961},"settledAt":{"iso":"2019-03-28T14:56:03.659Z","unix":1553784963}}"""
assert(serialization.write(prel) == expectedSerializedPrel)
system.eventStream.publish(prel)
wsClient.expectMessage(expectedSerializedPrel)
@@ -1147,17 +1146,11 @@ class ApiServiceSpec extends AnyFunSuite with ScalatestRouteTest with IdiomaticM
wsClient.expectMessage(expectedSerializedPtrel)
val precv = PaymentReceived(ByteVector32.Zeroes, Seq(PaymentReceived.PartialPayment(21 msat, ByteVector32.Zeroes, TimestampMilli(1553784963659L))))
- val expectedSerializedPrecv = """{"type":"payment-received","paymentHash":"0000000000000000000000000000000000000000000000000000000000000000","parts":[{"amount":21,"fromChannelId":"0000000000000000000000000000000000000000000000000000000000000000","timestamp":{"iso":"2019-03-28T14:56:03.659Z","unix":1553784963}}]}"""
+ val expectedSerializedPrecv = """{"type":"payment-received","paymentHash":"0000000000000000000000000000000000000000000000000000000000000000","parts":[{"amount":21,"fromChannelId":"0000000000000000000000000000000000000000000000000000000000000000","receivedAt":{"iso":"2019-03-28T14:56:03.659Z","unix":1553784963}}]}"""
assert(serialization.write(precv) == expectedSerializedPrecv)
system.eventStream.publish(precv)
wsClient.expectMessage(expectedSerializedPrecv)
- val pset = PaymentSettlingOnChain(fixedUUID, 21 msat, ByteVector32.One, timestamp = TimestampMilli(1553785442676L))
- val expectedSerializedPset = """{"type":"payment-settling-onchain","id":"487da196-a4dc-4b1e-92b4-3e5e905e9f3f","amount":21,"paymentHash":"0100000000000000000000000000000000000000000000000000000000000000","timestamp":{"iso":"2019-03-28T15:04:02.676Z","unix":1553785442}}"""
- assert(serialization.write(pset) == expectedSerializedPset)
- system.eventStream.publish(pset)
- wsClient.expectMessage(expectedSerializedPset)
-
val chcr = ChannelCreated(system.deadLetters, system.deadLetters, bobNodeId, isOpener = true, ByteVector32.One, FeeratePerKw(25 sat), Some(FeeratePerKw(20 sat)))
val expectedSerializedChcr = """{"type":"channel-created","remoteNodeId":"039dc0e0b1d25905e44fdf6f8e89755a5e219685840d0bc1d28d3308f9628a3585","isOpener":true,"temporaryChannelId":"0100000000000000000000000000000000000000000000000000000000000000","commitTxFeeratePerKw":25,"fundingTxFeeratePerKw":20}"""
assert(serialization.write(chcr) == expectedSerializedChcr)
Why this scored 19/100
Community notes
Notes can correct, qualify, or add evidence to the AI analysis. Every note shown here has been validated by a human moderator.
The AI analysis stands alone for now. Submit a note if you can add evidence or important context.