What changed, and why it matters
This commit adds a new optional encrypted payload to Lightning payment fulfillment messages. It is a feature implementation, not a fix for an active vulnerability. The code does introduce a safety check: if a peer sends an oversized fulfillment payload, the node force-closes the channel after first relaying the payment preimage upstream so funds are not lost. The main risk is that any bugs in the new encryption, wrapping, or truncation logic could affect payment settlement or channel stability, but the commit itself is defensive and follows a proposed Lightning specification.
Review the new Sphinx.SuccessPacket.create/wrap/decrypt implementation and the channel force-close path for correctness. Ensure that truncation behavior cannot be abused to make a valid payload appear invalid, and that the force-close condition is only reachable for genuinely oversized payloads. Run the included reference test vectors and fuzz the size-limit edge cases.
Security signals we found
New cryptographic payload handling added to payment fulfillment path
Size limits and silent truncation applied to failure packets, fulfillment payloads, and attribution data
Channel force-close triggered on oversized peer fulfillment payload
Attribution HMACs extended to cover fulfillment payload for tamper detection
Blinded-route handling updated to avoid leaking attribution data while still wrapping payload
Evidence from the diff
The commit implements BOLTs PR 1344 support for a ‘fulfillment payload’ carried inside update_fulfill_htlc. It extends CMD_FULFILL_HTLC, UpdateFulfillHtlcTlv, PaymentSent, and the Sphinx SuccessPacket logic to create, wrap, decrypt, and forward a ChaCha20-Poly1305-encrypted payload. Attribution HMACs now cover the payload so senders can detect tampering. Sizes are capped at 32 kB (MAX_LENGTH) and silently truncated. Channel.scala adds checkFulfillmentPayload, which force-closes the channel if a peer sends a payload larger than Sphinx.SuccessPacket.MAX_LENGTH, after relaying the preimage upstream. The change touches 44 files and includes extensive tests and reference vectors.
Changed components
eclair-core/src/main/scala/fr/acinq/eclair/channel/fsm/Channel.scalaeclair-core/src/main/scala/fr/acinq/eclair/crypto/Sphinx.scalaeclair-core/src/main/scala/fr/acinq/eclair/payment/PaymentPacket.scalaeclair-core/src/main/scala/fr/acinq/eclair/payment/send/PaymentLifecycle.scalaeclair-core/src/main/scala/fr/acinq/eclair/payment/relay/ChannelRelay.scalaeclair-core/src/main/scala/fr/acinq/eclair/payment/relay/NodeRelay.scalaeclair-core/src/main/scala/fr/acinq/eclair/wire/protocol/HtlcTlv.scalaeclair-core/src/main/scala/fr/acinq/eclair/wire/protocol/LightningMessageTypes.scalaeclair-core/src/main/scala/fr/acinq/eclair/wire/internal/CommandCodecs.scalaInspect captured patch +812 / −233
### eclair-core/src/main/scala/fr/acinq/eclair/channel/ChannelData.scala
@@ -23,6 +23,7 @@ import fr.acinq.eclair.blockchain.fee.{ConfirmationTarget, FeeratePerKw}
import fr.acinq.eclair.channel.Helpers.Closing
import fr.acinq.eclair.channel.fund.InteractiveTxBuilder._
import fr.acinq.eclair.channel.fund.{InteractiveTxBuilder, InteractiveTxSigningSession}
+import fr.acinq.eclair.crypto.Sphinx
import fr.acinq.eclair.crypto.keymanager.ChannelKeys
import fr.acinq.eclair.io.Peer
import fr.acinq.eclair.reputation.Reputation
@@ -242,7 +243,7 @@ case class FailureAttributionData(htlcReceivedAt: TimestampMilli, trampolineRece
case class FulfillAttributionData(htlcReceivedAt: TimestampMilli, trampolineReceivedAt_opt: Option[TimestampMilli], downstreamAttribution_opt: Option[ByteVector])
sealed trait HtlcSettlementCommand extends HasOptionalReplyToCommand with ForbiddenCommandDuringQuiescenceNegotiation with ForbiddenCommandWhenQuiescent { def id: Long }
-final case class CMD_FULFILL_HTLC(id: Long, r: ByteVector32, attribution_opt: Option[FulfillAttributionData], commit: Boolean = false, replyTo_opt: Option[ActorRef] = None) extends HtlcSettlementCommand
+final case class CMD_FULFILL_HTLC(id: Long, r: ByteVector32, fulfillmentPayload_opt: Option[ByteVector], attribution_opt: Option[FulfillAttributionData], commit: Boolean = false, replyTo_opt: Option[ActorRef] = None) extends HtlcSettlementCommand
final case class CMD_FAIL_HTLC(id: Long, reason: FailureReason, attribution_opt: Option[FailureAttributionData], delay_opt: Option[FiniteDuration] = None, commit: Boolean = false, replyTo_opt: Option[ActorRef] = None) extends HtlcSettlementCommand
final case class CMD_FAIL_MALFORMED_HTLC(id: Long, onionHash: ByteVector32, failureCode: Int, commit: Boolean = false, replyTo_opt: Option[ActorRef] = None) extends HtlcSettlementCommand
final case class CMD_UPDATE_FEE(feeratePerKw: FeeratePerKw, commit: Boolean = false, replyTo_opt: Option[ActorRef] = None) extends HasOptionalReplyToCommand with ForbiddenCommandDuringQuiescenceNegotiation with ForbiddenCommandWhenQuiescent
@@ -304,7 +305,13 @@ final case class RES_FAILURE[+C <: Command, +T <: Throwable](cmd: C, t: T) exten
final case class RES_ADD_FAILED[+T <: ChannelException](c: CMD_ADD_HTLC, t: T, channelUpdate: Option[ChannelUpdate]) extends CommandFailure[CMD_ADD_HTLC, T] { override def toString = s"cannot add htlc with origin=${c.origin} reason=${t.getMessage}" }
sealed trait HtlcResult
object HtlcResult {
- sealed trait Fulfill extends HtlcResult { def paymentPreimage: ByteVector32 }
+ sealed trait Fulfill extends HtlcResult {
+ def paymentPreimage: ByteVector32
+ def fulfillmentPayload_opt: Option[ByteVector] = this match {
+ case RemoteFulfill(fulfill) => fulfill.fulfillmentPayload_opt.map(_.take(Sphinx.SuccessPacket.MAX_LENGTH))
+ case _: OnChainFulfill => None
+ }
+ }
case class RemoteFulfill(fulfill: UpdateFulfillHtlc) extends Fulfill { override val paymentPreimage: ByteVector32 = fulfill.paymentPreimage }
case class OnChainFulfill(paymentPreimage: ByteVector32) extends Fulfill
sealed trait Fail extends HtlcResult
### eclair-core/src/main/scala/fr/acinq/eclair/channel/ChannelExceptions.scala
@@ -136,6 +136,7 @@ case class RemoteDustHtlcExposureTooHigh (override val channelId: Byte
case class InsufficientFunds (override val channelId: ByteVector32, amount: MilliSatoshi, missing: Satoshi, reserve: Satoshi, fees: Satoshi) extends ChannelException(channelId, s"insufficient funds: missing=$missing reserve=$reserve fees=$fees")
case class RemoteCannotAffordFeesForNewHtlc (override val channelId: ByteVector32, amount: MilliSatoshi, missing: Satoshi, reserve: Satoshi, fees: Satoshi) extends ChannelException(channelId, s"remote can't afford increased commit tx fees once new HTLC is added: missing=$missing reserve=$reserve fees=$fees")
case class InvalidHtlcPreimage (override val channelId: ByteVector32, id: Long) extends ChannelException(channelId, s"invalid htlc preimage for htlc id=$id")
+case class InvalidFulfillmentPayload (override val channelId: ByteVector32, id: Long) extends ChannelException(channelId, s"invalid fulfillment payload for htlc id=$id")
case class UnknownHtlcId (override val channelId: ByteVector32, id: Long) extends ChannelException(channelId, s"unknown htlc id=$id")
case class CannotExtractSharedSecret (override val channelId: ByteVector32, htlc: UpdateAddHtlc) extends ChannelException(channelId, s"can't extract shared secret: paymentHash=${htlc.paymentHash} onion=${htlc.onionRoutingPacket}")
case class NonInitiatorCannotSendUpdateFee (override val channelId: ByteVector32) extends ChannelException(channelId, s"only the initiator should send update_fee messages")
### eclair-core/src/main/scala/fr/acinq/eclair/channel/fsm/Channel.scala
@@ -41,7 +41,7 @@ import fr.acinq.eclair.channel.fund.InteractiveTxBuilder._
import fr.acinq.eclair.channel.fund.{InteractiveTxBuilder, InteractiveTxFunder, InteractiveTxSigningSession}
import fr.acinq.eclair.channel.publish.TxPublisher.{PublishReplaceableTx, SetChannelId}
import fr.acinq.eclair.channel.publish._
-import fr.acinq.eclair.crypto.NonceGenerator
+import fr.acinq.eclair.crypto.{NonceGenerator, Sphinx}
import fr.acinq.eclair.crypto.keymanager.ChannelKeys
import fr.acinq.eclair.db.DbEventHandler.ChannelEvent.EventType
import fr.acinq.eclair.db.PendingCommandsDb
@@ -580,7 +580,7 @@ class Channel(val nodeParams: NodeParams, val channelKeys: ChannelKeys, val wall
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)
+ checkFulfillmentPayload(fulfill, d.copy(commitments = commitments1))
case Left(cause) => handleLocalError(cause, d, Some(fulfill))
}
@@ -1605,7 +1605,7 @@ class Channel(val nodeParams: NodeParams, val channelKeys: ChannelKeys, val wall
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, remoteNodeId, htlc, HtlcResult.RemoteFulfill(fulfill))
- stay() using d.copy(commitments = commitments1)
+ checkFulfillmentPayload(fulfill, d.copy(commitments = commitments1))
case Left(cause) => handleLocalError(cause, d, Some(fulfill))
}
@@ -3329,6 +3329,18 @@ class Channel(val nodeParams: NodeParams, val channelKeys: ChannelKeys, val wall
stay()
}
+ /**
+ * If our peer includes a fulfillment payload that is larger than what the specification allows, they are clearly
+ * malicious, so we immediately force-close. Note that we always relay the preimage upstream first, to make sure we
+ * don't lose funds.
+ */
+ private def checkFulfillmentPayload(fulfill: UpdateFulfillHtlc, d1: ChannelDataWithCommitments): State = {
+ fulfill.fulfillmentPayload_opt match {
+ case Some(payload) if payload.size > Sphinx.SuccessPacket.MAX_LENGTH => handleLocalError(InvalidFulfillmentPayload(d1.channelId, fulfill.id), d1, Some(fulfill))
+ case _ => stay() using d1
+ }
+ }
+
private def handleCommandSuccess(c: channel.Command, newData: ChannelData) = {
val replyTo_opt = c match {
case hasOptionalReplyTo: HasOptionalReplyToCommand => hasOptionalReplyTo.replyTo_opt
### eclair-core/src/main/scala/fr/acinq/eclair/crypto/Sphinx.scala
@@ -290,9 +290,13 @@ object Sphinx extends Logging {
case class HtlcFailure(holdTimes: Seq[HoldTime], failure: Either[CannotDecryptFailurePacket, DecryptedFailurePacket])
- case class HtlcSuccess(holdTimes: Seq[HoldTime], remainingAttribution_opt: Option[ByteVector])
+ case class HtlcSuccess(holdTimes: Seq[HoldTime], fulfillmentPayload_opt: Option[ByteVector], remainingAttribution_opt: Option[ByteVector])
object FailurePacket {
+ // The failure payload is limited to 32kB, to leave room for attribution data and other TLVs.
+ // We silently truncate payloads larger than 32kB.
+ val MAX_LENGTH = 32_768
+
/**
* Create a failure packet that needs to be wrapped before being returned to the sender.
* Each intermediate hop will add a layer of encryption and forward to the previous hop.
@@ -305,7 +309,7 @@ object Sphinx extends Logging {
*/
def create(sharedSecret: ByteVector32, failure: FailureMessage): ByteVector = {
val um = generateKey("um", sharedSecret)
- val packet = FailureMessageCodecs.failureOnionCodec(Hmac256(um)).encode(failure).require.toByteVector
+ val packet = FailureMessageCodecs.failureOnionCodec(Hmac256(um)).encode(failure).require.toByteVector.take(MAX_LENGTH)
logger.debug(s"um key: $um")
logger.debug(s"raw error packet: ${packet.toHex}")
packet
@@ -320,10 +324,10 @@ object Sphinx extends Logging {
*/
def wrap(packet: ByteVector, sharedSecret: ByteVector32): ByteVector = {
val key = generateKey("ammag", sharedSecret)
- val stream = generateStream(key, packet.length.toInt)
+ val stream = generateStream(key, packet.length.toInt.min(MAX_LENGTH))
logger.debug(s"ammag key: $key")
logger.debug(s"error stream: $stream")
- packet xor stream
+ packet.take(MAX_LENGTH) xor stream
}
/**
@@ -339,9 +343,9 @@ object Sphinx extends Logging {
*/
def decrypt(packet: ByteVector, attribution_opt: Option[ByteVector], sharedSecrets: Seq[SharedSecret], index: Int = 1): HtlcFailure = {
sharedSecrets match {
- case Nil => HtlcFailure(Nil, Left(CannotDecryptFailurePacket(packet, attribution_opt)))
+ case Nil => HtlcFailure(Nil, Left(CannotDecryptFailurePacket(packet.take(MAX_LENGTH), attribution_opt)))
case ss :: tail =>
- val packet1 = wrap(packet, ss.secret)
+ val packet1 = wrap(packet.take(MAX_LENGTH), ss.secret)
val attribution1_opt = attribution_opt.flatMap(attribution => Attribution.decrypt(attribution, Some(packet1), ss, sharedSecrets.length))
val um = generateKey("um", ss.secret)
val downstream = FailureMessageCodecs.failureOnionCodec(Hmac256(um)).decode(packet1.toBitVector) match {
@@ -356,27 +360,84 @@ object Sphinx extends Logging {
}
object SuccessPacket {
+ // The encrypted fulfillment payload that is sent on the wire is limited to 32kB, to leave room for attribution data
+ // and other TLVs. We silently truncate packets larger than 32kB.
+ val MAX_LENGTH: Int = 32_768
+ // We add a 16-bytes mac, so the plaintext payload must be slightly smaller.
+ val MAX_PAYLOAD_LENGTH: Int = MAX_LENGTH - 16
+
+ /**
+ * Create an encrypted fulfillment payload, that will be wrapped by each intermediate node before being returned to
+ * the sender. Note that the final node (which creates this payload) does *not* apply additional wrapping since this
+ * is encrypted (unlike what happens for failure messages).
+ *
+ * Note that malicious intermediate hops may drop the packet or alter it, but since each intermediate node includes
+ * the payload they received in their attribution data, the sender will be able to infer who dropped or altered the
+ * payload.
+ */
+ def create(sharedSecret: ByteVector32, payload: ByteVector): ByteVector = {
+ val key = generateKey("fulfillment", sharedSecret)
+ val (encryptedPayload, mac) = ChaCha20Poly1305.encrypt(key, zeroes(12), payload.take(MAX_PAYLOAD_LENGTH), ByteVector.empty)
+ encryptedPayload ++ mac
+ }
+
/**
- * Decrypt the attribution data provided in the HTLC-success case.
+ * Wrap the fulfillment payload received from the downstream node in an additional layer of onion encryption.
+ * Each intermediate node wraps the fulfillment payload until it reaches the original sender.
+ * Each intermediate node also includes the received fulfillment payload in its attribution data.
+ */
+ def wrap(payload: ByteVector, sharedSecret: ByteVector32): ByteVector = {
+ val key = generateKey("ammag", sharedSecret)
+ val stream = generateStream(key, payload.length.toInt.min(MAX_LENGTH))
+ payload.take(MAX_LENGTH) xor stream
+ }
+
+ /**
+ * Decrypt the fulfillment payload and attribution data provided in the HTLC-success case.
+ * Node shared secrets are applied until we reach the recipient's shared secret, where the decryption step differs.
* Note that malicious nodes in the route may have altered the packet, triggering a decryption failure.
*
- * @param attribution_opt attribution data for this success packet.
- * @param sharedSecrets nodes shared secrets.
+ * @param payload_opt fulfillment payload.
+ * @param attribution_opt attribution data for this success packet.
+ * @param sharedSecrets nodes shared secrets.
+ * @param lastSecretIsRecipient whether the last element of `sharedSecrets` belongs to the node that created the
+ * fulfillment payload: only that node encrypts it instead of wrapping it. This must
+ * be set to false when decrypting a partial route (e.g. as an intermediate
+ * trampoline node), and when sending a trampoline payment the trampoline onion shared
+ * secrets must be appended to the outer onion shared secrets for this to hold.
*/
- def decrypt(attribution_opt: Option[ByteVector], sharedSecrets: Seq[SharedSecret], index: Int = 1): HtlcSuccess = {
+ def decrypt(payload_opt: Option[ByteVector], attribution_opt: Option[ByteVector], sharedSecrets: Seq[SharedSecret], lastSecretIsRecipient: Boolean = true): HtlcSuccess = {
+ val truncatedPayload_opt = payload_opt.map(_.take(MAX_LENGTH))
+ val truncatedAttribution_opt = attribution_opt.map(_.take(Attribution.totalLength))
sharedSecrets match {
- case Nil => HtlcSuccess(Nil, attribution_opt)
+ case Nil => HtlcSuccess(Nil, truncatedPayload_opt, truncatedAttribution_opt)
case ss :: tail =>
- attribution_opt match {
- case None => HtlcSuccess(Nil, None)
- case Some(attribution) =>
- Attribution.decrypt(attribution, None, ss, sharedSecrets.length) match {
- case Some(perHopAttribution) =>
- val downstream = decrypt(Some(perHopAttribution.downstreamAttribution), tail, index + 1)
- HtlcSuccess(perHopAttribution.holdTime +: downstream.holdTimes, downstream.remainingAttribution_opt)
- case None => HtlcSuccess(Nil, Some(attribution))
- }
+ // We start by unwrapping the fulfillment payload, if provided.
+ val isFinalNode = tail.isEmpty && lastSecretIsRecipient
+ val unwrappedPayload_opt = truncatedPayload_opt match {
+ case Some(payload) if isFinalNode =>
+ // We decrypt the payload provided by the final node.
+ val key = generateKey("fulfillment", ss.secret)
+ Try(ChaCha20Poly1305.decrypt(key, zeroes(12), payload.dropRight(16), ByteVector.empty, payload.takeRight(16))).toOption
+ case Some(payload) =>
+ // We peel the wrapping added by the intermediate node.
+ Some(wrap(payload, ss.secret))
+ case None => None
}
+ // We decrypt the attribution data provided by this node: its HMACs must cover the unwrapped fulfillment payload.
+ // The code below is quite subtle, because nodes inside a blinded path don't include attribution data, so when
+ // we reach that point in the recursion, attribution decryption will fail, which is expected.
+ // After that, attribution_opt will be set to None for recursive calls, because there is no point trying to
+ // decrypt attribution data that wasn't actually provided or that was tampered with.
+ // Note that if an intermediate node tampered with the attribution data, it will have the same effect: we will
+ // stop processing attribution after that node.
+ // The caller can look at the reported hold times to know which nodes provided valid attribution data: this
+ // allows identifying which nodes are acting maliciously, if any.
+ // We keep processing the fulfillment payload recursively though, because we need to use all shared secrets
+ // to decrypt it.
+ val attribution1_opt = truncatedAttribution_opt.flatMap(attribution => Attribution.decrypt(attribution, if (!isFinalNode) unwrappedPayload_opt else None, ss, sharedSecrets.length))
+ val downstream = decrypt(unwrappedPayload_opt, attribution1_opt.map(_.downstreamAttribution), tail, lastSecretIsRecipient)
+ HtlcSuccess(attribution1_opt.map(_.holdTime).toSeq ++ downstream.holdTimes, downstream.fulfillmentPayload_opt, downstream.remainingAttribution_opt)
}
}
}
@@ -395,7 +456,8 @@ object Sphinx extends Logging {
private val maxNumHops = 20
private val holdTimeLength = 4
private val hmacLength = 4 // HMACs are truncated to 4 bytes to save space
- val totalLength: Int = maxNumHops * holdTimeLength + maxNumHops * (maxNumHops + 1) / 2 * hmacLength // = 920
+ // Attribution data uses a fixed length of 920 bytes: we silently truncate it if we receive a larger one.
+ val totalLength: Int = maxNumHops * holdTimeLength + maxNumHops * (maxNumHops + 1) / 2 * hmacLength
/** Valid attribution data from one hop in the payment path. */
case class PerHopAttribution(holdTime: HoldTime, downstreamAttribution: ByteVector) {
@@ -405,7 +467,7 @@ object Sphinx extends Logging {
private def wrap(attributionData: ByteVector, sharedSecret: ByteVector32): ByteVector = {
val key = generateKey("ammagext", sharedSecret)
val stream = generateStream(key, totalLength)
- attributionData xor stream
+ attributionData.take(totalLength) xor stream
}
/**
@@ -449,7 +511,7 @@ object Sphinx extends Logging {
* @param payload_opt payload that should be covered by the attribution HMACs.
*/
def create(downstreamAttribution_opt: Option[ByteVector], payload_opt: Option[ByteVector], holdTime: FiniteDuration, sharedSecret: ByteVector32): ByteVector = {
- val downstreamAttribution = downstreamAttribution_opt.getOrElse(zeroes(totalLength))
+ val downstreamAttribution = downstreamAttribution_opt.map(_.take(totalLength)).getOrElse(zeroes(totalLength))
val downstreamHmacs = getHmacs(downstreamAttribution).dropRight(1).map(_.drop(1))
val downstreamHoldTimes = downstreamAttribution.take((maxNumHops - 1) * holdTimeLength)
val holdTimes = uint32.encode(holdTime.toMillis / 100).require.bytes ++ downstreamHoldTimes
@@ -468,7 +530,7 @@ object Sphinx extends Logging {
* @param remainingHops number of remaining downstream nodes.
*/
def decrypt(attribution: ByteVector, payload_opt: Option[ByteVector], sharedSecret: SharedSecret, remainingHops: Int): Option[PerHopAttribution] = {
- val decrypted = wrap(attribution, sharedSecret.secret)
+ val decrypted = wrap(attribution.take(totalLength), sharedSecret.secret)
val holdTime = (uint32.decode(decrypted.take(holdTimeLength).bits).require.value * 100).milliseconds
val holdTimes = decrypted.take(maxNumHops * holdTimeLength)
val hmacs = getHmacs(decrypted)
### eclair-core/src/main/scala/fr/acinq/eclair/db/pg/PgAuditDb.scala
@@ -592,6 +592,7 @@ class PgAuditDb(implicit ds: DataSource) extends AuditDb with Logging {
PublicKey(rs.getByteVectorFromHex("recipient_node_id")),
Seq(part),
None,
+ None,
part.startedAt)
}
sentByParentId + (parentId -> sent)
### eclair-core/src/main/scala/fr/acinq/eclair/db/sqlite/SqliteAuditDb.scala
@@ -563,6 +563,7 @@ class SqliteAuditDb(val sqlite: Connection) extends AuditDb with Logging {
PublicKey(rs.getByteVectorFromHex("recipient_node_id")),
Seq(part),
None,
+ None,
part.startedAt)
}
sentByParentId + (parentId -> sent)
### eclair-core/src/main/scala/fr/acinq/eclair/payment/PaymentEvents.scala
@@ -77,9 +77,12 @@ object PaymentEvent {
* @param recipientAmount amount that has been received by the final recipient.
* @param recipientNodeId id of the final recipient.
* @param parts child payments (actual outgoing HTLCs).
+ * @param fulfillmentPayload_opt fulfillment payload provided by the recipient, after applying our shared secrets: it
+ * is decrypted when we are the sender, and must be forwarded upstream (after adding a
+ * layer of encryption) when relaying a trampoline payment.
* @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.PaymentPart], remainingAttribution_opt: Option[ByteVector], startedAt: TimestampMilli) extends PaymentEvent {
+case class PaymentSent(id: UUID, paymentPreimage: ByteVector32, recipientAmount: MilliSatoshi, recipientNodeId: PublicKey, parts: Seq[PaymentSent.PaymentPart], fulfillmentPayload_opt: Option[ByteVector], 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
### eclair-core/src/main/scala/fr/acinq/eclair/payment/PaymentPacket.scala
@@ -356,7 +356,7 @@ object OutgoingPaymentPacket {
}
}
- private case class HtlcSharedSecrets(outerOnionSecret: ByteVector32, trampolineOnionSecret_opt: Option[ByteVector32], blinded: Boolean)
+ private case class HtlcSharedSecrets(outerOnionSecret: ByteVector32, trampolineOnionSecret_opt: Option[ByteVector32], blinded: Boolean, isFinalNode: Boolean)
/**
* We decrypt the onion again to extract the shared secret(s) used to encrypt onion failures.
@@ -365,24 +365,32 @@ object OutgoingPaymentPacket {
* It's simpler to extract it again from the encrypted onion.
*/
private def extractSharedSecret(nodeSecret: PrivateKey, add: UpdateAddHtlc): Either[CannotExtractSharedSecret, HtlcSharedSecrets] = {
- Sphinx.peel(nodeSecret, Some(add.paymentHash), add.onionRoutingPacket) match {
- case Right(Sphinx.DecryptedPacket(payload, _, outerOnionSecret)) =>
+ val outerOnionDecryptionKey = add.pathKey_opt match {
+ case Some(blinding) => Sphinx.RouteBlinding.derivePrivateKey(nodeSecret, blinding)
+ case None => nodeSecret
+ }
+ Sphinx.peel(outerOnionDecryptionKey, Some(add.paymentHash), add.onionRoutingPacket) match {
+ case Right(packet@Sphinx.DecryptedPacket(payload, _, outerOnionSecret)) =>
// Let's look at the onion payload to see if it contains a trampoline onion.
PaymentOnionCodecs.perHopPayloadCodec.decode(payload.bits) match {
case Attempt.Successful(DecodeResult(perHopPayload, _)) =>
// We try to extract the trampoline shared secret, if we can find one.
- val trampolineOnionSecret_opt = perHopPayload.get[OnionPaymentPayloadTlv.TrampolineOnion].map(_.packet).flatMap(trampolinePacket => {
+ val trampolinePacket_opt = perHopPayload.get[OnionPaymentPayloadTlv.TrampolineOnion].map(_.packet).flatMap(trampolinePacket => {
val trampolinePathKey_opt = perHopPayload.get[OnionPaymentPayloadTlv.PathKey].map(_.publicKey)
val trampolineOnionDecryptionKey = trampolinePathKey_opt.map(pathKey => Sphinx.RouteBlinding.derivePrivateKey(nodeSecret, pathKey)).getOrElse(nodeSecret)
- Sphinx.peel(trampolineOnionDecryptionKey, Some(add.paymentHash), trampolinePacket).toOption.map(_.sharedSecret)
+ Sphinx.peel(trampolineOnionDecryptionKey, Some(add.paymentHash), trampolinePacket).toOption
})
- // We check if we are an intermediate node in a blinded (potentially trampoline) path.
- val blinded = trampolineOnionSecret_opt match {
- case Some(_) => perHopPayload.get[OnionPaymentPayloadTlv.PathKey].nonEmpty
- case None => add.pathKey_opt.nonEmpty
+ // We check if we are inside a blinded (potentially trampoline) path: we must then not reveal any attribution
+ // data. Note that we must check both path keys: `add.pathKey_opt` is set when we're not the introduction
+ // node of a blinded outer path, and the payload's path key is set when we're inside a blinded trampoline
+ // path.
+ val blinded = add.pathKey_opt.nonEmpty || (trampolinePacket_opt.nonEmpty && perHopPayload.get[OnionPaymentPayloadTlv.PathKey].nonEmpty)
+ val isFinalNode = trampolinePacket_opt match {
+ case Some(trampolinePacket) => trampolinePacket.isLastPacket
+ case None => packet.isLastPacket
}
- Right(HtlcSharedSecrets(outerOnionSecret, trampolineOnionSecret_opt, blinded))
- case Attempt.Failure(_) => Right(HtlcSharedSecrets(outerOnionSecret, None, blinded = add.pathKey_opt.nonEmpty))
+ Right(HtlcSharedSecrets(outerOnionSecret, trampolinePacket_opt.map(_.sharedSecret), blinded, isFinalNode))
+ case Attempt.Failure(_) => Right(HtlcSharedSecrets(outerOnionSecret, None, blinded = add.pathKey_opt.nonEmpty, isFinalNode = packet.isLastPacket))
}
case Left(_) => Left(CannotExtractSharedSecret(add.channelId, add))
}
@@ -423,24 +431,43 @@ object OutgoingPaymentPacket {
}
}
+ private def wrapFulfillmentPayload(cmd: CMD_FULFILL_HTLC, sharedSecret: ByteVector32, isFinalNode: Boolean): Option[ByteVector] = {
+ if (isFinalNode) {
+ // The final node encrypts the fulfillment payload without applying the wrapping step.
+ cmd.fulfillmentPayload_opt.map(p => Sphinx.SuccessPacket.create(sharedSecret, p))
+ } else {
+ // Intermediate nodes wrap the downstream fulfillment payload with their shared secret.
+ cmd.fulfillmentPayload_opt.map(p => Sphinx.SuccessPacket.wrap(p, sharedSecret))
+ }
+ }
+
def buildHtlcFulfill(nodeSecret: PrivateKey, useAttributionData: Boolean, cmd: CMD_FULFILL_HTLC, add: UpdateAddHtlc, now: TimestampMilli = TimestampMilli.now()): UpdateFulfillHtlc = {
- // If we are part of a blinded route, we must not include any attribution data.
- val attributionData_opt = add.pathKey_opt match {
- case None if useAttributionData =>
- val trampolineHoldTime = cmd.attribution_opt.flatMap(_.trampolineReceivedAt_opt).map(receivedAt => now - receivedAt).getOrElse(0 millisecond)
- val holdTime = cmd.attribution_opt.map(a => now - a.htlcReceivedAt).getOrElse(0 millisecond)
- extractSharedSecret(nodeSecret, add) match {
- case Right(HtlcSharedSecrets(outerOnionSecret, None, _)) =>
- Some(Sphinx.Attribution.create(cmd.attribution_opt.flatMap(_.downstreamAttribution_opt), None, holdTime, outerOnionSecret))
- case Right(HtlcSharedSecrets(outerOnionSecret, Some(trampolineOnionSecret), blinded)) if !blinded =>
- val trampolineAttribution = Sphinx.Attribution.create(cmd.attribution_opt.flatMap(_.downstreamAttribution_opt), None, trampolineHoldTime, trampolineOnionSecret)
- Some(Sphinx.Attribution.create(Some(trampolineAttribution), None, holdTime, outerOnionSecret))
- case _ => None
- }
- case _ => None
+ // Note that if we are part of a blinded route, we must not include any attribution data.
+ // But we must wrap the fulfillment payload in all cases to ensure that the sender receives it.
+ val downstreamAttribution_opt = cmd.attribution_opt.flatMap(_.downstreamAttribution_opt)
+ val trampolineHoldTime = cmd.attribution_opt.flatMap(_.trampolineReceivedAt_opt).map(receivedAt => now - receivedAt).getOrElse(0 millisecond)
+ val holdTime = cmd.attribution_opt.map(a => now - a.htlcReceivedAt).getOrElse(0 millisecond)
+ val (attributionData_opt, fulfillmentPayload_opt) = extractSharedSecret(nodeSecret, add) match {
+ case Right(HtlcSharedSecrets(outerOnionSecret, None, blinded, isFinalNode)) =>
+ // The final node doesn't include its fulfillment payload in the attribution HMACs: it already has its own MAC.
+ val attribution = Sphinx.Attribution.create(downstreamAttribution_opt, if (!isFinalNode) cmd.fulfillmentPayload_opt else None, holdTime, outerOnionSecret)
+ val attribution_opt = if (useAttributionData && !blinded) Some(attribution) else None
+ val fulfillmentPayload_opt = wrapFulfillmentPayload(cmd, outerOnionSecret, isFinalNode)
+ (attribution_opt, fulfillmentPayload_opt)
+ case Right(HtlcSharedSecrets(outerOnionSecret, Some(trampolineOnionSecret), blinded, isFinalNode)) =>
+ // We do a first pass with the trampoline shared secret.
+ val trampolineAttribution = Sphinx.Attribution.create(downstreamAttribution_opt, if (!isFinalNode) cmd.fulfillmentPayload_opt else None, trampolineHoldTime, trampolineOnionSecret)
+ val trampolineFulfillmentPayload_opt = wrapFulfillmentPayload(cmd, trampolineOnionSecret, isFinalNode)
+ // Then a second pass with the outer onion shared secret.
+ val attribution = Sphinx.Attribution.create(Some(trampolineAttribution), trampolineFulfillmentPayload_opt, holdTime, outerOnionSecret)
+ val attribution_opt = if (useAttributionData && !blinded) Some(attribution) else None
+ val fulfillmentPayload_opt = trampolineFulfillmentPayload_opt.map(p => Sphinx.SuccessPacket.wrap(p, outerOnionSecret))
+ (attribution_opt, fulfillmentPayload_opt)
+ case Left(_) => (None, None)
}
val tlvs: Set[UpdateFulfillHtlcTlv] = Set(
- attributionData_opt.map(UpdateFulfillHtlcTlv.AttributionData(_))
+ attributionData_opt.map(UpdateFulfillHtlcTlv.AttributionData(_)),
+ fulfillmentPayload_opt.map(UpdateFulfillHtlcTlv.FulfillmentPayload(_)),
).flatten
UpdateFulfillHtlc(add.channelId, cmd.id, cmd.r, TlvStream(tlvs))
}
### eclair-core/src/main/scala/fr/acinq/eclair/payment/receive/MultiPartHandler.scala
@@ -190,7 +190,7 @@ class MultiPartHandler(nodeParams: NodeParams, register: ActorRef, db: IncomingP
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))
+ PendingCommandsDb.safeSend(register, nodeParams.db.pendingCommands, p.htlc.channelId, CMD_FULFILL_HTLC(p.htlc.id, record.paymentPreimage, None, Some(attribution), commit = true))
ctx.system.eventStream.publish(received)
} else {
val attribution = FailureAttributionData(htlcReceivedAt = p.receivedAt, trampolineReceivedAt_opt = None)
@@ -223,7 +223,7 @@ class MultiPartHandler(nodeParams: NodeParams, register: ActorRef, db: IncomingP
parts.collect {
case p: MultiPartPaymentFSM.HtlcPart =>
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, payment.paymentPreimage, Some(attribution), commit = true))
+ PendingCommandsDb.safeSend(register, nodeParams.db.pendingCommands, p.htlc.channelId, CMD_FULFILL_HTLC(p.htlc.id, payment.paymentPreimage, None, Some(attribution), commit = true))
}
postFulfill(received)
ctx.system.eventStream.publish(received)
### eclair-core/src/main/scala/fr/acinq/eclair/payment/relay/ChannelRelay.scala
@@ -255,7 +255,7 @@ class ChannelRelay private(nodeParams: NodeParams,
case HtlcResult.OnChainFulfill(_) => None
}
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)
+ val cmd = CMD_FULFILL_HTLC(upstream.add.id, fulfill.paymentPreimage, fulfill.fulfillmentPayload_opt, Some(attribution), commit = true)
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, Seq(incoming), Seq(outgoing)))
### eclair-core/src/main/scala/fr/acinq/eclair/payment/relay/NodeRelay.scala
@@ -393,11 +393,11 @@ class NodeRelay private(nodeParams: NodeParams,
Behaviors.receiveMessagePartial {
rejectExtraHtlcPartialFunction orElse {
// this is the fulfill that arrives from downstream channels
- case WrappedPreimageReceived(PreimageReceived(_, paymentPreimage, attribution_opt)) =>
+ case WrappedPreimageReceived(PreimageReceived(_, paymentPreimage, fulfillmentPayload_opt, attribution_opt)) =>
if (!fulfilledUpstream) {
// We want to fulfill upstream as soon as we receive the preimage (even if not all HTLCs have fulfilled downstream).
context.log.debug("got preimage from downstream")
- fulfillPayment(upstream, paymentPreimage, attribution_opt)
+ fulfillPayment(upstream, paymentPreimage, fulfillmentPayload_opt, attribution_opt)
sending(upstream, recipient, walletNodeId_opt, recipientFeatures_opt, nextPayload, startedAt, fulfilledUpstream = true, accountable)
} else {
// we don't want to fulfill multiple times
@@ -512,16 +512,16 @@ class NodeRelay private(nodeParams: NodeParams,
upstream.received.foreach(r => rejectHtlc(r.add.id, r.add.channelId, upstream.amountIn, r.receivedAt, Some(upstream.receivedAt), failure))
}
- private def fulfillPayment(upstream: Upstream.Hot.Trampoline, paymentPreimage: ByteVector32, downstreamAttribution_opt: Option[ByteVector]): Unit = upstream.received.foreach(r => {
+ private def fulfillPayment(upstream: Upstream.Hot.Trampoline, paymentPreimage: ByteVector32, downstreamFulfillmentPayload_opt: Option[ByteVector], downstreamAttribution_opt: Option[ByteVector]): Unit = upstream.received.foreach(r => {
val attribution = FulfillAttributionData(r.receivedAt, Some(upstream.receivedAt), downstreamAttribution_opt)
- val cmd = CMD_FULFILL_HTLC(r.add.id, paymentPreimage, Some(attribution), commit = true)
+ val cmd = CMD_FULFILL_HTLC(r.add.id, paymentPreimage, downstreamFulfillmentPayload_opt, Some(attribution), commit = true)
PendingCommandsDb.safeSend(register, nodeParams.db.pendingCommands, r.add.channelId, cmd)
})
private def success(upstream: Upstream.Hot.Trampoline, fulfilledUpstream: Boolean, paymentSent: PaymentSent): Unit = {
// We may have already fulfilled upstream, but we can now emit an accurate relayed event and clean-up resources.
if (!fulfilledUpstream) {
- fulfillPayment(upstream, paymentSent.paymentPreimage, paymentSent.remainingAttribution_opt)
+ fulfillPayment(upstream, paymentSent.paymentPreimage, paymentSent.fulfillmentPayload_opt, paymentSent.remainingAttribution_opt)
}
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))
### eclair-core/src/main/scala/fr/acinq/eclair/payment/relay/OnTheFlyFunding.scala
@@ -140,10 +140,10 @@ object OnTheFlyFunding {
/** Create commands to fulfill all upstream HTLCs. */
def createFulfillCommands(preimage: ByteVector32): Seq[(ByteVector32, CMD_FULFILL_HTLC)] = upstream match {
case _: Upstream.Local => Nil
- case u: Upstream.Hot.Channel => Seq(u.add.channelId -> CMD_FULFILL_HTLC(u.add.id, preimage, Some(FulfillAttributionData(htlcReceivedAt = u.receivedAt, trampolineReceivedAt_opt = None, downstreamAttribution_opt = None)), commit = true))
+ case u: Upstream.Hot.Channel => Seq(u.add.channelId -> CMD_FULFILL_HTLC(u.add.id, preimage, None, Some(FulfillAttributionData(htlcReceivedAt = u.receivedAt, trampolineReceivedAt_opt = None, downstreamAttribution_opt = None)), commit = true))
case u: Upstream.Hot.Trampoline => u.received.map(c => {
val attribution = FulfillAttributionData(htlcReceivedAt = c.receivedAt, trampolineReceivedAt_opt = Some(u.receivedAt), downstreamAttribution_opt = None)
- c.add.channelId -> CMD_FULFILL_HTLC(c.add.id, preimage, Some(attribution), commit = true)
+ c.add.channelId -> CMD_FULFILL_HTLC(c.add.id, preimage, None, Some(attribution), commit = true)
})
}
}
### eclair-core/src/main/scala/fr/acinq/eclair/payment/relay/PostRestartHtlcCleaner.scala
@@ -31,6 +31,7 @@ import fr.acinq.eclair.transactions.DirectedHtlc.outgoing
import fr.acinq.eclair.wire.protocol._
import fr.acinq.eclair.{CustomCommitmentsPlugin, Feature, Features, Logs, MilliSatoshiLong, NodeParams, TimestampMilli}
import kamon.metric.{Gauge, Metric}
+import scodec.bits.ByteVector
import scala.concurrent.Promise
import scala.util.Try
@@ -122,7 +123,7 @@ class PostRestartHtlcCleaner(nodeParams: NodeParams, register: ActorRef, initial
Metrics.Resolved.withTag(Tags.Success, value = true).withTag(Metrics.Relayed, value = false).increment()
if (e.currentState != CLOSED) {
log.info(s"fulfilling broken htlc=$htlc")
- channel ! CMD_FULFILL_HTLC(htlc.id, preimage, None, commit = true)
+ channel ! CMD_FULFILL_HTLC(htlc.id, preimage, None, None, commit = true)
} else {
log.info(s"got preimage but upstream channel is closed for htlc=$htlc")
}
@@ -161,7 +162,7 @@ class PostRestartHtlcCleaner(nodeParams: NodeParams, register: ActorRef, initial
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)
+ handleDownstreamFulfill(brokenHtlcs, o, remoteNodeId, htlc, fulfill.paymentPreimage, fulfill.fulfillmentPayload_opt)
case RES_ADD_SETTLED(o: Origin.Cold, remoteNodeId, htlc, fail: HtlcResult.Fail) =>
// Note that an on-chain failure guarantees that our peer cannot claim that HTLC anymore. At that point, we must
@@ -177,22 +178,23 @@ class PostRestartHtlcCleaner(nodeParams: NodeParams, register: ActorRef, initial
case GetBrokenHtlcs => sender() ! brokenHtlcs
}
- private def handleDownstreamFulfill(brokenHtlcs: BrokenHtlcs, origin: Origin.Cold, downstreamNodeId: PublicKey, fulfilledHtlc: UpdateAddHtlc, paymentPreimage: ByteVector32): Unit =
+ private def handleDownstreamFulfill(brokenHtlcs: BrokenHtlcs, origin: Origin.Cold, downstreamNodeId: PublicKey, fulfilledHtlc: UpdateAddHtlc, paymentPreimage: ByteVector32, fulfillmentPayload_opt: Option[ByteVector]): 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.PaymentPart(id, PaymentEvent.OutgoingPayment(fulfilledHtlc.channelId, downstreamNodeId, fulfilledHtlc.amountMsat, settledAt = TimestampMilli.now()), feesPaid, None, startedAt = p.createdAt) :: Nil, None, p.createdAt))
+ // Note that we lost the onion shared secrets when restarting, so we cannot decrypt the fulfillment payload.
+ 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, 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.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)
+ val sent = PaymentSent(p.parentId, paymentPreimage, p.recipientAmount, p.recipientNodeId, succeeded, None, None, p.createdAt)
log.info(s"payment id=${sent.id} paymentHash=${sent.paymentHash} successfully sent (amount=${sent.recipientAmount})")
context.system.eventStream.publish(sent)
}
@@ -204,7 +206,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.PaymentPart(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, 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.
@@ -215,7 +217,7 @@ class PostRestartHtlcCleaner(nodeParams: NodeParams, register: ActorRef, initial
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, u.originChannelId, CMD_FULFILL_HTLC(u.originHtlcId, paymentPreimage, None, commit = true))
+ PendingCommandsDb.safeSend(register, nodeParams.db.pendingCommands, u.originChannelId, CMD_FULFILL_HTLC(u.originHtlcId, paymentPreimage, fulfillmentPayload_opt, 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(fulfilledHtlc.paymentHash, Seq(PaymentEvent.IncomingPayment(u.originChannelId, u.originNodeId, u.amountIn, TimestampMilli.now())), Seq(PaymentEvent.OutgoingPayment(fulfilledHtlc.channelId, downstreamNodeId, fulfilledHtlc.amountMsat, TimestampMilli.now()))))
Metrics.PendingRelayedOut.decrement()
@@ -226,7 +228,7 @@ class PostRestartHtlcCleaner(nodeParams: NodeParams, register: ActorRef, initial
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))
+ PendingCommandsDb.safeSend(register, nodeParams.db.pendingCommands, channelId, CMD_FULFILL_HTLC(htlcId, paymentPreimage, fulfillmentPayload_opt, None, commit = true))
}
}
val relayedOut1 = relayedOut diff Set((fulfilledHtlc.channelId, fulfilledHtlc.id))
### eclair-core/src/main/scala/fr/acinq/eclair/payment/send/MultiPartPaymentLifecycle.scala
@@ -120,7 +120,7 @@ class MultiPartPaymentLifecycle(nodeParams: NodeParams, cfg: SendPaymentConfig,
case Event(ps: PaymentSent, d: PaymentProgress) =>
require(ps.parts.length == 1, "child payment must contain only one part")
// As soon as we get the preimage we can consider that the whole payment succeeded (we have a proof of payment).
- gotoSucceededOrStop(PaymentSucceeded(d.request, ps.paymentPreimage, ps.parts, d.pending.keySet - ps.parts.head.id, ps.remainingAttribution_opt))
+ gotoSucceededOrStop(PaymentSucceeded(d.request, ps.paymentPreimage, ps.parts, d.pending.keySet - ps.parts.head.id, ps.fulfillmentPayload_opt, ps.remainingAttribution_opt))
}
when(PAYMENT_IN_PROGRESS) {
@@ -146,7 +146,7 @@ class MultiPartPaymentLifecycle(nodeParams: NodeParams, cfg: SendPaymentConfig,
require(ps.parts.length == 1, "child payment must contain only one part")
// As soon as we get the preimage we can consider that the whole payment succeeded (we have a proof of payment).
Metrics.PaymentAttempt.withTag(Tags.MultiPart, value = true).record(d.request.maxAttempts - d.remainingAttempts)
- gotoSucceededOrStop(PaymentSucceeded(d.request, ps.paymentPreimage, ps.parts, d.pending.keySet - ps.parts.head.id, ps.remainingAttribution_opt))
+ gotoSucceededOrStop(PaymentSucceeded(d.request, ps.paymentPreimage, ps.parts, d.pending.keySet - ps.parts.head.id, ps.fulfillmentPayload_opt, ps.remainingAttribution_opt))
}
when(PAYMENT_ABORTED) {
@@ -164,7 +164,7 @@ class MultiPartPaymentLifecycle(nodeParams: NodeParams, cfg: SendPaymentConfig,
case Event(ps: PaymentSent, d: PaymentAborted) =>
require(ps.parts.length == 1, "child payment must contain only one part")
log.warning(s"payment recipient fulfilled incomplete multi-part payment (id=${ps.parts.head.id})")
- gotoSucceededOrStop(PaymentSucceeded(d.request, ps.paymentPreimage, ps.parts, d.pending - ps.parts.head.id, ps.remainingAttribution_opt))
+ gotoSucceededOrStop(PaymentSucceeded(d.request, ps.paymentPreimage, ps.parts, d.pending - ps.parts.head.id, ps.fulfillmentPayload_opt, ps.remainingAttribution_opt))
case Event(_: RouteResponse, _) => stay()
case Event(_: PaymentRouteNotFound, _) => stay()
@@ -176,7 +176,7 @@ class MultiPartPaymentLifecycle(nodeParams: NodeParams, cfg: SendPaymentConfig,
val parts = d.parts ++ ps.parts
val pending = d.pending - ps.parts.head.id
if (pending.isEmpty) {
- myStop(d.request, Right(cfg.createPaymentSent(d.request.recipient, d.preimage, parts, d.remainingAttribution_opt, start)))
+ myStop(d.request, Right(cfg.createPaymentSent(d.request.recipient, d.preimage, parts, d.fulfillmentPayload_opt, d.remainingAttribution_opt, start)))
} else {
stay() using d.copy(parts = parts, pending = pending)
}
@@ -187,7 +187,7 @@ class MultiPartPaymentLifecycle(nodeParams: NodeParams, cfg: SendPaymentConfig,
log.warning(s"payment succeeded but partial payment failed (id=${pf.id})")
val pending = d.pending - pf.id
if (pending.isEmpty) {
- myStop(d.request, Right(cfg.createPaymentSent(d.request.recipient, d.preimage, d.parts, d.remainingAttribution_opt, start)))
+ myStop(d.request, Right(cfg.createPaymentSent(d.request.recipient, d.preimage, d.parts, d.fulfillmentPayload_opt, d.remainingAttribution_opt, start)))
} else {
stay() using d.copy(pending = pending)
}
@@ -214,10 +214,10 @@ class MultiPartPaymentLifecycle(nodeParams: NodeParams, cfg: SendPaymentConfig,
private def gotoSucceededOrStop(d: PaymentSucceeded): State = {
if (publishPreimage) {
- d.request.replyTo ! PreimageReceived(paymentHash, d.preimage, d.remainingAttribution_opt)
+ d.request.replyTo ! PreimageReceived(paymentHash, d.preimage, d.fulfillmentPayload_opt, d.remainingAttribution_opt)
}
if (d.pending.isEmpty) {
- myStop(d.request, Right(cfg.createPaymentSent(d.request.recipient, d.preimage, d.parts, d.remainingAttribution_opt, start)))
+ myStop(d.request, Right(cfg.createPaymentSent(d.request.recipient, d.preimage, d.parts, d.fulfillmentPayload_opt, d.remainingAttribution_opt, start)))
} else
goto(PAYMENT_SUCCEEDED) using d
}
@@ -312,7 +312,7 @@ object MultiPartPaymentLifecycle {
* The payment FSM will wait for all child payments to settle before emitting payment events, but the preimage will be
* shared as soon as it's received to unblock other actors that may need it.
*/
- case class PreimageReceived(paymentHash: ByteVector32, paymentPreimage: ByteVector32, remainingAttribution_opt: Option[ByteVector])
+ case class PreimageReceived(paymentHash: ByteVector32, paymentPreimage: ByteVector32, fulfillmentPayload_opt: Option[ByteVector], remainingAttribution_opt: Option[ByteVector])
// @formatter:off
sealed trait State
@@ -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[PaymentPart], pending: Set[UUID], remainingAttribution_opt: Option[ByteVector]) extends Data
+ case class PaymentSucceeded(request: SendMultiPartPayment, preimage: ByteVector32, parts: Seq[PaymentPart], pending: Set[UUID], fulfillmentPayload_opt: Option[ByteVector], 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))
### eclair-core/src/main/scala/fr/acinq/eclair/payment/send/PaymentInitiator.scala
@@ -338,8 +338,8 @@ object PaymentInitiator {
case _ => PaymentType.Standard
}
- 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)
+ def createPaymentSent(recipient: Recipient, preimage: ByteVector32, parts: Seq[PaymentSent.PaymentPart], fulfillmentPayload_opt: Option[ByteVector], remainingAttribution_opt: Option[ByteVector], startedAt: TimestampMilli): PaymentSent = {
+ PaymentSent(parentId, preimage, recipient.totalAmount, recipient.nodeId, parts, fulfillmentPayload_opt, remainingAttribution_opt, startedAt)
}
}
### eclair-core/src/main/scala/fr/acinq/eclair/payment/send/PaymentLifecycle.scala
@@ -115,20 +115,20 @@ class PaymentLifecycle(nodeParams: NodeParams, cfg: SendPaymentConfig, router: A
router ! Router.RouteDidRelay(d.route)
Metrics.PaymentAttempt.withTag(Tags.MultiPart, value = false).record(d.failures.size + 1)
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 {
- case Some(attribution) =>
- val attributionDetails = Sphinx.SuccessPacket.decrypt(Some(attribution), d.sharedSecrets)
- if (attributionDetails.holdTimes.nonEmpty) {
- context.system.eventStream.publish(Router.ReportedHoldTimes(attributionDetails.holdTimes))
- }
- attributionDetails.remainingAttribution_opt
- case None => None
+ val success_opt = fulfill match {
+ case HtlcResult.RemoteFulfill(f) =>
+ // When we're relaying a payment, the last hop of our route isn't the node that created the fulfillment
+ // payload: it is an intermediate node that only wrapped it, and we must forward it to our upstream node.
+ val lastSecretIsRecipient = cfg.upstream match {
+ case _: Upstream.Local => true
+ case _: Upstream.Hot.Channel => false
+ case _: Upstream.Hot.Trampoline => false
}
+ Some(Sphinx.SuccessPacket.decrypt(f.fulfillmentPayload_opt, f.attribution_opt, d.sharedSecrets, lastSecretIsRecipient))
case _: HtlcResult.OnChainFulfill => None
}
- myStop(d.request, Right(cfg.createPaymentSent(d.recipient, fulfill.paymentPreimage, p :: Nil, remainingAttribution_opt, start)))
+ success_opt.foreach(s => if (s.holdTimes.nonEmpty) context.system.eventStream.publish(Router.ReportedHoldTimes(s.holdTimes)))
+ myStop(d.request, Right(cfg.createPaymentSent(d.recipient, fulfill.paymentPreimage, p :: Nil, success_opt.flatMap(_.fulfillmentPayload_opt), success_opt.flatMap(_.remainingAttribution_opt), start)))
case Event(RES_ADD_SETTLED(_, _, _, fail: HtlcResult.Fail), d: WaitingForComplete) =>
fail match {
### eclair-core/src/main/scala/fr/acinq/eclair/payment/send/TrampolinePaymentLifecycle.scala
@@ -133,7 +133,12 @@ object TrampolinePaymentLifecycle {
val holdTimes = fulfill match {
case HtlcResult.RemoteFulfill(updateFulfill) =>
updateFulfill.attribution_opt match {
- case Some(attribution) => Sphinx.SuccessPacket.decrypt(Some(attribution), outerOnionSecrets).holdTimes
+ // Note that we only use the outer onion shared secrets here: this actor only exists to help other
+ // implementations run cross-compatibility tests, so we don't bother fully decrypting the fulfillment
+ // payload, which would require appending the trampoline onion shared secrets (see Sphinx.SuccessPacket.decrypt).
+ // This means that PaymentSent below contains an encrypted payload and that hold times may be missing
+ // when a fulfillment payload is used.
+ case Some(attribution) => Sphinx.SuccessPacket.decrypt(updateFulfill.fulfillmentPayload_opt, Some(attribution), outerOnionSecrets).holdTimes
case None => Nil
}
case _: HtlcResult.OnChainFulfill => Nil
@@ -241,7 +246,7 @@ class TrampolinePaymentLifecycle private(nodeParams: NodeParams,
waitForSettlement(remaining - 1, attemptNumber, part +: fulfilledParts)
} else {
context.log.info("trampoline payment succeeded")
- cmd.replyTo ! PaymentSent(cmd.paymentId, fulfill.paymentPreimage, totalAmount, cmd.invoice.nodeId, part +: fulfilledParts, None, startedAt)
+ cmd.replyTo ! PaymentSent(cmd.paymentId, fulfill.paymentPreimage, totalAmount, cmd.invoice.nodeId, part +: fulfilledParts, fulfill.fulfillmentPayload_opt, None, startedAt)
Behaviors.stopped
}
case fail: HtlcResult.Fail =>
### eclair-core/src/main/scala/fr/acinq/eclair/wire/internal/CommandCodecs.scala
@@ -24,6 +24,7 @@ import fr.acinq.eclair.wire.protocol.CommonCodecs._
import fr.acinq.eclair.wire.protocol.FailureMessageCodecs._
import fr.acinq.eclair.wire.protocol._
import scodec.Codec
+import scodec.bits.ByteVector
import scodec.codecs._
import shapeless.{::, HNil}
@@ -59,12 +60,13 @@ object CommandCodecs {
("replyTo_opt" | provide(Option.empty[ActorRef]))).map {
case id :: r :: downstreamAttribution_opt :: htlcReceivedAt_opt :: commit :: replyTo_opt :: HNil =>
val attribution_opt = htlcReceivedAt_opt.map(receivedAt => FulfillAttributionData(receivedAt, None, downstreamAttribution_opt))
- CMD_FULFILL_HTLC(id, r, attribution_opt, commit, replyTo_opt)
+ CMD_FULFILL_HTLC(id, r, None, attribution_opt, commit, replyTo_opt)
}.decodeOnly
private val cmdFulfillWithoutAttributionCodec: Codec[CMD_FULFILL_HTLC] =
(("id" | int64) ::
("r" | bytes32) ::
+ ("fulfillmentPayload_opt" | provide(Option.empty[ByteVector])) ::
("attribution_opt" | provide(Option.empty[FulfillAttributionData])) ::
("commit" | provide(false)) ::
("replyTo_opt" | provide(Option.empty[ActorRef]))).as[CMD_FULFILL_HTLC]
@@ -74,9 +76,18 @@ object CommandCodecs {
("trampolineReceivedAt_opt" | optional(bool8, uint64overflow.as[TimestampMilli])) ::
("downstreamAttribution_opt" | optional(bool8, bytes(Sphinx.Attribution.totalLength)))).as[FulfillAttributionData]
- private val cmdFullfillCodec: Codec[CMD_FULFILL_HTLC] =
+ private val cmdFullfillWithoutFulfillmentPayloadCodec: Codec[CMD_FULFILL_HTLC] =
(("id" | int64) ::
("r" | bytes32) ::
+ ("fulfillmentPayload_opt" | provide(Option.empty[ByteVector])) ::
+ ("attribution_opt" | optional(bool8, fulfillAttributionCodec)) ::
+ ("commit" | provide(false)) ::
+ ("replyTo_opt" | provide(Option.empty[ActorRef]))).as[CMD_FULFILL_HTLC]
+
+ private val cmdFulfillCodec: Codec[CMD_FULFILL_HTLC] =
+ (("id" | int64) ::
+ ("r" | bytes32) ::
+ ("fulfillmentPayload_opt" | optional(bool8, variableSizeBytes(uint16, bytes))) ::
("attribution_opt" | optional(bool8, fulfillAttributionCodec)) ::
("commit" | provide(false)) ::
("replyTo_opt" | provide(Option.empty[ActorRef]))).as[CMD_FULFILL_HTLC]
@@ -144,7 +155,8 @@ object CommandCodecs {
val cmdCodec: Codec[HtlcSettlementCommand] = discriminated[HtlcSettlementCommand].by(uint16)
// NB: order matters!
- .typecase(8, cmdFullfillCodec)
+ .typecase(9, cmdFulfillCodec)
+ .typecase(8, cmdFullfillWithoutFulfillmentPayloadCodec)
.typecase(7, cmdFailCodec)
.typecase(6, cmdFulfillWithPartialAttributionCodec)
.typecase(5, cmdFailWithPartialAttributionCodec)
### eclair-core/src/main/scala/fr/acinq/eclair/wire/protocol/HtlcTlv.scala
@@ -67,10 +67,16 @@ sealed trait UpdateFulfillHtlcTlv extends Tlv
object UpdateFulfillHtlcTlv {
case class AttributionData(data: ByteVector) extends UpdateFulfillHtlcTlv
+ /** The recipient may include an encrypted arbitrary payload that will be returned to the sender. */
+ case class FulfillmentPayload(data: ByteVector) extends UpdateFulfillHtlcTlv
+
private val attributionData: Codec[AttributionData] = (("length" | constant(hex"fd0398")) :: ("data" | bytes(Sphinx.Attribution.totalLength))).as[AttributionData]
+ private val fulfillmentPayload: Codec[FulfillmentPayload] = tlvField(bytes.as[FulfillmentPayload])
+
val updateFulfillHtlcTlvCodec: Codec[TlvStream[UpdateFulfillHtlcTlv]] = tlvStream(discriminated[UpdateFulfillHtlcTlv].by(varint)
.typecase(UInt64(1), attributionData)
+ .typecase(UInt64(3), fulfillmentPayload)
)
}
### eclair-core/src/main/scala/fr/acinq/eclair/wire/protocol/LightningMessageTypes.scala
@@ -580,6 +580,7 @@ case class UpdateFulfillHtlc(channelId: ByteVector32,
paymentPreimage: ByteVector32,
tlvStream: TlvStream[UpdateFulfillHtlcTlv] = TlvStream.empty) extends HtlcMessage with UpdateMessage with HasChannelId with HtlcSettlementMessage {
val attribution_opt: Option[ByteVector] = tlvStream.get[UpdateFulfillHtlcTlv.AttributionData].map(_.data)
+ val fulfillmentPayload_opt: Option[ByteVector] = tlvStream.get[UpdateFulfillHtlcTlv.FulfillmentPayload].map(_.data)
}
case class UpdateFailHtlc(channelId: ByteVector32,
### eclair-core/src/test/scala/fr/acinq/eclair/channel/CommitmentsSpec.scala
@@ -24,7 +24,6 @@ import fr.acinq.eclair.blockchain.fee._
import fr.acinq.eclair.channel.ChannelSpendSignature.IndividualSignature
import fr.acinq.eclair.channel.states.ChannelStateTestsBase
import fr.acinq.eclair.crypto.ShaChain
-import fr.acinq.eclair.reputation.Reputation
import fr.acinq.eclair.transactions.Transactions.ZeroFeeHtlcTxAnchorOutputsCommitmentFormat
import fr.acinq.eclair.transactions.{CommitmentSpec, Transactions}
import fr.acinq.eclair.wire.protocol._
@@ -114,7 +113,7 @@ class CommitmentsSpec extends TestKitBaseClass with FixtureAnyFunSuiteLike with
assert(bc4.availableBalanceForSend == b)
assert(bc4.availableBalanceForReceive == a - p - htlcOutputFee)
- val cmdFulfill = CMD_FULFILL_HTLC(0, payment_preimage, None)
+ val cmdFulfill = CMD_FULFILL_HTLC(0, payment_preimage, None, None)
val Right((bc5, fulfill)) = bc4.sendFulfill(cmdFulfill, bob.underlyingActor.nodeParams.privateKey, useAttributionData = false)
assert(bc5.availableBalanceForSend == b + p) // as soon as we have the fulfill, the balance increases
assert(bc5.availableBalanceForReceive == a - p - htlcOutputFee)
@@ -317,7 +316,7 @@ class CommitmentsSpec extends TestKitBaseClass with FixtureAnyFunSuiteLike with
assert(ac8.availableBalanceForSend == a - p1 - htlcOutputFee - p2 - htlcOutputFee - htlcOutputFee)
assert(ac8.availableBalanceForReceive == b - p3)
- val cmdFulfill1 = CMD_FULFILL_HTLC(0, payment_preimage1, None)
+ val cmdFulfill1 = CMD_FULFILL_HTLC(0, payment_preimage1, None, None)
val Right((bc8, fulfill1)) = bc7.sendFulfill(cmdFulfill1, bob.underlyingActor.nodeParams.privateKey, useAttributionData = false)
assert(bc8.availableBalanceForSend == b + p1 - p3) // as soon as we have the fulfill, the balance increases
assert(bc8.availableBalanceForReceive == a - p1 - htlcOutputFee - p2 - htlcOutputFee - htlcOutputFee)
@@ -327,7 +326,7 @@ class CommitmentsSpec extends TestKitBaseClass with FixtureAnyFunSuiteLike with
assert(bc9.availableBalanceForSend == b + p1 - p3)
assert(bc9.availableBalanceForReceive == a - p1 - htlcOutputFee - p2 - htlcOutputFee - htlcOutputFee) // a's balance won't return to previous before she acknowledges the fail
- val cmdFulfill3 = CMD_FULFILL_HTLC(0, payment_preimage3, None)
+ val cmdFulfill3 = CMD_FULFILL_HTLC(0, payment_preimage3, None, None)
val Right((ac9, fulfill3)) = ac8.sendFulfill(cmdFulfill3, alice.underlyingActor.nodeParams.privateKey, useAttributionData = false)
assert(ac9.availableBalanceForSend == a - p1 - htlcOutputFee - p2 - htlcOutputFee + p3) // as soon as we have the fulfill, the balance increases
assert(ac9.availableBalanceForReceive == b - p3)
### eclair-core/src/test/scala/fr/acinq/eclair/channel/HelpersSpec.scala
@@ -83,9 +83,9 @@ class HelpersSpec extends TestKitBaseClass with AnyFunSuiteLike with ChannelStat
crossSign(bob, alice, bob2alice, alice2bob)
// Alice and Bob both know the preimage for only one of the two HTLCs they received.
- alice ! CMD_FULFILL_HTLC(htlcb2.id, rb2, None, replyTo_opt = Some(probe.ref))
+ alice ! CMD_FULFILL_HTLC(htlcb2.id, rb2, None, None, replyTo_opt = Some(probe.ref))
probe.expectMsgType[CommandSuccess[CMD_FULFILL_HTLC]]
- bob ! CMD_FULFILL_HTLC(htlca2.id, ra2, None, replyTo_opt = Some(probe.ref))
+ bob ! CMD_FULFILL_HTLC(htlca2.id, ra2, None, None, replyTo_opt = Some(probe.ref))
probe.expectMsgType[CommandSuccess[CMD_FULFILL_HTLC]]
// Alice publishes her commitment.
### eclair-core/src/test/scala/fr/acinq/eclair/channel/publish/ReplaceableTxPublisherSpec.scala
@@ -1212,7 +1212,7 @@ class ReplaceableTxPublisherSpec extends TestKitBaseClass with AnyFunSuiteLike w
crossSign(alice, bob, alice2bob, bob2alice)
val (r, htlc) = addHtlc(4_000_000 msat, bob, alice, bob2alice, alice2bob)
crossSign(bob, alice, bob2alice, alice2bob)
- probe.send(alice, CMD_FULFILL_HTLC(htlc.id, r, None, replyTo_opt = Some(probe.ref)))
+ probe.send(alice, CMD_FULFILL_HTLC(htlc.id, r, None, None, replyTo_opt = Some(probe.ref)))
probe.expectMsgType[CommandSuccess[CMD_FULFILL_HTLC]]
// Force-close channel.
@@ -1323,7 +1323,7 @@ class ReplaceableTxPublisherSpec extends TestKitBaseClass with AnyFunSuiteLike w
crossSign(alice, bob, alice2bob, bob2alice)
val (r, htlc) = addHtlc(incomingHtlcAmount, bob, alice, bob2alice, alice2bob)
crossSign(bob, alice, bob2alice, alice2bob)
- probe.send(alice, CMD_FULFILL_HTLC(htlc.id, r, None, replyTo_opt = Some(probe.ref)))
+ probe.send(alice, CMD_FULFILL_HTLC(htlc.id, r, None, None, replyTo_opt = Some(probe.ref)))
probe.expectMsgType[CommandSuccess[CMD_FULFILL_HTLC]]
// Force-close channel and verify txs sent to watcher.
@@ -1780,7 +1780,7 @@ class ReplaceableTxPublisherSpec extends TestKitBaseClass with AnyFunSuiteLike w
crossSign(alice, bob, alice2bob, bob2alice)
val (r, htlc) = addHtlc(20_000_000 msat, bob, alice, bob2alice, alice2bob)
crossSign(bob, alice, bob2alice, alice2bob)
- probe.send(alice, CMD_FULFILL_HTLC(htlc.id, r, None, replyTo_opt = Some(probe.ref)))
+ probe.send(alice, CMD_FULFILL_HTLC(htlc.id, r, None, None, replyTo_opt = Some(probe.ref)))
probe.expectMsgType[CommandSuccess[CMD_FULFILL_HTLC]]
// Force-close channel.
@@ -1855,7 +1855,7 @@ class ReplaceableTxPublisherSpec extends TestKitBaseClass with AnyFunSuiteLike w
} else {
crossSign(alice, bob, alice2bob, bob2alice)
}
- probe.send(alice, CMD_FULFILL_HTLC(htlc.id, r, None, replyTo_opt = Some(probe.ref)))
+ probe.send(alice, CMD_FULFILL_HTLC(htlc.id, r, None, None, replyTo_opt = Some(probe.ref)))
probe.expectMsgType[CommandSuccess[CMD_FULFILL_HTLC]]
// Force-close channel and verify txs sent to watcher.
### eclair-core/src/test/scala/fr/acinq/eclair/channel/states/ChannelStateTestsHelperMethods.scala
@@ -534,7 +534,7 @@ trait ChannelStateTestsBase extends Assertions with Eventually {
}
def fulfillHtlc(id: Long, preimage: ByteVector32, s: TestFSMRef[ChannelState, ChannelData, Channel], r: TestFSMRef[ChannelState, ChannelData, Channel], s2r: TestProbe, r2s: TestProbe): Unit = {
- s ! CMD_FULFILL_HTLC(id, preimage, None)
+ s ! CMD_FULFILL_HTLC(id, preimage, None, None)
val fulfill = s2r.expectMsgType[UpdateFulfillHtlc]
s2r.forward(r)
eventually(assert(r.stateData.asInstanceOf[ChannelDataWithCommitments].commitments.changes.remoteChanges.proposed.contains(fulfill)))
### eclair-core/src/test/scala/fr/acinq/eclair/channel/states/e/NormalQuiescentStateSpec.scala
@@ -190,7 +190,7 @@ class NormalQuiescentStateSpec extends TestKitBaseClass with FixtureAnyFunSuiteL
val (preimage, add) = addHtlc(50_000_000 msat, bob, alice, bob2alice, alice2bob)
val cmd = c match {
- case FulfillHtlc => CMD_FULFILL_HTLC(add.id, preimage, None)
+ case FulfillHtlc => CMD_FULFILL_HTLC(add.id, preimage, None, None)
case FailHtlc => CMD_FAIL_HTLC(add.id, FailureReason.EncryptedDownstreamFailure(randomBytes(252), None), None)
}
crossSign(bob, alice, bob2alice, alice2bob)
@@ -482,7 +482,7 @@ class NormalQuiescentStateSpec extends TestKitBaseClass with FixtureAnyFunSuiteL
bob2blockchain.expectNoMessage(100 millis)
// bob receives the fulfill for htlc, which is ignored because the channel is quiescent
- val fulfillHtlc = CMD_FULFILL_HTLC(add.id, preimage, None)
+ val fulfillHtlc = CMD_FULFILL_HTLC(add.id, preimage, None, None)
safeSend(bob, Seq(fulfillHtlc))
// the HTLC timeout from alice is near, bob needs to close the channel to avoid an on-chain race condition
### eclair-core/src/test/scala/fr/acinq/eclair/channel/states/e/NormalStateSpec.scala
@@ -43,7 +43,7 @@ import fr.acinq.eclair.testutils.PimpTestProbe.convert
import fr.acinq.eclair.transactions.DirectedHtlc.{incoming, outgoing}
import fr.acinq.eclair.transactions.Transactions
import fr.acinq.eclair.transactions.Transactions._
-import fr.acinq.eclair.wire.protocol.{AnnouncementSignatures, ChannelReestablish, ChannelReestablishTlv, ChannelUpdate, ClosingSigned, CommitSig, CommitSigTlv, Error, FailureMessageCodecs, FailureReason, Init, PermanentChannelFailure, RevokeAndAck, RevokeAndAckTlv, Shutdown, TemporaryNodeFailure, TlvStream, UpdateAddHtlc, UpdateFailHtlc, UpdateFailMalformedHtlc, UpdateFee, UpdateFulfillHtlc, Warning}
+import fr.acinq.eclair.wire.protocol.{AnnouncementSignatures, ChannelReestablish, ChannelReestablishTlv, ChannelUpdate, ClosingSigned, CommitSig, CommitSigTlv, Error, FailureMessageCodecs, FailureReason, Init, PermanentChannelFailure, RevokeAndAck, RevokeAndAckTlv, Shutdown, TemporaryNodeFailure, TlvStream, UpdateAddHtlc, UpdateFailHtlc, UpdateFailMalformedHtlc, UpdateFee, UpdateFulfillHtlc, UpdateFulfillHtlcTlv, Warning}
import org.scalatest.Inside.inside
import org.scalatest.funsuite.FixtureAnyFunSuiteLike
import org.scalatest.{Outcome, Tag}
@@ -1691,7 +1691,7 @@ class NormalStateSpec extends TestKitBaseClass with FixtureAnyFunSuiteLike with
// actual test begins
val initialState = bob.stateData.asInstanceOf[DATA_NORMAL]
- bob ! CMD_FULFILL_HTLC(htlc.id, r, None)
+ bob ! CMD_FULFILL_HTLC(htlc.id, r, None, None)
val fulfill = bob2alice.expectMsgType[UpdateFulfillHtlc]
awaitCond(bob.stateData == initialState.modify(_.commitments.changes.localChanges.proposed).using(_ :+ fulfill))
}
@@ -1718,7 +1718,7 @@ class NormalStateSpec extends TestKitBaseClass with FixtureAnyFunSuiteLike with
val r = randomBytes32()
val initialState = bob.stateData.asInstanceOf[DATA_NORMAL]
- val c = CMD_FULFILL_HTLC(42, r, None, replyTo_opt = Some(sender.ref))
+ val c = CMD_FULFILL_HTLC(42, r, None, None, replyTo_opt = Some(sender.ref))
bob ! c
sender.expectMsg(RES_FAILURE(c, UnknownHtlcId(channelId(bob), 42)))
assert(initialState == bob.stateData)
@@ -1732,7 +1732,7 @@ class NormalStateSpec extends TestKitBaseClass with FixtureAnyFunSuiteLike with
// actual test begins
val initialState = bob.stateData.asInstanceOf[DATA_NORMAL]
- val c = CMD_FULFILL_HTLC(htlc.id, ByteVector32.Zeroes, None, replyTo_opt = Some(sender.ref))
+ val c = CMD_FULFILL_HTLC(htlc.id, ByteVector32.Zeroes, None, None, replyTo_opt = Some(sender.ref))
bob ! c
sender.expectMsg(RES_FAILURE(c, InvalidHtlcPreimage(channelId(bob), 0)))
assert(initialState == bob.stateData)
@@ -1746,7 +1746,7 @@ class NormalStateSpec extends TestKitBaseClass with FixtureAnyFunSuiteLike with
// actual test begins
val initialState = bob.stateData.asInstanceOf[DATA_NORMAL]
- val c = CMD_FULFILL_HTLC(htlc.id, r, None, replyTo_opt = Some(sender.ref))
+ val c = CMD_FULFILL_HTLC(htlc.id, r, None, None, replyTo_opt = Some(sender.ref))
// this would be done automatically when the relayer calls safeSend
bob.underlyingActor.nodeParams.db.pendingCommands.addSettlementCommand(initialState.channelId, c)
bob ! c
@@ -1761,7 +1761,7 @@ class NormalStateSpec extends TestKitBaseClass with FixtureAnyFunSuiteLike with
val sender = TestProbe()
val initialState = bob.stateData.asInstanceOf[DATA_NORMAL]
- val c = CMD_FULFILL_HTLC(42, randomBytes32(), None, replyTo_opt = Some(sender.ref))
+ val c = CMD_FULFILL_HTLC(42, randomBytes32(), None, None, replyTo_opt = Some(sender.ref))
sender.send(bob, c) // this will fail
sender.expectMsg(RES_FAILURE(c, UnknownHtlcId(channelId(bob), 42)))
awaitCond(bob.underlyingActor.nodeParams.db.pendingCommands.listSettlementCommands(initialState.channelId).isEmpty)
@@ -1775,7 +1775,7 @@ class NormalStateSpec extends TestKitBaseClass with FixtureAnyFunSuiteLike with
val (r, htlc) = addHtlc(50000000 msat, alice, bob, alice2bob, bob2alice)
crossSign(alice, bob, alice2bob, bob2alice)
- bob ! CMD_FULFILL_HTLC(htlc.id, r, None)
+ bob ! CMD_FULFILL_HTLC(htlc.id, r, None, None)
val fulfill = bob2alice.expectMsgType[UpdateFulfillHtlc]
// actual test begins
@@ -1833,6 +1833,43 @@ class NormalStateSpec extends TestKitBaseClass with FixtureAnyFunSuiteLike with
alice2blockchain.expectWatchTxConfirmed(tx.txid)
}
+ test("recv UpdateFulfillHtlc (fulfillment payload)") { f =>
+ import f._
+ val (r, htlc) = addHtlc(50_000_000 msat, alice, bob, alice2bob, bob2alice)
+ crossSign(alice, bob, alice2bob, bob2alice)
+ bob2relayer.expectMsgType[RelayForward]
+
+ // actual test begins: bob includes a fulfillment payload of the maximum allowed size.
+ val payload = randomBytes(Sphinx.SuccessPacket.MAX_LENGTH)
+ alice ! UpdateFulfillHtlc(channelId(alice), htlc.id, r, TlvStream(UpdateFulfillHtlcTlv.FulfillmentPayload(payload)))
+ val forward = alice2relayer.expectMsgType[RES_ADD_SETTLED[Origin, HtlcResult.RemoteFulfill]]
+ assert(forward.htlc == htlc)
+ assert(forward.result.fulfillmentPayload_opt.contains(payload))
+ alice2bob.expectNoMessage(100 millis)
+ assert(alice.stateName == NORMAL)
+ }
+
+ test("recv UpdateFulfillHtlc (fulfillment payload too large)") { f =>
+ import f._
+ val (r, htlc) = addHtlc(50_000_000 msat, alice, bob, alice2bob, bob2alice)
+ crossSign(alice, bob, alice2bob, bob2alice)
+ bob2relayer.expectMsgType[RelayForward]
+ val tx = alice.signCommitTx()
+
+ // actual test begins: bob includes a fulfillment payload that is larger than what we would be able to relay.
+ val payload = randomBytes(Sphinx.SuccessPacket.MAX_LENGTH + 1)
+ alice ! UpdateFulfillHtlc(channelId(alice), htlc.id, r, TlvStream(UpdateFulfillHtlcTlv.FulfillmentPayload(payload)))
+ // We relay the preimage upstream before force-closing, to make sure that we don't lose funds.
+ val forward = alice2relayer.expectMsgType[RES_ADD_SETTLED[Origin, HtlcResult.RemoteFulfill]]
+ assert(forward.htlc == htlc)
+ // We truncate the payload, since we cannot relay a larger one upstream.
+ assert(forward.result.fulfillmentPayload_opt.contains(payload.take(Sphinx.SuccessPacket.MAX_LENGTH)))
+ // Our peer is not following the specification: we force-close to make that costly for them.
+ alice2bob.expectMsgType[Error]
+ awaitCond(alice.stateName == CLOSING)
+ alice2blockchain.expectFinalTxPublished(tx.txid)
+ }
+
test("recv UpdateFulfillHtlc (invalid preimage)") { f =>
import f._
val (_, htlc) = addHtlc(50000000 msat, alice, bob, alice2bob, bob2alice)
@@ -1858,7 +1895,7 @@ class NormalStateSpec extends TestKitBaseClass with FixtureAnyFunSuiteLike with
val (r, htlc) = addHtlc(50_000_000 msat, alice, bob, alice2bob, bob2alice)
crossSign(alice, bob, alice2bob, bob2alice)
- bob ! CMD_FULFILL_HTLC(htlc.id, r, None)
+ bob ! CMD_FULFILL_HTLC(htlc.id, r, None, None)
val fulfill = bob2alice.expectMsgType[UpdateFulfillHtlc]
bob2alice.forward(alice, fulfill)
awaitCond(alice.commitments.changes.remoteChanges.proposed.contains(fulfill))
@@ -1944,7 +1981,7 @@ class NormalStateSpec extends TestKitBaseClass with FixtureAnyFunSuiteLike with
crossSign(alice, bob, alice2bob, bob2alice)
// HTLC is fulfilled but alice doesn't send its revocation.
- bob ! CMD_FULFILL_HTLC(htlc.id, r, None)
+ bob ! CMD_FULFILL_HTLC(htlc.id, r, None, None)
bob ! CMD_SIGN()
bob2alice.expectMsgType[UpdateFulfillHtlc]
bob2alice.expectMsgType[CommitSig]
@@ -2707,7 +2744,7 @@ class NormalStateSpec extends TestKitBaseClass with FixtureAnyFunSuiteLike with
val htlcSuccessTx = bob.htlcTxs().head
assert(htlcSuccessTx.isInstanceOf[UnsignedHtlcSuccessTx])
- bob ! CMD_FULFILL_HTLC(htlc.id, r, None, commit = true)
+ bob ! CMD_FULFILL_HTLC(htlc.id, r, None, None, commit = true)
bob2alice.expectMsgType[UpdateFulfillHtlc]
bob2alice.expectMsgType[CommitSig]
bob ! CurrentBlockHeight(htlc.cltvExpiry.blockHeight - Bob.nodeParams.channelConf.fulfillSafetyBeforeTimeout.toInt)
@@ -2742,7 +2779,7 @@ class NormalStateSpec extends TestKitBaseClass with FixtureAnyFunSuiteLike with
val htlcSuccessTx = bob.htlcTxs().head
assert(htlcSuccessTx.isInstanceOf[UnsignedHtlcSuccessTx])
- bob ! CMD_FULFILL_HTLC(htlc.id, r, None, commit = false)
+ bob ! CMD_FULFILL_HTLC(htlc.id, r, None, None, commit = false)
bob2alice.expectMsgType[UpdateFulfillHtlc]
bob2alice.expectNoMessage(100 millis)
bob ! CurrentBlockHeight(htlc.cltvExpiry.blockHeight - Bob.nodeParams.channelConf.fulfillSafetyBeforeTimeout.toInt)
@@ -2777,7 +2814,7 @@ class NormalStateSpec extends TestKitBaseClass with FixtureAnyFunSuiteLike with
val htlcSuccessTx = bob.htlcTxs().head
assert(htlcSuccessTx.isInstanceOf[UnsignedHtlcSuccessTx])
- bob ! CMD_FULFILL_HTLC(htlc.id, r, None, commit = true)
+ bob ! CMD_FULFILL_HTLC(htlc.id, r, None, None, commit = true)
bob2alice.expectMsgType[UpdateFulfillHtlc]
bob2alice.forward(alice)
bob2alice.expectMsgType[CommitSig]
### eclair-core/src/test/scala/fr/acinq/eclair/channel/states/e/OfflineStateSpec.scala
@@ -558,7 +558,7 @@ class OfflineStateSpec extends TestKitBaseClass with FixtureAnyFunSuiteLike with
disconnect(alice, bob)
// We simulate a pending fulfill
- bob.underlyingActor.nodeParams.db.pendingCommands.addSettlementCommand(initialState.channelId, CMD_FULFILL_HTLC(htlc.id, r, None, commit = true))
+ bob.underlyingActor.nodeParams.db.pendingCommands.addSettlementCommand(initialState.channelId, CMD_FULFILL_HTLC(htlc.id, r, None, None, commit = true))
// then we reconnect them
reconnect(alice, bob, alice2bob, bob2alice)
@@ -589,7 +589,7 @@ class OfflineStateSpec extends TestKitBaseClass with FixtureAnyFunSuiteLike with
disconnect(alice, bob)
// We simulate a pending fulfill
- bob.underlyingActor.nodeParams.db.pendingCommands.addSettlementCommand(initialState.channelId, CMD_FULFILL_HTLC(htlc.id, r, None, commit = true))
+ bob.underlyingActor.nodeParams.db.pendingCommands.addSettlementCommand(initialState.channelId, CMD_FULFILL_HTLC(htlc.id, r, None, None, commit = true))
// then we reconnect them
reconnect(alice, bob, alice2bob, bob2alice)
@@ -627,7 +627,7 @@ class OfflineStateSpec extends TestKitBaseClass with FixtureAnyFunSuiteLike with
// We simulate a pending fulfill on that HTLC but not relayed.
// When it is close to expiring upstream, we should close the channel.
- bob.underlyingActor.nodeParams.db.pendingCommands.addSettlementCommand(initialState.channelId, CMD_FULFILL_HTLC(htlc.id, r, None, commit = true))
+ bob.underlyingActor.nodeParams.db.pendingCommands.addSettlementCommand(initialState.channelId, CMD_FULFILL_HTLC(htlc.id, r, None, None, commit = true))
bob ! CurrentBlockHeight(htlc.cltvExpiry.blockHeight - bob.underlyingActor.nodeParams.channelConf.fulfillSafetyBeforeTimeout.toInt)
val ChannelErrorOccurred(_, _, _, LocalError(err), isFatal) = listener.expectMsgType[ChannelErrorOccurred]
### eclair-core/src/test/scala/fr/acinq/eclair/channel/states/f/ShutdownStateSpec.scala
@@ -35,8 +35,9 @@ import fr.acinq.eclair.payment.send.SpontaneousRecipient
import fr.acinq.eclair.reputation.Reputation
import fr.acinq.eclair.testutils.PimpTestProbe.convert
import fr.acinq.eclair.transactions.Transactions._
-import fr.acinq.eclair.wire.protocol.{AnnouncementSignatures, ChannelReestablish, ChannelUpdate, ClosingComplete, ClosingSig, ClosingSigned, CommitSig, Error, FailureMessageCodecs, FailureReason, Init, PermanentChannelFailure, RevokeAndAck, Shutdown, UpdateAddHtlc, UpdateFailHtlc, UpdateFailMalformedHtlc, UpdateFee, UpdateFulfillHtlc}
-import fr.acinq.eclair.{BlockHeight, CltvExpiry, CltvExpiryDelta, MilliSatoshiLong, TestConstants, TestKitBaseClass, randomBytes32, randomKey}
+import fr.acinq.eclair.wire.protocol.{AnnouncementSignatures, ChannelReestablish, ChannelUpdate, ClosingComplete, ClosingSig, ClosingSigned, CommitSig, Error, FailureMessageCodecs, FailureReason, Init, PermanentChannelFailure, RevokeAndAck, Shutdown, TlvStream, UpdateAddHtlc, UpdateFailHtlc, UpdateFailMalformedHtlc, UpdateFee, UpdateFulfillHtlc, UpdateFulfillHtlcTlv}
+import fr.acinq.eclair.crypto.Sphinx
+import fr.acinq.eclair.{BlockHeight, CltvExpiry, CltvExpiryDelta, MilliSatoshiLong, TestConstants, TestKitBaseClass, randomBytes, randomBytes32, randomKey}
import org.scalatest.funsuite.FixtureAnyFunSuiteLike
import org.scalatest.{Outcome, Tag}
import scodec.bits.ByteVector
@@ -154,7 +155,7 @@ class ShutdownStateSpec extends TestKitBaseClass with FixtureAnyFunSuiteLike wit
test("recv CMD_FULFILL_HTLC") { f =>
import f._
val initialState = bob.stateData.asInstanceOf[DATA_SHUTDOWN]
- bob ! CMD_FULFILL_HTLC(0, r1, None)
+ bob ! CMD_FULFILL_HTLC(0, r1, None, None)
val fulfill = bob2alice.expectMsgType[UpdateFulfillHtlc]
awaitCond(bob.stateData == initialState.modify(_.commitments.changes.localChanges.proposed).using(_ :+ fulfill)
)
@@ -163,7 +164,7 @@ class ShutdownStateSpec extends TestKitBaseClass with FixtureAnyFunSuiteLike wit
test("recv CMD_FULFILL_HTLC (taproot)", Tag(ChannelStateTestsTags.SimpleClose), Tag(ChannelStateTestsTags.OptionSimpleTaproot)) { f =>
import f._
val initialState = bob.stateData.asInstanceOf[DATA_SHUTDOWN]
- bob ! CMD_FULFILL_HTLC(0, r1, None)
+ bob ! CMD_FULFILL_HTLC(0, r1, None, None)
val fulfill = bob2alice.expectMsgType[UpdateFulfillHtlc]
awaitCond(bob.stateData == initialState.modify(_.commitments.changes.localChanges.proposed).using(_ :+ fulfill))
}
@@ -172,7 +173,7 @@ class ShutdownStateSpec extends TestKitBaseClass with FixtureAnyFunSuiteLike wit
import f._
val sender = TestProbe()
val initialState = bob.stateData.asInstanceOf[DATA_SHUTDOWN]
- bob ! CMD_FULFILL_HTLC(42, randomBytes32(), None, replyTo_opt = Some(sender.ref))
+ bob ! CMD_FULFILL_HTLC(42, randomBytes32(), None, None, replyTo_opt = Some(sender.ref))
sender.expectMsgType[RES_FAILURE[CMD_FULFILL_HTLC, UnknownHtlcId]]
assert(initialState == bob.stateData)
}
@@ -181,7 +182,7 @@ class ShutdownStateSpec extends TestKitBaseClass with FixtureAnyFunSuiteLike wit
import f._
val sender = TestProbe()
val initialState = bob.stateData.asInstanceOf[DATA_SHUTDOWN]
- val c = CMD_FULFILL_HTLC(1, ByteVector32.Zeroes, None, replyTo_opt = Some(sender.ref))
+ val c = CMD_FULFILL_HTLC(1, ByteVector32.Zeroes, None, None, replyTo_opt = Some(sender.ref))
bob ! c
sender.expectMsg(RES_FAILURE(c, InvalidHtlcPreimage(channelId(bob), 1)))
@@ -194,7 +195,7 @@ class ShutdownStateSpec extends TestKitBaseClass with FixtureAnyFunSuiteLike wit
// actual test begins
val initialState = bob.stateData.asInstanceOf[DATA_SHUTDOWN]
- val c = CMD_FULFILL_HTLC(0, r1, None, replyTo_opt = Some(sender.ref))
+ val c = CMD_FULFILL_HTLC(0, r1, None, None, replyTo_opt = Some(sender.ref))
// this would be done automatically when the relayer calls safeSend
bob.underlyingActor.nodeParams.db.pendingCommands.addSettlementCommand(initialState.channelId, c)
bob ! c
@@ -209,7 +210,7 @@ class ShutdownStateSpec extends TestKitBaseClass with FixtureAnyFunSuiteLike wit
val sender = TestProbe()
val initialState = bob.stateData.asInstanceOf[DATA_SHUTDOWN]
- val c = CMD_FULFILL_HTLC(42, randomBytes32(), None, replyTo_opt = Some(sender.ref))
+ val c = CMD_FULFILL_HTLC(42, randomBytes32(), None, None, replyTo_opt = Some(sender.ref))
sender.send(bob, c) // this will fail
sender.expectMsg(RES_FAILURE(c, UnknownHtlcId(channelId(bob), 42)))
awaitCond(bob.underlyingActor.nodeParams.db.pendingCommands.listSettlementCommands(initialState.channelId).isEmpty)
@@ -223,6 +224,21 @@ class ShutdownStateSpec extends TestKitBaseClass with FixtureAnyFunSuiteLike wit
awaitCond(alice.stateData.asInstanceOf[DATA_SHUTDOWN].commitments == initialState.commitments.modify(_.changes.remoteChanges).setTo(initialState.commitments.changes.remoteChanges.copy(initialState.commitments.changes.remoteChanges.proposed :+ fulfill)))
}
+ test("recv UpdateFulfillHtlc (fulfillment payload too large)") { f =>
+ import f._
+ val tx = alice.signCommitTx()
+
+ // Our peer includes a fulfillment payload that is larger than what is specified in the BOLTs.
+ val payload = randomBytes(Sphinx.SuccessPacket.MAX_LENGTH + 1)
+ alice ! UpdateFulfillHtlc(ByteVector32.Zeroes, 0, r1, TlvStream(UpdateFulfillHtlcTlv.FulfillmentPayload(payload)))
+ // We relay the preimage upstream before force-closing, to make sure that we don't lose funds.
+ val forward = alice2relayer.expectMsgType[RES_ADD_SETTLED[Origin, HtlcResult.RemoteFulfill]]
+ assert(forward.result.fulfillmentPayload_opt.contains(payload.take(Sphinx.SuccessPacket.MAX_LENGTH)))
+ alice2bob.expectMsgType[Error]
+ awaitCond(alice.stateName == CLOSING)
+ alice2blockchain.expectFinalTxPublished(tx.txid)
+ }
+
test("recv UpdateFulfillHtlc (unknown htlc id)") { f =>
import f._
val tx = alice.signCommitTx()
@@ -357,7 +373,7 @@ class ShutdownStateSpec extends TestKitBaseClass with FixtureAnyFunSuiteLike wit
import f._
val sender = TestProbe()
// we need to have something to sign so we first send a fulfill and acknowledge (=sign) it
- bob ! CMD_FULFILL_HTLC(0, r1, None)
+ bob ! CMD_FULFILL_HTLC(0, r1, None, None)
bob2alice.expectMsgType[UpdateFulfillHtlc]
bob2alice.forward(alice)
bob ! CMD_SIGN(replyTo_opt = Some(sender.ref))
@@ -373,7 +389,7 @@ class ShutdownStateSpec extends TestKitBaseClass with FixtureAnyFunSuiteLike wit
test("recv CMD_SIGN (taproot)", Tag(ChannelStateTestsTags.SimpleClose), Tag(ChannelStateTestsTags.OptionSimpleTaproot)) { f =>
import f._
val sender = TestProbe()
- bob ! CMD_FULFILL_HTLC(0, r1, None)
+ bob ! CMD_FULFILL_HTLC(0, r1, None, None)
bob2alice.expectMsgType[UpdateFulfillHtlc]
bob2alice.forward(alice)
bob ! CMD_SIGN(replyTo_opt = Some(sender.ref))
@@ -397,7 +413,7 @@ class ShutdownStateSpec extends TestKitBaseClass with FixtureAnyFunSuiteLike wit
test("recv CMD_SIGN (while waiting for RevokeAndAck)") { f =>
import f._
val sender = TestProbe()
- bob ! CMD_FULFILL_HTLC(0, r1, None)
+ bob ! CMD_FULFILL_HTLC(0, r1, None, None)
bob2alice.expectMsgType[UpdateFulfillHtlc]
bob ! CMD_SIGN(replyTo_opt = Some(sender.ref))
sender.expectMsgType[RES_SUCCESS[CMD_SIGN]]
@@ -413,7 +429,7 @@ class ShutdownStateSpec extends TestKitBaseClass with FixtureAnyFunSuiteLike wit
test("recv CommitSig") { f =>
import f._
- bob ! CMD_FULFILL_HTLC(0, r1, None)
+ bob ! CMD_FULFILL_HTLC(0, r1, None, None)
bob2alice.expectMsgType[UpdateFulfillHtlc]
bob2alice.forward(alice)
bob ! CMD_SIGN()
@@ -484,7 +500,7 @@ class ShutdownStateSpec extends TestKitBaseClass with FixtureAnyFunSuiteLike wit
test("recv RevokeAndAck (invalid preimage)") { f =>
import f._
val tx = bob.signCommitTx()
- bob ! CMD_FULFILL_HTLC(0, r1, None)
+ bob ! CMD_FULFILL_HTLC(0, r1, None, None)
bob2alice.expectMsgType[UpdateFulfillHtlc]
bob2alice.forward(alice)
bob ! CMD_SIGN()
### eclair-core/src/test/scala/fr/acinq/eclair/channel/states/h/ClosingStateSpec.scala
@@ -290,7 +290,7 @@ class ClosingStateSpec extends TestKitBaseClass with FixtureAnyFunSuiteLike with
// actual test starts here
val sender = TestProbe()
- val c = CMD_FULFILL_HTLC(42, randomBytes32(), None, replyTo_opt = Some(sender.ref))
+ val c = CMD_FULFILL_HTLC(42, randomBytes32(), None, None, replyTo_opt = Some(sender.ref))
alice ! c
sender.expectMsg(RES_FAILURE(c, UnknownHtlcId(channelId(alice), 42)))
}
@@ -385,7 +385,7 @@ class ClosingStateSpec extends TestKitBaseClass with FixtureAnyFunSuiteLike with
crossSign(alice, bob, alice2bob, bob2alice)
// Bob has the preimage for those HTLCs, but Alice force-closes before receiving it.
- bob ! CMD_FULFILL_HTLC(htlc1.id, preimage, None)
+ bob ! CMD_FULFILL_HTLC(htlc1.id, preimage, None, None)
bob2alice.expectMsgType[UpdateFulfillHtlc] // ignored
val (lcp, closingTxs) = localClose(alice, alice2blockchain, htlcTimeoutCount = 2)
assert(lcp.htlcOutputs.size == 2)
@@ -442,7 +442,7 @@ class ClosingStateSpec extends TestKitBaseClass with FixtureAnyFunSuiteLike with
val (preimage, htlc) = addHtlc(50_000_000 msat, alice, bob, alice2bob, bob2alice)
crossSign(alice, bob, alice2bob, bob2alice)
// Bob has the preimage, but Alice force-closes before receiving it.
- bob ! CMD_FULFILL_HTLC(htlc.id, preimage, None)
+ bob ! CMD_FULFILL_HTLC(htlc.id, preimage, None, None)
bob2alice.expectMsgType[UpdateFulfillHtlc] // ignored
val (lcp, _) = localClose(alice, alice2blockchain, htlcTimeoutCount = 1)
@@ -481,7 +481,7 @@ class ClosingStateSpec extends TestKitBaseClass with FixtureAnyFunSuiteLike with
crossSign(alice, bob, alice2bob, bob2alice)
// Bob has the preimage for those HTLCs, but he force-closes before Alice receives it.
- bob ! CMD_FULFILL_HTLC(htlc1.id, preimage, None)
+ bob ! CMD_FULFILL_HTLC(htlc1.id, preimage, None, None)
bob2alice.expectMsgType[UpdateFulfillHtlc] // ignored
val (rcp, closingTxs) = localClose(bob, bob2blockchain, htlcSuccessCount = 2)
@@ -540,7 +540,7 @@ class ClosingStateSpec extends TestKitBaseClass with FixtureAnyFunSuiteLike with
// At that point, the HTLCs are not in Alice's commitment anymore.
// But Bob has not revoked his commitment yet that contains them.
bob.setState(NORMAL, bobStateWithHtlc)
- bob ! CMD_FULFILL_HTLC(htlc1.id, preimage, None)
+ bob ! CMD_FULFILL_HTLC(htlc1.id, preimage, None, None)
bob2alice.expectMsgType[UpdateFulfillHtlc] // ignored
// Bob claims the htlc outputs from his previous commit tx using its preimage.
@@ -636,7 +636,7 @@ class ClosingStateSpec extends TestKitBaseClass with FixtureAnyFunSuiteLike with
// Bob doesn't have the preimage yet for any of those HTLCs.
assert(closingTxs.htlcTxs.isEmpty)
// Bob receives the preimage for the first two HTLCs.
- bob ! CMD_FULFILL_HTLC(htlc1.id, preimage, None)
+ bob ! CMD_FULFILL_HTLC(htlc1.id, preimage, None, None)
val htlcSuccessTxs = {
val htlcSuccess = (0 until 2).map(_ => bob2blockchain.expectReplaceableTxPublished[HtlcSuccessTx])
assert(htlcSuccess.map(_.htlcId).toSet == Set(htlc1.id, htlc2.id))
@@ -931,7 +931,7 @@ class ClosingStateSpec extends TestKitBaseClass with FixtureAnyFunSuiteLike with
assert(aliceCommitTx.txOut.size == 5) // 2 main outputs + 2 anchor outputs + 1 htlc
// alice fulfills the HTLC but bob doesn't receive the signature
- alice ! CMD_FULFILL_HTLC(htlc.id, r, None, commit = true)
+ alice ! CMD_FULFILL_HTLC(htlc.id, r, None, None, commit = true)
alice2bob.expectMsgType[UpdateFulfillHtlc]
alice2bob.forward(bob)
inside(bob2relayer.expectMsgType[RES_ADD_SETTLED[Origin, HtlcResult.Fulfill]]) { settled =>
@@ -1002,7 +1002,7 @@ class ClosingStateSpec extends TestKitBaseClass with FixtureAnyFunSuiteLike with
assert(alice.stateName == CLOSING)
// Alice receives the preimage for the first HTLC from downstream; she can now claim the corresponding HTLC output.
- alice ! CMD_FULFILL_HTLC(htlc1.id, r1, None, commit = true)
+ alice ! CMD_FULFILL_HTLC(htlc1.id, r1, None, None, commit = true)
val htlcSuccess = alice2blockchain.expectReplaceableTxPublished[HtlcSuccessTx](ConfirmationTarget.Absolute(htlc1.cltvExpiry.blockHeight))
assert(htlcSuccess.preimage == r1)
Transaction.correctlySpends(htlcSuccess.sign(), closingState.commitTx :: Nil, ScriptFlags.STANDARD_SCRIPT_VERIFY_FLAGS)
@@ -1130,7 +1130,7 @@ class ClosingStateSpec extends TestKitBaseClass with FixtureAnyFunSuiteLike with
alice2blockchain.expectNoMessage(100 millis)
// Alice receives the preimage for the incoming HTLC.
- alice ! CMD_FULFILL_HTLC(incomingHtlc.id, preimage, None, commit = true)
+ alice ! CMD_FULFILL_HTLC(incomingHtlc.id, preimage, None, None, commit = true)
val htlcSuccess = alice2blockchain.expectReplaceableTxPublished[HtlcSuccessTx]
assert(htlcSuccess.preimage == preimage)
alice2blockchain.expectNoMessage(100 millis)
@@ -1168,7 +1168,7 @@ class ClosingStateSpec extends TestKitBaseClass with FixtureAnyFunSuiteLike with
alice2blockchain.expectWatchOutputsSpent(htlcDelayedTxs.map(_.input))
// We replay the HTLC fulfillment: nothing happens since we already published a 3rd-stage transaction.
- alice ! CMD_FULFILL_HTLC(incomingHtlc.id, preimage, None, commit = true)
+ alice ! CMD_FULFILL_HTLC(incomingHtlc.id, preimage, None, None, commit = true)
alice2blockchain.expectNoMessage(100 millis)
// The remaining transactions confirm.
@@ -1193,14 +1193,14 @@ class ClosingStateSpec extends TestKitBaseClass with FixtureAnyFunSuiteLike with
addHtlc(40_000_000 msat, bob, alice, bob2alice, alice2bob)
crossSign(alice, bob, alice2bob, bob2alice)
// Bob has the preimage for 2 of the 3 HTLCs he received.
- bob ! CMD_FULFILL_HTLC(htlc1a.id, r1a, None)
+ bob ! CMD_FULFILL_HTLC(htlc1a.id, r1a, None, None)
bob2alice.expectMsgType[UpdateFulfillHtlc]
- bob ! CMD_FULFILL_HTLC(htlc2a.id, r2a, None)
+ bob ! CMD_FULFILL_HTLC(htlc2a.id, r2a, None, None)
bob2alice.expectMsgType[UpdateFulfillHtlc]
// Alice has the preimage for 2 of the 3 HTLCs she received.
- alice ! CMD_FULFILL_HTLC(htlc1b.id, r1b, None)
+ alice ! CMD_FULFILL_HTLC(htlc1b.id, r1b, None, None)
alice2bob.expectMsgType[UpdateFulfillHtlc]
- alice ! CMD_FULFILL_HTLC(htlc2b.id, r2b, None)
+ alice ! CMD_FULFILL_HTLC(htlc2b.id, r2b, None, None)
alice2bob.expectMsgType[UpdateFulfillHtlc]
// Alice force-closes.
@@ -1584,7 +1584,7 @@ class ClosingStateSpec extends TestKitBaseClass with FixtureAnyFunSuiteLike with
assert(closingTxs.htlcTxs.isEmpty) // we don't have the preimage to claim the htlc-success yet
// Alice receives the preimage for the first HTLC from downstream; she can now claim the corresponding HTLC output.
- alice ! CMD_FULFILL_HTLC(htlc1.id, r1, None, commit = true)
+ alice ! CMD_FULFILL_HTLC(htlc1.id, r1, None, None, commit = true)
val htlcSuccess = alice2blockchain.expectReplaceableTxPublished[ClaimHtlcSuccessTx](ConfirmationTarget.Absolute(htlc1.cltvExpiry.blockHeight))
assert(htlcSuccess.preimage == r1)
Transaction.correctlySpends(htlcSuccess.sign(), bobCommitTx :: Nil, ScriptFlags.STANDARD_SCRIPT_VERIFY_FLAGS)
@@ -1770,7 +1770,7 @@ class ClosingStateSpec extends TestKitBaseClass with FixtureAnyFunSuiteLike with
val htlcTimeoutTx = closingTxs.htlcTimeoutTxs.head
// Alice receives the preimage for the first HTLC from downstream; she can now claim the corresponding HTLC output.
- alice ! CMD_FULFILL_HTLC(htlc1.id, r1, None, commit = true)
+ alice ! CMD_FULFILL_HTLC(htlc1.id, r1, None, None, commit = true)
val htlcSuccess = alice2blockchain.expectReplaceableTxPublished[ClaimHtlcSuccessTx](ConfirmationTarget.Absolute(htlc1.cltvExpiry.blockHeight))
assert(htlcSuccess.preimage == r1)
Transaction.correctlySpends(htlcSuccess.sign(), bobCommitTx :: Nil, ScriptFlags.STANDARD_SCRIPT_VERIFY_FLAGS)
### eclair-core/src/test/scala/fr/acinq/eclair/crypto/SphinxSpec.scala
@@ -361,12 +361,96 @@ class SphinxSpec extends AnyFunSuite {
val attribution4 = Attribution.create(Some(attribution3), None, 500 milliseconds, sharedSecret0)
assert(attribution4 == hex"84986c936d26bfd3bb2d34d3ec62cfdb63e0032fdb3d9d75f3e5d456f73dffa7e35aab1db4f1bd3b98ff585caf004f656c51037a3f4e810d275f3f6aea0c8e3a125ebee5f374b6440bcb9bb2955ebf70c06d64090f9f6cf098200305f7f4305ba9e1350a0c3f7dab4ccf35b8399b9650d8e363bf83d3a0a09706433f0adae6562eb338b21ea6f21329b3775905e59187c325c9cbf589f5da5e915d9e5ad1d21aa1431f9bdc587185ed8b5d4928e697e67cc96bee6d5354e3764cede3f385588fa665310356b2b1e68f8bd30c75d395405614a40a587031ebd6ace60dfb7c6dd188b572bd8e3e9a47b06c2187b528c5ed35c32da5130a21cd881138a5fcac806858ce6c596d810a7492eb261bcc91cead1dae75075b950c2e81cecf7e5fdb2b51df005d285803201ce914dfbf3218383829a0caa8f15486dd801133f1ed7edec436730b0ec98f48732547927229ac80269fcdc5e4f4db264274e940178732b429f9f0e582c559f994a7cdfb76c93ffc39de91ff936316726cc561a6520d47b2cd487299a96322dadc463ef06127fc63902ff9cc4f265e2fbd9de3fa5e48b7b51aa0850580ef9f3b5ebb60c6c3216c5a75a93e82936113d9cad57ae4a94dd6481954a9bd1b5cff4ab29ca221fa2bf9b28a362c9661206f896fc7cec563fb80aa5eaccb26c09fa4ef7a981e63028a9c4dac12f82ccb5bea090d56bbb1a4c431e315d9a169299224a8dbd099fb67ea61dfc604edf8a18ee742550b636836bb552dabb28820221bf8546331f32b0c143c1c89310c4fa2e1e0e895ce1a1eb0f43278fdb528131a3e32bfffe0c6de9006418f5309cba773ca38b6ad8507cc59445ccc0257506ebc16a4c01d4cd97e03fcf7a2049fea0db28447858f73b8e9fe98b391b136c9dc510288630a1f0af93b26a8891b857bfe4b818af99a1e011e6dbaa53982d29cf74ae7dffef45545279f19931708ed3eede5e82280eab908e8eb80abff3f1f023ab66869297b40da8496861dc455ac3abe1efa8a6f9e2c4eda48025d43a486a3f26f269743eaa30d6f0e1f48db6287751358a41f5b07aee0f098862e3493731fe2697acce734f004907c6f11eef189424fee52cd30ad708707eaf2e441f52bcf3d0c5440c1742458653c0c8a27b5ade784d9e09c8b47f1671901a29360e7e5e94946b9c75752a1a8d599d2a3e14ac81b84d42115cd688c8383a64fc6e7e1dc5568bb4837358ebe63207a4067af66b2027ad2ce8fb7ae3a452d40723a51fdf9f9c9913e8029a222cf81d12ad41e58860d75deb6de30ad")
- val result = SuccessPacket.decrypt(Some(attribution4), sharedSecrets)
+ val result = SuccessPacket.decrypt(None, Some(attribution4), sharedSecrets)
assert(result.remainingAttribution_opt.nonEmpty)
assert(result.holdTimes == Seq(HoldTime(500 millisecond, publicKeys(0)), HoldTime(400 milliseconds, publicKeys(1)), HoldTime(300 milliseconds, publicKeys(2)), HoldTime(200 milliseconds, publicKeys(3)), HoldTime(100 milliseconds, publicKeys(4))))
}
}
+ test("fulfilled HTLC with attribution data and fulfillment payload (reference test vector)") {
+ // We build the onion packet to obtain the shared secrets.
+ val Success(PacketAndSecrets(packet, sharedSecrets)) = create(sessionKey, 1300, publicKeys, referencePaymentPayloads, associatedData)
+ val Right(DecryptedPacket(_, packet1, sharedSecret0)) = peel(privKeys(0), associatedData, packet)
+ val Right(DecryptedPacket(_, packet2, sharedSecret1)) = peel(privKeys(1), associatedData, packet1)
+ val Right(DecryptedPacket(_, packet3, sharedSecret2)) = peel(privKeys(2), associatedData, packet2)
+ val Right(DecryptedPacket(_, packet4, sharedSecret3)) = peel(privKeys(3), associatedData, packet3)
+ val Right(lastPacket@DecryptedPacket(_, _, sharedSecret4)) = peel(privKeys(4), associatedData, packet4)
+ assert(lastPacket.isLastPacket)
+
+ // The last node includes a fulfillment payload.
+ val fulfillmentPayload = hex"01f50000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000 fe0001000103070809"
+ val fulfillmentPayload4 = SuccessPacket.create(sharedSecret4, fulfillmentPayload)
+ assert(fulfillmentPayload4 == hex"b5b1fed711469adc0da510494818ce54624b537e7a8aba4b190403d90439798a694bdca0d0d53c18487d9940664a8416eb855290621e186081b2222e279bfa6dcb1645a39c2cbc569ed66f8f9ed0c10d4dcbc61abe6685d558d5fb0ec104bf1d14ab1e42a4cf41f1950c3fd06574a8278f553382a78b2725f36559d6db7aeccd41569f88b3722c3535358915648bd2e82c53cbaaa14c34487a77799cc4650d5c2e2edece013cf4095ee99bc96e1a850fa3373538448bd6d5a1771f50ca39b2a4e85c7201be5bedd5fae801e5e99491e6fb1e76457c48094f347205d1170df06e2fa7128d0dfe7d24716fb621632b948f9ad9edba24fba9293906e193a7ca3c234cce42ca9470c08952be1dbd3dcc8452")
+ val attribution4 = Attribution.create(None, None, 100 millisecond, sharedSecret4)
+ assert(attribution4 == hex"d77d0711b5f71d1d1be56bd88b3bb7ebc1792bb739ea7ebc1bc3b031b8bc2df3a50e25aeb99f47d7f7ab39e24187d3f4df9c4333463b053832ee9ac07274a5261b8b2a01fc09ce9ea7cd04d7b585dfb83299fb6570d71f793c1fcac0ef498766952c8c6840efa02a567d558a3cf6822b12476324b9b9efa03e5f8f26f81fa93daac46cbf00c98e69b6747cf69caaa2a71b025bd18830c4c54cd08f598cfde6197b3f2a951aba907c964c0f5d19a44e6d1d7279637321fa598adde927b3087d238f8b426ecde500d318617cdb7a56e6ce3520fc95be41a549973764e4dc483853ecc313947709f1b5199cb077d46e701fa633e11d3e13b03e9212c115ca6fa004b2f3dd912814693b705a561a06da54cdf603677a3abecdc22c7358c2de3cef771b366a568150aeecc86ad1990bb0f4e2865933b03ea0df87901bff467908273dc6cea31cbab0e2b8d398d10b001058c259ed221b7b55762f4c7e49c8c11a45a107b7a2c605c26dc5b0b10d719b1c844670102b2b6a36c43fe4753a78a483fc39166ae28420f112d50c10ee64ca69569a2f690712905236b7c2cb7ac8954f02922d2d918c56d42649261593c47b14b324a65038c3c5be8d3c403ce0c8f19299b1664bf077d7cf1636c4fb9685a8e58b7029fd0939fa07925a60bed339b23f973293598f595e75c8f9d455d7cebe4b5e23357c8bd47d66d6628b39427e37e0aecbabf46c11be6771f7136e108a143ae9bafba0fc47a51b6c7deef4cba54bae906398ee3162a41f2191ca386b628bde7e1dd63d1611aa01a95c456df337c763cb8c3a81a6013aa633739d8cd554c688102211725e6adad165adc1bcd429d020c51b4b25d2117e8bb27eb0cc7020f9070d4ad19ac31a76ebdf5f9246646aeadbfb9a3f1d75bd8237961e786302516a1a781780e8b73f58dc06f307e58bd0eb1d8f5c9111f01312974c1dc777a6a2d3834d8a2a40014e9818d0685cb3919f6b3b788ddc640b0ff9b1854d7098c7dd6f35196e902b26709640bc87935a3914869a807e8339281e9cedaaca99474c3e7bdd35050bb998ab4546f9900904e0e39135e861ff7862049269701081ebce32e4cca992c6967ff0fd239e38233eaf614af31e186635e9439ec5884d798f9174da6ff569d68ed5c092b78bd3f880f5e88a7a8ab36789e1b57b035fb6c32a6358f51f83e4e5f46220bcad072943df8bd9541a61b7dae8f30fa3dd5fb39b1fd9a0b8e802552b78d4ec306ecee15bfe6da14b29ba6d19ce5be4dd478bca74a52429cd5309d404655c3dec85c252")
+
+ val attribution3 = Attribution.create(Some(attribution4), Some(fulfillmentPayload4), 200 millisecond, sharedSecret3)
+ assert(attribution3 == hex"1571e10db7f8aa9f8e7e99caaf9c892e106c817df1d8e3b7b0e39d1c48f631e473e17e205489dd7b3c634cac3be0825cbf01418cd46e83c24b8d9c207742db9a0f0e5bcd888086498159f08080ba7bf3bc5cfe92707ca5d3bc81720c889a034b89cf2d376a67c5d63651e3af267efb768a38f488eac2940cf5525e959599984c3cf56c1daaa92476de574582eba16f90b9a6a74f4aa5f1cdad2115c994a09565f9755940ec0665f9e5bc58cad6e523091f94d0bcd3c6c65ca1a5d401128dcc5e14f9108b32e660017c13de598bcf9d403710857cccb0fb9c2a81bfd66bc4552e1132afa3119203a4aaa1e8839c1dab8cbdcde7b527aca3f54bde651aa9f3f2178829cee3f1c0b9292758a40cc63bd998fcd0d3ed4bdcaf1023267b8f8e44130a63ad15f76145936552381eabb6d684c0a3af6ba8efcf207cebaea5b7acdbb63f8e7221102409d10c23f0514dc9f4d0efb2264161a193a999a23e992632710580a0d320f676d367b9190721194514457761af05207cdab2b6328b1b3767eacb36a7ef4f7bd2e16762d13df188e0898b7410f62459458712a44bf594ae662fd89eb300abb6952ff8ad40164f2bcd7f86db5c7650b654b79046de55d51aa8061ce35f867a3e8f5bf98ad920be827101c64fb871d86e53a4b3c0455bfac5784168218aa72cbee86d9c750a9fa63c363a8b43d7bf4b2762516706a306f0aa3be1ec788b5e13f8b24837e53ac414f211e11c7a093cd9653dfa5fba4e377c79adfa5e841e2ddb6afc054fc715c05ddc6c8fc3e1ee3406e1ffceb2df77dc2f02652614d1bfcfaddebaa53ba919c7051034e2c7b7cfaabdf89f26e7f8e3f956d205dfab747ad0cb505b85b54a68439621b25832cbc2898919d0cd7c0a64cfd235388982dd4dd68240cb668f57e1d2619a656ed326f8c92357ee0d9acead3c20008bc5f04ca8059b55d77861c6d04dfc57cfba57315075acbe1451c96cf28e1e328e142890248d18f53b5d3513ce574dea7156cf596fdb3d909095ec287651f9cf1bcdc791c5938a5dd9b47e84c004d24ab3ae74492c7e8dcc1da15f65324be2672947ec82074cac8ce2b925bc555facbbf1b55d63ea6fbea6a785c97d4caf2e1dad9551b7f66c31caae5ebc7c0047e892f201308fcf452c588be0e63d89152113d87bf0dbd01603b4cdc7f0b724b0714a9851887a01f709408882e18230fe810b9fafa58a666654576d8eba3005f07221f55a6193815a672e5db56204053bc4286fa3db38250396309fd28011b5708a26a2d76c4a333b69b6bfd272fb")
+ val fulfillmentPayload3 = SuccessPacket.wrap(fulfillmentPayload4, sharedSecret3)
+ assert(fulfillmentPayload3 == hex"d4cd5f33730d592c4249f3e31233af0572bf729218ca375ddd8aa518b3fa466d6101f77904b013dd4ef3c912d9278edc09923288c1c6df3fb636053cb6a2fa4bf7e9d7500310a4fc819d4f26a0a03d4fd73c74ab281a2dcf2ec8bb56ecabb1a9884478a172348bf38ddf3dfd579d3c9393dd79a5650da2caedd41fd531fa4ec9f3a46bfc05df7244f30d115687eacc568395e83a165bfe1bc91073af0172e27488771c9fd7744b45c883d538e90bbbc627bcc428c5ebb71fee5877b7a6d23a198a544565dee2fb2ed736b69f8c7c696dd56fd48e88a24017306f74106a0898622abb24fbf6f3b534ff32ceda7d07d05800f9c32730fcb47a5a774cd464503936d2432e8b450ade0cdd14b2ea1bef2e71")
+
+ val attribution2 = Attribution.create(Some(attribution3), Some(fulfillmentPayload3), 300 millisecond, sharedSecret2)
+ assert(attribution2 == hex"34e34397b8621ec2f2b54dbe6c14073e267324cd60b152bce76aec8729a6ddefb61bc263be4b57bd592aae604a32bea69afe6ef4a6b573c26b17d69381ec1fc9b5aa769d148f2f1f8b5377a73840bb6d40734019bf9dcbb731aeac3b1052e6703e8ffa550f2f3b6b40df0faa5bd67271a99a81e81265b567522733358a1c8a3b770d207ba8a22ba238abf056af11042474f4fd44bbb21e3ea8d978058a8cc67a11ad429f816de27a43dd69a3f1fe8ccf670fa081fd9d89af6079f7baf05c7f4bf318b2dcfe18f5bd8628795265f61cbf40b6efa757fc9242927c329f5e06ddbce3be8c1f5dd48a0370eb559f5dc181a09f2209da72f79ae6745992c803310d39f960e8ecf327aed706e4b3e2704eeb9b304dc0e0685f5dcd0389ec377bdba37610ad556a0e957a413a56339dd3c40817214bced5802beee2ee545bdd713208751added5fc0eb2bc89a5aa2decb18ee37dac39f22a33b60cc1a369d24de9f3d2d8b63c039e248806de4e36a47c7a0aed30edd30c3d62debdf1ad82bf7aedd7edec413850d91c261e12beec7ad1586a9ad25b2db62c58ca17119d61dcc4f3e5c4520c42a8e384a45d8659b338b3a08f9e123a1d3781f5fc97564ccff2c1d97f06fa0150cfa1e20eacabefb0c339ec109336d207cc63d9170752fc58314c43e6d4a528fd0975afa85f3aa186ff1b6b8cb12c97ed4ace295b0ef5f075f0217665b8bb180246b87982d10f43c9866b22878106f5214e99188781180478b07764a5e12876ddcb709e0a0a8dd42cf004c695c6fc1669a6fd0e4a1ca54b024d0d80eac492a9e5036501f36fb25b72a054189294955830e43c18e55668337c8c6733abb09fc2d4ade18d5a853a2b82f7b4d77151a64985004f1d9218f2945b63c56fdebd1e96a2a7e49fa70acb4c39873947b83c191c10e9a8f40f60f3ad5a2be47145c22ea59ed3f5f4e61cb069e875fb67142d281d784bf925cc286eacc2c43e94d08da4924b83e58dbf2e43fa625bdd620eba6d9ce960ff17d14ed1f2dbee7d08eceb540fdc75ff06dabc767267658fad8ce99e2a3236e46d2deedcb51c3c6f81589357edebac9772a70b3d910d83cd1b9ce6534a011e9fa557b891a23b5d88afcc0d9856c6dabeab25eea55e9a248182229e4927f268fe5431672fcce52f434ca3d27d1a2136bae5770bb36920df12fbc01d0e8165610efa04794f414c1417f1d4059435c5385bfe2de83ce0e238d6fd2dbd3c0487c69843298577bfa480fe2a16ab2a0e4bc712cd8b5a14871cda61c993b6835303d9043d7689a")
+ val fulfillmentPayload2 = SuccessPacket.wrap(fulfillmentPayload3, sharedSecret2)
+ assert(fulfillmentPayload2 == hex"b584abb82973d6112d1474e8685a21253e4936c0b27e7f122c5443694d99bb263e1a8f9724f7cc7b50ce9e79cdbc63738a97bd0d31acee4d15143fea72d1d7dd44c0828a7e3f4fbdc178c2a05caf13fa0eedf325479052aa855aa955e22520fcfbafc5de47d52fc304d4da1b2ba2882861038fc79f6ff6eb0a600e17af44cf25941318a95cb47788e65a4430c9b30d38a2d4e291f3657e5b83712cebeac1f942d4d066f146505d227aefb995cdf098e2e8d82c9118859bf9b1477e3b5bccce02288082bb2e1ded74d94a3278d500298e0a91435a9f6304436a614cfe59e33283067be5bda617e6e094ce861a2c48e51f73ba9dcfab879c6a3a19fbe60697432bb5b04dbc401d53757c133ed01a5ad4ad")
+
+ val attribution1 = Attribution.create(Some(attribution2), Some(fulfillmentPayload2), 400 millisecond, sharedSecret1)
+ assert(attribution1 == hex"74a4ea61339463642a2182758871b2ea724f31f531aa98d80f1c3043febca41d5ee52e8b1e127e61719a0d078db8909748d57839e58424b91f063c4fbc8a221bef261140e66a9b596ca6d420a973ad54651be4bf8728ba81adcb9a1d3ce3820cea24f2c6bfdde427edb7706dfd1e8e50c0ab544022275fd457fbd57555cfc85bbfbed6eb0f7a8ea04ebc1b40fc8d3f67402a01d169ed1448ed2e708e16a0fe2c1ab5055be446594036b5ecae456486addbebbd09ca3423d4c15e2afce6b40b545bfbf5859b6bf913e80bcec00e88cba671fee9770fdace16301fbb23b0eca85549ae0be1a96bda5167b6e5fb837edecd981b61e06858badb34ead1e93746ab5afd2bc10e95bffc4dc965de242db63eb8ec221cf57e2c77241fcfca70818a85a447b6fbcaaf4b2fc2135565cbfc03804aa11ad0ab0582e5d86590df2ecfd561dc6e1cdb08d3e10901312326a45fb0498a177319389809c6ba07a76cfad621e07b9af097730e94df92fbd311b2cb5da32c80ab5f14971b6d40f8e2ab202ac98bd8439790764a40bf309ea2205c1632610956495720030a25dc7118e0c868fdfa78c3e9ecce58215579a0581b3bafdb7dbbe53be9e904567fdc0ce1236aab5d22f1ebc18997e3ea83d362d891e04c5785fd5238326f767bce499209f8db211a50e1402160486e98e7235cf397dbb9ae19fd9b79ef589c821c6f99f28be33452405a003b33f4540fe0a41dfcc286f4d7cc10b70552ba7850869abadcd4bb7f256823face853633d6e2a999ac9fcd259c71d08e266db5d744e1909a62c0db673745ad9585949d108ab96640d2bc27fb4acac7fa8b170a30055a5ede90e004df9a44bdc29aeb4a6bec1e85dde1de6aaf01c6a5d12405d0bec22f49026cb23264f8c04b8401d3c2ab6f2e109948b6193b3bec27adfe19fb8afb8a92364d6fc5b219e8737d583e7ff3a4bcb75d53edda3bf3f52896ac36d8a877ad9f296ea6c045603fc62ac4ae41272bde85ef7c3b3fd3538aacfd5b025fefbe277c2906821ecb20e6f75ea479fa3280f9100fb0089203455c56b6bc775e5c2f0f58c63edd63fa3eec0b40da4b276d0d41da2ec0ead865a98d12bc694e23d8eaadd2b4d0ee88e9570c88fb878930f492e036d27998d593e47763927ff7eb80b188864a3846dd2238f7f95f4090ed399ae95deaeb37abca1cf37c397cc12189affb42dca46b4ff6988eb8c060691d155302d448f50ff70a794d97c0408f8cee9385d6a71fa412e36edcb22dbf433db9db4779f27b682ee17fc05e70c8e794b9f7f6d1")
+ val fulfillmentPayload1 = SuccessPacket.wrap(fulfillmentPayload2, sharedSecret1)
+ assert(fulfillmentPayload1 == hex"ba9463d72ce55bc83c9ccd70861ffdf8c5b1a9291be4d9257016ae3c6d3becc6765fe07333369e629ef7475898e535dda560281a4dcf2654362b0421e95aff38e8aeb1298787331bec87815490e534af8de7c85bdf537418fdb81464168cb7c2123ee4186fa6da7cb60583c8ff2ed07982b0ce3e690cb9ba99f186d416bb8adb7f020ea2ef2894bbc732fcd78d1a2c3f7983b85f8b039ae370e1dd062e6d2e6439c003a4d65a55102db383ef1ceb291cd057dfba81396959613104b894e5f4202218b86b3ac9e9cb7b4a10a06669d788fd7d8a9dd832d8996592d17593978d1a500fbe1943c57f004e41eeb3da28e114997aa3b6f183fa2af59b8ad67c1c2753beb5c5fe6f4733980225e8d40d0cfd95")
+
+ val attribution0 = Attribution.create(Some(attribution1), Some(fulfillmentPayload1), 500 millisecond, sharedSecret0)
+ assert(attribution0 == hex"84986c936d26bfd3bb2d34d3ec62cfdb63e0032fdb3d9d75f3e5d456f73dffa7e35aab1db4f1bd3b98ff585caf004f656c51037a3f4e810d275f3f6aea0c8e3a125ebee5f374b6440bcb9bb2955ebf706a8fd90a3bef40add4106d8c215290aafbc30881b17d68b6a7aac1b6ab52261df2fceef75d3a490862d4bab3aa510cfb8ca695b38c640b58bb8685db8bb57f93db49657ddda39d21d6074d9ecaa1ea0da6cc904e136dbb4c1ba9839a7fb8367c88662f7f573c82c2746d122cca96b2e7872a48e32c8adba45babdc57a9bca55720b1d2c922622fcf8f8a5ed7daae286484b701e69bd40554a4cc3418444ff7eb9982ecee020c8a92717f089ce1ab0ca00bb3a582253cd832a2f869703a2fadafd8261ebb93fde2a9f4f13dc8470436c3cd110bdcc7f5a1e9f9c743c8e55b50c45dac07e07a8e5c7416de895940fde032786594bbaa8a1147422bd5e926fea510408c249fb61164bf7bdbac53d9fb9704023314c332a865146893ad99743708c4a34a30a82fd8b756c561a6520d47b2cd487299a96322dadc463ef06127fc63902ff9cc4f265e2fbd9de3fa5e48b7b51aa0850580ef9f3b5ebb60c6c3216c5a75a93e82936113d9cad57ae4a94dd6481954a9bd1b5cff4ab29ca221fa2bf9b28a362c9661206f896fc7cec563fb80aa5eaccb26c09fa4ef7a981e63028a9c4dac12f82ccb5bea090d56bbb1a4c431e315d9a169299224a8dbd099fb67ea61dfc604edf8a18ee742550b636836bb552dabb28820221bf8546331f32b0c143c1c89310c4fa2e1e0e895ce1a1eb0f43278fdb528131a3e32bfffe0c6de9006418f5309cba773ca38b6ad8507cc59445ccc0257506ebc16a4c01d4cd97e03fcf7a2049fea0db28447858f73b8e9fe98b391b136c9dc510288630a1f0af93b26a8891b857bfe4b818af99a1e011e6dbaa53982d29cf74ae7dffef45545279f19931708ed3eede5e82280eab908e8eb80abff3f1f023ab66869297b40da8496861dc455ac3abe1efa8a6f9e2c4eda48025d43a486a3f26f269743eaa30d6f0e1f48db6287751358a41f5b07aee0f098862e3493731fe2697acce734f004907c6f11eef189424fee52cd30ad708707eaf2e441f52bcf3d0c5440c1742458653c0c8a27b5ade784d9e09c8b47f1671901a29360e7e5e94946b9c75752a1a8d599d2a3e14ac81b84d42115cd688c8383a64fc6e7e1dc5568bb4837358ebe63207a4067af66b2027ad2ce8fb7ae3a452d40723a51fdf9f9c9913e8029a222cf81d12ad41e58860d75deb6de30ad")
+ val fulfillmentPayload0 = SuccessPacket.wrap(fulfillmentPayload1, sharedSecret0)
+ assert(fulfillmentPayload0 == hex"8c0d9ee20671d1cdca98bb4c8dd5d490105b63021afcb90b22f33f1d9d6b7faafb86ec57ec4b56ad11e122bb0289403447a0b814ef8aefb90d2b0c3567501eb3570011ee96514db6050747a1d5cba50fe6e0c3e639d8208c549d61a502eab8772eceafeee940a586fc0ad92ce57773e307a5055604b9ea68c780617d6a797467d357d185fe9eb4279719d35faaab1105ab77299bd10f3504c439058720ed738a1c16756a44ba348bb193ec7d3dd9f11ac3d0cc1277ace098ed0ed8e25a01e83216a9c6a183a2659ddf2d971bc4a9b5ce776e1b07242a89a15026a3d208bbb8e3f91086f2785be68b5e4122f9d79583b883266ba6dfb4cf0c22737915dda898e1461651818c17f811f51f496f0842223c")
+
+ val result = SuccessPacket.decrypt(Some(fulfillmentPayload0), Some(attribution0), sharedSecrets)
+ assert(result.fulfillmentPayload_opt.contains(fulfillmentPayload))
+ assert(result.holdTimes == Seq(HoldTime(500 millisecond, publicKeys(0)), HoldTime(400 milliseconds, publicKeys(1)), HoldTime(300 milliseconds, publicKeys(2)), HoldTime(200 milliseconds, publicKeys(3)), HoldTime(100 milliseconds, publicKeys(4))))
+ }
+
+ test("fulfillment payload size limits") {
+ // We build the onion packet to obtain the shared secrets.
+ val Success(PacketAndSecrets(packet, sharedSecrets)) = create(sessionKey, 1300, publicKeys, referencePaymentPayloads, associatedData)
+ val Right(DecryptedPacket(_, packet1, sharedSecret0)) = peel(privKeys(0), associatedData, packet)
+ val Right(DecryptedPacket(_, packet2, sharedSecret1)) = peel(privKeys(1), associatedData, packet1)
+ val Right(DecryptedPacket(_, packet3, sharedSecret2)) = peel(privKeys(2), associatedData, packet2)
+ val Right(DecryptedPacket(_, packet4, sharedSecret3)) = peel(privKeys(3), associatedData, packet3)
+ val Right(lastPacket@DecryptedPacket(_, _, sharedSecret4)) = peel(privKeys(4), associatedData, packet4)
+ assert(lastPacket.isLastPacket)
+ val intermediateSecrets = Seq(sharedSecret3, sharedSecret2, sharedSecret1, sharedSecret0)
+ def payloadOfLength(length: Int): ByteVector = if (length == 0) ByteVector.empty else randomBytes(length)
+
+ // Payloads of any size up to MAX_PAYLOAD_LENGTH can be sent and received.
+ for (payloadLength <- Seq(0, 1, 16, 1000, SuccessPacket.MAX_PAYLOAD_LENGTH - 1, SuccessPacket.MAX_PAYLOAD_LENGTH)) {
+ val payload = payloadOfLength(payloadLength)
+ val encrypted = SuccessPacket.create(sharedSecret4, payload)
+ // The recipient adds a 16-bytes mac, and the result must fit in what intermediate nodes are allowed to relay.
+ assert(encrypted.length == payloadLength + 16)
+ assert(encrypted.length <= SuccessPacket.MAX_LENGTH)
+ // Intermediate nodes don't change the size of the payload.
+ val wrapped = intermediateSecrets.foldLeft(encrypted)((current, ss) => SuccessPacket.wrap(current, ss))
+ assert(wrapped.length == encrypted.length)
+ assert(SuccessPacket.decrypt(Some(wrapped), None, sharedSecrets).fulfillmentPayload_opt.contains(payload))
+ }
+
+ // Payloads that are too large are truncated, but the result is still a valid packet: this guarantees that we cannot
+ // create a packet that our peers would reject (see Channel.checkFulfillmentPayload).
+ for (payloadLength <- Seq(SuccessPacket.MAX_PAYLOAD_LENGTH + 1, SuccessPacket.MAX_LENGTH, 2 * SuccessPacket.MAX_LENGTH)) {
+ val payload = randomBytes(payloadLength)
+ val encrypted = SuccessPacket.create(sharedSecret4, payload)
+ assert(encrypted.length == SuccessPacket.MAX_LENGTH)
+ val wrapped = intermediateSecrets.foldLeft(encrypted)((current, ss) => SuccessPacket.wrap(current, ss))
+ assert(wrapped.length == SuccessPacket.MAX_LENGTH)
+ val decrypted = SuccessPacket.decrypt(Some(wrapped), None, sharedSecrets)
+ assert(decrypted.fulfillmentPayload_opt.contains(payload.take(SuccessPacket.MAX_PAYLOAD_LENGTH)))
+ }
+
+ // If a malicious node relays a packet that is larger than what we allow, we truncate it, which makes it invalid.
+ val tooLarge = SuccessPacket.create(sharedSecret4, randomBytes(1000)) ++ randomBytes(SuccessPacket.MAX_LENGTH)
+ assert(SuccessPacket.decrypt(Some(tooLarge), None, sharedSecrets).fulfillmentPayload_opt.isEmpty)
+ }
+
test("only some nodes in the route support attributable failures") {
for ((payloads, packetPayloadLength) <- Seq((referencePaymentPayloads, 1300), (paymentPayloadsFull, 1300))) {
// origin build the onion packet
### eclair-core/src/test/scala/fr/acinq/eclair/db/AuditDbSpec.scala
@@ -130,16 +130,16 @@ class AuditDbSpec extends AnyFunSuite {
val preimage2 = randomBytes32()
val paymentHash2 = Crypto.sha256(preimage2)
- val e1 = PaymentSent(ZERO_UUID, preimage1, 40000 msat, remoteNodeId2, PaymentSent.PaymentPart(ZERO_UUID, PaymentEvent.OutgoingPayment(channelId1, remoteNodeId1, 42000 msat, now - 75.seconds), 1000 msat, None, now - 100.seconds) :: Nil, None, now - 100.seconds)
+ val e1 = PaymentSent(ZERO_UUID, preimage1, 40000 msat, remoteNodeId2, PaymentSent.PaymentPart(ZERO_UUID, PaymentEvent.OutgoingPayment(channelId1, remoteNodeId1, 42000 msat, now - 75.seconds), 1000 msat, None, now - 100.seconds) :: Nil, None, None, now - 100.seconds)
val pp2a = PaymentEvent.IncomingPayment(channelId1, remoteNodeId1, 42000 msat, now - 1.seconds)
val pp2b = PaymentEvent.IncomingPayment(channelId2, remoteNodeId2, 42100 msat, now)
val e2 = PaymentReceived(paymentHash1, pp2a :: pp2b :: Nil)
val e3 = ChannelPaymentRelayed(paymentHash1, Seq(PaymentEvent.IncomingPayment(channelId1, remoteNodeId1, 42000 msat, now - 3.seconds)), Seq(PaymentEvent.OutgoingPayment(channelId2, remoteNodeId2, 1000 msat, now)))
val pp4a = PaymentSent.PaymentPart(uuid1, PaymentEvent.OutgoingPayment(channelId1, remoteNodeId1, 42000 msat, now - 15.seconds), 1000 msat, None, startedAt = now - 30.seconds)
val pp4b = PaymentSent.PaymentPart(uuid2, PaymentEvent.OutgoingPayment(channelId2, remoteNodeId2, 42100 msat, now - 10.seconds), 900 msat, None, startedAt = now - 25.seconds)
- val e4 = PaymentSent(uuid3, preimage1, 84100 msat, remoteNodeId2, pp4a :: pp4b :: Nil, None, startedAt = now - 30.seconds)
+ val e4 = PaymentSent(uuid3, preimage1, 84100 msat, remoteNodeId2, pp4a :: pp4b :: Nil, None, None, startedAt = now - 30.seconds)
val pp5 = PaymentSent.PaymentPart(uuid2, PaymentEvent.OutgoingPayment(channelId1, remoteNodeId1, 42000 msat, settledAt = now + 10.minutes), 1000 msat, None, startedAt = now + 9.minutes)
- val e5 = PaymentSent(uuid2, preimage1, 42000 msat, remoteNodeId1, pp5 :: Nil, None, startedAt = now + 9.minutes)
+ val e5 = PaymentSent(uuid2, preimage1, 42000 msat, remoteNodeId1, pp5 :: Nil, None, None, startedAt = now + 9.minutes)
val e6 = TrampolinePaymentRelayed(paymentHash1,
Seq(
PaymentEvent.IncomingPayment(channelId1, remoteNodeId1, 20000 msat, now - 7.seconds),
@@ -351,7 +351,7 @@ class AuditDbSpec extends AnyFunSuite {
val channelCreated = ChannelEvent(channelId1, remoteNodeId1, fundingTx.txid, "anchor_outputs", 100_000 sat, isChannelOpener = true, isPrivate = false, "created", now)
val txPublished = TransactionPublished(channelId1, remoteNodeId1, fundingTx, 200 sat, 100 sat, "funding", None)
val txConfirmed = TransactionConfirmed(channelId1, remoteNodeId1, fundingTx)
- val paymentSent = PaymentSent(UUID.randomUUID(), randomBytes32(), 25_000_000 msat, remoteNodeId2, Seq(PaymentSent.PaymentPart(UUID.randomUUID(), PaymentEvent.OutgoingPayment(channelId1, remoteNodeId1, 24_999_999 msat, now), 561 msat, None, now - 10.seconds)), None, now - 10.seconds)
+ val paymentSent = PaymentSent(UUID.randomUUID(), randomBytes32(), 25_000_000 msat, remoteNodeId2, Seq(PaymentSent.PaymentPart(UUID.randomUUID(), PaymentEvent.OutgoingPayment(channelId1, remoteNodeId1, 24_999_999 msat, now), 561 msat, None, now - 10.seconds)), None, None, now - 10.seconds)
val paymentReceived = PaymentReceived(randomBytes32(), Seq(PaymentEvent.IncomingPayment(channelId1, remoteNodeId1, 15_350 msat, now - 1.seconds)))
val paymentRelayed = ChannelPaymentRelayed(randomBytes32(), Seq(PaymentEvent.IncomingPayment(channelId1, remoteNodeId1, 1100 msat, now - 1.seconds)), Seq(PaymentEvent.OutgoingPayment(channelId2, remoteNodeId2, 1000 msat, now)))
forAllDbs {
### 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.PaymentPart(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, None, 900 unixms))
db.addOutgoingPayment(ps6.copy(status = OutgoingPaymentStatus.Pending))
db.updateOutgoingPayment(PaymentFailed(ps6.id, ps6.paymentHash, Nil, 1100 unixms, 1300 unixms))
@@ -772,12 +772,12 @@ 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.PaymentPart(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, None, startedAt = 100 unixms)))
val paymentSent = PaymentSent(parentId, preimage1, 600 msat, carol, Seq(
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)
+ ), None, None, startedAt = 100 unixms)
val ss1 = s1.copy(status = OutgoingPaymentStatus.Succeeded(preimage1, 15 msat, Nil, 400 unixms))
val ss2 = s2.copy(status = OutgoingPaymentStatus.Succeeded(preimage1, 20 msat, Seq(HopSummary(alice, bob, Some(ShortChannelId(42))), HopSummary(bob, carol, None)), 410 unixms))
db.updateOutgoingPayment(paymentSent)
### eclair-core/src/test/scala/fr/acinq/eclair/db/PendingCommandsDbSpec.scala
@@ -28,6 +28,7 @@ import fr.acinq.eclair.wire.internal.CommandCodecs.cmdCodec
import fr.acinq.eclair.wire.protocol.{FailureMessageCodecs, FailureReason, UnknownNextPeer}
import fr.acinq.eclair.{TimestampMilli, randomBytes, randomBytes32}
import org.scalatest.funsuite.AnyFunSuite
+import scodec.bits.HexStringSyntax
import scala.util.Random
@@ -53,8 +54,8 @@ class PendingCommandsDbSpec extends AnyFunSuite {
val channelId1 = randomBytes32()
val channelId2 = randomBytes32()
- val msg0 = CMD_FULFILL_HTLC(0, randomBytes32(), None)
- val msg1 = CMD_FULFILL_HTLC(1, randomBytes32(), Some(FulfillAttributionData(TimestampMilli(600), None, Some(randomBytes(Attribution.totalLength)))))
+ val msg0 = CMD_FULFILL_HTLC(0, randomBytes32(), None, None)
+ val msg1 = CMD_FULFILL_HTLC(1, randomBytes32(), Some(hex"deadbeef"), Some(FulfillAttributionData(TimestampMilli(600), None, Some(randomBytes(Attribution.totalLength)))))
val msg2 = CMD_FAIL_HTLC(2, FailureReason.EncryptedDownstreamFailure(randomBytes32(), None), None)
val msg3 = CMD_FAIL_HTLC(3, FailureReason.EncryptedDownstreamFailure(randomBytes32(), Some(randomBytes(Sphinx.Attribution.totalLength))), Some(FailureAttributionData(TimestampMilli(500), Some(TimestampMilli(550)))))
val msg4 = CMD_FAIL_MALFORMED_HTLC(4, randomBytes32(), FailureMessageCodecs.BADONION)
@@ -66,7 +67,7 @@ class PendingCommandsDbSpec extends AnyFunSuite {
db.addSettlementCommand(channelId1, msg1)
db.addSettlementCommand(channelId1, msg2)
db.addSettlementCommand(channelId1, msg3)
- db.addSettlementCommand(channelId1, CMD_FULFILL_HTLC(msg3.id, randomBytes32(), None)) // conflicting command
+ db.addSettlementCommand(channelId1, CMD_FULFILL_HTLC(msg3.id, randomBytes32(), None, None)) // conflicting command
db.addSettlementCommand(channelId1, msg4)
db.addSettlementCommand(channelId2, msg0) // same messages but for different channel
db.addSettlementCommand(channelId2, msg1)
@@ -140,7 +141,7 @@ object PendingCommandsDbSpec {
val channelId = randomBytes32()
val cmds = (0 until Random.nextInt(5)).map { _ =>
Random.nextInt(2) match {
- case 0 => CMD_FULFILL_HTLC(Random.nextLong(100_000), randomBytes32(), None)
+ case 0 => CMD_FULFILL_HTLC(Random.nextLong(100_000), randomBytes32(), None, None)
case 1 => CMD_FAIL_HTLC(Random.nextLong(100_000), FailureReason.LocalFailure(UnknownNextPeer()), None)
}
}
### eclair-core/src/test/scala/fr/acinq/eclair/integration/ChannelIntegrationSpec.scala
@@ -174,7 +174,7 @@ abstract class ChannelIntegrationSpec extends IntegrationSpec {
// we generate a few blocks to get the commit tx confirmed
generateBlocks(3, Some(minerAddress))
// we then fulfill the htlc, which will make F redeem it on-chain
- sender.send(nodes("F").register, Register.Forward(sender.ref.toTyped[Any], htlc.channelId, CMD_FULFILL_HTLC(htlc.id, preimage, None)))
+ sender.send(nodes("F").register, Register.Forward(sender.ref.toTyped[Any], htlc.channelId, CMD_FULFILL_HTLC(htlc.id, preimage, None, None)))
// we don't need to generate blocks to confirm the htlc-success; C should extract the preimage as soon as it enters
// the mempool and fulfill the payment upstream.
paymentSender.expectMsgType[PaymentSent](max = 60 seconds)
@@ -212,7 +212,7 @@ abstract class ChannelIntegrationSpec extends IntegrationSpec {
// we generate a few blocks to get the commit tx confirmed
generateBlocks(3, Some(minerAddress))
// we then fulfill the htlc (it won't be sent to C, and will be used to pull funds on-chain)
- sender.send(nodes("F").register, Register.Forward(sender.ref.toTyped[Any], htlc.channelId, CMD_FULFILL_HTLC(htlc.id, preimage, None)))
+ sender.send(nodes("F").register, Register.Forward(sender.ref.toTyped[Any], htlc.channelId, CMD_FULFILL_HTLC(htlc.id, preimage, None, None)))
// we don't need to generate blocks to confirm the htlc-success; C should extract the preimage as soon as it enters
// the mempool and fulfill the payment upstream.
paymentSender.expectMsgType[PaymentSent](max = 60 seconds)
### eclair-core/src/test/scala/fr/acinq/eclair/payment/MultiPartHandlerSpec.scala
@@ -623,8 +623,8 @@ class MultiPartHandlerSpec extends TestKitBaseClass with FixtureAnyFunSuiteLike
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)),
- Register.Forward(null, add1.channelId, CMD_FULFILL_HTLC(add1.id, preimage, Some(FulfillAttributionData(receivedAt1, None, None)), commit = true)),
- Register.Forward(null, add3.channelId, CMD_FULFILL_HTLC(add3.id, preimage, Some(FulfillAttributionData(receivedAt3, None, None)), commit = true))
+ Register.Forward(null, add1.channelId, CMD_FULFILL_HTLC(add1.id, preimage, None, Some(FulfillAttributionData(receivedAt1, None, None)), commit = true)),
+ Register.Forward(null, add3.channelId, CMD_FULFILL_HTLC(add3.id, preimage, None, Some(FulfillAttributionData(receivedAt3, None, None)), commit = true))
)
val paymentReceived = f.eventListener.expectMsgType[PaymentReceived]
@@ -641,7 +641,7 @@ class MultiPartHandlerSpec extends TestKitBaseClass with FixtureAnyFunSuiteLike
val receivedAt4 = receivedAt1 + 3.millis
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)))
+ f.register.expectMsg(Register.Forward(null, ByteVector32.One, CMD_FULFILL_HTLC(44, preimage, None, Some(FulfillAttributionData(receivedAt4, None, None)), commit = true)))
assert(f.eventListener.expectMsgType[PaymentReceived].amount == 200.msat)
val received2 = nodeParams.db.payments.getIncomingPayment(invoice.paymentHash)
assert(received2.get.status.asInstanceOf[IncomingPaymentStatus.Received].amount == 1200.msat)
@@ -668,8 +668,8 @@ class MultiPartHandlerSpec extends TestKitBaseClass with FixtureAnyFunSuiteLike
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)),
- Register.Forward(null, add2.channelId, CMD_FULFILL_HTLC(add2.id, preimage, Some(FulfillAttributionData(receivedAt2, None, None)), commit = true))
+ Register.Forward(null, add1.channelId, CMD_FULFILL_HTLC(add1.id, preimage, None, Some(FulfillAttributionData(receivedAt1, None, None)), commit = true)),
+ Register.Forward(null, add2.channelId, CMD_FULFILL_HTLC(add2.id, preimage, None, Some(FulfillAttributionData(receivedAt2, None, None)), commit = true))
)
val paymentReceived = f.eventListener.expectMsgType[PaymentReceived]
@@ -710,8 +710,8 @@ class MultiPartHandlerSpec extends TestKitBaseClass with FixtureAnyFunSuiteLike
// the fulfill are not necessarily in the same order as the commands
f.register.expectMsgAllOf(
- Register.Forward(null, add2.channelId, CMD_FULFILL_HTLC(2, preimage, Some(FulfillAttributionData(receivedAt2, None, None)), commit = true)),
- Register.Forward(null, add3.channelId, CMD_FULFILL_HTLC(5, preimage, Some(FulfillAttributionData(receivedAt3, None, None)), commit = true))
+ Register.Forward(null, add2.channelId, CMD_FULFILL_HTLC(2, preimage, None, Some(FulfillAttributionData(receivedAt2, None, None)), commit = true)),
+ Register.Forward(null, add3.channelId, CMD_FULFILL_HTLC(5, preimage, None, Some(FulfillAttributionData(receivedAt3, None, None)), commit = true))
)
val paymentReceived = f.eventListener.expectMsgType[PaymentReceived]
### eclair-core/src/test/scala/fr/acinq/eclair/payment/MultiPartPaymentLifecycleSpec.scala
@@ -580,8 +580,8 @@ 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.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))
+ 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, None, 75 unixms))
+ sender.expectMsg(PreimageReceived(paymentHash, paymentPreimage, None, None))
val result = sender.expectMsgType[PaymentSent]
assert(result.id == cfg.id)
assert(result.paymentHash == paymentHash)
@@ -608,8 +608,8 @@ 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.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))
+ 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, None, 75 unixms))
+ sender.expectMsg(PreimageReceived(paymentHash, paymentPreimage, None, None))
awaitCond(payFsm.stateName == PAYMENT_SUCCEEDED)
sender.watch(payFsm)
@@ -634,8 +634,8 @@ class MultiPartPaymentLifecycleSpec extends TestKitBaseClass with FixtureAnyFunS
val partialPayments = pending.map {
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))
+ partialPayments.foreach(pp => childPayFsm.send(payFsm, PaymentSent(cfg.id, paymentPreimage, finalAmount, e, Seq(pp), None, None, 100 unixms)))
+ sender.expectMsg(PreimageReceived(paymentHash, paymentPreimage, None, None))
val result = sender.expectMsgType[PaymentSent]
assert(result.id == cfg.id)
assert(result.paymentHash == paymentHash)
### eclair-core/src/test/scala/fr/acinq/eclair/payment/PaymentInitiatorSpec.scala
@@ -209,6 +209,7 @@ class PaymentInitiatorSpec extends TestKitBaseClass with FixtureAnyFunSuiteLike
priv_c.publicKey,
Seq(PaymentPart(payment.paymentId, PaymentEvent.OutgoingPayment(randomBytes32(), randomKey().publicKey, finalAmount, 200 unixms), 0 msat, None, 100 unixms)),
None,
+ None,
80 unixms)
payFsm.send(initiator, paymentSent)
@@ -256,7 +257,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(PaymentPart(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, None, 80 unixms)
payFsm.send(initiator, ps)
sender.expectMsg(ps)
eventListener.expectNoMessage(100 millis)
@@ -375,7 +376,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(PaymentPart(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, None, 100 unixms)
payFsm.send(initiator, ps)
sender.expectMsg(ps)
eventListener.expectNoMessage(100 millis)
### eclair-core/src/test/scala/fr/acinq/eclair/payment/PaymentLifecycleSpec.scala
@@ -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, _, PaymentPart(_, 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)
### eclair-core/src/test/scala/fr/acinq/eclair/payment/PaymentPacketSpec.scala
@@ -39,7 +39,7 @@ import fr.acinq.eclair.transactions.Transactions.ZeroFeeHtlcTxAnchorOutputsCommi
import fr.acinq.eclair.wire.protocol.OfferTypes.{InvoiceRequest, Offer, PaymentInfo}
import fr.acinq.eclair.wire.protocol.PaymentOnion.{FinalPayload, IntermediatePayload, OutgoingBlindedPerHopPayload}
import fr.acinq.eclair.wire.protocol._
-import fr.acinq.eclair.{BlockHeight, Bolt11Feature, Bolt12Feature, CltvExpiry, CltvExpiryDelta, EncodedNodeId, Features, MilliSatoshi, MilliSatoshiLong, ShortChannelId, TestConstants, TimestampMilli, TimestampMilliLong, TimestampSecondLong, UInt64, nodeFee, randomBytes32, randomKey}
+import fr.acinq.eclair.{BlockHeight, Bolt11Feature, Bolt12Feature, CltvExpiry, CltvExpiryDelta, EncodedNodeId, Features, MilliSatoshi, MilliSatoshiLong, ShortChannelId, TestConstants, TimestampMilli, TimestampMilliLong, TimestampSecondLong, UInt64, nodeFee, randomBytes, randomBytes32, randomKey}
import org.scalatest.BeforeAndAfterAll
import org.scalatest.funsuite.AnyFunSuite
import scodec.bits.{ByteVector, HexStringSyntax}
@@ -643,6 +643,228 @@ class PaymentPacketSpec extends AnyFunSuite with BeforeAndAfterAll {
assert(failure.isInstanceOf[FinalIncorrectCltvExpiry])
}
+ test("build htlc success fulfillment payload") {
+ // a -> b -> c -> d -> e
+ val recipient = ClearRecipient(e, Features.empty, finalAmount, finalExpiry, paymentSecret, upgradeAccountability = false)
+ val Right(payment) = buildOutgoingPayment(TestConstants.emptyOrigin, paymentHash, Route(finalAmount, hops, None), recipient, Reputation.Score.max(accountable = false))
+ val add_b = UpdateAddHtlc(randomBytes32(), 0, amount_ab, paymentHash, expiry_ab, payment.cmd.onion, None, accountable = false, None)
+ val Right(ChannelRelayPacket(_, _, packet_c, _)) = decrypt(add_b, priv_b.privateKey, Features.empty)
+ val add_c = UpdateAddHtlc(randomBytes32(), 1, amount_bc, paymentHash, expiry_bc, packet_c, None, accountable = false, None)
+ val Right(ChannelRelayPacket(_, _, packet_d, _)) = decrypt(add_c, priv_c.privateKey, Features.empty)
+ val add_d = UpdateAddHtlc(randomBytes32(), 2, amount_cd, paymentHash, expiry_cd, packet_d, None, accountable = false, None)
+ val Right(ChannelRelayPacket(_, _, packet_e, _)) = decrypt(add_d, priv_d.privateKey, Features.empty)
+ val add_e = UpdateAddHtlc(randomBytes32(), 3, amount_de, paymentHash, expiry_de, packet_e, None, accountable = false, None)
+ val Right(FinalPacket(_, payload_e, _)) = decrypt(add_e, priv_e.privateKey, Features.empty)
+ assert(payload_e.isInstanceOf[FinalPayload.Standard])
+
+ // e fulfills the payment and includes a fulfillment payload
+ val cmd_e = CMD_FULFILL_HTLC(add_e.id, paymentPreimage, Some(randomBytes(256)), Some(FulfillAttributionData(670 unixms, None, None)))
+ val fulfill_e = buildHtlcFulfill(priv_e.privateKey, useAttributionData = true, cmd_e, add_e, now = 800 unixms)
+ val cmd_d = CMD_FULFILL_HTLC(add_d.id, paymentPreimage, fulfill_e.fulfillmentPayload_opt, Some(FulfillAttributionData(650 unixms, None, fulfill_e.attribution_opt)))
+ val fulfill_d = buildHtlcFulfill(priv_d.privateKey, useAttributionData = true, cmd_d, add_d, now = 810 unixms)
+ val cmd_c = CMD_FULFILL_HTLC(add_c.id, paymentPreimage, fulfill_d.fulfillmentPayload_opt, Some(FulfillAttributionData(560 unixms, None, fulfill_d.attribution_opt)))
+ val fulfill_c = buildHtlcFulfill(priv_c.privateKey, useAttributionData = true, cmd_c, add_c, now = 815 unixms)
+ val cmd_b = CMD_FULFILL_HTLC(add_b.id, paymentPreimage, fulfill_c.fulfillmentPayload_opt, Some(FulfillAttributionData(490 unixms, None, fulfill_c.attribution_opt)))
+ val fulfill_b = buildHtlcFulfill(priv_b.privateKey, useAttributionData = true, cmd_b, add_b, now = 830 unixms)
+ val successDetails = Sphinx.SuccessPacket.decrypt(fulfill_b.fulfillmentPayload_opt, fulfill_b.attribution_opt, payment.sharedSecrets)
+ assert(successDetails.fulfillmentPayload_opt == cmd_e.fulfillmentPayload_opt)
+ assert(successDetails.holdTimes == Seq(HoldTime(300 millis, b), HoldTime(200 millis, c), HoldTime(100 millis, d), HoldTime(100 millis, e)))
+ }
+
+ test("build htlc success fulfillment payload (malicious intermediate node)") {
+ // a -> b -> c -> d -> e
+ val recipient = ClearRecipient(e, Features.empty, finalAmount, finalExpiry, paymentSecret, upgradeAccountability = false)
+ val Right(payment) = buildOutgoingPayment(TestConstants.emptyOrigin, paymentHash, Route(finalAmount, hops, None), recipient, Reputation.Score.max(accountable = false))
+ val add_b = UpdateAddHtlc(randomBytes32(), 0, amount_ab, paymentHash, expiry_ab, payment.cmd.onion, None, accountable = false, None)
+ val Right(ChannelRelayPacket(_, _, packet_c, _)) = decrypt(add_b, priv_b.privateKey, Features.empty)
+ val add_c = UpdateAddHtlc(randomBytes32(), 1, amount_bc, paymentHash, expiry_bc, packet_c, None, accountable = false, None)
+ val Right(ChannelRelayPacket(_, _, packet_d, _)) = decrypt(add_c, priv_c.privateKey, Features.empty)
+ val add_d = UpdateAddHtlc(randomBytes32(), 2, amount_cd, paymentHash, expiry_cd, packet_d, None, accountable = false, None)
+ val Right(ChannelRelayPacket(_, _, packet_e, _)) = decrypt(add_d, priv_d.privateKey, Features.empty)
+ val add_e = UpdateAddHtlc(randomBytes32(), 3, amount_de, paymentHash, expiry_de, packet_e, None, accountable = false, None)
+ val Right(FinalPacket(_, payload_e, _)) = decrypt(add_e, priv_e.privateKey, Features.empty)
+ assert(payload_e.isInstanceOf[FinalPayload.Standard])
+
+ // e fulfills the payment and includes a fulfillment payload
+ val cmd_e = CMD_FULFILL_HTLC(add_e.id, paymentPreimage, Some(randomBytes(256)), Some(FulfillAttributionData(670 unixms, None, None)))
+ val fulfill_e = buildHtlcFulfill(priv_e.privateKey, useAttributionData = true, cmd_e, add_e, now = 800 unixms)
+ val cmd_d = CMD_FULFILL_HTLC(add_d.id, paymentPreimage, fulfill_e.fulfillmentPayload_opt, Some(FulfillAttributionData(650 unixms, None, fulfill_e.attribution_opt)))
+ // d removes attribution data and modifies the fulfillment payload
+ val fulfill_d = buildHtlcFulfill(priv_d.privateKey, useAttributionData = true, cmd_d, add_d, now = 810 unixms).copy(
+ tlvStream = TlvStream(UpdateFulfillHtlcTlv.FulfillmentPayload(randomBytes(256)))
+ )
+ val cmd_c = CMD_FULFILL_HTLC(add_c.id, paymentPreimage, fulfill_d.fulfillmentPayload_opt, Some(FulfillAttributionData(560 unixms, None, fulfill_d.attribution_opt)))
+ val fulfill_c = buildHtlcFulfill(priv_c.privateKey, useAttributionData = true, cmd_c, add_c, now = 815 unixms)
+ val cmd_b = CMD_FULFILL_HTLC(add_b.id, paymentPreimage, fulfill_c.fulfillmentPayload_opt, Some(FulfillAttributionData(490 unixms, None, fulfill_c.attribution_opt)))
+ val fulfill_b = buildHtlcFulfill(priv_b.privateKey, useAttributionData = true, cmd_b, add_b, now = 830 unixms)
+ val successDetails = Sphinx.SuccessPacket.decrypt(fulfill_b.fulfillmentPayload_opt, fulfill_b.attribution_opt, payment.sharedSecrets)
+ // a can attribute the issue to either c or d because it is missing attribution data after c
+ assert(successDetails.fulfillmentPayload_opt.isEmpty)
+ assert(successDetails.holdTimes == Seq(HoldTime(300 millis, b), HoldTime(200 millis, c)))
+ }
+
+ test("build htlc success fulfillment payload (blinded payment)") {
+ // a -> b -> c -> d -> e, blinded after c
+ val (_, route, recipient) = longBlindedHops(hex"0451")
+ val Right(payment) = buildOutgoingPayment(TestConstants.emptyOrigin, paymentHash, route, recipient, Reputation.Score.max(accountable = false))
+ val add_b = UpdateAddHtlc(randomBytes32(), 0, payment.cmd.amount, payment.cmd.paymentHash, payment.cmd.cltvExpiry, payment.cmd.onion, payment.cmd.nextPathKey_opt, accountable = false, payment.cmd.fundingFee_opt)
+ val Right(ChannelRelayPacket(_, _, packet_c, _)) = decrypt(add_b, priv_b.privateKey, Features.empty)
+ val add_c = UpdateAddHtlc(randomBytes32(), 1, amount_bc, paymentHash, expiry_bc, packet_c, None, accountable = false, None)
+ val Right(ChannelRelayPacket(_, payload_c, packet_d, _)) = decrypt(add_c, priv_c.privateKey, Features(RouteBlinding -> Optional))
+ val pathKey_d = payload_c.asInstanceOf[IntermediatePayload.ChannelRelay.Blinded].nextPathKey
+ val add_d = UpdateAddHtlc(randomBytes32(), 2, amount_cd, paymentHash, expiry_cd, packet_d, Some(pathKey_d), accountable = false, None)
+ val Right(ChannelRelayPacket(_, payload_d, packet_e, _)) = decrypt(add_d, priv_d.privateKey, Features(RouteBlinding -> Optional))
+ val pathKey_e = payload_d.asInstanceOf[IntermediatePayload.ChannelRelay.Blinded].nextPathKey
+ val add_e = UpdateAddHtlc(randomBytes32(), 3, amount_de, paymentHash, expiry_de, packet_e, Some(pathKey_e), accountable = false, None)
+ val Right(FinalPacket(_, payload_e, _)) = decrypt(add_e, priv_e.privateKey, Features(RouteBlinding -> Optional))
+ assert(payload_e.isInstanceOf[FinalPayload.Blinded])
+
+ // all nodes include the fulfillment payload, even though attribution data is not included inside the blinded path
+ val cmd_e = CMD_FULFILL_HTLC(add_e.id, paymentPreimage, Some(randomBytes(256)), Some(FulfillAttributionData(820 unixms, None, None)))
+ val fulfill_e = buildHtlcFulfill(priv_e.privateKey, useAttributionData = true, cmd_e, add_e, now = 850 unixms)
+ assert(fulfill_e.attribution_opt.isEmpty)
+ assert(fulfill_e.fulfillmentPayload_opt.nonEmpty)
+ val cmd_d = CMD_FULFILL_HTLC(add_d.id, paymentPreimage, fulfill_e.fulfillmentPayload_opt, Some(FulfillAttributionData(810 unixms, None, fulfill_e.attribution_opt)))
+ val fulfill_d = buildHtlcFulfill(priv_d.privateKey, useAttributionData = true, cmd_d, add_d, now = 850 unixms)
+ assert(fulfill_d.attribution_opt.isEmpty)
+ assert(fulfill_d.fulfillmentPayload_opt.nonEmpty)
+ val cmd_c = CMD_FULFILL_HTLC(add_c.id, paymentPreimage, fulfill_d.fulfillmentPayload_opt, Some(FulfillAttributionData(750 unixms, None, fulfill_d.attribution_opt)))
+ val fulfill_c = buildHtlcFulfill(priv_c.privateKey, useAttributionData = true, cmd_c, add_c, now = 860 unixms)
+ assert(fulfill_c.attribution_opt.nonEmpty)
+ assert(fulfill_c.fulfillmentPayload_opt.nonEmpty)
+ val cmd_b = CMD_FULFILL_HTLC(add_b.id, paymentPreimage, fulfill_c.fulfillmentPayload_opt, Some(FulfillAttributionData(670 unixms, None, fulfill_c.attribution_opt)))
+ val fulfill_b = buildHtlcFulfill(priv_b.privateKey, useAttributionData = true, cmd_b, add_b, now = 870 unixms)
+ val successDetails = Sphinx.SuccessPacket.decrypt(fulfill_b.fulfillmentPayload_opt, fulfill_b.attribution_opt, payment.sharedSecrets)
+ assert(successDetails.fulfillmentPayload_opt == cmd_e.fulfillmentPayload_opt)
+ assert(successDetails.holdTimes == Seq(HoldTime(200 millis, b), HoldTime(100 millis, c)))
+ }
+
+ test("build htlc success fulfillment payload (trampoline payment)") {
+ // Create a trampoline payment to e:
+ // .--> d --.
+ // / \
+ // b -> c e
+ val invoiceFeatures = Features[Bolt11Feature](VariableLengthOnion -> Mandatory, PaymentSecret -> Mandatory, BasicMultiPartPayment -> Optional, Features.TrampolinePaymentPrototype -> Optional)
+ val invoice = Bolt11Invoice(Block.RegtestGenesisBlock.hash, Some(finalAmount), paymentHash, priv_e.privateKey, Left("invoice"), CltvExpiryDelta(12), paymentSecret = paymentSecret, features = invoiceFeatures)
+ val payment = TrampolinePayment.buildOutgoingPayment(c, invoice, finalExpiry)
+ val add_c = UpdateAddHtlc(randomBytes32(), 0, payment.trampolineAmount, paymentHash, payment.trampolineExpiry, payment.onion.packet, None, accountable = false, None)
+ val Right(RelayToTrampolinePacket(_, _, payload_c, trampolinePacket_e, _)) = decrypt(add_c, priv_c.privateKey, Features.empty)
+ val (add_d, sharedSecrets_c) = {
+ // c finds a path c->d->e
+ val payloads = Seq(
+ NodePayload(d, PaymentOnion.IntermediatePayload.ChannelRelay.Standard(channelUpdate_de.shortChannelId, payload_c.amountToForward, payload_c.outgoingCltv, upgradeAccountability = false)),
+ NodePayload(e, PaymentOnion.FinalPayload.Standard.createTrampolinePayload(payload_c.amountToForward, payload_c.amountToForward, payload_c.outgoingCltv, paymentSecret, trampolinePacket_e, upgradeAccountability = false))
+ )
+ val onion_d = OutgoingPaymentPacket.buildOnion(payloads, paymentHash, Some(PaymentOnionCodecs.paymentOnionPayloadLength)).toOption.get
+ val add_d = UpdateAddHtlc(randomBytes32(), 0, payload_c.amountToForward + 500.msat, paymentHash, payload_c.outgoingCltv + CltvExpiryDelta(36), onion_d.packet, None, accountable = false, None)
+ (add_d, onion_d.sharedSecrets)
+ }
+ val Right(ChannelRelayPacket(_, _, packet_e, _)) = decrypt(add_d, priv_d.privateKey, Features.empty)
+ val add_e = UpdateAddHtlc(randomBytes32(), 3, payload_c.amountToForward, paymentHash, payload_c.outgoingCltv, packet_e, None, accountable = false, None)
+ val Right(FinalPacket(_, payload_e, _)) = decrypt(add_e, priv_e.privateKey, Features.empty)
+ assert(payload_e.isInstanceOf[FinalPayload.Standard])
+
+ // e fulfills the payment and includes a fulfillment payload
+ val cmd_e = CMD_FULFILL_HTLC(add_e.id, paymentPreimage, Some(randomBytes(256)), Some(FulfillAttributionData(800 unixms, Some(800 unixms), None)))
+ val fulfill_e = buildHtlcFulfill(priv_e.privateKey, useAttributionData = true, cmd_e, add_e, now = 850 unixms)
+ val cmd_d = CMD_FULFILL_HTLC(add_d.id, paymentPreimage, fulfill_e.fulfillmentPayload_opt, Some(FulfillAttributionData(750 unixms, None, fulfill_e.attribution_opt)))
+ val fulfill_d = buildHtlcFulfill(priv_d.privateKey, useAttributionData = true, cmd_d, add_d, now = 860 unixms)
+ // c needs to unwrap the fulfillment payload and attribution data from downstream
+ val success_c = Sphinx.SuccessPacket.decrypt(fulfill_d.fulfillmentPayload_opt, fulfill_d.attribution_opt, sharedSecrets_c, lastSecretIsRecipient = false)
+ assert(success_c.fulfillmentPayload_opt.nonEmpty)
+ assert(success_c.remainingAttribution_opt.nonEmpty)
+ assert(success_c.holdTimes == Seq(HoldTime(100 millis, d), HoldTime(0 millis, e)))
+ val cmd_c = CMD_FULFILL_HTLC(add_c.id, paymentPreimage, success_c.fulfillmentPayload_opt, Some(FulfillAttributionData(700 unixms, Some(700 unixms), success_c.remainingAttribution_opt)))
+ val fulfill_c = buildHtlcFulfill(priv_c.privateKey, useAttributionData = true, cmd_c, add_c, now = 875 unixms)
+ val successDetails = Sphinx.SuccessPacket.decrypt(fulfill_c.fulfillmentPayload_opt, fulfill_c.attribution_opt, payment.onion.sharedSecrets ++ payment.trampolineOnion.sharedSecrets)
+ assert(successDetails.fulfillmentPayload_opt == cmd_e.fulfillmentPayload_opt)
+ assert(successDetails.holdTimes == Seq(HoldTime(100 millis, c), HoldTime(100 millis, c), HoldTime(0 millis, e)))
+ }
+
+ test("build htlc success fulfillment payload (maximum size)") {
+ // a -> b -> c -> d -> e
+ val recipient = ClearRecipient(e, Features.empty, finalAmount, finalExpiry, paymentSecret, upgradeAccountability = false)
+ val Right(payment) = buildOutgoingPayment(TestConstants.emptyOrigin, paymentHash, Route(finalAmount, hops, None), recipient, Reputation.Score.max(accountable = false))
+ val add_b = UpdateAddHtlc(randomBytes32(), 0, amount_ab, paymentHash, expiry_ab, payment.cmd.onion, None, accountable = false, None)
+ val Right(ChannelRelayPacket(_, _, packet_c, _)) = decrypt(add_b, priv_b.privateKey, Features.empty)
+ val add_c = UpdateAddHtlc(randomBytes32(), 1, amount_bc, paymentHash, expiry_bc, packet_c, None, accountable = false, None)
+ val Right(ChannelRelayPacket(_, _, packet_d, _)) = decrypt(add_c, priv_c.privateKey, Features.empty)
+ val add_d = UpdateAddHtlc(randomBytes32(), 2, amount_cd, paymentHash, expiry_cd, packet_d, None, accountable = false, None)
+ val Right(ChannelRelayPacket(_, _, packet_e, _)) = decrypt(add_d, priv_d.privateKey, Features.empty)
+ val add_e = UpdateAddHtlc(randomBytes32(), 3, amount_de, paymentHash, expiry_de, packet_e, None, accountable = false, None)
+
+ // e fulfills the payment with the largest fulfillment payload it is allowed to send.
+ val cmd_e = CMD_FULFILL_HTLC(add_e.id, paymentPreimage, Some(randomBytes(Sphinx.SuccessPacket.MAX_PAYLOAD_LENGTH)), Some(FulfillAttributionData(670 unixms, None, None)))
+ val fulfill_e = buildHtlcFulfill(priv_e.privateKey, useAttributionData = true, cmd_e, add_e, now = 800 unixms)
+ val cmd_d = CMD_FULFILL_HTLC(add_d.id, paymentPreimage, fulfill_e.fulfillmentPayload_opt, Some(FulfillAttributionData(650 unixms, None, fulfill_e.attribution_opt)))
+ val fulfill_d = buildHtlcFulfill(priv_d.privateKey, useAttributionData = true, cmd_d, add_d, now = 810 unixms)
+ val cmd_c = CMD_FULFILL_HTLC(add_c.id, paymentPreimage, fulfill_d.fulfillmentPayload_opt, Some(FulfillAttributionData(560 unixms, None, fulfill_d.attribution_opt)))
+ val fulfill_c = buildHtlcFulfill(priv_c.privateKey, useAttributionData = true, cmd_c, add_c, now = 815 unixms)
+ val cmd_b = CMD_FULFILL_HTLC(add_b.id, paymentPreimage, fulfill_c.fulfillmentPayload_opt, Some(FulfillAttributionData(490 unixms, None, fulfill_c.attribution_opt)))
+ val fulfill_b = buildHtlcFulfill(priv_b.privateKey, useAttributionData = true, cmd_b, add_b, now = 830 unixms)
+
+ // Every node relays a payload of the maximum size, without exceeding the maximum size of a lightning message.
+ Seq(fulfill_e, fulfill_d, fulfill_c, fulfill_b).foreach(fulfill => {
+ assert(fulfill.fulfillmentPayload_opt.get.size == Sphinx.SuccessPacket.MAX_LENGTH)
+ assert(fulfill.attribution_opt.get.size == Sphinx.Attribution.totalLength)
+ assert(LightningMessageCodecs.lightningMessageCodec.encode(fulfill).require.bytes.size <= 65535)
+ })
+
+ val successDetails = Sphinx.SuccessPacket.decrypt(fulfill_b.fulfillmentPayload_opt, fulfill_b.attribution_opt, payment.sharedSecrets)
+ assert(successDetails.fulfillmentPayload_opt == cmd_e.fulfillmentPayload_opt)
+ assert(successDetails.holdTimes == Seq(HoldTime(300 millis, b), HoldTime(200 millis, c), HoldTime(100 millis, d), HoldTime(100 millis, e)))
+ }
+
+ test("build htlc success fulfillment payload (payload too large)") {
+ // a -> b -> c -> d -> e
+ val recipient = ClearRecipient(e, Features.empty, finalAmount, finalExpiry, paymentSecret, upgradeAccountability = false)
+ val Right(payment) = buildOutgoingPayment(TestConstants.emptyOrigin, paymentHash, Route(finalAmount, hops, None), recipient, Reputation.Score.max(accountable = false))
+ val add_b = UpdateAddHtlc(randomBytes32(), 0, amount_ab, paymentHash, expiry_ab, payment.cmd.onion, None, accountable = false, None)
+ val Right(ChannelRelayPacket(_, _, packet_c, _)) = decrypt(add_b, priv_b.privateKey, Features.empty)
+ val add_c = UpdateAddHtlc(randomBytes32(), 1, amount_bc, paymentHash, expiry_bc, packet_c, None, accountable = false, None)
+ val Right(ChannelRelayPacket(_, _, packet_d, _)) = decrypt(add_c, priv_c.privateKey, Features.empty)
+ val add_d = UpdateAddHtlc(randomBytes32(), 2, amount_cd, paymentHash, expiry_cd, packet_d, None, accountable = false, None)
+ val Right(ChannelRelayPacket(_, _, packet_e, _)) = decrypt(add_d, priv_d.privateKey, Features.empty)
+ val add_e = UpdateAddHtlc(randomBytes32(), 3, amount_de, paymentHash, expiry_de, packet_e, None, accountable = false, None)
+
+ // e is asked to include a payload that is larger than what it is allowed to send: it must truncate it, otherwise its
+ // peers would reject the resulting update_fulfill_htlc (see Channel.checkFulfillmentPayload).
+ for (payloadLength <- Seq(Sphinx.SuccessPacket.MAX_PAYLOAD_LENGTH + 1, Sphinx.SuccessPacket.MAX_LENGTH, 60_000)) {
+ val cmd_e = CMD_FULFILL_HTLC(add_e.id, paymentPreimage, Some(randomBytes(payloadLength)), Some(FulfillAttributionData(670 unixms, None, None)))
+ val fulfill_e = buildHtlcFulfill(priv_e.privateKey, useAttributionData = true, cmd_e, add_e, now = 800 unixms)
+ assert(fulfill_e.fulfillmentPayload_opt.get.size == Sphinx.SuccessPacket.MAX_LENGTH)
+ }
+
+ // b receives a fulfillment payload that is larger than what the specification allows: it must truncate it,
+ // otherwise it wouldn't be able to send the resulting update_fulfill_htlc to a.
+ val cmd_b = CMD_FULFILL_HTLC(add_b.id, paymentPreimage, Some(randomBytes(60_000)), Some(FulfillAttributionData(490 unixms, None, Some(randomBytes(Sphinx.Attribution.totalLength)))))
+ val fulfill_b = buildHtlcFulfill(priv_b.privateKey, useAttributionData = true, cmd_b, add_b, now = 830 unixms)
+ assert(fulfill_b.fulfillmentPayload_opt.get.size == Sphinx.SuccessPacket.MAX_LENGTH)
+ assert(fulfill_b.attribution_opt.get.size == Sphinx.Attribution.totalLength)
+ assert(LightningMessageCodecs.lightningMessageCodec.encode(fulfill_b).require.bytes.size <= 65535)
+ }
+
+ test("build htlc success fulfillment payload (blinded trampoline payment)") {
+ // We are the recipient of a trampoline payment and are hidden inside a blinded path: we must include the fulfillment
+ // payload, but we must not reveal any attribution data.
+ val pathKey = randomKey()
+ val blindedNodeId = Sphinx.RouteBlinding.derivePrivateKey(priv_e.privateKey, pathKey.publicKey).publicKey
+ val trampolinePayload = PaymentOnion.FinalPayload.Standard.createPayload(finalAmount, finalAmount, finalExpiry, paymentSecret, upgradeAccountability = false)
+ val trampolineOnion = OutgoingPaymentPacket.buildOnion(NodePayload(e, trampolinePayload) :: Nil, paymentHash, None).toOption.get
+ val outerPayload = PaymentOnion.FinalPayload.Standard.createPayload(finalAmount, finalAmount, finalExpiry, paymentSecret, trampolineOnion_opt = Some(trampolineOnion.packet), upgradeAccountability = false)
+ val outerOnion = OutgoingPaymentPacket.buildOnion(NodePayload(blindedNodeId, outerPayload) :: Nil, paymentHash, Some(PaymentOnionCodecs.paymentOnionPayloadLength)).toOption.get
+ val add_e = UpdateAddHtlc(randomBytes32(), 3, amount_de, paymentHash, expiry_de, outerOnion.packet, Some(pathKey.publicKey), accountable = false, None)
+
+ val cmd_e = CMD_FULFILL_HTLC(add_e.id, paymentPreimage, Some(randomBytes(256)), Some(FulfillAttributionData(820 unixms, Some(820 unixms), None)))
+ val fulfill_e = buildHtlcFulfill(priv_e.privateKey, useAttributionData = true, cmd_e, add_e, now = 850 unixms)
+ assert(fulfill_e.attribution_opt.isEmpty)
+ assert(fulfill_e.fulfillmentPayload_opt.nonEmpty)
+ // The sender applies the outer onion shared secret, then the trampoline onion shared secret.
+ val successDetails = Sphinx.SuccessPacket.decrypt(fulfill_e.fulfillmentPayload_opt, fulfill_e.attribution_opt, outerOnion.sharedSecrets ++ trampolineOnion.sharedSecrets)
+ assert(successDetails.fulfillmentPayload_opt == cmd_e.fulfillmentPayload_opt)
+ assert(successDetails.holdTimes.isEmpty)
+ }
+
test("build htlc failure onion") {
// a -> b -> c -> d -> e
val recipient = ClearRecipient(e, Features.empty, finalAmount, finalExpiry, paymentSecret, upgradeAccountability = false)
@@ -672,6 +894,22 @@ class PaymentPacketSpec extends AnyFunSuite with BeforeAndAfterAll {
assert(decryptedFailure == failure)
}
+ test("build htlc failure onion (failure packet too large)") {
+ // a -> b -> c -> d -> e
+ val recipient = ClearRecipient(e, Features.empty, finalAmount, finalExpiry, paymentSecret, upgradeAccountability = false)
+ val Right(payment) = buildOutgoingPayment(TestConstants.emptyOrigin, paymentHash, Route(finalAmount, hops, None), recipient, Reputation.Score.max(accountable = false))
+ val add_b = UpdateAddHtlc(randomBytes32(), 0, amount_ab, paymentHash, expiry_ab, payment.cmd.onion, None, accountable = false, None)
+
+ // b receives a failure packet that is larger than what the specification allows: it must truncate it, otherwise it
+ // wouldn't be able to send the resulting update_fail_htlc to a.
+ val reason = FailureReason.EncryptedDownstreamFailure(randomBytes(60_000), Some(randomBytes(Sphinx.Attribution.totalLength)))
+ val cmd_b = CMD_FAIL_HTLC(add_b.id, reason, Some(FailureAttributionData(490 unixms, None)))
+ val Right(fail_b: UpdateFailHtlc) = buildHtlcFailure(priv_b.privateKey, useAttributableFailures = true, cmd_b, add_b, now = 830 unixms)
+ assert(fail_b.reason.size == Sphinx.FailurePacket.MAX_LENGTH)
+ assert(fail_b.attribution_opt.get.size == Sphinx.Attribution.totalLength)
+ assert(LightningMessageCodecs.lightningMessageCodec.encode(fail_b).require.bytes.size <= 65535)
+ }
+
test("build htlc failure onion with attribution data") {
// a -> b -> c -> d -> e
val recipient = ClearRecipient(e, Features.empty, finalAmount, finalExpiry, paymentSecret, upgradeAccountability = false)
### eclair-core/src/test/scala/fr/acinq/eclair/payment/PostRestartHtlcCleanerSpec.scala
@@ -263,8 +263,8 @@ class PostRestartHtlcCleanerSpec extends TestKitBaseClass with FixtureAnyFunSuit
system.eventStream.publish(ChannelStateChanged(channel.ref, channels.head.channelId, system.deadLetters, a, OFFLINE, NORMAL, Some(channels.head.commitments)))
val expected1 = Set(
CMD_FAIL_HTLC(0, FailureReason.LocalFailure(TemporaryNodeFailure()), None, commit = true),
- CMD_FULFILL_HTLC(3, preimage, None, commit = true),
- CMD_FULFILL_HTLC(5, preimage, None, commit = true),
+ CMD_FULFILL_HTLC(3, preimage, None, None, commit = true),
+ CMD_FULFILL_HTLC(5, preimage, None, None, commit = true),
CMD_FAIL_HTLC(7, FailureReason.LocalFailure(TemporaryNodeFailure()), None, commit = true)
)
val received1 = expected1.map(_ => channel.expectMsgType[Command])
@@ -276,7 +276,7 @@ class PostRestartHtlcCleanerSpec extends TestKitBaseClass with FixtureAnyFunSuit
val expected2 = Set(
CMD_FAIL_HTLC(1, FailureReason.LocalFailure(TemporaryNodeFailure()), None, commit = true),
CMD_FAIL_HTLC(3, FailureReason.LocalFailure(TemporaryNodeFailure()), None, commit = true),
- CMD_FULFILL_HTLC(4, preimage, None, commit = true),
+ CMD_FULFILL_HTLC(4, preimage, None, None, commit = true),
CMD_FAIL_HTLC(9, FailureReason.LocalFailure(TemporaryNodeFailure()), None, commit = true)
)
val received2 = expected2.map(_ => channel.expectMsgType[Command])
@@ -493,8 +493,8 @@ class PostRestartHtlcCleanerSpec extends TestKitBaseClass with FixtureAnyFunSuit
val origin_2 = Origin.Cold(Upstream.Cold(upstream_2))
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))
+ Register.Forward(replyTo = null, channelId_ab_1, CMD_FULFILL_HTLC(5, preimage2, None, None, commit = true)),
+ Register.Forward(replyTo = null, channelId_ab_2, CMD_FULFILL_HTLC(9, preimage2, None, None, commit = true))
)
// Payment 3 should not be failed: we are still waiting for on-chain confirmation.
@@ -511,7 +511,7 @@ class PostRestartHtlcCleanerSpec extends TestKitBaseClass with FixtureAnyFunSuit
buildHtlcIn(4, channelId_ab_1, randomBytes32()),
)
val channelData = ChannelCodecsSpec.makeChannelDataNormal(htlc_ab, Map.empty)
- nodeParams.db.pendingCommands.addSettlementCommand(channelId_ab_1, CMD_FULFILL_HTLC(1, randomBytes32(), None))
+ nodeParams.db.pendingCommands.addSettlementCommand(channelId_ab_1, CMD_FULFILL_HTLC(1, randomBytes32(), None, None))
nodeParams.db.pendingCommands.addSettlementCommand(channelId_ab_1, CMD_FAIL_HTLC(4, FailureReason.LocalFailure(PermanentChannelFailure()), None))
val (_, postRestart) = f.createRelayer(nodeParams)
@@ -608,7 +608,7 @@ class PostRestartHtlcCleanerSpec extends TestKitBaseClass with FixtureAnyFunSuit
register.expectNoMessage(100 millis)
sender.send(relayer, buildForwardFulfill(testCase.downstream, testCase.upstream, preimage1))
- register.expectMsg(Register.Forward(null, testCase.upstream.originChannelId, CMD_FULFILL_HTLC(testCase.upstream.originHtlcId, preimage1, None, commit = true)))
+ register.expectMsg(Register.Forward(null, testCase.upstream.originChannelId, CMD_FULFILL_HTLC(testCase.upstream.originHtlcId, preimage1, None, None, commit = true)))
eventListener.expectMsgType[ChannelPaymentRelayed]
sender.send(relayer, buildForwardFulfill(testCase.downstream, testCase.upstream, preimage1))
@@ -659,7 +659,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, None, commit = true))
}.toSet)
sender.send(relayer, buildForwardFulfill(testCase.downstream_1_1, testCase.upstream_1, preimage1))
@@ -668,7 +668,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, None, commit = true))
}.head)
sender.send(relayer, buildForwardFulfill(testCase.downstream_2_2, testCase.upstream_2, preimage2))
@@ -688,7 +688,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, None, commit = true))
}.head)
sender.send(relayer, buildForwardFail(testCase.downstream_2_3, testCase.upstream_2))
### eclair-core/src/test/scala/fr/acinq/eclair/payment/relay/NodeRelayerSpec.scala
@@ -49,7 +49,7 @@ import fr.acinq.eclair.wire.protocol.PaymentOnion.{FinalPayload, IntermediatePay
import fr.acinq.eclair.wire.protocol.RouteBlindingEncryptedDataCodecs.blindedRouteDataCodec
import fr.acinq.eclair.wire.protocol.RouteBlindingEncryptedDataTlv.{AllowedFeatures, PathId, PaymentConstraints}
import fr.acinq.eclair.wire.protocol._
-import fr.acinq.eclair.{Alias, BlockHeight, Bolt11Feature, Bolt12Feature, CltvExpiry, CltvExpiryDelta, EncodedNodeId, FeatureSupport, Features, MilliSatoshi, MilliSatoshiLong, NodeParams, RealShortChannelId, ShortChannelId, TestConstants, TimestampMilli, TimestampMilliLong, UInt64, randomBytes32, randomKey}
+import fr.acinq.eclair.{Alias, BlockHeight, Bolt11Feature, Bolt12Feature, CltvExpiry, CltvExpiryDelta, EncodedNodeId, FeatureSupport, Features, MilliSatoshi, MilliSatoshiLong, NodeParams, RealShortChannelId, ShortChannelId, TestConstants, TimestampMilli, TimestampMilliLong, UInt64, randomBytes, randomBytes32, randomKey}
import org.scalatest.funsuite.FixtureAnyFunSuiteLike
import org.scalatest.{Outcome, Tag}
import scodec.bits.{ByteVector, HexStringSyntax}
@@ -393,11 +393,11 @@ class NodeRelayerSpec extends ScalaTestWithActorTestKit(ConfigFactory.load("appl
val nodeRelayerAdapters = outgoingPayment.replyTo
// A first downstream HTLC is fulfilled: we should immediately forward the fulfill upstream.
- nodeRelayerAdapters ! PreimageReceived(paymentHash, paymentPreimage, None)
+ nodeRelayerAdapters ! PreimageReceived(paymentHash, paymentPreimage, None, None)
incomingAsyncPayment.foreach { p =>
val fwd = register.expectMessageType[Register.Forward[CMD_FULFILL_HTLC]]
assert(fwd.channelId == p.add.channelId)
- assert(fwd.message == CMD_FULFILL_HTLC(p.add.id, paymentPreimage, Some(FulfillAttributionData(p.receivedAt, Some(incomingAsyncPayment.last.receivedAt), None)), commit = true))
+ assert(fwd.message == CMD_FULFILL_HTLC(p.add.id, paymentPreimage, None, Some(FulfillAttributionData(p.receivedAt, Some(incomingAsyncPayment.last.receivedAt), None)), commit = true))
}
// Once all the downstream payments have settled, we should emit the relayed event.
@@ -622,15 +622,15 @@ class NodeRelayerSpec extends ScalaTestWithActorTestKit(ConfigFactory.load("appl
val nodeRelayerAdapters = outgoingPayment.replyTo
// A first downstream HTLC is fulfilled: we should immediately forward the fulfill upstream.
- nodeRelayerAdapters ! PreimageReceived(paymentHash, paymentPreimage, None)
+ nodeRelayerAdapters ! PreimageReceived(paymentHash, paymentPreimage, None, None)
incomingMultiPart.foreach { p =>
val fwd = register.expectMessageType[Register.Forward[CMD_FULFILL_HTLC]]
assert(fwd.channelId == p.add.channelId)
- assert(fwd.message == CMD_FULFILL_HTLC(p.add.id, paymentPreimage, Some(FulfillAttributionData(p.receivedAt, Some(incomingMultiPart.last.receivedAt), None)), commit = true))
+ assert(fwd.message == CMD_FULFILL_HTLC(p.add.id, paymentPreimage, None, Some(FulfillAttributionData(p.receivedAt, Some(incomingMultiPart.last.receivedAt), None)), commit = true))
}
// If the payment FSM sends us duplicate preimage events, we should not fulfill a second time upstream.
- nodeRelayerAdapters ! PreimageReceived(paymentHash, paymentPreimage, None)
+ nodeRelayerAdapters ! PreimageReceived(paymentHash, paymentPreimage, None, None)
register.expectNoMessage(100 millis)
// Once all the downstream payments have settled, we should emit the relayed event.
@@ -643,6 +643,68 @@ class NodeRelayerSpec extends ScalaTestWithActorTestKit(ConfigFactory.load("appl
register.expectNoMessage(100 millis)
}
+ test("relay incoming multi-part payment with fulfillment payload") { f =>
+ import f._
+
+ // Receive an upstream multi-part payment.
+ val (nodeRelayer, parent) = f.createNodeRelay(incomingMultiPart.head)
+ incomingMultiPart.foreach(p => nodeRelayer ! NodeRelay.Relay(p, randomKey().publicKey, 0.01))
+
+ val getPeerInfo = register.expectMessageType[Register.ForwardNodeId[Peer.GetPeerInfo]](100 millis)
+ getPeerInfo.message.replyTo.foreach(_ ! Peer.PeerNotFound(getPeerInfo.nodeId))
+
+ mockPayFSM.expectMessageType[SendPaymentConfig]
+ val outgoingPayment = mockPayFSM.expectMessageType[SendMultiPartPayment]
+ validateOutgoingPayment(outgoingPayment)
+ val nodeRelayerAdapters = outgoingPayment.replyTo
+
+ // The downstream payment is fulfilled with a fulfillment payload that we must relay upstream.
+ val fulfillmentPayload = randomBytes(500)
+ val attribution = randomBytes(Sphinx.Attribution.totalLength)
+ nodeRelayerAdapters ! PreimageReceived(paymentHash, paymentPreimage, Some(fulfillmentPayload), Some(attribution))
+ incomingMultiPart.foreach { p =>
+ val fwd = register.expectMessageType[Register.Forward[CMD_FULFILL_HTLC]]
+ assert(fwd.channelId == p.add.channelId)
+ assert(fwd.message == CMD_FULFILL_HTLC(p.add.id, paymentPreimage, Some(fulfillmentPayload), Some(FulfillAttributionData(p.receivedAt, Some(incomingMultiPart.last.receivedAt), Some(attribution))), commit = true))
+ }
+
+ nodeRelayerAdapters ! createSuccessEvent()
+ eventListener.expectMessageType[TrampolinePaymentRelayed]
+ parent.expectMessageType[NodeRelayer.RelayComplete]
+ register.expectNoMessage(100 millis)
+ }
+
+ test("relay incoming multi-part payment with fulfillment payload (without preimage event)") { f =>
+ import f._
+
+ // Receive an upstream multi-part payment.
+ val (nodeRelayer, parent) = f.createNodeRelay(incomingMultiPart.head)
+ incomingMultiPart.foreach(p => nodeRelayer ! NodeRelay.Relay(p, randomKey().publicKey, 0.01))
+
+ val getPeerInfo = register.expectMessageType[Register.ForwardNodeId[Peer.GetPeerInfo]](100 millis)
+ getPeerInfo.message.replyTo.foreach(_ ! Peer.PeerNotFound(getPeerInfo.nodeId))
+
+ mockPayFSM.expectMessageType[SendPaymentConfig]
+ val outgoingPayment = mockPayFSM.expectMessageType[SendMultiPartPayment]
+ validateOutgoingPayment(outgoingPayment)
+ val nodeRelayerAdapters = outgoingPayment.replyTo
+
+ // Some payment FSMs only send us the final PaymentSent event, without a preceding PreimageReceived event: we must
+ // then take the fulfillment payload and the attribution data from that event, without mixing them up.
+ val fulfillmentPayload = randomBytes(500)
+ val attribution = randomBytes(Sphinx.Attribution.totalLength)
+ nodeRelayerAdapters ! createSuccessEvent().copy(fulfillmentPayload_opt = Some(fulfillmentPayload), remainingAttribution_opt = Some(attribution))
+ incomingMultiPart.foreach { p =>
+ val fwd = register.expectMessageType[Register.Forward[CMD_FULFILL_HTLC]]
+ assert(fwd.channelId == p.add.channelId)
+ assert(fwd.message == CMD_FULFILL_HTLC(p.add.id, paymentPreimage, Some(fulfillmentPayload), Some(FulfillAttributionData(p.receivedAt, Some(incomingMultiPart.last.receivedAt), Some(attribution))), commit = true))
+ }
+
+ eventListener.expectMessageType[TrampolinePaymentRelayed]
+ parent.expectMessageType[NodeRelayer.RelayComplete]
+ register.expectNoMessage(100 millis)
+ }
+
test("ignore downstream failures after fulfill") { f =>
import f._
@@ -660,11 +722,11 @@ class NodeRelayerSpec extends ScalaTestWithActorTestKit(ConfigFactory.load("appl
val nodeRelayerAdapters = outgoingPayment.replyTo
// A first downstream HTLC is fulfilled: we immediately forward the fulfill upstream.
- nodeRelayerAdapters ! PreimageReceived(paymentHash, paymentPreimage, None)
+ nodeRelayerAdapters ! PreimageReceived(paymentHash, paymentPreimage, None, None)
val fulfills = incomingMultiPart.map { p =>
val fwd = register.expectMessageType[Register.Forward[CMD_FULFILL_HTLC]]
assert(fwd.channelId == p.add.channelId)
- assert(fwd.message == CMD_FULFILL_HTLC(p.add.id, paymentPreimage, Some(FulfillAttributionData(p.receivedAt, Some(incomingMultiPart.last.receivedAt), None)), commit = true))
+ assert(fwd.message == CMD_FULFILL_HTLC(p.add.id, paymentPreimage, None, Some(FulfillAttributionData(p.receivedAt, Some(incomingMultiPart.last.receivedAt), None)), commit = true))
fwd
}
// We store the commands in our DB in case we restart before relaying them upstream.
@@ -703,11 +765,11 @@ class NodeRelayerSpec extends ScalaTestWithActorTestKit(ConfigFactory.load("appl
// those are adapters for pay-fsm messages
val nodeRelayerAdapters = outgoingPayment.replyTo
- nodeRelayerAdapters ! PreimageReceived(paymentHash, paymentPreimage, None)
+ nodeRelayerAdapters ! PreimageReceived(paymentHash, paymentPreimage, None, None)
val incomingAdd = incomingSinglePart.add
val fwd = register.expectMessageType[Register.Forward[CMD_FULFILL_HTLC]]
assert(fwd.channelId == incomingAdd.channelId)
- assert(fwd.message == CMD_FULFILL_HTLC(incomingAdd.id, paymentPreimage, Some(FulfillAttributionData(incomingSinglePart.receivedAt, Some(incomingSinglePart.receivedAt), None)), commit = true))
+ assert(fwd.message == CMD_FULFILL_HTLC(incomingAdd.id, paymentPreimage, None, Some(FulfillAttributionData(incomingSinglePart.receivedAt, Some(incomingSinglePart.receivedAt), None)), commit = true))
nodeRelayerAdapters ! createSuccessEvent()
val relayEvent = eventListener.expectMessageType[TrampolinePaymentRelayed]
@@ -834,11 +896,11 @@ class NodeRelayerSpec extends ScalaTestWithActorTestKit(ConfigFactory.load("appl
// those are adapters for pay-fsm messages
val nodeRelayerAdapters = outgoingPayment.replyTo
- nodeRelayerAdapters ! PreimageReceived(paymentHash, paymentPreimage, None)
+ nodeRelayerAdapters ! PreimageReceived(paymentHash, paymentPreimage, None, None)
incomingMultiPart.foreach { p =>
val fwd = register.expectMessageType[Register.Forward[CMD_FULFILL_HTLC]]
assert(fwd.channelId == p.add.channelId)
- assert(fwd.message == CMD_FULFILL_HTLC(p.add.id, paymentPreimage, Some(FulfillAttributionData(p.receivedAt, Some(incomingMultiPart.last.receivedAt), None)), commit = true))
+ assert(fwd.message == CMD_FULFILL_HTLC(p.add.id, paymentPreimage, None, Some(FulfillAttributionData(p.receivedAt, Some(incomingMultiPart.last.receivedAt), None)), commit = true))
}
nodeRelayerAdapters ! createSuccessEvent()
@@ -882,11 +944,11 @@ class NodeRelayerSpec extends ScalaTestWithActorTestKit(ConfigFactory.load("appl
// those are adapters for pay-fsm messages
val nodeRelayerAdapters = outgoingPayment.replyTo
- nodeRelayerAdapters ! PreimageReceived(paymentHash, paymentPreimage, None)
+ nodeRelayerAdapters ! PreimageReceived(paymentHash, paymentPreimage, None, None)
incomingMultiPart.foreach { p =>
val fwd = register.expectMessageType[Register.Forward[CMD_FULFILL_HTLC]]
assert(fwd.channelId == p.add.channelId)
- assert(fwd.message == CMD_FULFILL_HTLC(p.add.id, paymentPreimage, Some(FulfillAttributionData(p.receivedAt, Some(incomingMultiPart.last.receivedAt), None)), commit = true))
+ assert(fwd.message == CMD_FULFILL_HTLC(p.add.id, paymentPreimage, None, Some(FulfillAttributionData(p.receivedAt, Some(incomingMultiPart.last.receivedAt), None)), commit = true))
}
nodeRelayerAdapters ! createSuccessEvent()
@@ -915,11 +977,11 @@ class NodeRelayerSpec extends ScalaTestWithActorTestKit(ConfigFactory.load("appl
// those are adapters for pay-fsm messages
val nodeRelayerAdapters = outgoingPayment.replyTo
- nodeRelayerAdapters ! PreimageReceived(paymentHash, paymentPreimage, None)
+ nodeRelayerAdapters ! PreimageReceived(paymentHash, paymentPreimage, None, None)
incomingPayments.foreach { p =>
val fwd = register.expectMessageType[Register.Forward[CMD_FULFILL_HTLC]]
assert(fwd.channelId == p.add.channelId)
- assert(fwd.message == CMD_FULFILL_HTLC(p.add.id, paymentPreimage, Some(FulfillAttributionData(p.receivedAt, Some(incomingMultiPart.last.receivedAt), None)), commit = true))
+ assert(fwd.message == CMD_FULFILL_HTLC(p.add.id, paymentPreimage, None, Some(FulfillAttributionData(p.receivedAt, Some(incomingMultiPart.last.receivedAt), None)), commit = true))
}
nodeRelayerAdapters ! createSuccessEvent()
@@ -948,11 +1010,11 @@ class NodeRelayerSpec extends ScalaTestWithActorTestKit(ConfigFactory.load("appl
// those are adapters for pay-fsm messages
val nodeRelayerAdapters = outgoingPayment.replyTo
- nodeRelayerAdapters ! PreimageReceived(paymentHash, paymentPreimage, None)
+ nodeRelayerAdapters ! PreimageReceived(paymentHash, paymentPreimage, None, None)
incomingPayments.foreach { p =>
val fwd = register.expectMessageType[Register.Forward[CMD_FULFILL_HTLC]]
assert(fwd.channelId == p.add.channelId)
- assert(fwd.message == CMD_FULFILL_HTLC(p.add.id, paymentPreimage, Some(FulfillAttributionData(p.receivedAt, Some(incomingMultiPart.last.receivedAt), None)), commit = true))
+ assert(fwd.message == CMD_FULFILL_HTLC(p.add.id, paymentPreimage, None, Some(FulfillAttributionData(p.receivedAt, Some(incomingMultiPart.last.receivedAt), None)), commit = true))
}
nodeRelayerAdapters ! createSuccessEvent()
@@ -990,11 +1052,11 @@ class NodeRelayerSpec extends ScalaTestWithActorTestKit(ConfigFactory.load("appl
// those are adapters for pay-fsm messages
val nodeRelayerAdapters = outgoingPayment.replyTo
- nodeRelayerAdapters ! PreimageReceived(paymentHash, paymentPreimage, None)
+ nodeRelayerAdapters ! PreimageReceived(paymentHash, paymentPreimage, None, None)
incomingPayments.foreach { p =>
val fwd = register.expectMessageType[Register.Forward[CMD_FULFILL_HTLC]]
assert(fwd.channelId == p.add.channelId)
- assert(fwd.message == CMD_FULFILL_HTLC(p.add.id, paymentPreimage, Some(FulfillAttributionData(p.receivedAt, Some(incomingMultiPart.last.receivedAt), None)), commit = true))
+ assert(fwd.message == CMD_FULFILL_HTLC(p.add.id, paymentPreimage, None, Some(FulfillAttributionData(p.receivedAt, Some(incomingMultiPart.last.receivedAt), None)), commit = true))
}
nodeRelayerAdapters ! createSuccessEvent()
@@ -1121,11 +1183,11 @@ class NodeRelayerSpec extends ScalaTestWithActorTestKit(ConfigFactory.load("appl
// those are adapters for pay-fsm messages
val nodeRelayerAdapters = outgoingPayment.replyTo
- nodeRelayerAdapters ! PreimageReceived(paymentHash, paymentPreimage, None)
+ nodeRelayerAdapters ! PreimageReceived(paymentHash, paymentPreimage, None, None)
incomingPayments.foreach { p =>
val fwd = register.expectMessageType[Register.Forward[CMD_FULFILL_HTLC]]
assert(fwd.channelId == p.add.channelId)
- assert(fwd.message == CMD_FULFILL_HTLC(p.add.id, paymentPreimage, Some(FulfillAttributionData(p.receivedAt, Some(incomingMultiPart.last.receivedAt), None)), commit = true))
+ assert(fwd.message == CMD_FULFILL_HTLC(p.add.id, paymentPreimage, None, Some(FulfillAttributionData(p.receivedAt, Some(incomingMultiPart.last.receivedAt), None)), commit = true))
}
nodeRelayerAdapters ! createSuccessEvent()
@@ -1222,7 +1284,7 @@ object NodeRelayerSpec {
(paymentPackets.map(_.outerPayload.expiry).min - nodeParams.relayParams.asyncPaymentsParams.cancelSafetyBeforeTimeout).blockHeight
def createSuccessEvent(): PaymentSent =
- 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)
+ PaymentSent(relayId, paymentPreimage, outgoingAmount, outgoingNodeId, Seq(PaymentSent.PaymentPart(UUID.randomUUID(), PaymentEvent.OutgoingPayment(randomBytes32(), randomKey().publicKey, outgoingAmount, 50 unixms), 10 msat, None, 0 unixms)), None, None, 0 unixms)
def createTrampolinePacket(amount: MilliSatoshi, expiry: CltvExpiry): OnionRoutingPacket = {
val payload = NodePayload(outgoingNodeId, FinalPayload.Standard.createPayload(amount, amount, expiry, paymentSecret, upgradeAccountability = false))
### eclair-core/src/test/scala/fr/acinq/eclair/profit/PeerStatsTrackerSpec.scala
@@ -365,7 +365,7 @@ class PeerStatsTrackerSpec extends ScalaTestWithActorTestKit(ConfigFactory.load(
db.add(PaymentSent(
UUID.randomUUID(), randomBytes32(), sentAmount - sentFees, randomKey().publicKey,
PaymentSent.PaymentPart(UUID.randomUUID(), OutgoingPayment(channelId1, remoteNodeId1, sentAmount, sentAt), sentFees, None, sentAt - 10.seconds) :: Nil,
- None, sentAt - 10.seconds
+ None, None, sentAt - 10.seconds
))
// PaymentReceived through remoteNodeId2, 2 days ago.
@@ -407,7 +407,7 @@ class PeerStatsTrackerSpec extends ScalaTestWithActorTestKit(ConfigFactory.load(
db.add(PaymentSent(
UUID.randomUUID(), randomBytes32(), 500_000_000 msat, randomKey().publicKey,
PaymentSent.PaymentPart(UUID.randomUUID(), OutgoingPayment(channelId1, remoteNodeId1, 500_000_000 msat, oldAt), 0 msat, None, oldAt) :: Nil,
- None, oldAt
+ None, None, oldAt
))
// Spawn tracker with channels for both peers.
### eclair-core/src/test/scala/fr/acinq/eclair/wire/internal/CommandCodecsSpec.scala
@@ -31,17 +31,16 @@ class CommandCodecsSpec extends AnyFunSuite {
test("encode/decode all settlement commands") {
val testCases: Map[HtlcSettlementCommand, ByteVector] = Map(
- CMD_FULFILL_HTLC(1573, ByteVector32(hex"e64e7c07667366e517886af99a25a5dd547014c95ba392ea4623fbf47fe00927"), Some(FulfillAttributionData(TimestampMilli(123), None, None))) -> hex"0008 0000000000000625 e64e7c07667366e517886af99a25a5dd547014c95ba392ea4623fbf47fe00927 ff 000000000000007b 00 00",
- CMD_FULFILL_HTLC(1573, ByteVector32(hex"e64e7c07667366e517886af99a25a5dd547014c95ba392ea4623fbf47fe00927"), Some(FulfillAttributionData(TimestampMilli(123), Some(TimestampMilli(125)), None))) -> hex"0008 0000000000000625 e64e7c07667366e517886af99a25a5dd547014c95ba392ea4623fbf47fe00927 ff 000000000000007b ff 000000000000007d 00",
- CMD_FULFILL_HTLC(1573, ByteVector32(hex"e64e7c07667366e517886af99a25a5dd547014c95ba392ea4623fbf47fe00927"), Some(FulfillAttributionData(TimestampMilli(123), None, Some(hex"636ac35e3d982332a8347578a2753f443fa53b7ca3ab9ce5486eeb857ebbf4c49eed261430e666aa5a724484e9ea4a194c3c21d7d1fbce42c13376e9b1c420b6d8c90cb3883af13a73c4d541ebd83e0fc6366263110922cd71133babfa375dc1e0aa8d898dfd152805a48cfc16e7fac88a3a298caf7511d23ab5ecd4eafeb0275a6f736f25d07f6d8182a75f7e692705a3ac481400bf7944f27f611bfb4cdffdfb726f7d877d3c031ff16992f67b7f6e8951aa8b974d245520487cb4a8d9149d0bce892e7af8da9997d679556d515f849a5acf028fbaa81290c681124b14957b1beba8b2716c5ace289fe495ddf3222dcf00764f73d3a4f979586327d460ae61ddbb3b249efa73b3ca8dfc1105a88bdd0b9470fe17b052f954ba41b75c6d28e5da27bd05f63a20cdde22211a37dd460137bb90bf684ac47f1a5e3a82b1ed7550414282d02bca3920f2d0db1064ede422de2151872586462623b280a99284a2faa571aab1b92da43ad331e82480cb18bfc62a9a20b2614e1daf28ed5a3ef294296bbca459edf90becf4a883e1486c0bf9db8a0a842b1868482447aed4fdb28f8de201d9aa6009a2e0a85b560f8333665400fad8d3f75fc924e72f1ef259db26dbf0a59813146fff405876bdde9b877d5828a933d6cccab461a81d3791712f65353863b64b7045a0d1bc27157dd71ad9c18913421a077e4d073d0c50480b0fa0f6b160ed72fd106e3c9aea00683f4532549dbc9d40a81f66b20ec4c9ee1a873bbbb6ca4b167ee163a001880e455da43e7d9c00bf747fdf3587b3407afe2de86c5f5a76a3052866260514bb06b433f64bf6ca2f69c335ac5afada81c801c708be9872c8a242ed93e5e9ebb70ec9b615a083700db350463d3e9368f6d778635663cfaaa3b725a03298e736e43a9201d222dff7d54e045da37ea84b0d607c7244ad46a90aa77d7905dae4a2160caadd1282918f80f8659a559f29f2eefc5709625d7169fd8abb9c532772fbdce93fd9b6495bad4f184a3cc39a302555cf1361d68c0b1528d38bb10f20638e407f3252f053f15d0b21ae26046c29b3a83ac2e8d69e354ce4178df615bc46cd66cc65e9206dfbb3b0d6613071c1d92d6f1116513f38777ee686771dcacc8ad5d8b615ca48ca6da267f48ea401bd2abd0e251dd5e1953c4f5d5619eb31e4d91176db345f9f269225d7598384ecc0a7eb0e24bccd1dc9a6822e41fb7c2fff7baddee630ff4d7ed08f15c241b15568ced090e2d8f94b51c8cb14d5b4f450ceda4008907f2e31ae636579a383499aaa85")))) -> hex"0008 0000000000000625 e64e7c07667366e517886af99a25a5dd547014c95ba392ea4623fbf47fe00927 ff 000000000000007b 00 ff 636ac35e3d982332a8347578a2753f443fa53b7ca3ab9ce5486eeb857ebbf4c49eed261430e666aa5a724484e9ea4a194c3c21d7d1fbce42c13376e9b1c420b6d8c90cb3883af13a73c4d541ebd83e0fc6366263110922cd71133babfa375dc1e0aa8d898dfd152805a48cfc16e7fac88a3a298caf7511d23ab5ecd4eafeb0275a6f736f25d07f6d8182a75f7e692705a3ac481400bf7944f27f611bfb4cdffdfb726f7d877d3c031ff16992f67b7f6e8951aa8b974d245520487cb4a8d9149d0bce892e7af8da9997d679556d515f849a5acf028fbaa81290c681124b14957b1beba8b2716c5ace289fe495ddf3222dcf00764f73d3a4f979586327d460ae61ddbb3b249efa73b3ca8dfc1105a88bdd0b9470fe17b052f954ba41b75c6d28e5da27bd05f63a20cdde22211a37dd460137bb90bf684ac47f1a5e3a82b1ed7550414282d02bca3920f2d0db1064ede422de2151872586462623b280a99284a2faa571aab1b92da43ad331e82480cb18bfc62a9a20b2614e1daf28ed5a3ef294296bbca459edf90becf4a883e1486c0bf9db8a0a842b1868482447aed4fdb28f8de201d9aa6009a2e0a85b560f8333665400fad8d3f75fc924e72f1ef259db26dbf0a59813146fff405876bdde9b877d5828a933d6cccab461a81d3791712f65353863b64b7045a0d1bc27157dd71ad9c18913421a077e4d073d0c50480b0fa0f6b160ed72fd106e3c9aea00683f4532549dbc9d40a81f66b20ec4c9ee1a873bbbb6ca4b167ee163a001880e455da43e7d9c00bf747fdf3587b3407afe2de86c5f5a76a3052866260514bb06b433f64bf6ca2f69c335ac5afada81c801c708be9872c8a242ed93e5e9ebb70ec9b615a083700db350463d3e9368f6d778635663cfaaa3b725a03298e736e43a9201d222dff7d54e045da37ea84b0d607c7244ad46a90aa77d7905dae4a2160caadd1282918f80f8659a559f29f2eefc5709625d7169fd8abb9c532772fbdce93fd9b6495bad4f184a3cc39a302555cf1361d68c0b1528d38bb10f20638e407f3252f053f15d0b21ae26046c29b3a83ac2e8d69e354ce4178df615bc46cd66cc65e9206dfbb3b0d6613071c1d92d6f1116513f38777ee686771dcacc8ad5d8b615ca48ca6da267f48ea401bd2abd0e251dd5e1953c4f5d5619eb31e4d91176db345f9f269225d7598384ecc0a7eb0e24bccd1dc9a6822e41fb7c2fff7baddee630ff4d7ed08f15c241b15568ced090e2d8f94b51c8cb14d5b4f450ceda4008907f2e31ae636579a383499aaa85",
+ CMD_FULFILL_HTLC(1573, ByteVector32(hex"e64e7c07667366e517886af99a25a5dd547014c95ba392ea4623fbf47fe00927"), None, Some(FulfillAttributionData(TimestampMilli(123), None, None))) -> hex"0009 0000000000000625 e64e7c07667366e517886af99a25a5dd547014c95ba392ea4623fbf47fe00927 00 ff 000000000000007b 00 00",
+ CMD_FULFILL_HTLC(1573, ByteVector32(hex"e64e7c07667366e517886af99a25a5dd547014c95ba392ea4623fbf47fe00927"), Some(hex"deadbeef"), Some(FulfillAttributionData(TimestampMilli(123), Some(TimestampMilli(125)), None))) -> hex"0009 0000000000000625 e64e7c07667366e517886af99a25a5dd547014c95ba392ea4623fbf47fe00927 ff0004deadbeef ff 000000000000007b ff 000000000000007d 00",
+ CMD_FULFILL_HTLC(1573, ByteVector32(hex"e64e7c07667366e517886af99a25a5dd547014c95ba392ea4623fbf47fe00927"), Some(hex"deadbeefdeadbeefdeadbeefdeadbeefdeadbeefdeadbeefdeadbeefdeadbeefdeadbeefdeadbeefdeadbeefdeadbeefdeadbeefdeadbeefdeadbeefdeadbeefdeadbeefdeadbeefdeadbeefdeadbeefdeadbeefdeadbeefdeadbeefdeadbeefdeadbeefdeadbeefdeadbeefdeadbeefdeadbeefdeadbeefdeadbeefdeadbeefdeadbeefdeadbeefdeadbeefdeadbeefdeadbeefdeadbeefdeadbeefdeadbeefdeadbeefdeadbeefdeadbeefdeadbeefdeadbeefdeadbeefdeadbeefdeadbeefdeadbeefdeadbeefdeadbeefdeadbeefdeadbeefdeadbeefdeadbeefdeadbeefdeadbeefdeadbeefdeadbeefdeadbeefdeadbeefdeadbeefdeadbeefdeadbeef"), Some(FulfillAttributionData(TimestampMilli(123), None, Some(hex"636ac35e3d982332a8347578a2753f443fa53b7ca3ab9ce5486eeb857ebbf4c49eed261430e666aa5a724484e9ea4a194c3c21d7d1fbce42c13376e9b1c420b6d8c90cb3883af13a73c4d541ebd83e0fc6366263110922cd71133babfa375dc1e0aa8d898dfd152805a48cfc16e7fac88a3a298caf7511d23ab5ecd4eafeb0275a6f736f25d07f6d8182a75f7e692705a3ac481400bf7944f27f611bfb4cdffdfb726f7d877d3c031ff16992f67b7f6e8951aa8b974d245520487cb4a8d9149d0bce892e7af8da9997d679556d515f849a5acf028fbaa81290c681124b14957b1beba8b2716c5ace289fe495ddf3222dcf00764f73d3a4f979586327d460ae61ddbb3b249efa73b3ca8dfc1105a88bdd0b9470fe17b052f954ba41b75c6d28e5da27bd05f63a20cdde22211a37dd460137bb90bf684ac47f1a5e3a82b1ed7550414282d02bca3920f2d0db1064ede422de2151872586462623b280a99284a2faa571aab1b92da43ad331e82480cb18bfc62a9a20b2614e1daf28ed5a3ef294296bbca459edf90becf4a883e1486c0bf9db8a0a842b1868482447aed4fdb28f8de201d9aa6009a2e0a85b560f8333665400fad8d3f75fc924e72f1ef259db26dbf0a59813146fff405876bdde9b877d5828a933d6cccab461a81d3791712f65353863b64b7045a0d1bc27157dd71ad9c18913421a077e4d073d0c50480b0fa0f6b160ed72fd106e3c9aea00683f4532549dbc9d40a81f66b20ec4c9ee1a873bbbb6ca4b167ee163a001880e455da43e7d9c00bf747fdf3587b3407afe2de86c5f5a76a3052866260514bb06b433f64bf6ca2f69c335ac5afada81c801c708be9872c8a242ed93e5e9ebb70ec9b615a083700db350463d3e9368f6d778635663cfaaa3b725a03298e736e43a9201d222dff7d54e045da37ea84b0d607c7244ad46a90aa77d7905dae4a2160caadd1282918f80f8659a559f29f2eefc5709625d7169fd8abb9c532772fbdce93fd9b6495bad4f184a3cc39a302555cf1361d68c0b1528d38bb10f20638e407f3252f053f15d0b21ae26046c29b3a83ac2e8d69e354ce4178df615bc46cd66cc65e9206dfbb3b0d6613071c1d92d6f1116513f38777ee686771dcacc8ad5d8b615ca48ca6da267f48ea401bd2abd0e251dd5e1953c4f5d5619eb31e4d91176db345f9f269225d7598384ecc0a7eb0e24bccd1dc9a6822e41fb7c2fff7baddee630ff4d7ed08f15c241b15568ced090e2d8f94b51c8cb14d5b4f450ceda4008907f2e31ae636579a383499aaa85")))) -> hex"0009 0000000000000625 e64e7c07667366e517886af99a25a5dd547014c95ba392ea4623fbf47fe00927 ff0100deadbeefdeadbeefdeadbeefdeadbeefdeadbeefdeadbeefdeadbeefdeadbeefdeadbeefdeadbeefdeadbeefdeadbeefdeadbeefdeadbeefdeadbeefdeadbeefdeadbeefdeadbeefdeadbeefdeadbeefdeadbeefdeadbeefdeadbeefdeadbeefdeadbeefdeadbeefdeadbeefdeadbeefdeadbeefdeadbeefdeadbeefdeadbeefdeadbeefdeadbeefdeadbeefdeadbeefdeadbeefdeadbeefdeadbeefdeadbeefdeadbeefdeadbeefdeadbeefdeadbeefdeadbeefdeadbeefdeadbeefdeadbeefdeadbeefdeadbeefdeadbeefdeadbeefdeadbeefdeadbeefdeadbeefdeadbeefdeadbeefdeadbeefdeadbeefdeadbeefdeadbeefdeadbeefdeadbeefdeadbeef ff 000000000000007b 00 ff 636ac35e3d982332a8347578a2753f443fa53b7ca3ab9ce5486eeb857ebbf4c49eed261430e666aa5a724484e9ea4a194c3c21d7d1fbce42c13376e9b1c420b6d8c90cb3883af13a73c4d541ebd83e0fc6366263110922cd71133babfa375dc1e0aa8d898dfd152805a48cfc16e7fac88a3a298caf7511d23ab5ecd4eafeb0275a6f736f25d07f6d8182a75f7e692705a3ac481400bf7944f27f611bfb4cdffdfb726f7d877d3c031ff16992f67b7f6e8951aa8b974d245520487cb4a8d9149d0bce892e7af8da9997d679556d515f849a5acf028fbaa81290c681124b14957b1beba8b2716c5ace289fe495ddf3222dcf00764f73d3a4f979586327d460ae61ddbb3b249efa73b3ca8dfc1105a88bdd0b9470fe17b052f954ba41b75c6d28e5da27bd05f63a20cdde22211a37dd460137bb90bf684ac47f1a5e3a82b1ed7550414282d02bca3920f2d0db1064ede422de2151872586462623b280a99284a2faa571aab1b92da43ad331e82480cb18bfc62a9a20b2614e1daf28ed5a3ef294296bbca459edf90becf4a883e1486c0bf9db8a0a842b1868482447aed4fdb28f8de201d9aa6009a2e0a85b560f8333665400fad8d3f75fc924e72f1ef259db26dbf0a59813146fff405876bdde9b877d5828a933d6cccab461a81d3791712f65353863b64b7045a0d1bc27157dd71ad9c18913421a077e4d073d0c50480b0fa0f6b160ed72fd106e3c9aea00683f4532549dbc9d40a81f66b20ec4c9ee1a873bbbb6ca4b167ee163a001880e455da43e7d9c00bf747fdf3587b3407afe2de86c5f5a76a3052866260514bb06b433f64bf6ca2f69c335ac5afada81c801c708be9872c8a242ed93e5e9ebb70ec9b615a083700db350463d3e9368f6d778635663cfaaa3b725a03298e736e43a9201d222dff7d54e045da37ea84b0d607c7244ad46a90aa77d7905dae4a2160caadd1282918f80f8659a559f29f2eefc5709625d7169fd8abb9c532772fbdce93fd9b6495bad4f184a3cc39a302555cf1361d68c0b1528d38bb10f20638e407f3252f053f15d0b21ae26046c29b3a83ac2e8d69e354ce4178df615bc46cd66cc65e9206dfbb3b0d6613071c1d92d6f1116513f38777ee686771dcacc8ad5d8b615ca48ca6da267f48ea401bd2abd0e251dd5e1953c4f5d5619eb31e4d91176db345f9f269225d7598384ecc0a7eb0e24bccd1dc9a6822e41fb7c2fff7baddee630ff4d7ed08f15c241b15568ced090e2d8f94b51c8cb14d5b4f450ceda4008907f2e31ae636579a383499aaa85",
CMD_FAIL_HTLC(42456, FailureReason.EncryptedDownstreamFailure(hex"d21a88a158067efecbee41da24e1d7407747f135e585e7417843729d3bff5c160817d14ce569761d93749a23d227edc0ade99c1a8d59541e45e1f623af2602d568a9a3c3bca71f1b4860ae0b599ba016c58224eab7721ed930eb2bdfd83ff940cc9e8106b0bd6b2027821f8d102b8c680664d90ce9e69d8bb96453a7b495710b83c13e4b3085bb0156b7091ed927305c44", None), Some(FailureAttributionData(TimestampMilli(123456), None))) -> hex"0007 000000000000a5d8 02 0091 d21a88a158067efecbee41da24e1d7407747f135e585e7417843729d3bff5c160817d14ce569761d93749a23d227edc0ade99c1a8d59541e45e1f623af2602d568a9a3c3bca71f1b4860ae0b599ba016c58224eab7721ed930eb2bdfd83ff940cc9e8106b0bd6b2027821f8d102b8c680664d90ce9e69d8bb96453a7b495710b83c13e4b3085bb0156b7091ed927305c44 00 ff 000000000001e240 00",
CMD_FAIL_HTLC(42456, FailureReason.EncryptedDownstreamFailure(hex"d21a88a158067efecbee41da24e1d7407747f135e585e7417843729d3bff5c160817d14ce569761d93749a23d227edc0ade99c1a8d59541e45e1f623af2602d568a9a3c3bca71f1b4860ae0b599ba016c58224eab7721ed930eb2bdfd83ff940cc9e8106b0bd6b2027821f8d102b8c680664d90ce9e69d8bb96453a7b495710b83c13e4b3085bb0156b7091ed927305c44", Some(hex"c8e7a432d1db82765a5e4cecf360b439ff9cabe795f470249065a1b8d893c9ac5142092e9260b073caadc7097d3d0aa6b5f8181c02b8eb9ecb9c17a9867e8e533766e79c00844321a7a15afcd562cefbde4cc7a66865b57192f770f68260cc09e133876181921018ade17ac1bdca027af5073b5e354cbe888651a0592a228de9019b1136b702e56424c84e01ec60cee5df2ad7179196809bcbf58e17f91fd851f91fd2a5be1ea5efb43ea18d1a5b8ad2d1fe69a9f29fbea84565c34ca3088e848e5c4297f7bacea917f332a311beb365f3f131d21871120fd3fdeefb0566cb56e8dac56cdb1eee4ebb2bfcdc954326566cfb42481e7ef5fb3031ac9190e8e02f35a9430ce7cf5167f75e5c016056e2dba3022acabfe20a6891f57cfcf0abf09102b7af1b91223badca2eb865e8a523b141dcf91955631e4efd7e9664205e89aaa2282826ac65e9651620dc3392231f8f28821271da0ce9c5eb3f145837aefde0e5b33b5cb8f847de6caa51b3488baedbb706012c9b7034919f23b7c043d3e484f4be9ab72b9b37985c34c21b5ffcbb40e9b11fe83661cef912c97f5a6b4cce76518a0245d45d0fb2844b2853457a982a9418fa83934fead109f8a38ac23c3a03bbb32d573d2349bb2228c8a53efc65e9165526b53034e53d4f8540960129657b88e28e75c9f8d26f48c4d2cc86006456f03e0bff14262e97f94d64e58369798e43bb89f6f1ae731301eded7bce4ec21613670be6939ad17b8b6d4a9ca059cc5ed33dcc7c7608dda6e2810c7d76b20fa23f5e8adb91fa895ef2ff030086168e16bcc4a4376d36f2c169e5821bb40316df88e456a8b99057f130b8ad1f097e7c9f81d64c816531a325e6b517de4022bea321a41a92386ce50ca271ff1d2ebaf0694e57545e624ef3f76eae1454314136276bf1bab91e3fcdf541eb60052fc65932505be888e3ec1de782e27a3689727128cb1018fbd4ca7b51916ba05280f1004cf5bfdde6159453e2936b76842c1d978e34d0a5f65ba2a27dd235538c2875a1ca9433b7a799aa30e28facb6603e8644da1ae9a8cf169df35f905a366aada5e0c4fc1a7f9cb36f75a12983fd9e43d3339d506d37aebd9886a52f98cc330c812605508525d8c8ad5b93a9c08c5d41123dbd7e15644f61a9ab5758aa2615cc1781d997f4d2177d4b56c4be0276e67debb4cd01e398e8a6d9d3ceac030a01235b965b1a733f2710251bb1638cdb77894667aecedb1bd56b8e9979f938a4f9a66a9da44a6d6727fdfa01641b021d0d89061370a0cb2fa68d1f242a")), Some(FailureAttributionData(TimestampMilli(123456), None))) -> hex"0007 000000000000a5d8 02 0091 d21a88a158067efecbee41da24e1d7407747f135e585e7417843729d3bff5c160817d14ce569761d93749a23d227edc0ade99c1a8d59541e45e1f623af2602d568a9a3c3bca71f1b4860ae0b599ba016c58224eab7721ed930eb2bdfd83ff940cc9e8106b0bd6b2027821f8d102b8c680664d90ce9e69d8bb96453a7b495710b83c13e4b3085bb0156b7091ed927305c44 ff c8e7a432d1db82765a5e4cecf360b439ff9cabe795f470249065a1b8d893c9ac5142092e9260b073caadc7097d3d0aa6b5f8181c02b8eb9ecb9c17a9867e8e533766e79c00844321a7a15afcd562cefbde4cc7a66865b57192f770f68260cc09e133876181921018ade17ac1bdca027af5073b5e354cbe888651a0592a228de9019b1136b702e56424c84e01ec60cee5df2ad7179196809bcbf58e17f91fd851f91fd2a5be1ea5efb43ea18d1a5b8ad2d1fe69a9f29fbea84565c34ca3088e848e5c4297f7bacea917f332a311beb365f3f131d21871120fd3fdeefb0566cb56e8dac56cdb1eee4ebb2bfcdc954326566cfb42481e7ef5fb3031ac9190e8e02f35a9430ce7cf5167f75e5c016056e2dba3022acabfe20a6891f57cfcf0abf09102b7af1b91223badca2eb865e8a523b141dcf91955631e4efd7e9664205e89aaa2282826ac65e9651620dc3392231f8f28821271da0ce9c5eb3f145837aefde0e5b33b5cb8f847de6caa51b3488baedbb706012c9b7034919f23b7c043d3e484f4be9ab72b9b37985c34c21b5ffcbb40e9b11fe83661cef912c97f5a6b4cce76518a0245d45d0fb2844b2853457a982a9418fa83934fead109f8a38ac23c3a03bbb32d573d2349bb2228c8a53efc65e9165526b53034e53d4f8540960129657b88e28e75c9f8d26f48c4d2cc86006456f03e0bff14262e97f94d64e58369798e43bb89f6f1ae731301eded7bce4ec21613670be6939ad17b8b6d4a9ca059cc5ed33dcc7c7608dda6e2810c7d76b20fa23f5e8adb91fa895ef2ff030086168e16bcc4a4376d36f2c169e5821bb40316df88e456a8b99057f130b8ad1f097e7c9f81d64c816531a325e6b517de4022bea321a41a92386ce50ca271ff1d2ebaf0694e57545e624ef3f76eae1454314136276bf1bab91e3fcdf541eb60052fc65932505be888e3ec1de782e27a3689727128cb1018fbd4ca7b51916ba05280f1004cf5bfdde6159453e2936b76842c1d978e34d0a5f65ba2a27dd235538c2875a1ca9433b7a799aa30e28facb6603e8644da1ae9a8cf169df35f905a366aada5e0c4fc1a7f9cb36f75a12983fd9e43d3339d506d37aebd9886a52f98cc330c812605508525d8c8ad5b93a9c08c5d41123dbd7e15644f61a9ab5758aa2615cc1781d997f4d2177d4b56c4be0276e67debb4cd01e398e8a6d9d3ceac030a01235b965b1a733f2710251bb1638cdb77894667aecedb1bd56b8e9979f938a4f9a66a9da44a6d6727fdfa01641b021d0d89061370a0cb2fa68d1f242a ff 000000000001e240 00",
CMD_FAIL_HTLC(253, FailureReason.LocalFailure(TemporaryNodeFailure()), Some(FailureAttributionData(TimestampMilli(123), None))) -> hex"0007 00000000000000fd 01 0002 2002 ff 000000000000007b 00",
CMD_FAIL_HTLC(253, FailureReason.LocalFailure(TemporaryNodeFailure()), Some(FailureAttributionData(TimestampMilli(123), Some(TimestampMilli(125))))) -> hex"0007 00000000000000fd 01 0002 2002 ff 000000000000007b ff 000000000000007d",
CMD_FAIL_HTLC(253, FailureReason.LocalFailure(TemporaryNodeFailure(TlvStream(Set.empty[FailureMessageTlv], Set(GenericTlv(UInt64(17), hex"deadbeef"))))), Some(FailureAttributionData(TimestampMilli(456), None))) -> hex"0007 00000000000000fd 01 0008 2002 1104deadbeef ff 00000000000001c8 00",
CMD_FAIL_MALFORMED_HTLC(7984, ByteVector32(hex"17cc093e177c7a7fcaa9e96ab407146c8886546a5690f945c98ac20c4ab3b4f3"), FailureMessageCodecs.BADONION) -> hex"0002 0000000000001f30 17cc093e177c7a7fcaa9e96ab407146c8886546a5690f945c98ac20c4ab3b4f38000",
)
-
testCases.foreach { case (command, bin) =>
val encoded = CommandCodecs.cmdCodec.encode(command).require.bytes
assert(encoded == bin)
@@ -54,8 +53,8 @@ class CommandCodecsSpec extends AnyFunSuite {
val data32 = ByteVector32(hex"e4927c04913251b44d0a3a8e57ded746fee80ff3b424e70dad2a1428eeba86cb")
val data123 = hex"fea75bb8cf45349eb544d8da832af5af30eefa671ec27cf2e4867bacada2dbe00a6ce5141164aa153ac8b4b25c75c3af15c4b5cb6a293607751a079bc546da17f654b76a74bc57b6b21ed73d2d3909f3682f01b85418a0f0ecddb759e9481d4563a572ac1ddcb77c64ae167d8dfbd889703cb5c33b4b9636bad472"
val testCases = Map(
- hex"0000 000000000000002ae4927c04913251b44d0a3a8e57ded746fee80ff3b424e70dad2a1428eeba86cb" -> CMD_FULFILL_HTLC(42, data32, None, commit = false, None),
- hex"0000 0000000000000625 e64e7c07667366e517886af99a25a5dd547014c95ba392ea4623fbf47fe00927" -> CMD_FULFILL_HTLC(1573, ByteVector32(hex"e64e7c07667366e517886af99a25a5dd547014c95ba392ea4623fbf47fe00927"), None),
+ hex"0000 000000000000002ae4927c04913251b44d0a3a8e57ded746fee80ff3b424e70dad2a1428eeba86cb" -> CMD_FULFILL_HTLC(42, data32, None, None, commit = false, None),
+ hex"0000 0000000000000625 e64e7c07667366e517886af99a25a5dd547014c95ba392ea4623fbf47fe00927" -> CMD_FULFILL_HTLC(1573, ByteVector32(hex"e64e7c07667366e517886af99a25a5dd547014c95ba392ea4623fbf47fe00927"), None, None),
hex"0001 000000000000002a003dff53addc67a29a4f5aa26c6d41957ad798777d338f613e7972433dd656d16df00536728a08b2550a9d645a592e3ae1d78ae25ae5b5149b03ba8d03cde2a36d0bfb2a5bb53a5e2bdb590f6b9e969c84f9b41780dc2a0c5078766edbacf4a40ea2b1d2b9560eee5bbe32570b3ec6fdec44b81e5ae19da5cb1b5d6a3900" -> CMD_FAIL_HTLC(42, FailureReason.EncryptedDownstreamFailure(data123, None), None, None, commit = false, None),
hex"0001 000000000000002a900100" -> CMD_FAIL_HTLC(42, FailureReason.LocalFailure(TemporaryNodeFailure()), None),
hex"0003 000000000000a5d8 00 0091 d21a88a158067efecbee41da24e1d7407747f135e585e7417843729d3bff5c160817d14ce569761d93749a23d227edc0ade99c1a8d59541e45e1f623af2602d568a9a3c3bca71f1b4860ae0b599ba016c58224eab7721ed930eb2bdfd83ff940cc9e8106b0bd6b2027821f8d102b8c680664d90ce9e69d8bb96453a7b495710b83c13e4b3085bb0156b7091ed927305c44" -> CMD_FAIL_HTLC(42456, FailureReason.EncryptedDownstreamFailure(hex"d21a88a158067efecbee41da24e1d7407747f135e585e7417843729d3bff5c160817d14ce569761d93749a23d227edc0ade99c1a8d59541e45e1f623af2602d568a9a3c3bca71f1b4860ae0b599ba016c58224eab7721ed930eb2bdfd83ff940cc9e8106b0bd6b2027821f8d102b8c680664d90ce9e69d8bb96453a7b495710b83c13e4b3085bb0156b7091ed927305c44", None), None),
@@ -65,8 +64,10 @@ class CommandCodecsSpec extends AnyFunSuite {
hex"0004 000000000000a5d8 00 0091 d21a88a158067efecbee41da24e1d7407747f135e585e7417843729d3bff5c160817d14ce569761d93749a23d227edc0ade99c1a8d59541e45e1f623af2602d568a9a3c3bca71f1b4860ae0b599ba016c58224eab7721ed930eb2bdfd83ff940cc9e8106b0bd6b2027821f8d102b8c680664d90ce9e69d8bb96453a7b495710b83c13e4b3085bb0156b7091ed927305c44" -> CMD_FAIL_HTLC(42456, FailureReason.EncryptedDownstreamFailure(hex"d21a88a158067efecbee41da24e1d7407747f135e585e7417843729d3bff5c160817d14ce569761d93749a23d227edc0ade99c1a8d59541e45e1f623af2602d568a9a3c3bca71f1b4860ae0b599ba016c58224eab7721ed930eb2bdfd83ff940cc9e8106b0bd6b2027821f8d102b8c680664d90ce9e69d8bb96453a7b495710b83c13e4b3085bb0156b7091ed927305c44", None), None),
hex"0004 00000000000000fd 01 0002 2002" -> CMD_FAIL_HTLC(253, FailureReason.LocalFailure(TemporaryNodeFailure()), None),
hex"0004 00000000000000fd 01 0008 2002 1104deadbeef" -> CMD_FAIL_HTLC(253, FailureReason.LocalFailure(TemporaryNodeFailure(TlvStream(Set.empty[FailureMessageTlv], Set(GenericTlv(UInt64(17), hex"deadbeef"))))), None),
+ hex"0008 0000000000000625 e64e7c07667366e517886af99a25a5dd547014c95ba392ea4623fbf47fe00927 ff 000000000000007b 00 00" -> CMD_FULFILL_HTLC(1573, ByteVector32(hex"e64e7c07667366e517886af99a25a5dd547014c95ba392ea4623fbf47fe00927"), None, Some(FulfillAttributionData(TimestampMilli(123), None, None))),
+ hex"0008 0000000000000625 e64e7c07667366e517886af99a25a5dd547014c95ba392ea4623fbf47fe00927 ff 000000000000007b ff 000000000000007d 00" -> CMD_FULFILL_HTLC(1573, ByteVector32(hex"e64e7c07667366e517886af99a25a5dd547014c95ba392ea4623fbf47fe00927"), None, Some(FulfillAttributionData(TimestampMilli(123), Some(TimestampMilli(125)), None))),
+ hex"0008 0000000000000625 e64e7c07667366e517886af99a25a5dd547014c95ba392ea4623fbf47fe00927 ff 000000000000007b 00 ff 636ac35e3d982332a8347578a2753f443fa53b7ca3ab9ce5486eeb857ebbf4c49eed261430e666aa5a724484e9ea4a194c3c21d7d1fbce42c13376e9b1c420b6d8c90cb3883af13a73c4d541ebd83e0fc6366263110922cd71133babfa375dc1e0aa8d898dfd152805a48cfc16e7fac88a3a298caf7511d23ab5ecd4eafeb0275a6f736f25d07f6d8182a75f7e692705a3ac481400bf7944f27f611bfb4cdffdfb726f7d877d3c031ff16992f67b7f6e8951aa8b974d245520487cb4a8d9149d0bce892e7af8da9997d679556d515f849a5acf028fbaa81290c681124b14957b1beba8b2716c5ace289fe495ddf3222dcf00764f73d3a4f979586327d460ae61ddbb3b249efa73b3ca8dfc1105a88bdd0b9470fe17b052f954ba41b75c6d28e5da27bd05f63a20cdde22211a37dd460137bb90bf684ac47f1a5e3a82b1ed7550414282d02bca3920f2d0db1064ede422de2151872586462623b280a99284a2faa571aab1b92da43ad331e82480cb18bfc62a9a20b2614e1daf28ed5a3ef294296bbca459edf90becf4a883e1486c0bf9db8a0a842b1868482447aed4fdb28f8de201d9aa6009a2e0a85b560f8333665400fad8d3f75fc924e72f1ef259db26dbf0a59813146fff405876bdde9b877d5828a933d6cccab461a81d3791712f65353863b64b7045a0d1bc27157dd71ad9c18913421a077e4d073d0c50480b0fa0f6b160ed72fd106e3c9aea00683f4532549dbc9d40a81f66b20ec4c9ee1a873bbbb6ca4b167ee163a001880e455da43e7d9c00bf747fdf3587b3407afe2de86c5f5a76a3052866260514bb06b433f64bf6ca2f69c335ac5afada81c801c708be9872c8a242ed93e5e9ebb70ec9b615a083700db350463d3e9368f6d778635663cfaaa3b725a03298e736e43a9201d222dff7d54e045da37ea84b0d607c7244ad46a90aa77d7905dae4a2160caadd1282918f80f8659a559f29f2eefc5709625d7169fd8abb9c532772fbdce93fd9b6495bad4f184a3cc39a302555cf1361d68c0b1528d38bb10f20638e407f3252f053f15d0b21ae26046c29b3a83ac2e8d69e354ce4178df615bc46cd66cc65e9206dfbb3b0d6613071c1d92d6f1116513f38777ee686771dcacc8ad5d8b615ca48ca6da267f48ea401bd2abd0e251dd5e1953c4f5d5619eb31e4d91176db345f9f269225d7598384ecc0a7eb0e24bccd1dc9a6822e41fb7c2fff7baddee630ff4d7ed08f15c241b15568ced090e2d8f94b51c8cb14d5b4f450ceda4008907f2e31ae636579a383499aaa85" -> CMD_FULFILL_HTLC(1573, ByteVector32(hex"e64e7c07667366e517886af99a25a5dd547014c95ba392ea4623fbf47fe00927"), None, Some(FulfillAttributionData(TimestampMilli(123), None, Some(hex"636ac35e3d982332a8347578a2753f443fa53b7ca3ab9ce5486eeb857ebbf4c49eed261430e666aa5a724484e9ea4a194c3c21d7d1fbce42c13376e9b1c420b6d8c90cb3883af13a73c4d541ebd83e0fc6366263110922cd71133babfa375dc1e0aa8d898dfd152805a48cfc16e7fac88a3a298caf7511d23ab5ecd4eafeb0275a6f736f25d07f6d8182a75f7e692705a3ac481400bf7944f27f611bfb4cdffdfb726f7d877d3c031ff16992f67b7f6e8951aa8b974d245520487cb4a8d9149d0bce892e7af8da9997d679556d515f849a5acf028fbaa81290c681124b14957b1beba8b2716c5ace289fe495ddf3222dcf00764f73d3a4f979586327d460ae61ddbb3b249efa73b3ca8dfc1105a88bdd0b9470fe17b052f954ba41b75c6d28e5da27bd05f63a20cdde22211a37dd460137bb90bf684ac47f1a5e3a82b1ed7550414282d02bca3920f2d0db1064ede422de2151872586462623b280a99284a2faa571aab1b92da43ad331e82480cb18bfc62a9a20b2614e1daf28ed5a3ef294296bbca459edf90becf4a883e1486c0bf9db8a0a842b1868482447aed4fdb28f8de201d9aa6009a2e0a85b560f8333665400fad8d3f75fc924e72f1ef259db26dbf0a59813146fff405876bdde9b877d5828a933d6cccab461a81d3791712f65353863b64b7045a0d1bc27157dd71ad9c18913421a077e4d073d0c50480b0fa0f6b160ed72fd106e3c9aea00683f4532549dbc9d40a81f66b20ec4c9ee1a873bbbb6ca4b167ee163a001880e455da43e7d9c00bf747fdf3587b3407afe2de86c5f5a76a3052866260514bb06b433f64bf6ca2f69c335ac5afada81c801c708be9872c8a242ed93e5e9ebb70ec9b615a083700db350463d3e9368f6d778635663cfaaa3b725a03298e736e43a9201d222dff7d54e045da37ea84b0d607c7244ad46a90aa77d7905dae4a2160caadd1282918f80f8659a559f29f2eefc5709625d7169fd8abb9c532772fbdce93fd9b6495bad4f184a3cc39a302555cf1361d68c0b1528d38bb10f20638e407f3252f053f15d0b21ae26046c29b3a83ac2e8d69e354ce4178df615bc46cd66cc65e9206dfbb3b0d6613071c1d92d6f1116513f38777ee686771dcacc8ad5d8b615ca48ca6da267f48ea401bd2abd0e251dd5e1953c4f5d5619eb31e4d91176db345f9f269225d7598384ecc0a7eb0e24bccd1dc9a6822e41fb7c2fff7baddee630ff4d7ed08f15c241b15568ced090e2d8f94b51c8cb14d5b4f450ceda4008907f2e31ae636579a383499aaa85")))),
)
-
testCases.foreach { case (bin, command) =>
val decoded = CommandCodecs.cmdCodec.decode(bin.bits).require
assert(decoded.value == command)
### 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.PaymentPart(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, 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.PaymentPart(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, 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)Why this scored 37/100
Community notes
Notes can correct, qualify, or add evidence to the AI analysis. Every note shown here has been validated by a human moderator.
The AI analysis stands alone for now. Submit a note if you can add evidence or important context.