Skip to content

Commit 297f7f0

Browse files
Prepare attribution data for trampoline payments (#3109)
The trampoline node must unwrap the attribution data with its shared secrets and use what's remaining as the attribution data from the next trampoline node. This commit does not yet relay the attribution data but makes it available.
1 parent 8e3e206 commit 297f7f0

20 files changed

+107
-93
lines changed

eclair-core/src/main/scala/fr/acinq/eclair/crypto/Sphinx.scala

Lines changed: 11 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -280,9 +280,10 @@ object Sphinx extends Logging {
280280
/**
281281
* The downstream failure could not be decrypted.
282282
*
283-
* @param unwrapped encrypted failure packet after unwrapping using our shared secrets.
283+
* @param unwrapped encrypted failure packet after unwrapping using our shared secrets.
284+
* @param attribution_opt attribution data after unwrapping using our shared secrets
284285
*/
285-
case class CannotDecryptFailurePacket(unwrapped: ByteVector)
286+
case class CannotDecryptFailurePacket(unwrapped: ByteVector, attribution_opt: Option[ByteVector])
286287

287288
case class HoldTime(duration: FiniteDuration, remoteNodeId: PublicKey)
288289

@@ -336,7 +337,7 @@ object Sphinx extends Logging {
336337
*/
337338
def decrypt(packet: ByteVector, attribution_opt: Option[ByteVector], sharedSecrets: Seq[SharedSecret], hopIndex: Int = 0): HtlcFailure = {
338339
sharedSecrets match {
339-
case Nil => HtlcFailure(Nil, Left(CannotDecryptFailurePacket(packet)))
340+
case Nil => HtlcFailure(Nil, Left(CannotDecryptFailurePacket(packet, attribution_opt)))
340341
case ss :: tail =>
341342
val packet1 = wrap(packet, ss.secret)
342343
val attribution1_opt = attribution_opt.flatMap(Attribution.unwrap(_, packet1, ss.secret, hopIndex))
@@ -432,17 +433,20 @@ object Sphinx extends Logging {
432433
}
433434
}
434435

436+
case class UnwrappedAttribution(holdTimes: List[HoldTime], remaining_opt: Option[ByteVector])
437+
435438
/**
436439
* Decrypt the hold times from the attribution data of a fulfilled HTLC
437440
*/
438-
def fulfillHoldTimes(attribution: ByteVector, sharedSecrets: Seq[SharedSecret], hopIndex: Int = 0): List[HoldTime] = {
441+
def fulfillHoldTimes(attribution: ByteVector, sharedSecrets: Seq[SharedSecret], hopIndex: Int = 0): UnwrappedAttribution = {
439442
sharedSecrets match {
440-
case Nil => Nil
443+
case Nil => UnwrappedAttribution(Nil, Some(attribution))
441444
case ss :: tail =>
442445
unwrap(attribution, ByteVector.empty, ss.secret, hopIndex) match {
443446
case Some((holdTime, nextAttribution)) =>
444-
HoldTime(holdTime, ss.remoteNodeId) :: fulfillHoldTimes(nextAttribution, tail, hopIndex + 1)
445-
case None => Nil
447+
val UnwrappedAttribution(holdTimes, remaining_opt) = fulfillHoldTimes(nextAttribution, tail, hopIndex + 1)
448+
UnwrappedAttribution(HoldTime(holdTime, ss.remoteNodeId) :: holdTimes, remaining_opt)
449+
case None => UnwrappedAttribution(Nil, None)
446450
}
447451
}
448452
}

eclair-core/src/main/scala/fr/acinq/eclair/db/pg/PgAuditDb.scala

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -391,7 +391,8 @@ class PgAuditDb(implicit ds: DataSource) extends AuditDb with Logging {
391391
rs.getByteVector32FromHex("payment_preimage"),
392392
MilliSatoshi(rs.getLong("recipient_amount_msat")),
393393
PublicKey(rs.getByteVectorFromHex("recipient_node_id")),
394-
Seq(part))
394+
Seq(part),
395+
None)
395396
}
396397
sentByParentId + (parentId -> sent)
397398
}.values.toSeq.sortBy(_.timestamp)

eclair-core/src/main/scala/fr/acinq/eclair/db/sqlite/SqliteAuditDb.scala

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -363,7 +363,8 @@ class SqliteAuditDb(val sqlite: Connection) extends AuditDb with Logging {
363363
rs.getByteVector32("payment_preimage"),
364364
MilliSatoshi(rs.getLong("recipient_amount_msat")),
365365
PublicKey(rs.getByteVector("recipient_node_id")),
366-
Seq(part))
366+
Seq(part),
367+
None)
367368
}
368369
sentByParentId + (parentId -> sent)
369370
}.values.toSeq.sortBy(_.timestamp)

