From e3ce646500eea6e1c2cc80520b60b7c1b4f10dc6 Mon Sep 17 00:00:00 2001 From: t-bast Date: Fri, 23 Jan 2026 16:01:12 +0100 Subject: [PATCH 1/2] Include the `node_id` of channel peers in payment events When scoring peers, we don't really care about individual channels but rather about the peer itself, who is identified by its `node_id`. We update payment events to include the `node_id` of our channel peer, to make it easier to compute statistics about each of our peers. We also rework the events in `PaymentEvents.scala` to be more consistent now that we've added support for trampoline, splicing, liquidity ads and on-the-fly funding, which are all the features we had planned that can impact peer scoring. Note that we don't yet update the schema of the `AuditDb`, which means that some of the data isn't stored and is currently filled with a dummy value. We will include *all* updates to the DB schema in a future PR and will fix this. --- .../fr/acinq/eclair/channel/ChannelData.scala | 10 +- .../fr/acinq/eclair/channel/Commitments.scala | 9 +- .../fr/acinq/eclair/channel/fsm/Channel.scala | 18 +-- .../fr/acinq/eclair/db/DbEventHandler.scala | 10 +- .../fr/acinq/eclair/db/pg/PgAuditDb.scala | 52 +++++---- .../eclair/db/sqlite/SqliteAuditDb.scala | 50 ++++---- .../main/scala/fr/acinq/eclair/io/Peer.scala | 10 +- .../acinq/eclair/json/JsonSerializers.scala | 12 +- .../acinq/eclair/payment/PaymentEvents.scala | 104 +++++++++-------- .../payment/receive/MultiPartHandler.scala | 43 +++---- .../payment/receive/MultiPartPaymentFSM.scala | 9 +- .../eclair/payment/relay/ChannelRelay.scala | 10 +- .../eclair/payment/relay/NodeRelay.scala | 6 +- .../relay/PostRestartHtlcCleaner.scala | 52 ++++----- .../acinq/eclair/payment/relay/Relayer.scala | 3 +- .../send/MultiPartPaymentLifecycle.scala | 2 +- .../payment/send/PaymentLifecycle.scala | 8 +- .../send/TrampolinePaymentLifecycle.scala | 7 +- .../channel/version5/ChannelCodecs5.scala | 16 ++- .../fr/acinq/eclair/channel/FuzzySpec.scala | 4 +- .../channel/states/h/ClosingStateSpec.scala | 6 +- .../fr/acinq/eclair/db/AuditDbSpec.scala | 53 +++++---- .../fr/acinq/eclair/db/PaymentsDbSpec.scala | 8 +- .../fr/acinq/eclair/db/PgUtilsSpec.scala | 4 +- .../integration/ChannelIntegrationSpec.scala | 20 ++-- .../integration/PaymentIntegrationSpec.scala | 2 +- .../eclair/json/JsonSerializersSpec.scala | 14 +-- .../eclair/payment/MultiPartHandlerSpec.scala | 107 ++++++++++-------- .../payment/MultiPartPaymentFSMSpec.scala | 9 +- .../MultiPartPaymentLifecycleSpec.scala | 6 +- .../eclair/payment/PaymentInitiatorSpec.scala | 4 +- .../eclair/payment/PaymentLifecycleSpec.scala | 5 +- .../payment/PostRestartHtlcCleanerSpec.scala | 66 +++++------ .../payment/relay/ChannelRelayerSpec.scala | 18 +-- .../payment/relay/NodeRelayerSpec.scala | 2 +- .../payment/relay/OnTheFlyFundingSpec.scala | 12 +- .../eclair/payment/relay/RelayerSpec.scala | 33 +++--- .../internal/channel/ChannelCodecsSpec.scala | 5 +- .../fr/acinq/eclair/api/ApiServiceSpec.scala | 23 ++-- 39 files changed, 449 insertions(+), 383 deletions(-) diff --git a/eclair-core/src/main/scala/fr/acinq/eclair/channel/ChannelData.scala b/eclair-core/src/main/scala/fr/acinq/eclair/channel/ChannelData.scala index 0c1dc2f31f..548ae9497f 100644 --- a/eclair-core/src/main/scala/fr/acinq/eclair/channel/ChannelData.scala +++ b/eclair-core/src/main/scala/fr/acinq/eclair/channel/ChannelData.scala @@ -181,14 +181,14 @@ object Upstream { object Cold { def apply(hot: Hot): Cold = hot match { case Local(id) => Local(id) - case Hot.Channel(add, _, _, _) => Cold.Channel(add.channelId, add.id, add.amountMsat) - case Hot.Trampoline(received) => Cold.Trampoline(received.map(r => Cold.Channel(r.add.channelId, r.add.id, r.add.amountMsat))) + case Hot.Channel(add, _, receivedFrom, _) => Cold.Channel(add.channelId, receivedFrom, add.id, add.amountMsat) + case Hot.Trampoline(received) => Cold.Trampoline(received.map(r => Cold.Channel(r.add.channelId, r.receivedFrom, r.add.id, r.add.amountMsat))) } /** Our node is forwarding a single incoming HTLC. */ - case class Channel(originChannelId: ByteVector32, originHtlcId: Long, amountIn: MilliSatoshi) extends Cold + case class Channel(originChannelId: ByteVector32, originNodeId: PublicKey, originHtlcId: Long, amountIn: MilliSatoshi) extends Cold object Channel { - def apply(add: UpdateAddHtlc): Channel = Channel(add.channelId, add.id, add.amountMsat) + def apply(add: UpdateAddHtlc, remoteNodeId: PublicKey): Channel = Channel(add.channelId, remoteNodeId, add.id, add.amountMsat) } /** Our node is forwarding a payment based on a set of HTLCs from potentially multiple upstream channels. */ @@ -313,7 +313,7 @@ object HtlcResult { case object ChannelFailureBeforeSigned extends Fail case class DisconnectedBeforeSigned(channelUpdate: ChannelUpdate) extends Fail { require(!channelUpdate.channelFlags.isEnabled, "channel update must have disabled flag set") } } -final case class RES_ADD_SETTLED[+O <: Origin, +R <: HtlcResult](origin: O, htlc: UpdateAddHtlc, result: R) extends CommandSuccess[CMD_ADD_HTLC] +final case class RES_ADD_SETTLED[+O <: Origin, +R <: HtlcResult](origin: O, remoteNodeId: PublicKey, htlc: UpdateAddHtlc, result: R) extends CommandSuccess[CMD_ADD_HTLC] /** other specific responses */ final case class RES_BUMP_FUNDING_FEE(rbfIndex: Int, fundingTxId: TxId, fee: Satoshi) extends CommandSuccess[CMD_BUMP_FUNDING_FEE] diff --git a/eclair-core/src/main/scala/fr/acinq/eclair/channel/Commitments.scala b/eclair-core/src/main/scala/fr/acinq/eclair/channel/Commitments.scala index 383c031f9a..901f45ec85 100644 --- a/eclair-core/src/main/scala/fr/acinq/eclair/channel/Commitments.scala +++ b/eclair-core/src/main/scala/fr/acinq/eclair/channel/Commitments.scala @@ -12,7 +12,6 @@ import fr.acinq.eclair.channel.fsm.Channel.ChannelConf import fr.acinq.eclair.crypto.keymanager.{ChannelKeys, LocalCommitmentKeys, RemoteCommitmentKeys} import fr.acinq.eclair.crypto.{NonceGenerator, ShaChain} import fr.acinq.eclair.payment.OutgoingPaymentPacket -import fr.acinq.eclair.reputation.Reputation import fr.acinq.eclair.router.Announcements import fr.acinq.eclair.transactions.Transactions._ import fr.acinq.eclair.transactions._ @@ -460,7 +459,7 @@ case class Commitment(fundingTxIndex: Long, localCommit.spec.htlcs.collect(DirectedHtlc.incoming).filter(nearlyExpired) } - def canSendAdd(amount: MilliSatoshi, params: ChannelParams, changes: CommitmentChanges, feeConf: OnChainFeeConf, reputationScore: Reputation.Score): Either[ChannelException, Unit] = { + def canSendAdd(amount: MilliSatoshi, params: ChannelParams, changes: CommitmentChanges, feeConf: OnChainFeeConf): Either[ChannelException, Unit] = { // let's compute the current commitments *as seen by them* with the additional htlc // we need to base the next current commitment on the last sig we sent, even if we didn't yet receive their revocation val remoteCommit1 = nextRemoteCommit_opt.getOrElse(remoteCommit) @@ -901,7 +900,7 @@ case class Commitments(channelParams: ChannelParams, val changes1 = changes.addLocalProposal(add).copy(localNextHtlcId = changes.localNextHtlcId + 1) val originChannels1 = originChannels + (add.id -> cmd.origin) // we verify that this htlc is allowed in every active commitment - val failures = active.map(_.canSendAdd(add.amountMsat, channelParams, changes1, feeConf, cmd.reputationScore)) + val failures = active.map(_.canSendAdd(add.amountMsat, channelParams, changes1, feeConf)) // and that we don't exceed the authorized channel occupancy (jamming) .appended(cmd.reputationScore.checkIncomingChannelOccupancy(cmd.origin.upstream.incomingChannelOccupancy, channelId)) .collect { case Left(f) => f } @@ -1144,12 +1143,12 @@ case class Commitments(channelParams: ChannelParams, case fail: UpdateFailHtlc => val origin = originChannels(fail.id) val add = remoteSpec.findIncomingHtlcById(fail.id).map(_.add).get - RES_ADD_SETTLED(origin, add, HtlcResult.RemoteFail(fail)) + RES_ADD_SETTLED(origin, remoteNodeId, add, HtlcResult.RemoteFail(fail)) // same as above case fail: UpdateFailMalformedHtlc => val origin = originChannels(fail.id) val add = remoteSpec.findIncomingHtlcById(fail.id).map(_.add).get - RES_ADD_SETTLED(origin, add, HtlcResult.RemoteFailMalformed(fail)) + RES_ADD_SETTLED(origin, remoteNodeId, add, HtlcResult.RemoteFailMalformed(fail)) } val (acceptedHtlcs, rejectedHtlcs) = { // the received htlcs have already been added to commitments (they've been signed by our peer), and may already 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 c6e138a759..699cd5ab86 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 @@ -510,7 +510,7 @@ class Channel(val nodeParams: NodeParams, val channelKeys: ChannelKeys, val wall // We forward preimages as soon as possible to the upstream channel because it allows us to pull funds. msg match { case fulfill: UpdateFulfillHtlc => d.commitments.receiveFulfill(fulfill) match { - case Right((_, origin, htlc)) => relayer ! RES_ADD_SETTLED(origin, htlc, HtlcResult.RemoteFulfill(fulfill)) + case Right((_, origin, htlc)) => relayer ! RES_ADD_SETTLED(origin, remoteNodeId, htlc, HtlcResult.RemoteFulfill(fulfill)) case _ => () } case _ => () @@ -562,7 +562,7 @@ class Channel(val nodeParams: NodeParams, val channelKeys: ChannelKeys, val wall d.commitments.receiveFulfill(fulfill) match { case Right((commitments1, origin, htlc)) => // we forward preimages as soon as possible to the upstream channel because it allows us to pull funds - relayer ! RES_ADD_SETTLED(origin, htlc, HtlcResult.RemoteFulfill(fulfill)) + relayer ! RES_ADD_SETTLED(origin, remoteNodeId, htlc, HtlcResult.RemoteFulfill(fulfill)) context.system.eventStream.publish(OutgoingHtlcFulfilled(fulfill)) log.info("OutgoingHtlcFulfilled: channelId={}, id={}", fulfill.channelId.toHex, fulfill.id) stay() using d.copy(commitments = commitments1) @@ -1562,7 +1562,7 @@ class Channel(val nodeParams: NodeParams, val channelKeys: ChannelKeys, val wall val channelUpdate1 = Helpers.channelUpdate(nodeParams, scidForChannelUpdate(d), d.commitments, d.channelUpdate.relayFees, enable = false) // NB: the htlcs stay in the commitments.localChange, they will be cleaned up after reconnection d.commitments.changes.localChanges.proposed.collect { - case add: UpdateAddHtlc => relayer ! RES_ADD_SETTLED(d.commitments.originChannels(add.id), add, HtlcResult.DisconnectedBeforeSigned(channelUpdate1)) + case add: UpdateAddHtlc => relayer ! RES_ADD_SETTLED(d.commitments.originChannels(add.id), remoteNodeId, add, HtlcResult.DisconnectedBeforeSigned(channelUpdate1)) } goto(OFFLINE) using d1.copy(channelUpdate = channelUpdate1) storing() } else { @@ -1599,7 +1599,7 @@ class Channel(val nodeParams: NodeParams, val channelKeys: ChannelKeys, val wall d.commitments.receiveFulfill(fulfill) match { case Right((commitments1, origin, htlc)) => // we forward preimages as soon as possible to the upstream channel because it allows us to pull funds - relayer ! RES_ADD_SETTLED(origin, htlc, HtlcResult.RemoteFulfill(fulfill)) + relayer ! RES_ADD_SETTLED(origin, remoteNodeId, htlc, HtlcResult.RemoteFulfill(fulfill)) stay() using d.copy(commitments = commitments1) case Left(cause) => handleLocalError(cause, d, Some(fulfill)) } @@ -2153,7 +2153,7 @@ class Channel(val nodeParams: NodeParams, val channelKeys: ChannelKeys, val wall d.commitments.originChannels.get(add.id) match { case Some(origin) => log.info("failing htlc #{} paymentHash={} origin={}: overridden by revoked remote commit", add.id, add.paymentHash, origin) - relayer ! RES_ADD_SETTLED(origin, add, HtlcResult.OnChainFail(HtlcOverriddenByLocalCommit(d.channelId, add))) + relayer ! RES_ADD_SETTLED(origin, remoteNodeId, add, HtlcResult.OnChainFail(HtlcOverriddenByLocalCommit(d.channelId, add))) case None => () } } @@ -2176,7 +2176,7 @@ class Channel(val nodeParams: NodeParams, val channelKeys: ChannelKeys, val wall d.commitments.originChannels.get(htlc.id) match { case Some(origin) => log.info("fulfilling htlc #{} paymentHash={} origin={}", htlc.id, htlc.paymentHash, origin) - relayer ! RES_ADD_SETTLED(origin, htlc, HtlcResult.OnChainFulfill(preimage)) + relayer ! RES_ADD_SETTLED(origin, remoteNodeId, htlc, HtlcResult.OnChainFulfill(preimage)) case None => // If we don't have the origin, it means that we already have forwarded the fulfill so that's not a big deal. // This can happen if they send a signature containing the fulfill, then fail the channel before we have time to sign it. @@ -2254,7 +2254,7 @@ class Channel(val nodeParams: NodeParams, val channelKeys: ChannelKeys, val wall d.commitments.originChannels.get(add.id) match { case Some(origin) => log.info("failing htlc #{} paymentHash={} origin={}: htlc timed out", add.id, add.paymentHash, origin) - relayer ! RES_ADD_SETTLED(origin, add, HtlcResult.OnChainFail(HtlcsTimedoutDownstream(d.channelId, Set(add)))) + relayer ! RES_ADD_SETTLED(origin, remoteNodeId, add, HtlcResult.OnChainFail(HtlcsTimedoutDownstream(d.channelId, Set(add)))) case None => // same as for fulfilling the htlc (no big deal) log.info("cannot fail timed out htlc #{} paymentHash={} (origin not found)", add.id, add.paymentHash) @@ -2266,7 +2266,7 @@ class Channel(val nodeParams: NodeParams, val channelKeys: ChannelKeys, val wall d.commitments.originChannels.get(add.id) match { case Some(origin) => log.info("failing htlc #{} paymentHash={} origin={}: overridden by local commit", add.id, add.paymentHash, origin) - relayer ! RES_ADD_SETTLED(origin, add, HtlcResult.OnChainFail(HtlcOverriddenByLocalCommit(d.channelId, add))) + relayer ! RES_ADD_SETTLED(origin, remoteNodeId, add, HtlcResult.OnChainFail(HtlcOverriddenByLocalCommit(d.channelId, add))) case None => // same as for fulfilling the htlc (no big deal) log.info("cannot fail overridden htlc #{} paymentHash={} (origin not found)", add.id, add.paymentHash) @@ -3189,7 +3189,7 @@ class Channel(val nodeParams: NodeParams, val channelKeys: ChannelKeys, val wall nextStateData match { case d: DATA_CLOSING => d.commitments.changes.localChanges.proposed.collect { - case add: UpdateAddHtlc => relayer ! RES_ADD_SETTLED(d.commitments.originChannels(add.id), add, HtlcResult.ChannelFailureBeforeSigned) + case add: UpdateAddHtlc => relayer ! RES_ADD_SETTLED(d.commitments.originChannels(add.id), remoteNodeId, add, HtlcResult.ChannelFailureBeforeSigned) } case _ => () } diff --git a/eclair-core/src/main/scala/fr/acinq/eclair/db/DbEventHandler.scala b/eclair-core/src/main/scala/fr/acinq/eclair/db/DbEventHandler.scala index bc50eda18e..ba249a831d 100644 --- a/eclair-core/src/main/scala/fr/acinq/eclair/db/DbEventHandler.scala +++ b/eclair-core/src/main/scala/fr/acinq/eclair/db/DbEventHandler.scala @@ -63,7 +63,7 @@ class DbEventHandler(nodeParams: NodeParams) extends Actor with DiagnosticActorL PaymentMetrics.PaymentFees.withTag(PaymentTags.Direction, PaymentTags.Directions.Sent).record(e.feesPaid.truncateToSatoshi.toLong) PaymentMetrics.PaymentParts.withTag(PaymentTags.Direction, PaymentTags.Directions.Sent).record(e.parts.length) auditDb.add(e) - e.parts.foreach(p => channelsDb.updateChannelMeta(p.toChannelId, ChannelEvent.EventType.PaymentSent)) + e.parts.foreach(p => channelsDb.updateChannelMeta(p.channelId, ChannelEvent.EventType.PaymentSent)) case _: PaymentFailed => PaymentMetrics.PaymentFailed.withTag(PaymentTags.Direction, PaymentTags.Directions.Sent).increment() @@ -72,7 +72,7 @@ class DbEventHandler(nodeParams: NodeParams) extends Actor with DiagnosticActorL PaymentMetrics.PaymentAmount.withTag(PaymentTags.Direction, PaymentTags.Directions.Received).record(e.amount.truncateToSatoshi.toLong) PaymentMetrics.PaymentParts.withTag(PaymentTags.Direction, PaymentTags.Directions.Received).record(e.parts.length) auditDb.add(e) - e.parts.foreach(p => channelsDb.updateChannelMeta(p.fromChannelId, ChannelEvent.EventType.PaymentReceived)) + e.parts.foreach(p => channelsDb.updateChannelMeta(p.channelId, ChannelEvent.EventType.PaymentReceived)) case e: PaymentRelayed => PaymentMetrics.PaymentAmount @@ -89,9 +89,9 @@ class DbEventHandler(nodeParams: NodeParams) extends Actor with DiagnosticActorL PaymentMetrics.PaymentParts.withTag(PaymentTags.Direction, PaymentTags.Directions.Sent).record(outgoing.length) incoming.foreach(p => channelsDb.updateChannelMeta(p.channelId, ChannelEvent.EventType.PaymentReceived)) outgoing.foreach(p => channelsDb.updateChannelMeta(p.channelId, ChannelEvent.EventType.PaymentSent)) - case ChannelPaymentRelayed(_, _, _, fromChannelId, toChannelId, _, _) => - channelsDb.updateChannelMeta(fromChannelId, ChannelEvent.EventType.PaymentReceived) - channelsDb.updateChannelMeta(toChannelId, ChannelEvent.EventType.PaymentSent) + case ChannelPaymentRelayed(_, incoming, outgoing) => + channelsDb.updateChannelMeta(incoming.channelId, ChannelEvent.EventType.PaymentReceived) + channelsDb.updateChannelMeta(outgoing.channelId, ChannelEvent.EventType.PaymentSent) case OnTheFlyFundingPaymentRelayed(_, incoming, outgoing) => incoming.foreach(p => channelsDb.updateChannelMeta(p.channelId, ChannelEvent.EventType.PaymentReceived)) outgoing.foreach(p => channelsDb.updateChannelMeta(p.channelId, ChannelEvent.EventType.PaymentSent)) 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 3623a0cdb7..4af6b47752 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 @@ -16,7 +16,7 @@ package fr.acinq.eclair.db.pg -import fr.acinq.bitcoin.scalacompat.Crypto.PublicKey +import fr.acinq.bitcoin.scalacompat.Crypto.{PrivateKey, PublicKey} import fr.acinq.bitcoin.scalacompat.{ByteVector32, Satoshi, SatoshiLong, TxId} import fr.acinq.eclair.channel._ import fr.acinq.eclair.db.AuditDb.{NetworkFee, PublishedTransaction, Stats} @@ -206,7 +206,7 @@ class PgAuditDb(implicit ds: DataSource) extends AuditDb with Logging { inTransaction { pg => using(pg.prepareStatement("INSERT INTO audit.sent VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?)")) { statement => e.parts.foreach(p => { - statement.setLong(1, p.amount.toLong) + statement.setLong(1, p.amountWithFees.toLong) statement.setLong(2, p.feesPaid.toLong) statement.setLong(3, e.recipientAmount.toLong) statement.setString(4, p.id.toString) @@ -214,7 +214,7 @@ class PgAuditDb(implicit ds: DataSource) extends AuditDb with Logging { statement.setString(6, e.paymentHash.toHex) statement.setString(7, e.paymentPreimage.toHex) statement.setString(8, e.recipientNodeId.value.toHex) - statement.setString(9, p.toChannelId.toHex) + statement.setString(9, p.payment.channelId.toHex) statement.setTimestamp(10, p.settledAt.toSqlTimestamp) statement.addBatch() }) @@ -229,7 +229,7 @@ class PgAuditDb(implicit ds: DataSource) extends AuditDb with Logging { e.parts.foreach(p => { statement.setLong(1, p.amount.toLong) statement.setString(2, e.paymentHash.toHex) - statement.setString(3, p.fromChannelId.toHex) + statement.setString(3, p.channelId.toHex) statement.setTimestamp(4, p.receivedAt.toSqlTimestamp) statement.addBatch() }) @@ -241,10 +241,10 @@ class PgAuditDb(implicit ds: DataSource) extends AuditDb with Logging { override def add(e: PaymentRelayed): Unit = withMetrics("audit/add-payment-relayed", DbBackends.Postgres) { inTransaction { pg => val payments = e match { - case ChannelPaymentRelayed(amountIn, amountOut, _, fromChannelId, toChannelId, startedAt, settledAt) => + case e: ChannelPaymentRelayed => // non-trampoline relayed payments have one input and one output - val in = Seq(RelayedPart(fromChannelId, amountIn, "IN", "channel", startedAt)) - val out = Seq(RelayedPart(toChannelId, amountOut, "OUT", "channel", settledAt)) + val in = Seq(RelayedPart(e.paymentIn.channelId, e.paymentIn.amount, "IN", "channel", e.startedAt)) + val out = Seq(RelayedPart(e.paymentOut.channelId, e.paymentOut.amount, "OUT", "channel", e.settledAt)) in ++ out case TrampolinePaymentRelayed(_, incoming, outgoing, nextTrampolineNodeId, nextTrampolineAmount) => using(pg.prepareStatement("INSERT INTO audit.relayed_trampoline VALUES (?, ?, ?, ?)")) { statement => @@ -357,14 +357,17 @@ class PgAuditDb(implicit ds: DataSource) extends AuditDb with Logging { .foldLeft(Map.empty[UUID, PaymentSent]) { (sentByParentId, rs) => val parentId = UUID.fromString(rs.getString("parent_payment_id")) val part = PaymentSent.PartialPayment( - UUID.fromString(rs.getString("payment_id")), - MilliSatoshi(rs.getLong("amount_msat")), - MilliSatoshi(rs.getLong("fees_msat")), - rs.getByteVector32FromHex("to_channel_id"), - None, // we don't store the route in the audit DB + id = UUID.fromString(rs.getString("payment_id")), + payment = PaymentEvent.OutgoingPayment( + channelId = rs.getByteVector32FromHex("to_channel_id"), + remoteNodeId = PrivateKey(ByteVector32.One).publicKey, // we're not storing the remote node_id yet + amount = MilliSatoshi(rs.getLong("amount_msat")), + settledAt = TimestampMilli.fromSqlTimestamp(rs.getTimestamp("timestamp")) + ), + feesPaid = MilliSatoshi(rs.getLong("fees_msat")), + route = None, // we don't store the route in the audit DB // TODO: store startedAt when updating the DB schema instead of duplicating settledAt. - startedAt = TimestampMilli.fromSqlTimestamp(rs.getTimestamp("timestamp")), - settledAt = TimestampMilli.fromSqlTimestamp(rs.getTimestamp("timestamp"))) + startedAt = TimestampMilli.fromSqlTimestamp(rs.getTimestamp("timestamp"))) val sent = sentByParentId.get(parentId) match { case Some(s) => s.copy(parts = s.parts :+ part) case None => PaymentSent( @@ -393,10 +396,11 @@ class PgAuditDb(implicit ds: DataSource) extends AuditDb with Logging { val result = statement.executeQuery() .foldLeft(Map.empty[ByteVector32, PaymentReceived]) { (receivedByHash, rs) => val paymentHash = rs.getByteVector32FromHex("payment_hash") - val part = PaymentReceived.PartialPayment( - MilliSatoshi(rs.getLong("amount_msat")), - rs.getByteVector32FromHex("from_channel_id"), - TimestampMilli.fromSqlTimestamp(rs.getTimestamp("timestamp"))) + val part = PaymentEvent.IncomingPayment( + channelId = rs.getByteVector32FromHex("from_channel_id"), + remoteNodeId = PrivateKey(ByteVector32.One).publicKey, // we're not storing the remote node_id yet + amount = MilliSatoshi(rs.getLong("amount_msat")), + receivedAt = TimestampMilli.fromSqlTimestamp(rs.getTimestamp("timestamp"))) val received = receivedByHash.get(paymentHash) match { case Some(r) => r.copy(parts = r.parts :+ part) case None => PaymentReceived(paymentHash, Seq(part)) @@ -442,11 +446,11 @@ class PgAuditDb(implicit ds: DataSource) extends AuditDb with Logging { case (paymentHash, parts) => // We may have been routing multiple payments for the same payment_hash (MPP) in both cases (trampoline and channel). // NB: we may link the wrong in-out parts, but the overall sum will be correct: we sort by amounts to minimize the risk of mismatch. - val incoming = parts.filter(_.direction == "IN").map(p => PaymentRelayed.IncomingPart(p.amount, p.channelId, p.timestamp)).sortBy(_.amount) - val outgoing = parts.filter(_.direction == "OUT").map(p => PaymentRelayed.OutgoingPart(p.amount, p.channelId, p.timestamp)).sortBy(_.amount) + val incoming = parts.filter(_.direction == "IN").map(p => PaymentEvent.IncomingPayment(p.channelId, PrivateKey(ByteVector32.One).publicKey, p.amount, p.timestamp)).sortBy(_.amount) + val outgoing = parts.filter(_.direction == "OUT").map(p => PaymentEvent.OutgoingPayment(p.channelId, PrivateKey(ByteVector32.One).publicKey, p.amount, p.timestamp)).sortBy(_.amount) parts.headOption match { case Some(RelayedPart(_, _, _, "channel", _)) => incoming.zip(outgoing).map { - case (in, out) => ChannelPaymentRelayed(in.amount, out.amount, paymentHash, in.channelId, out.channelId, in.receivedAt, out.settledAt) + case (in, out) => ChannelPaymentRelayed(paymentHash, in, out) } case Some(RelayedPart(_, _, _, "trampoline", _)) => trampolineByHash.get(paymentHash) match { case Some((nextTrampolineAmount, nextTrampolineNodeId)) => TrampolinePaymentRelayed(paymentHash, incoming, outgoing, nextTrampolineNodeId, nextTrampolineAmount) :: Nil @@ -483,7 +487,7 @@ class PgAuditDb(implicit ds: DataSource) extends AuditDb with Logging { override def stats(from: TimestampMilli, to: TimestampMilli, paginated_opt: Option[Paginated]): Seq[Stats] = { case class Relayed(amount: MilliSatoshi, fee: MilliSatoshi, direction: String) - def aggregateRelayStats(previous: Map[ByteVector32, Seq[Relayed]], incoming: PaymentRelayed.Incoming, outgoing: PaymentRelayed.Outgoing): Map[ByteVector32, Seq[Relayed]] = { + def aggregateRelayStats(previous: Map[ByteVector32, Seq[Relayed]], incoming: Seq[PaymentEvent.IncomingPayment], outgoing: Seq[PaymentEvent.OutgoingPayment]): Map[ByteVector32, Seq[Relayed]] = { // We ensure trampoline payments are counted only once per channel and per direction (if multiple HTLCs were sent // from/to the same channel, we group them). val amountIn = incoming.map(_.amount).sum @@ -500,8 +504,8 @@ class PgAuditDb(implicit ds: DataSource) extends AuditDb with Logging { // NB: we must avoid counting the fee twice: we associate it to the outgoing channels rather than the incoming ones. val current = e match { case c: ChannelPaymentRelayed => Map( - c.fromChannelId -> (Relayed(c.amountIn, 0 msat, "IN") +: previous.getOrElse(c.fromChannelId, Nil)), - c.toChannelId -> (Relayed(c.amountOut, c.amountIn - c.amountOut, "OUT") +: previous.getOrElse(c.toChannelId, Nil)), + c.paymentIn.channelId -> (Relayed(c.amountIn, 0 msat, "IN") +: previous.getOrElse(c.paymentIn.channelId, Nil)), + c.paymentOut.channelId -> (Relayed(c.amountOut, c.amountIn - c.amountOut, "OUT") +: previous.getOrElse(c.paymentOut.channelId, Nil)), ) case t: TrampolinePaymentRelayed => aggregateRelayStats(previous, t.incoming, t.outgoing) 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 b69d2d96fb..7df7db8e5b 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 @@ -198,7 +198,7 @@ class SqliteAuditDb(val sqlite: Connection) extends AuditDb with Logging { override def add(e: PaymentSent): Unit = withMetrics("audit/add-payment-sent", DbBackends.Sqlite) { using(sqlite.prepareStatement("INSERT INTO sent VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?)")) { statement => e.parts.foreach(p => { - statement.setLong(1, p.amount.toLong) + statement.setLong(1, p.amountWithFees.toLong) statement.setLong(2, p.feesPaid.toLong) statement.setLong(3, e.recipientAmount.toLong) statement.setString(4, p.id.toString) @@ -206,7 +206,7 @@ class SqliteAuditDb(val sqlite: Connection) extends AuditDb with Logging { statement.setBytes(6, e.paymentHash.toArray) statement.setBytes(7, e.paymentPreimage.toArray) statement.setBytes(8, e.recipientNodeId.value.toArray) - statement.setBytes(9, p.toChannelId.toArray) + statement.setBytes(9, p.channelId.toArray) statement.setLong(10, p.settledAt.toLong) statement.addBatch() }) @@ -219,7 +219,7 @@ class SqliteAuditDb(val sqlite: Connection) extends AuditDb with Logging { e.parts.foreach(p => { statement.setLong(1, p.amount.toLong) statement.setBytes(2, e.paymentHash.toArray) - statement.setBytes(3, p.fromChannelId.toArray) + statement.setBytes(3, p.channelId.toArray) statement.setLong(4, p.receivedAt.toLong) statement.addBatch() }) @@ -229,10 +229,10 @@ class SqliteAuditDb(val sqlite: Connection) extends AuditDb with Logging { override def add(e: PaymentRelayed): Unit = withMetrics("audit/add-payment-relayed", DbBackends.Sqlite) { val payments = e match { - case ChannelPaymentRelayed(amountIn, amountOut, _, fromChannelId, toChannelId, startedAt, settledAt) => + case e: ChannelPaymentRelayed => // non-trampoline relayed payments have one input and one output - val in = Seq(RelayedPart(fromChannelId, amountIn, "IN", "channel", startedAt)) - val out = Seq(RelayedPart(toChannelId, amountOut, "OUT", "channel", settledAt)) + val in = Seq(RelayedPart(e.paymentIn.channelId, e.paymentIn.amount, "IN", "channel", e.startedAt)) + val out = Seq(RelayedPart(e.paymentOut.channelId, e.paymentOut.amount, "OUT", "channel", e.settledAt)) in ++ out case TrampolinePaymentRelayed(_, incoming, outgoing, nextTrampolineNodeId, nextTrampolineAmount) => using(sqlite.prepareStatement("INSERT INTO relayed_trampoline VALUES (?, ?, ?, ?)")) { statement => @@ -331,14 +331,17 @@ class SqliteAuditDb(val sqlite: Connection) extends AuditDb with Logging { .foldLeft(Map.empty[UUID, PaymentSent]) { (sentByParentId, rs) => val parentId = UUID.fromString(rs.getString("parent_payment_id")) val part = PaymentSent.PartialPayment( - UUID.fromString(rs.getString("payment_id")), - MilliSatoshi(rs.getLong("amount_msat")), - MilliSatoshi(rs.getLong("fees_msat")), - rs.getByteVector32("to_channel_id"), - None, // we don't store the route in the audit DB + id = UUID.fromString(rs.getString("payment_id")), + payment = PaymentEvent.OutgoingPayment( + channelId = rs.getByteVector32("to_channel_id"), + remoteNodeId = PrivateKey(ByteVector32.One).publicKey, // we're not storing the remote node_id yet + amount = MilliSatoshi(rs.getLong("amount_msat")), + settledAt = TimestampMilli(rs.getLong("timestamp")) + ), + feesPaid = MilliSatoshi(rs.getLong("fees_msat")), + route = None, // we don't store the route in the audit DB // TODO: store startedAt when updating the DB schema instead of duplicating settledAt. - startedAt = TimestampMilli(rs.getLong("timestamp")), - settledAt = TimestampMilli(rs.getLong("timestamp"))) + startedAt = TimestampMilli(rs.getLong("timestamp"))) val sent = sentByParentId.get(parentId) match { case Some(s) => s.copy(parts = s.parts :+ part) case None => PaymentSent( @@ -365,10 +368,11 @@ class SqliteAuditDb(val sqlite: Connection) extends AuditDb with Logging { val result = statement.executeQuery() .foldLeft(Map.empty[ByteVector32, PaymentReceived]) { (receivedByHash, rs) => val paymentHash = rs.getByteVector32("payment_hash") - val part = PaymentReceived.PartialPayment( - MilliSatoshi(rs.getLong("amount_msat")), - rs.getByteVector32("from_channel_id"), - TimestampMilli(rs.getLong("timestamp"))) + val part = PaymentEvent.IncomingPayment( + channelId = rs.getByteVector32("from_channel_id"), + remoteNodeId = PrivateKey(ByteVector32.One).publicKey, // we're not storing the remote node_id yet + amount = MilliSatoshi(rs.getLong("amount_msat")), + receivedAt = TimestampMilli(rs.getLong("timestamp"))) val received = receivedByHash.get(paymentHash) match { case Some(r) => r.copy(parts = r.parts :+ part) case None => PaymentReceived(paymentHash, Seq(part)) @@ -413,11 +417,11 @@ class SqliteAuditDb(val sqlite: Connection) extends AuditDb with Logging { case (paymentHash, parts) => // We may have been routing multiple payments for the same payment_hash (MPP) in both cases (trampoline and channel). // NB: we may link the wrong in-out parts, but the overall sum will be correct: we sort by amounts to minimize the risk of mismatch. - val incoming = parts.filter(_.direction == "IN").map(p => PaymentRelayed.IncomingPart(p.amount, p.channelId, p.timestamp)).sortBy(_.amount) - val outgoing = parts.filter(_.direction == "OUT").map(p => PaymentRelayed.OutgoingPart(p.amount, p.channelId, p.timestamp)).sortBy(_.amount) + val incoming = parts.filter(_.direction == "IN").map(p => PaymentEvent.IncomingPayment(p.channelId, PrivateKey(ByteVector32.One).publicKey, p.amount, p.timestamp)).sortBy(_.amount) + val outgoing = parts.filter(_.direction == "OUT").map(p => PaymentEvent.OutgoingPayment(p.channelId, PrivateKey(ByteVector32.One).publicKey, p.amount, p.timestamp)).sortBy(_.amount) parts.headOption match { case Some(RelayedPart(_, _, _, "channel", _)) => incoming.zip(outgoing).map { - case (in, out) => ChannelPaymentRelayed(in.amount, out.amount, paymentHash, in.channelId, out.channelId, in.receivedAt, out.settledAt) + case (in, out) => ChannelPaymentRelayed(paymentHash, in, out) } case Some(RelayedPart(_, _, _, "trampoline", _)) => trampolineByHash.get(paymentHash) match { case Some((nextTrampolineAmount, nextTrampolineNodeId)) => TrampolinePaymentRelayed(paymentHash, incoming, outgoing, nextTrampolineNodeId, nextTrampolineAmount) :: Nil @@ -453,7 +457,7 @@ class SqliteAuditDb(val sqlite: Connection) extends AuditDb with Logging { override def stats(from: TimestampMilli, to: TimestampMilli, paginated_opt: Option[Paginated]): Seq[Stats] = { case class Relayed(amount: MilliSatoshi, fee: MilliSatoshi, direction: String) - def aggregateRelayStats(previous: Map[ByteVector32, Seq[Relayed]], incoming: PaymentRelayed.Incoming, outgoing: PaymentRelayed.Outgoing): Map[ByteVector32, Seq[Relayed]] = { + def aggregateRelayStats(previous: Map[ByteVector32, Seq[Relayed]], incoming: Seq[PaymentEvent.IncomingPayment], outgoing: Seq[PaymentEvent.OutgoingPayment]): Map[ByteVector32, Seq[Relayed]] = { // We ensure trampoline payments are counted only once per channel and per direction (if multiple HTLCs were sent // from/to the same channel, we group them). val amountIn = incoming.map(_.amount).sum @@ -470,8 +474,8 @@ class SqliteAuditDb(val sqlite: Connection) extends AuditDb with Logging { // NB: we must avoid counting the fee twice: we associate it to the outgoing channels rather than the incoming ones. val current = e match { case c: ChannelPaymentRelayed => Map( - c.fromChannelId -> (Relayed(c.amountIn, 0 msat, "IN") +: previous.getOrElse(c.fromChannelId, Nil)), - c.toChannelId -> (Relayed(c.amountOut, c.amountIn - c.amountOut, "OUT") +: previous.getOrElse(c.toChannelId, Nil)), + c.paymentIn.channelId -> (Relayed(c.amountIn, 0 msat, "IN") +: previous.getOrElse(c.paymentIn.channelId, Nil)), + c.paymentOut.channelId -> (Relayed(c.amountOut, c.amountIn - c.amountOut, "OUT") +: previous.getOrElse(c.paymentOut.channelId, Nil)), ) case t: TrampolinePaymentRelayed => aggregateRelayStats(previous, t.incoming, t.outgoing) diff --git a/eclair-core/src/main/scala/fr/acinq/eclair/io/Peer.scala b/eclair-core/src/main/scala/fr/acinq/eclair/io/Peer.scala index c782a1fc63..7529e0788e 100644 --- a/eclair-core/src/main/scala/fr/acinq/eclair/io/Peer.scala +++ b/eclair-core/src/main/scala/fr/acinq/eclair/io/Peer.scala @@ -41,7 +41,7 @@ import fr.acinq.eclair.io.OpenChannelInterceptor.{OpenChannelInitiator, OpenChan import fr.acinq.eclair.io.PeerConnection.KillReason import fr.acinq.eclair.message.OnionMessages import fr.acinq.eclair.payment.relay.OnTheFlyFunding -import fr.acinq.eclair.payment.{OnTheFlyFundingPaymentRelayed, PaymentRelayed} +import fr.acinq.eclair.payment.{OnTheFlyFundingPaymentRelayed, PaymentEvent} import fr.acinq.eclair.remote.EclairInternalsSerializer.RemoteTypes import fr.acinq.eclair.router.Router import fr.acinq.eclair.wire.protocol @@ -814,12 +814,12 @@ class Peer(val nodeParams: NodeParams, case OnTheFlyFunding.Proposal(htlc, upstream, _) => upstream match { case _: Upstream.Local => () case u: Upstream.Hot.Channel => - val incoming = PaymentRelayed.IncomingPart(u.add.amountMsat, u.add.channelId, u.receivedAt) - val outgoing = PaymentRelayed.OutgoingPart(htlc.amount, success.channelId, TimestampMilli.now()) + val incoming = PaymentEvent.IncomingPayment(u.add.channelId, u.receivedFrom, u.add.amountMsat, u.receivedAt) + val outgoing = PaymentEvent.OutgoingPayment(success.channelId, remoteNodeId, htlc.amount, TimestampMilli.now()) context.system.eventStream.publish(OnTheFlyFundingPaymentRelayed(htlc.paymentHash, Seq(incoming), Seq(outgoing))) case u: Upstream.Hot.Trampoline => - val incoming = u.received.map(r => PaymentRelayed.IncomingPart(r.add.amountMsat, r.add.channelId, r.receivedAt)) - val outgoing = PaymentRelayed.OutgoingPart(htlc.amount, success.channelId, TimestampMilli.now()) + val incoming = u.received.map(r => PaymentEvent.IncomingPayment(r.add.channelId, r.receivedFrom, r.add.amountMsat, r.receivedAt)) + val outgoing = PaymentEvent.OutgoingPayment(success.channelId, remoteNodeId, htlc.amount, TimestampMilli.now()) context.system.eventStream.publish(OnTheFlyFundingPaymentRelayed(htlc.paymentHash, incoming, Seq(outgoing))) } } 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 d81e9b4e39..9a387374a1 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 @@ -383,8 +383,12 @@ 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)) +private case class PartialPaymentJson(id: UUID, channelId: ByteVector32, nextNodeId: PublicKey, amountWithFees: MilliSatoshi, fees: MilliSatoshi, route: Option[Seq[Hop]], startedAt: TimestampMilli, settledAt: TimestampMilli) +private case class PaymentSentJson(id: UUID, paymentHash: ByteVector32, paymentPreimage: ByteVector32, recipientAmount: MilliSatoshi, recipientNodeId: PublicKey, parts: Seq[PartialPaymentJson], fees: MilliSatoshi, startedAt: TimestampMilli, settledAt: TimestampMilli) +object PaymentSentSerializer extends ConvertClassSerializer[PaymentSent](p => { + val parts = p.parts.map(pp => PartialPaymentJson(pp.id, pp.channelId, pp.remoteNodeId, pp.amountWithFees, pp.feesPaid, pp.route, pp.startedAt, pp.settledAt)) + PaymentSentJson(p.id, p.paymentHash, p.paymentPreimage, p.recipientAmount, p.recipientNodeId, parts, p.feesPaid, p.startedAt, p.settledAt) +}) object ThrowableSerializer extends MinimalSerializer({ case t: Throwable if t.getMessage != null => JString(t.getMessage) @@ -591,6 +595,7 @@ object OriginSerializer extends MinimalSerializer({ case u: Upstream.Local => JObject(JField("paymentId", JString(u.id.toString))) case u: Upstream.Hot.Channel => JObject( JField("channelId", JString(u.add.channelId.toHex)), + JField("remoteNodeId", JString(u.receivedFrom.toHex)), JField("htlcId", JLong(u.add.id)), JField("amount", JLong(u.add.amountMsat.toLong)), JField("expiry", JLong(u.add.cltvExpiry.toLong)), @@ -599,6 +604,7 @@ object OriginSerializer extends MinimalSerializer({ case u: Upstream.Hot.Trampoline => JArray(u.received.map { htlc => JObject( JField("channelId", JString(htlc.add.channelId.toHex)), + JField("remoteNodeId", JString(htlc.receivedFrom.toHex)), JField("htlcId", JLong(htlc.add.id)), JField("amount", JLong(htlc.add.amountMsat.toLong)), JField("expiry", JLong(htlc.add.cltvExpiry.toLong)), @@ -607,12 +613,14 @@ object OriginSerializer extends MinimalSerializer({ }) case o: Upstream.Cold.Channel => JObject( JField("channelId", JString(o.originChannelId.toHex)), + JField("remoteNodeId", JString(o.originNodeId.toHex)), JField("htlcId", JLong(o.originHtlcId)), JField("amount", JLong(o.amountIn.toLong)), ) case o: Upstream.Cold.Trampoline => JArray(o.originHtlcs.map { htlc => JObject( JField("channelId", JString(htlc.originChannelId.toHex)), + JField("remoteNodeId", JString(htlc.originNodeId.toHex)), JField("htlcId", JLong(htlc.originHtlcId)), JField("amount", JLong(htlc.amountIn.toLong)), ) 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 e1a9477dda..2229772fd8 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 @@ -47,11 +47,32 @@ sealed trait PaymentEvent { // @formatter:on } +object PaymentEvent { + /** + * An incoming payment that has been received through one of our channels. + * + * @param channelId the incoming channelId. + * @param remoteNodeId the nodeId of our channel peer. + * @param amount amount received. + * @param receivedAt absolute time in milliseconds since UNIX epoch when the payment was received. + */ + case class IncomingPayment(channelId: ByteVector32, remoteNodeId: PublicKey, amount: MilliSatoshi, receivedAt: TimestampMilli) + + /** + * An outgoing payment that was sent through one of our channels. + * + * @param channelId the outgoing channelId. + * @param remoteNodeId the nodeId of our channel peer. + * @param amount amount sent. + * @param settledAt absolute time in milliseconds since UNIX epoch when the payment was settled (fulfilled or failed). + */ + case class OutgoingPayment(channelId: ByteVector32, remoteNodeId: PublicKey, amount: MilliSatoshi, settledAt: TimestampMilli) +} + /** * A payment was successfully sent and fulfilled. * - * @param id id of the whole payment attempt (if using multi-part, there will be multiple parts, - * each with a different id). + * @param id id of the whole payment attempt (if using multi-part, there will be multiple parts, each with a different id). * @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. @@ -67,74 +88,63 @@ case class PaymentSent(id: UUID, paymentPreimage: ByteVector32, recipientAmount: } object PaymentSent { - /** * A successfully sent partial payment (single outgoing HTLC). * - * @param id id of the outgoing payment. - * @param amount amount received by the target node. - * @param feesPaid fees paid to route to the target node. - * @param toChannelId id of the channel used. - * @param route payment route used. - * @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. + * @param id id of the outgoing payment. + * @param payment payment sent to the target node through one of our channels (including fees). + * @param feesPaid fees paid to route to the target node. + * @param route payment route used. + * @param startedAt absolute time in milliseconds since UNIX epoch when the payment was started. */ - case class PartialPayment(id: UUID, amount: MilliSatoshi, feesPaid: MilliSatoshi, toChannelId: ByteVector32, route: Option[Seq[Hop]], startedAt: TimestampMilli, settledAt: TimestampMilli) { + case class PartialPayment(id: UUID, payment: PaymentEvent.OutgoingPayment, feesPaid: MilliSatoshi, route: Option[Seq[Hop]], startedAt: 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 + val channelId: ByteVector32 = payment.channelId + val remoteNodeId: PublicKey = payment.remoteNodeId + /** Amount received by the final recipient. */ + val amount: MilliSatoshi = payment.amount - feesPaid + /** Amount we paid, which includes routing fees for the route. */ + val amountWithFees: MilliSatoshi = payment.amount + val settledAt: TimestampMilli = payment.settledAt + val duration: FiniteDuration = payment.settledAt - startedAt } - } +/** A payment that we tried to send was failed and aborted. */ case class PaymentFailed(id: UUID, paymentHash: ByteVector32, failures: Seq[PaymentFailure], startedAt: TimestampMilli, settledAt: TimestampMilli = TimestampMilli.now()) extends PaymentEvent +/** A payment was relayed and fulfilled. */ sealed trait PaymentRelayed extends PaymentEvent { - val amountIn: MilliSatoshi - val amountOut: MilliSatoshi - val startedAt: TimestampMilli - val settledAt: TimestampMilli -} - -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 startedAt: TimestampMilli = incoming.map(_.receivedAt).minOption.getOrElse(TimestampMilli.now()) - override val settledAt: TimestampMilli = outgoing.map(_.settledAt).maxOption.getOrElse(TimestampMilli.now()) + // @formatter:off + def incoming: Seq[PaymentEvent.IncomingPayment] + def outgoing: Seq[PaymentEvent.OutgoingPayment] + def amountIn: MilliSatoshi = incoming.map(_.amount).sum + def amountOut: MilliSatoshi = outgoing.map(_.amount).sum + override def startedAt: TimestampMilli = incoming.map(_.receivedAt).minOption.getOrElse(TimestampMilli.now()) + override def settledAt: TimestampMilli = outgoing.map(_.settledAt).maxOption.getOrElse(TimestampMilli.now()) + // @formatter:on } -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 startedAt: TimestampMilli = incoming.map(_.receivedAt).minOption.getOrElse(TimestampMilli.now()) - override val settledAt: TimestampMilli = outgoing.map(_.settledAt).maxOption.getOrElse(TimestampMilli.now()) +/** A payment was successfully relayed from a single incoming channel to a single outgoing channel. */ +case class ChannelPaymentRelayed(paymentHash: ByteVector32, paymentIn: PaymentEvent.IncomingPayment, paymentOut: PaymentEvent.OutgoingPayment) extends PaymentRelayed { + override val incoming: Seq[PaymentEvent.IncomingPayment] = Seq(paymentIn) + override val outgoing: Seq[PaymentEvent.OutgoingPayment] = Seq(paymentOut) } -object PaymentRelayed { - - case class IncomingPart(amount: MilliSatoshi, channelId: ByteVector32, receivedAt: TimestampMilli) - case class OutgoingPart(amount: MilliSatoshi, channelId: ByteVector32, settledAt: TimestampMilli) +/** A trampoline payment was successfully relayed, using potentially multiple incoming and outgoing channels. */ +case class TrampolinePaymentRelayed(paymentHash: ByteVector32, incoming: Seq[PaymentEvent.IncomingPayment], outgoing: Seq[PaymentEvent.OutgoingPayment], nextTrampolineNodeId: PublicKey, nextTrampolineAmount: MilliSatoshi) extends PaymentRelayed - type Incoming = Seq[IncomingPart] - type Outgoing = Seq[OutgoingPart] +/** A payment (potentially using MPP and/or trampoline) was successfully relayed after funding an outgoing channel (liquidity purchase). */ +case class OnTheFlyFundingPaymentRelayed(paymentHash: ByteVector32, incoming: Seq[PaymentEvent.IncomingPayment], outgoing: Seq[PaymentEvent.OutgoingPayment]) extends PaymentRelayed -} - -case class PaymentReceived(paymentHash: ByteVector32, parts: Seq[PaymentReceived.PartialPayment]) extends PaymentEvent { +/** A payment has been received through some of our channels. */ +case class PaymentReceived(paymentHash: ByteVector32, parts: Seq[PaymentEvent.IncomingPayment]) extends PaymentEvent { require(parts.nonEmpty, "must have at least one payment part") val amount: MilliSatoshi = parts.map(_.amount).sum 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, receivedAt: TimestampMilli = TimestampMilli.now()) - -} - case class PaymentMetadataReceived(paymentHash: ByteVector32, paymentMetadata: ByteVector) case class PaymentSettlingOnChain(id: UUID, channelId: ByteVector32, amount: MilliSatoshi, paymentHash: ByteVector32, timestamp: TimestampMilli = TimestampMilli.now()) 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 b837838d29..6c9f271c6d 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 @@ -22,7 +22,7 @@ import akka.actor.typed.scaladsl.Behaviors import akka.actor.typed.scaladsl.adapter.ClassicActorContextOps import akka.actor.{ActorContext, ActorRef, PoisonPill, typed} import akka.event.{DiagnosticLoggingAdapter, LoggingAdapter} -import fr.acinq.bitcoin.scalacompat.Crypto.PrivateKey +import fr.acinq.bitcoin.scalacompat.Crypto.{PrivateKey, PublicKey} import fr.acinq.bitcoin.scalacompat.{ByteVector32, Crypto} import fr.acinq.eclair.EncodedNodeId.ShortChannelIdDir import fr.acinq.eclair.Logs.LogCategory @@ -53,13 +53,13 @@ class MultiPartHandler(nodeParams: NodeParams, register: ActorRef, db: IncomingP // NB: this is safe because this handler will be called from within an actor private var pendingPayments: Map[ByteVector32, (IncomingPayment, ActorRef)] = Map.empty - private def addHtlcPart(ctx: ActorContext, add: UpdateAddHtlc, payload: FinalPayload, payment: IncomingPayment, receivedAt: TimestampMilli): Unit = { + private def addHtlcPart(ctx: ActorContext, add: UpdateAddHtlc, payload: FinalPayload, remoteNodeId: PublicKey, payment: IncomingPayment, receivedAt: TimestampMilli): Unit = { pendingPayments.get(add.paymentHash) match { case Some((_, handler)) => - handler ! MultiPartPaymentFSM.HtlcPart(payload.totalAmount, add, receivedAt) + handler ! MultiPartPaymentFSM.HtlcPart(payload.totalAmount, add, remoteNodeId, receivedAt) case None => val handler = ctx.actorOf(MultiPartPaymentFSM.props(nodeParams, add.paymentHash, payload.totalAmount, ctx.self)) - handler ! MultiPartPaymentFSM.HtlcPart(payload.totalAmount, add, receivedAt) + handler ! MultiPartPaymentFSM.HtlcPart(payload.totalAmount, add, remoteNodeId, receivedAt) pendingPayments = pendingPayments + (add.paymentHash -> (payment, handler)) } } @@ -82,11 +82,11 @@ class MultiPartHandler(nodeParams: NodeParams, register: ActorRef, db: IncomingP val child = ctx.spawnAnonymous(CreateInvoiceActor(nodeParams)) child ! CreateInvoiceActor.CreateBolt12Invoice(receivePayment) - case p: IncomingPaymentPacket.FinalPacket if doHandle(p.add.paymentHash) => - val child = ctx.spawnAnonymous(GetIncomingPaymentActor(nodeParams, p, offerManager)) + case receivePayment: ReceivePacket if doHandle(receivePayment.packet.add.paymentHash) => + val child = ctx.spawnAnonymous(GetIncomingPaymentActor(nodeParams, receivePayment.packet, receivePayment.remoteNodeId, offerManager)) child ! GetIncomingPaymentActor.GetIncomingPayment(ctx.self) - case ProcessPacket(add, payload, payment_opt, receivedAt) if doHandle(add.paymentHash) => + case ProcessPacket(add, payload, remoteNodeId, payment_opt, receivedAt) if doHandle(add.paymentHash) => Logs.withMdc(log)(Logs.mdc(paymentHash_opt = Some(add.paymentHash))) { payment_opt match { case Some(payment) => validateStandardPayment(nodeParams, add, payload, payment, receivedAt) match { @@ -100,7 +100,7 @@ class MultiPartHandler(nodeParams: NodeParams, register: ActorRef, db: IncomingP log.debug("received payment for amount={} totalAmount={} paymentMetadata={}", add.amountMsat, payload.totalAmount, payload.paymentMetadata.map(_.toHex).getOrElse("none")) Metrics.PaymentHtlcReceived.withTag(Tags.PaymentMetadataIncluded, payload.paymentMetadata.nonEmpty).increment() payload.paymentMetadata.foreach(metadata => ctx.system.eventStream.publish(PaymentMetadataReceived(add.paymentHash, metadata))) - addHtlcPart(ctx, add, payload, payment, receivedAt) + addHtlcPart(ctx, add, payload, remoteNodeId, payment, receivedAt) } case None => payload.paymentPreimage match { case Some(paymentPreimage) if nodeParams.features.hasFeature(Features.KeySend) => @@ -116,7 +116,7 @@ class MultiPartHandler(nodeParams: NodeParams, register: ActorRef, db: IncomingP val invoice = Bolt11Invoice(nodeParams.chainHash, amount, paymentHash, nodeParams.privateKey, desc, nodeParams.channelConf.minFinalExpiryDelta, paymentSecret = payload.paymentSecret, features = features) log.debug("generated fake invoice={} from amount={} (KeySend)", invoice.toString, amount) db.addIncomingPayment(invoice, paymentPreimage, PaymentType.KeySend) - ctx.self ! ProcessPacket(add, payload, Some(IncomingStandardPayment(invoice, paymentPreimage, PaymentType.KeySend, TimestampMilli.now(), IncomingPaymentStatus.Pending)), receivedAt) + ctx.self ! ProcessPacket(add, payload, remoteNodeId, Some(IncomingStandardPayment(invoice, paymentPreimage, PaymentType.KeySend, TimestampMilli.now(), IncomingPaymentStatus.Pending)), receivedAt) case _ => Metrics.PaymentFailed.withTag(Tags.Direction, Tags.Directions.Received).withTag(Tags.Failure, "InvoiceNotFound").increment() val attribution = FailureAttributionData(htlcReceivedAt = receivedAt, trampolineReceivedAt_opt = None) @@ -126,7 +126,7 @@ class MultiPartHandler(nodeParams: NodeParams, register: ActorRef, db: IncomingP } } - case ProcessBlindedPacket(add, payload, payment, maxRecipientPathFees, receivedAt) if doHandle(add.paymentHash) => + case ProcessBlindedPacket(add, payload, remoteNodeId, payment, maxRecipientPathFees, receivedAt) if doHandle(add.paymentHash) => Logs.withMdc(log)(Logs.mdc(paymentHash_opt = Some(add.paymentHash))) { validateBlindedPayment(nodeParams, add, payload, payment, maxRecipientPathFees, receivedAt) match { case Some(cmdFail) => @@ -135,7 +135,7 @@ class MultiPartHandler(nodeParams: NodeParams, register: ActorRef, db: IncomingP case None => val recipientPathFees = payload.amount - add.amountMsat log.debug("received payment for amount={} recipientPathFees={} totalAmount={}", add.amountMsat, recipientPathFees, payload.totalAmount) - addHtlcPart(ctx, add, payload, payment, receivedAt) + addHtlcPart(ctx, add, payload, remoteNodeId, payment, receivedAt) if (recipientPathFees > 0.msat) { // We've opted into deducing the blinded paths fees from the amount we receive for this payment. // We add an artificial payment part for those fees, otherwise we will never reach the total amount. @@ -187,7 +187,7 @@ 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, p.receivedAt) :: Nil) + val received = PaymentReceived(paymentHash, PaymentEvent.IncomingPayment(p.htlc.channelId, p.remoteNodeId, p.amount, 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)) @@ -207,7 +207,7 @@ 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, p.receivedAt)) + case p: MultiPartPaymentFSM.HtlcPart => Some(PaymentEvent.IncomingPayment(p.htlc.channelId, p.remoteNodeId, p.amount, p.receivedAt)) case _: MultiPartPaymentFSM.RecipientBlindedPathFeePart => None }) val recordedInDb = payment match { @@ -248,8 +248,9 @@ class MultiPartHandler(nodeParams: NodeParams, register: ActorRef, db: IncomingP object MultiPartHandler { // @formatter:off - private case class ProcessPacket(add: UpdateAddHtlc, payload: FinalPayload.Standard, payment_opt: Option[IncomingStandardPayment], receivedAt: TimestampMilli) - private case class ProcessBlindedPacket(add: UpdateAddHtlc, payload: FinalPayload.Blinded, payment: IncomingBlindedPayment, maxRecipientPathFees: MilliSatoshi, receivedAt: TimestampMilli) + case class ReceivePacket(packet: IncomingPaymentPacket.FinalPacket, remoteNodeId: PublicKey) + private case class ProcessPacket(add: UpdateAddHtlc, payload: FinalPayload.Standard, remoteNodeId: PublicKey, payment_opt: Option[IncomingStandardPayment], receivedAt: TimestampMilli) + private case class ProcessBlindedPacket(add: UpdateAddHtlc, payload: FinalPayload.Blinded, remoteNodeId: PublicKey, payment: IncomingBlindedPayment, maxRecipientPathFees: MilliSatoshi, receivedAt: TimestampMilli) private case class RejectPacket(add: UpdateAddHtlc, failure: FailureMessage, receivedAt: TimestampMilli) case class DoFulfill(payment: IncomingPayment, success: MultiPartPaymentFSM.MultiPartPaymentSucceeded) @@ -372,7 +373,7 @@ object MultiPartHandler { case class RejectPayment(reason: String) extends Command // @formatter:on - def apply(nodeParams: NodeParams, packet: IncomingPaymentPacket.FinalPacket, offerManager: typed.ActorRef[OfferManager.ReceivePayment]): Behavior[Command] = { + def apply(nodeParams: NodeParams, packet: IncomingPaymentPacket.FinalPacket, remoteNodeId: PublicKey, offerManager: typed.ActorRef[OfferManager.ReceivePayment]): Behavior[Command] = { Behaviors.setup { context => Behaviors.withMdc(Logs.mdc(category_opt = Some(LogCategory.PAYMENT), paymentHash_opt = Some(packet.add.paymentHash))) { Behaviors.receiveMessagePartial { @@ -383,23 +384,23 @@ object MultiPartHandler { case Some(_: IncomingBlindedPayment) => context.log.info("rejecting non-blinded htlc #{} from channel {}: expected a blinded payment", packet.add.id, packet.add.channelId) replyTo ! RejectPacket(packet.add, IncorrectOrUnknownPaymentDetails(payload.totalAmount, nodeParams.currentBlockHeight), packet.receivedAt) - case Some(payment: IncomingStandardPayment) => replyTo ! ProcessPacket(packet.add, payload, Some(payment), packet.receivedAt) - case None => replyTo ! ProcessPacket(packet.add, payload, None, packet.receivedAt) + case Some(payment: IncomingStandardPayment) => replyTo ! ProcessPacket(packet.add, payload, remoteNodeId, Some(payment), packet.receivedAt) + case None => replyTo ! ProcessPacket(packet.add, payload, remoteNodeId, None, packet.receivedAt) } Behaviors.stopped case payload: FinalPayload.Blinded => offerManager ! OfferManager.ReceivePayment(context.self, packet.add.paymentHash, payload, packet.add.amountMsat) - waitForPayment(context, nodeParams, replyTo, packet.add, payload, packet.receivedAt) + waitForPayment(context, nodeParams, replyTo, packet.add, payload, remoteNodeId, packet.receivedAt) } } } } } - private def waitForPayment(context: typed.scaladsl.ActorContext[Command], nodeParams: NodeParams, replyTo: ActorRef, add: UpdateAddHtlc, payload: FinalPayload.Blinded, packetReceivedAt: TimestampMilli): Behavior[Command] = { + private def waitForPayment(context: typed.scaladsl.ActorContext[Command], nodeParams: NodeParams, replyTo: ActorRef, add: UpdateAddHtlc, payload: FinalPayload.Blinded, remoteNodeId: PublicKey, packetReceivedAt: TimestampMilli): Behavior[Command] = { Behaviors.receiveMessagePartial { case ProcessPayment(payment, maxRecipientPathFees) => - replyTo ! ProcessBlindedPacket(add, payload, payment, maxRecipientPathFees, packetReceivedAt) + replyTo ! ProcessBlindedPacket(add, payload, remoteNodeId, payment, maxRecipientPathFees, packetReceivedAt) Behaviors.stopped case RejectPayment(reason) => context.log.info("rejecting blinded htlc #{} from channel {}: {}", add.id, add.channelId, reason) diff --git a/eclair-core/src/main/scala/fr/acinq/eclair/payment/receive/MultiPartPaymentFSM.scala b/eclair-core/src/main/scala/fr/acinq/eclair/payment/receive/MultiPartPaymentFSM.scala index 5e8f96f21f..b48bb74253 100644 --- a/eclair-core/src/main/scala/fr/acinq/eclair/payment/receive/MultiPartPaymentFSM.scala +++ b/eclair-core/src/main/scala/fr/acinq/eclair/payment/receive/MultiPartPaymentFSM.scala @@ -19,6 +19,7 @@ package fr.acinq.eclair.payment.receive import akka.actor.{ActorRef, Props} import akka.event.Logging.MDC import fr.acinq.bitcoin.scalacompat.ByteVector32 +import fr.acinq.bitcoin.scalacompat.Crypto.PublicKey import fr.acinq.eclair.payment.Monitoring.{Metrics, Tags} import fr.acinq.eclair.wire.protocol import fr.acinq.eclair.wire.protocol.{FailureMessage, IncorrectOrUnknownPaymentDetails, UpdateAddHtlc} @@ -41,7 +42,7 @@ class MultiPartPaymentFSM(nodeParams: NodeParams, paymentHash: ByteVector32, tot import MultiPartPaymentFSM._ - val start = TimestampMilli.now() + val start: TimestampMilli = TimestampMilli.now() startSingleTimer(PaymentTimeout.toString, PaymentTimeout, nodeParams.multiPartPaymentExpiry) @@ -134,7 +135,7 @@ object MultiPartPaymentFSM { def totalAmount: MilliSatoshi } /** An incoming HTLC. */ - case class HtlcPart(totalAmount: MilliSatoshi, htlc: UpdateAddHtlc, receivedAt: TimestampMilli) extends PaymentPart { + case class HtlcPart(totalAmount: MilliSatoshi, htlc: UpdateAddHtlc, remoteNodeId: PublicKey, receivedAt: TimestampMilli) extends PaymentPart { override def paymentHash: ByteVector32 = htlc.paymentHash override def amount: MilliSatoshi = htlc.amountMsat } @@ -158,9 +159,9 @@ object MultiPartPaymentFSM { // @formatter:off sealed trait Data { def parts: Queue[PaymentPart] - lazy val paidAmount = parts.map(_.amount).sum + lazy val paidAmount: MilliSatoshi = parts.map(_.amount).sum } - case class WaitingForHtlc(parts: Queue[PaymentPart]) extends Data + private case class WaitingForHtlc(parts: Queue[PaymentPart]) extends Data case class PaymentSucceeded(parts: Queue[PaymentPart]) extends Data case class PaymentFailed(failure: FailureMessage, parts: Queue[PaymentPart]) extends Data // @formatter:on 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 167f4773d0..130e4b7365 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 @@ -30,7 +30,7 @@ import fr.acinq.eclair.io.Peer.ProposeOnTheFlyFundingResponse import fr.acinq.eclair.io.{Peer, PeerReadyNotifier} import fr.acinq.eclair.payment.Monitoring.{Metrics, Tags} import fr.acinq.eclair.payment.relay.Relayer.{OutgoingChannel, OutgoingChannelParams} -import fr.acinq.eclair.payment.{ChannelPaymentRelayed, IncomingPaymentPacket} +import fr.acinq.eclair.payment.{ChannelPaymentRelayed, IncomingPaymentPacket, PaymentEvent} import fr.acinq.eclair.reputation.Reputation import fr.acinq.eclair.reputation.ReputationRecorder.GetConfidence import fr.acinq.eclair.wire.protocol.FailureMessageCodecs.createBadOnionFailure @@ -244,7 +244,7 @@ class ChannelRelay private(nodeParams: NodeParams, private def waitForAddSettled(): Behavior[Command] = Behaviors.receiveMessagePartial { - case WrappedAddResponse(RES_ADD_SETTLED(_, htlc, fulfill: HtlcResult.Fulfill)) => + case WrappedAddResponse(RES_ADD_SETTLED(_, remoteNodeId, htlc, fulfill: HtlcResult.Fulfill)) => 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) @@ -254,10 +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, now)) + val incoming = PaymentEvent.IncomingPayment(upstream.add.channelId, upstream.receivedFrom, upstream.amountIn, upstream.receivedAt) + val outgoing = PaymentEvent.OutgoingPayment(htlc.channelId, remoteNodeId, htlc.amountMsat, now) + context.system.eventStream ! EventStream.Publish(ChannelPaymentRelayed(htlc.paymentHash, incoming, outgoing)) recordRelayDuration(isSuccess = true) safeSendAndStop(upstream.add.channelId, cmd) - case WrappedAddResponse(RES_ADD_SETTLED(_, htlc, fail: HtlcResult.Fail)) => + case WrappedAddResponse(RES_ADD_SETTLED(_, _, htlc, fail: HtlcResult.Fail)) => 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) 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 5e121622a3..3f0da9465b 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 @@ -226,7 +226,7 @@ class NodeRelay private(nodeParams: NodeParams, case Relay(packet: IncomingPaymentPacket.NodeRelayPacket, originNode, incomingChannelOccupancy) => require(packet.outerPayload.paymentSecret == paymentSecret, "payment secret mismatch") context.log.debug("forwarding incoming htlc #{} from channel {} to the payment FSM", packet.add.id, packet.add.channelId) - handler ! MultiPartPaymentFSM.HtlcPart(packet.outerPayload.totalAmount, packet.add, packet.receivedAt) + handler ! MultiPartPaymentFSM.HtlcPart(packet.outerPayload.totalAmount, packet.add, originNode, packet.receivedAt) receiving(htlcs :+ Upstream.Hot.Channel(packet.add.removeUnknownTlvs(), packet.receivedAt, originNode, incomingChannelOccupancy), nextPayload, nextPacket_opt, handler, upgradeAccountability && packet.innerPayload.upgradeAccountability) case WrappedMultiPartPaymentFailed(MultiPartPaymentFSM.MultiPartPaymentFailed(_, failure, parts)) => context.log.warn("could not complete incoming multi-part payment (parts={} paidAmount={} failure={})", parts.size, parts.map(_.amount).sum, failure) @@ -515,8 +515,8 @@ class NodeRelay private(nodeParams: NodeParams, if (!fulfilledUpstream) { 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.settledAt)) + val incoming = upstream.received.map(r => PaymentEvent.IncomingPayment(r.add.channelId, r.receivedFrom, r.add.amountMsat, r.receivedAt)) + val outgoing = paymentSent.parts.map(p => PaymentEvent.OutgoingPayment(p.channelId, p.remoteNodeId, p.amountWithFees, p.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 cdc8fb1252..8637e7185b 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 @@ -21,12 +21,12 @@ import akka.actor.{Actor, ActorRef, DiagnosticActorLogging, Props, Stash} import akka.event.Logging.MDC import akka.event.LoggingAdapter import fr.acinq.bitcoin.scalacompat.ByteVector32 -import fr.acinq.bitcoin.scalacompat.Crypto.PrivateKey +import fr.acinq.bitcoin.scalacompat.Crypto.{PrivateKey, PublicKey} import fr.acinq.eclair.channel.Helpers.Closing import fr.acinq.eclair.channel._ import fr.acinq.eclair.db._ import fr.acinq.eclair.payment.Monitoring.Tags -import fr.acinq.eclair.payment.{ChannelPaymentRelayed, IncomingPaymentPacket, PaymentFailed, PaymentSent} +import fr.acinq.eclair.payment._ import fr.acinq.eclair.transactions.DirectedHtlc.outgoing import fr.acinq.eclair.wire.protocol._ import fr.acinq.eclair.{CustomCommitmentsPlugin, Feature, Features, Logs, MilliSatoshiLong, NodeParams, TimestampMilli} @@ -155,36 +155,36 @@ class PostRestartHtlcCleaner(nodeParams: NodeParams, register: ActorRef, initial case _: ChannelStateChanged => // ignore other channel state changes - case RES_ADD_SETTLED(o: Origin.Cold, htlc, fulfill: HtlcResult.Fulfill) => - log.info("htlc #{} from channelId={} fulfilled downstream", htlc.id, htlc.channelId) - handleDownstreamFulfill(brokenHtlcs, o, htlc, fulfill.paymentPreimage) + case RES_ADD_SETTLED(o: Origin.Cold, remoteNodeId, htlc, fulfill: HtlcResult.Fulfill) => + log.info("htlc #{} from channelId={} fulfilled downstream by {}", htlc.id, htlc.channelId, remoteNodeId) + handleDownstreamFulfill(brokenHtlcs, o, remoteNodeId, htlc, fulfill.paymentPreimage) - case RES_ADD_SETTLED(o: Origin.Cold, htlc, fail: HtlcResult.Fail) => + case RES_ADD_SETTLED(o: Origin.Cold, remoteNodeId, htlc, fail: HtlcResult.Fail) => if (htlc.fundingFee_opt.nonEmpty) { - log.info("htlc #{} from channelId={} failed downstream but has a pending on-the-fly funding", htlc.id, htlc.channelId) + log.info("htlc #{} from channelId={} failed downstream by {} but has a pending on-the-fly funding", htlc.id, htlc.channelId, remoteNodeId) // We don't fail upstream: we haven't been paid our funding fee yet, so we will try relaying again. } else { - log.info("htlc #{} from channelId={} failed downstream: {}", htlc.id, htlc.channelId, fail.getClass.getSimpleName) + log.info("htlc #{} from channelId={} failed downstream by {}: {}", htlc.id, htlc.channelId, remoteNodeId, fail.getClass.getSimpleName) handleDownstreamFailure(brokenHtlcs, o, htlc, fail) } case GetBrokenHtlcs => sender() ! brokenHtlcs } - private def handleDownstreamFulfill(brokenHtlcs: BrokenHtlcs, origin: Origin.Cold, fulfilledHtlc: UpdateAddHtlc, paymentPreimage: ByteVector32): Unit = + private def handleDownstreamFulfill(brokenHtlcs: BrokenHtlcs, origin: Origin.Cold, downstreamNodeId: PublicKey, fulfilledHtlc: UpdateAddHtlc, paymentPreimage: ByteVector32): Unit = brokenHtlcs.relayedOut.get(origin) match { case Some(relayedOut) => origin.upstream match { case Upstream.Local(id) => val feesPaid = 0.msat // fees are unknown since we lost the reference to the payment nodeParams.db.payments.getOutgoingPayment(id) match { case Some(p) => - 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)) + nodeParams.db.payments.updateOutgoingPayment(PaymentSent(p.parentId, paymentPreimage, p.recipientAmount, p.recipientNodeId, PaymentSent.PartialPayment(id, PaymentEvent.OutgoingPayment(fulfilledHtlc.channelId, downstreamNodeId, fulfilledHtlc.amountMsat, settledAt = TimestampMilli.now()), feesPaid, None, startedAt = p.createdAt) :: 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, _, _, createdAt, _, _, OutgoingPaymentStatus.Succeeded(_, feesPaid, _, completedAt)) => - PaymentSent.PartialPayment(id, amount, feesPaid, ByteVector32.Zeroes, None, createdAt, completedAt) + PaymentSent.PartialPayment(id, PaymentEvent.OutgoingPayment(ByteVector32.Zeroes, downstreamNodeId, amount, completedAt), feesPaid, None, createdAt) } 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})") @@ -198,27 +198,27 @@ class PostRestartHtlcCleaner(nodeParams: NodeParams, register: ActorRef, initial val dummyNodeId = nodeParams.nodeId 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)) + nodeParams.db.payments.updateOutgoingPayment(PaymentSent(id, paymentPreimage, dummyFinalAmount, dummyNodeId, PaymentSent.PartialPayment(id, PaymentEvent.OutgoingPayment(fulfilledHtlc.channelId, downstreamNodeId, fulfilledHtlc.amountMsat, settledAt = now), feesPaid, None, startedAt = 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. Metrics.PendingRelayedOut.decrement() context become main(brokenHtlcs.copy(relayedOut = brokenHtlcs.relayedOut - origin)) - case Upstream.Cold.Channel(originChannelId, originHtlcId, amountIn) => + case u: Upstream.Cold.Channel => log.info(s"received preimage for paymentHash=${fulfilledHtlc.paymentHash}: fulfilling 1 HTLC upstream") if (relayedOut != Set((fulfilledHtlc.channelId, fulfilledHtlc.id))) { log.error(s"unexpected channel relay downstream HTLCs: expected (${fulfilledHtlc.channelId},${fulfilledHtlc.id}), found $relayedOut") } - PendingCommandsDb.safeSend(register, nodeParams.db.pendingCommands, originChannelId, CMD_FULFILL_HTLC(originHtlcId, paymentPreimage, None, commit = true)) + PendingCommandsDb.safeSend(register, nodeParams.db.pendingCommands, u.originChannelId, CMD_FULFILL_HTLC(u.originHtlcId, paymentPreimage, None, commit = true)) // We don't know when we received this HTLC so we just pretend that we received it just now. - context.system.eventStream.publish(ChannelPaymentRelayed(amountIn, fulfilledHtlc.amountMsat, fulfilledHtlc.paymentHash, originChannelId, fulfilledHtlc.channelId, TimestampMilli.now(), TimestampMilli.now())) + context.system.eventStream.publish(ChannelPaymentRelayed(fulfilledHtlc.paymentHash, PaymentEvent.IncomingPayment(u.originChannelId, u.originNodeId, u.amountIn, TimestampMilli.now()), PaymentEvent.OutgoingPayment(fulfilledHtlc.channelId, downstreamNodeId, fulfilledHtlc.amountMsat, TimestampMilli.now()))) Metrics.PendingRelayedOut.decrement() context become main(brokenHtlcs.copy(relayedOut = brokenHtlcs.relayedOut - origin)) - case Upstream.Cold.Trampoline(originHtlcs) => + case u: Upstream.Cold.Trampoline => // We fulfill upstream as soon as we have the payment preimage available. if (!brokenHtlcs.settledUpstream.contains(origin)) { - log.info(s"received preimage for paymentHash=${fulfilledHtlc.paymentHash}: fulfilling ${originHtlcs.length} HTLCs upstream") - originHtlcs.foreach { case Upstream.Cold.Channel(channelId, htlcId, _) => + log.info("received preimage for paymentHash={}: fulfilling {} HTLCs upstream", fulfilledHtlc.paymentHash, u.originHtlcs.length) + u.originHtlcs.foreach { case Upstream.Cold.Channel(channelId, _, htlcId, _) => Metrics.Resolved.withTag(Tags.Success, value = true).withTag(Metrics.Relayed, value = true).increment() PendingCommandsDb.safeSend(register, nodeParams.db.pendingCommands, channelId, CMD_FULFILL_HTLC(htlcId, paymentPreimage, None, commit = true)) } @@ -260,8 +260,8 @@ class PostRestartHtlcCleaner(nodeParams: NodeParams, register: ActorRef, initial context.system.eventStream.publish(PaymentFailed(p.parentId, failedHtlc.paymentHash, Nil, p.createdAt, settledAt = TimestampMilli.now())) } }) - case Upstream.Cold.Channel(originChannelId, originHtlcId, _) => - log.warning(s"payment failed for paymentHash=${failedHtlc.paymentHash}: failing 1 HTLC upstream") + case u: Upstream.Cold.Channel => + log.warning("payment failed for paymentHash={}: failing 1 HTLC upstream", failedHtlc.paymentHash) Metrics.Resolved.withTag(Tags.Success, value = false).withTag(Metrics.Relayed, value = true).increment() val cmd = failedHtlc.pathKey_opt match { case Some(_) => @@ -269,14 +269,14 @@ class PostRestartHtlcCleaner(nodeParams: NodeParams, register: ActorRef, initial // we don't have access to the incoming onion: to avoid leaking information, we act as if we were an // intermediate node and send invalid_onion_blinding in an update_fail_malformed_htlc message. val failure = InvalidOnionBlinding(ByteVector32.Zeroes) - CMD_FAIL_MALFORMED_HTLC(originHtlcId, failure.onionHash, failure.code, commit = true) + CMD_FAIL_MALFORMED_HTLC(u.originHtlcId, failure.onionHash, failure.code, commit = true) case None => - ChannelRelay.translateRelayFailure(originHtlcId, fail, None) + ChannelRelay.translateRelayFailure(u.originHtlcId, fail, None) } - PendingCommandsDb.safeSend(register, nodeParams.db.pendingCommands, originChannelId, cmd) - case Upstream.Cold.Trampoline(originHtlcs) => - log.warning(s"payment failed for paymentHash=${failedHtlc.paymentHash}: failing ${originHtlcs.length} HTLCs upstream") - originHtlcs.foreach { case Upstream.Cold.Channel(channelId, htlcId, _) => + PendingCommandsDb.safeSend(register, nodeParams.db.pendingCommands, u.originChannelId, cmd) + case u: Upstream.Cold.Trampoline => + log.warning("payment failed for paymentHash={}: failing {} HTLCs upstream", failedHtlc.paymentHash, u.originHtlcs.length) + u.originHtlcs.foreach { case Upstream.Cold.Channel(channelId, _, htlcId, _) => Metrics.Resolved.withTag(Tags.Success, value = false).withTag(Metrics.Relayed, value = true).increment() // We don't bother decrypting the downstream failure to forward a more meaningful error upstream, it's // very likely that it won't be actionable anyway because of our node restart. diff --git a/eclair-core/src/main/scala/fr/acinq/eclair/payment/relay/Relayer.scala b/eclair-core/src/main/scala/fr/acinq/eclair/payment/relay/Relayer.scala index 7f4609269a..fe44843304 100644 --- a/eclair-core/src/main/scala/fr/acinq/eclair/payment/relay/Relayer.scala +++ b/eclair-core/src/main/scala/fr/acinq/eclair/payment/relay/Relayer.scala @@ -28,6 +28,7 @@ import fr.acinq.bitcoin.scalacompat.Crypto.PublicKey import fr.acinq.eclair.channel._ import fr.acinq.eclair.db.PendingCommandsDb import fr.acinq.eclair.payment._ +import fr.acinq.eclair.payment.receive.MultiPartHandler import fr.acinq.eclair.reputation.{Reputation, ReputationRecorder} import fr.acinq.eclair.wire.protocol._ import fr.acinq.eclair.{CltvExpiryDelta, Logs, MilliSatoshi, NodeParams, RealShortChannelId, TimestampMilli} @@ -68,7 +69,7 @@ class Relayer(nodeParams: NodeParams, router: ActorRef, register: ActorRef, paym IncomingPaymentPacket.decrypt(add, nodeParams.privateKey, nodeParams.features) match { case Right(p: IncomingPaymentPacket.FinalPacket) => log.debug(s"forwarding htlc #${add.id} to payment-handler") - paymentHandler forward p + paymentHandler forward MultiPartHandler.ReceivePacket(p, originNode) case Right(r: IncomingPaymentPacket.ChannelRelayPacket) => channelRelayer ! ChannelRelayer.Relay(r, originNode, incomingChannelOccupancy) case Right(r: IncomingPaymentPacket.NodeRelayPacket) => 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 a25cf1b462..f75a88231e 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 @@ -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/PaymentLifecycle.scala b/eclair-core/src/main/scala/fr/acinq/eclair/payment/send/PaymentLifecycle.scala index f571a0165a..93dabee06b 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 @@ -111,10 +111,10 @@ class PaymentLifecycle(nodeParams: NodeParams, cfg: SendPaymentConfig, router: A case Event(_: Register.ForwardShortIdFailure[_], d: WaitingForComplete) => handleLocalFail(d, DisconnectedException, isFatal = false) - case Event(RES_ADD_SETTLED(_, htlc, fulfill: HtlcResult.Fulfill), d: WaitingForComplete) => + case Event(RES_ADD_SETTLED(_, remoteNodeId, 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), startedAt = d.sentAt, settledAt = TimestampMilli.now()) + val p = PartialPayment(id, PaymentEvent.OutgoingPayment(htlc.channelId, remoteNodeId, d.cmd.amount, settledAt = TimestampMilli.now()), d.cmd.amount - d.request.amount, Some(d.route.fullRoute), startedAt = d.sentAt) val remainingAttribution_opt = fulfill match { case HtlcResult.RemoteFulfill(updateFulfill) => updateFulfill.attribution_opt match { @@ -130,7 +130,7 @@ class PaymentLifecycle(nodeParams: NodeParams, cfg: SendPaymentConfig, router: A } 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) => + case Event(RES_ADD_SETTLED(_, _, _, fail: HtlcResult.Fail), d: WaitingForComplete) => fail match { case HtlcResult.RemoteFail(fail) => handleRemoteFail(d, fail) case HtlcResult.RemoteFailMalformed(fail) => @@ -418,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 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 22cc576798..1f122d6aff 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 @@ -113,7 +113,8 @@ 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, startedAt = TimestampMilli.now(), settledAt = TimestampMilli.now()) // we will update settledAt below + val trampolineFees = computeFees(amount, attemptNumber) + val part = PartialPayment(cmd.paymentId, PaymentEvent.OutgoingPayment(channelId, cmd.trampolineNodeId, amount + trampolineFees, settledAt = TimestampMilli.now()), trampolineFees, None, startedAt = TimestampMilli.now()) // we will update settledAt below waitForSettlement(part, outgoing.onion.sharedSecrets, outgoing.trampolineOnion.sharedSecrets) } @@ -137,7 +138,7 @@ object TrampolinePaymentLifecycle { } case _: HtlcResult.OnChainFulfill => Nil } - parent ! HtlcSettled(fulfill, part.copy(settledAt = TimestampMilli.now()), holdTimes) + parent ! HtlcSettled(fulfill, part.copy(payment = part.payment.copy(settledAt = TimestampMilli.now())), holdTimes) Behaviors.stopped case fail: HtlcResult.Fail => val holdTimes = fail match { @@ -145,7 +146,7 @@ object TrampolinePaymentLifecycle { Sphinx.FailurePacket.decrypt(updateFail.reason, updateFail.attribution_opt, outerOnionSecrets).holdTimes case _ => Nil } - parent ! HtlcSettled(fail, part.copy(settledAt = TimestampMilli.now()), holdTimes) + parent ! HtlcSettled(fail, part.copy(payment = part.payment.copy(settledAt = TimestampMilli.now())), holdTimes) Behaviors.stopped } } diff --git a/eclair-core/src/main/scala/fr/acinq/eclair/wire/internal/channel/version5/ChannelCodecs5.scala b/eclair-core/src/main/scala/fr/acinq/eclair/wire/internal/channel/version5/ChannelCodecs5.scala index 82a9cd32a9..dbf45f2f71 100644 --- a/eclair-core/src/main/scala/fr/acinq/eclair/wire/internal/channel/version5/ChannelCodecs5.scala +++ b/eclair-core/src/main/scala/fr/acinq/eclair/wire/internal/channel/version5/ChannelCodecs5.scala @@ -16,8 +16,9 @@ package fr.acinq.eclair.wire.internal.channel.version5 +import fr.acinq.bitcoin.scalacompat.Crypto.PrivateKey import fr.acinq.bitcoin.scalacompat.DeterministicWallet.KeyPath -import fr.acinq.bitcoin.scalacompat.{OutPoint, ScriptWitness, Transaction, TxOut} +import fr.acinq.bitcoin.scalacompat.{ByteVector32, OutPoint, ScriptWitness, Transaction, TxOut} import fr.acinq.eclair.channel._ import fr.acinq.eclair.channel.fund.{InteractiveTxBuilder, InteractiveTxSigningSession} import fr.acinq.eclair.crypto.ShaChain @@ -335,15 +336,24 @@ private[channel] object ChannelCodecs5 { ("localNextHtlcId" | uint64overflow) :: ("remoteNextHtlcId" | uint64overflow)).as[CommitmentChanges] + private val upstreamChannelCodecWithoutNodeId: Codec[Upstream.Cold.Channel] = ( + ("originChannelId" | bytes32) :: + ("originNodeId" | provide(PrivateKey(ByteVector32.One).publicKey)) :: + ("originHtlcId" | int64) :: + ("amountIn" | millisatoshi)).as[Upstream.Cold.Channel] + private val upstreamChannelCodec: Codec[Upstream.Cold.Channel] = ( ("originChannelId" | bytes32) :: + ("originNodeId" | publicKey) :: ("originHtlcId" | int64) :: ("amountIn" | millisatoshi)).as[Upstream.Cold.Channel] private val coldUpstreamCodec: Codec[Upstream.Cold] = discriminated[Upstream.Cold].by(uint16) // NB: order matters! - .typecase(0x03, upstreamChannelCodec) - .typecase(0x02, listOfN(uint16, upstreamChannelCodec).as[Upstream.Cold.Trampoline]) + .typecase(0x05, listOfN(uint16, upstreamChannelCodec).as[Upstream.Cold.Trampoline]) + .typecase(0x04, upstreamChannelCodec) + .typecase(0x03, upstreamChannelCodecWithoutNodeId) + .typecase(0x02, listOfN(uint16, upstreamChannelCodecWithoutNodeId).as[Upstream.Cold.Trampoline]) .typecase(0x01, ("id" | uuid).as[Upstream.Local]) private val originCodec: Codec[Origin] = coldUpstreamCodec.xmap[Origin]( diff --git a/eclair-core/src/test/scala/fr/acinq/eclair/channel/FuzzySpec.scala b/eclair-core/src/test/scala/fr/acinq/eclair/channel/FuzzySpec.scala index 86c3d4b6a9..9946f03107 100644 --- a/eclair-core/src/test/scala/fr/acinq/eclair/channel/FuzzySpec.scala +++ b/eclair-core/src/test/scala/fr/acinq/eclair/channel/FuzzySpec.scala @@ -134,10 +134,10 @@ class FuzzySpec extends TestKitBaseClass with FixtureAnyFunSuiteLike with Channe sendChannel ! buildCmdAdd(invoice) context become { case RES_SUCCESS(_: CMD_ADD_HTLC, _) => () - case RES_ADD_SETTLED(_, htlc, _: HtlcResult.Fulfill) => + case RES_ADD_SETTLED(_, _, htlc, _: HtlcResult.Fulfill) => log.info(s"successfully sent htlc #${htlc.id}") initiatePaymentOrStop(remaining - 1) - case RES_ADD_SETTLED(_, htlc, _: HtlcResult.Fail) => + case RES_ADD_SETTLED(_, _, htlc, _: HtlcResult.Fail) => log.warning(s"htlc failed: ${htlc.id}") initiatePaymentOrStop(remaining - 1) case RES_ADD_FAILED(_, t: Throwable, _) => diff --git a/eclair-core/src/test/scala/fr/acinq/eclair/channel/states/h/ClosingStateSpec.scala b/eclair-core/src/test/scala/fr/acinq/eclair/channel/states/h/ClosingStateSpec.scala index 730303e9c3..652a60f340 100644 --- a/eclair-core/src/test/scala/fr/acinq/eclair/channel/states/h/ClosingStateSpec.scala +++ b/eclair-core/src/test/scala/fr/acinq/eclair/channel/states/h/ClosingStateSpec.scala @@ -795,7 +795,7 @@ class ClosingStateSpec extends TestKitBaseClass with FixtureAnyFunSuiteLike with val origin = alice.stateData.asInstanceOf[DATA_CLOSING].commitments.originChannels(htlc.id) alice ! WatchTxConfirmedTriggered(BlockHeight(0), 0, aliceCommitTx) // so she fails it - alice2relayer.expectMsg(RES_ADD_SETTLED(origin, htlc, HtlcResult.OnChainFail(HtlcOverriddenByLocalCommit(channelId(alice), htlc)))) + alice2relayer.expectMsg(RES_ADD_SETTLED(origin, bob.nodeParams.nodeId, htlc, HtlcResult.OnChainFail(HtlcOverriddenByLocalCommit(channelId(alice), htlc)))) // the htlc will not settle on chain listener.expectNoMessage(100 millis) alice2relayer.expectNoMessage(100 millis) @@ -909,7 +909,7 @@ class ClosingStateSpec extends TestKitBaseClass with FixtureAnyFunSuiteLike with val origin = alice.stateData.asInstanceOf[DATA_CLOSING].commitments.originChannels(htlc.id) alice ! WatchTxConfirmedTriggered(BlockHeight(0), 0, closingState.commitTx) // so she fails it - alice2relayer.expectMsg(RES_ADD_SETTLED(origin, htlc, HtlcResult.OnChainFail(HtlcOverriddenByLocalCommit(channelId(alice), htlc)))) + alice2relayer.expectMsg(RES_ADD_SETTLED(origin, bob.nodeParams.nodeId, htlc, HtlcResult.OnChainFail(HtlcOverriddenByLocalCommit(channelId(alice), htlc)))) // the htlc will not settle on chain listener.expectNoMessage(100 millis) alice2relayer.expectNoMessage(100 millis) @@ -1269,7 +1269,7 @@ class ClosingStateSpec extends TestKitBaseClass with FixtureAnyFunSuiteLike with val origin = alice.stateData.asInstanceOf[DATA_CLOSING].commitments.originChannels(htlc.id) alice ! WatchTxConfirmedTriggered(BlockHeight(0), 0, bobCommitTx) // so she fails it - alice2relayer.expectMsg(RES_ADD_SETTLED(origin, htlc, HtlcResult.OnChainFail(HtlcOverriddenByLocalCommit(channelId(alice), htlc)))) + alice2relayer.expectMsg(RES_ADD_SETTLED(origin, bob.nodeParams.nodeId, htlc, HtlcResult.OnChainFail(HtlcOverriddenByLocalCommit(channelId(alice), htlc)))) // the htlc will not settle on chain listener.expectNoMessage(100 millis) alice2relayer.expectNoMessage(100 millis) 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 88bdd5364d..19e84eade1 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 @@ -16,7 +16,7 @@ package fr.acinq.eclair.db -import fr.acinq.bitcoin.scalacompat.Crypto.PublicKey +import fr.acinq.bitcoin.scalacompat.Crypto.{PrivateKey, PublicKey} import fr.acinq.bitcoin.scalacompat.{Block, ByteVector32, SatoshiLong, Script, Transaction, TxOut} import fr.acinq.eclair.TestDatabases.{TestPgDatabases, TestSqliteDatabases} import fr.acinq.eclair.TestUtils.randomTxId @@ -61,36 +61,39 @@ class AuditDbSpec extends AnyFunSuite { test("add/list events") { forAllDbs { dbs => val db = dbs.audit + // We don't yet store the remote node_id in our DB: we use this placeholder instead. + // TODO: update this test once we store the remote node_id for incoming/outgoing payments. + val dummyRemoteNodeId = PrivateKey(ByteVector32.One).publicKey val now = TimestampMilli.now() - 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 e1 = PaymentSent(ZERO_UUID, randomBytes32(), 40000 msat, randomKey().publicKey, PaymentSent.PartialPayment(ZERO_UUID, PaymentEvent.OutgoingPayment(randomBytes32(), dummyRemoteNodeId, 42000 msat, now), 1000 msat, None, now) :: Nil, None, now) + val pp2a = PaymentEvent.IncomingPayment(randomBytes32(), dummyRemoteNodeId, 42000 msat, now) + val pp2b = PaymentEvent.IncomingPayment(randomBytes32(), dummyRemoteNodeId, 42100 msat, now) val e2 = PaymentReceived(randomBytes32(), pp2a :: pp2b :: Nil) - val e3 = ChannelPaymentRelayed(42000 msat, 1000 msat, randomBytes32(), randomBytes32(), randomBytes32(), now - 3.seconds, now) + val e3 = ChannelPaymentRelayed(randomBytes32(), PaymentEvent.IncomingPayment(randomBytes32(), dummyRemoteNodeId, 42000 msat, now - 3.seconds), PaymentEvent.OutgoingPayment(randomBytes32(), dummyRemoteNodeId, 1000 msat, now)) 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, 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 pp5a = PaymentSent.PartialPayment(UUID.randomUUID(), PaymentEvent.OutgoingPayment(randomBytes32(), dummyRemoteNodeId, 42000 msat, 0 unixms), 1000 msat, None, startedAt = 0 unixms) + val pp5b = PaymentSent.PartialPayment(UUID.randomUUID(), PaymentEvent.OutgoingPayment(randomBytes32(), dummyRemoteNodeId, 42100 msat, 1 unixms), 900 msat, None, startedAt = 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 pp6 = PaymentSent.PartialPayment(UUID.randomUUID(), PaymentEvent.OutgoingPayment(randomBytes32(), dummyRemoteNodeId, 42000 msat, settledAt = now + 10.minutes), 1000 msat, None, startedAt = 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( - PaymentRelayed.IncomingPart(20000 msat, randomBytes32(), now - 7.seconds), - PaymentRelayed.IncomingPart(22000 msat, randomBytes32(), now - 5.seconds) + PaymentEvent.IncomingPayment(randomBytes32(), dummyRemoteNodeId, 20000 msat, now - 7.seconds), + PaymentEvent.IncomingPayment(randomBytes32(), dummyRemoteNodeId, 22000 msat, now - 5.seconds) ), Seq( - PaymentRelayed.OutgoingPart(10000 msat, randomBytes32(), now + 1.milli), - PaymentRelayed.OutgoingPart(12000 msat, randomBytes32(), now + 2.milli), - PaymentRelayed.OutgoingPart(15000 msat, randomBytes32(), now + 3.milli) + PaymentEvent.OutgoingPayment(randomBytes32(), dummyRemoteNodeId, 10000 msat, now + 1.milli), + PaymentEvent.OutgoingPayment(randomBytes32(), dummyRemoteNodeId, 12000 msat, now + 2.milli), + PaymentEvent.OutgoingPayment(randomBytes32(), dummyRemoteNodeId, 15000 msat, now + 3.milli) ), randomKey().publicKey, 30000 msat) val multiPartPaymentHash = randomBytes32() - val e11 = ChannelPaymentRelayed(13000 msat, 11000 msat, multiPartPaymentHash, randomBytes32(), randomBytes32(), now - 5.seconds, now + 4.milli) - val e12 = ChannelPaymentRelayed(15000 msat, 12500 msat, multiPartPaymentHash, randomBytes32(), randomBytes32(), now - 4.seconds, now + 5.milli) + val e11 = ChannelPaymentRelayed(multiPartPaymentHash, PaymentEvent.IncomingPayment(randomBytes32(), dummyRemoteNodeId, 13000 msat, now - 5.seconds), PaymentEvent.OutgoingPayment(randomBytes32(), dummyRemoteNodeId, 11000 msat, now + 4.milli)) + val e12 = ChannelPaymentRelayed(multiPartPaymentHash, PaymentEvent.IncomingPayment(randomBytes32(), dummyRemoteNodeId, 15000 msat, now - 4.seconds), PaymentEvent.OutgoingPayment(randomBytes32(), dummyRemoteNodeId, 12500 msat, now + 5.milli)) db.add(e1) db.add(e2) @@ -141,13 +144,13 @@ class AuditDbSpec extends AnyFunSuite { val c5 = c1.copy(bytes = 0x05b +: c1.tail) val c6 = c1.copy(bytes = 0x06b +: c1.tail) - db.add(ChannelPaymentRelayed(46000 msat, 44000 msat, randomBytes32(), c6, c1, 1000 unixms, 1001 unixms)) - db.add(ChannelPaymentRelayed(41000 msat, 40000 msat, randomBytes32(), c6, c1, 1002 unixms, 1003 unixms)) - db.add(ChannelPaymentRelayed(43000 msat, 42000 msat, randomBytes32(), c5, c1, 1004 unixms, 1005 unixms)) - db.add(ChannelPaymentRelayed(42000 msat, 40000 msat, randomBytes32(), c5, c2, 1006 unixms, 1007 unixms)) - db.add(ChannelPaymentRelayed(45000 msat, 40000 msat, randomBytes32(), c5, c6, 1008 unixms, 1009 unixms)) - db.add(TrampolinePaymentRelayed(randomBytes32(), Seq(PaymentRelayed.IncomingPart(25000 msat, c6, 1010 unixms)), Seq(PaymentRelayed.OutgoingPart(20000 msat, c4, 1011 unixms)), randomKey().publicKey, 15000 msat)) - db.add(TrampolinePaymentRelayed(randomBytes32(), Seq(PaymentRelayed.IncomingPart(46000 msat, c6, 1012 unixms)), Seq(PaymentRelayed.OutgoingPart(16000 msat, c2, 1013 unixms), PaymentRelayed.OutgoingPart(10000 msat, c4, 1014 unixms), PaymentRelayed.OutgoingPart(14000 msat, c4, 1015 unixms)), randomKey().publicKey, 37000 msat)) + db.add(ChannelPaymentRelayed(randomBytes32(), PaymentEvent.IncomingPayment(c6, randomKey().publicKey, 46000 msat, 1000 unixms), PaymentEvent.OutgoingPayment(c1, randomKey().publicKey, 44000 msat, 1001 unixms))) + db.add(ChannelPaymentRelayed(randomBytes32(), PaymentEvent.IncomingPayment(c6, randomKey().publicKey, 41000 msat, 1002 unixms), PaymentEvent.OutgoingPayment(c1, randomKey().publicKey, 40000 msat, 1003 unixms))) + db.add(ChannelPaymentRelayed(randomBytes32(), PaymentEvent.IncomingPayment(c5, randomKey().publicKey, 43000 msat, 1004 unixms), PaymentEvent.OutgoingPayment(c1, randomKey().publicKey, 42000 msat, 1005 unixms))) + db.add(ChannelPaymentRelayed(randomBytes32(), PaymentEvent.IncomingPayment(c5, randomKey().publicKey, 42000 msat, 1006 unixms), PaymentEvent.OutgoingPayment(c2, randomKey().publicKey, 40000 msat, 1007 unixms))) + db.add(ChannelPaymentRelayed(randomBytes32(), PaymentEvent.IncomingPayment(c5, randomKey().publicKey, 45000 msat, 1008 unixms), PaymentEvent.OutgoingPayment(c6, randomKey().publicKey, 40000 msat, 1009 unixms))) + db.add(TrampolinePaymentRelayed(randomBytes32(), Seq(PaymentEvent.IncomingPayment(c6, randomKey().publicKey, 25000 msat, 1010 unixms)), Seq(PaymentEvent.OutgoingPayment(c4, randomKey().publicKey, 20000 msat, 1011 unixms)), randomKey().publicKey, 15000 msat)) + db.add(TrampolinePaymentRelayed(randomBytes32(), Seq(PaymentEvent.IncomingPayment(c6, randomKey().publicKey, 46000 msat, 1012 unixms)), Seq(PaymentEvent.OutgoingPayment(c2, randomKey().publicKey, 16000 msat, 1013 unixms), PaymentEvent.OutgoingPayment(c4, randomKey().publicKey, 10000 msat, 1014 unixms), PaymentEvent.OutgoingPayment(c4, randomKey().publicKey, 14000 msat, 1015 unixms)), randomKey().publicKey, 37000 msat)) // The following confirmed txs will be taken into account. db.add(TransactionPublished(c2, n2, Transaction(0, Seq.empty, Seq(TxOut(5000 sat, hex"12345")), 0), 200 sat, "funding")) @@ -209,12 +212,12 @@ class AuditDbSpec extends AnyFunSuite { // 25% trampoline relays. if (Random.nextInt(4) == 0) { val outgoingCount = 1 + Random.nextInt(4) - val incoming = Seq(PaymentRelayed.IncomingPart(10000 msat, randomBytes32(), TimestampMilli.now() - 3.seconds)) - val outgoing = (1 to outgoingCount).map(_ => PaymentRelayed.OutgoingPart(Random.nextInt(2000).msat, channelIds(Random.nextInt(channelCount)), TimestampMilli.now())) + val incoming = Seq(PaymentEvent.IncomingPayment(randomBytes32(), randomKey().publicKey, 10000 msat, TimestampMilli.now() - 3.seconds)) + val outgoing = (1 to outgoingCount).map(_ => PaymentEvent.OutgoingPayment(channelIds(Random.nextInt(channelCount)), randomKey().publicKey, Random.nextInt(2000).msat, TimestampMilli.now())) db.add(TrampolinePaymentRelayed(randomBytes32(), incoming, outgoing, randomKey().publicKey, 5000 msat)) } else { val toChannelId = channelIds(Random.nextInt(channelCount)) - db.add(ChannelPaymentRelayed(10000 msat, Random.nextInt(10000).msat, randomBytes32(), randomBytes32(), toChannelId, TimestampMilli.now() - 2.seconds, TimestampMilli.now())) + db.add(ChannelPaymentRelayed(randomBytes32(), PaymentEvent.IncomingPayment(randomBytes32(), randomKey().publicKey, 10000 msat, TimestampMilli.now() - 2.seconds), PaymentEvent.OutgoingPayment(toChannelId, randomKey().publicKey, Random.nextInt(10000).msat, TimestampMilli.now()))) } }) // Test starts here. 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 238e6a2fae..55092dfcba 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,7 +200,7 @@ 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, preimage1, ps5.amount, ps5.recipientNodeId, Seq(PaymentSent.PartialPayment(ps5.id, ps5.amount, 42 msat, randomBytes32(), None, 1000 unixms, 1180 unixms)), None, 900 unixms)) + db.updateOutgoingPayment(PaymentSent(ps5.parentId, preimage1, ps5.amount, ps5.recipientNodeId, Seq(PaymentSent.PartialPayment(ps5.id, PaymentEvent.OutgoingPayment(randomBytes32(), randomKey().publicKey, ps5.amount, 1180 unixms), 42 msat, None, 1000 unixms)), None, 900 unixms)) db.addOutgoingPayment(ps6.copy(status = OutgoingPaymentStatus.Pending)) db.updateOutgoingPayment(PaymentFailed(ps6.id, ps6.paymentHash, Nil, 1100 unixms, 1300 unixms)) @@ -772,11 +772,11 @@ class PaymentsDbSpec extends AnyFunSuite { assert(db.getOutgoingPayment(s4.id).contains(ss4)) // can't update again once it's in a final state - 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))) + assertThrows[IllegalArgumentException](db.updateOutgoingPayment(PaymentSent(parentId, preimage1, s3.recipientAmount, s3.recipientNodeId, Seq(PaymentSent.PartialPayment(s3.id, PaymentEvent.OutgoingPayment(randomBytes32(), randomKey().publicKey, s3.amount, settledAt = 500 unixms), 42 msat, None, startedAt = 100 unixms)), None, startedAt = 100 unixms))) 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) + PaymentSent.PartialPayment(s1.id, PaymentEvent.OutgoingPayment(randomBytes32(), randomKey().publicKey, s1.amount, settledAt = 400 unixms), 15 msat, None, startedAt = 200 unixms), + PaymentSent.PartialPayment(s2.id, PaymentEvent.OutgoingPayment(randomBytes32(), randomKey().publicKey, s2.amount, settledAt = 410 unixms), 20 msat, Some(Seq(hop_ab, hop_bc)), startedAt = 210 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)) diff --git a/eclair-core/src/test/scala/fr/acinq/eclair/db/PgUtilsSpec.scala b/eclair-core/src/test/scala/fr/acinq/eclair/db/PgUtilsSpec.scala index 3ce41af8f6..89bfd76c5b 100644 --- a/eclair-core/src/test/scala/fr/acinq/eclair/db/PgUtilsSpec.scala +++ b/eclair-core/src/test/scala/fr/acinq/eclair/db/PgUtilsSpec.scala @@ -6,7 +6,7 @@ import fr.acinq.eclair.db.DbEventHandler.ChannelEvent import fr.acinq.eclair.db.pg.PgUtils.ExtendedResultSet._ import fr.acinq.eclair.db.pg.PgUtils.PgLock.{LeaseLock, LockFailure, LockFailureHandler} import fr.acinq.eclair.db.pg.PgUtils.{migrateTable, using} -import fr.acinq.eclair.payment.ChannelPaymentRelayed +import fr.acinq.eclair.payment.{ChannelPaymentRelayed, PaymentEvent} import fr.acinq.eclair.router.Announcements import fr.acinq.eclair.wire.internal.channel.ChannelCodecsSpec import fr.acinq.eclair.wire.protocol.Color @@ -173,7 +173,7 @@ class PgUtilsSpec extends TestKitBaseClass with AnyFunSuiteLike with Eventually db.network.addNode(Announcements.makeNodeAnnouncement(randomKey(), "node-A", Color(50, 99, -80), Nil, Features.empty, TimestampSecond.now() - 45.days)) db.network.addNode(Announcements.makeNodeAnnouncement(randomKey(), "node-B", Color(50, 99, -80), Nil, Features.empty, TimestampSecond.now() - 3.days)) db.network.addNode(Announcements.makeNodeAnnouncement(randomKey(), "node-C", Color(50, 99, -80), Nil, Features.empty, TimestampSecond.now() - 7.minutes)) - db.audit.add(ChannelPaymentRelayed(421 msat, 400 msat, randomBytes32(), randomBytes32(), randomBytes32(), TimestampMilli.now() - 5.seconds, TimestampMilli.now() - 3.seconds)) + db.audit.add(ChannelPaymentRelayed(randomBytes32(), PaymentEvent.IncomingPayment(randomBytes32(), randomKey().publicKey, 421 msat, TimestampMilli.now() - 5.seconds), PaymentEvent.OutgoingPayment(randomBytes32(), randomKey().publicKey, 400 msat, TimestampMilli.now() - 3.seconds))) db.dataSource.close() } diff --git a/eclair-core/src/test/scala/fr/acinq/eclair/integration/ChannelIntegrationSpec.scala b/eclair-core/src/test/scala/fr/acinq/eclair/integration/ChannelIntegrationSpec.scala index 4af0971755..761da2b6d9 100644 --- a/eclair-core/src/test/scala/fr/acinq/eclair/integration/ChannelIntegrationSpec.scala +++ b/eclair-core/src/test/scala/fr/acinq/eclair/integration/ChannelIntegrationSpec.scala @@ -31,7 +31,7 @@ import fr.acinq.eclair.crypto.Sphinx.DecryptedFailurePacket import fr.acinq.eclair.io.{Peer, Switchboard} import fr.acinq.eclair.payment._ import fr.acinq.eclair.payment.receive.MultiPartHandler.ReceiveStandardPayment -import fr.acinq.eclair.payment.receive.{ForwardHandler, PaymentHandler} +import fr.acinq.eclair.payment.receive.{ForwardHandler, MultiPartHandler, PaymentHandler} import fr.acinq.eclair.payment.send.PaymentInitiator.SendPaymentToNode import fr.acinq.eclair.router.Router import fr.acinq.eclair.transactions.{OutgoingHtlc, Scripts, Transactions} @@ -143,7 +143,7 @@ abstract class ChannelIntegrationSpec extends IntegrationSpec { paymentSender.send(nodes("A").paymentInitiator, paymentReq) val paymentId = paymentSender.expectMsgType[UUID] // F gets the htlc - val htlc = htlcReceiver.expectMsgType[IncomingPaymentPacket.FinalPacket](max = 60 seconds).add + val htlc = htlcReceiver.expectMsgType[MultiPartHandler.ReceivePacket](max = 60 seconds).packet.add // now that we have the channel id, we retrieve channels default final addresses sender.send(nodes("C").register, Register.Forward(sender.ref.toTyped[Any], htlc.channelId, CMD_GET_CHANNEL_DATA(ActorRef.noSender))) val dataC = sender.expectMsgType[RES_GET_CHANNEL_DATA[DATA_NORMAL]].data @@ -372,19 +372,19 @@ abstract class ChannelIntegrationSpec extends IntegrationSpec { val buffer = TestProbe() send(100000000 msat, paymentHandlerF, nodes("C").paymentInitiator) - forwardHandlerF.expectMsgType[IncomingPaymentPacket.FinalPacket](max = 60 seconds) + forwardHandlerF.expectMsgType[MultiPartHandler.ReceivePacket](max = 60 seconds) forwardHandlerF.forward(buffer.ref) sigListener.expectMsgType[ChannelSignatureReceived] send(110000000 msat, paymentHandlerF, nodes("C").paymentInitiator) - forwardHandlerF.expectMsgType[IncomingPaymentPacket.FinalPacket](max = 60 seconds) + forwardHandlerF.expectMsgType[MultiPartHandler.ReceivePacket](max = 60 seconds) forwardHandlerF.forward(buffer.ref) sigListener.expectMsgType[ChannelSignatureReceived] send(120000000 msat, paymentHandlerC, nodes("F").paymentInitiator) - forwardHandlerC.expectMsgType[IncomingPaymentPacket.FinalPacket](max = 60 seconds) + forwardHandlerC.expectMsgType[MultiPartHandler.ReceivePacket](max = 60 seconds) forwardHandlerC.forward(buffer.ref) sigListener.expectMsgType[ChannelSignatureReceived] send(130000000 msat, paymentHandlerC, nodes("F").paymentInitiator) - forwardHandlerC.expectMsgType[IncomingPaymentPacket.FinalPacket](max = 60 seconds) + forwardHandlerC.expectMsgType[MultiPartHandler.ReceivePacket](max = 60 seconds) forwardHandlerC.forward(buffer.ref) val commitmentsF = sigListener.expectMsgType[ChannelSignatureReceived].commitments sigListener.expectNoMessage(1 second) @@ -402,19 +402,19 @@ abstract class ChannelIntegrationSpec extends IntegrationSpec { assert(htlcTimeoutTxs.size == 2) assert(htlcSuccessTxs.size == 2) // we fulfill htlcs to get the preimages - buffer.expectMsgType[IncomingPaymentPacket.FinalPacket] + buffer.expectMsgType[MultiPartHandler.ReceivePacket] buffer.forward(paymentHandlerF) sigListener.expectMsgType[ChannelSignatureReceived] val preimage1 = sender.expectMsgType[PaymentSent].paymentPreimage - buffer.expectMsgType[IncomingPaymentPacket.FinalPacket] + buffer.expectMsgType[MultiPartHandler.ReceivePacket] buffer.forward(paymentHandlerF) sigListener.expectMsgType[ChannelSignatureReceived] val preimage2 = sender.expectMsgType[PaymentSent].paymentPreimage - buffer.expectMsgType[IncomingPaymentPacket.FinalPacket] + buffer.expectMsgType[MultiPartHandler.ReceivePacket] buffer.forward(paymentHandlerC) sigListener.expectMsgType[ChannelSignatureReceived] sender.expectMsgType[PaymentSent] - buffer.expectMsgType[IncomingPaymentPacket.FinalPacket] + buffer.expectMsgType[MultiPartHandler.ReceivePacket] buffer.forward(paymentHandlerC) sigListener.expectMsgType[ChannelSignatureReceived] sender.expectMsgType[PaymentSent] 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 cad805fc12..3e57e05b27 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 @@ -383,7 +383,7 @@ class PaymentIntegrationSpec extends IntegrationSpec { assert(sent.length == 1, 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), + parts = paymentSent.parts.map(p => p.copy(payment = p.payment.copy(remoteNodeId = PrivateKey(ByteVector32.One).publicKey), route = None, startedAt = p.settledAt)).sortBy(_.settledAt), // We don't store attribution data in the DB. remainingAttribution_opt = None, startedAt = 0 unixms, 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 857eb2933a..ffdd099ba7 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.{ChannelPaymentRelayed, Invoice} +import fr.acinq.eclair.payment.{ChannelPaymentRelayed, Invoice, PaymentEvent} import fr.acinq.eclair.transactions.Transactions._ import fr.acinq.eclair.transactions.{CommitmentSpec, IncomingHtlc, OutgoingHtlc, Transactions} import fr.acinq.eclair.wire.internal.channel.ChannelCodecs @@ -282,16 +282,16 @@ class JsonSerializersSpec extends TestKitBaseClass with AnyFunSuiteLike with Mat val expectedLocalOrigin = """{"paymentId":"11111111-1111-1111-1111-111111111111"}""" JsonSerializers.serialization.write(localOrigin)(org.json4s.DefaultFormats + OriginSerializer) shouldBe expectedLocalOrigin - val channelOrigin = Origin.Cold(Upstream.Cold.Channel(ByteVector32(hex"345b2b05ec046ffe0c14d3b61838c79980713ad1cf8ae7a45c172ce90c9c0b9f"), 7, 500 msat)) - val expectedChannelOrigin = """{"channelId":"345b2b05ec046ffe0c14d3b61838c79980713ad1cf8ae7a45c172ce90c9c0b9f","htlcId":7,"amount":500}""" + val channelOrigin = Origin.Cold(Upstream.Cold.Channel(ByteVector32(hex"345b2b05ec046ffe0c14d3b61838c79980713ad1cf8ae7a45c172ce90c9c0b9f"), PublicKey(hex"0270685ca81a8e4d4d01beec5781f4cc924684072ae52c507f8ebe9daf0caaab7b"), 7, 500 msat)) + val expectedChannelOrigin = """{"channelId":"345b2b05ec046ffe0c14d3b61838c79980713ad1cf8ae7a45c172ce90c9c0b9f","remoteNodeId":"0270685ca81a8e4d4d01beec5781f4cc924684072ae52c507f8ebe9daf0caaab7b","htlcId":7,"amount":500}""" JsonSerializers.serialization.write(channelOrigin)(org.json4s.DefaultFormats + OriginSerializer) shouldBe expectedChannelOrigin val relayedHtlcs = List( - Upstream.Cold.Channel(ByteVector32(hex"9fcd45bbaa09c60c991ac0425704163c3f3d2d683c789fa409455b9c97792692"), 3, 600 msat), - Upstream.Cold.Channel(ByteVector32(hex"70685ca81a8e4d4d01beec5781f4cc924684072ae52c507f8ebe9daf0caaab7b"), 7, 500 msat), + Upstream.Cold.Channel(ByteVector32(hex"9fcd45bbaa09c60c991ac0425704163c3f3d2d683c789fa409455b9c97792692"), PublicKey(hex"0270685ca81a8e4d4d01beec5781f4cc924684072ae52c507f8ebe9daf0caaab7b"), 3, 600 msat), + Upstream.Cold.Channel(ByteVector32(hex"70685ca81a8e4d4d01beec5781f4cc924684072ae52c507f8ebe9daf0caaab7b"), PublicKey(hex"03864ef025fde8fb587d989186ce6a4a186895ee44a926bfc370e2c366597a3f8f"), 7, 500 msat), ) val trampolineOrigin = Origin.Cold(Upstream.Cold.Trampoline(relayedHtlcs)) - val expectedTrampolineOrigin = """[{"channelId":"9fcd45bbaa09c60c991ac0425704163c3f3d2d683c789fa409455b9c97792692","htlcId":3,"amount":600},{"channelId":"70685ca81a8e4d4d01beec5781f4cc924684072ae52c507f8ebe9daf0caaab7b","htlcId":7,"amount":500}]""" + val expectedTrampolineOrigin = """[{"channelId":"9fcd45bbaa09c60c991ac0425704163c3f3d2d683c789fa409455b9c97792692","remoteNodeId":"0270685ca81a8e4d4d01beec5781f4cc924684072ae52c507f8ebe9daf0caaab7b","htlcId":3,"amount":600},{"channelId":"70685ca81a8e4d4d01beec5781f4cc924684072ae52c507f8ebe9daf0caaab7b","remoteNodeId":"03864ef025fde8fb587d989186ce6a4a186895ee44a926bfc370e2c366597a3f8f","htlcId":7,"amount":500}]""" JsonSerializers.serialization.write(trampolineOrigin)(org.json4s.DefaultFormats + OriginSerializer) shouldBe expectedTrampolineOrigin } @@ -404,7 +404,7 @@ class JsonSerializersSpec extends TestKitBaseClass with AnyFunSuiteLike with Mat } test("type hints") { - val e1 = ChannelPaymentRelayed(110 msat, 100 msat, randomBytes32(), randomBytes32(), randomBytes32(), 100 unixms, 150 unixms) + val e1 = ChannelPaymentRelayed(randomBytes32(), PaymentEvent.IncomingPayment(randomBytes32(), randomKey().publicKey, 110 msat, 100 unixms), PaymentEvent.OutgoingPayment(randomBytes32(), randomKey().publicKey, 100 msat, 150 unixms)) assert(JsonSerializers.serialization.writePretty(e1)(JsonSerializers.formats).contains("\"type\" : \"payment-relayed\"")) } 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 0557333ade..715a241ea7 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 @@ -25,7 +25,6 @@ import fr.acinq.eclair.TestConstants.Alice 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 import fr.acinq.eclair.payment.offer.OfferManager import fr.acinq.eclair.payment.receive.MultiPartHandler._ import fr.acinq.eclair.payment.receive.MultiPartPaymentFSM.HtlcPart @@ -105,6 +104,7 @@ class MultiPartHandlerSpec extends TestKitBaseClass with FixtureAnyFunSuiteLike import f._ val amountMsat = 42000 msat + val receivedFrom = randomKey().publicKey val receivedAt = TimestampMilli.now() { @@ -117,11 +117,11 @@ 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), receivedAt)) + sender.send(handlerWithoutMpp, ReceivePacket(IncomingPaymentPacket.FinalPacket(add, FinalPayload.Standard.createPayload(add.amountMsat, add.amountMsat, add.cltvExpiry, invoice.paymentSecret, invoice.paymentMetadata, upgradeAccountability = false), receivedAt), receivedFrom)) assert(register.expectMsgType[Register.Forward[CMD_FULFILL_HTLC]].message.id == add.id) val paymentReceived = eventListener.expectMsgType[PaymentReceived] - assert(paymentReceived == PaymentReceived(add.paymentHash, PartialPayment(amountMsat, add.channelId, receivedAt) :: Nil)) + assert(paymentReceived == PaymentReceived(add.paymentHash, PaymentEvent.IncomingPayment(add.channelId, receivedFrom, amountMsat, 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] == IncomingPaymentStatus.Received(amountMsat, receivedAt)) @@ -133,11 +133,11 @@ 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), receivedAt)) + sender.send(handlerWithoutMpp, ReceivePacket(IncomingPaymentPacket.FinalPacket(add, FinalPayload.Standard.createPayload(70_000 msat, 70_000 msat, defaultExpiry, invoice.paymentSecret, invoice.paymentMetadata, upgradeAccountability = false), receivedAt), receivedFrom)) assert(register.expectMsgType[Register.Forward[CMD_FULFILL_HTLC]].message.id == add.id) val paymentReceived = eventListener.expectMsgType[PaymentReceived] - assert(paymentReceived == PaymentReceived(add.paymentHash, PartialPayment(add.amountMsat, add.channelId, receivedAt) :: Nil)) + assert(paymentReceived == PaymentReceived(add.paymentHash, PaymentEvent.IncomingPayment(add.channelId, receivedFrom, add.amountMsat, 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] == IncomingPaymentStatus.Received(add.amountMsat, receivedAt)) @@ -151,11 +151,11 @@ 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), receivedAt)) + sender.send(handlerWithMpp, ReceivePacket(IncomingPaymentPacket.FinalPacket(add, FinalPayload.Standard.createPayload(add.amountMsat, add.amountMsat, add.cltvExpiry, invoice.paymentSecret, invoice.paymentMetadata, upgradeAccountability = false), receivedAt), receivedFrom)) assert(register.expectMsgType[Register.Forward[CMD_FULFILL_HTLC]].message.id == add.id) val paymentReceived = eventListener.expectMsgType[PaymentReceived] - assert(paymentReceived == PaymentReceived(add.paymentHash, PartialPayment(amountMsat, add.channelId, receivedAt) :: Nil)) + assert(paymentReceived == PaymentReceived(add.paymentHash, PaymentEvent.IncomingPayment(add.channelId, receivedFrom, amountMsat, 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] == IncomingPaymentStatus.Received(amountMsat, receivedAt)) @@ -176,7 +176,7 @@ class MultiPartHandlerSpec extends TestKitBaseClass with FixtureAnyFunSuiteLike assert(nodeParams.db.payments.getIncomingPayment(invoice.paymentHash).isEmpty) val finalPacket = createBlindedPacket(amountMsat, invoice.paymentHash, defaultExpiry, CltvExpiry(nodeParams.currentBlockHeight), pathId, receivedAt) - sender.send(handlerWithRouteBlinding, finalPacket) + sender.send(handlerWithRouteBlinding, ReceivePacket(finalPacket, receivedFrom)) val receivePayment = offerManager.expectMsgType[OfferManager.ReceivePayment] assert(receivePayment.paymentHash == invoice.paymentHash) assert(receivePayment.payload.pathId == pathId.bytes) @@ -185,7 +185,7 @@ class MultiPartHandlerSpec extends TestKitBaseClass with FixtureAnyFunSuiteLike assert(register.expectMsgType[Register.Forward[CMD_FULFILL_HTLC]].message.id == finalPacket.add.id) val paymentReceived = eventListener.expectMsgType[PaymentReceived] - assert(paymentReceived == PaymentReceived(finalPacket.add.paymentHash, PartialPayment(amountMsat, finalPacket.add.channelId, receivedAt) :: Nil)) + assert(paymentReceived == PaymentReceived(finalPacket.add.paymentHash, PaymentEvent.IncomingPayment(finalPacket.add.channelId, receivedFrom, amountMsat, 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] == IncomingPaymentStatus.Received(amountMsat, receivedAt)) @@ -198,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), receivedAt)) + sender.send(handlerWithMpp, ReceivePacket(IncomingPaymentPacket.FinalPacket(add, FinalPayload.Standard.createPayload(add.amountMsat, add.amountMsat, add.cltvExpiry, invoice.paymentSecret, invoice.paymentMetadata, upgradeAccountability = false), receivedAt), receivedFrom)) 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) @@ -331,7 +331,7 @@ class MultiPartHandlerSpec extends TestKitBaseClass with FixtureAnyFunSuiteLike assert(invoice.isExpired()) val add = UpdateAddHtlc(ByteVector32.One, 0, 1000 msat, 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, ReceivePacket(IncomingPaymentPacket.FinalPacket(add, FinalPayload.Standard.createPayload(add.amountMsat, add.amountMsat, add.cltvExpiry, invoice.paymentSecret, invoice.paymentMetadata, upgradeAccountability = false), TimestampMilli.now()), randomKey().publicKey)) register.expectMsgType[Register.Forward[CMD_FAIL_HTLC]] val Some(incoming) = nodeParams.db.payments.getIncomingPayment(invoice.paymentHash) assert(incoming.invoice.isExpired() && incoming.status == IncomingPaymentStatus.Expired) @@ -346,7 +346,7 @@ class MultiPartHandlerSpec extends TestKitBaseClass with FixtureAnyFunSuiteLike assert(invoice.isExpired()) val add = UpdateAddHtlc(ByteVector32.One, 0, 800 msat, invoice.paymentHash, defaultExpiry, TestConstants.emptyOnionPacket, None, accountable = false, None) - sender.send(handlerWithMpp, IncomingPaymentPacket.FinalPacket(add, FinalPayload.Standard.createPayload(add.amountMsat, 1000 msat, add.cltvExpiry, invoice.paymentSecret, invoice.paymentMetadata, upgradeAccountability = false), TimestampMilli.now())) + sender.send(handlerWithMpp, ReceivePacket(IncomingPaymentPacket.FinalPacket(add, FinalPayload.Standard.createPayload(add.amountMsat, 1000 msat, add.cltvExpiry, invoice.paymentSecret, invoice.paymentMetadata, upgradeAccountability = false), TimestampMilli.now()), randomKey().publicKey)) val cmd = register.expectMsgType[Register.Forward[CMD_FAIL_HTLC]].message assert(cmd.reason == FailureReason.LocalFailure(IncorrectOrUnknownPaymentDetails(1000 msat, nodeParams.currentBlockHeight))) val Some(incoming) = nodeParams.db.payments.getIncomingPayment(invoice.paymentHash) @@ -361,7 +361,7 @@ class MultiPartHandlerSpec extends TestKitBaseClass with FixtureAnyFunSuiteLike assert(!invoice.features.hasFeature(BasicMultiPartPayment)) val add = UpdateAddHtlc(ByteVector32.One, 0, 800 msat, invoice.paymentHash, defaultExpiry, TestConstants.emptyOnionPacket, None, accountable = false, None) - sender.send(handlerWithoutMpp, IncomingPaymentPacket.FinalPacket(add, FinalPayload.Standard.createPayload(add.amountMsat, 1000 msat, add.cltvExpiry, invoice.paymentSecret, invoice.paymentMetadata, upgradeAccountability = false), TimestampMilli.now())) + sender.send(handlerWithoutMpp, ReceivePacket(IncomingPaymentPacket.FinalPacket(add, FinalPayload.Standard.createPayload(add.amountMsat, 1000 msat, add.cltvExpiry, invoice.paymentSecret, invoice.paymentMetadata, upgradeAccountability = false), TimestampMilli.now()), randomKey().publicKey)) val cmd = register.expectMsgType[Register.Forward[CMD_FAIL_HTLC]].message assert(cmd.reason == FailureReason.LocalFailure(IncorrectOrUnknownPaymentDetails(1000 msat, nodeParams.currentBlockHeight))) assert(nodeParams.db.payments.getIncomingPayment(invoice.paymentHash).get.status == IncomingPaymentStatus.Pending) @@ -376,7 +376,7 @@ class MultiPartHandlerSpec extends TestKitBaseClass with FixtureAnyFunSuiteLike val lowCltvExpiry = nodeParams.channelConf.fulfillSafetyBeforeTimeout.toCltvExpiry(nodeParams.currentBlockHeight) val add = UpdateAddHtlc(ByteVector32.One, 0, 800 msat, invoice.paymentHash, lowCltvExpiry, TestConstants.emptyOnionPacket, None, accountable = false, None) - sender.send(handlerWithMpp, IncomingPaymentPacket.FinalPacket(add, FinalPayload.Standard.createPayload(add.amountMsat, 1000 msat, add.cltvExpiry, invoice.paymentSecret, invoice.paymentMetadata, upgradeAccountability = false), TimestampMilli.now())) + sender.send(handlerWithMpp, ReceivePacket(IncomingPaymentPacket.FinalPacket(add, FinalPayload.Standard.createPayload(add.amountMsat, 1000 msat, add.cltvExpiry, invoice.paymentSecret, invoice.paymentMetadata, upgradeAccountability = false), TimestampMilli.now()), randomKey().publicKey)) val cmd = register.expectMsgType[Register.Forward[CMD_FAIL_HTLC]].message assert(cmd.reason == FailureReason.LocalFailure(IncorrectOrUnknownPaymentDetails(1000 msat, nodeParams.currentBlockHeight))) assert(nodeParams.db.payments.getIncomingPayment(invoice.paymentHash).get.status == IncomingPaymentStatus.Pending) @@ -390,7 +390,7 @@ class MultiPartHandlerSpec extends TestKitBaseClass with FixtureAnyFunSuiteLike assert(invoice.features.hasFeature(BasicMultiPartPayment)) val add = UpdateAddHtlc(ByteVector32.One, 0, 800 msat, invoice.paymentHash.reverse, defaultExpiry, TestConstants.emptyOnionPacket, None, accountable = false, None) - sender.send(handlerWithMpp, IncomingPaymentPacket.FinalPacket(add, FinalPayload.Standard.createPayload(add.amountMsat, 1000 msat, add.cltvExpiry, invoice.paymentSecret, invoice.paymentMetadata, upgradeAccountability = false), TimestampMilli.now())) + sender.send(handlerWithMpp, ReceivePacket(IncomingPaymentPacket.FinalPacket(add, FinalPayload.Standard.createPayload(add.amountMsat, 1000 msat, add.cltvExpiry, invoice.paymentSecret, invoice.paymentMetadata, upgradeAccountability = false), TimestampMilli.now()), randomKey().publicKey)) val cmd = register.expectMsgType[Register.Forward[CMD_FAIL_HTLC]].message assert(cmd.reason == FailureReason.LocalFailure(IncorrectOrUnknownPaymentDetails(1000 msat, nodeParams.currentBlockHeight))) assert(nodeParams.db.payments.getIncomingPayment(invoice.paymentHash).get.status == IncomingPaymentStatus.Pending) @@ -404,7 +404,7 @@ class MultiPartHandlerSpec extends TestKitBaseClass with FixtureAnyFunSuiteLike assert(invoice.features.hasFeature(BasicMultiPartPayment)) val add = UpdateAddHtlc(ByteVector32.One, 0, 800 msat, invoice.paymentHash, defaultExpiry, TestConstants.emptyOnionPacket, None, accountable = false, None) - sender.send(handlerWithMpp, IncomingPaymentPacket.FinalPacket(add, FinalPayload.Standard.createPayload(add.amountMsat, 999 msat, add.cltvExpiry, invoice.paymentSecret, invoice.paymentMetadata, upgradeAccountability = false), TimestampMilli.now())) + sender.send(handlerWithMpp, ReceivePacket(IncomingPaymentPacket.FinalPacket(add, FinalPayload.Standard.createPayload(add.amountMsat, 999 msat, add.cltvExpiry, invoice.paymentSecret, invoice.paymentMetadata, upgradeAccountability = false), TimestampMilli.now()), randomKey().publicKey)) val cmd = register.expectMsgType[Register.Forward[CMD_FAIL_HTLC]].message assert(cmd.reason == FailureReason.LocalFailure(IncorrectOrUnknownPaymentDetails(999 msat, nodeParams.currentBlockHeight))) assert(nodeParams.db.payments.getIncomingPayment(invoice.paymentHash).get.status == IncomingPaymentStatus.Pending) @@ -418,7 +418,7 @@ class MultiPartHandlerSpec extends TestKitBaseClass with FixtureAnyFunSuiteLike assert(invoice.features.hasFeature(BasicMultiPartPayment)) val add = UpdateAddHtlc(ByteVector32.One, 0, 800 msat, invoice.paymentHash, defaultExpiry, TestConstants.emptyOnionPacket, None, accountable = false, None) - sender.send(handlerWithMpp, IncomingPaymentPacket.FinalPacket(add, FinalPayload.Standard.createPayload(add.amountMsat, 2001 msat, add.cltvExpiry, invoice.paymentSecret, invoice.paymentMetadata, upgradeAccountability = false), TimestampMilli.now())) + sender.send(handlerWithMpp, ReceivePacket(IncomingPaymentPacket.FinalPacket(add, FinalPayload.Standard.createPayload(add.amountMsat, 2001 msat, add.cltvExpiry, invoice.paymentSecret, invoice.paymentMetadata, upgradeAccountability = false), TimestampMilli.now()), randomKey().publicKey)) val cmd = register.expectMsgType[Register.Forward[CMD_FAIL_HTLC]].message assert(cmd.reason == FailureReason.LocalFailure(IncorrectOrUnknownPaymentDetails(2001 msat, nodeParams.currentBlockHeight))) assert(nodeParams.db.payments.getIncomingPayment(invoice.paymentHash).get.status == IncomingPaymentStatus.Pending) @@ -433,7 +433,7 @@ class MultiPartHandlerSpec extends TestKitBaseClass with FixtureAnyFunSuiteLike // Invalid payment secret. val add = UpdateAddHtlc(ByteVector32.One, 0, 800 msat, invoice.paymentHash, defaultExpiry, TestConstants.emptyOnionPacket, None, accountable = false, None) - sender.send(handlerWithMpp, IncomingPaymentPacket.FinalPacket(add, FinalPayload.Standard.createPayload(add.amountMsat, 1000 msat, add.cltvExpiry, invoice.paymentSecret.reverse, invoice.paymentMetadata, upgradeAccountability = false), TimestampMilli.now())) + sender.send(handlerWithMpp, ReceivePacket(IncomingPaymentPacket.FinalPacket(add, FinalPayload.Standard.createPayload(add.amountMsat, 1000 msat, add.cltvExpiry, invoice.paymentSecret.reverse, invoice.paymentMetadata, upgradeAccountability = false), TimestampMilli.now()), randomKey().publicKey)) val cmd = register.expectMsgType[Register.Forward[CMD_FAIL_HTLC]].message assert(cmd.reason == FailureReason.LocalFailure(IncorrectOrUnknownPaymentDetails(1000 msat, nodeParams.currentBlockHeight))) assert(nodeParams.db.payments.getIncomingPayment(invoice.paymentHash).get.status == IncomingPaymentStatus.Pending) @@ -446,7 +446,7 @@ class MultiPartHandlerSpec extends TestKitBaseClass with FixtureAnyFunSuiteLike val invoice = sender.expectMsgType[Bolt11Invoice] val packet = createBlindedPacket(1000 msat, invoice.paymentHash, defaultExpiry, CltvExpiry(nodeParams.currentBlockHeight), randomBytes32()) - sender.send(handlerWithRouteBlinding, packet) + sender.send(handlerWithRouteBlinding, ReceivePacket(packet, invoice.nodeId)) val receivePayment = offerManager.expectMsgType[OfferManager.ReceivePayment] assert(receivePayment.paymentHash == invoice.paymentHash) receivePayment.replyTo ! GetIncomingPaymentActor.RejectPayment("non blinded payment") @@ -465,7 +465,7 @@ class MultiPartHandlerSpec extends TestKitBaseClass with FixtureAnyFunSuiteLike val invoice = sender.expectMsgType[Bolt12Invoice] val add = UpdateAddHtlc(ByteVector32.One, 0, 5000 msat, invoice.paymentHash, defaultExpiry, TestConstants.emptyOnionPacket, None, accountable = false, None) - sender.send(handlerWithMpp, IncomingPaymentPacket.FinalPacket(add, FinalPayload.Standard.createPayload(add.amountMsat, add.amountMsat, add.cltvExpiry, randomBytes32(), None, upgradeAccountability = false), TimestampMilli.now())) + sender.send(handlerWithMpp, ReceivePacket(IncomingPaymentPacket.FinalPacket(add, FinalPayload.Standard.createPayload(add.amountMsat, add.amountMsat, add.cltvExpiry, randomBytes32(), None, upgradeAccountability = false), TimestampMilli.now()), randomKey().publicKey)) val cmd = register.expectMsgType[Register.Forward[CMD_FAIL_HTLC]].message assert(cmd.reason == FailureReason.LocalFailure(IncorrectOrUnknownPaymentDetails(5000 msat, nodeParams.currentBlockHeight))) assert(nodeParams.db.payments.getIncomingPayment(invoice.paymentHash).isEmpty) @@ -484,7 +484,7 @@ class MultiPartHandlerSpec extends TestKitBaseClass with FixtureAnyFunSuiteLike assert(nodeParams.db.payments.getIncomingPayment(invoice.paymentHash).isEmpty) val packet = createBlindedPacket(5000 msat, invoice.paymentHash, defaultExpiry, CltvExpiry(nodeParams.currentBlockHeight), pathId) - sender.send(handlerWithRouteBlinding, packet) + sender.send(handlerWithRouteBlinding, ReceivePacket(packet, nodeKey.publicKey)) val receivePayment = offerManager.expectMsgType[OfferManager.ReceivePayment] assert(receivePayment.paymentHash == invoice.paymentHash) assert(receivePayment.payload.pathId == pathId.bytes) @@ -506,7 +506,7 @@ class MultiPartHandlerSpec extends TestKitBaseClass with FixtureAnyFunSuiteLike val invoice = sender.expectMsgType[Bolt12Invoice] val packet = createBlindedPacket(5000 msat, invoice.paymentHash, defaultExpiry, CltvExpiry(nodeParams.currentBlockHeight), pathId) - sender.send(handlerWithRouteBlinding, packet) + sender.send(handlerWithRouteBlinding, ReceivePacket(packet, nodeKey.publicKey)) val payment = offerManager.expectMsgType[OfferManager.ReceivePayment] assert(payment.payload.pathId == pathId) payment.replyTo ! GetIncomingPaymentActor.RejectPayment("internal error") @@ -527,7 +527,7 @@ class MultiPartHandlerSpec extends TestKitBaseClass with FixtureAnyFunSuiteLike // We test the case where the HTLC's cltv_expiry is lower than expected and doesn't meet the min_final_expiry_delta. val packet = createBlindedPacket(5000 msat, invoice.paymentHash, defaultExpiry - CltvExpiryDelta(1), defaultExpiry, pathId) - sender.send(handlerWithRouteBlinding, packet) + sender.send(handlerWithRouteBlinding, ReceivePacket(packet, nodeKey.publicKey)) val receivePayment = offerManager.expectMsgType[OfferManager.ReceivePayment] assert(receivePayment.paymentHash == invoice.paymentHash) assert(receivePayment.payload.pathId == pathId.bytes) @@ -546,15 +546,17 @@ class MultiPartHandlerSpec extends TestKitBaseClass with FixtureAnyFunSuiteLike f.sender.send(handler, ReceiveStandardPayment(f.sender.ref, Some(1000 msat), Left("1 slow coffee"))) val pr1 = f.sender.expectMsgType[Bolt11Invoice] val receivedAt1 = TimestampMilli.now() + val receivedFrom1 = randomKey().publicKey val add1 = UpdateAddHtlc(ByteVector32.One, 0, 800 msat, pr1.paymentHash, f.defaultExpiry, TestConstants.emptyOnionPacket, None, accountable = false, None) - f.sender.send(handler, IncomingPaymentPacket.FinalPacket(add1, FinalPayload.Standard.createPayload(add1.amountMsat, 1000 msat, add1.cltvExpiry, pr1.paymentSecret, pr1.paymentMetadata, upgradeAccountability = false), receivedAt1)) + f.sender.send(handler, ReceivePacket(IncomingPaymentPacket.FinalPacket(add1, FinalPayload.Standard.createPayload(add1.amountMsat, 1000 msat, add1.cltvExpiry, pr1.paymentSecret, pr1.paymentMetadata, upgradeAccountability = false), receivedAt1), receivedFrom1)) // Partial payment exceeding the invoice amount, but incomplete because it promises to overpay. f.sender.send(handler, ReceiveStandardPayment(f.sender.ref, Some(1500 msat), Left("1 slow latte"))) val pr2 = f.sender.expectMsgType[Bolt11Invoice] val receivedAt2 = receivedAt1 + 1.millis + val receivedFrom2 = randomKey().publicKey val add2 = UpdateAddHtlc(ByteVector32.One, 1, 1600 msat, pr2.paymentHash, f.defaultExpiry, TestConstants.emptyOnionPacket, None, accountable = false, None) - f.sender.send(handler, IncomingPaymentPacket.FinalPacket(add2, FinalPayload.Standard.createPayload(add2.amountMsat, 2000 msat, add2.cltvExpiry, pr2.paymentSecret, pr2.paymentMetadata, upgradeAccountability = false), receivedAt2)) + f.sender.send(handler, ReceivePacket(IncomingPaymentPacket.FinalPacket(add2, FinalPayload.Standard.createPayload(add2.amountMsat, 2000 msat, add2.cltvExpiry, pr2.paymentSecret, pr2.paymentMetadata, upgradeAccountability = false), receivedAt2), receivedFrom2)) awaitCond { f.sender.send(handler, GetPendingPayments) @@ -573,7 +575,8 @@ class MultiPartHandlerSpec extends TestKitBaseClass with FixtureAnyFunSuiteLike // Extraneous HTLCs should be failed. val receivedAt3 = receivedAt1 + 2.millis - f.sender.send(handler, MultiPartPaymentFSM.ExtraPaymentReceived(pr1.paymentHash, HtlcPart(1000 msat, UpdateAddHtlc(ByteVector32.One, 42, 200 msat, pr1.paymentHash, add1.cltvExpiry, add1.onionRoutingPacket, None, accountable = false, None), receivedAt3), Some(PaymentTimeout()))) + val receivedFrom3 = randomKey().publicKey + f.sender.send(handler, MultiPartPaymentFSM.ExtraPaymentReceived(pr1.paymentHash, HtlcPart(1000 msat, UpdateAddHtlc(ByteVector32.One, 42, 200 msat, pr1.paymentHash, add1.cltvExpiry, add1.onionRoutingPacket, None, accountable = false, None), receivedFrom3, receivedAt3), Some(PaymentTimeout()))) f.register.expectMsg(Register.Forward(null, ByteVector32.One, CMD_FAIL_HTLC(42, FailureReason.LocalFailure(PaymentTimeout()), Some(FailureAttributionData(receivedAt3, None)), commit = true))) // The payment should still be pending in DB. @@ -590,15 +593,18 @@ class MultiPartHandlerSpec extends TestKitBaseClass with FixtureAnyFunSuiteLike val invoice = f.sender.expectMsgType[Bolt11Invoice] val receivedAt1 = TimestampMilli.now() + val receivedFrom1 = randomKey().publicKey val add1 = UpdateAddHtlc(ByteVector32.One, 0, 800 msat, invoice.paymentHash, f.defaultExpiry, TestConstants.emptyOnionPacket, None, accountable = false, None) - f.sender.send(handler, IncomingPaymentPacket.FinalPacket(add1, FinalPayload.Standard.createPayload(add1.amountMsat, 1000 msat, add1.cltvExpiry, invoice.paymentSecret, invoice.paymentMetadata, upgradeAccountability = false), receivedAt1)) + f.sender.send(handler, ReceivePacket(IncomingPaymentPacket.FinalPacket(add1, FinalPayload.Standard.createPayload(add1.amountMsat, 1000 msat, add1.cltvExpiry, invoice.paymentSecret, invoice.paymentMetadata, upgradeAccountability = false), receivedAt1), receivedFrom1)) // Invalid payment secret -> should be rejected. val receivedAt2 = receivedAt1 + 1.millis + val receivedFrom2 = randomKey().publicKey val add2 = UpdateAddHtlc(ByteVector32.Zeroes, 42, 200 msat, invoice.paymentHash, f.defaultExpiry, TestConstants.emptyOnionPacket, None, accountable = false, None) - f.sender.send(handler, IncomingPaymentPacket.FinalPacket(add2, FinalPayload.Standard.createPayload(add2.amountMsat, 1000 msat, add2.cltvExpiry, invoice.paymentSecret.reverse, invoice.paymentMetadata, upgradeAccountability = false), receivedAt2)) + f.sender.send(handler, ReceivePacket(IncomingPaymentPacket.FinalPacket(add2, FinalPayload.Standard.createPayload(add2.amountMsat, 1000 msat, add2.cltvExpiry, invoice.paymentSecret.reverse, invoice.paymentMetadata, upgradeAccountability = false), receivedAt2), receivedFrom2)) val receivedAt3 = receivedAt1 + 2.millis + val receivedFrom3 = randomKey().publicKey val add3 = add2.copy(id = 43) - f.sender.send(handler, IncomingPaymentPacket.FinalPacket(add3, FinalPayload.Standard.createPayload(add3.amountMsat, 1000 msat, add3.cltvExpiry, invoice.paymentSecret, invoice.paymentMetadata, upgradeAccountability = false), receivedAt3)) + f.sender.send(handler, ReceivePacket(IncomingPaymentPacket.FinalPacket(add3, FinalPayload.Standard.createPayload(add3.amountMsat, 1000 msat, add3.cltvExpiry, invoice.paymentSecret, invoice.paymentMetadata, upgradeAccountability = false), receivedAt3), receivedFrom3)) f.register.expectMsgAllOf( Register.Forward(null, add2.channelId, CMD_FAIL_HTLC(add2.id, FailureReason.LocalFailure(IncorrectOrUnknownPaymentDetails(1000 msat, nodeParams.currentBlockHeight)), Some(FailureAttributionData(receivedAt2, None)), commit = true)), @@ -607,7 +613,7 @@ class MultiPartHandlerSpec extends TestKitBaseClass with FixtureAnyFunSuiteLike ) val paymentReceived = f.eventListener.expectMsgType[PaymentReceived] - assert(paymentReceived.parts.toSet == Set(PartialPayment(800 msat, ByteVector32.One, receivedAt1), PartialPayment(200 msat, ByteVector32.Zeroes, receivedAt3))) + assert(paymentReceived.parts.toSet == Set(PaymentEvent.IncomingPayment(ByteVector32.One, receivedFrom1, 800 msat, receivedAt1), PaymentEvent.IncomingPayment(ByteVector32.Zeroes, receivedFrom3, 200 msat, 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) @@ -618,7 +624,8 @@ class MultiPartHandlerSpec extends TestKitBaseClass with FixtureAnyFunSuiteLike // Extraneous HTLCs should be fulfilled. val receivedAt4 = receivedAt1 + 3.millis - f.sender.send(handler, MultiPartPaymentFSM.ExtraPaymentReceived(invoice.paymentHash, HtlcPart(1000 msat, UpdateAddHtlc(ByteVector32.One, 44, 200 msat, invoice.paymentHash, add1.cltvExpiry, add1.onionRoutingPacket, None, accountable = false, None), receivedAt4), None)) + val receivedFrom4 = randomKey().publicKey + f.sender.send(handler, MultiPartPaymentFSM.ExtraPaymentReceived(invoice.paymentHash, HtlcPart(1000 msat, UpdateAddHtlc(ByteVector32.One, 44, 200 msat, invoice.paymentHash, add1.cltvExpiry, add1.onionRoutingPacket, None, accountable = false, None), receivedFrom4, receivedAt4), None)) f.register.expectMsg(Register.Forward(null, ByteVector32.One, CMD_FULFILL_HTLC(44, preimage, Some(FulfillAttributionData(receivedAt4, None, None)), commit = true))) assert(f.eventListener.expectMsgType[PaymentReceived].amount == 200.msat) val received2 = nodeParams.db.payments.getIncomingPayment(invoice.paymentHash) @@ -638,10 +645,12 @@ class MultiPartHandlerSpec extends TestKitBaseClass with FixtureAnyFunSuiteLike val add1 = UpdateAddHtlc(randomBytes32(), 0, 1100 msat, invoice.paymentHash, f.defaultExpiry, TestConstants.emptyOnionPacket, None, accountable = false, None) val receivedAt1 = TimestampMilli.now() - f.sender.send(handler, IncomingPaymentPacket.FinalPacket(add1, FinalPayload.Standard.createPayload(add1.amountMsat, 1500 msat, add1.cltvExpiry, invoice.paymentSecret, invoice.paymentMetadata, upgradeAccountability = false), receivedAt1)) + val receivedFrom1 = randomKey().publicKey + f.sender.send(handler, ReceivePacket(IncomingPaymentPacket.FinalPacket(add1, FinalPayload.Standard.createPayload(add1.amountMsat, 1500 msat, add1.cltvExpiry, invoice.paymentSecret, invoice.paymentMetadata, upgradeAccountability = false), receivedAt1), receivedFrom1)) val add2 = UpdateAddHtlc(randomBytes32(), 1, 500 msat, invoice.paymentHash, f.defaultExpiry, TestConstants.emptyOnionPacket, None, accountable = false, None) val receivedAt2 = TimestampMilli.now() + 5.millis - f.sender.send(handler, IncomingPaymentPacket.FinalPacket(add2, FinalPayload.Standard.createPayload(add2.amountMsat, 1500 msat, add2.cltvExpiry, invoice.paymentSecret, invoice.paymentMetadata, upgradeAccountability = false), receivedAt2)) + val receivedFrom2 = randomKey().publicKey + f.sender.send(handler, ReceivePacket(IncomingPaymentPacket.FinalPacket(add2, FinalPayload.Standard.createPayload(add2.amountMsat, 1500 msat, add2.cltvExpiry, invoice.paymentSecret, invoice.paymentMetadata, upgradeAccountability = false), receivedAt2), receivedFrom2)) f.register.expectMsgAllOf( Register.Forward(null, add1.channelId, CMD_FULFILL_HTLC(add1.id, preimage, Some(FulfillAttributionData(receivedAt1, None, None)), commit = true)), @@ -649,7 +658,7 @@ class MultiPartHandlerSpec extends TestKitBaseClass with FixtureAnyFunSuiteLike ) val paymentReceived = f.eventListener.expectMsgType[PaymentReceived] - assert(paymentReceived.parts.toSet == Set(PartialPayment(1100 msat, add1.channelId, receivedAt1), PartialPayment(500 msat, add2.channelId, receivedAt2))) + assert(paymentReceived.parts.toSet == Set(PaymentEvent.IncomingPayment(add1.channelId, receivedFrom1, 1100 msat, receivedAt1), PaymentEvent.IncomingPayment(add2.channelId, receivedFrom2, 500 msat, 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) @@ -667,7 +676,8 @@ class MultiPartHandlerSpec extends TestKitBaseClass with FixtureAnyFunSuiteLike val add1 = UpdateAddHtlc(ByteVector32.One, 0, 800 msat, invoice.paymentHash, f.defaultExpiry, TestConstants.emptyOnionPacket, None, accountable = false, None) val receivedAt1 = TimestampMilli.now() - f.sender.send(handler, IncomingPaymentPacket.FinalPacket(add1, FinalPayload.Standard.createPayload(add1.amountMsat, 1000 msat, add1.cltvExpiry, invoice.paymentSecret, invoice.paymentMetadata, upgradeAccountability = false), receivedAt1)) + val receivedFrom1 = randomKey().publicKey + f.sender.send(handler, ReceivePacket(IncomingPaymentPacket.FinalPacket(add1, FinalPayload.Standard.createPayload(add1.amountMsat, 1000 msat, add1.cltvExpiry, invoice.paymentSecret, invoice.paymentMetadata, upgradeAccountability = false), receivedAt1), receivedFrom1)) f.register.expectMsg(Register.Forward(null, ByteVector32.One, CMD_FAIL_HTLC(0, FailureReason.LocalFailure(PaymentTimeout()), Some(FailureAttributionData(receivedAt1, None)), commit = true))) awaitCond({ f.sender.send(handler, GetPendingPayments) @@ -676,10 +686,12 @@ class MultiPartHandlerSpec extends TestKitBaseClass with FixtureAnyFunSuiteLike val add2 = UpdateAddHtlc(ByteVector32.One, 2, 300 msat, invoice.paymentHash, f.defaultExpiry, TestConstants.emptyOnionPacket, None, accountable = false, None) val receivedAt2 = TimestampMilli.now() + 10.millis - f.sender.send(handler, IncomingPaymentPacket.FinalPacket(add2, FinalPayload.Standard.createPayload(add2.amountMsat, 1000 msat, add2.cltvExpiry, invoice.paymentSecret, invoice.paymentMetadata, upgradeAccountability = false), receivedAt2)) + val receivedFrom2 = randomKey().publicKey + f.sender.send(handler, ReceivePacket(IncomingPaymentPacket.FinalPacket(add2, FinalPayload.Standard.createPayload(add2.amountMsat, 1000 msat, add2.cltvExpiry, invoice.paymentSecret, invoice.paymentMetadata, upgradeAccountability = false), receivedAt2), receivedFrom2)) val add3 = UpdateAddHtlc(ByteVector32.Zeroes, 5, 700 msat, invoice.paymentHash, f.defaultExpiry, TestConstants.emptyOnionPacket, None, accountable = false, None) val receivedAt3 = TimestampMilli.now() + 50.millis - f.sender.send(handler, IncomingPaymentPacket.FinalPacket(add3, FinalPayload.Standard.createPayload(add3.amountMsat, 1000 msat, add3.cltvExpiry, invoice.paymentSecret, invoice.paymentMetadata, upgradeAccountability = false), receivedAt3)) + val receivedFrom3 = randomKey().publicKey + f.sender.send(handler, ReceivePacket(IncomingPaymentPacket.FinalPacket(add3, FinalPayload.Standard.createPayload(add3.amountMsat, 1000 msat, add3.cltvExpiry, invoice.paymentSecret, invoice.paymentMetadata, upgradeAccountability = false), receivedAt3), receivedFrom3)) // the fulfill are not necessarily in the same order as the commands f.register.expectMsgAllOf( @@ -689,7 +701,7 @@ class MultiPartHandlerSpec extends TestKitBaseClass with FixtureAnyFunSuiteLike val paymentReceived = f.eventListener.expectMsgType[PaymentReceived] assert(paymentReceived.paymentHash == invoice.paymentHash) - assert(paymentReceived.parts.toSet == Set(PartialPayment(300 msat, ByteVector32.One, receivedAt2), PartialPayment(700 msat, ByteVector32.Zeroes, receivedAt3))) + assert(paymentReceived.parts.toSet == Set(PaymentEvent.IncomingPayment(ByteVector32.One, receivedFrom2, 300 msat, receivedAt2), PaymentEvent.IncomingPayment(ByteVector32.Zeroes, receivedFrom3, 700 msat, 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,11 +724,12 @@ class MultiPartHandlerSpec extends TestKitBaseClass with FixtureAnyFunSuiteLike val add = UpdateAddHtlc(ByteVector32.One, 0, amountMsat, paymentHash, defaultExpiry, TestConstants.emptyOnionPacket, None, accountable = false, None) val receivedAt = TimestampMilli.now() - sender.send(handlerWithKeySend, IncomingPaymentPacket.FinalPacket(add, payload, receivedAt)) + val receivedFrom = randomKey().publicKey + sender.send(handlerWithKeySend, ReceivePacket(IncomingPaymentPacket.FinalPacket(add, payload, receivedAt), receivedFrom)) register.expectMsgType[Register.Forward[CMD_FULFILL_HTLC]] val paymentReceived = eventListener.expectMsgType[PaymentReceived] - assert(paymentReceived == PaymentReceived(add.paymentHash, PartialPayment(amountMsat, add.channelId, receivedAt) :: Nil)) + assert(paymentReceived == PaymentReceived(add.paymentHash, PaymentEvent.IncomingPayment(add.channelId, receivedFrom, amountMsat, 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] == IncomingPaymentStatus.Received(amountMsat, receivedAt)) @@ -734,11 +747,12 @@ class MultiPartHandlerSpec extends TestKitBaseClass with FixtureAnyFunSuiteLike val add = UpdateAddHtlc(ByteVector32.One, 0, amountMsat, paymentHash, defaultExpiry, TestConstants.emptyOnionPacket, None, accountable = false, None) val receivedAt = TimestampMilli.now() - sender.send(handlerWithKeySend, IncomingPaymentPacket.FinalPacket(add, payload, receivedAt)) + val receivedFrom = randomKey().publicKey + sender.send(handlerWithKeySend, ReceivePacket(IncomingPaymentPacket.FinalPacket(add, payload, receivedAt), receivedFrom)) register.expectMsgType[Register.Forward[CMD_FULFILL_HTLC]] val paymentReceived = eventListener.expectMsgType[PaymentReceived] - assert(paymentReceived == PaymentReceived(add.paymentHash, PartialPayment(amountMsat, add.channelId, receivedAt) :: Nil)) + assert(paymentReceived == PaymentReceived(add.paymentHash, PaymentEvent.IncomingPayment(add.channelId, receivedFrom, amountMsat, 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] == IncomingPaymentStatus.Received(amountMsat, receivedAt)) @@ -757,7 +771,8 @@ class MultiPartHandlerSpec extends TestKitBaseClass with FixtureAnyFunSuiteLike val add = UpdateAddHtlc(ByteVector32.One, 0, amountMsat, paymentHash, defaultExpiry, TestConstants.emptyOnionPacket, None, accountable = false, None) val receivedAt = TimestampMilli.now() - sender.send(handlerWithMpp, IncomingPaymentPacket.FinalPacket(add, payload, receivedAt)) + val receivedFrom = randomKey().publicKey + sender.send(handlerWithMpp, ReceivePacket(IncomingPaymentPacket.FinalPacket(add, payload, receivedAt), receivedFrom)) f.register.expectMsg(Register.Forward(null, add.channelId, CMD_FAIL_HTLC(add.id, FailureReason.LocalFailure(IncorrectOrUnknownPaymentDetails(42000 msat, nodeParams.currentBlockHeight)), Some(FailureAttributionData(receivedAt, None)), commit = true))) assert(nodeParams.db.payments.getIncomingPayment(paymentHash).isEmpty) @@ -771,7 +786,7 @@ class MultiPartHandlerSpec extends TestKitBaseClass with FixtureAnyFunSuiteLike assert(nodeParams.db.payments.getIncomingPayment(paymentHash).isEmpty) val add = UpdateAddHtlc(ByteVector32.One, 0, 1000 msat, paymentHash, defaultExpiry, TestConstants.emptyOnionPacket, None, accountable = false, None) - sender.send(handlerWithoutMpp, IncomingPaymentPacket.FinalPacket(add, FinalPayload.Standard.createPayload(add.amountMsat, add.amountMsat, add.cltvExpiry, paymentSecret, None, upgradeAccountability = false), TimestampMilli.now())) + sender.send(handlerWithoutMpp, ReceivePacket(IncomingPaymentPacket.FinalPacket(add, FinalPayload.Standard.createPayload(add.amountMsat, add.amountMsat, add.cltvExpiry, paymentSecret, None, upgradeAccountability = false), TimestampMilli.now()), randomKey().publicKey)) val cmd = register.expectMsgType[Register.Forward[CMD_FAIL_HTLC]].message assert(cmd.id == add.id) assert(cmd.reason == FailureReason.LocalFailure(IncorrectOrUnknownPaymentDetails(1000 msat, nodeParams.currentBlockHeight))) @@ -785,7 +800,7 @@ class MultiPartHandlerSpec extends TestKitBaseClass with FixtureAnyFunSuiteLike assert(nodeParams.db.payments.getIncomingPayment(paymentHash).isEmpty) val add = UpdateAddHtlc(ByteVector32.One, 0, 800 msat, paymentHash, defaultExpiry, TestConstants.emptyOnionPacket, None, accountable = false, None) - sender.send(handlerWithMpp, IncomingPaymentPacket.FinalPacket(add, FinalPayload.Standard.createPayload(add.amountMsat, 1000 msat, add.cltvExpiry, paymentSecret, Some(hex"012345"), upgradeAccountability = false), TimestampMilli.now())) + sender.send(handlerWithMpp, ReceivePacket(IncomingPaymentPacket.FinalPacket(add, FinalPayload.Standard.createPayload(add.amountMsat, 1000 msat, add.cltvExpiry, paymentSecret, Some(hex"012345"), upgradeAccountability = false), TimestampMilli.now()), randomKey().publicKey)) val cmd = register.expectMsgType[Register.Forward[CMD_FAIL_HTLC]].message assert(cmd.id == add.id) assert(cmd.reason == FailureReason.LocalFailure(IncorrectOrUnknownPaymentDetails(1000 msat, nodeParams.currentBlockHeight))) @@ -801,7 +816,7 @@ class MultiPartHandlerSpec extends TestKitBaseClass with FixtureAnyFunSuiteLike val add = UpdateAddHtlc(ByteVector32.One, 0, 1000 msat, paymentHash, defaultExpiry, TestConstants.emptyOnionPacket, None, accountable = false, None) val invoice = Bolt11Invoice(Block.Testnet3GenesisBlock.hash, None, paymentHash, randomKey(), Left("dummy"), CltvExpiryDelta(12)) val incomingPayment = IncomingStandardPayment(invoice, paymentPreimage, PaymentType.Standard, invoice.createdAt.toTimestampMilli, IncomingPaymentStatus.Pending) - val fulfill = DoFulfill(incomingPayment, MultiPartPaymentFSM.MultiPartPaymentSucceeded(paymentHash, Queue(HtlcPart(1000 msat, add, TimestampMilli.now())))) + val fulfill = DoFulfill(incomingPayment, MultiPartPaymentFSM.MultiPartPaymentSucceeded(paymentHash, Queue(HtlcPart(1000 msat, add, randomKey().publicKey, TimestampMilli.now())))) sender.send(handlerWithoutMpp, fulfill) val cmd = register.expectMsgType[Register.Forward[CMD_FAIL_HTLC]].message assert(cmd.id == add.id) diff --git a/eclair-core/src/test/scala/fr/acinq/eclair/payment/MultiPartPaymentFSMSpec.scala b/eclair-core/src/test/scala/fr/acinq/eclair/payment/MultiPartPaymentFSMSpec.scala index 66beba446f..6841e0b894 100644 --- a/eclair-core/src/test/scala/fr/acinq/eclair/payment/MultiPartPaymentFSMSpec.scala +++ b/eclair-core/src/test/scala/fr/acinq/eclair/payment/MultiPartPaymentFSMSpec.scala @@ -21,10 +21,9 @@ import akka.testkit.{TestActorRef, TestProbe} import fr.acinq.bitcoin.scalacompat.ByteVector32 import fr.acinq.eclair.payment.receive.MultiPartPaymentFSM import fr.acinq.eclair.payment.receive.MultiPartPaymentFSM._ -import fr.acinq.eclair.reputation.Reputation import fr.acinq.eclair.wire.protocol import fr.acinq.eclair.wire.protocol.{IncorrectOrUnknownPaymentDetails, UpdateAddHtlc} -import fr.acinq.eclair.{BlockHeight, CltvExpiry, MilliSatoshi, MilliSatoshiLong, NodeParams, TestConstants, TestKitBaseClass, TimestampMilli, randomBytes32} +import fr.acinq.eclair.{BlockHeight, CltvExpiry, MilliSatoshi, MilliSatoshiLong, NodeParams, TestConstants, TestKitBaseClass, TimestampMilli, randomBytes32, randomKey} import org.scalatest.funsuite.AnyFunSuiteLike import scodec.bits.ByteVector @@ -92,7 +91,7 @@ class MultiPartPaymentFSMSpec extends TestKitBaseClass with AnyFunSuiteLike { f.parent.send(f.handler, extraPart) val fail = f.parent.expectMsgType[ExtraPaymentReceived[PaymentPart]] assert(fail.paymentHash == paymentHash) - assert(fail.failure == Some(protocol.PaymentTimeout())) + assert(fail.failure.contains(protocol.PaymentTimeout())) assert(fail.payment == extraPart) f.parent.expectNoMessage(50 millis) @@ -228,13 +227,13 @@ class MultiPartPaymentFSMSpec extends TestKitBaseClass with AnyFunSuiteLike { object MultiPartPaymentFSMSpec { - val paymentHash = randomBytes32() + val paymentHash: ByteVector32 = randomBytes32() def htlcIdToChannelId(htlcId: Long) = ByteVector32(ByteVector.fromLong(htlcId).padLeft(32)) def createMultiPartHtlc(totalAmount: MilliSatoshi, htlcAmount: MilliSatoshi, htlcId: Long): HtlcPart = { val htlc = UpdateAddHtlc(htlcIdToChannelId(htlcId), htlcId, htlcAmount, paymentHash, CltvExpiry(42), TestConstants.emptyOnionPacket, None, accountable = false, None) - HtlcPart(totalAmount, htlc, TimestampMilli.now()) + HtlcPart(totalAmount, htlc, randomKey().publicKey, TimestampMilli.now()) } } \ No newline at end of file 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 d7b636d2c2..0cf1c6c112 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 @@ -580,7 +580,7 @@ class MultiPartPaymentLifecycleSpec extends TestKitBaseClass with FixtureAnyFunS awaitCond(payFsm.stateName == PAYMENT_ABORTED) sender.watch(payFsm) - 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)) + childPayFsm.send(payFsm, PaymentSent(cfg.id, paymentPreimage, finalAmount, e, Seq(PaymentSent.PartialPayment(successId, PaymentEvent.OutgoingPayment(randomBytes32(), randomKey().publicKey, successRoute.amount, 250 unixms), successRoute.channelFee(false), Some(successRoute.fullRoute), 100 unixms)), None, 75 unixms)) sender.expectMsg(PreimageReceived(paymentHash, paymentPreimage, None)) val result = sender.expectMsgType[PaymentSent] assert(result.id == cfg.id) @@ -608,7 +608,7 @@ 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, paymentPreimage, finalAmount, e, Seq(PaymentSent.PartialPayment(childId, route.amount, route.channelFee(false), randomBytes32(), Some(route.fullRoute), 100 unixms, 250 unixms)), None, 75 unixms)) + childPayFsm.send(payFsm, PaymentSent(cfg.id, paymentPreimage, finalAmount, e, Seq(PaymentSent.PartialPayment(childId, PaymentEvent.OutgoingPayment(randomBytes32(), randomKey().publicKey, route.amount, 250 unixms), route.channelFee(false), Some(route.fullRoute), 100 unixms)), None, 75 unixms)) sender.expectMsg(PreimageReceived(paymentHash, paymentPreimage, None)) awaitCond(payFsm.stateName == PAYMENT_SUCCEEDED) @@ -632,7 +632,7 @@ 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), 100 unixms, 250 unixms) + case (childId, route) => PaymentSent.PartialPayment(childId, PaymentEvent.OutgoingPayment(randomBytes32(), randomKey().publicKey, route.amount + route.channelFee(false) + route.blindedFee, 250 unixms), route.channelFee(false) + route.blindedFee, Some(route.fullRoute), 100 unixms) } partialPayments.foreach(pp => childPayFsm.send(payFsm, PaymentSent(cfg.id, paymentPreimage, finalAmount, e, Seq(pp), None, 100 unixms))) sender.expectMsg(PreimageReceived(paymentHash, paymentPreimage, None)) 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 4e43828e77..6c72c0dee6 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 @@ -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, paymentPreimage, finalAmount, priv_c.publicKey, Seq(PartialPayment(UUID.randomUUID(), finalAmount, 0 msat, randomBytes32(), None, 100 unixms, 200 unixms)), None, 80 unixms) + val ps = PaymentSent(id, paymentPreimage, finalAmount, priv_c.publicKey, Seq(PartialPayment(UUID.randomUUID(), PaymentEvent.OutgoingPayment(randomBytes32(), randomKey().publicKey, finalAmount, 200 unixms), 0 msat, None, 100 unixms)), None, 80 unixms) payFsm.send(initiator, ps) sender.expectMsg(ps) 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, paymentPreimage, finalAmount, invoice.nodeId, Seq(PartialPayment(UUID.randomUUID(), finalAmount, 0 msat, randomBytes32(), None, 100 unixms, 200 unixms)), None, 100 unixms) + val ps = PaymentSent(id, paymentPreimage, finalAmount, invoice.nodeId, Seq(PartialPayment(UUID.randomUUID(), PaymentEvent.OutgoingPayment(randomBytes32(), randomKey().publicKey, finalAmount, 200 unixms), 0 msat, None, 100 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 c3003ead4d..0a00f9bc23 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 @@ -109,6 +109,7 @@ class PaymentLifecycleSpec extends BaseRouterSpec { def addCompleted(result: HtlcResult): RES_ADD_SETTLED[Origin, HtlcResult] = { RES_ADD_SETTLED( origin = defaultOrigin, + remoteNodeId = randomKey().publicKey, htlc = UpdateAddHtlc(ByteVector32.Zeroes, 0, defaultAmountMsat, defaultPaymentHash, defaultExpiry, TestConstants.emptyOnionPacket, None, accountable = false, None), result) } @@ -846,8 +847,8 @@ 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] - assert(partAmount == request.amount) + val PaymentSent(_, paymentOK.paymentPreimage, finalAmount, _, PartialPayment(_, part, fee, _, _) :: Nil, _, _) = eventListener.expectMsgType[PaymentSent] + assert(part.amount == request.amount) assert(finalAmount == defaultAmountMsat) // NB: A -> B doesn't pay fees because it's our direct neighbor diff --git a/eclair-core/src/test/scala/fr/acinq/eclair/payment/PostRestartHtlcCleanerSpec.scala b/eclair-core/src/test/scala/fr/acinq/eclair/payment/PostRestartHtlcCleanerSpec.scala index ab4c6ec78a..5c630d6e68 100644 --- a/eclair-core/src/test/scala/fr/acinq/eclair/payment/PostRestartHtlcCleanerSpec.scala +++ b/eclair-core/src/test/scala/fr/acinq/eclair/payment/PostRestartHtlcCleanerSpec.scala @@ -23,7 +23,7 @@ import akka.testkit.TestProbe import com.softwaremill.quicklens.{ModifyPimp, QuicklensAt} import fr.acinq.bitcoin.scalacompat.{Block, ByteVector32, Crypto, OutPoint, SatoshiLong, Script, Transaction, TxId, TxIn, TxOut} import fr.acinq.eclair.blockchain.bitcoind.ZmqWatcher.WatchTxConfirmedTriggered -import fr.acinq.eclair.channel.Helpers.{Closing, updateCommitments} +import fr.acinq.eclair.channel.Helpers.Closing import fr.acinq.eclair.channel._ import fr.acinq.eclair.channel.states.ChannelStateTestsBase import fr.acinq.eclair.db.{OutgoingPayment, OutgoingPaymentStatus, PaymentType} @@ -89,9 +89,9 @@ class PostRestartHtlcCleanerSpec extends TestKitBaseClass with FixtureAnyFunSuit // (channel AB2) val relayedPaymentHash = randomBytes32() - val relayed = Origin.Cold(Upstream.Cold.Channel(channelId_ab_1, 5, 10 msat)) + val relayed = Origin.Cold(Upstream.Cold.Channel(channelId_ab_1, a, 5, 10 msat)) val trampolineRelayedPaymentHash = randomBytes32() - val trampolineRelayed = Origin.Cold(Upstream.Cold.Trampoline(Upstream.Cold.Channel(channelId_ab_1, 0, 1000 msat) :: Upstream.Cold.Channel(channelId_ab_2, 2, 2500 msat) :: Nil)) + val trampolineRelayed = Origin.Cold(Upstream.Cold.Trampoline(Upstream.Cold.Channel(channelId_ab_1, a, 0, 1000 msat) :: Upstream.Cold.Channel(channelId_ab_2, a, 2, 2500 msat) :: Nil)) val htlc_ab_1 = Seq( buildHtlcIn(0, channelId_ab_1, trampolineRelayedPaymentHash), @@ -454,7 +454,7 @@ class PostRestartHtlcCleanerSpec extends TestKitBaseClass with FixtureAnyFunSuit // Payment 2 should fulfill once we receive the preimage. val origin_2 = Origin.Cold(Upstream.Cold(upstream_2)) - sender.send(relayer, RES_ADD_SETTLED(origin_2, htlc_2_2, HtlcResult.OnChainFulfill(preimage2))) + sender.send(relayer, RES_ADD_SETTLED(origin_2, a, htlc_2_2, HtlcResult.OnChainFulfill(preimage2))) register.expectMsgAllOf( Register.Forward(replyTo = null, channelId_ab_1, CMD_FULFILL_HTLC(5, preimage2, None, commit = true)), Register.Forward(replyTo = null, channelId_ab_2, CMD_FULFILL_HTLC(9, preimage2, None, commit = true)) @@ -502,10 +502,10 @@ class PostRestartHtlcCleanerSpec extends TestKitBaseClass with FixtureAnyFunSuit buildHtlcOut(5, channelId_bc_1, paymentHash2), ) val origins: Map[Long, Origin] = Map( - 2L -> Origin.Cold(Upstream.Cold.Trampoline(Upstream.Cold.Channel(channelId_ab_1, 1, 500 msat) :: Upstream.Cold.Channel(channelId_ab_1, 2, 200 msat) :: Nil)), - 3L -> Origin.Cold(Upstream.Cold.Trampoline(Upstream.Cold.Channel(channelId_ab_1, 1, 500 msat) :: Upstream.Cold.Channel(channelId_ab_1, 2, 200 msat) :: Nil)), - 4L -> Origin.Cold(Upstream.Cold.Trampoline(Upstream.Cold.Channel(channelId_ab_1, 1, 500 msat) :: Upstream.Cold.Channel(channelId_ab_1, 2, 200 msat) :: Nil)), - 5L -> Origin.Cold(Upstream.Cold.Channel(channelId_ab_1, 4, 550 msat)), + 2L -> Origin.Cold(Upstream.Cold.Trampoline(Upstream.Cold.Channel(channelId_ab_1, a, 1, 500 msat) :: Upstream.Cold.Channel(channelId_ab_1, a, 2, 200 msat) :: Nil)), + 3L -> Origin.Cold(Upstream.Cold.Trampoline(Upstream.Cold.Channel(channelId_ab_1, a, 1, 500 msat) :: Upstream.Cold.Channel(channelId_ab_1, a, 2, 200 msat) :: Nil)), + 4L -> Origin.Cold(Upstream.Cold.Trampoline(Upstream.Cold.Channel(channelId_ab_1, a, 1, 500 msat) :: Upstream.Cold.Channel(channelId_ab_1, a, 2, 200 msat) :: Nil)), + 5L -> Origin.Cold(Upstream.Cold.Channel(channelId_ab_1, a, 4, 550 msat)), ) val downstreamChannel = { val normal = ChannelCodecsSpec.makeChannelDataNormal(htlc_bc, origins) @@ -547,7 +547,7 @@ class PostRestartHtlcCleanerSpec extends TestKitBaseClass with FixtureAnyFunSuit import f._ val htlc_ab = buildHtlcIn(0, channelId_ab_1, paymentHash1, blinded = true) - val upstream = Upstream.Cold.Channel(htlc_ab.add) + val upstream = Upstream.Cold.Channel(htlc_ab.add, a) val htlc_bc = buildHtlcOut(6, channelId_bc_1, paymentHash1, blinded = true) val data_ab = ChannelCodecsSpec.makeChannelDataNormal(Seq(htlc_ab), Map.empty) val data_bc = ChannelCodecsSpec.makeChannelDataNormal(Seq(htlc_bc), Map(6L -> Origin.Cold(upstream))) @@ -591,7 +591,7 @@ class PostRestartHtlcCleanerSpec extends TestKitBaseClass with FixtureAnyFunSuit sender.send(relayer, buildForwardFail(testCase.downstream_1_1, testCase.upstream_1)) val fails = register.expectMsgType[Register.Forward[CMD_FAIL_HTLC]] :: register.expectMsgType[Register.Forward[CMD_FAIL_HTLC]] :: Nil assert(fails.toSet == testCase.upstream_1.originHtlcs.map { - case Upstream.Cold.Channel(channelId, htlcId, _) => Register.Forward(null, channelId, CMD_FAIL_HTLC(htlcId, FailureReason.LocalFailure(TemporaryNodeFailure()), None, commit = true)) + case Upstream.Cold.Channel(channelId, _, htlcId, _) => Register.Forward(null, channelId, CMD_FAIL_HTLC(htlcId, FailureReason.LocalFailure(TemporaryNodeFailure()), None, commit = true)) }.toSet) sender.send(relayer, buildForwardFail(testCase.downstream_1_1, testCase.upstream_1)) @@ -603,7 +603,7 @@ class PostRestartHtlcCleanerSpec extends TestKitBaseClass with FixtureAnyFunSuit sender.send(relayer, buildForwardFail(testCase.downstream_2_3, testCase.upstream_2)) register.expectMsg(testCase.upstream_2.originHtlcs.map { - case Upstream.Cold.Channel(channelId, htlcId, _) => Register.Forward(null, channelId, CMD_FAIL_HTLC(htlcId, FailureReason.LocalFailure(TemporaryNodeFailure()), None, commit = true)) + case Upstream.Cold.Channel(channelId, _, htlcId, _) => Register.Forward(null, channelId, CMD_FAIL_HTLC(htlcId, FailureReason.LocalFailure(TemporaryNodeFailure()), None, commit = true)) }.head) register.expectNoMessage(100 millis) @@ -622,7 +622,7 @@ class PostRestartHtlcCleanerSpec extends TestKitBaseClass with FixtureAnyFunSuit sender.send(relayer, buildForwardFulfill(testCase.downstream_1_1, testCase.upstream_1, preimage1)) val fulfills = register.expectMsgType[Register.Forward[CMD_FULFILL_HTLC]] :: register.expectMsgType[Register.Forward[CMD_FULFILL_HTLC]] :: Nil assert(fulfills.toSet == testCase.upstream_1.originHtlcs.map { - case Upstream.Cold.Channel(channelId, htlcId, _) => Register.Forward(null, channelId, CMD_FULFILL_HTLC(htlcId, preimage1, None, commit = true)) + case Upstream.Cold.Channel(channelId, _, htlcId, _) => Register.Forward(null, channelId, CMD_FULFILL_HTLC(htlcId, preimage1, None, commit = true)) }.toSet) sender.send(relayer, buildForwardFulfill(testCase.downstream_1_1, testCase.upstream_1, preimage1)) @@ -631,7 +631,7 @@ class PostRestartHtlcCleanerSpec extends TestKitBaseClass with FixtureAnyFunSuit // This payment has 3 downstream HTLCs, but we should fulfill upstream as soon as we receive the preimage. sender.send(relayer, buildForwardFulfill(testCase.downstream_2_1, testCase.upstream_2, preimage2)) register.expectMsg(testCase.upstream_2.originHtlcs.map { - case Upstream.Cold.Channel(channelId, htlcId, _) => Register.Forward(null, channelId, CMD_FULFILL_HTLC(htlcId, preimage2, None, commit = true)) + case Upstream.Cold.Channel(channelId, _, htlcId, _) => Register.Forward(null, channelId, CMD_FULFILL_HTLC(htlcId, preimage2, None, commit = true)) }.head) sender.send(relayer, buildForwardFulfill(testCase.downstream_2_2, testCase.upstream_2, preimage2)) @@ -651,7 +651,7 @@ class PostRestartHtlcCleanerSpec extends TestKitBaseClass with FixtureAnyFunSuit sender.send(relayer, buildForwardFail(testCase.downstream_2_1, testCase.upstream_2)) sender.send(relayer, buildForwardFulfill(testCase.downstream_2_2, testCase.upstream_2, preimage2)) register.expectMsg(testCase.upstream_2.originHtlcs.map { - case Upstream.Cold.Channel(channelId, htlcId, _) => Register.Forward(null, channelId, CMD_FULFILL_HTLC(htlcId, preimage2, None, commit = true)) + case Upstream.Cold.Channel(channelId, _, htlcId, _) => Register.Forward(null, channelId, CMD_FULFILL_HTLC(htlcId, preimage2, None, commit = true)) }.head) sender.send(relayer, buildForwardFail(testCase.downstream_2_3, testCase.upstream_2)) @@ -677,8 +677,8 @@ class PostRestartHtlcCleanerSpec extends TestKitBaseClass with FixtureAnyFunSuit buildHtlcOut(2, channelId_bc_1, paymentHash2).modify(_.add.tlvStream).setTo(TlvStream(UpdateAddHtlcTlv.FundingFeeTlv(LiquidityAds.FundingFee(1500 msat, TxId(randomBytes32()))))), // trampoline relayed ) - val upstreamChannel = Upstream.Cold.Channel(htlc_ab(1).add) - val upstreamTrampoline = Upstream.Cold.Trampoline(Upstream.Cold.Channel(htlc_ab(2).add) :: Nil) + val upstreamChannel = Upstream.Cold.Channel(htlc_ab(1).add, a) + val upstreamTrampoline = Upstream.Cold.Trampoline(Upstream.Cold.Channel(htlc_ab(2).add, a) :: Nil) val data_ab = ChannelCodecsSpec.makeChannelDataNormal(htlc_ab, Map.empty) val data_bc = ChannelCodecsSpec.makeChannelDataNormal(htlc_bc, Map(1L -> Origin.Cold(upstreamChannel), 2L -> Origin.Cold(upstreamTrampoline))) @@ -686,7 +686,7 @@ class PostRestartHtlcCleanerSpec extends TestKitBaseClass with FixtureAnyFunSuit relayer ! PostRestartHtlcCleaner.Init(Seq(data_ab, data_bc)) // HTLC failures are not relayed upstream, as we will retry until we reach the HTLC timeout. - sender.send(relayer, buildForwardFail(htlc_bc(0).add, Upstream.Cold.Channel(htlc_ab(0).add))) + sender.send(relayer, buildForwardFail(htlc_bc(0).add, Upstream.Cold.Channel(htlc_ab(0).add, a))) sender.send(relayer, buildForwardFail(htlc_bc(0).add, upstreamChannel)) sender.send(relayer, buildForwardOnChainFail(htlc_bc(0).add, upstreamChannel)) sender.send(relayer, buildForwardFail(htlc_bc(1).add, upstreamTrampoline)) @@ -712,7 +712,7 @@ class PostRestartHtlcCleanerSpec extends TestKitBaseClass with FixtureAnyFunSuit val relayedPaymentHash = randomBytes32() val trampolineRelayedPaymentHash = randomBytes32() - val trampolineRelayed = Origin.Cold(Upstream.Cold.Trampoline(Upstream.Cold.Channel(channelId_ab_2, 0, 1 msat) :: Nil)) + val trampolineRelayed = Origin.Cold(Upstream.Cold.Trampoline(Upstream.Cold.Channel(channelId_ab_2, a, 0, 1 msat) :: Nil)) val relayedHtlcIn = buildHtlcIn(0L, channelId_ab_2, trampolineRelayedPaymentHash) val nonRelayedHtlcIn = buildHtlcIn(1L, channelId_ab_2, relayedPaymentHash) @@ -767,13 +767,13 @@ class PostRestartHtlcCleanerSpec extends TestKitBaseClass with FixtureAnyFunSuit object PostRestartHtlcCleanerSpec { - val channelId_ab_1 = randomBytes32() - val channelId_ab_2 = randomBytes32() - val channelId_bc_1 = randomBytes32() - val channelId_bc_2 = randomBytes32() - val channelId_bc_3 = randomBytes32() - val channelId_bc_4 = randomBytes32() - val channelId_bc_5 = randomBytes32() + val channelId_ab_1: ByteVector32 = randomBytes32() + val channelId_ab_2: ByteVector32 = randomBytes32() + val channelId_bc_1: ByteVector32 = randomBytes32() + val channelId_bc_2: ByteVector32 = randomBytes32() + val channelId_bc_3: ByteVector32 = randomBytes32() + val channelId_bc_4: ByteVector32 = randomBytes32() + val channelId_bc_5: ByteVector32 = randomBytes32() val (preimage1, preimage2, preimage3) = (randomBytes32(), randomBytes32(), randomBytes32()) val (paymentHash1, paymentHash2, paymentHash3) = (Crypto.sha256(preimage1), Crypto.sha256(preimage2), Crypto.sha256(preimage3)) @@ -798,13 +798,13 @@ object PostRestartHtlcCleanerSpec { } def buildForwardFail(add: UpdateAddHtlc, upstream: Upstream.Cold): RES_ADD_SETTLED[Origin.Cold, HtlcResult.Fail] = - RES_ADD_SETTLED(Origin.Cold(upstream), add, HtlcResult.RemoteFail(UpdateFailHtlc(add.channelId, add.id, ByteVector.empty))) + RES_ADD_SETTLED(Origin.Cold(upstream), randomKey().publicKey, add, HtlcResult.RemoteFail(UpdateFailHtlc(add.channelId, add.id, ByteVector.empty))) def buildForwardOnChainFail(add: UpdateAddHtlc, upstream: Upstream.Cold): RES_ADD_SETTLED[Origin.Cold, HtlcResult.Fail] = - RES_ADD_SETTLED(Origin.Cold(upstream), add, HtlcResult.OnChainFail(HtlcsTimedoutDownstream(add.channelId, Set(add)))) + RES_ADD_SETTLED(Origin.Cold(upstream), randomKey().publicKey, add, HtlcResult.OnChainFail(HtlcsTimedoutDownstream(add.channelId, Set(add)))) def buildForwardFulfill(add: UpdateAddHtlc, upstream: Upstream.Cold, preimage: ByteVector32): RES_ADD_SETTLED[Origin.Cold, HtlcResult.Fulfill] = - RES_ADD_SETTLED(Origin.Cold(upstream), add, HtlcResult.RemoteFulfill(UpdateFulfillHtlc(add.channelId, add.id, preimage))) + RES_ADD_SETTLED(Origin.Cold(upstream), randomKey().publicKey, add, HtlcResult.RemoteFulfill(UpdateFulfillHtlc(add.channelId, add.id, preimage))) case class LocalPaymentTest(channel: PersistentChannelData, parentId: UUID, childIds: Seq[UUID], fails: Seq[RES_ADD_SETTLED[Origin.Cold, HtlcResult.Fail]], fulfills: Seq[RES_ADD_SETTLED[Origin.Cold, HtlcResult.Fulfill]]) @@ -846,7 +846,7 @@ object PostRestartHtlcCleanerSpec { buildHtlcIn(0, channelId_ab_1, paymentHash1) ) - val upstream_1 = Upstream.Cold.Channel(htlc_ab_1.head.add) + val upstream_1 = Upstream.Cold.Channel(htlc_ab_1.head.add, a) val htlc_bc_1 = Seq( buildHtlcOut(6, channelId_bc_1, paymentHash1) @@ -897,12 +897,12 @@ object PostRestartHtlcCleanerSpec { buildHtlcOut(9, channelId_ab_2, randomBytes32()) // ignored ) - val upstream_1 = Upstream.Cold.Trampoline(Upstream.Cold.Channel(channelId_ab_1, 0, 500 msat) :: Upstream.Cold.Channel(channelId_ab_2, 7, 250 msat) :: Nil) - val upstream_2 = Upstream.Cold.Trampoline(Upstream.Cold.Channel(channelId_ab_1, 5, 500 msat) :: Nil) + val upstream_1 = Upstream.Cold.Trampoline(Upstream.Cold.Channel(channelId_ab_1, a, 0, 500 msat) :: Upstream.Cold.Channel(channelId_ab_2, a, 7, 250 msat) :: Nil) + val upstream_2 = Upstream.Cold.Trampoline(Upstream.Cold.Channel(channelId_ab_1, a, 5, 500 msat) :: Nil) // The following two origins reference upstream HTLCs that have already been settled. // They should be ignored by the post-restart clean-up. - val upstream_3 = Upstream.Cold.Trampoline(Upstream.Cold.Channel(channelId_ab_1, 57, 100 msat) :: Nil) - val upstream_4 = Upstream.Cold.Channel(channelId_ab_2, 57, 150 msat) + val upstream_3 = Upstream.Cold.Trampoline(Upstream.Cold.Channel(channelId_ab_1, a, 57, 100 msat) :: Nil) + val upstream_4 = Upstream.Cold.Channel(channelId_ab_2, a, 57, 150 msat) // Downstream HTLCs. val htlc_bc_1 = Seq( 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 1603964e70..8120339cc9 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 @@ -680,7 +680,7 @@ class ChannelRelayerSpec extends ScalaTestWithActorTestKit(ConfigFactory.load("a receiveConfidence(Reputation.Score.max(accountable = false)) val fwd = expectFwdAdd(register, channelIds(realScid1), outgoingAmount, outgoingExpiry, outAccountable = false) fwd.message.replyTo ! RES_SUCCESS(fwd.message, channelId1) - fwd.message.origin.replyTo ! RES_ADD_SETTLED(fwd.message.origin, downstream_htlc, testCase.result) + fwd.message.origin.replyTo ! RES_ADD_SETTLED(fwd.message.origin, remoteNodeId1, downstream_htlc, testCase.result) expectFwdFail(register, r.add.channelId, testCase.cmd) } } @@ -707,7 +707,7 @@ class ChannelRelayerSpec extends ScalaTestWithActorTestKit(ConfigFactory.load("a receiveConfidence(Reputation.Score(1.0, accountable = false)) val fwd = expectFwdAdd(register, channelId1, outgoingAmount, outgoingExpiry, outAccountable = false) fwd.message.replyTo ! RES_SUCCESS(fwd.message, channelId1) - fwd.message.origin.replyTo ! RES_ADD_SETTLED(fwd.message.origin, downstream, htlcResult) + fwd.message.origin.replyTo ! RES_ADD_SETTLED(fwd.message.origin, remoteNodeId1, downstream, htlcResult) val cmd = register.expectMessageType[Register.Forward[channel.Command]] assert(cmd.channelId == r.add.channelId) if (isIntroduction) { @@ -753,7 +753,7 @@ class ChannelRelayerSpec extends ScalaTestWithActorTestKit(ConfigFactory.load("a val fwd1 = expectFwdAdd(register, channelIds(realScid1), outgoingAmount, outgoingExpiry, outAccountable = false) fwd1.message.replyTo ! RES_SUCCESS(fwd1.message, channelId1) - fwd1.message.origin.replyTo ! RES_ADD_SETTLED(fwd1.message.origin, downstream_htlc, testCase.result) + fwd1.message.origin.replyTo ! RES_ADD_SETTLED(fwd1.message.origin, remoteNodeId2, downstream_htlc, testCase.result) val fwd2 = register.expectMessageType[Register.Forward[CMD_FULFILL_HTLC]] assert(fwd2.channelId == r.add.channelId) @@ -763,9 +763,11 @@ class ChannelRelayerSpec extends ScalaTestWithActorTestKit(ConfigFactory.load("a val paymentRelayed = eventListener.expectMessageType[ChannelPaymentRelayed] assert(paymentRelayed.paymentHash == r.add.paymentHash) assert(paymentRelayed.amountIn == r.add.amountMsat) - assert(paymentRelayed.fromChannelId == r.add.channelId) + assert(paymentRelayed.paymentIn.channelId == r.add.channelId) + assert(paymentRelayed.paymentIn.remoteNodeId == TestConstants.Alice.nodeParams.nodeId) assert(paymentRelayed.amountOut == r.amountToForward) - assert(paymentRelayed.toChannelId == channelId1) + assert(paymentRelayed.paymentOut.channelId == channelId1) + assert(paymentRelayed.paymentOut.remoteNodeId == remoteNodeId2) assert(paymentRelayed.startedAt == r.receivedAt) assert(paymentRelayed.settledAt >= now) } @@ -787,7 +789,7 @@ class ChannelRelayerSpec extends ScalaTestWithActorTestKit(ConfigFactory.load("a // The downstream HTLC is fulfilled. val downstream = UpdateAddHtlc(randomBytes32(), 7, outgoingAmount, paymentHash, outgoingExpiry, emptyOnionPacket, None, accountable = false, None) - fwd1.message.origin.replyTo ! RES_ADD_SETTLED(fwd1.message.origin, downstream, HtlcResult.RemoteFulfill(UpdateFulfillHtlc(downstream.channelId, downstream.id, paymentPreimage))) + fwd1.message.origin.replyTo ! RES_ADD_SETTLED(fwd1.message.origin, remoteNodeId2, downstream, HtlcResult.RemoteFulfill(UpdateFulfillHtlc(downstream.channelId, downstream.id, paymentPreimage))) val fulfill = inside(register.expectMessageType[Register.Forward[CMD_FULFILL_HTLC]]) { fwd => assert(fwd.channelId == r.add.channelId) assert(fwd.message.id == r.add.id) @@ -799,7 +801,7 @@ class ChannelRelayerSpec extends ScalaTestWithActorTestKit(ConfigFactory.load("a eventually(assert(nodeParams.db.pendingCommands.listSettlementCommands(r.add.channelId) == Seq(fulfill.copy(commit = false)))) // The downstream HTLC is now failed (e.g. because a revoked commitment confirmed that doesn't include it): this conflicting command is ignored. - fwd1.message.origin.replyTo ! RES_ADD_SETTLED(fwd1.message.origin, downstream, HtlcResult.OnChainFail(HtlcOverriddenByLocalCommit(downstream.channelId, downstream))) + fwd1.message.origin.replyTo ! RES_ADD_SETTLED(fwd1.message.origin, remoteNodeId2, downstream, HtlcResult.OnChainFail(HtlcOverriddenByLocalCommit(downstream.channelId, downstream))) register.expectNoMessage(100 millis) assert(nodeParams.db.pendingCommands.listSettlementCommands(r.add.channelId) == Seq(fulfill.copy(commit = false))) } @@ -868,7 +870,9 @@ object ChannelRelayerSpec { val localAlias2: Alias = Alias(222000) val channelId1: ByteVector32 = randomBytes32() + val remoteNodeId1: PublicKey = randomKey().publicKey val channelId2: ByteVector32 = randomBytes32() + val remoteNodeId2: PublicKey = randomKey().publicKey val channelIds: Map[ShortChannelId, ByteVector32] = Map( realScid1 -> channelId1, 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 9d37bd3310..f8387fae4d 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 @@ -1213,7 +1213,7 @@ object NodeRelayerSpec { (paymentPackets.map(_.outerPayload.expiry).min - nodeParams.relayParams.asyncPaymentsParams.cancelSafetyBeforeTimeout).blockHeight def createSuccessEvent(): PaymentSent = - PaymentSent(relayId, paymentPreimage, outgoingAmount, outgoingNodeId, Seq(PaymentSent.PartialPayment(UUID.randomUUID(), outgoingAmount, 10 msat, randomBytes32(), None, 0 unixms, 50 unixms)), None, 0 unixms) + PaymentSent(relayId, paymentPreimage, outgoingAmount, outgoingNodeId, Seq(PaymentSent.PartialPayment(UUID.randomUUID(), PaymentEvent.OutgoingPayment(randomBytes32(), randomKey().publicKey, outgoingAmount, 50 unixms), 10 msat, None, 0 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-core/src/test/scala/fr/acinq/eclair/payment/relay/OnTheFlyFundingSpec.scala b/eclair-core/src/test/scala/fr/acinq/eclair/payment/relay/OnTheFlyFundingSpec.scala index 4b77e96e7f..36d41eee1e 100644 --- a/eclair-core/src/test/scala/fr/acinq/eclair/payment/relay/OnTheFlyFundingSpec.scala +++ b/eclair-core/src/test/scala/fr/acinq/eclair/payment/relay/OnTheFlyFundingSpec.scala @@ -860,9 +860,9 @@ class OnTheFlyFundingSpec extends TestKitBaseClass with FixtureAnyFunSuiteLike { // The payments are fulfilled. val (add1, add2) = if (cmd1.paymentHash == paymentHash1) (cmd1, cmd2) else (cmd2, cmd1) val outgoing = Seq(add1, add2).map(add => UpdateAddHtlc(purchase.channelId, randomHtlcId(), add.amount, add.paymentHash, add.cltvExpiry, add.onion, add.nextPathKey_opt, add.reputationScore.accountable, add.fundingFee_opt)) - add1.replyTo ! RES_ADD_SETTLED(add1.origin, outgoing.head, HtlcResult.RemoteFulfill(UpdateFulfillHtlc(purchase.channelId, outgoing.head.id, preimage1))) + add1.replyTo ! RES_ADD_SETTLED(add1.origin, remoteNodeId, outgoing.head, HtlcResult.RemoteFulfill(UpdateFulfillHtlc(purchase.channelId, outgoing.head.id, preimage1))) verifyFulfilledUpstream(upstream1, preimage1) - add2.replyTo ! RES_ADD_SETTLED(add2.origin, outgoing.last, HtlcResult.OnChainFulfill(preimage2)) + add2.replyTo ! RES_ADD_SETTLED(add2.origin, remoteNodeId, outgoing.last, HtlcResult.OnChainFulfill(preimage2)) verifyFulfilledUpstream(upstream2, preimage2) awaitCond(nodeParams.db.liquidity.listPendingOnTheFlyFunding(remoteNodeId).isEmpty, interval = 100 millis) register.expectNoMessage(100 millis) @@ -914,7 +914,7 @@ class OnTheFlyFundingSpec extends TestKitBaseClass with FixtureAnyFunSuiteLike { val htlc = UpdateAddHtlc(channelId, randomHtlcId(), add.amount, paymentHash, add.cltvExpiry, add.onion, add.nextPathKey_opt, add.reputationScore.accountable, add.fundingFee_opt) val fail = UpdateFailHtlc(channelId, htlc.id, randomBytes(50)) add.replyTo ! RES_SUCCESS(add, purchase.channelId) - add.replyTo ! RES_ADD_SETTLED(add.origin, htlc, HtlcResult.RemoteFail(fail)) + add.replyTo ! RES_ADD_SETTLED(add.origin, remoteNodeId, htlc, HtlcResult.RemoteFail(fail)) }) adds1.last.replyTo ! RES_ADD_FAILED(adds1.last, TooManyAcceptedHtlcs(channelId, 5), None) register.expectNoMessage(100 millis) @@ -932,7 +932,7 @@ class OnTheFlyFundingSpec extends TestKitBaseClass with FixtureAnyFunSuiteLike { // The payment succeeds. adds2.foreach(add => { val htlc = UpdateAddHtlc(channelId, randomHtlcId(), add.amount, paymentHash, add.cltvExpiry, add.onion, add.nextPathKey_opt, add.reputationScore.accountable, add.fundingFee_opt) - add.replyTo ! RES_ADD_SETTLED(add.origin, htlc, HtlcResult.OnChainFulfill(preimage)) + add.replyTo ! RES_ADD_SETTLED(add.origin, remoteNodeId, htlc, HtlcResult.OnChainFulfill(preimage)) }) val fwds = Seq( register.expectMsgType[Register.Forward[CMD_FULFILL_HTLC]], @@ -1002,7 +1002,7 @@ class OnTheFlyFundingSpec extends TestKitBaseClass with FixtureAnyFunSuiteLike { channel.expectNoMessage(100 millis) // The payment is fulfilled by our peer. - cmd2.replyTo ! RES_ADD_SETTLED(cmd2.origin, htlc, HtlcResult.OnChainFulfill(preimage)) + cmd2.replyTo ! RES_ADD_SETTLED(cmd2.origin, remoteNodeId, htlc, HtlcResult.OnChainFulfill(preimage)) verifyFulfilledUpstream(upstream, preimage) nodeParams.db.liquidity.addOnTheFlyFundingPreimage(preimage) register.expectNoMessage(100 millis) @@ -1044,7 +1044,7 @@ class OnTheFlyFundingSpec extends TestKitBaseClass with FixtureAnyFunSuiteLike { channel.expectNoMessage(100 millis) val add = UpdateAddHtlc(purchase.channelId, randomHtlcId(), cmd.amount, cmd.paymentHash, cmd.cltvExpiry, cmd.onion, cmd.nextPathKey_opt, cmd.reputationScore.accountable, cmd.fundingFee_opt) - cmd.replyTo ! RES_ADD_SETTLED(cmd.origin, add, HtlcResult.RemoteFulfill(UpdateFulfillHtlc(purchase.channelId, add.id, preimage2))) + cmd.replyTo ! RES_ADD_SETTLED(cmd.origin, remoteNodeId, add, HtlcResult.RemoteFulfill(UpdateFulfillHtlc(purchase.channelId, add.id, preimage2))) verifyFulfilledUpstream(upstream2, preimage2) register.expectNoMessage(100 millis) awaitCond(nodeParams.db.liquidity.getFeeCredit(remoteNodeId) == 0.msat, interval = 100 millis) diff --git a/eclair-core/src/test/scala/fr/acinq/eclair/payment/relay/RelayerSpec.scala b/eclair-core/src/test/scala/fr/acinq/eclair/payment/relay/RelayerSpec.scala index 02df2b2661..6aa411c0a0 100644 --- a/eclair-core/src/test/scala/fr/acinq/eclair/payment/relay/RelayerSpec.scala +++ b/eclair-core/src/test/scala/fr/acinq/eclair/payment/relay/RelayerSpec.scala @@ -32,6 +32,7 @@ import fr.acinq.eclair.payment.Bolt11Invoice import fr.acinq.eclair.payment.IncomingPaymentPacket.FinalPacket import fr.acinq.eclair.payment.OutgoingPaymentPacket.{NodePayload, buildOnion, buildOutgoingPayment} import fr.acinq.eclair.payment.PaymentPacketSpec._ +import fr.acinq.eclair.payment.receive.MultiPartHandler import fr.acinq.eclair.payment.relay.Relayer._ import fr.acinq.eclair.payment.send.{ClearRecipient, TrampolinePayment} import fr.acinq.eclair.reputation.{Reputation, ReputationRecorder} @@ -76,8 +77,8 @@ class RelayerSpec extends ScalaTestWithActorTestKit(ConfigFactory.load("applicat withFixture(test.toNoArgTest(FixtureParam(nodeParams, relayer, router, register, childActors, paymentHandler, reputationRecorder))) } - val channelId_ab = randomBytes32() - val channelId_bc = randomBytes32() + val channelId_ab: ByteVector32 = randomBytes32() + val channelId_bc: ByteVector32 = randomBytes32() test("relay an htlc-add") { f => import f._ @@ -112,7 +113,7 @@ class RelayerSpec extends ScalaTestWithActorTestKit(ConfigFactory.load("applicat val add_ab = UpdateAddHtlc(channelId_ab, 123456, payment.cmd.amount, payment.cmd.paymentHash, payment.cmd.cltvExpiry, payment.cmd.onion, None, accountable = false, None) relayer ! RelayForward(add_ab, priv_a.publicKey, 0.05) - val fp = paymentHandler.expectMessageType[FinalPacket] + val fp = paymentHandler.expectMessageType[MultiPartHandler.ReceivePacket].packet assert(fp.add == add_ab) assert(fp.payload == FinalPayload.Standard.createPayload(finalAmount, finalAmount, finalExpiry, paymentSecret, upgradeAccountability = false)) @@ -133,7 +134,7 @@ class RelayerSpec extends ScalaTestWithActorTestKit(ConfigFactory.load("applicat val add_ab = UpdateAddHtlc(channelId_ab, 123456, payment.cmd.amount, payment.cmd.paymentHash, payment.cmd.cltvExpiry, payment.cmd.onion, None, accountable = false, None) relayer ! RelayForward(add_ab, priv_a.publicKey, 0.05) - val fp = paymentHandler.expectMessageType[FinalPacket] + val fp = paymentHandler.expectMessageType[MultiPartHandler.ReceivePacket].packet assert(fp.add == add_ab) assert(fp.payload.isInstanceOf[FinalPayload.Standard]) assert(fp.payload.amount == finalAmount) @@ -225,14 +226,14 @@ class RelayerSpec extends ScalaTestWithActorTestKit(ConfigFactory.load("applicat val trampolineOrigin = Origin.Hot(replyTo.ref.toClassic, Upstream.Hot.Trampoline(List(Upstream.Hot.Channel(add_ab, TimestampMilli.now(), priv_a.publicKey, 0.1)))) val addSettled = Seq( - RES_ADD_SETTLED(channelOrigin, add_bc, HtlcResult.OnChainFulfill(randomBytes32())), - RES_ADD_SETTLED(channelOrigin, add_bc, HtlcResult.RemoteFulfill(UpdateFulfillHtlc(add_bc.channelId, add_bc.id, randomBytes32()))), - RES_ADD_SETTLED(channelOrigin, add_bc, HtlcResult.OnChainFail(HtlcsTimedoutDownstream(channelId_bc, Set(add_bc)))), - RES_ADD_SETTLED(channelOrigin, add_bc, HtlcResult.RemoteFail(UpdateFailHtlc(add_bc.channelId, add_bc.id, randomBytes32()))), - RES_ADD_SETTLED(trampolineOrigin, add_bc, HtlcResult.OnChainFulfill(randomBytes32())), - RES_ADD_SETTLED(trampolineOrigin, add_bc, HtlcResult.RemoteFulfill(UpdateFulfillHtlc(add_bc.channelId, add_bc.id, randomBytes32()))), - RES_ADD_SETTLED(trampolineOrigin, add_bc, HtlcResult.OnChainFail(HtlcsTimedoutDownstream(channelId_bc, Set(add_bc)))), - RES_ADD_SETTLED(trampolineOrigin, add_bc, HtlcResult.RemoteFail(UpdateFailHtlc(add_bc.channelId, add_bc.id, randomBytes32()))) + RES_ADD_SETTLED(channelOrigin, priv_a.publicKey, add_bc, HtlcResult.OnChainFulfill(randomBytes32())), + RES_ADD_SETTLED(channelOrigin, priv_a.publicKey, add_bc, HtlcResult.RemoteFulfill(UpdateFulfillHtlc(add_bc.channelId, add_bc.id, randomBytes32()))), + RES_ADD_SETTLED(channelOrigin, priv_a.publicKey, add_bc, HtlcResult.OnChainFail(HtlcsTimedoutDownstream(channelId_bc, Set(add_bc)))), + RES_ADD_SETTLED(channelOrigin, priv_a.publicKey, add_bc, HtlcResult.RemoteFail(UpdateFailHtlc(add_bc.channelId, add_bc.id, randomBytes32()))), + RES_ADD_SETTLED(trampolineOrigin, priv_a.publicKey, add_bc, HtlcResult.OnChainFulfill(randomBytes32())), + RES_ADD_SETTLED(trampolineOrigin, priv_a.publicKey, add_bc, HtlcResult.RemoteFulfill(UpdateFulfillHtlc(add_bc.channelId, add_bc.id, randomBytes32()))), + RES_ADD_SETTLED(trampolineOrigin, priv_a.publicKey, add_bc, HtlcResult.OnChainFail(HtlcsTimedoutDownstream(channelId_bc, Set(add_bc)))), + RES_ADD_SETTLED(trampolineOrigin, priv_a.publicKey, add_bc, HtlcResult.RemoteFail(UpdateFailHtlc(add_bc.channelId, add_bc.id, randomBytes32()))) ) for (res <- addSettled) { @@ -251,10 +252,10 @@ class RelayerSpec extends ScalaTestWithActorTestKit(ConfigFactory.load("applicat val originCold = Origin.Cold(originHot) val addFulfilled = Seq( - RES_ADD_SETTLED(originHot, add_bc, HtlcResult.OnChainFulfill(randomBytes32())), - RES_ADD_SETTLED(originHot, add_bc, HtlcResult.RemoteFulfill(UpdateFulfillHtlc(add_bc.channelId, add_bc.id, randomBytes32()))), - RES_ADD_SETTLED(originCold, add_bc, HtlcResult.OnChainFulfill(randomBytes32())), - RES_ADD_SETTLED(originCold, add_bc, HtlcResult.RemoteFulfill(UpdateFulfillHtlc(add_bc.channelId, add_bc.id, randomBytes32()))), + RES_ADD_SETTLED(originHot, randomKey().publicKey, add_bc, HtlcResult.OnChainFulfill(randomBytes32())), + RES_ADD_SETTLED(originHot, randomKey().publicKey, add_bc, HtlcResult.RemoteFulfill(UpdateFulfillHtlc(add_bc.channelId, add_bc.id, randomBytes32()))), + RES_ADD_SETTLED(originCold, randomKey().publicKey, add_bc, HtlcResult.OnChainFulfill(randomBytes32())), + RES_ADD_SETTLED(originCold, randomKey().publicKey, add_bc, HtlcResult.RemoteFulfill(UpdateFulfillHtlc(add_bc.channelId, add_bc.id, randomBytes32()))), ) for (res <- addFulfilled) { diff --git a/eclair-core/src/test/scala/fr/acinq/eclair/wire/internal/channel/ChannelCodecsSpec.scala b/eclair-core/src/test/scala/fr/acinq/eclair/wire/internal/channel/ChannelCodecsSpec.scala index fecfd3f169..52b14da60c 100644 --- a/eclair-core/src/test/scala/fr/acinq/eclair/wire/internal/channel/ChannelCodecsSpec.scala +++ b/eclair-core/src/test/scala/fr/acinq/eclair/wire/internal/channel/ChannelCodecsSpec.scala @@ -16,7 +16,7 @@ package fr.acinq.eclair.wire.internal.channel -import fr.acinq.bitcoin.scalacompat.Crypto.PrivateKey +import fr.acinq.bitcoin.scalacompat.Crypto.{PrivateKey, PublicKey} import fr.acinq.bitcoin.scalacompat.{Block, BlockHash, ByteVector32, ByteVector64, Crypto, DeterministicWallet, OutPoint, SatoshiLong, Transaction, TxId} import fr.acinq.eclair._ import fr.acinq.eclair.blockchain.fee.FeeratePerKw @@ -26,7 +26,6 @@ import fr.acinq.eclair.channel.fsm.Channel import fr.acinq.eclair.crypto.ShaChain import fr.acinq.eclair.crypto.keymanager.{ChannelKeyManager, LocalChannelKeyManager, LocalNodeKeyManager, NodeKeyManager} import fr.acinq.eclair.json.JsonSerializers -import fr.acinq.eclair.reputation.Reputation import fr.acinq.eclair.router.Announcements import fr.acinq.eclair.transactions.Transactions.ZeroFeeHtlcTxAnchorOutputsCommitmentFormat import fr.acinq.eclair.transactions._ @@ -156,7 +155,7 @@ object ChannelCodecsSpec { val normal: DATA_NORMAL = { val origins = Map( 42L -> Origin.Cold(Upstream.Local(UUID.randomUUID)), - 15000L -> Origin.Cold(Upstream.Cold.Channel(ByteVector32(ByteVector.fill(32)(42)), 43, 11_000_000 msat)) + 15000L -> Origin.Cold(Upstream.Cold.Channel(ByteVector32(ByteVector.fill(32)(42)), PublicKey(hex"0358e32d245ff5f5a3eb14c78c6f69c67cea7846bdf9aeeb7199e8f6fbb0306484"), 43, 11_000_000 msat)) ) makeChannelDataNormal(htlcs, origins) } 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 50e8cc53b6..34311c980f 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 @@ -619,11 +619,12 @@ class ApiServiceSpec extends AnyFunSuite with ScalatestRouteTest with IdiomaticM test("'send' method should allow blocking until payment completes") { val invoice = "lnbc12580n1pw2ywztsp563x9gq6keftcu8kh9kh5qdj4p5wpmc3h802ekxwspcgfwpz2g25qpp554ganw404sh4yjkwnysgn3wjcxfcq7gtx53gxczkjr9nlpc3hzvqdq2wpskwctddyxqr4rqrzjqwryaup9lh50kkranzgcdnn2fgvx390wgj5jd07rwr3vxeje0glc7z9rtvqqwngqqqqqqqlgqqqqqeqqjqrrt8smgjvfj7sg38dwtr9kc9gg3era9k3t2hvq3cup0jvsrtrxuplevqgfhd3rzvhulgcxj97yjuj8gdx8mllwj4wzjd8gdjhpz3lpqqmfe0a6" + val nextNodeId = PublicKey(hex"030bb6a5e0c6b203c7e2180fb78c7ba4bdce46126761d8201b91ddac089cdecc87") val eclair = mock[Eclair] val mockService = new MockService(eclair) val uuid = UUID.fromString("487da196-a4dc-4b1e-92b4-3e5e905e9f3f") - 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)) + val paymentSent = PaymentSent(uuid, ByteVector32.One, 25 msat, aliceNodeId, Seq(PaymentSent.PartialPayment(uuid, PaymentEvent.OutgoingPayment(ByteVector32.Zeroes, nextNodeId, 28 msat, TimestampMilli(1553784337711L)), 3 msat, None, TimestampMilli(1553784337650L))), 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,7 +633,7 @@ 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":"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}}""" + 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","channelId":"0000000000000000000000000000000000000000000000000000000000000000","nextNodeId":"030bb6a5e0c6b203c7e2180fb78c7ba4bdce46126761d8201b91ddac089cdecc87","amountWithFees":28,"fees":3,"startedAt":{"iso":"2019-03-28T14:45:37.650Z","unix":1553784337},"settledAt":{"iso":"2019-03-28T14:45:37.711Z","unix":1553784337}}],"fees":3,"startedAt":{"iso":"2019-03-28T14:45:37.120Z","unix":1553784337},"settledAt":{"iso":"2019-03-28T14:45:37.711Z","unix":1553784337}}""" assert(response == expected) } @@ -1115,6 +1116,8 @@ class ApiServiceSpec extends AnyFunSuite with ScalatestRouteTest with IdiomaticM val mockService = new MockService(mock[Eclair]) val fixedUUID = UUID.fromString("487da196-a4dc-4b1e-92b4-3e5e905e9f3f") val fundingTxId = TxId.fromValidHex("9fcd45bbaa09c60c991ac0425704163c3f3d2d683c789fa409455b9c97792692") + val previousNodeId = PublicKey(hex"02e899d99662f2e64ea0eeaecb53c4628fa40a22d7185076e42e8a3d67fcb7b8e6") + val nextNodeId = PublicKey(hex"030bb6a5e0c6b203c7e2180fb78c7ba4bdce46126761d8201b91ddac089cdecc87") val wsClient = WSProbe() WS("/ws", wsClient.flow) ~> @@ -1127,26 +1130,26 @@ class ApiServiceSpec extends AnyFunSuite with ScalatestRouteTest with IdiomaticM system.eventStream.publish(pf) wsClient.expectMessage(expectedSerializedPf) - 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}}""" + val ps = PaymentSent(fixedUUID, ByteVector32.One, 25 msat, aliceNodeId, Seq(PaymentSent.PartialPayment(fixedUUID, PaymentEvent.OutgoingPayment(ByteVector32.Zeroes, nextNodeId, 28 msat, settledAt = TimestampMilli(1553784337711L)), 3 msat, None, startedAt = TimestampMilli(1553784337539L))), 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","channelId":"0000000000000000000000000000000000000000000000000000000000000000","nextNodeId":"030bb6a5e0c6b203c7e2180fb78c7ba4bdce46126761d8201b91ddac089cdecc87","amountWithFees":28,"fees":3,"startedAt":{"iso":"2019-03-28T14:45:37.539Z","unix":1553784337},"settledAt":{"iso":"2019-03-28T14:45:37.711Z","unix":1553784337}}],"fees":3,"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","startedAt":{"iso":"2019-03-28T14:56:01.048Z","unix":1553784961},"settledAt":{"iso":"2019-03-28T14:56:03.659Z","unix":1553784963}}""" + val prel = ChannelPaymentRelayed(ByteVector32.Zeroes, PaymentEvent.IncomingPayment(ByteVector32.Zeroes, previousNodeId, 21 msat, TimestampMilli(1553784961048L)), PaymentEvent.OutgoingPayment(ByteVector32.One, nextNodeId, 20 msat, TimestampMilli(1553784963659L))) + val expectedSerializedPrel = """{"type":"payment-relayed","paymentHash":"0000000000000000000000000000000000000000000000000000000000000000","paymentIn":{"channelId":"0000000000000000000000000000000000000000000000000000000000000000","remoteNodeId":"02e899d99662f2e64ea0eeaecb53c4628fa40a22d7185076e42e8a3d67fcb7b8e6","amount":21,"receivedAt":{"iso":"2019-03-28T14:56:01.048Z","unix":1553784961}},"paymentOut":{"channelId":"0100000000000000000000000000000000000000000000000000000000000000","remoteNodeId":"030bb6a5e0c6b203c7e2180fb78c7ba4bdce46126761d8201b91ddac089cdecc87","amount":20,"settledAt":{"iso":"2019-03-28T14:56:03.659Z","unix":1553784963}}}""" assert(serialization.write(prel) == expectedSerializedPrel) system.eventStream.publish(prel) wsClient.expectMessage(expectedSerializedPrel) - val ptrel = TrampolinePaymentRelayed(ByteVector32.Zeroes, Seq(PaymentRelayed.IncomingPart(21 msat, ByteVector32.Zeroes, TimestampMilli(1553784963659L))), Seq(PaymentRelayed.OutgoingPart(8 msat, ByteVector32.Zeroes, TimestampMilli(1553784963659L)), PaymentRelayed.OutgoingPart(10 msat, ByteVector32.One, TimestampMilli(1553784963659L))), bobNodeId, 17 msat) - val expectedSerializedPtrel = """{"type":"trampoline-payment-relayed","paymentHash":"0000000000000000000000000000000000000000000000000000000000000000","incoming":[{"amount":21,"channelId":"0000000000000000000000000000000000000000000000000000000000000000","receivedAt":{"iso":"2019-03-28T14:56:03.659Z","unix":1553784963}}],"outgoing":[{"amount":8,"channelId":"0000000000000000000000000000000000000000000000000000000000000000","settledAt":{"iso":"2019-03-28T14:56:03.659Z","unix":1553784963}},{"amount":10,"channelId":"0100000000000000000000000000000000000000000000000000000000000000","settledAt":{"iso":"2019-03-28T14:56:03.659Z","unix":1553784963}}],"nextTrampolineNodeId":"039dc0e0b1d25905e44fdf6f8e89755a5e219685840d0bc1d28d3308f9628a3585","nextTrampolineAmount":17}""" + val ptrel = TrampolinePaymentRelayed(ByteVector32.Zeroes, Seq(PaymentEvent.IncomingPayment(ByteVector32.Zeroes, previousNodeId, 21 msat, TimestampMilli(1553784963659L))), Seq(PaymentEvent.OutgoingPayment(ByteVector32.Zeroes, nextNodeId, 8 msat, TimestampMilli(1553784963659L)), PaymentEvent.OutgoingPayment(ByteVector32.One, nextNodeId, 10 msat, TimestampMilli(1553784963659L))), bobNodeId, 17 msat) + val expectedSerializedPtrel = """{"type":"trampoline-payment-relayed","paymentHash":"0000000000000000000000000000000000000000000000000000000000000000","incoming":[{"channelId":"0000000000000000000000000000000000000000000000000000000000000000","remoteNodeId":"02e899d99662f2e64ea0eeaecb53c4628fa40a22d7185076e42e8a3d67fcb7b8e6","amount":21,"receivedAt":{"iso":"2019-03-28T14:56:03.659Z","unix":1553784963}}],"outgoing":[{"channelId":"0000000000000000000000000000000000000000000000000000000000000000","remoteNodeId":"030bb6a5e0c6b203c7e2180fb78c7ba4bdce46126761d8201b91ddac089cdecc87","amount":8,"settledAt":{"iso":"2019-03-28T14:56:03.659Z","unix":1553784963}},{"channelId":"0100000000000000000000000000000000000000000000000000000000000000","remoteNodeId":"030bb6a5e0c6b203c7e2180fb78c7ba4bdce46126761d8201b91ddac089cdecc87","amount":10,"settledAt":{"iso":"2019-03-28T14:56:03.659Z","unix":1553784963}}],"nextTrampolineNodeId":"039dc0e0b1d25905e44fdf6f8e89755a5e219685840d0bc1d28d3308f9628a3585","nextTrampolineAmount":17}""" assert(serialization.write(ptrel) == expectedSerializedPtrel) system.eventStream.publish(ptrel) 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","receivedAt":{"iso":"2019-03-28T14:56:03.659Z","unix":1553784963}}]}""" + val precv = PaymentReceived(ByteVector32.Zeroes, Seq(PaymentEvent.IncomingPayment(ByteVector32.Zeroes, previousNodeId, 21 msat, TimestampMilli(1553784963659L)))) + val expectedSerializedPrecv = """{"type":"payment-received","paymentHash":"0000000000000000000000000000000000000000000000000000000000000000","parts":[{"channelId":"0000000000000000000000000000000000000000000000000000000000000000","remoteNodeId":"02e899d99662f2e64ea0eeaecb53c4628fa40a22d7185076e42e8a3d67fcb7b8e6","amount":21,"receivedAt":{"iso":"2019-03-28T14:56:03.659Z","unix":1553784963}}]}""" assert(serialization.write(precv) == expectedSerializedPrecv) system.eventStream.publish(precv) wsClient.expectMessage(expectedSerializedPrecv) From 019f074947e66eac7408d8d44fdfd6e4ee349f95 Mon Sep 17 00:00:00 2001 From: t-bast Date: Fri, 23 Jan 2026 17:09:09 +0100 Subject: [PATCH 2/2] Rename `PartialPayment` to `PaymentPart` This removes the confusion that the payment could be incomplete. --- .../main/scala/fr/acinq/eclair/db/pg/PgAuditDb.scala | 2 +- .../fr/acinq/eclair/db/sqlite/SqliteAuditDb.scala | 2 +- .../scala/fr/acinq/eclair/json/JsonSerializers.scala | 6 +++--- .../scala/fr/acinq/eclair/payment/PaymentEvents.scala | 4 ++-- .../eclair/payment/relay/PostRestartHtlcCleaner.scala | 6 +++--- .../payment/send/MultiPartPaymentLifecycle.scala | 6 +++--- .../acinq/eclair/payment/send/PaymentInitiator.scala | 2 +- .../acinq/eclair/payment/send/PaymentLifecycle.scala | 6 +++--- .../payment/send/TrampolinePaymentLifecycle.scala | 10 +++++----- .../test/scala/fr/acinq/eclair/db/AuditDbSpec.scala | 8 ++++---- .../test/scala/fr/acinq/eclair/db/PaymentsDbSpec.scala | 8 ++++---- .../eclair/payment/MultiPartPaymentLifecycleSpec.scala | 6 +++--- .../fr/acinq/eclair/payment/PaymentInitiatorSpec.scala | 6 +++--- .../fr/acinq/eclair/payment/PaymentLifecycleSpec.scala | 4 ++-- .../acinq/eclair/payment/relay/NodeRelayerSpec.scala | 2 +- .../scala/fr/acinq/eclair/api/ApiServiceSpec.scala | 4 ++-- 16 files changed, 41 insertions(+), 41 deletions(-) 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 4af6b47752..4caa243a72 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 @@ -356,7 +356,7 @@ class PgAuditDb(implicit ds: DataSource) extends AuditDb with Logging { val result = statement.executeQuery() .foldLeft(Map.empty[UUID, PaymentSent]) { (sentByParentId, rs) => val parentId = UUID.fromString(rs.getString("parent_payment_id")) - val part = PaymentSent.PartialPayment( + val part = PaymentSent.PaymentPart( id = UUID.fromString(rs.getString("payment_id")), payment = PaymentEvent.OutgoingPayment( channelId = rs.getByteVector32FromHex("to_channel_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 7df7db8e5b..464d9daed4 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 @@ -330,7 +330,7 @@ class SqliteAuditDb(val sqlite: Connection) extends AuditDb with Logging { val result = statement.executeQuery() .foldLeft(Map.empty[UUID, PaymentSent]) { (sentByParentId, rs) => val parentId = UUID.fromString(rs.getString("parent_payment_id")) - val part = PaymentSent.PartialPayment( + val part = PaymentSent.PaymentPart( id = UUID.fromString(rs.getString("payment_id")), payment = PaymentEvent.OutgoingPayment( channelId = rs.getByteVector32("to_channel_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 9a387374a1..bea4040b96 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 @@ -383,10 +383,10 @@ object PaymentFailedSummarySerializer extends ConvertClassSerializer[PaymentFail )) // @formatter:on -private case class PartialPaymentJson(id: UUID, channelId: ByteVector32, nextNodeId: PublicKey, amountWithFees: MilliSatoshi, fees: MilliSatoshi, route: Option[Seq[Hop]], startedAt: TimestampMilli, settledAt: TimestampMilli) -private case class PaymentSentJson(id: UUID, paymentHash: ByteVector32, paymentPreimage: ByteVector32, recipientAmount: MilliSatoshi, recipientNodeId: PublicKey, parts: Seq[PartialPaymentJson], fees: MilliSatoshi, startedAt: TimestampMilli, settledAt: TimestampMilli) +private case class PaymentPartJson(id: UUID, channelId: ByteVector32, nextNodeId: PublicKey, amountWithFees: MilliSatoshi, fees: MilliSatoshi, route: Option[Seq[Hop]], startedAt: TimestampMilli, settledAt: TimestampMilli) +private case class PaymentSentJson(id: UUID, paymentHash: ByteVector32, paymentPreimage: ByteVector32, recipientAmount: MilliSatoshi, recipientNodeId: PublicKey, parts: Seq[PaymentPartJson], fees: MilliSatoshi, startedAt: TimestampMilli, settledAt: TimestampMilli) object PaymentSentSerializer extends ConvertClassSerializer[PaymentSent](p => { - val parts = p.parts.map(pp => PartialPaymentJson(pp.id, pp.channelId, pp.remoteNodeId, pp.amountWithFees, pp.feesPaid, pp.route, pp.startedAt, pp.settledAt)) + val parts = p.parts.map(pp => PaymentPartJson(pp.id, pp.channelId, pp.remoteNodeId, pp.amountWithFees, pp.feesPaid, pp.route, pp.startedAt, pp.settledAt)) PaymentSentJson(p.id, p.paymentHash, p.paymentPreimage, p.recipientAmount, p.recipientNodeId, parts, p.feesPaid, p.startedAt, p.settledAt) }) 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 2229772fd8..2315cf3140 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 @@ -79,7 +79,7 @@ object PaymentEvent { * @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, paymentPreimage: ByteVector32, recipientAmount: MilliSatoshi, recipientNodeId: PublicKey, parts: Seq[PaymentSent.PartialPayment], remainingAttribution_opt: Option[ByteVector], startedAt: TimestampMilli) extends PaymentEvent { +case class PaymentSent(id: UUID, paymentPreimage: ByteVector32, recipientAmount: MilliSatoshi, recipientNodeId: PublicKey, parts: Seq[PaymentSent.PaymentPart], 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 @@ -97,7 +97,7 @@ object PaymentSent { * @param route payment route used. * @param startedAt absolute time in milliseconds since UNIX epoch when the payment was started. */ - case class PartialPayment(id: UUID, payment: PaymentEvent.OutgoingPayment, feesPaid: MilliSatoshi, route: Option[Seq[Hop]], startedAt: TimestampMilli) { + case class PaymentPart(id: UUID, payment: PaymentEvent.OutgoingPayment, feesPaid: MilliSatoshi, route: Option[Seq[Hop]], startedAt: TimestampMilli) { require(route.isEmpty || route.get.nonEmpty, "route must be None or contain at least one hop") val channelId: ByteVector32 = payment.channelId val remoteNodeId: PublicKey = payment.remoteNodeId 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 8637e7185b..7952335af7 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,13 +178,13 @@ 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, paymentPreimage, p.recipientAmount, p.recipientNodeId, PaymentSent.PartialPayment(id, PaymentEvent.OutgoingPayment(fulfilledHtlc.channelId, downstreamNodeId, fulfilledHtlc.amountMsat, settledAt = TimestampMilli.now()), feesPaid, None, startedAt = p.createdAt) :: Nil, None, p.createdAt)) + nodeParams.db.payments.updateOutgoingPayment(PaymentSent(p.parentId, paymentPreimage, p.recipientAmount, p.recipientNodeId, PaymentSent.PaymentPart(id, PaymentEvent.OutgoingPayment(fulfilledHtlc.channelId, downstreamNodeId, fulfilledHtlc.amountMsat, settledAt = TimestampMilli.now()), feesPaid, None, startedAt = p.createdAt) :: 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, _, _, createdAt, _, _, OutgoingPaymentStatus.Succeeded(_, feesPaid, _, completedAt)) => - PaymentSent.PartialPayment(id, PaymentEvent.OutgoingPayment(ByteVector32.Zeroes, downstreamNodeId, amount, completedAt), feesPaid, None, createdAt) + PaymentSent.PaymentPart(id, PaymentEvent.OutgoingPayment(ByteVector32.Zeroes, downstreamNodeId, amount, completedAt), feesPaid, None, createdAt) } 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})") @@ -198,7 +198,7 @@ class PostRestartHtlcCleaner(nodeParams: NodeParams, register: ActorRef, initial val dummyNodeId = nodeParams.nodeId 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, PaymentEvent.OutgoingPayment(fulfilledHtlc.channelId, downstreamNodeId, fulfilledHtlc.amountMsat, settledAt = now), feesPaid, None, startedAt = now) :: Nil, None, startedAt = now)) + nodeParams.db.payments.updateOutgoingPayment(PaymentSent(id, paymentPreimage, dummyFinalAmount, dummyNodeId, PaymentSent.PaymentPart(id, PaymentEvent.OutgoingPayment(fulfilledHtlc.channelId, downstreamNodeId, fulfilledHtlc.amountMsat, settledAt = now), feesPaid, None, startedAt = 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. 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 f75a88231e..1937346aa9 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 @@ -23,7 +23,7 @@ import fr.acinq.bitcoin.scalacompat.ByteVector32 import fr.acinq.eclair.channel.{HtlcOverriddenByLocalCommit, HtlcsTimedoutDownstream, HtlcsWillTimeoutUpstream, Upstream} import fr.acinq.eclair.db.{OutgoingPayment, OutgoingPaymentStatus} import fr.acinq.eclair.payment.Monitoring.{Metrics, Tags} -import fr.acinq.eclair.payment.PaymentSent.PartialPayment +import fr.acinq.eclair.payment.PaymentSent.PaymentPart import fr.acinq.eclair.payment._ import fr.acinq.eclair.payment.send.PaymentInitiator.SendPaymentConfig import fr.acinq.eclair.payment.send.PaymentLifecycle.SendPaymentToRoute @@ -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@PaymentPart(_, _, _, Some(route), _) => route.head.fee(p.amountWithFees) }.sum } paymentSent.feesPaid + localFees @@ -369,7 +369,7 @@ object MultiPartPaymentLifecycle { * @param parts fulfilled child payments. * @param pending pending child payments (we are waiting for them to be fulfilled downstream). */ - case class PaymentSucceeded(request: SendMultiPartPayment, preimage: ByteVector32, parts: Seq[PartialPayment], pending: Set[UUID], remainingAttribution_opt: Option[ByteVector]) extends Data + case class PaymentSucceeded(request: SendMultiPartPayment, preimage: ByteVector32, parts: Seq[PaymentPart], pending: Set[UUID], remainingAttribution_opt: Option[ByteVector]) extends Data private def createRouteRequest(replyTo: ActorRef, nodeParams: NodeParams, routeParams: RouteParams, d: PaymentProgress, cfg: SendPaymentConfig): RouteRequest = { RouteRequest(replyTo.toTyped, nodeParams.nodeId, d.request.recipient, routeParams, d.ignore, allowMultiPart = true, d.pending.values.toSeq, Some(cfg.paymentContext)) 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 6fdb12d147..041c50c1fa 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 @@ -336,7 +336,7 @@ object PaymentInitiator { case _ => PaymentType.Standard } - def createPaymentSent(recipient: Recipient, preimage: ByteVector32, parts: Seq[PaymentSent.PartialPayment], remainingAttribution_opt: Option[ByteVector], startedAt: TimestampMilli): PaymentSent = { + def createPaymentSent(recipient: Recipient, preimage: ByteVector32, parts: Seq[PaymentSent.PaymentPart], 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 93dabee06b..c165a46dbb 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 @@ -26,7 +26,7 @@ import fr.acinq.eclair.crypto.{Sphinx, TransportHandler} import fr.acinq.eclair.db.{OutgoingPayment, OutgoingPaymentStatus} import fr.acinq.eclair.payment.Invoice.ExtraEdge import fr.acinq.eclair.payment.Monitoring.{Metrics, Tags} -import fr.acinq.eclair.payment.PaymentSent.PartialPayment +import fr.acinq.eclair.payment.PaymentSent.PaymentPart import fr.acinq.eclair.payment._ import fr.acinq.eclair.payment.send.PaymentInitiator.SendPaymentConfig import fr.acinq.eclair.payment.send.PaymentLifecycle._ @@ -114,7 +114,7 @@ class PaymentLifecycle(nodeParams: NodeParams, cfg: SendPaymentConfig, router: A case Event(RES_ADD_SETTLED(_, remoteNodeId, 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, PaymentEvent.OutgoingPayment(htlc.channelId, remoteNodeId, d.cmd.amount, settledAt = TimestampMilli.now()), d.cmd.amount - d.request.amount, Some(d.route.fullRoute), startedAt = d.sentAt) + val p = PaymentPart(id, PaymentEvent.OutgoingPayment(htlc.channelId, remoteNodeId, d.cmd.amount, settledAt = TimestampMilli.now()), d.cmd.amount - d.request.amount, Some(d.route.fullRoute), startedAt = d.sentAt) val remainingAttribution_opt = fulfill match { case HtlcResult.RemoteFulfill(updateFulfill) => updateFulfill.attribution_opt match { @@ -418,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@PaymentPart(_, _, _, Some(route), _) => route.head.fee(p.amountWithFees) }.sum } paymentSent.feesPaid + localFees 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 1f122d6aff..0a316101e8 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 @@ -26,7 +26,7 @@ import fr.acinq.eclair.channel._ import fr.acinq.eclair.crypto.Sphinx import fr.acinq.eclair.io.Peer import fr.acinq.eclair.payment.OutgoingPaymentPacket.{NodePayload, buildOnion} -import fr.acinq.eclair.payment.PaymentSent.PartialPayment +import fr.acinq.eclair.payment.PaymentSent.PaymentPart import fr.acinq.eclair.payment._ import fr.acinq.eclair.payment.send.TrampolinePayment.{buildOutgoingPayment, computeFees} import fr.acinq.eclair.reputation.Reputation @@ -59,7 +59,7 @@ object TrampolinePaymentLifecycle { } private case class TrampolinePeerNotFound(trampolineNodeId: PublicKey) extends Command private case class CouldntAddHtlc(failure: Throwable) extends Command - private case class HtlcSettled(result: HtlcResult, part: PartialPayment, holdTimes: Seq[Sphinx.HoldTime]) extends Command + private case class HtlcSettled(result: HtlcResult, part: PaymentPart, holdTimes: Seq[Sphinx.HoldTime]) extends Command private case class WrappedPeerChannels(channels: Seq[Peer.ChannelInfo]) extends Command // @formatter:on @@ -114,11 +114,11 @@ object TrampolinePaymentLifecycle { channelInfo.channel ! add val channelId = channelInfo.data.asInstanceOf[DATA_NORMAL].channelId val trampolineFees = computeFees(amount, attemptNumber) - val part = PartialPayment(cmd.paymentId, PaymentEvent.OutgoingPayment(channelId, cmd.trampolineNodeId, amount + trampolineFees, settledAt = TimestampMilli.now()), trampolineFees, None, startedAt = TimestampMilli.now()) // we will update settledAt below + val part = PaymentPart(cmd.paymentId, PaymentEvent.OutgoingPayment(channelId, cmd.trampolineNodeId, amount + trampolineFees, settledAt = TimestampMilli.now()), trampolineFees, None, startedAt = TimestampMilli.now()) // we will update settledAt below waitForSettlement(part, outgoing.onion.sharedSecrets, outgoing.trampolineOnion.sharedSecrets) } - def waitForSettlement(part: PartialPayment, outerOnionSecrets: Seq[Sphinx.SharedSecret], trampolineOnionSecrets: Seq[Sphinx.SharedSecret]): Behavior[PartHandler.Command] = { + def waitForSettlement(part: PaymentPart, outerOnionSecrets: Seq[Sphinx.SharedSecret], trampolineOnionSecrets: Seq[Sphinx.SharedSecret]): Behavior[PartHandler.Command] = { Behaviors.receiveMessagePartial { case WrappedAddHtlcResponse(response) => response match { case _: CommandSuccess[_] => @@ -213,7 +213,7 @@ class TrampolinePaymentLifecycle private(nodeParams: NodeParams, } } - private def waitForSettlement(remaining: Int, attemptNumber: Int, fulfilledParts: Seq[PartialPayment]): Behavior[Command] = { + private def waitForSettlement(remaining: Int, attemptNumber: Int, fulfilledParts: Seq[PaymentPart]): Behavior[Command] = { Behaviors.receiveMessagePartial { case CouldntAddHtlc(failure) => context.log.warn("HTLC could not be sent: {}", failure.getMessage) 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 19e84eade1..8f2b4ae9b9 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 @@ -66,7 +66,7 @@ class AuditDbSpec extends AnyFunSuite { val dummyRemoteNodeId = PrivateKey(ByteVector32.One).publicKey val now = TimestampMilli.now() - val e1 = PaymentSent(ZERO_UUID, randomBytes32(), 40000 msat, randomKey().publicKey, PaymentSent.PartialPayment(ZERO_UUID, PaymentEvent.OutgoingPayment(randomBytes32(), dummyRemoteNodeId, 42000 msat, now), 1000 msat, None, now) :: Nil, None, now) + val e1 = PaymentSent(ZERO_UUID, randomBytes32(), 40000 msat, randomKey().publicKey, PaymentSent.PaymentPart(ZERO_UUID, PaymentEvent.OutgoingPayment(randomBytes32(), dummyRemoteNodeId, 42000 msat, now), 1000 msat, None, now) :: Nil, None, now) val pp2a = PaymentEvent.IncomingPayment(randomBytes32(), dummyRemoteNodeId, 42000 msat, now) val pp2b = PaymentEvent.IncomingPayment(randomBytes32(), dummyRemoteNodeId, 42100 msat, now) val e2 = PaymentReceived(randomBytes32(), pp2a :: pp2b :: Nil) @@ -74,10 +74,10 @@ 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(), PaymentEvent.OutgoingPayment(randomBytes32(), dummyRemoteNodeId, 42000 msat, 0 unixms), 1000 msat, None, startedAt = 0 unixms) - val pp5b = PaymentSent.PartialPayment(UUID.randomUUID(), PaymentEvent.OutgoingPayment(randomBytes32(), dummyRemoteNodeId, 42100 msat, 1 unixms), 900 msat, None, startedAt = 1 unixms) + val pp5a = PaymentSent.PaymentPart(UUID.randomUUID(), PaymentEvent.OutgoingPayment(randomBytes32(), dummyRemoteNodeId, 42000 msat, 0 unixms), 1000 msat, None, startedAt = 0 unixms) + val pp5b = PaymentSent.PaymentPart(UUID.randomUUID(), PaymentEvent.OutgoingPayment(randomBytes32(), dummyRemoteNodeId, 42100 msat, 1 unixms), 900 msat, None, startedAt = 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(), PaymentEvent.OutgoingPayment(randomBytes32(), dummyRemoteNodeId, 42000 msat, settledAt = now + 10.minutes), 1000 msat, None, startedAt = now + 10.minutes) + val pp6 = PaymentSent.PaymentPart(UUID.randomUUID(), PaymentEvent.OutgoingPayment(randomBytes32(), dummyRemoteNodeId, 42000 msat, settledAt = now + 10.minutes), 1000 msat, None, startedAt = 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(), 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 55092dfcba..9700767d92 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,7 +200,7 @@ 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, preimage1, ps5.amount, ps5.recipientNodeId, Seq(PaymentSent.PartialPayment(ps5.id, PaymentEvent.OutgoingPayment(randomBytes32(), randomKey().publicKey, ps5.amount, 1180 unixms), 42 msat, None, 1000 unixms)), None, 900 unixms)) + db.updateOutgoingPayment(PaymentSent(ps5.parentId, preimage1, ps5.amount, ps5.recipientNodeId, Seq(PaymentSent.PaymentPart(ps5.id, PaymentEvent.OutgoingPayment(randomBytes32(), randomKey().publicKey, ps5.amount, 1180 unixms), 42 msat, None, 1000 unixms)), None, 900 unixms)) db.addOutgoingPayment(ps6.copy(status = OutgoingPaymentStatus.Pending)) db.updateOutgoingPayment(PaymentFailed(ps6.id, ps6.paymentHash, Nil, 1100 unixms, 1300 unixms)) @@ -772,11 +772,11 @@ class PaymentsDbSpec extends AnyFunSuite { assert(db.getOutgoingPayment(s4.id).contains(ss4)) // can't update again once it's in a final state - assertThrows[IllegalArgumentException](db.updateOutgoingPayment(PaymentSent(parentId, preimage1, s3.recipientAmount, s3.recipientNodeId, Seq(PaymentSent.PartialPayment(s3.id, PaymentEvent.OutgoingPayment(randomBytes32(), randomKey().publicKey, s3.amount, settledAt = 500 unixms), 42 msat, None, startedAt = 100 unixms)), None, startedAt = 100 unixms))) + assertThrows[IllegalArgumentException](db.updateOutgoingPayment(PaymentSent(parentId, preimage1, s3.recipientAmount, s3.recipientNodeId, Seq(PaymentSent.PaymentPart(s3.id, PaymentEvent.OutgoingPayment(randomBytes32(), randomKey().publicKey, s3.amount, settledAt = 500 unixms), 42 msat, None, startedAt = 100 unixms)), None, startedAt = 100 unixms))) val paymentSent = PaymentSent(parentId, preimage1, 600 msat, carol, Seq( - PaymentSent.PartialPayment(s1.id, PaymentEvent.OutgoingPayment(randomBytes32(), randomKey().publicKey, s1.amount, settledAt = 400 unixms), 15 msat, None, startedAt = 200 unixms), - PaymentSent.PartialPayment(s2.id, PaymentEvent.OutgoingPayment(randomBytes32(), randomKey().publicKey, s2.amount, settledAt = 410 unixms), 20 msat, Some(Seq(hop_ab, hop_bc)), startedAt = 210 unixms) + PaymentSent.PaymentPart(s1.id, PaymentEvent.OutgoingPayment(randomBytes32(), randomKey().publicKey, s1.amount, settledAt = 400 unixms), 15 msat, None, startedAt = 200 unixms), + PaymentSent.PaymentPart(s2.id, PaymentEvent.OutgoingPayment(randomBytes32(), randomKey().publicKey, s2.amount, settledAt = 410 unixms), 20 msat, Some(Seq(hop_ab, hop_bc)), startedAt = 210 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)) 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 0cf1c6c112..f9023a2c77 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 @@ -580,7 +580,7 @@ class MultiPartPaymentLifecycleSpec extends TestKitBaseClass with FixtureAnyFunS awaitCond(payFsm.stateName == PAYMENT_ABORTED) sender.watch(payFsm) - childPayFsm.send(payFsm, PaymentSent(cfg.id, paymentPreimage, finalAmount, e, Seq(PaymentSent.PartialPayment(successId, PaymentEvent.OutgoingPayment(randomBytes32(), randomKey().publicKey, successRoute.amount, 250 unixms), successRoute.channelFee(false), Some(successRoute.fullRoute), 100 unixms)), None, 75 unixms)) + childPayFsm.send(payFsm, PaymentSent(cfg.id, paymentPreimage, finalAmount, e, Seq(PaymentSent.PaymentPart(successId, PaymentEvent.OutgoingPayment(randomBytes32(), randomKey().publicKey, successRoute.amount, 250 unixms), successRoute.channelFee(false), Some(successRoute.fullRoute), 100 unixms)), None, 75 unixms)) sender.expectMsg(PreimageReceived(paymentHash, paymentPreimage, None)) val result = sender.expectMsgType[PaymentSent] assert(result.id == cfg.id) @@ -608,7 +608,7 @@ 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, paymentPreimage, finalAmount, e, Seq(PaymentSent.PartialPayment(childId, PaymentEvent.OutgoingPayment(randomBytes32(), randomKey().publicKey, route.amount, 250 unixms), route.channelFee(false), Some(route.fullRoute), 100 unixms)), None, 75 unixms)) + childPayFsm.send(payFsm, PaymentSent(cfg.id, paymentPreimage, finalAmount, e, Seq(PaymentSent.PaymentPart(childId, PaymentEvent.OutgoingPayment(randomBytes32(), randomKey().publicKey, route.amount, 250 unixms), route.channelFee(false), Some(route.fullRoute), 100 unixms)), None, 75 unixms)) sender.expectMsg(PreimageReceived(paymentHash, paymentPreimage, None)) awaitCond(payFsm.stateName == PAYMENT_SUCCEEDED) @@ -632,7 +632,7 @@ class MultiPartPaymentLifecycleSpec extends TestKitBaseClass with FixtureAnyFunS assert(pending.size == childCount) val partialPayments = pending.map { - case (childId, route) => PaymentSent.PartialPayment(childId, PaymentEvent.OutgoingPayment(randomBytes32(), randomKey().publicKey, route.amount + route.channelFee(false) + route.blindedFee, 250 unixms), route.channelFee(false) + route.blindedFee, Some(route.fullRoute), 100 unixms) + case (childId, route) => PaymentSent.PaymentPart(childId, PaymentEvent.OutgoingPayment(randomBytes32(), randomKey().publicKey, route.amount + route.channelFee(false) + route.blindedFee, 250 unixms), route.channelFee(false) + route.blindedFee, Some(route.fullRoute), 100 unixms) } partialPayments.foreach(pp => childPayFsm.send(payFsm, PaymentSent(cfg.id, paymentPreimage, finalAmount, e, Seq(pp), None, 100 unixms))) sender.expectMsg(PreimageReceived(paymentHash, paymentPreimage, None)) 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 6c72c0dee6..b0c7dd4939 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 @@ -28,7 +28,7 @@ import fr.acinq.eclair.channel.Upstream import fr.acinq.eclair.channel.fsm.Channel import fr.acinq.eclair.payment.Bolt11Invoice.ExtraHop import fr.acinq.eclair.payment.PaymentPacketSpec._ -import fr.acinq.eclair.payment.PaymentSent.PartialPayment +import fr.acinq.eclair.payment.PaymentSent.PaymentPart import fr.acinq.eclair.payment.send.BlindedPathsResolver.{FullBlindedRoute, ResolvedPath} import fr.acinq.eclair.payment.send.MultiPartPaymentLifecycle.SendMultiPartPayment import fr.acinq.eclair.payment.send.PaymentError.UnsupportedFeatures @@ -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, paymentPreimage, finalAmount, priv_c.publicKey, Seq(PartialPayment(UUID.randomUUID(), PaymentEvent.OutgoingPayment(randomBytes32(), randomKey().publicKey, finalAmount, 200 unixms), 0 msat, None, 100 unixms)), None, 80 unixms) + val ps = PaymentSent(id, paymentPreimage, finalAmount, priv_c.publicKey, Seq(PaymentPart(UUID.randomUUID(), PaymentEvent.OutgoingPayment(randomBytes32(), randomKey().publicKey, finalAmount, 200 unixms), 0 msat, None, 100 unixms)), None, 80 unixms) payFsm.send(initiator, ps) sender.expectMsg(ps) 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, paymentPreimage, finalAmount, invoice.nodeId, Seq(PartialPayment(UUID.randomUUID(), PaymentEvent.OutgoingPayment(randomBytes32(), randomKey().publicKey, finalAmount, 200 unixms), 0 msat, None, 100 unixms)), None, 100 unixms) + val ps = PaymentSent(id, paymentPreimage, finalAmount, invoice.nodeId, Seq(PaymentPart(UUID.randomUUID(), PaymentEvent.OutgoingPayment(randomBytes32(), randomKey().publicKey, finalAmount, 200 unixms), 0 msat, None, 100 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 0a00f9bc23..fab1526e66 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 @@ -34,7 +34,7 @@ import fr.acinq.eclair.crypto.{Sphinx, SphinxSpec} import fr.acinq.eclair.db.{OutgoingPayment, OutgoingPaymentStatus, PaymentType} import fr.acinq.eclair.io.Peer.PeerRoutingMessage import fr.acinq.eclair.payment.Invoice.ExtraEdge -import fr.acinq.eclair.payment.PaymentSent.PartialPayment +import fr.acinq.eclair.payment.PaymentSent.PaymentPart import fr.acinq.eclair.payment.relay.Relayer.RelayFees import fr.acinq.eclair.payment.send.PaymentInitiator.SendPaymentConfig import fr.acinq.eclair.payment.send.PaymentLifecycle._ @@ -847,7 +847,7 @@ class PaymentLifecycleSpec extends BaseRouterSpec { sender.send(paymentFSM, addCompleted(HtlcResult.OnChainFulfill(defaultPaymentPreimage))) val paymentOK = sender.expectMsgType[PaymentSent] - val PaymentSent(_, paymentOK.paymentPreimage, finalAmount, _, PartialPayment(_, part, fee, _, _) :: Nil, _, _) = eventListener.expectMsgType[PaymentSent] + val PaymentSent(_, paymentOK.paymentPreimage, finalAmount, _, PaymentPart(_, part, fee, _, _) :: Nil, _, _) = eventListener.expectMsgType[PaymentSent] assert(part.amount == request.amount) assert(finalAmount == defaultAmountMsat) 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 f8387fae4d..4368b0f161 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 @@ -1213,7 +1213,7 @@ object NodeRelayerSpec { (paymentPackets.map(_.outerPayload.expiry).min - nodeParams.relayParams.asyncPaymentsParams.cancelSafetyBeforeTimeout).blockHeight def createSuccessEvent(): PaymentSent = - PaymentSent(relayId, paymentPreimage, outgoingAmount, outgoingNodeId, Seq(PaymentSent.PartialPayment(UUID.randomUUID(), PaymentEvent.OutgoingPayment(randomBytes32(), randomKey().publicKey, outgoingAmount, 50 unixms), 10 msat, None, 0 unixms)), None, 0 unixms) + PaymentSent(relayId, paymentPreimage, outgoingAmount, outgoingNodeId, Seq(PaymentSent.PaymentPart(UUID.randomUUID(), PaymentEvent.OutgoingPayment(randomBytes32(), randomKey().publicKey, outgoingAmount, 50 unixms), 10 msat, None, 0 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 34311c980f..71529e795c 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 @@ -624,7 +624,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.One, 25 msat, aliceNodeId, Seq(PaymentSent.PartialPayment(uuid, PaymentEvent.OutgoingPayment(ByteVector32.Zeroes, nextNodeId, 28 msat, TimestampMilli(1553784337711L)), 3 msat, None, TimestampMilli(1553784337650L))), None, TimestampMilli(1553784337120L)) + val paymentSent = PaymentSent(uuid, ByteVector32.One, 25 msat, aliceNodeId, Seq(PaymentSent.PaymentPart(uuid, PaymentEvent.OutgoingPayment(ByteVector32.Zeroes, nextNodeId, 28 msat, TimestampMilli(1553784337711L)), 3 msat, None, TimestampMilli(1553784337650L))), 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)) ~> @@ -1130,7 +1130,7 @@ class ApiServiceSpec extends AnyFunSuite with ScalatestRouteTest with IdiomaticM system.eventStream.publish(pf) wsClient.expectMessage(expectedSerializedPf) - val ps = PaymentSent(fixedUUID, ByteVector32.One, 25 msat, aliceNodeId, Seq(PaymentSent.PartialPayment(fixedUUID, PaymentEvent.OutgoingPayment(ByteVector32.Zeroes, nextNodeId, 28 msat, settledAt = TimestampMilli(1553784337711L)), 3 msat, None, startedAt = TimestampMilli(1553784337539L))), None, startedAt = TimestampMilli(1553784337073L)) + val ps = PaymentSent(fixedUUID, ByteVector32.One, 25 msat, aliceNodeId, Seq(PaymentSent.PaymentPart(fixedUUID, PaymentEvent.OutgoingPayment(ByteVector32.Zeroes, nextNodeId, 28 msat, settledAt = TimestampMilli(1553784337711L)), 3 msat, None, startedAt = TimestampMilli(1553784337539L))), 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","channelId":"0000000000000000000000000000000000000000000000000000000000000000","nextNodeId":"030bb6a5e0c6b203c7e2180fb78c7ba4bdce46126761d8201b91ddac089cdecc87","amountWithFees":28,"fees":3,"startedAt":{"iso":"2019-03-28T14:45:37.539Z","unix":1553784337},"settledAt":{"iso":"2019-03-28T14:45:37.711Z","unix":1553784337}}],"fees":3,"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)