eclair-core/src/main/scala/fr/acinq/eclair/payment/PaymentEvents.scala

Lines changed: 10 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -46,15 +46,16 @@ sealed trait PaymentEvent {
4646
/**
4747
* A payment was successfully sent and fulfilled.
4848
*
49-
* @param id id of the whole payment attempt (if using multi-part, there will be multiple parts, each with
50-
* a different id).
51-
* @param paymentHash payment hash.
52-
* @param paymentPreimage payment preimage (proof of payment).
53-
* @param recipientAmount amount that has been received by the final recipient.
54-
* @param recipientNodeId id of the final recipient.
55-
* @param parts child payments (actual outgoing HTLCs).
49+
* @param id id of the whole payment attempt (if using multi-part, there will be multiple parts,
50+
* each with a different id).
51+
* @param paymentHash payment hash.
52+
* @param paymentPreimage payment preimage (proof of payment).
53+
* @param recipientAmount amount that has been received by the final recipient.
54+
* @param recipientNodeId id of the final recipient.
55+
* @param parts child payments (actual outgoing HTLCs).
56+
* @param remainingAttribution_opt for relayed trampoline payments, the attribution data that needs to be sent upstream
5657
*/
57-
case class PaymentSent(id: UUID, paymentHash: ByteVector32, paymentPreimage: ByteVector32, recipientAmount: MilliSatoshi, recipientNodeId: PublicKey, parts: Seq[PaymentSent.PartialPayment]) extends PaymentEvent {
58+
case class PaymentSent(id: UUID, paymentHash: ByteVector32, paymentPreimage: ByteVector32, recipientAmount: MilliSatoshi, recipientNodeId: PublicKey, parts: Seq[PaymentSent.PartialPayment], remainingAttribution_opt: Option[ByteVector]) extends PaymentEvent {
5859
require(parts.nonEmpty, "must have at least one payment part")
5960
val amountWithFees: MilliSatoshi = parts.map(_.amountWithFees).sum
6061
val feesPaid: MilliSatoshi = amountWithFees - recipientAmount // overall fees for this payment
@@ -151,7 +152,7 @@ case class LocalFailure(amount: MilliSatoshi, route: Seq[Hop], t: Throwable) ext
151152
case class RemoteFailure(amount: MilliSatoshi, route: Seq[Hop], e: Sphinx.DecryptedFailurePacket) extends PaymentFailure
152153

153154
/** A remote node failed the payment but we couldn't decrypt the failure (e.g. a malicious node tampered with the message). */
154-
case class UnreadableRemoteFailure(amount: MilliSatoshi, route: Seq[Hop], failurePacket: ByteVector, holdTimes: Seq[HoldTime]) extends PaymentFailure
155+
case class UnreadableRemoteFailure(amount: MilliSatoshi, route: Seq[Hop], e: Sphinx.CannotDecryptFailurePacket, holdTimes: Seq[HoldTime]) extends PaymentFailure
155156

156157
object PaymentFailure {
157158

eclair-core/src/main/scala/fr/acinq/eclair/payment/relay/NodeRelay.scala

Lines changed: 6 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -42,6 +42,7 @@ import fr.acinq.eclair.router.{BalanceTooLow, RouteNotFound}
4242
import fr.acinq.eclair.wire.protocol.PaymentOnion.IntermediatePayload
4343
import fr.acinq.eclair.wire.protocol._
4444
import fr.acinq.eclair.{Alias, CltvExpiry, CltvExpiryDelta, EncodedNodeId, Features, InitFeature, Logs, MilliSatoshi, MilliSatoshiLong, NodeParams, TimestampMilli, UInt64, nodeFee, randomBytes32}
45+
import scodec.bits.ByteVector
4546

4647
import java.util.UUID
4748
import java.util.concurrent.TimeUnit
@@ -374,11 +375,11 @@ class NodeRelay private(nodeParams: NodeParams,
374375
Behaviors.receiveMessagePartial {
375376
rejectExtraHtlcPartialFunction orElse {
376377
// this is the fulfill that arrives from downstream channels
377-
case WrappedPreimageReceived(PreimageReceived(_, paymentPreimage)) =>
378+
case WrappedPreimageReceived(PreimageReceived(_, paymentPreimage, attribution_opt)) =>
378379
if (!fulfilledUpstream) {
379380
// We want to fulfill upstream as soon as we receive the preimage (even if not all HTLCs have fulfilled downstream).
380381
context.log.debug("got preimage from downstream")
381-
fulfillPayment(upstream, paymentPreimage)
382+
fulfillPayment(upstream, paymentPreimage, attribution_opt)
382383
sending(upstream, recipient, walletNodeId_opt, recipientFeatures_opt, nextPayload, startedAt, fulfilledUpstream = true)
383384
} else {
384385
// we don't want to fulfill multiple times
@@ -491,16 +492,15 @@ class NodeRelay private(nodeParams: NodeParams,
491492
upstream.received.foreach(r => rejectHtlc(r.add.id, r.add.channelId, upstream.amountIn, r.receivedAt, failure))
492493
}
493494

494-
private def fulfillPayment(upstream: Upstream.Hot.Trampoline, paymentPreimage: ByteVector32): Unit = upstream.received.foreach(r => {
495-
// TODO: process downstream attribution data
496-
val cmd = CMD_FULFILL_HTLC(r.add.id, paymentPreimage, None, Some(r.receivedAt), commit = true)
495+
private def fulfillPayment(upstream: Upstream.Hot.Trampoline, paymentPreimage: ByteVector32, downstreamAttribution_opt: Option[ByteVector]): Unit = upstream.received.foreach(r => {
496+
val cmd = CMD_FULFILL_HTLC(r.add.id, paymentPreimage, downstreamAttribution_opt, Some(r.receivedAt), commit = true)
497497
PendingCommandsDb.safeSend(register, nodeParams.db.pendingCommands, r.add.channelId, cmd)
498498
})
499499

500500
private def success(upstream: Upstream.Hot.Trampoline, fulfilledUpstream: Boolean, paymentSent: PaymentSent): Unit = {
501501
// We may have already fulfilled upstream, but we can now emit an accurate relayed event and clean-up resources.
502502
if (!fulfilledUpstream) {
503-
fulfillPayment(upstream, paymentSent.paymentPreimage)
503+
fulfillPayment(upstream, paymentSent.paymentPreimage, paymentSent.remainingAttribution_opt)
504504
}
505505
val incoming = upstream.received.map(r => PaymentRelayed.IncomingPart(r.add.amountMsat, r.add.channelId, r.receivedAt))
506506
val outgoing = paymentSent.parts.map(part => PaymentRelayed.OutgoingPart(part.amountWithFees, part.toChannelId, part.timestamp))

eclair-core/src/main/scala/fr/acinq/eclair/payment/relay/OnTheFlyFunding.scala

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -108,7 +108,7 @@ object OnTheFlyFunding {
108108
// In the trampoline case, we currently ignore downstream failures: we should add dedicated failures to
109109
// the BOLTs to better handle those cases.
110110
Sphinx.FailurePacket.decrypt(f.packet, f.attribution_opt, onionSharedSecrets).failure match {
111-
case Left(Sphinx.CannotDecryptFailurePacket(_)) =>
111+
case Left(Sphinx.CannotDecryptFailurePacket(_, _)) =>
112112
log.warning("couldn't decrypt downstream on-the-fly funding failure")
113113
case Right(f) =>
114114
log.warning("downstream on-the-fly funding failure: {}", f.failureMessage.message)

eclair-core/src/main/scala/fr/acinq/eclair/payment/relay/PostRestartHtlcCleaner.scala

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -177,15 +177,15 @@ class PostRestartHtlcCleaner(nodeParams: NodeParams, register: ActorRef, initial
177177
val feesPaid = 0.msat // fees are unknown since we lost the reference to the payment
178178
nodeParams.db.payments.getOutgoingPayment(id) match {
179179
case Some(p) =>
180-
nodeParams.db.payments.updateOutgoingPayment(PaymentSent(p.parentId, fulfilledHtlc.paymentHash, paymentPreimage, p.recipientAmount, p.recipientNodeId, PaymentSent.PartialPayment(id, fulfilledHtlc.amountMsat, feesPaid, fulfilledHtlc.channelId, None) :: Nil))
180+
nodeParams.db.payments.updateOutgoingPayment(PaymentSent(p.parentId, fulfilledHtlc.paymentHash, paymentPreimage, p.recipientAmount, p.recipientNodeId, PaymentSent.PartialPayment(id, fulfilledHtlc.amountMsat, feesPaid, fulfilledHtlc.channelId, None) :: Nil, None))
181181
// If all downstream HTLCs are now resolved, we can emit the payment event.
182182
val payments = nodeParams.db.payments.listOutgoingPayments(p.parentId)
183183
if (!payments.exists(p => p.status == OutgoingPaymentStatus.Pending)) {
184184
val succeeded = payments.collect {
185185
case OutgoingPayment(id, _, _, _, _, amount, _, _, _, _, _, OutgoingPaymentStatus.Succeeded(_, feesPaid, _, completedAt)) =>
186186
PaymentSent.PartialPayment(id, amount, feesPaid, ByteVector32.Zeroes, None, completedAt)
187187
}
188-
val sent = PaymentSent(p.parentId, fulfilledHtlc.paymentHash, paymentPreimage, p.recipientAmount, p.recipientNodeId, succeeded)
188+
val sent = PaymentSent(p.parentId, fulfilledHtlc.paymentHash, paymentPreimage, p.recipientAmount, p.recipientNodeId, succeeded, None)
189189
log.info(s"payment id=${sent.id} paymentHash=${sent.paymentHash} successfully sent (amount=${sent.recipientAmount})")
190190
context.system.eventStream.publish(sent)
191191
}
@@ -196,7 +196,7 @@ class PostRestartHtlcCleaner(nodeParams: NodeParams, register: ActorRef, initial
196196
val dummyFinalAmount = fulfilledHtlc.amountMsat
197197
val dummyNodeId = nodeParams.nodeId
198198
nodeParams.db.payments.addOutgoingPayment(OutgoingPayment(id, id, None, fulfilledHtlc.paymentHash, PaymentType.Standard, fulfilledHtlc.amountMsat, dummyFinalAmount, dummyNodeId, TimestampMilli.now(), None, None, OutgoingPaymentStatus.Pending))
199-
nodeParams.db.payments.updateOutgoingPayment(PaymentSent(id, fulfilledHtlc.paymentHash, paymentPreimage, dummyFinalAmount, dummyNodeId, PaymentSent.PartialPayment(id, fulfilledHtlc.amountMsat, feesPaid, fulfilledHtlc.channelId, None) :: Nil))
199+
nodeParams.db.payments.updateOutgoingPayment(PaymentSent(id, fulfilledHtlc.paymentHash, paymentPreimage, dummyFinalAmount, dummyNodeId, PaymentSent.PartialPayment(id, fulfilledHtlc.amountMsat, feesPaid, fulfilledHtlc.channelId, None) :: Nil, None))
200200
}
201201
// There can never be more than one pending downstream HTLC for a given local origin (a multi-part payment is
202202
// instead spread across multiple local origins) so we can now forget this origin.

eclair-core/src/main/scala/fr/acinq/eclair/payment/send/MultiPartPaymentLifecycle.scala

Lines changed: 10 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -29,6 +29,7 @@ import fr.acinq.eclair.payment.send.PaymentInitiator.SendPaymentConfig
2929
import fr.acinq.eclair.payment.send.PaymentLifecycle.SendPaymentToRoute
3030
import fr.acinq.eclair.router.Router._
3131
import fr.acinq.eclair.{FSMDiagnosticActorLogging, Logs, MilliSatoshiLong, NodeParams, TimestampMilli}
32+
import scodec.bits.ByteVector
3233

3334
import java.util.UUID
3435
import java.util.concurrent.TimeUnit
@@ -118,7 +119,7 @@ class MultiPartPaymentLifecycle(nodeParams: NodeParams, cfg: SendPaymentConfig,
118119
case Event(ps: PaymentSent, d: PaymentProgress) =>
119120
require(ps.parts.length == 1, "child payment must contain only one part")
120121
// As soon as we get the preimage we can consider that the whole payment succeeded (we have a proof of payment).
121-
gotoSucceededOrStop(PaymentSucceeded(d.request, ps.paymentPreimage, ps.parts, d.pending.keySet - ps.parts.head.id))
122+
gotoSucceededOrStop(PaymentSucceeded(d.request, ps.paymentPreimage, ps.parts, d.pending.keySet - ps.parts.head.id, ps.remainingAttribution_opt))
122123
}
123124

124125
when(PAYMENT_IN_PROGRESS) {
@@ -144,7 +145,7 @@ class MultiPartPaymentLifecycle(nodeParams: NodeParams, cfg: SendPaymentConfig,
144145
require(ps.parts.length == 1, "child payment must contain only one part")
145146
// As soon as we get the preimage we can consider that the whole payment succeeded (we have a proof of payment).
146147
Metrics.PaymentAttempt.withTag(Tags.MultiPart, value = true).record(d.request.maxAttempts - d.remainingAttempts)
147-
gotoSucceededOrStop(PaymentSucceeded(d.request, ps.paymentPreimage, ps.parts, d.pending.keySet - ps.parts.head.id))
148+
gotoSucceededOrStop(PaymentSucceeded(d.request, ps.paymentPreimage, ps.parts, d.pending.keySet - ps.parts.head.id, ps.remainingAttribution_opt))
148149
}
149150

150151
when(PAYMENT_ABORTED) {
@@ -162,7 +163,7 @@ class MultiPartPaymentLifecycle(nodeParams: NodeParams, cfg: SendPaymentConfig,
162163
case Event(ps: PaymentSent, d: PaymentAborted) =>
163164
require(ps.parts.length == 1, "child payment must contain only one part")
164165
log.warning(s"payment recipient fulfilled incomplete multi-part payment (id=${ps.parts.head.id})")
165-
gotoSucceededOrStop(PaymentSucceeded(d.request, ps.paymentPreimage, ps.parts, d.pending - ps.parts.head.id))
166+
gotoSucceededOrStop(PaymentSucceeded(d.request, ps.paymentPreimage, ps.parts, d.pending - ps.parts.head.id, ps.remainingAttribution_opt))
166167

167168
case Event(_: RouteResponse, _) => stay()
168169
case Event(_: PaymentRouteNotFound, _) => stay()
@@ -174,7 +175,7 @@ class MultiPartPaymentLifecycle(nodeParams: NodeParams, cfg: SendPaymentConfig,
174175
val parts = d.parts ++ ps.parts
175176
val pending = d.pending - ps.parts.head.id
176177
if (pending.isEmpty) {
177-
myStop(d.request, Right(cfg.createPaymentSent(d.request.recipient, d.preimage, parts)))
178+
myStop(d.request, Right(cfg.createPaymentSent(d.request.recipient, d.preimage, parts, d.remainingAttribution_opt)))
178179
} else {
179180
stay() using d.copy(parts = parts, pending = pending)
180181
}
@@ -185,7 +186,7 @@ class MultiPartPaymentLifecycle(nodeParams: NodeParams, cfg: SendPaymentConfig,
185186
log.warning(s"payment succeeded but partial payment failed (id=${pf.id})")
186187
val pending = d.pending - pf.id
187188
if (pending.isEmpty) {
188-
myStop(d.request, Right(cfg.createPaymentSent(d.request.recipient, d.preimage, d.parts)))
189+
myStop(d.request, Right(cfg.createPaymentSent(d.request.recipient, d.preimage, d.parts, d.remainingAttribution_opt)))
189190
} else {
190191
stay() using d.copy(pending = pending)
191192
}
@@ -212,10 +213,10 @@ class MultiPartPaymentLifecycle(nodeParams: NodeParams, cfg: SendPaymentConfig,
212213

213214
private def gotoSucceededOrStop(d: PaymentSucceeded): State = {
214215
if (publishPreimage) {
215-
d.request.replyTo ! PreimageReceived(paymentHash, d.preimage)
216+
d.request.replyTo ! PreimageReceived(paymentHash, d.preimage, d.remainingAttribution_opt)
216217
}
217218
if (d.pending.isEmpty) {
218-
myStop(d.request, Right(cfg.createPaymentSent(d.request.recipient, d.preimage, d.parts)))
219+
myStop(d.request, Right(cfg.createPaymentSent(d.request.recipient, d.preimage, d.parts, d.remainingAttribution_opt)))
219220
} else
220221
goto(PAYMENT_SUCCEEDED) using d
221222
}
@@ -310,7 +311,7 @@ object MultiPartPaymentLifecycle {
310311
* The payment FSM will wait for all child payments to settle before emitting payment events, but the preimage will be
311312
* shared as soon as it's received to unblock other actors that may need it.
312313
*/
313-
case class PreimageReceived(paymentHash: ByteVector32, paymentPreimage: ByteVector32)
314+
case class PreimageReceived(paymentHash: ByteVector32, paymentPreimage: ByteVector32, remainingAttribution_opt: Option[ByteVector])
314315

315316
// @formatter:off
316317
sealed trait State
@@ -367,7 +368,7 @@ object MultiPartPaymentLifecycle {
367368
* @param parts fulfilled child payments.
368369
* @param pending pending child payments (we are waiting for them to be fulfilled downstream).
369370
*/
370-
case class PaymentSucceeded(request: SendMultiPartPayment, preimage: ByteVector32, parts: Seq[PartialPayment], pending: Set[UUID]) extends Data
371+
case class PaymentSucceeded(request: SendMultiPartPayment, preimage: ByteVector32, parts: Seq[PartialPayment], pending: Set[UUID], remainingAttribution_opt: Option[ByteVector]) extends Data
371372

372373
private def createRouteRequest(replyTo: ActorRef, nodeParams: NodeParams, routeParams: RouteParams, d: PaymentProgress, cfg: SendPaymentConfig): RouteRequest = {
373374
RouteRequest(replyTo.toTyped, nodeParams.nodeId, d.request.recipient, routeParams, d.ignore, allowMultiPart = true, d.pending.values.toSeq, Some(cfg.paymentContext))

eclair-core/src/main/scala/fr/acinq/eclair/payment/send/PaymentInitiator.scala

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -29,6 +29,7 @@ import fr.acinq.eclair.payment.send.PaymentError._
2929
import fr.acinq.eclair.router.Router._
3030
import fr.acinq.eclair.wire.protocol._
3131
import fr.acinq.eclair.{CltvExpiry, CltvExpiryDelta, Features, MilliSatoshi, MilliSatoshiLong, NodeParams}
32+
import scodec.bits.ByteVector
3233

3334
import java.util.UUID
3435

@@ -335,7 +336,7 @@ object PaymentInitiator {
335336
case _ => PaymentType.Standard
336337
}
337338

338-
def createPaymentSent(recipient: Recipient, preimage: ByteVector32, parts: Seq[PaymentSent.PartialPayment]) = PaymentSent(parentId, paymentHash, preimage, recipient.totalAmount, recipient.nodeId, parts)
339+
def createPaymentSent(recipient: Recipient, preimage: ByteVector32, parts: Seq[PaymentSent.PartialPayment], remainingAttribution_opt: Option[ByteVector]) = PaymentSent(parentId, paymentHash, preimage, recipient.totalAmount, recipient.nodeId, parts, remainingAttribution_opt)
339340
}
340341

341342
}

0 commit comments

Comments
 (0)