1
0
Fork 0
mirror of https://github.com/ACINQ/eclair.git synced 2025-02-24 06:47:46 +01:00

Add support for option_payment_metadata (#2063)

* Filter init, node and invoice features

We should explicitly filter features based on where they can be included
(`init`, `node_announcement` or `invoice`) as specified in Bolt 9.

* Add support for sending `payment_metadata`

Whenever we find a payment metadata field in an invoice, we send it in
the onion payload for the final recipient.

* Include `payment_metadata` in all invoices

We include a payment metadata in every invoice we generate. This lets us
see whether our payers support it or not, which is important data to have
before we make it mandatory and use it for storage-less invoices.

See https://github.com/lightning/bolts/pull/912 for reference.
This commit is contained in:
Bastien Teinturier 2022-01-10 15:55:43 +01:00 committed by GitHub
parent 27579a5786
commit 6e88532d18
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
35 changed files with 467 additions and 224 deletions

View file

@ -24,6 +24,15 @@ Eclair now supports the feature `option_onion_messages`. If this feature is enab
It can also send onion messages with the `sendonionmessage` API. It can also send onion messages with the `sendonionmessage` API.
Messages sent to Eclair can be read with the websocket API. Messages sent to Eclair can be read with the websocket API.
### Support for `option_payment_metadata`
Eclair now supports the `option_payment_metadata` feature (see https://github.com/lightning/bolts/pull/912).
This feature will let recipients generate "light" invoices that don't need to be stored locally until they're paid.
This is particularly useful for payment hubs that generate a lot of invoices (e.g. to be displayed on a website) but expect only a fraction of them to actually be paid.
Eclair includes a small `payment_metadata` field in all invoices it generates.
This lets node operators verify that payers actually support that feature.
### API changes ### API changes
#### Timestamps #### Timestamps

View file

@ -58,6 +58,7 @@ eclair {
option_shutdown_anysegwit = optional option_shutdown_anysegwit = optional
option_onion_messages = disabled option_onion_messages = disabled
option_channel_type = optional option_channel_type = optional
option_payment_metadata = optional
trampoline_payment = disabled trampoline_payment = disabled
keysend = disabled keysend = disabled
} }

View file

@ -34,6 +34,8 @@ object FeatureSupport {
trait Feature { trait Feature {
this: FeatureScope =>
def rfcName: String def rfcName: String
def mandatory: Int def mandatory: Int
def optional: Int = mandatory + 1 def optional: Int = mandatory + 1
@ -46,6 +48,15 @@ trait Feature {
override def toString = rfcName override def toString = rfcName
} }
/** Feature scope as defined in Bolt 9. */
sealed trait FeatureScope
/** Feature that should be advertised in init messages. */
trait InitFeature extends FeatureScope
/** Feature that should be advertised in node announcements. */
trait NodeFeature extends FeatureScope
/** Feature that should be advertised in invoices. */
trait InvoiceFeature extends FeatureScope
// @formatter:on // @formatter:on
case class UnknownFeature(bitIndex: Int) case class UnknownFeature(bitIndex: Int)
@ -71,6 +82,13 @@ case class Features(activated: Map[Feature, FeatureSupport], unknown: Set[Unknow
unknownFeaturesOk && knownFeaturesOk unknownFeaturesOk && knownFeaturesOk
} }
def initFeatures(): Features = Features(activated.collect { case (f: InitFeature, s) => (f: Feature, s) }, unknown)
def nodeAnnouncementFeatures(): Features = Features(activated.collect { case (f: NodeFeature, s) => (f: Feature, s) }, unknown)
// NB: we don't include unknown features in invoices, which means plugins cannot inject invoice features.
def invoiceFeatures(): Map[Feature with InvoiceFeature, FeatureSupport] = activated.collect { case (f: InvoiceFeature, s) => (f, s) }
def toByteVector: ByteVector = { def toByteVector: ByteVector = {
val activatedFeatureBytes = toByteVectorFromIndex(activated.map { case (feature, support) => feature.supportBit(support) }.toSet) val activatedFeatureBytes = toByteVectorFromIndex(activated.map { case (feature, support) => feature.supportBit(support) }.toSet)
val unknownFeatureBytes = toByteVectorFromIndex(unknown.map(_.bitIndex)) val unknownFeatureBytes = toByteVectorFromIndex(unknown.map(_.bitIndex))
@ -137,91 +155,96 @@ object Features {
} }
} }
case object OptionDataLossProtect extends Feature { case object OptionDataLossProtect extends Feature with InitFeature with NodeFeature {
val rfcName = "option_data_loss_protect" val rfcName = "option_data_loss_protect"
val mandatory = 0 val mandatory = 0
} }
case object InitialRoutingSync extends Feature { case object InitialRoutingSync extends Feature with InitFeature {
val rfcName = "initial_routing_sync" val rfcName = "initial_routing_sync"
// reserved but not used as per lightningnetwork/lightning-rfc/pull/178 // reserved but not used as per lightningnetwork/lightning-rfc/pull/178
val mandatory = 2 val mandatory = 2
} }
case object OptionUpfrontShutdownScript extends Feature { case object OptionUpfrontShutdownScript extends Feature with InitFeature with NodeFeature {
val rfcName = "option_upfront_shutdown_script" val rfcName = "option_upfront_shutdown_script"
val mandatory = 4 val mandatory = 4
} }
case object ChannelRangeQueries extends Feature { case object ChannelRangeQueries extends Feature with InitFeature with NodeFeature {
val rfcName = "gossip_queries" val rfcName = "gossip_queries"
val mandatory = 6 val mandatory = 6
} }
case object VariableLengthOnion extends Feature { case object VariableLengthOnion extends Feature with InitFeature with NodeFeature with InvoiceFeature {
val rfcName = "var_onion_optin" val rfcName = "var_onion_optin"
val mandatory = 8 val mandatory = 8
} }
case object ChannelRangeQueriesExtended extends Feature { case object ChannelRangeQueriesExtended extends Feature with InitFeature with NodeFeature {
val rfcName = "gossip_queries_ex" val rfcName = "gossip_queries_ex"
val mandatory = 10 val mandatory = 10
} }
case object StaticRemoteKey extends Feature { case object StaticRemoteKey extends Feature with InitFeature with NodeFeature {
val rfcName = "option_static_remotekey" val rfcName = "option_static_remotekey"
val mandatory = 12 val mandatory = 12
} }
case object PaymentSecret extends Feature { case object PaymentSecret extends Feature with InitFeature with NodeFeature with InvoiceFeature {
val rfcName = "payment_secret" val rfcName = "payment_secret"
val mandatory = 14 val mandatory = 14
} }
case object BasicMultiPartPayment extends Feature { case object BasicMultiPartPayment extends Feature with InitFeature with NodeFeature with InvoiceFeature {
val rfcName = "basic_mpp" val rfcName = "basic_mpp"
val mandatory = 16 val mandatory = 16
} }
case object Wumbo extends Feature { case object Wumbo extends Feature with InitFeature with NodeFeature {
val rfcName = "option_support_large_channel" val rfcName = "option_support_large_channel"
val mandatory = 18 val mandatory = 18
} }
case object AnchorOutputs extends Feature { case object AnchorOutputs extends Feature with InitFeature with NodeFeature {
val rfcName = "option_anchor_outputs" val rfcName = "option_anchor_outputs"
val mandatory = 20 val mandatory = 20
} }
case object AnchorOutputsZeroFeeHtlcTx extends Feature { case object AnchorOutputsZeroFeeHtlcTx extends Feature with InitFeature with NodeFeature {
val rfcName = "option_anchors_zero_fee_htlc_tx" val rfcName = "option_anchors_zero_fee_htlc_tx"
val mandatory = 22 val mandatory = 22
} }
case object ShutdownAnySegwit extends Feature { case object ShutdownAnySegwit extends Feature with InitFeature with NodeFeature {
val rfcName = "option_shutdown_anysegwit" val rfcName = "option_shutdown_anysegwit"
val mandatory = 26 val mandatory = 26
} }
case object OnionMessages extends Feature { case object OnionMessages extends Feature with InitFeature with NodeFeature {
val rfcName = "option_onion_messages" val rfcName = "option_onion_messages"
val mandatory = 38 val mandatory = 38
} }
case object ChannelType extends Feature { case object ChannelType extends Feature with InitFeature with NodeFeature {
val rfcName = "option_channel_type" val rfcName = "option_channel_type"
val mandatory = 44 val mandatory = 44
} }
case object PaymentMetadata extends Feature with InvoiceFeature {
val rfcName = "option_payment_metadata"
val mandatory = 48
}
// TODO: @t-bast: update feature bits once spec-ed (currently reserved here: https://github.com/lightningnetwork/lightning-rfc/issues/605) // TODO: @t-bast: update feature bits once spec-ed (currently reserved here: https://github.com/lightningnetwork/lightning-rfc/issues/605)
// We're not advertising these bits yet in our announcements, clients have to assume support. // We're not advertising these bits yet in our announcements, clients have to assume support.
// This is why we haven't added them yet to `areSupported`. // This is why we haven't added them yet to `areSupported`.
case object TrampolinePayment extends Feature { case object TrampolinePayment extends Feature with InitFeature with NodeFeature with InvoiceFeature {
val rfcName = "trampoline_payment" val rfcName = "trampoline_payment"
val mandatory = 50 val mandatory = 50
} }
case object KeySend extends Feature { case object KeySend extends Feature with NodeFeature {
val rfcName = "keysend" val rfcName = "keysend"
val mandatory = 54 val mandatory = 54
} }
@ -242,6 +265,7 @@ object Features {
ShutdownAnySegwit, ShutdownAnySegwit,
OnionMessages, OnionMessages,
ChannelType, ChannelType,
PaymentMetadata,
TrampolinePayment, TrampolinePayment,
KeySend KeySend
) )

View file

@ -110,7 +110,8 @@ case class NodeParams(nodeKeyManager: NodeKeyManager,
def currentBlockHeight: Long = blockCount.get def currentBlockHeight: Long = blockCount.get
def featuresFor(nodeId: PublicKey): Features = overrideFeatures.getOrElse(nodeId, features) /** Returns the features that should be used in our init message with the given peer. */
def initFeaturesFor(nodeId: PublicKey): Features = overrideFeatures.getOrElse(nodeId, features).initFeatures()
} }
object NodeParams extends Logging { object NodeParams extends Logging {

View file

@ -92,7 +92,7 @@ class Switchboard(nodeParams: NodeParams, peerFactory: Switchboard.PeerFactory)
case authenticated: PeerConnection.Authenticated => case authenticated: PeerConnection.Authenticated =>
// if this is an incoming connection, we might not yet have created the peer // if this is an incoming connection, we might not yet have created the peer
val peer = createOrGetPeer(authenticated.remoteNodeId, offlineChannels = Set.empty) val peer = createOrGetPeer(authenticated.remoteNodeId, offlineChannels = Set.empty)
val features = nodeParams.featuresFor(authenticated.remoteNodeId) val features = nodeParams.initFeaturesFor(authenticated.remoteNodeId)
// if the peer is whitelisted, we sync with them, otherwise we only sync with peers with whom we have at least one channel // if the peer is whitelisted, we sync with them, otherwise we only sync with peers with whom we have at least one channel
val doSync = nodeParams.syncWhitelist.contains(authenticated.remoteNodeId) || (nodeParams.syncWhitelist.isEmpty && peersWithChannels.contains(authenticated.remoteNodeId)) val doSync = nodeParams.syncWhitelist.contains(authenticated.remoteNodeId) || (nodeParams.syncWhitelist.isEmpty && peersWithChannels.contains(authenticated.remoteNodeId))
authenticated.peerConnection ! PeerConnection.InitializeConnection(peer, nodeParams.chainHash, features, doSync) authenticated.peerConnection ! PeerConnection.InitializeConnection(peer, nodeParams.chainHash, features, doSync)

View file

@ -335,6 +335,7 @@ object PaymentRequestSerializer extends MinimalSerializer({
FeatureSupportSerializer + FeatureSupportSerializer +
UnknownFeatureSerializer UnknownFeatureSerializer
)) ))
val paymentMetadata = p.paymentMetadata.map(m => JField("paymentMetadata", JString(m.toHex))).toSeq
val routingInfo = JField("routingInfo", Extraction.decompose(p.routingInfo)( val routingInfo = JField("routingInfo", Extraction.decompose(p.routingInfo)(
DefaultFormats + DefaultFormats +
ByteVector32Serializer + ByteVector32Serializer +
@ -344,12 +345,14 @@ object PaymentRequestSerializer extends MinimalSerializer({
MilliSatoshiSerializer + MilliSatoshiSerializer +
CltvExpiryDeltaSerializer CltvExpiryDeltaSerializer
)) ))
val fieldList = List(JField("prefix", JString(p.prefix)), val fieldList = List(
JField("prefix", JString(p.prefix)),
JField("timestamp", JLong(p.timestamp.toLong)), JField("timestamp", JLong(p.timestamp.toLong)),
JField("nodeId", JString(p.nodeId.toString())), JField("nodeId", JString(p.nodeId.toString())),
JField("serialized", JString(PaymentRequest.write(p))), JField("serialized", JString(PaymentRequest.write(p))),
p.description.fold(string => JField("description", JString(string)), hash => JField("descriptionHash", JString(hash.toHex))), p.description.fold(string => JField("description", JString(string)), hash => JField("descriptionHash", JString(hash.toHex))),
JField("paymentHash", JString(p.paymentHash.toString()))) ++ JField("paymentHash", JString(p.paymentHash.toString()))) ++
paymentMetadata ++
expiry ++ expiry ++
minFinalCltvExpiry ++ minFinalCltvExpiry ++
amount :+ amount :+

View file

@ -27,6 +27,7 @@ object Monitoring {
val PaymentAmount = Kamon.histogram("payment.amount", "Payment amount (satoshi)") val PaymentAmount = Kamon.histogram("payment.amount", "Payment amount (satoshi)")
val PaymentFees = Kamon.histogram("payment.fees", "Payment fees (satoshi)") val PaymentFees = Kamon.histogram("payment.fees", "Payment fees (satoshi)")
val PaymentParts = Kamon.histogram("payment.parts", "Number of HTLCs per payment (MPP)") val PaymentParts = Kamon.histogram("payment.parts", "Number of HTLCs per payment (MPP)")
val PaymentHtlcReceived = Kamon.counter("payment.received", "Number of valid htlcs received")
val PaymentFailed = Kamon.counter("payment.failed", "Number of failed payment") val PaymentFailed = Kamon.counter("payment.failed", "Number of failed payment")
val PaymentError = Kamon.counter("payment.error", "Non-fatal errors encountered during payment attempts") val PaymentError = Kamon.counter("payment.error", "Non-fatal errors encountered during payment attempts")
val PaymentAttempt = Kamon.histogram("payment.attempt", "Number of attempts before a payment succeeds") val PaymentAttempt = Kamon.histogram("payment.attempt", "Number of attempts before a payment succeeds")
@ -71,6 +72,7 @@ object Monitoring {
val PaymentId = "paymentId" val PaymentId = "paymentId"
val ParentId = "parentPaymentId" val ParentId = "parentPaymentId"
val PaymentHash = "paymentHash" val PaymentHash = "paymentHash"
val PaymentMetadataIncluded = "paymentMetadataIncluded"
val Amount = "amount" val Amount = "amount"
val TotalAmount = "totalAmount" val TotalAmount = "totalAmount"

View file

@ -25,6 +25,7 @@ import fr.acinq.eclair.router.Announcements
import fr.acinq.eclair.router.Router.{ChannelDesc, ChannelHop, Hop, Ignore} import fr.acinq.eclair.router.Router.{ChannelDesc, ChannelHop, Hop, Ignore}
import fr.acinq.eclair.wire.protocol.{ChannelDisabled, ChannelUpdate, Node, TemporaryChannelFailure} import fr.acinq.eclair.wire.protocol.{ChannelDisabled, ChannelUpdate, Node, TemporaryChannelFailure}
import fr.acinq.eclair.{MilliSatoshi, ShortChannelId, TimestampMilli} import fr.acinq.eclair.{MilliSatoshi, ShortChannelId, TimestampMilli}
import scodec.bits.ByteVector
import java.util.UUID import java.util.UUID
import scala.concurrent.duration.FiniteDuration import scala.concurrent.duration.FiniteDuration
@ -114,6 +115,8 @@ object PaymentReceived {
} }
case class PaymentMetadataReceived(paymentHash: ByteVector32, paymentMetadata: ByteVector)
case class PaymentSettlingOnChain(id: UUID, amount: MilliSatoshi, paymentHash: ByteVector32, timestamp: TimestampMilli = TimestampMilli.now()) extends PaymentEvent case class PaymentSettlingOnChain(id: UUID, amount: MilliSatoshi, paymentHash: ByteVector32, timestamp: TimestampMilli = TimestampMilli.now()) extends PaymentEvent
sealed trait PaymentFailure { sealed trait PaymentFailure {

View file

@ -117,7 +117,7 @@ object IncomingPaymentPacket {
} else { } else {
// We merge contents from the outer and inner payloads. // We merge contents from the outer and inner payloads.
// We must use the inner payload's total amount and payment secret because the payment may be split between multiple trampoline payments (#reckless). // We must use the inner payload's total amount and payment secret because the payment may be split between multiple trampoline payments (#reckless).
Right(FinalPacket(add, PaymentOnion.createMultiPartPayload(outerPayload.amount, innerPayload.totalAmount, outerPayload.expiry, innerPayload.paymentSecret))) Right(FinalPacket(add, PaymentOnion.createMultiPartPayload(outerPayload.amount, innerPayload.totalAmount, outerPayload.expiry, innerPayload.paymentSecret, innerPayload.paymentMetadata)))
} }
} }
@ -213,9 +213,12 @@ object OutgoingPaymentPacket {
* - the trampoline onion to include in final payload of a normal onion * - the trampoline onion to include in final payload of a normal onion
*/ */
def buildTrampolineToLegacyPacket(invoice: PaymentRequest, hops: Seq[NodeHop], finalPayload: PaymentOnion.FinalPayload): (MilliSatoshi, CltvExpiry, Sphinx.PacketAndSecrets) = { def buildTrampolineToLegacyPacket(invoice: PaymentRequest, hops: Seq[NodeHop], finalPayload: PaymentOnion.FinalPayload): (MilliSatoshi, CltvExpiry, Sphinx.PacketAndSecrets) = {
val (firstAmount, firstExpiry, payloads) = hops.drop(1).reverse.foldLeft((finalPayload.amount, finalPayload.expiry, Seq[PaymentOnion.PerHopPayload](finalPayload))) { // NB: the final payload will never reach the recipient, since the next-to-last node in the trampoline route will convert that to a non-trampoline payment.
// We use the smallest final payload possible, otherwise we may overflow the trampoline onion size.
val dummyFinalPayload = PaymentOnion.createSinglePartPayload(finalPayload.amount, finalPayload.expiry, finalPayload.paymentSecret, None)
val (firstAmount, firstExpiry, payloads) = hops.drop(1).reverse.foldLeft((finalPayload.amount, finalPayload.expiry, Seq[PaymentOnion.PerHopPayload](dummyFinalPayload))) {
case ((amount, expiry, payloads), hop) => case ((amount, expiry, payloads), hop) =>
// The next-to-last trampoline hop must include invoice data to indicate the conversion to a legacy payment. // The next-to-last node in the trampoline route must receive invoice data to indicate the conversion to a non-trampoline payment.
val payload = if (payloads.length == 1) { val payload = if (payloads.length == 1) {
PaymentOnion.createNodeRelayToNonTrampolinePayload(finalPayload.amount, finalPayload.totalAmount, finalPayload.expiry, hop.nextNodeId, invoice) PaymentOnion.createNodeRelayToNonTrampolinePayload(finalPayload.amount, finalPayload.totalAmount, finalPayload.expiry, hop.nextNodeId, invoice)
} else { } else {

View file

@ -19,12 +19,11 @@ package fr.acinq.eclair.payment
import fr.acinq.bitcoin.Crypto.{PrivateKey, PublicKey} import fr.acinq.bitcoin.Crypto.{PrivateKey, PublicKey}
import fr.acinq.bitcoin.{Base58, Base58Check, Bech32, Block, ByteVector32, ByteVector64, Crypto} import fr.acinq.bitcoin.{Base58, Base58Check, Bech32, Block, ByteVector32, ByteVector64, Crypto}
import fr.acinq.eclair.payment.PaymentRequest._ import fr.acinq.eclair.payment.PaymentRequest._
import fr.acinq.eclair.{CltvExpiryDelta, FeatureSupport, Features, MilliSatoshi, MilliSatoshiLong, NodeParams, ShortChannelId, TimestampSecond, randomBytes32} import fr.acinq.eclair.{CltvExpiryDelta, Feature, FeatureSupport, Features, InvoiceFeature, MilliSatoshi, MilliSatoshiLong, NodeParams, ShortChannelId, TimestampSecond, randomBytes32}
import scodec.bits.{BitVector, ByteOrdering, ByteVector} import scodec.bits.{BitVector, ByteOrdering, ByteVector}
import scodec.codecs.{list, ubyte} import scodec.codecs.{list, ubyte}
import scodec.{Codec, Err} import scodec.{Codec, Err}
import scala.concurrent.duration._
import scala.util.{Failure, Success, Try} import scala.util.{Failure, Success, Try}
/** /**
@ -67,6 +66,11 @@ case class PaymentRequest(prefix: String, amount: Option[MilliSatoshi], timestam
case PaymentRequest.DescriptionHash(h) => Right(h) case PaymentRequest.DescriptionHash(h) => Right(h)
}.get }.get
/**
* @return metadata about the payment (see option_payment_metadata).
*/
lazy val paymentMetadata: Option[ByteVector] = tags.collectFirst { case m: PaymentRequest.PaymentMetadata => m.data }
/** /**
* @return the fallback address if any. It could be a script address, pubkey address, .. * @return the fallback address if any. It could be a script address, pubkey address, ..
*/ */
@ -126,6 +130,11 @@ object PaymentRequest {
Block.LivenetGenesisBlock.hash -> "lnbc" Block.LivenetGenesisBlock.hash -> "lnbc"
) )
val defaultFeatures: Map[Feature with InvoiceFeature, FeatureSupport] = Map(
Features.VariableLengthOnion -> FeatureSupport.Mandatory,
Features.PaymentSecret -> FeatureSupport.Mandatory,
)
def apply(chainHash: ByteVector32, def apply(chainHash: ByteVector32,
amount: Option[MilliSatoshi], amount: Option[MilliSatoshi],
paymentHash: ByteVector32, paymentHash: ByteVector32,
@ -137,7 +146,8 @@ object PaymentRequest {
extraHops: List[List[ExtraHop]] = Nil, extraHops: List[List[ExtraHop]] = Nil,
timestamp: TimestampSecond = TimestampSecond.now(), timestamp: TimestampSecond = TimestampSecond.now(),
paymentSecret: ByteVector32 = randomBytes32(), paymentSecret: ByteVector32 = randomBytes32(),
features: PaymentRequestFeatures = PaymentRequestFeatures(Features.VariableLengthOnion.mandatory, Features.PaymentSecret.mandatory)): PaymentRequest = { paymentMetadata: Option[ByteVector] = None,
features: PaymentRequestFeatures = PaymentRequestFeatures(defaultFeatures)): PaymentRequest = {
require(features.requirePaymentSecret, "invoices must require a payment secret") require(features.requirePaymentSecret, "invoices must require a payment secret")
val prefix = prefixes(chainHash) val prefix = prefixes(chainHash)
val tags = { val tags = {
@ -145,6 +155,7 @@ object PaymentRequest {
Some(PaymentHash(paymentHash)), Some(PaymentHash(paymentHash)),
Some(description.fold(Description, DescriptionHash)), Some(description.fold(Description, DescriptionHash)),
Some(PaymentSecret(paymentSecret)), Some(PaymentSecret(paymentSecret)),
paymentMetadata.map(PaymentMetadata),
fallbackAddress.map(FallbackAddress(_)), fallbackAddress.map(FallbackAddress(_)),
expirySeconds.map(Expiry(_)), expirySeconds.map(Expiry(_)),
Some(MinFinalCltvExpiry(minFinalCltvExpiryDelta.toInt)), Some(MinFinalCltvExpiry(minFinalCltvExpiryDelta.toInt)),
@ -193,7 +204,6 @@ object PaymentRequest {
case class InvalidTag23(data: BitVector) extends InvalidTaggedField case class InvalidTag23(data: BitVector) extends InvalidTaggedField
case class UnknownTag25(data: BitVector) extends UnknownTaggedField case class UnknownTag25(data: BitVector) extends UnknownTaggedField
case class UnknownTag26(data: BitVector) extends UnknownTaggedField case class UnknownTag26(data: BitVector) extends UnknownTaggedField
case class UnknownTag27(data: BitVector) extends UnknownTaggedField
case class UnknownTag28(data: BitVector) extends UnknownTaggedField case class UnknownTag28(data: BitVector) extends UnknownTaggedField
case class UnknownTag29(data: BitVector) extends UnknownTaggedField case class UnknownTag29(data: BitVector) extends UnknownTaggedField
case class UnknownTag30(data: BitVector) extends UnknownTaggedField case class UnknownTag30(data: BitVector) extends UnknownTaggedField
@ -229,6 +239,11 @@ object PaymentRequest {
*/ */
case class DescriptionHash(hash: ByteVector32) extends TaggedField case class DescriptionHash(hash: ByteVector32) extends TaggedField
/**
* Additional metadata to attach to the payment.
*/
case class PaymentMetadata(data: ByteVector) extends TaggedField
/** /**
* Fallback Payment that specifies a fallback payment address to be used if LN payment cannot be processed * Fallback Payment that specifies a fallback payment address to be used if LN payment cannot be processed
*/ */
@ -355,8 +370,8 @@ object PaymentRequest {
} }
object PaymentRequestFeatures { object PaymentRequestFeatures {
def apply(features: Int*): PaymentRequestFeatures = PaymentRequestFeatures(long2bits(features.foldLeft(0L) { def apply(features: Map[Feature with InvoiceFeature, FeatureSupport]): PaymentRequestFeatures = PaymentRequestFeatures(long2bits(features.foldLeft(0L) {
case (current, feature) => current + (1L << feature) case (current, (feature, support)) => current + (1L << feature.supportBit(support))
})) }))
} }
@ -429,7 +444,7 @@ object PaymentRequest {
.typecase(24, dataCodec(bits).as[MinFinalCltvExpiry]) .typecase(24, dataCodec(bits).as[MinFinalCltvExpiry])
.typecase(25, dataCodec(bits).as[UnknownTag25]) .typecase(25, dataCodec(bits).as[UnknownTag25])
.typecase(26, dataCodec(bits).as[UnknownTag26]) .typecase(26, dataCodec(bits).as[UnknownTag26])
.typecase(27, dataCodec(bits).as[UnknownTag27]) .typecase(27, dataCodec(alignedBytesCodec(bytes)).as[PaymentMetadata])
.typecase(28, dataCodec(bits).as[UnknownTag28]) .typecase(28, dataCodec(bits).as[UnknownTag28])
.typecase(29, dataCodec(bits).as[UnknownTag29]) .typecase(29, dataCodec(bits).as[UnknownTag29])
.typecase(30, dataCodec(bits).as[UnknownTag30]) .typecase(30, dataCodec(bits).as[UnknownTag30])

View file

@ -27,9 +27,10 @@ import fr.acinq.eclair.channel.{CMD_FAIL_HTLC, CMD_FULFILL_HTLC, RES_SUCCESS}
import fr.acinq.eclair.db._ import fr.acinq.eclair.db._
import fr.acinq.eclair.payment.Monitoring.{Metrics, Tags} import fr.acinq.eclair.payment.Monitoring.{Metrics, Tags}
import fr.acinq.eclair.payment.PaymentRequest.{ExtraHop, PaymentRequestFeatures} import fr.acinq.eclair.payment.PaymentRequest.{ExtraHop, PaymentRequestFeatures}
import fr.acinq.eclair.payment.{IncomingPaymentPacket, PaymentReceived, PaymentRequest} import fr.acinq.eclair.payment.{IncomingPaymentPacket, PaymentMetadataReceived, PaymentReceived, PaymentRequest}
import fr.acinq.eclair.wire.protocol._ import fr.acinq.eclair.wire.protocol._
import fr.acinq.eclair.{Features, Logs, MilliSatoshi, NodeParams, randomBytes32} import fr.acinq.eclair.{Feature, FeatureSupport, Features, InvoiceFeature, Logs, MilliSatoshi, NodeParams, randomBytes32}
import scodec.bits.HexStringSyntax
import scala.util.{Failure, Success, Try} import scala.util.{Failure, Success, Try}
@ -71,7 +72,12 @@ class MultiPartHandler(nodeParams: NodeParams, register: ActorRef, db: IncomingP
Metrics.PaymentFailed.withTag(Tags.Direction, Tags.Directions.Received).withTag(Tags.Failure, Tags.FailureType(cmdFail)).increment() Metrics.PaymentFailed.withTag(Tags.Direction, Tags.Directions.Received).withTag(Tags.Failure, Tags.FailureType(cmdFail)).increment()
PendingCommandsDb.safeSend(register, nodeParams.db.pendingCommands, p.add.channelId, cmdFail) PendingCommandsDb.safeSend(register, nodeParams.db.pendingCommands, p.add.channelId, cmdFail)
case None => case None =>
log.info("received payment for amount={} totalAmount={}", p.add.amountMsat, p.payload.totalAmount) // We log whether the sender included the payment metadata field.
// We always set it in our invoices to test whether senders support it.
// Once all incoming payments correctly set that field, we can make it mandatory.
log.info("received payment for amount={} totalAmount={} paymentMetadata={}", p.add.amountMsat, p.payload.totalAmount, p.payload.paymentMetadata.map(_.toHex).getOrElse("none"))
Metrics.PaymentHtlcReceived.withTag(Tags.PaymentMetadataIncluded, p.payload.paymentMetadata.nonEmpty).increment()
p.payload.paymentMetadata.foreach(metadata => ctx.system.eventStream.publish(PaymentMetadataReceived(p.add.paymentHash, metadata)))
pendingPayments.get(p.add.paymentHash) match { pendingPayments.get(p.add.paymentHash) match {
case Some((_, handler)) => case Some((_, handler)) =>
handler ! MultiPartPaymentFSM.HtlcPart(p.payload.totalAmount, p.add) handler ! MultiPartPaymentFSM.HtlcPart(p.payload.totalAmount, p.add)
@ -86,13 +92,13 @@ class MultiPartHandler(nodeParams: NodeParams, register: ActorRef, db: IncomingP
val amount = Some(p.payload.totalAmount) val amount = Some(p.payload.totalAmount)
val paymentHash = Crypto.sha256(paymentPreimage) val paymentHash = Crypto.sha256(paymentPreimage)
val desc = Left("Donation") val desc = Left("Donation")
val features = if (nodeParams.features.hasFeature(Features.BasicMultiPartPayment)) { val features: Map[Feature with InvoiceFeature, FeatureSupport] = if (nodeParams.features.hasFeature(Features.BasicMultiPartPayment)) {
PaymentRequestFeatures(Features.BasicMultiPartPayment.optional, Features.PaymentSecret.mandatory, Features.VariableLengthOnion.mandatory) Map(Features.BasicMultiPartPayment -> FeatureSupport.Optional, Features.PaymentSecret -> FeatureSupport.Mandatory, Features.VariableLengthOnion -> FeatureSupport.Mandatory)
} else { } else {
PaymentRequestFeatures(Features.PaymentSecret.mandatory, Features.VariableLengthOnion.mandatory) Map(Features.PaymentSecret -> FeatureSupport.Mandatory, Features.VariableLengthOnion -> FeatureSupport.Mandatory)
} }
// Insert a fake invoice and then restart the incoming payment handler // Insert a fake invoice and then restart the incoming payment handler
val paymentRequest = PaymentRequest(nodeParams.chainHash, amount, paymentHash, nodeParams.privateKey, desc, nodeParams.minFinalExpiryDelta, paymentSecret = p.payload.paymentSecret, features = features) val paymentRequest = PaymentRequest(nodeParams.chainHash, amount, paymentHash, nodeParams.privateKey, desc, nodeParams.minFinalExpiryDelta, paymentSecret = p.payload.paymentSecret, features = PaymentRequestFeatures(features))
log.debug("generated fake payment request={} from amount={} (KeySend)", PaymentRequest.write(paymentRequest), amount) log.debug("generated fake payment request={} from amount={} (KeySend)", PaymentRequest.write(paymentRequest), amount)
db.addIncomingPayment(paymentRequest, paymentPreimage, paymentType = PaymentType.KeySend) db.addIncomingPayment(paymentRequest, paymentPreimage, paymentType = PaymentType.KeySend)
ctx.self ! p ctx.self ! p
@ -223,14 +229,25 @@ object MultiPartHandler {
val paymentPreimage = paymentPreimage_opt.getOrElse(randomBytes32()) val paymentPreimage = paymentPreimage_opt.getOrElse(randomBytes32())
val paymentHash = Crypto.sha256(paymentPreimage) val paymentHash = Crypto.sha256(paymentPreimage)
val expirySeconds = expirySeconds_opt.getOrElse(nodeParams.paymentRequestExpiry.toSeconds) val expirySeconds = expirySeconds_opt.getOrElse(nodeParams.paymentRequestExpiry.toSeconds)
val features = { val paymentMetadata = hex"2a"
val f1 = Seq(Features.PaymentSecret.mandatory, Features.VariableLengthOnion.mandatory) val invoiceFeatures = if (nodeParams.enableTrampolinePayment) {
val allowMultiPart = nodeParams.features.hasFeature(Features.BasicMultiPartPayment) nodeParams.features.invoiceFeatures() + (Features.TrampolinePayment -> FeatureSupport.Optional)
val f2 = if (allowMultiPart) Seq(Features.BasicMultiPartPayment.optional) else Nil } else {
val f3 = if (nodeParams.enableTrampolinePayment) Seq(Features.TrampolinePayment.optional) else Nil nodeParams.features.invoiceFeatures()
PaymentRequest.PaymentRequestFeatures(f1 ++ f2 ++ f3: _*)
} }
val paymentRequest = PaymentRequest(nodeParams.chainHash, amount_opt, paymentHash, nodeParams.privateKey, description, nodeParams.minFinalExpiryDelta, fallbackAddress_opt, expirySeconds = Some(expirySeconds), extraHops = extraHops, features = features) val paymentRequest = PaymentRequest(
nodeParams.chainHash,
amount_opt,
paymentHash,
nodeParams.privateKey,
description,
nodeParams.minFinalExpiryDelta,
fallbackAddress_opt,
expirySeconds = Some(expirySeconds),
extraHops = extraHops,
paymentMetadata = Some(paymentMetadata),
features = PaymentRequestFeatures(invoiceFeatures)
)
context.log.debug("generated payment request={} from amount={}", PaymentRequest.write(paymentRequest), amount_opt) context.log.debug("generated payment request={} from amount={}", PaymentRequest.write(paymentRequest), amount_opt)
nodeParams.db.payments.addIncomingPayment(paymentRequest, paymentPreimage, paymentType) nodeParams.db.payments.addIncomingPayment(paymentRequest, paymentPreimage, paymentType)
paymentRequest paymentRequest

View file

@ -271,13 +271,13 @@ class NodeRelay private(nodeParams: NodeParams,
val paymentSecret = payloadOut.paymentSecret.get // NB: we've verified that there was a payment secret in validateRelay val paymentSecret = payloadOut.paymentSecret.get // NB: we've verified that there was a payment secret in validateRelay
if (Features(features).hasFeature(Features.BasicMultiPartPayment)) { if (Features(features).hasFeature(Features.BasicMultiPartPayment)) {
context.log.debug("sending the payment to non-trampoline recipient using MPP") context.log.debug("sending the payment to non-trampoline recipient using MPP")
val payment = SendMultiPartPayment(payFsmAdapters, paymentSecret, payloadOut.outgoingNodeId, payloadOut.amountToForward, payloadOut.outgoingCltv, nodeParams.maxPaymentAttempts, routingHints, routeParams) val payment = SendMultiPartPayment(payFsmAdapters, paymentSecret, payloadOut.outgoingNodeId, payloadOut.amountToForward, payloadOut.outgoingCltv, nodeParams.maxPaymentAttempts, payloadOut.paymentMetadata, routingHints, routeParams)
val payFSM = outgoingPaymentFactory.spawnOutgoingPayFSM(context, paymentCfg, multiPart = true) val payFSM = outgoingPaymentFactory.spawnOutgoingPayFSM(context, paymentCfg, multiPart = true)
payFSM ! payment payFSM ! payment
payFSM payFSM
} else { } else {
context.log.debug("sending the payment to non-trampoline recipient without MPP") context.log.debug("sending the payment to non-trampoline recipient without MPP")
val finalPayload = PaymentOnion.createSinglePartPayload(payloadOut.amountToForward, payloadOut.outgoingCltv, paymentSecret) val finalPayload = PaymentOnion.createSinglePartPayload(payloadOut.amountToForward, payloadOut.outgoingCltv, paymentSecret, payloadOut.paymentMetadata)
val payment = SendPaymentToNode(payFsmAdapters, payloadOut.outgoingNodeId, finalPayload, nodeParams.maxPaymentAttempts, routingHints, routeParams) val payment = SendPaymentToNode(payFsmAdapters, payloadOut.outgoingNodeId, finalPayload, nodeParams.maxPaymentAttempts, routingHints, routeParams)
val payFSM = outgoingPaymentFactory.spawnOutgoingPayFSM(context, paymentCfg, multiPart = false) val payFSM = outgoingPaymentFactory.spawnOutgoingPayFSM(context, paymentCfg, multiPart = false)
payFSM ! payment payFSM ! payment
@ -287,7 +287,7 @@ class NodeRelay private(nodeParams: NodeParams,
context.log.debug("sending the payment to the next trampoline node") context.log.debug("sending the payment to the next trampoline node")
val payFSM = outgoingPaymentFactory.spawnOutgoingPayFSM(context, paymentCfg, multiPart = true) val payFSM = outgoingPaymentFactory.spawnOutgoingPayFSM(context, paymentCfg, multiPart = true)
val paymentSecret = randomBytes32() // we generate a new secret to protect against probing attacks val paymentSecret = randomBytes32() // we generate a new secret to protect against probing attacks
val payment = SendMultiPartPayment(payFsmAdapters, paymentSecret, payloadOut.outgoingNodeId, payloadOut.amountToForward, payloadOut.outgoingCltv, nodeParams.maxPaymentAttempts, routeParams = routeParams, additionalTlvs = Seq(OnionPaymentPayloadTlv.TrampolineOnion(packetOut))) val payment = SendMultiPartPayment(payFsmAdapters, paymentSecret, payloadOut.outgoingNodeId, payloadOut.amountToForward, payloadOut.outgoingCltv, nodeParams.maxPaymentAttempts, None, routeParams = routeParams, additionalTlvs = Seq(OnionPaymentPayloadTlv.TrampolineOnion(packetOut)))
payFSM ! payment payFSM ! payment
payFSM payFSM
} }

View file

@ -32,6 +32,7 @@ import fr.acinq.eclair.payment.send.PaymentLifecycle.SendPaymentToRoute
import fr.acinq.eclair.router.Router._ import fr.acinq.eclair.router.Router._
import fr.acinq.eclair.wire.protocol._ import fr.acinq.eclair.wire.protocol._
import fr.acinq.eclair.{CltvExpiry, FSMDiagnosticActorLogging, Logs, MilliSatoshi, MilliSatoshiLong, NodeParams, TimestampMilli} import fr.acinq.eclair.{CltvExpiry, FSMDiagnosticActorLogging, Logs, MilliSatoshi, MilliSatoshiLong, NodeParams, TimestampMilli}
import scodec.bits.ByteVector
import java.util.UUID import java.util.UUID
import java.util.concurrent.TimeUnit import java.util.concurrent.TimeUnit
@ -307,6 +308,7 @@ object MultiPartPaymentLifecycle {
* @param totalAmount total amount to send to the target node. * @param totalAmount total amount to send to the target node.
* @param targetExpiry expiry at the target node (CLTV for the target node's received HTLCs). * @param targetExpiry expiry at the target node (CLTV for the target node's received HTLCs).
* @param maxAttempts maximum number of retries. * @param maxAttempts maximum number of retries.
* @param paymentMetadata payment metadata (usually from the Bolt 11 invoice).
* @param assistedRoutes routing hints (usually from a Bolt 11 invoice). * @param assistedRoutes routing hints (usually from a Bolt 11 invoice).
* @param routeParams parameters to fine-tune the routing algorithm. * @param routeParams parameters to fine-tune the routing algorithm.
* @param additionalTlvs when provided, additional tlvs that will be added to the onion sent to the target node. * @param additionalTlvs when provided, additional tlvs that will be added to the onion sent to the target node.
@ -318,6 +320,7 @@ object MultiPartPaymentLifecycle {
totalAmount: MilliSatoshi, totalAmount: MilliSatoshi,
targetExpiry: CltvExpiry, targetExpiry: CltvExpiry,
maxAttempts: Int, maxAttempts: Int,
paymentMetadata: Option[ByteVector],
assistedRoutes: Seq[Seq[ExtraHop]] = Nil, assistedRoutes: Seq[Seq[ExtraHop]] = Nil,
routeParams: RouteParams, routeParams: RouteParams,
additionalTlvs: Seq[OnionPaymentPayloadTlv] = Nil, additionalTlvs: Seq[OnionPaymentPayloadTlv] = Nil,
@ -400,7 +403,7 @@ object MultiPartPaymentLifecycle {
Some(cfg.paymentContext)) Some(cfg.paymentContext))
private def createChildPayment(replyTo: ActorRef, route: Route, request: SendMultiPartPayment): SendPaymentToRoute = { private def createChildPayment(replyTo: ActorRef, route: Route, request: SendMultiPartPayment): SendPaymentToRoute = {
val finalPayload = PaymentOnion.createMultiPartPayload(route.amount, request.totalAmount, request.targetExpiry, request.paymentSecret, request.additionalTlvs, request.userCustomTlvs) val finalPayload = PaymentOnion.createMultiPartPayload(route.amount, request.totalAmount, request.targetExpiry, request.paymentSecret, request.paymentMetadata, request.additionalTlvs, request.userCustomTlvs)
SendPaymentToRoute(replyTo, Right(route), finalPayload) SendPaymentToRoute(replyTo, Right(route), finalPayload)
} }

View file

@ -59,9 +59,9 @@ class PaymentInitiator(nodeParams: NodeParams, outgoingPaymentFactory: PaymentIn
sender() ! PaymentFailed(paymentId, r.paymentHash, LocalFailure(r.recipientAmount, Nil, PaymentSecretMissing) :: Nil) sender() ! PaymentFailed(paymentId, r.paymentHash, LocalFailure(r.recipientAmount, Nil, PaymentSecretMissing) :: Nil)
case Some(paymentSecret) if r.paymentRequest.features.allowMultiPart && nodeParams.features.hasFeature(BasicMultiPartPayment) => case Some(paymentSecret) if r.paymentRequest.features.allowMultiPart && nodeParams.features.hasFeature(BasicMultiPartPayment) =>
val fsm = outgoingPaymentFactory.spawnOutgoingMultiPartPayment(context, paymentCfg) val fsm = outgoingPaymentFactory.spawnOutgoingMultiPartPayment(context, paymentCfg)
fsm ! SendMultiPartPayment(sender(), paymentSecret, r.recipientNodeId, r.recipientAmount, finalExpiry, r.maxAttempts, r.assistedRoutes, r.routeParams, userCustomTlvs = r.userCustomTlvs) fsm ! SendMultiPartPayment(sender(), paymentSecret, r.recipientNodeId, r.recipientAmount, finalExpiry, r.maxAttempts, r.paymentRequest.paymentMetadata, r.assistedRoutes, r.routeParams, userCustomTlvs = r.userCustomTlvs)
case Some(paymentSecret) => case Some(paymentSecret) =>
val finalPayload = PaymentOnion.createSinglePartPayload(r.recipientAmount, finalExpiry, paymentSecret, r.userCustomTlvs) val finalPayload = PaymentOnion.createSinglePartPayload(r.recipientAmount, finalExpiry, paymentSecret, r.paymentRequest.paymentMetadata, r.userCustomTlvs)
val fsm = outgoingPaymentFactory.spawnOutgoingPayment(context, paymentCfg) val fsm = outgoingPaymentFactory.spawnOutgoingPayment(context, paymentCfg)
fsm ! PaymentLifecycle.SendPaymentToNode(sender(), r.recipientNodeId, finalPayload, r.maxAttempts, r.assistedRoutes, r.routeParams) fsm ! PaymentLifecycle.SendPaymentToNode(sender(), r.recipientNodeId, finalPayload, r.maxAttempts, r.assistedRoutes, r.routeParams)
} }
@ -140,11 +140,11 @@ class PaymentInitiator(nodeParams: NodeParams, outgoingPaymentFactory: PaymentIn
sender() ! SendPaymentToRouteResponse(paymentId, parentPaymentId, Some(trampolineSecret)) sender() ! SendPaymentToRouteResponse(paymentId, parentPaymentId, Some(trampolineSecret))
val payFsm = outgoingPaymentFactory.spawnOutgoingPayment(context, paymentCfg) val payFsm = outgoingPaymentFactory.spawnOutgoingPayment(context, paymentCfg)
val (trampolineAmount, trampolineExpiry, trampolineOnion) = buildTrampolinePayment(r, trampoline, r.trampolineFees, r.trampolineExpiryDelta) val (trampolineAmount, trampolineExpiry, trampolineOnion) = buildTrampolinePayment(r, trampoline, r.trampolineFees, r.trampolineExpiryDelta)
payFsm ! PaymentLifecycle.SendPaymentToRoute(sender(), Left(r.route), PaymentOnion.createMultiPartPayload(r.amount, trampolineAmount, trampolineExpiry, trampolineSecret, Seq(OnionPaymentPayloadTlv.TrampolineOnion(trampolineOnion))), r.paymentRequest.routingInfo) payFsm ! PaymentLifecycle.SendPaymentToRoute(sender(), Left(r.route), PaymentOnion.createMultiPartPayload(r.amount, trampolineAmount, trampolineExpiry, trampolineSecret, r.paymentRequest.paymentMetadata, Seq(OnionPaymentPayloadTlv.TrampolineOnion(trampolineOnion))), r.paymentRequest.routingInfo)
case Nil => case Nil =>
sender() ! SendPaymentToRouteResponse(paymentId, parentPaymentId, None) sender() ! SendPaymentToRouteResponse(paymentId, parentPaymentId, None)
val payFsm = outgoingPaymentFactory.spawnOutgoingPayment(context, paymentCfg) val payFsm = outgoingPaymentFactory.spawnOutgoingPayment(context, paymentCfg)
payFsm ! PaymentLifecycle.SendPaymentToRoute(sender(), Left(r.route), PaymentOnion.createMultiPartPayload(r.amount, r.recipientAmount, finalExpiry, r.paymentRequest.paymentSecret.get), r.paymentRequest.routingInfo) payFsm ! PaymentLifecycle.SendPaymentToRoute(sender(), Left(r.route), PaymentOnion.createMultiPartPayload(r.amount, r.recipientAmount, finalExpiry, r.paymentRequest.paymentSecret.get, r.paymentRequest.paymentMetadata), r.paymentRequest.routingInfo)
case _ => case _ =>
sender() ! PaymentFailed(paymentId, r.paymentHash, LocalFailure(r.recipientAmount, Nil, TrampolineMultiNodeNotSupported) :: Nil) sender() ! PaymentFailed(paymentId, r.paymentHash, LocalFailure(r.recipientAmount, Nil, TrampolineMultiNodeNotSupported) :: Nil)
} }
@ -156,9 +156,9 @@ class PaymentInitiator(nodeParams: NodeParams, outgoingPaymentFactory: PaymentIn
NodeHop(trampolineNodeId, r.recipientNodeId, trampolineExpiryDelta, trampolineFees) // for now we only use a single trampoline hop NodeHop(trampolineNodeId, r.recipientNodeId, trampolineExpiryDelta, trampolineFees) // for now we only use a single trampoline hop
) )
val finalPayload = if (r.paymentRequest.features.allowMultiPart) { val finalPayload = if (r.paymentRequest.features.allowMultiPart) {
PaymentOnion.createMultiPartPayload(r.recipientAmount, r.recipientAmount, r.finalExpiry(nodeParams.currentBlockHeight), r.paymentRequest.paymentSecret.get) PaymentOnion.createMultiPartPayload(r.recipientAmount, r.recipientAmount, r.finalExpiry(nodeParams.currentBlockHeight), r.paymentRequest.paymentSecret.get, r.paymentRequest.paymentMetadata)
} else { } else {
PaymentOnion.createSinglePartPayload(r.recipientAmount, r.finalExpiry(nodeParams.currentBlockHeight), r.paymentRequest.paymentSecret.get) PaymentOnion.createSinglePartPayload(r.recipientAmount, r.finalExpiry(nodeParams.currentBlockHeight), r.paymentRequest.paymentSecret.get, r.paymentRequest.paymentMetadata)
} }
// We assume that the trampoline node supports multi-part payments (it should). // We assume that the trampoline node supports multi-part payments (it should).
val (trampolineAmount, trampolineExpiry, trampolineOnion) = if (r.paymentRequest.features.allowTrampoline) { val (trampolineAmount, trampolineExpiry, trampolineOnion) = if (r.paymentRequest.features.allowTrampoline) {
@ -175,7 +175,7 @@ class PaymentInitiator(nodeParams: NodeParams, outgoingPaymentFactory: PaymentIn
val trampolineSecret = randomBytes32() val trampolineSecret = randomBytes32()
val (trampolineAmount, trampolineExpiry, trampolineOnion) = buildTrampolinePayment(r, r.trampolineNodeId, trampolineFees, trampolineExpiryDelta) val (trampolineAmount, trampolineExpiry, trampolineOnion) = buildTrampolinePayment(r, r.trampolineNodeId, trampolineFees, trampolineExpiryDelta)
val fsm = outgoingPaymentFactory.spawnOutgoingMultiPartPayment(context, paymentCfg) val fsm = outgoingPaymentFactory.spawnOutgoingMultiPartPayment(context, paymentCfg)
fsm ! SendMultiPartPayment(self, trampolineSecret, r.trampolineNodeId, trampolineAmount, trampolineExpiry, nodeParams.maxPaymentAttempts, r.paymentRequest.routingInfo, r.routeParams, Seq(OnionPaymentPayloadTlv.TrampolineOnion(trampolineOnion))) fsm ! SendMultiPartPayment(self, trampolineSecret, r.trampolineNodeId, trampolineAmount, trampolineExpiry, nodeParams.maxPaymentAttempts, r.paymentRequest.paymentMetadata, r.paymentRequest.routingInfo, r.routeParams, Seq(OnionPaymentPayloadTlv.TrampolineOnion(trampolineOnion)))
} }
} }

View file

@ -76,7 +76,8 @@ object Announcements {
case address@(_: Tor2) => (3, address) case address@(_: Tor2) => (3, address)
case address@(_: Tor3) => (4, address) case address@(_: Tor3) => (4, address)
}.sortBy(_._1).map(_._2) }.sortBy(_._1).map(_._2)
val witness = nodeAnnouncementWitnessEncode(timestamp, nodeSecret.publicKey, color, alias, features, sortedAddresses, TlvStream.empty) val nodeAnnouncementFeatures = features.nodeAnnouncementFeatures()
val witness = nodeAnnouncementWitnessEncode(timestamp, nodeSecret.publicKey, color, alias, nodeAnnouncementFeatures, sortedAddresses, TlvStream.empty)
val sig = Crypto.sign(witness, nodeSecret) val sig = Crypto.sign(witness, nodeSecret)
NodeAnnouncement( NodeAnnouncement(
signature = sig, signature = sig,
@ -84,7 +85,7 @@ object Announcements {
nodeId = nodeSecret.publicKey, nodeId = nodeSecret.publicKey,
rgbColor = color, rgbColor = color,
alias = alias, alias = alias,
features = features, features = nodeAnnouncementFeatures,
addresses = sortedAddresses addresses = sortedAddresses
) )
} }

View file

@ -155,6 +155,12 @@ object OnionPaymentPayloadTlv {
/** Id of the next node. */ /** Id of the next node. */
case class OutgoingNodeId(nodeId: PublicKey) extends OnionPaymentPayloadTlv case class OutgoingNodeId(nodeId: PublicKey) extends OnionPaymentPayloadTlv
/**
* When payment metadata is included in a Bolt 11 invoice, we should send it as-is to the recipient.
* This lets recipients generate invoices without having to store anything on their side until the invoice is paid.
*/
case class PaymentMetadata(data: ByteVector) extends OnionPaymentPayloadTlv
/** /**
* Invoice feature bits. Only included for intermediate trampoline nodes when they should convert to a legacy payment * Invoice feature bits. Only included for intermediate trampoline nodes when they should convert to a legacy payment
* because the final recipient doesn't support trampoline. * because the final recipient doesn't support trampoline.
@ -242,6 +248,7 @@ object PaymentOnion {
val paymentSecret: ByteVector32 val paymentSecret: ByteVector32
val totalAmount: MilliSatoshi val totalAmount: MilliSatoshi
val paymentPreimage: Option[ByteVector32] val paymentPreimage: Option[ByteVector32]
val paymentMetadata: Option[ByteVector]
} }
case class RelayLegacyPayload(outgoingChannelId: ShortChannelId, amountToForward: MilliSatoshi, outgoingCltv: CltvExpiry) extends ChannelRelayPayload with LegacyFormat case class RelayLegacyPayload(outgoingChannelId: ShortChannelId, amountToForward: MilliSatoshi, outgoingCltv: CltvExpiry) extends ChannelRelayPayload with LegacyFormat
@ -267,6 +274,7 @@ object PaymentOnion {
case totalAmount => totalAmount case totalAmount => totalAmount
}).getOrElse(amountToForward) }).getOrElse(amountToForward)
val paymentSecret = records.get[PaymentData].map(_.secret) val paymentSecret = records.get[PaymentData].map(_.secret)
val paymentMetadata = records.get[PaymentMetadata].map(_.data)
val invoiceFeatures = records.get[InvoiceFeatures].map(_.features) val invoiceFeatures = records.get[InvoiceFeatures].map(_.features)
val invoiceRoutingInfo = records.get[InvoiceRoutingInfo].map(_.extraHops) val invoiceRoutingInfo = records.get[InvoiceRoutingInfo].map(_.extraHops)
} }
@ -280,6 +288,7 @@ object PaymentOnion {
case totalAmount => totalAmount case totalAmount => totalAmount
}).getOrElse(amount) }).getOrElse(amount)
override val paymentPreimage = records.get[KeySend].map(_.paymentPreimage) override val paymentPreimage = records.get[KeySend].map(_.paymentPreimage)
override val paymentMetadata = records.get[PaymentMetadata].map(_.data)
} }
def createNodeRelayPayload(amount: MilliSatoshi, expiry: CltvExpiry, nextNodeId: PublicKey): NodeRelayPayload = def createNodeRelayPayload(amount: MilliSatoshi, expiry: CltvExpiry, nextNodeId: PublicKey): NodeRelayPayload =
@ -287,16 +296,37 @@ object PaymentOnion {
/** Create a trampoline inner payload instructing the trampoline node to relay via a non-trampoline payment. */ /** Create a trampoline inner payload instructing the trampoline node to relay via a non-trampoline payment. */
def createNodeRelayToNonTrampolinePayload(amount: MilliSatoshi, totalAmount: MilliSatoshi, expiry: CltvExpiry, targetNodeId: PublicKey, invoice: PaymentRequest): NodeRelayPayload = { def createNodeRelayToNonTrampolinePayload(amount: MilliSatoshi, totalAmount: MilliSatoshi, expiry: CltvExpiry, targetNodeId: PublicKey, invoice: PaymentRequest): NodeRelayPayload = {
val tlvs = Seq[OnionPaymentPayloadTlv](AmountToForward(amount), OutgoingCltv(expiry), OutgoingNodeId(targetNodeId), InvoiceFeatures(invoice.features.toByteVector), InvoiceRoutingInfo(invoice.routingInfo.toList.map(_.toList))) val tlvs = Seq(
val tlvs2 = invoice.paymentSecret.map(s => tlvs :+ PaymentData(s, totalAmount)).getOrElse(tlvs) Some(AmountToForward(amount)),
NodeRelayPayload(TlvStream(tlvs2)) Some(OutgoingCltv(expiry)),
invoice.paymentSecret.map(s => PaymentData(s, totalAmount)),
invoice.paymentMetadata.map(m => PaymentMetadata(m)),
Some(OutgoingNodeId(targetNodeId)),
Some(InvoiceFeatures(invoice.features.toByteVector)),
Some(InvoiceRoutingInfo(invoice.routingInfo.toList.map(_.toList)))
).flatten
NodeRelayPayload(TlvStream(tlvs))
} }
def createSinglePartPayload(amount: MilliSatoshi, expiry: CltvExpiry, paymentSecret: ByteVector32, userCustomTlvs: Seq[GenericTlv] = Nil): FinalPayload = def createSinglePartPayload(amount: MilliSatoshi, expiry: CltvExpiry, paymentSecret: ByteVector32, paymentMetadata: Option[ByteVector], userCustomTlvs: Seq[GenericTlv] = Nil): FinalPayload = {
FinalTlvPayload(TlvStream(Seq(AmountToForward(amount), OutgoingCltv(expiry), PaymentData(paymentSecret, amount)), userCustomTlvs)) val tlvs = Seq(
Some(AmountToForward(amount)),
Some(OutgoingCltv(expiry)),
Some(PaymentData(paymentSecret, amount)),
paymentMetadata.map(m => PaymentMetadata(m))
).flatten
FinalTlvPayload(TlvStream(tlvs, userCustomTlvs))
}
def createMultiPartPayload(amount: MilliSatoshi, totalAmount: MilliSatoshi, expiry: CltvExpiry, paymentSecret: ByteVector32, additionalTlvs: Seq[OnionPaymentPayloadTlv] = Nil, userCustomTlvs: Seq[GenericTlv] = Nil): FinalPayload = def createMultiPartPayload(amount: MilliSatoshi, totalAmount: MilliSatoshi, expiry: CltvExpiry, paymentSecret: ByteVector32, paymentMetadata: Option[ByteVector], additionalTlvs: Seq[OnionPaymentPayloadTlv] = Nil, userCustomTlvs: Seq[GenericTlv] = Nil): FinalPayload = {
FinalTlvPayload(TlvStream(AmountToForward(amount) +: OutgoingCltv(expiry) +: PaymentData(paymentSecret, totalAmount) +: additionalTlvs, userCustomTlvs)) val tlvs = Seq(
Some(AmountToForward(amount)),
Some(OutgoingCltv(expiry)),
Some(PaymentData(paymentSecret, totalAmount)),
paymentMetadata.map(m => PaymentMetadata(m))
).flatten
FinalTlvPayload(TlvStream(tlvs ++ additionalTlvs, userCustomTlvs))
}
/** Create a trampoline outer payload. */ /** Create a trampoline outer payload. */
def createTrampolinePayload(amount: MilliSatoshi, totalAmount: MilliSatoshi, expiry: CltvExpiry, paymentSecret: ByteVector32, trampolinePacket: OnionRoutingPacket): FinalPayload = { def createTrampolinePayload(amount: MilliSatoshi, totalAmount: MilliSatoshi, expiry: CltvExpiry, paymentSecret: ByteVector32, trampolinePacket: OnionRoutingPacket): FinalPayload = {
@ -340,6 +370,8 @@ object PaymentOnionCodecs {
private val outgoingNodeId: Codec[OutgoingNodeId] = (("length" | constant(hex"21")) :: ("node_id" | publicKey)).as[OutgoingNodeId] private val outgoingNodeId: Codec[OutgoingNodeId] = (("length" | constant(hex"21")) :: ("node_id" | publicKey)).as[OutgoingNodeId]
private val paymentMetadata: Codec[PaymentMetadata] = variableSizeBytesLong(varintoverflow, "payment_metadata" | bytes).as[PaymentMetadata]
private val invoiceFeatures: Codec[InvoiceFeatures] = variableSizeBytesLong(varintoverflow, bytes).as[InvoiceFeatures] private val invoiceFeatures: Codec[InvoiceFeatures] = variableSizeBytesLong(varintoverflow, bytes).as[InvoiceFeatures]
private val invoiceRoutingInfo: Codec[InvoiceRoutingInfo] = variableSizeBytesLong(varintoverflow, list(listOfN(uint8, PaymentRequest.Codecs.extraHopCodec))).as[InvoiceRoutingInfo] private val invoiceRoutingInfo: Codec[InvoiceRoutingInfo] = variableSizeBytesLong(varintoverflow, list(listOfN(uint8, PaymentRequest.Codecs.extraHopCodec))).as[InvoiceRoutingInfo]
@ -355,6 +387,7 @@ object PaymentOnionCodecs {
.typecase(UInt64(8), paymentData) .typecase(UInt64(8), paymentData)
.typecase(UInt64(10), encryptedRecipientData) .typecase(UInt64(10), encryptedRecipientData)
.typecase(UInt64(12), blindingPoint) .typecase(UInt64(12), blindingPoint)
.typecase(UInt64(16), paymentMetadata)
// Types below aren't specified - use cautiously when deploying (be careful with backwards-compatibility). // Types below aren't specified - use cautiously when deploying (be careful with backwards-compatibility).
.typecase(UInt64(66097), invoiceFeatures) .typecase(UInt64(66097), invoiceFeatures)
.typecase(UInt64(66098), outgoingNodeId) .typecase(UInt64(66098), outgoingNodeId)

View file

@ -211,6 +211,22 @@ class FeaturesSpec extends AnyFunSuite {
} }
} }
test("filter features based on their usage") {
val features = Features(
Map[Feature, FeatureSupport](OptionDataLossProtect -> Optional, InitialRoutingSync -> Optional, VariableLengthOnion -> Mandatory, PaymentMetadata -> Optional),
Set(UnknownFeature(753), UnknownFeature(852))
)
assert(features.initFeatures() === Features(
Map[Feature, FeatureSupport](OptionDataLossProtect -> Optional, InitialRoutingSync -> Optional, VariableLengthOnion -> Mandatory),
Set(UnknownFeature(753), UnknownFeature(852))
))
assert(features.nodeAnnouncementFeatures() === Features(
Map[Feature, FeatureSupport](OptionDataLossProtect -> Optional, VariableLengthOnion -> Mandatory),
Set(UnknownFeature(753), UnknownFeature(852))
))
assert(features.invoiceFeatures() === Map[Feature, FeatureSupport](VariableLengthOnion -> Mandatory, PaymentMetadata -> Optional))
}
test("features to bytes") { test("features to bytes") {
val testCases = Map( val testCases = Map(
hex"" -> Features.empty, hex"" -> Features.empty,

View file

@ -181,10 +181,47 @@ class StartupSpec extends AnyFunSuite {
) )
val nodeParams = makeNodeParamsWithDefaults(perNodeConf.withFallback(defaultConf)) val nodeParams = makeNodeParamsWithDefaults(perNodeConf.withFallback(defaultConf))
val perNodeFeatures = nodeParams.featuresFor(PublicKey(ByteVector.fromValidHex("02aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa"))) val perNodeFeatures = nodeParams.initFeaturesFor(PublicKey(ByteVector.fromValidHex("02aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa")))
assert(perNodeFeatures === Features(VariableLengthOnion -> Mandatory, PaymentSecret -> Mandatory, BasicMultiPartPayment -> Mandatory, ChannelType -> Optional)) assert(perNodeFeatures === Features(VariableLengthOnion -> Mandatory, PaymentSecret -> Mandatory, BasicMultiPartPayment -> Mandatory, ChannelType -> Optional))
} }
test("filter out non-init features in node override") {
val perNodeConf = ConfigFactory.parseString(
"""
| override-features = [ // optional per-node features
| {
| nodeid = "02aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa",
| features {
| var_onion_optin = mandatory
| payment_secret = mandatory
| option_channel_type = optional
| option_payment_metadata = disabled
| }
| },
| {
| nodeid = "02bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb",
| features {
| var_onion_optin = mandatory
| payment_secret = mandatory
| option_channel_type = optional
| option_payment_metadata = mandatory
| }
| }
| ]
""".stripMargin
)
val nodeParams = makeNodeParamsWithDefaults(perNodeConf.withFallback(defaultConf))
val perNodeFeaturesA = nodeParams.initFeaturesFor(PublicKey(ByteVector.fromValidHex("02aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa")))
val perNodeFeaturesB = nodeParams.initFeaturesFor(PublicKey(ByteVector.fromValidHex("02bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb")))
val defaultNodeFeatures = nodeParams.initFeaturesFor(PublicKey(ByteVector.fromValidHex("02cccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccc")))
// Some features should never be sent in init messages.
assert(nodeParams.features.hasFeature(PaymentMetadata))
assert(!perNodeFeaturesA.hasFeature(PaymentMetadata))
assert(!perNodeFeaturesB.hasFeature(PaymentMetadata))
assert(!defaultNodeFeatures.hasFeature(PaymentMetadata))
}
test("override feerate mismatch tolerance") { test("override feerate mismatch tolerance") {
val perNodeConf = ConfigFactory.parseString( val perNodeConf = ConfigFactory.parseString(
""" """

View file

@ -64,7 +64,7 @@ object TestConstants {
} }
} }
case object TestFeature extends Feature { case object TestFeature extends Feature with InitFeature with NodeFeature {
val rfcName = "test_feature" val rfcName = "test_feature"
val mandatory = 50000 val mandatory = 50000
} }
@ -106,7 +106,8 @@ object TestConstants {
ChannelRangeQueriesExtended -> Optional, ChannelRangeQueriesExtended -> Optional,
VariableLengthOnion -> Mandatory, VariableLengthOnion -> Mandatory,
PaymentSecret -> Mandatory, PaymentSecret -> Mandatory,
BasicMultiPartPayment -> Optional BasicMultiPartPayment -> Optional,
PaymentMetadata -> Optional,
), ),
Set(UnknownFeature(TestFeature.optional)) Set(UnknownFeature(TestFeature.optional))
), ),
@ -240,7 +241,8 @@ object TestConstants {
ChannelRangeQueriesExtended -> Optional, ChannelRangeQueriesExtended -> Optional,
VariableLengthOnion -> Mandatory, VariableLengthOnion -> Mandatory,
PaymentSecret -> Mandatory, PaymentSecret -> Mandatory,
BasicMultiPartPayment -> Optional BasicMultiPartPayment -> Optional,
PaymentMetadata -> Optional,
), ),
pluginParams = Nil, pluginParams = Nil,
overrideFeatures = Map.empty, overrideFeatures = Map.empty,

View file

@ -122,7 +122,7 @@ class FuzzySpec extends TestKitBaseClass with FixtureAnyFunSuiteLike with Channe
// allow overpaying (no more than 2 times the required amount) // allow overpaying (no more than 2 times the required amount)
val amount = requiredAmount + Random.nextInt(requiredAmount.toLong.toInt).msat val amount = requiredAmount + Random.nextInt(requiredAmount.toLong.toInt).msat
val expiry = (Channel.MIN_CLTV_EXPIRY_DELTA + 1).toCltvExpiry(blockHeight = 400000) val expiry = (Channel.MIN_CLTV_EXPIRY_DELTA + 1).toCltvExpiry(blockHeight = 400000)
OutgoingPaymentPacket.buildCommand(self, Upstream.Local(UUID.randomUUID()), paymentHash, ChannelHop(null, dest, null) :: Nil, PaymentOnion.createSinglePartPayload(amount, expiry, paymentSecret))._1 OutgoingPaymentPacket.buildCommand(self, Upstream.Local(UUID.randomUUID()), paymentHash, ChannelHop(null, dest, null) :: Nil, PaymentOnion.createSinglePartPayload(amount, expiry, paymentSecret, None))._1
} }
def initiatePaymentOrStop(remaining: Int): Unit = def initiatePaymentOrStop(remaining: Int): Unit =

View file

@ -246,7 +246,7 @@ trait ChannelStateTestsHelperMethods extends TestKitBase {
def makeCmdAdd(amount: MilliSatoshi, cltvExpiryDelta: CltvExpiryDelta, destination: PublicKey, paymentPreimage: ByteVector32, currentBlockHeight: Long, upstream: Upstream, replyTo: ActorRef = TestProbe().ref): (ByteVector32, CMD_ADD_HTLC) = { def makeCmdAdd(amount: MilliSatoshi, cltvExpiryDelta: CltvExpiryDelta, destination: PublicKey, paymentPreimage: ByteVector32, currentBlockHeight: Long, upstream: Upstream, replyTo: ActorRef = TestProbe().ref): (ByteVector32, CMD_ADD_HTLC) = {
val paymentHash: ByteVector32 = Crypto.sha256(paymentPreimage) val paymentHash: ByteVector32 = Crypto.sha256(paymentPreimage)
val expiry = cltvExpiryDelta.toCltvExpiry(currentBlockHeight) val expiry = cltvExpiryDelta.toCltvExpiry(currentBlockHeight)
val cmd = OutgoingPaymentPacket.buildCommand(replyTo, upstream, paymentHash, ChannelHop(null, destination, null) :: Nil, PaymentOnion.createSinglePartPayload(amount, expiry, randomBytes32()))._1.copy(commit = false) val cmd = OutgoingPaymentPacket.buildCommand(replyTo, upstream, paymentHash, ChannelHop(null, destination, null) :: Nil, PaymentOnion.createSinglePartPayload(amount, expiry, randomBytes32(), None))._1.copy(commit = false)
(paymentPreimage, cmd) (paymentPreimage, cmd)
} }

View file

@ -59,7 +59,7 @@ class ShutdownStateSpec extends TestKitBaseClass with FixtureAnyFunSuiteLike wit
val h1 = Crypto.sha256(r1) val h1 = Crypto.sha256(r1)
val amount1 = 300000000 msat val amount1 = 300000000 msat
val expiry1 = CltvExpiryDelta(144).toCltvExpiry(currentBlockHeight) val expiry1 = CltvExpiryDelta(144).toCltvExpiry(currentBlockHeight)
val cmd1 = OutgoingPaymentPacket.buildCommand(sender.ref, Upstream.Local(UUID.randomUUID), h1, ChannelHop(null, TestConstants.Bob.nodeParams.nodeId, null) :: Nil, PaymentOnion.createSinglePartPayload(amount1, expiry1, randomBytes32()))._1.copy(commit = false) val cmd1 = OutgoingPaymentPacket.buildCommand(sender.ref, Upstream.Local(UUID.randomUUID), h1, ChannelHop(null, TestConstants.Bob.nodeParams.nodeId, null) :: Nil, PaymentOnion.createSinglePartPayload(amount1, expiry1, randomBytes32(), None))._1.copy(commit = false)
alice ! cmd1 alice ! cmd1
sender.expectMsgType[RES_SUCCESS[CMD_ADD_HTLC]] sender.expectMsgType[RES_SUCCESS[CMD_ADD_HTLC]]
val htlc1 = alice2bob.expectMsgType[UpdateAddHtlc] val htlc1 = alice2bob.expectMsgType[UpdateAddHtlc]
@ -69,7 +69,7 @@ class ShutdownStateSpec extends TestKitBaseClass with FixtureAnyFunSuiteLike wit
val h2 = Crypto.sha256(r2) val h2 = Crypto.sha256(r2)
val amount2 = 200000000 msat val amount2 = 200000000 msat
val expiry2 = CltvExpiryDelta(144).toCltvExpiry(currentBlockHeight) val expiry2 = CltvExpiryDelta(144).toCltvExpiry(currentBlockHeight)
val cmd2 = OutgoingPaymentPacket.buildCommand(sender.ref, Upstream.Local(UUID.randomUUID), h2, ChannelHop(null, TestConstants.Bob.nodeParams.nodeId, null) :: Nil, PaymentOnion.createSinglePartPayload(amount2, expiry2, randomBytes32()))._1.copy(commit = false) val cmd2 = OutgoingPaymentPacket.buildCommand(sender.ref, Upstream.Local(UUID.randomUUID), h2, ChannelHop(null, TestConstants.Bob.nodeParams.nodeId, null) :: Nil, PaymentOnion.createSinglePartPayload(amount2, expiry2, randomBytes32(), None))._1.copy(commit = false)
alice ! cmd2 alice ! cmd2
sender.expectMsgType[RES_SUCCESS[CMD_ADD_HTLC]] sender.expectMsgType[RES_SUCCESS[CMD_ADD_HTLC]]
val htlc2 = alice2bob.expectMsgType[UpdateAddHtlc] val htlc2 = alice2bob.expectMsgType[UpdateAddHtlc]

View file

@ -154,11 +154,15 @@ class PaymentIntegrationSpec extends IntegrationSpec {
} }
test("send an HTLC A->D") { test("send an HTLC A->D") {
val sender = TestProbe() val (sender, eventListener) = (TestProbe(), TestProbe())
val amountMsat = 4200000.msat nodes("D").system.eventStream.subscribe(eventListener.ref, classOf[PaymentMetadataReceived])
// first we retrieve a payment hash from D // first we retrieve a payment hash from D
val amountMsat = 4200000.msat
sender.send(nodes("D").paymentHandler, ReceivePayment(Some(amountMsat), Left("1 coffee"))) sender.send(nodes("D").paymentHandler, ReceivePayment(Some(amountMsat), Left("1 coffee")))
val pr = sender.expectMsgType[PaymentRequest] val pr = sender.expectMsgType[PaymentRequest]
assert(pr.paymentMetadata.nonEmpty)
// then we make the actual payment // then we make the actual payment
sender.send(nodes("A").paymentInitiator, SendPaymentToNode(amountMsat, pr, fallbackFinalExpiryDelta = finalCltvExpiryDelta, routeParams = integrationTestRouteParams, maxAttempts = 1)) sender.send(nodes("A").paymentInitiator, SendPaymentToNode(amountMsat, pr, fallbackFinalExpiryDelta = finalCltvExpiryDelta, routeParams = integrationTestRouteParams, maxAttempts = 1))
val paymentId = sender.expectMsgType[UUID] val paymentId = sender.expectMsgType[UUID]
@ -166,6 +170,7 @@ class PaymentIntegrationSpec extends IntegrationSpec {
assert(Crypto.sha256(preimage) === pr.paymentHash) assert(Crypto.sha256(preimage) === pr.paymentHash)
val ps = sender.expectMsgType[PaymentSent] val ps = sender.expectMsgType[PaymentSent]
assert(ps.id == paymentId) assert(ps.id == paymentId)
eventListener.expectMsg(PaymentMetadataReceived(pr.paymentHash, pr.paymentMetadata.get))
} }
test("send an HTLC A->D with an invalid expiry delta for B") { test("send an HTLC A->D with an invalid expiry delta for B") {
@ -503,12 +508,14 @@ class PaymentIntegrationSpec extends IntegrationSpec {
test("send a trampoline payment D->B (via trampoline C)") { test("send a trampoline payment D->B (via trampoline C)") {
val start = TimestampMilli.now() val start = TimestampMilli.now()
val sender = TestProbe() val (sender, eventListener) = (TestProbe(), TestProbe())
nodes("B").system.eventStream.subscribe(eventListener.ref, classOf[PaymentMetadataReceived])
val amount = 2500000000L.msat val amount = 2500000000L.msat
sender.send(nodes("B").paymentHandler, ReceivePayment(Some(amount), Left("trampoline-MPP is so #reckless"))) sender.send(nodes("B").paymentHandler, ReceivePayment(Some(amount), Left("trampoline-MPP is so #reckless")))
val pr = sender.expectMsgType[PaymentRequest] val pr = sender.expectMsgType[PaymentRequest]
assert(pr.features.allowMultiPart) assert(pr.features.allowMultiPart)
assert(pr.features.allowTrampoline) assert(pr.features.allowTrampoline)
assert(pr.paymentMetadata.nonEmpty)
val payment = SendTrampolinePayment(amount, pr, nodes("C").nodeParams.nodeId, Seq((350000 msat, CltvExpiryDelta(288))), routeParams = integrationTestRouteParams) val payment = SendTrampolinePayment(amount, pr, nodes("C").nodeParams.nodeId, Seq((350000 msat, CltvExpiryDelta(288))), routeParams = integrationTestRouteParams)
sender.send(nodes("D").paymentInitiator, payment) sender.send(nodes("D").paymentInitiator, payment)
@ -523,6 +530,7 @@ class PaymentIntegrationSpec extends IntegrationSpec {
awaitCond(nodes("B").nodeParams.db.payments.getIncomingPayment(pr.paymentHash).exists(_.status.isInstanceOf[IncomingPaymentStatus.Received])) awaitCond(nodes("B").nodeParams.db.payments.getIncomingPayment(pr.paymentHash).exists(_.status.isInstanceOf[IncomingPaymentStatus.Received]))
val Some(IncomingPayment(_, _, _, _, IncomingPaymentStatus.Received(receivedAmount, _))) = nodes("B").nodeParams.db.payments.getIncomingPayment(pr.paymentHash) val Some(IncomingPayment(_, _, _, _, IncomingPaymentStatus.Received(receivedAmount, _))) = nodes("B").nodeParams.db.payments.getIncomingPayment(pr.paymentHash)
assert(receivedAmount === amount) assert(receivedAmount === amount)
eventListener.expectMsg(PaymentMetadataReceived(pr.paymentHash, pr.paymentMetadata.get))
awaitCond({ awaitCond({
val relayed = nodes("C").nodeParams.db.audit.listRelayed(start, TimestampMilli.now()).filter(_.paymentHash == pr.paymentHash) val relayed = nodes("C").nodeParams.db.audit.listRelayed(start, TimestampMilli.now()).filter(_.paymentHash == pr.paymentHash)
@ -548,7 +556,8 @@ class PaymentIntegrationSpec extends IntegrationSpec {
test("send a trampoline payment F1->A (via trampoline C, non-trampoline recipient)") { test("send a trampoline payment F1->A (via trampoline C, non-trampoline recipient)") {
// The A -> B channel is not announced. // The A -> B channel is not announced.
val start = TimestampMilli.now() val start = TimestampMilli.now()
val sender = TestProbe() val (sender, eventListener) = (TestProbe(), TestProbe())
nodes("A").system.eventStream.subscribe(eventListener.ref, classOf[PaymentMetadataReceived])
sender.send(nodes("B").relayer, Relayer.GetOutgoingChannels()) sender.send(nodes("B").relayer, Relayer.GetOutgoingChannels())
val channelUpdate_ba = sender.expectMsgType[Relayer.OutgoingChannels].channels.filter(c => c.nextNodeId == nodes("A").nodeParams.nodeId).head.channelUpdate val channelUpdate_ba = sender.expectMsgType[Relayer.OutgoingChannels].channels.filter(c => c.nextNodeId == nodes("A").nodeParams.nodeId).head.channelUpdate
val routingHints = List(List(ExtraHop(nodes("B").nodeParams.nodeId, channelUpdate_ba.shortChannelId, channelUpdate_ba.feeBaseMsat, channelUpdate_ba.feeProportionalMillionths, channelUpdate_ba.cltvExpiryDelta))) val routingHints = List(List(ExtraHop(nodes("B").nodeParams.nodeId, channelUpdate_ba.shortChannelId, channelUpdate_ba.feeBaseMsat, channelUpdate_ba.feeProportionalMillionths, channelUpdate_ba.cltvExpiryDelta)))
@ -558,6 +567,7 @@ class PaymentIntegrationSpec extends IntegrationSpec {
val pr = sender.expectMsgType[PaymentRequest] val pr = sender.expectMsgType[PaymentRequest]
assert(pr.features.allowMultiPart) assert(pr.features.allowMultiPart)
assert(!pr.features.allowTrampoline) assert(!pr.features.allowTrampoline)
assert(pr.paymentMetadata.nonEmpty)
val payment = SendTrampolinePayment(amount, pr, nodes("C").nodeParams.nodeId, Seq((1000000 msat, CltvExpiryDelta(432))), routeParams = integrationTestRouteParams) val payment = SendTrampolinePayment(amount, pr, nodes("C").nodeParams.nodeId, Seq((1000000 msat, CltvExpiryDelta(432))), routeParams = integrationTestRouteParams)
sender.send(nodes("F").paymentInitiator, payment) sender.send(nodes("F").paymentInitiator, payment)
@ -571,6 +581,7 @@ class PaymentIntegrationSpec extends IntegrationSpec {
awaitCond(nodes("A").nodeParams.db.payments.getIncomingPayment(pr.paymentHash).exists(_.status.isInstanceOf[IncomingPaymentStatus.Received])) awaitCond(nodes("A").nodeParams.db.payments.getIncomingPayment(pr.paymentHash).exists(_.status.isInstanceOf[IncomingPaymentStatus.Received]))
val Some(IncomingPayment(_, _, _, _, IncomingPaymentStatus.Received(receivedAmount, _))) = nodes("A").nodeParams.db.payments.getIncomingPayment(pr.paymentHash) val Some(IncomingPayment(_, _, _, _, IncomingPaymentStatus.Received(receivedAmount, _))) = nodes("A").nodeParams.db.payments.getIncomingPayment(pr.paymentHash)
assert(receivedAmount === amount) assert(receivedAmount === amount)
eventListener.expectMsg(PaymentMetadataReceived(pr.paymentHash, pr.paymentMetadata.get))
awaitCond({ awaitCond({
val relayed = nodes("C").nodeParams.db.audit.listRelayed(start, TimestampMilli.now()).filter(_.paymentHash == pr.paymentHash) val relayed = nodes("C").nodeParams.db.audit.listRelayed(start, TimestampMilli.now()).filter(_.paymentHash == pr.paymentHash)

View file

@ -77,7 +77,7 @@ class SwitchboardSpec extends TestKitBaseClass with AnyFunSuiteLike {
val nodeParams = Alice.nodeParams.copy(syncWhitelist = Set.empty) val nodeParams = Alice.nodeParams.copy(syncWhitelist = Set.empty)
val remoteNodeId = ChannelCodecsSpec.normal.commitments.remoteParams.nodeId val remoteNodeId = ChannelCodecsSpec.normal.commitments.remoteParams.nodeId
nodeParams.db.channels.addOrUpdateChannel(ChannelCodecsSpec.normal) nodeParams.db.channels.addOrUpdateChannel(ChannelCodecsSpec.normal)
sendFeatures(nodeParams, remoteNodeId, nodeParams.features, expectedSync = true) sendFeatures(nodeParams, remoteNodeId, nodeParams.features.initFeatures(), expectedSync = true)
} }
test("sync if no whitelist is defined and peer creates a channel") { test("sync if no whitelist is defined and peer creates a channel") {
@ -91,7 +91,7 @@ class SwitchboardSpec extends TestKitBaseClass with AnyFunSuiteLike {
switchboard ! PeerConnection.Authenticated(peerConnection.ref, remoteNodeId) switchboard ! PeerConnection.Authenticated(peerConnection.ref, remoteNodeId)
val initConnection1 = peerConnection.expectMsgType[PeerConnection.InitializeConnection] val initConnection1 = peerConnection.expectMsgType[PeerConnection.InitializeConnection]
assert(initConnection1.chainHash === nodeParams.chainHash) assert(initConnection1.chainHash === nodeParams.chainHash)
assert(initConnection1.features === nodeParams.features) assert(initConnection1.features === nodeParams.features.initFeatures())
assert(initConnection1.doSync) assert(initConnection1.doSync)
// We don't have channels with our peer, so we won't trigger a sync when connecting. // We don't have channels with our peer, so we won't trigger a sync when connecting.
@ -99,26 +99,26 @@ class SwitchboardSpec extends TestKitBaseClass with AnyFunSuiteLike {
switchboard ! PeerConnection.Authenticated(peerConnection.ref, remoteNodeId) switchboard ! PeerConnection.Authenticated(peerConnection.ref, remoteNodeId)
val initConnection2 = peerConnection.expectMsgType[PeerConnection.InitializeConnection] val initConnection2 = peerConnection.expectMsgType[PeerConnection.InitializeConnection]
assert(initConnection2.chainHash === nodeParams.chainHash) assert(initConnection2.chainHash === nodeParams.chainHash)
assert(initConnection2.features === nodeParams.features) assert(initConnection2.features === nodeParams.features.initFeatures())
assert(!initConnection2.doSync) assert(!initConnection2.doSync)
} }
test("don't sync if no whitelist is defined and peer does not have channels") { test("don't sync if no whitelist is defined and peer does not have channels") {
val nodeParams = Alice.nodeParams.copy(syncWhitelist = Set.empty) val nodeParams = Alice.nodeParams.copy(syncWhitelist = Set.empty)
sendFeatures(nodeParams, randomKey().publicKey, nodeParams.features, expectedSync = false) sendFeatures(nodeParams, randomKey().publicKey, nodeParams.features.initFeatures(), expectedSync = false)
} }
test("sync if whitelist contains peer") { test("sync if whitelist contains peer") {
val remoteNodeId = randomKey().publicKey val remoteNodeId = randomKey().publicKey
val nodeParams = Alice.nodeParams.copy(syncWhitelist = Set(remoteNodeId, randomKey().publicKey, randomKey().publicKey)) val nodeParams = Alice.nodeParams.copy(syncWhitelist = Set(remoteNodeId, randomKey().publicKey, randomKey().publicKey))
sendFeatures(nodeParams, remoteNodeId, nodeParams.features, expectedSync = true) sendFeatures(nodeParams, remoteNodeId, nodeParams.features.initFeatures(), expectedSync = true)
} }
test("don't sync if whitelist doesn't contain peer") { test("don't sync if whitelist doesn't contain peer") {
val nodeParams = Alice.nodeParams.copy(syncWhitelist = Set(randomKey().publicKey, randomKey().publicKey, randomKey().publicKey)) val nodeParams = Alice.nodeParams.copy(syncWhitelist = Set(randomKey().publicKey, randomKey().publicKey, randomKey().publicKey))
val remoteNodeId = ChannelCodecsSpec.normal.commitments.remoteParams.nodeId val remoteNodeId = ChannelCodecsSpec.normal.commitments.remoteParams.nodeId
nodeParams.db.channels.addOrUpdateChannel(ChannelCodecsSpec.normal) nodeParams.db.channels.addOrUpdateChannel(ChannelCodecsSpec.normal)
sendFeatures(nodeParams, remoteNodeId, nodeParams.features, expectedSync = false) sendFeatures(nodeParams, remoteNodeId, nodeParams.features.initFeatures(), expectedSync = false)
} }
test("get peer info") { test("get peer info") {

View file

@ -179,6 +179,12 @@ class JsonSerializersSpec extends AnyFunSuite with Matchers {
JsonSerializers.serialization.write(pr)(JsonSerializers.formats) shouldBe """{"prefix":"lntb","timestamp":1622474982,"nodeId":"03e89e4c3d41dc5332c2fb6cc66d12bfb9257ba681945a242f27a08d5ad210d891","serialized":"lntb1pst2q8xpp5qysan6j5xeq97tytxf7pfr0n75na8rztqhh03glmlgsqsyuqzgnqdqqxqrrss9qy9qsqsp5qq67gcxrn2drj5p0lc6p8wgdpqwxnc2h4s9kra5489q0fqsvhumsrzjqfqnj4upt5z6hdludky9vgk4ehzmwu2dk9rcevzczw5ywstehq79c83xr5qqqkqqqqqqqqlgqqqqqeqqjqrzjqwfn3p9278ttzzpe0e00uhyxhned3j5d9acqak5emwfpflp8z2cng838tqqqqxgqqqqqqqlgqqqqqeqqjqkxs4223x2r6sat65asfp0k2pze2rswe9np9vq08waqvsp832ffgymzgx8hgzejasesfxwcw6jj93azwq9klwuzmef3llns3n95pztgqpawp7an","description":"","paymentHash":"0121d9ea5436405f2c8b327c148df3f527d38c4b05eef8a3fbfa200813801226","expiry":3600,"features":{"activated":{"var_onion_optin":"optional","payment_secret":"optional","basic_mpp":"optional"},"unknown":[]},"routingInfo":[[{"nodeId":"02413957815d05abb7fc6d885622d5cdc5b7714db1478cb05813a8474179b83c5c","shortChannelId":"1975837x88x0","feeBase":1000,"feeProportionalMillionths":100,"cltvExpiryDelta":144}],[{"nodeId":"03933884aaf1d6b108397e5efe5c86bcf2d8ca8d2f700eda99db9214fc2712b134","shortChannelId":"1976152x25x0","feeBase":1000,"feeProportionalMillionths":100,"cltvExpiryDelta":144}]]}""" JsonSerializers.serialization.write(pr)(JsonSerializers.formats) shouldBe """{"prefix":"lntb","timestamp":1622474982,"nodeId":"03e89e4c3d41dc5332c2fb6cc66d12bfb9257ba681945a242f27a08d5ad210d891","serialized":"lntb1pst2q8xpp5qysan6j5xeq97tytxf7pfr0n75na8rztqhh03glmlgsqsyuqzgnqdqqxqrrss9qy9qsqsp5qq67gcxrn2drj5p0lc6p8wgdpqwxnc2h4s9kra5489q0fqsvhumsrzjqfqnj4upt5z6hdludky9vgk4ehzmwu2dk9rcevzczw5ywstehq79c83xr5qqqkqqqqqqqqlgqqqqqeqqjqrzjqwfn3p9278ttzzpe0e00uhyxhned3j5d9acqak5emwfpflp8z2cng838tqqqqxgqqqqqqqlgqqqqqeqqjqkxs4223x2r6sat65asfp0k2pze2rswe9np9vq08waqvsp832ffgymzgx8hgzejasesfxwcw6jj93azwq9klwuzmef3llns3n95pztgqpawp7an","description":"","paymentHash":"0121d9ea5436405f2c8b327c148df3f527d38c4b05eef8a3fbfa200813801226","expiry":3600,"features":{"activated":{"var_onion_optin":"optional","payment_secret":"optional","basic_mpp":"optional"},"unknown":[]},"routingInfo":[[{"nodeId":"02413957815d05abb7fc6d885622d5cdc5b7714db1478cb05813a8474179b83c5c","shortChannelId":"1975837x88x0","feeBase":1000,"feeProportionalMillionths":100,"cltvExpiryDelta":144}],[{"nodeId":"03933884aaf1d6b108397e5efe5c86bcf2d8ca8d2f700eda99db9214fc2712b134","shortChannelId":"1976152x25x0","feeBase":1000,"feeProportionalMillionths":100,"cltvExpiryDelta":144}]]}"""
} }
test("Payment Request with metadata") {
val ref = "lnbc10m1pvjluezpp5qqqsyqcyq5rqwzqfqqqsyqcyq5rqwzqfqqqsyqcyq5rqwzqfqypqdp9wpshjmt9de6zqmt9w3skgct5vysxjmnnd9jx2mq8q8a04uqnp4q0n326hr8v9zprg8gsvezcch06gfaqqhde2aj730yg0durunfhv66sp5zyg3zyg3zyg3zyg3zyg3zyg3zyg3zyg3zyg3zyg3zyg3zyg3zygs9q2gqqqqqqsgqy9gw6ymamd20jumvdgpfphkhp8fzhhdhycw36egcmla5vlrtrmhs9t7psfy3hkkdqzm9eq64fjg558znccds5nhsfmxveha5xe0dykgpspdha0"
val pr = PaymentRequest.read(ref)
JsonSerializers.serialization.write(pr)(JsonSerializers.formats) shouldBe """{"prefix":"lnbc","timestamp":1496314658,"nodeId":"03e7156ae33b0a208d0744199163177e909e80176e55d97a2f221ede0f934dd9ad","serialized":"lnbc10m1pvjluezpp5qqqsyqcyq5rqwzqfqqqsyqcyq5rqwzqfqqqsyqcyq5rqwzqfqypqdp9wpshjmt9de6zqmt9w3skgct5vysxjmnnd9jx2mq8q8a04uqnp4q0n326hr8v9zprg8gsvezcch06gfaqqhde2aj730yg0durunfhv66sp5zyg3zyg3zyg3zyg3zyg3zyg3zyg3zyg3zyg3zyg3zyg3zyg3zygs9q2gqqqqqqsgqy9gw6ymamd20jumvdgpfphkhp8fzhhdhycw36egcmla5vlrtrmhs9t7psfy3hkkdqzm9eq64fjg558znccds5nhsfmxveha5xe0dykgpspdha0","description":"payment metadata inside","paymentHash":"0001020304050607080900010203040506070809000102030405060708090102","paymentMetadata":"01fafaf0","amount":1000000000,"features":{"activated":{"var_onion_optin":"mandatory","payment_secret":"mandatory","option_payment_metadata":"mandatory"},"unknown":[]},"routingInfo":[]}"""
}
test("GlobalBalance serializer") { test("GlobalBalance serializer") {
val gb = GlobalBalance( val gb = GlobalBalance(
onChain = CheckBalance.CorrectedOnChainBalance(Btc(0.4), Btc(0.05)), onChain = CheckBalance.CorrectedOnChainBalance(Btc(0.4), Btc(0.05)),

View file

@ -35,6 +35,7 @@ import fr.acinq.eclair.wire.protocol._
import fr.acinq.eclair.{CltvExpiry, CltvExpiryDelta, Features, MilliSatoshiLong, NodeParams, ShortChannelId, TestConstants, TestKitBaseClass, TimestampMilliLong, randomBytes32, randomKey} import fr.acinq.eclair.{CltvExpiry, CltvExpiryDelta, Features, MilliSatoshiLong, NodeParams, ShortChannelId, TestConstants, TestKitBaseClass, TimestampMilliLong, randomBytes32, randomKey}
import org.scalatest.Outcome import org.scalatest.Outcome
import org.scalatest.funsuite.FixtureAnyFunSuiteLike import org.scalatest.funsuite.FixtureAnyFunSuiteLike
import scodec.bits.HexStringSyntax
import scala.collection.immutable.Queue import scala.collection.immutable.Queue
import scala.concurrent.duration._ import scala.concurrent.duration._
@ -93,7 +94,7 @@ class MultiPartHandlerSpec extends TestKitBaseClass with FixtureAnyFunSuiteLike
assert(Crypto.sha256(incoming.get.paymentPreimage) === pr.paymentHash) assert(Crypto.sha256(incoming.get.paymentPreimage) === pr.paymentHash)
val add = UpdateAddHtlc(ByteVector32.One, 0, amountMsat, pr.paymentHash, defaultExpiry, TestConstants.emptyOnionPacket) val add = UpdateAddHtlc(ByteVector32.One, 0, amountMsat, pr.paymentHash, defaultExpiry, TestConstants.emptyOnionPacket)
sender.send(handlerWithoutMpp, IncomingPaymentPacket.FinalPacket(add, PaymentOnion.createSinglePartPayload(add.amountMsat, add.cltvExpiry, pr.paymentSecret.get))) sender.send(handlerWithoutMpp, IncomingPaymentPacket.FinalPacket(add, PaymentOnion.createSinglePartPayload(add.amountMsat, add.cltvExpiry, pr.paymentSecret.get, pr.paymentMetadata)))
register.expectMsgType[Register.Forward[CMD_FULFILL_HTLC]] register.expectMsgType[Register.Forward[CMD_FULFILL_HTLC]]
val paymentReceived = eventListener.expectMsgType[PaymentReceived] val paymentReceived = eventListener.expectMsgType[PaymentReceived]
@ -112,7 +113,7 @@ class MultiPartHandlerSpec extends TestKitBaseClass with FixtureAnyFunSuiteLike
assert(nodeParams.db.payments.getIncomingPayment(pr.paymentHash).get.status === IncomingPaymentStatus.Pending) assert(nodeParams.db.payments.getIncomingPayment(pr.paymentHash).get.status === IncomingPaymentStatus.Pending)
val add = UpdateAddHtlc(ByteVector32.One, 0, amountMsat, pr.paymentHash, defaultExpiry, TestConstants.emptyOnionPacket) val add = UpdateAddHtlc(ByteVector32.One, 0, amountMsat, pr.paymentHash, defaultExpiry, TestConstants.emptyOnionPacket)
sender.send(handlerWithMpp, IncomingPaymentPacket.FinalPacket(add, PaymentOnion.createSinglePartPayload(add.amountMsat, add.cltvExpiry, pr.paymentSecret.get))) sender.send(handlerWithMpp, IncomingPaymentPacket.FinalPacket(add, PaymentOnion.createSinglePartPayload(add.amountMsat, add.cltvExpiry, pr.paymentSecret.get, pr.paymentMetadata)))
register.expectMsgType[Register.Forward[CMD_FULFILL_HTLC]] register.expectMsgType[Register.Forward[CMD_FULFILL_HTLC]]
val paymentReceived = eventListener.expectMsgType[PaymentReceived] val paymentReceived = eventListener.expectMsgType[PaymentReceived]
@ -130,7 +131,7 @@ class MultiPartHandlerSpec extends TestKitBaseClass with FixtureAnyFunSuiteLike
assert(nodeParams.db.payments.getIncomingPayment(pr.paymentHash).get.status === IncomingPaymentStatus.Pending) assert(nodeParams.db.payments.getIncomingPayment(pr.paymentHash).get.status === IncomingPaymentStatus.Pending)
val add = UpdateAddHtlc(ByteVector32.One, 0, amountMsat, pr.paymentHash, CltvExpiryDelta(3).toCltvExpiry(nodeParams.currentBlockHeight), TestConstants.emptyOnionPacket) val add = UpdateAddHtlc(ByteVector32.One, 0, amountMsat, pr.paymentHash, CltvExpiryDelta(3).toCltvExpiry(nodeParams.currentBlockHeight), TestConstants.emptyOnionPacket)
sender.send(handlerWithMpp, IncomingPaymentPacket.FinalPacket(add, PaymentOnion.createSinglePartPayload(add.amountMsat, add.cltvExpiry, pr.paymentSecret.get))) sender.send(handlerWithMpp, IncomingPaymentPacket.FinalPacket(add, PaymentOnion.createSinglePartPayload(add.amountMsat, add.cltvExpiry, pr.paymentSecret.get, pr.paymentMetadata)))
val cmd = register.expectMsgType[Register.Forward[CMD_FAIL_HTLC]].message val cmd = register.expectMsgType[Register.Forward[CMD_FAIL_HTLC]].message
assert(cmd.reason == Right(IncorrectOrUnknownPaymentDetails(amountMsat, nodeParams.currentBlockHeight))) assert(cmd.reason == Right(IncorrectOrUnknownPaymentDetails(amountMsat, nodeParams.currentBlockHeight)))
assert(nodeParams.db.payments.getIncomingPayment(pr.paymentHash).get.status === IncomingPaymentStatus.Pending) assert(nodeParams.db.payments.getIncomingPayment(pr.paymentHash).get.status === IncomingPaymentStatus.Pending)
@ -191,7 +192,6 @@ class MultiPartHandlerSpec extends TestKitBaseClass with FixtureAnyFunSuiteLike
assert(!pr.features.allowMultiPart) assert(!pr.features.allowMultiPart)
assert(!pr.features.allowTrampoline) assert(!pr.features.allowTrampoline)
} }
{ {
val handler = TestActorRef[PaymentHandler](PaymentHandler.props(Alice.nodeParams.copy(enableTrampolinePayment = false, features = featuresWithMpp), TestProbe().ref)) val handler = TestActorRef[PaymentHandler](PaymentHandler.props(Alice.nodeParams.copy(enableTrampolinePayment = false, features = featuresWithMpp), TestProbe().ref))
sender.send(handler, ReceivePayment(Some(42 msat), Left("1 coffee"))) sender.send(handler, ReceivePayment(Some(42 msat), Left("1 coffee")))
@ -199,7 +199,6 @@ class MultiPartHandlerSpec extends TestKitBaseClass with FixtureAnyFunSuiteLike
assert(pr.features.allowMultiPart) assert(pr.features.allowMultiPart)
assert(!pr.features.allowTrampoline) assert(!pr.features.allowTrampoline)
} }
{ {
val handler = TestActorRef[PaymentHandler](PaymentHandler.props(Alice.nodeParams.copy(enableTrampolinePayment = true, features = featuresWithoutMpp), TestProbe().ref)) val handler = TestActorRef[PaymentHandler](PaymentHandler.props(Alice.nodeParams.copy(enableTrampolinePayment = true, features = featuresWithoutMpp), TestProbe().ref))
sender.send(handler, ReceivePayment(Some(42 msat), Left("1 coffee"))) sender.send(handler, ReceivePayment(Some(42 msat), Left("1 coffee")))
@ -207,7 +206,6 @@ class MultiPartHandlerSpec extends TestKitBaseClass with FixtureAnyFunSuiteLike
assert(!pr.features.allowMultiPart) assert(!pr.features.allowMultiPart)
assert(pr.features.allowTrampoline) assert(pr.features.allowTrampoline)
} }
{ {
val handler = TestActorRef[PaymentHandler](PaymentHandler.props(Alice.nodeParams.copy(enableTrampolinePayment = true, features = featuresWithMpp), TestProbe().ref)) val handler = TestActorRef[PaymentHandler](PaymentHandler.props(Alice.nodeParams.copy(enableTrampolinePayment = true, features = featuresWithMpp), TestProbe().ref))
sender.send(handler, ReceivePayment(Some(42 msat), Left("1 coffee"))) sender.send(handler, ReceivePayment(Some(42 msat), Left("1 coffee")))
@ -244,7 +242,7 @@ class MultiPartHandlerSpec extends TestKitBaseClass with FixtureAnyFunSuiteLike
assert(pr.isExpired) assert(pr.isExpired)
val add = UpdateAddHtlc(ByteVector32.One, 0, 1000 msat, pr.paymentHash, defaultExpiry, TestConstants.emptyOnionPacket) val add = UpdateAddHtlc(ByteVector32.One, 0, 1000 msat, pr.paymentHash, defaultExpiry, TestConstants.emptyOnionPacket)
sender.send(handlerWithoutMpp, IncomingPaymentPacket.FinalPacket(add, PaymentOnion.createSinglePartPayload(add.amountMsat, add.cltvExpiry, pr.paymentSecret.get))) sender.send(handlerWithoutMpp, IncomingPaymentPacket.FinalPacket(add, PaymentOnion.createSinglePartPayload(add.amountMsat, add.cltvExpiry, pr.paymentSecret.get, pr.paymentMetadata)))
register.expectMsgType[Register.Forward[CMD_FAIL_HTLC]] register.expectMsgType[Register.Forward[CMD_FAIL_HTLC]]
val Some(incoming) = nodeParams.db.payments.getIncomingPayment(pr.paymentHash) val Some(incoming) = nodeParams.db.payments.getIncomingPayment(pr.paymentHash)
assert(incoming.paymentRequest.isExpired && incoming.status === IncomingPaymentStatus.Expired) assert(incoming.paymentRequest.isExpired && incoming.status === IncomingPaymentStatus.Expired)
@ -259,7 +257,7 @@ class MultiPartHandlerSpec extends TestKitBaseClass with FixtureAnyFunSuiteLike
assert(pr.isExpired) assert(pr.isExpired)
val add = UpdateAddHtlc(ByteVector32.One, 0, 800 msat, pr.paymentHash, defaultExpiry, TestConstants.emptyOnionPacket) val add = UpdateAddHtlc(ByteVector32.One, 0, 800 msat, pr.paymentHash, defaultExpiry, TestConstants.emptyOnionPacket)
sender.send(handlerWithMpp, IncomingPaymentPacket.FinalPacket(add, PaymentOnion.createMultiPartPayload(add.amountMsat, 1000 msat, add.cltvExpiry, pr.paymentSecret.get))) sender.send(handlerWithMpp, IncomingPaymentPacket.FinalPacket(add, PaymentOnion.createMultiPartPayload(add.amountMsat, 1000 msat, add.cltvExpiry, pr.paymentSecret.get, pr.paymentMetadata)))
val cmd = register.expectMsgType[Register.Forward[CMD_FAIL_HTLC]].message val cmd = register.expectMsgType[Register.Forward[CMD_FAIL_HTLC]].message
assert(cmd.reason == Right(IncorrectOrUnknownPaymentDetails(1000 msat, nodeParams.currentBlockHeight))) assert(cmd.reason == Right(IncorrectOrUnknownPaymentDetails(1000 msat, nodeParams.currentBlockHeight)))
val Some(incoming) = nodeParams.db.payments.getIncomingPayment(pr.paymentHash) val Some(incoming) = nodeParams.db.payments.getIncomingPayment(pr.paymentHash)
@ -274,7 +272,7 @@ class MultiPartHandlerSpec extends TestKitBaseClass with FixtureAnyFunSuiteLike
assert(!pr.features.allowMultiPart) assert(!pr.features.allowMultiPart)
val add = UpdateAddHtlc(ByteVector32.One, 0, 800 msat, pr.paymentHash, defaultExpiry, TestConstants.emptyOnionPacket) val add = UpdateAddHtlc(ByteVector32.One, 0, 800 msat, pr.paymentHash, defaultExpiry, TestConstants.emptyOnionPacket)
sender.send(handlerWithoutMpp, IncomingPaymentPacket.FinalPacket(add, PaymentOnion.createMultiPartPayload(add.amountMsat, 1000 msat, add.cltvExpiry, pr.paymentSecret.get))) sender.send(handlerWithoutMpp, IncomingPaymentPacket.FinalPacket(add, PaymentOnion.createMultiPartPayload(add.amountMsat, 1000 msat, add.cltvExpiry, pr.paymentSecret.get, pr.paymentMetadata)))
val cmd = register.expectMsgType[Register.Forward[CMD_FAIL_HTLC]].message val cmd = register.expectMsgType[Register.Forward[CMD_FAIL_HTLC]].message
assert(cmd.reason == Right(IncorrectOrUnknownPaymentDetails(1000 msat, nodeParams.currentBlockHeight))) assert(cmd.reason == Right(IncorrectOrUnknownPaymentDetails(1000 msat, nodeParams.currentBlockHeight)))
assert(nodeParams.db.payments.getIncomingPayment(pr.paymentHash).get.status === IncomingPaymentStatus.Pending) assert(nodeParams.db.payments.getIncomingPayment(pr.paymentHash).get.status === IncomingPaymentStatus.Pending)
@ -289,7 +287,7 @@ class MultiPartHandlerSpec extends TestKitBaseClass with FixtureAnyFunSuiteLike
val lowCltvExpiry = nodeParams.fulfillSafetyBeforeTimeout.toCltvExpiry(nodeParams.currentBlockHeight) val lowCltvExpiry = nodeParams.fulfillSafetyBeforeTimeout.toCltvExpiry(nodeParams.currentBlockHeight)
val add = UpdateAddHtlc(ByteVector32.One, 0, 800 msat, pr.paymentHash, lowCltvExpiry, TestConstants.emptyOnionPacket) val add = UpdateAddHtlc(ByteVector32.One, 0, 800 msat, pr.paymentHash, lowCltvExpiry, TestConstants.emptyOnionPacket)
sender.send(handlerWithMpp, IncomingPaymentPacket.FinalPacket(add, PaymentOnion.createMultiPartPayload(add.amountMsat, 1000 msat, add.cltvExpiry, pr.paymentSecret.get))) sender.send(handlerWithMpp, IncomingPaymentPacket.FinalPacket(add, PaymentOnion.createMultiPartPayload(add.amountMsat, 1000 msat, add.cltvExpiry, pr.paymentSecret.get, pr.paymentMetadata)))
val cmd = register.expectMsgType[Register.Forward[CMD_FAIL_HTLC]].message val cmd = register.expectMsgType[Register.Forward[CMD_FAIL_HTLC]].message
assert(cmd.reason == Right(IncorrectOrUnknownPaymentDetails(1000 msat, nodeParams.currentBlockHeight))) assert(cmd.reason == Right(IncorrectOrUnknownPaymentDetails(1000 msat, nodeParams.currentBlockHeight)))
assert(nodeParams.db.payments.getIncomingPayment(pr.paymentHash).get.status === IncomingPaymentStatus.Pending) assert(nodeParams.db.payments.getIncomingPayment(pr.paymentHash).get.status === IncomingPaymentStatus.Pending)
@ -303,7 +301,7 @@ class MultiPartHandlerSpec extends TestKitBaseClass with FixtureAnyFunSuiteLike
assert(pr.features.allowMultiPart) assert(pr.features.allowMultiPart)
val add = UpdateAddHtlc(ByteVector32.One, 0, 800 msat, pr.paymentHash.reverse, defaultExpiry, TestConstants.emptyOnionPacket) val add = UpdateAddHtlc(ByteVector32.One, 0, 800 msat, pr.paymentHash.reverse, defaultExpiry, TestConstants.emptyOnionPacket)
sender.send(handlerWithMpp, IncomingPaymentPacket.FinalPacket(add, PaymentOnion.createMultiPartPayload(add.amountMsat, 1000 msat, add.cltvExpiry, pr.paymentSecret.get))) sender.send(handlerWithMpp, IncomingPaymentPacket.FinalPacket(add, PaymentOnion.createMultiPartPayload(add.amountMsat, 1000 msat, add.cltvExpiry, pr.paymentSecret.get, pr.paymentMetadata)))
val cmd = register.expectMsgType[Register.Forward[CMD_FAIL_HTLC]].message val cmd = register.expectMsgType[Register.Forward[CMD_FAIL_HTLC]].message
assert(cmd.reason == Right(IncorrectOrUnknownPaymentDetails(1000 msat, nodeParams.currentBlockHeight))) assert(cmd.reason == Right(IncorrectOrUnknownPaymentDetails(1000 msat, nodeParams.currentBlockHeight)))
assert(nodeParams.db.payments.getIncomingPayment(pr.paymentHash).get.status === IncomingPaymentStatus.Pending) assert(nodeParams.db.payments.getIncomingPayment(pr.paymentHash).get.status === IncomingPaymentStatus.Pending)
@ -317,7 +315,7 @@ class MultiPartHandlerSpec extends TestKitBaseClass with FixtureAnyFunSuiteLike
assert(pr.features.allowMultiPart) assert(pr.features.allowMultiPart)
val add = UpdateAddHtlc(ByteVector32.One, 0, 800 msat, pr.paymentHash, defaultExpiry, TestConstants.emptyOnionPacket) val add = UpdateAddHtlc(ByteVector32.One, 0, 800 msat, pr.paymentHash, defaultExpiry, TestConstants.emptyOnionPacket)
sender.send(handlerWithMpp, IncomingPaymentPacket.FinalPacket(add, PaymentOnion.createMultiPartPayload(add.amountMsat, 999 msat, add.cltvExpiry, pr.paymentSecret.get))) sender.send(handlerWithMpp, IncomingPaymentPacket.FinalPacket(add, PaymentOnion.createMultiPartPayload(add.amountMsat, 999 msat, add.cltvExpiry, pr.paymentSecret.get, pr.paymentMetadata)))
val cmd = register.expectMsgType[Register.Forward[CMD_FAIL_HTLC]].message val cmd = register.expectMsgType[Register.Forward[CMD_FAIL_HTLC]].message
assert(cmd.reason == Right(IncorrectOrUnknownPaymentDetails(999 msat, nodeParams.currentBlockHeight))) assert(cmd.reason == Right(IncorrectOrUnknownPaymentDetails(999 msat, nodeParams.currentBlockHeight)))
assert(nodeParams.db.payments.getIncomingPayment(pr.paymentHash).get.status === IncomingPaymentStatus.Pending) assert(nodeParams.db.payments.getIncomingPayment(pr.paymentHash).get.status === IncomingPaymentStatus.Pending)
@ -331,7 +329,7 @@ class MultiPartHandlerSpec extends TestKitBaseClass with FixtureAnyFunSuiteLike
assert(pr.features.allowMultiPart) assert(pr.features.allowMultiPart)
val add = UpdateAddHtlc(ByteVector32.One, 0, 800 msat, pr.paymentHash, defaultExpiry, TestConstants.emptyOnionPacket) val add = UpdateAddHtlc(ByteVector32.One, 0, 800 msat, pr.paymentHash, defaultExpiry, TestConstants.emptyOnionPacket)
sender.send(handlerWithMpp, IncomingPaymentPacket.FinalPacket(add, PaymentOnion.createMultiPartPayload(add.amountMsat, 2001 msat, add.cltvExpiry, pr.paymentSecret.get))) sender.send(handlerWithMpp, IncomingPaymentPacket.FinalPacket(add, PaymentOnion.createMultiPartPayload(add.amountMsat, 2001 msat, add.cltvExpiry, pr.paymentSecret.get, pr.paymentMetadata)))
val cmd = register.expectMsgType[Register.Forward[CMD_FAIL_HTLC]].message val cmd = register.expectMsgType[Register.Forward[CMD_FAIL_HTLC]].message
assert(cmd.reason == Right(IncorrectOrUnknownPaymentDetails(2001 msat, nodeParams.currentBlockHeight))) assert(cmd.reason == Right(IncorrectOrUnknownPaymentDetails(2001 msat, nodeParams.currentBlockHeight)))
assert(nodeParams.db.payments.getIncomingPayment(pr.paymentHash).get.status === IncomingPaymentStatus.Pending) assert(nodeParams.db.payments.getIncomingPayment(pr.paymentHash).get.status === IncomingPaymentStatus.Pending)
@ -346,7 +344,7 @@ class MultiPartHandlerSpec extends TestKitBaseClass with FixtureAnyFunSuiteLike
// Invalid payment secret. // Invalid payment secret.
val add = UpdateAddHtlc(ByteVector32.One, 0, 800 msat, pr.paymentHash, defaultExpiry, TestConstants.emptyOnionPacket) val add = UpdateAddHtlc(ByteVector32.One, 0, 800 msat, pr.paymentHash, defaultExpiry, TestConstants.emptyOnionPacket)
sender.send(handlerWithMpp, IncomingPaymentPacket.FinalPacket(add, PaymentOnion.createMultiPartPayload(add.amountMsat, 1000 msat, add.cltvExpiry, pr.paymentSecret.get.reverse))) sender.send(handlerWithMpp, IncomingPaymentPacket.FinalPacket(add, PaymentOnion.createMultiPartPayload(add.amountMsat, 1000 msat, add.cltvExpiry, pr.paymentSecret.get.reverse, pr.paymentMetadata)))
val cmd = register.expectMsgType[Register.Forward[CMD_FAIL_HTLC]].message val cmd = register.expectMsgType[Register.Forward[CMD_FAIL_HTLC]].message
assert(cmd.reason == Right(IncorrectOrUnknownPaymentDetails(1000 msat, nodeParams.currentBlockHeight))) assert(cmd.reason == Right(IncorrectOrUnknownPaymentDetails(1000 msat, nodeParams.currentBlockHeight)))
assert(nodeParams.db.payments.getIncomingPayment(pr.paymentHash).get.status === IncomingPaymentStatus.Pending) assert(nodeParams.db.payments.getIncomingPayment(pr.paymentHash).get.status === IncomingPaymentStatus.Pending)
@ -360,13 +358,13 @@ class MultiPartHandlerSpec extends TestKitBaseClass with FixtureAnyFunSuiteLike
f.sender.send(handler, ReceivePayment(Some(1000 msat), Left("1 slow coffee"))) f.sender.send(handler, ReceivePayment(Some(1000 msat), Left("1 slow coffee")))
val pr1 = f.sender.expectMsgType[PaymentRequest] val pr1 = f.sender.expectMsgType[PaymentRequest]
val add1 = UpdateAddHtlc(ByteVector32.One, 0, 800 msat, pr1.paymentHash, f.defaultExpiry, TestConstants.emptyOnionPacket) val add1 = UpdateAddHtlc(ByteVector32.One, 0, 800 msat, pr1.paymentHash, f.defaultExpiry, TestConstants.emptyOnionPacket)
f.sender.send(handler, IncomingPaymentPacket.FinalPacket(add1, PaymentOnion.createMultiPartPayload(add1.amountMsat, 1000 msat, add1.cltvExpiry, pr1.paymentSecret.get))) f.sender.send(handler, IncomingPaymentPacket.FinalPacket(add1, PaymentOnion.createMultiPartPayload(add1.amountMsat, 1000 msat, add1.cltvExpiry, pr1.paymentSecret.get, pr1.paymentMetadata)))
// Partial payment exceeding the invoice amount, but incomplete because it promises to overpay. // Partial payment exceeding the invoice amount, but incomplete because it promises to overpay.
f.sender.send(handler, ReceivePayment(Some(1500 msat), Left("1 slow latte"))) f.sender.send(handler, ReceivePayment(Some(1500 msat), Left("1 slow latte")))
val pr2 = f.sender.expectMsgType[PaymentRequest] val pr2 = f.sender.expectMsgType[PaymentRequest]
val add2 = UpdateAddHtlc(ByteVector32.One, 1, 1600 msat, pr2.paymentHash, f.defaultExpiry, TestConstants.emptyOnionPacket) val add2 = UpdateAddHtlc(ByteVector32.One, 1, 1600 msat, pr2.paymentHash, f.defaultExpiry, TestConstants.emptyOnionPacket)
f.sender.send(handler, IncomingPaymentPacket.FinalPacket(add2, PaymentOnion.createMultiPartPayload(add2.amountMsat, 2000 msat, add2.cltvExpiry, pr2.paymentSecret.get))) f.sender.send(handler, IncomingPaymentPacket.FinalPacket(add2, PaymentOnion.createMultiPartPayload(add2.amountMsat, 2000 msat, add2.cltvExpiry, pr2.paymentSecret.get, pr2.paymentMetadata)))
awaitCond { awaitCond {
f.sender.send(handler, GetPendingPayments) f.sender.send(handler, GetPendingPayments)
@ -401,12 +399,12 @@ class MultiPartHandlerSpec extends TestKitBaseClass with FixtureAnyFunSuiteLike
val pr = f.sender.expectMsgType[PaymentRequest] val pr = f.sender.expectMsgType[PaymentRequest]
val add1 = UpdateAddHtlc(ByteVector32.One, 0, 800 msat, pr.paymentHash, f.defaultExpiry, TestConstants.emptyOnionPacket) val add1 = UpdateAddHtlc(ByteVector32.One, 0, 800 msat, pr.paymentHash, f.defaultExpiry, TestConstants.emptyOnionPacket)
f.sender.send(handler, IncomingPaymentPacket.FinalPacket(add1, PaymentOnion.createMultiPartPayload(add1.amountMsat, 1000 msat, add1.cltvExpiry, pr.paymentSecret.get))) f.sender.send(handler, IncomingPaymentPacket.FinalPacket(add1, PaymentOnion.createMultiPartPayload(add1.amountMsat, 1000 msat, add1.cltvExpiry, pr.paymentSecret.get, pr.paymentMetadata)))
// Invalid payment secret -> should be rejected. // Invalid payment secret -> should be rejected.
val add2 = UpdateAddHtlc(ByteVector32.Zeroes, 42, 200 msat, pr.paymentHash, f.defaultExpiry, TestConstants.emptyOnionPacket) val add2 = UpdateAddHtlc(ByteVector32.Zeroes, 42, 200 msat, pr.paymentHash, f.defaultExpiry, TestConstants.emptyOnionPacket)
f.sender.send(handler, IncomingPaymentPacket.FinalPacket(add2, PaymentOnion.createMultiPartPayload(add2.amountMsat, 1000 msat, add2.cltvExpiry, pr.paymentSecret.get.reverse))) f.sender.send(handler, IncomingPaymentPacket.FinalPacket(add2, PaymentOnion.createMultiPartPayload(add2.amountMsat, 1000 msat, add2.cltvExpiry, pr.paymentSecret.get.reverse, pr.paymentMetadata)))
val add3 = add2.copy(id = 43) val add3 = add2.copy(id = 43)
f.sender.send(handler, IncomingPaymentPacket.FinalPacket(add3, PaymentOnion.createMultiPartPayload(add3.amountMsat, 1000 msat, add3.cltvExpiry, pr.paymentSecret.get))) f.sender.send(handler, IncomingPaymentPacket.FinalPacket(add3, PaymentOnion.createMultiPartPayload(add3.amountMsat, 1000 msat, add3.cltvExpiry, pr.paymentSecret.get, pr.paymentMetadata)))
f.register.expectMsgAllOf( f.register.expectMsgAllOf(
Register.Forward(ActorRef.noSender, add2.channelId, CMD_FAIL_HTLC(add2.id, Right(IncorrectOrUnknownPaymentDetails(1000 msat, nodeParams.currentBlockHeight)), commit = true)), Register.Forward(ActorRef.noSender, add2.channelId, CMD_FAIL_HTLC(add2.id, Right(IncorrectOrUnknownPaymentDetails(1000 msat, nodeParams.currentBlockHeight)), commit = true)),
@ -446,7 +444,7 @@ class MultiPartHandlerSpec extends TestKitBaseClass with FixtureAnyFunSuiteLike
assert(pr.paymentHash == Crypto.sha256(preimage)) assert(pr.paymentHash == Crypto.sha256(preimage))
val add1 = UpdateAddHtlc(ByteVector32.One, 0, 800 msat, pr.paymentHash, f.defaultExpiry, TestConstants.emptyOnionPacket) val add1 = UpdateAddHtlc(ByteVector32.One, 0, 800 msat, pr.paymentHash, f.defaultExpiry, TestConstants.emptyOnionPacket)
f.sender.send(handler, IncomingPaymentPacket.FinalPacket(add1, PaymentOnion.createMultiPartPayload(add1.amountMsat, 1000 msat, add1.cltvExpiry, pr.paymentSecret.get))) f.sender.send(handler, IncomingPaymentPacket.FinalPacket(add1, PaymentOnion.createMultiPartPayload(add1.amountMsat, 1000 msat, add1.cltvExpiry, pr.paymentSecret.get, pr.paymentMetadata)))
f.register.expectMsg(Register.Forward(ActorRef.noSender, ByteVector32.One, CMD_FAIL_HTLC(0, Right(PaymentTimeout), commit = true))) f.register.expectMsg(Register.Forward(ActorRef.noSender, ByteVector32.One, CMD_FAIL_HTLC(0, Right(PaymentTimeout), commit = true)))
awaitCond({ awaitCond({
f.sender.send(handler, GetPendingPayments) f.sender.send(handler, GetPendingPayments)
@ -454,9 +452,9 @@ class MultiPartHandlerSpec extends TestKitBaseClass with FixtureAnyFunSuiteLike
}) })
val add2 = UpdateAddHtlc(ByteVector32.One, 2, 300 msat, pr.paymentHash, f.defaultExpiry, TestConstants.emptyOnionPacket) val add2 = UpdateAddHtlc(ByteVector32.One, 2, 300 msat, pr.paymentHash, f.defaultExpiry, TestConstants.emptyOnionPacket)
f.sender.send(handler, IncomingPaymentPacket.FinalPacket(add2, PaymentOnion.createMultiPartPayload(add2.amountMsat, 1000 msat, add2.cltvExpiry, pr.paymentSecret.get))) f.sender.send(handler, IncomingPaymentPacket.FinalPacket(add2, PaymentOnion.createMultiPartPayload(add2.amountMsat, 1000 msat, add2.cltvExpiry, pr.paymentSecret.get, pr.paymentMetadata)))
val add3 = UpdateAddHtlc(ByteVector32.Zeroes, 5, 700 msat, pr.paymentHash, f.defaultExpiry, TestConstants.emptyOnionPacket) val add3 = UpdateAddHtlc(ByteVector32.Zeroes, 5, 700 msat, pr.paymentHash, f.defaultExpiry, TestConstants.emptyOnionPacket)
f.sender.send(handler, IncomingPaymentPacket.FinalPacket(add3, PaymentOnion.createMultiPartPayload(add3.amountMsat, 1000 msat, add3.cltvExpiry, pr.paymentSecret.get))) f.sender.send(handler, IncomingPaymentPacket.FinalPacket(add3, PaymentOnion.createMultiPartPayload(add3.amountMsat, 1000 msat, add3.cltvExpiry, pr.paymentSecret.get, pr.paymentMetadata)))
// the fulfill are not necessarily in the same order as the commands // the fulfill are not necessarily in the same order as the commands
f.register.expectMsgAllOf( f.register.expectMsgAllOf(
@ -524,7 +522,7 @@ class MultiPartHandlerSpec extends TestKitBaseClass with FixtureAnyFunSuiteLike
assert(nodeParams.db.payments.getIncomingPayment(paymentHash) === None) assert(nodeParams.db.payments.getIncomingPayment(paymentHash) === None)
val add = UpdateAddHtlc(ByteVector32.One, 0, 1000 msat, paymentHash, defaultExpiry, TestConstants.emptyOnionPacket) val add = UpdateAddHtlc(ByteVector32.One, 0, 1000 msat, paymentHash, defaultExpiry, TestConstants.emptyOnionPacket)
sender.send(handlerWithoutMpp, IncomingPaymentPacket.FinalPacket(add, PaymentOnion.createSinglePartPayload(add.amountMsat, add.cltvExpiry, paymentSecret))) sender.send(handlerWithoutMpp, IncomingPaymentPacket.FinalPacket(add, PaymentOnion.createSinglePartPayload(add.amountMsat, add.cltvExpiry, paymentSecret, None)))
val cmd = register.expectMsgType[Register.Forward[CMD_FAIL_HTLC]].message val cmd = register.expectMsgType[Register.Forward[CMD_FAIL_HTLC]].message
assert(cmd.id === add.id) assert(cmd.id === add.id)
assert(cmd.reason === Right(IncorrectOrUnknownPaymentDetails(1000 msat, nodeParams.currentBlockHeight))) assert(cmd.reason === Right(IncorrectOrUnknownPaymentDetails(1000 msat, nodeParams.currentBlockHeight)))
@ -538,7 +536,7 @@ class MultiPartHandlerSpec extends TestKitBaseClass with FixtureAnyFunSuiteLike
assert(nodeParams.db.payments.getIncomingPayment(paymentHash) === None) assert(nodeParams.db.payments.getIncomingPayment(paymentHash) === None)
val add = UpdateAddHtlc(ByteVector32.One, 0, 800 msat, paymentHash, defaultExpiry, TestConstants.emptyOnionPacket) val add = UpdateAddHtlc(ByteVector32.One, 0, 800 msat, paymentHash, defaultExpiry, TestConstants.emptyOnionPacket)
sender.send(handlerWithMpp, IncomingPaymentPacket.FinalPacket(add, PaymentOnion.createMultiPartPayload(add.amountMsat, 1000 msat, add.cltvExpiry, paymentSecret))) sender.send(handlerWithMpp, IncomingPaymentPacket.FinalPacket(add, PaymentOnion.createMultiPartPayload(add.amountMsat, 1000 msat, add.cltvExpiry, paymentSecret, Some(hex"012345"))))
val cmd = register.expectMsgType[Register.Forward[CMD_FAIL_HTLC]].message val cmd = register.expectMsgType[Register.Forward[CMD_FAIL_HTLC]].message
assert(cmd.id === add.id) assert(cmd.id === add.id)
assert(cmd.reason === Right(IncorrectOrUnknownPaymentDetails(1000 msat, nodeParams.currentBlockHeight))) assert(cmd.reason === Right(IncorrectOrUnknownPaymentDetails(1000 msat, nodeParams.currentBlockHeight)))

View file

@ -78,7 +78,7 @@ class MultiPartPaymentLifecycleSpec extends TestKitBaseClass with FixtureAnyFunS
import f._ import f._
assert(payFsm.stateName === WAIT_FOR_PAYMENT_REQUEST) assert(payFsm.stateName === WAIT_FOR_PAYMENT_REQUEST)
val payment = SendMultiPartPayment(sender.ref, randomBytes32(), e, finalAmount, expiry, 1, routeParams = routeParams.copy(randomize = true)) val payment = SendMultiPartPayment(sender.ref, randomBytes32(), e, finalAmount, expiry, 1, None, routeParams = routeParams.copy(randomize = true))
sender.send(payFsm, payment) sender.send(payFsm, payment)
router.expectMsg(RouteRequest(nodeParams.nodeId, e, finalAmount, maxFee, routeParams = routeParams.copy(randomize = false), allowMultiPart = true, paymentContext = Some(cfg.paymentContext))) router.expectMsg(RouteRequest(nodeParams.nodeId, e, finalAmount, maxFee, routeParams = routeParams.copy(randomize = false), allowMultiPart = true, paymentContext = Some(cfg.paymentContext)))
@ -111,7 +111,7 @@ class MultiPartPaymentLifecycleSpec extends TestKitBaseClass with FixtureAnyFunS
import f._ import f._
assert(payFsm.stateName === WAIT_FOR_PAYMENT_REQUEST) assert(payFsm.stateName === WAIT_FOR_PAYMENT_REQUEST)
val payment = SendMultiPartPayment(sender.ref, randomBytes32(), e, 1200000 msat, expiry, 1, routeParams = routeParams.copy(randomize = false)) val payment = SendMultiPartPayment(sender.ref, randomBytes32(), e, 1200000 msat, expiry, 1, Some(hex"012345"), routeParams = routeParams.copy(randomize = false))
sender.send(payFsm, payment) sender.send(payFsm, payment)
router.expectMsg(RouteRequest(nodeParams.nodeId, e, 1200000 msat, maxFee, routeParams = routeParams.copy(randomize = false), allowMultiPart = true, paymentContext = Some(cfg.paymentContext))) router.expectMsg(RouteRequest(nodeParams.nodeId, e, 1200000 msat, maxFee, routeParams = routeParams.copy(randomize = false), allowMultiPart = true, paymentContext = Some(cfg.paymentContext)))
@ -126,6 +126,7 @@ class MultiPartPaymentLifecycleSpec extends TestKitBaseClass with FixtureAnyFunS
assert(childPayments.map(_.route).toSet === routes.map(r => Right(r)).toSet) assert(childPayments.map(_.route).toSet === routes.map(r => Right(r)).toSet)
assert(childPayments.map(_.finalPayload.expiry).toSet === Set(expiry)) assert(childPayments.map(_.finalPayload.expiry).toSet === Set(expiry))
assert(childPayments.map(_.finalPayload.paymentSecret).toSet === Set(payment.paymentSecret)) assert(childPayments.map(_.finalPayload.paymentSecret).toSet === Set(payment.paymentSecret))
assert(childPayments.map(_.finalPayload.paymentMetadata).toSet === Set(Some(hex"012345")))
assert(childPayments.map(_.finalPayload.amount).toSet === Set(500000 msat, 700000 msat)) assert(childPayments.map(_.finalPayload.amount).toSet === Set(500000 msat, 700000 msat))
assert(childPayments.map(_.finalPayload.totalAmount).toSet === Set(1200000 msat)) assert(childPayments.map(_.finalPayload.totalAmount).toSet === Set(1200000 msat))
assert(payFsm.stateName === PAYMENT_IN_PROGRESS) assert(payFsm.stateName === PAYMENT_IN_PROGRESS)
@ -149,7 +150,7 @@ class MultiPartPaymentLifecycleSpec extends TestKitBaseClass with FixtureAnyFunS
// We include a bunch of additional tlv records. // We include a bunch of additional tlv records.
val trampolineTlv = OnionPaymentPayloadTlv.TrampolineOnion(OnionRoutingPacket(0, ByteVector.fill(33)(0), ByteVector.fill(400)(0), randomBytes32())) val trampolineTlv = OnionPaymentPayloadTlv.TrampolineOnion(OnionRoutingPacket(0, ByteVector.fill(33)(0), ByteVector.fill(400)(0), randomBytes32()))
val userCustomTlv = GenericTlv(UInt64(561), hex"deadbeef") val userCustomTlv = GenericTlv(UInt64(561), hex"deadbeef")
val payment = SendMultiPartPayment(sender.ref, randomBytes32(), e, finalAmount + 1000.msat, expiry, 1, routeParams = routeParams, additionalTlvs = Seq(trampolineTlv), userCustomTlvs = Seq(userCustomTlv)) val payment = SendMultiPartPayment(sender.ref, randomBytes32(), e, finalAmount + 1000.msat, expiry, 1, None, routeParams = routeParams, additionalTlvs = Seq(trampolineTlv), userCustomTlvs = Seq(userCustomTlv))
sender.send(payFsm, payment) sender.send(payFsm, payment)
router.expectMsgType[RouteRequest] router.expectMsgType[RouteRequest]
router.send(payFsm, RouteResponse(Seq(Route(500000 msat, hop_ab_1 :: hop_be :: Nil), Route(501000 msat, hop_ac_1 :: hop_ce :: Nil)))) router.send(payFsm, RouteResponse(Seq(Route(500000 msat, hop_ab_1 :: hop_be :: Nil), Route(501000 msat, hop_ac_1 :: hop_ce :: Nil))))
@ -173,7 +174,7 @@ class MultiPartPaymentLifecycleSpec extends TestKitBaseClass with FixtureAnyFunS
test("successful retry") { f => test("successful retry") { f =>
import f._ import f._
val payment = SendMultiPartPayment(sender.ref, randomBytes32(), e, finalAmount, expiry, 3, routeParams = routeParams) val payment = SendMultiPartPayment(sender.ref, randomBytes32(), e, finalAmount, expiry, 3, None, routeParams = routeParams)
sender.send(payFsm, payment) sender.send(payFsm, payment)
router.expectMsgType[RouteRequest] router.expectMsgType[RouteRequest]
val failingRoute = Route(finalAmount, hop_ab_1 :: hop_be :: Nil) val failingRoute = Route(finalAmount, hop_ab_1 :: hop_be :: Nil)
@ -206,7 +207,7 @@ class MultiPartPaymentLifecycleSpec extends TestKitBaseClass with FixtureAnyFunS
test("retry failures while waiting for routes") { f => test("retry failures while waiting for routes") { f =>
import f._ import f._
val payment = SendMultiPartPayment(sender.ref, randomBytes32(), e, finalAmount, expiry, 3, routeParams = routeParams) val payment = SendMultiPartPayment(sender.ref, randomBytes32(), e, finalAmount, expiry, 3, None, routeParams = routeParams)
sender.send(payFsm, payment) sender.send(payFsm, payment)
router.expectMsgType[RouteRequest] router.expectMsgType[RouteRequest]
router.send(payFsm, RouteResponse(Seq(Route(400000 msat, hop_ab_1 :: hop_be :: Nil), Route(600000 msat, hop_ab_2 :: hop_be :: Nil)))) router.send(payFsm, RouteResponse(Seq(Route(400000 msat, hop_ab_1 :: hop_be :: Nil), Route(600000 msat, hop_ab_2 :: hop_be :: Nil))))
@ -248,7 +249,7 @@ class MultiPartPaymentLifecycleSpec extends TestKitBaseClass with FixtureAnyFunS
test("retry local channel failures") { f => test("retry local channel failures") { f =>
import f._ import f._
val payment = SendMultiPartPayment(sender.ref, randomBytes32(), e, finalAmount, expiry, 3, routeParams = routeParams) val payment = SendMultiPartPayment(sender.ref, randomBytes32(), e, finalAmount, expiry, 3, None, routeParams = routeParams)
sender.send(payFsm, payment) sender.send(payFsm, payment)
router.expectMsgType[RouteRequest] router.expectMsgType[RouteRequest]
router.send(payFsm, RouteResponse(Seq(Route(finalAmount, hop_ab_1 :: hop_be :: Nil)))) router.send(payFsm, RouteResponse(Seq(Route(finalAmount, hop_ab_1 :: hop_be :: Nil))))
@ -273,7 +274,7 @@ class MultiPartPaymentLifecycleSpec extends TestKitBaseClass with FixtureAnyFunS
test("retry without ignoring channels") { f => test("retry without ignoring channels") { f =>
import f._ import f._
val payment = SendMultiPartPayment(sender.ref, randomBytes32(), e, finalAmount, expiry, 3, routeParams = routeParams) val payment = SendMultiPartPayment(sender.ref, randomBytes32(), e, finalAmount, expiry, 3, None, routeParams = routeParams)
sender.send(payFsm, payment) sender.send(payFsm, payment)
router.expectMsgType[RouteRequest] router.expectMsgType[RouteRequest]
router.send(payFsm, RouteResponse(Seq(Route(500000 msat, hop_ab_1 :: hop_be :: Nil), Route(500000 msat, hop_ab_1 :: hop_be :: Nil)))) router.send(payFsm, RouteResponse(Seq(Route(500000 msat, hop_ab_1 :: hop_be :: Nil), Route(500000 msat, hop_ab_1 :: hop_be :: Nil))))
@ -317,7 +318,7 @@ class MultiPartPaymentLifecycleSpec extends TestKitBaseClass with FixtureAnyFunS
// The B -> E channel is private and provided in the invoice routing hints. // The B -> E channel is private and provided in the invoice routing hints.
val routingHint = ExtraHop(b, hop_be.lastUpdate.shortChannelId, hop_be.lastUpdate.feeBaseMsat, hop_be.lastUpdate.feeProportionalMillionths, hop_be.lastUpdate.cltvExpiryDelta) val routingHint = ExtraHop(b, hop_be.lastUpdate.shortChannelId, hop_be.lastUpdate.feeBaseMsat, hop_be.lastUpdate.feeProportionalMillionths, hop_be.lastUpdate.cltvExpiryDelta)
val payment = SendMultiPartPayment(sender.ref, randomBytes32(), e, finalAmount, expiry, 3, routeParams = routeParams, assistedRoutes = List(List(routingHint))) val payment = SendMultiPartPayment(sender.ref, randomBytes32(), e, finalAmount, expiry, 3, None, routeParams = routeParams, assistedRoutes = List(List(routingHint)))
sender.send(payFsm, payment) sender.send(payFsm, payment)
assert(router.expectMsgType[RouteRequest].assistedRoutes.head.head === routingHint) assert(router.expectMsgType[RouteRequest].assistedRoutes.head.head === routingHint)
val route = Route(finalAmount, hop_ab_1 :: hop_be :: Nil) val route = Route(finalAmount, hop_ab_1 :: hop_be :: Nil)
@ -338,7 +339,7 @@ class MultiPartPaymentLifecycleSpec extends TestKitBaseClass with FixtureAnyFunS
// The B -> E channel is private and provided in the invoice routing hints. // The B -> E channel is private and provided in the invoice routing hints.
val routingHint = ExtraHop(b, hop_be.lastUpdate.shortChannelId, hop_be.lastUpdate.feeBaseMsat, hop_be.lastUpdate.feeProportionalMillionths, hop_be.lastUpdate.cltvExpiryDelta) val routingHint = ExtraHop(b, hop_be.lastUpdate.shortChannelId, hop_be.lastUpdate.feeBaseMsat, hop_be.lastUpdate.feeProportionalMillionths, hop_be.lastUpdate.cltvExpiryDelta)
val payment = SendMultiPartPayment(sender.ref, randomBytes32(), e, finalAmount, expiry, 3, routeParams = routeParams, assistedRoutes = List(List(routingHint))) val payment = SendMultiPartPayment(sender.ref, randomBytes32(), e, finalAmount, expiry, 3, None, routeParams = routeParams, assistedRoutes = List(List(routingHint)))
sender.send(payFsm, payment) sender.send(payFsm, payment)
assert(router.expectMsgType[RouteRequest].assistedRoutes.head.head === routingHint) assert(router.expectMsgType[RouteRequest].assistedRoutes.head.head === routingHint)
val route = Route(finalAmount, hop_ab_1 :: hop_be :: Nil) val route = Route(finalAmount, hop_ab_1 :: hop_be :: Nil)
@ -397,7 +398,7 @@ class MultiPartPaymentLifecycleSpec extends TestKitBaseClass with FixtureAnyFunS
test("abort after too many failed attempts") { f => test("abort after too many failed attempts") { f =>
import f._ import f._
val payment = SendMultiPartPayment(sender.ref, randomBytes32(), e, finalAmount, expiry, 2, routeParams = routeParams) val payment = SendMultiPartPayment(sender.ref, randomBytes32(), e, finalAmount, expiry, 2, None, routeParams = routeParams)
sender.send(payFsm, payment) sender.send(payFsm, payment)
router.expectMsgType[RouteRequest] router.expectMsgType[RouteRequest]
router.send(payFsm, RouteResponse(Seq(Route(500000 msat, hop_ab_1 :: hop_be :: Nil), Route(500000 msat, hop_ac_1 :: hop_ce :: Nil)))) router.send(payFsm, RouteResponse(Seq(Route(500000 msat, hop_ab_1 :: hop_be :: Nil), Route(500000 msat, hop_ac_1 :: hop_ce :: Nil))))
@ -428,7 +429,7 @@ class MultiPartPaymentLifecycleSpec extends TestKitBaseClass with FixtureAnyFunS
import f._ import f._
sender.watch(payFsm) sender.watch(payFsm)
val payment = SendMultiPartPayment(sender.ref, randomBytes32(), e, finalAmount, expiry, 5, routeParams = routeParams) val payment = SendMultiPartPayment(sender.ref, randomBytes32(), e, finalAmount, expiry, 5, None, routeParams = routeParams)
sender.send(payFsm, payment) sender.send(payFsm, payment)
router.expectMsgType[RouteRequest] router.expectMsgType[RouteRequest]
router.send(payFsm, Status.Failure(RouteNotFound)) router.send(payFsm, Status.Failure(RouteNotFound))
@ -458,7 +459,7 @@ class MultiPartPaymentLifecycleSpec extends TestKitBaseClass with FixtureAnyFunS
test("abort if recipient sends error") { f => test("abort if recipient sends error") { f =>
import f._ import f._
val payment = SendMultiPartPayment(sender.ref, randomBytes32(), e, finalAmount, expiry, 5, routeParams = routeParams) val payment = SendMultiPartPayment(sender.ref, randomBytes32(), e, finalAmount, expiry, 5, None, routeParams = routeParams)
sender.send(payFsm, payment) sender.send(payFsm, payment)
router.expectMsgType[RouteRequest] router.expectMsgType[RouteRequest]
router.send(payFsm, RouteResponse(Seq(Route(finalAmount, hop_ab_1 :: hop_be :: Nil)))) router.send(payFsm, RouteResponse(Seq(Route(finalAmount, hop_ab_1 :: hop_be :: Nil))))
@ -479,7 +480,7 @@ class MultiPartPaymentLifecycleSpec extends TestKitBaseClass with FixtureAnyFunS
test("abort if payment gets settled on chain") { f => test("abort if payment gets settled on chain") { f =>
import f._ import f._
val payment = SendMultiPartPayment(sender.ref, randomBytes32(), e, finalAmount, expiry, 5, routeParams = routeParams) val payment = SendMultiPartPayment(sender.ref, randomBytes32(), e, finalAmount, expiry, 5, None, routeParams = routeParams)
sender.send(payFsm, payment) sender.send(payFsm, payment)
router.expectMsgType[RouteRequest] router.expectMsgType[RouteRequest]
router.send(payFsm, RouteResponse(Seq(Route(finalAmount, hop_ab_1 :: hop_be :: Nil)))) router.send(payFsm, RouteResponse(Seq(Route(finalAmount, hop_ab_1 :: hop_be :: Nil))))
@ -493,7 +494,7 @@ class MultiPartPaymentLifecycleSpec extends TestKitBaseClass with FixtureAnyFunS
test("abort if recipient sends error during retry") { f => test("abort if recipient sends error during retry") { f =>
import f._ import f._
val payment = SendMultiPartPayment(sender.ref, randomBytes32(), e, finalAmount, expiry, 5, routeParams = routeParams) val payment = SendMultiPartPayment(sender.ref, randomBytes32(), e, finalAmount, expiry, 5, None, routeParams = routeParams)
sender.send(payFsm, payment) sender.send(payFsm, payment)
router.expectMsgType[RouteRequest] router.expectMsgType[RouteRequest]
router.send(payFsm, RouteResponse(Seq(Route(400000 msat, hop_ab_1 :: hop_be :: Nil), Route(600000 msat, hop_ac_1 :: hop_ce :: Nil)))) router.send(payFsm, RouteResponse(Seq(Route(400000 msat, hop_ab_1 :: hop_be :: Nil), Route(600000 msat, hop_ac_1 :: hop_ce :: Nil))))
@ -511,7 +512,7 @@ class MultiPartPaymentLifecycleSpec extends TestKitBaseClass with FixtureAnyFunS
test("receive partial success after retriable failure (recipient spec violation)") { f => test("receive partial success after retriable failure (recipient spec violation)") { f =>
import f._ import f._
val payment = SendMultiPartPayment(sender.ref, randomBytes32(), e, finalAmount, expiry, 5, routeParams = routeParams) val payment = SendMultiPartPayment(sender.ref, randomBytes32(), e, finalAmount, expiry, 5, None, routeParams = routeParams)
sender.send(payFsm, payment) sender.send(payFsm, payment)
router.expectMsgType[RouteRequest] router.expectMsgType[RouteRequest]
router.send(payFsm, RouteResponse(Seq(Route(400000 msat, hop_ab_1 :: hop_be :: Nil), Route(600000 msat, hop_ac_1 :: hop_ce :: Nil)))) router.send(payFsm, RouteResponse(Seq(Route(400000 msat, hop_ab_1 :: hop_be :: Nil), Route(600000 msat, hop_ac_1 :: hop_ce :: Nil))))
@ -531,7 +532,7 @@ class MultiPartPaymentLifecycleSpec extends TestKitBaseClass with FixtureAnyFunS
test("receive partial success after abort (recipient spec violation)") { f => test("receive partial success after abort (recipient spec violation)") { f =>
import f._ import f._
val payment = SendMultiPartPayment(sender.ref, randomBytes32(), e, finalAmount, expiry, 5, routeParams = routeParams) val payment = SendMultiPartPayment(sender.ref, randomBytes32(), e, finalAmount, expiry, 5, None, routeParams = routeParams)
sender.send(payFsm, payment) sender.send(payFsm, payment)
router.expectMsgType[RouteRequest] router.expectMsgType[RouteRequest]
router.send(payFsm, RouteResponse(Seq(Route(400000 msat, hop_ab_1 :: hop_be :: Nil), Route(600000 msat, hop_ac_1 :: hop_ce :: Nil)))) router.send(payFsm, RouteResponse(Seq(Route(400000 msat, hop_ab_1 :: hop_be :: Nil), Route(600000 msat, hop_ac_1 :: hop_ce :: Nil))))
@ -564,7 +565,7 @@ class MultiPartPaymentLifecycleSpec extends TestKitBaseClass with FixtureAnyFunS
test("receive partial failure after success (recipient spec violation)") { f => test("receive partial failure after success (recipient spec violation)") { f =>
import f._ import f._
val payment = SendMultiPartPayment(sender.ref, randomBytes32(), e, finalAmount, expiry, 5, routeParams = routeParams) val payment = SendMultiPartPayment(sender.ref, randomBytes32(), e, finalAmount, expiry, 5, None, routeParams = routeParams)
sender.send(payFsm, payment) sender.send(payFsm, payment)
router.expectMsgType[RouteRequest] router.expectMsgType[RouteRequest]
router.send(payFsm, RouteResponse(Seq(Route(400000 msat, hop_ab_1 :: hop_be :: Nil), Route(600000 msat, hop_ac_1 :: hop_ce :: Nil)))) router.send(payFsm, RouteResponse(Seq(Route(400000 msat, hop_ab_1 :: hop_be :: Nil), Route(600000 msat, hop_ac_1 :: hop_ce :: Nil))))

View file

@ -28,6 +28,7 @@ import fr.acinq.eclair.payment.OutgoingPaymentPacket.Upstream
import fr.acinq.eclair.payment.PaymentPacketSpec._ import fr.acinq.eclair.payment.PaymentPacketSpec._
import fr.acinq.eclair.payment.PaymentRequest.{ExtraHop, PaymentRequestFeatures} import fr.acinq.eclair.payment.PaymentRequest.{ExtraHop, PaymentRequestFeatures}
import fr.acinq.eclair.payment.send.MultiPartPaymentLifecycle.SendMultiPartPayment import fr.acinq.eclair.payment.send.MultiPartPaymentLifecycle.SendMultiPartPayment
import fr.acinq.eclair.payment.send.PaymentError.UnsupportedFeatures
import fr.acinq.eclair.payment.send.PaymentInitiator._ import fr.acinq.eclair.payment.send.PaymentInitiator._
import fr.acinq.eclair.payment.send.{PaymentError, PaymentInitiator, PaymentLifecycle} import fr.acinq.eclair.payment.send.{PaymentError, PaymentInitiator, PaymentLifecycle}
import fr.acinq.eclair.router.RouteNotFound import fr.acinq.eclair.router.RouteNotFound
@ -35,10 +36,10 @@ import fr.acinq.eclair.router.Router._
import fr.acinq.eclair.wire.protocol.OnionPaymentPayloadTlv.{AmountToForward, KeySend, OutgoingCltv} import fr.acinq.eclair.wire.protocol.OnionPaymentPayloadTlv.{AmountToForward, KeySend, OutgoingCltv}
import fr.acinq.eclair.wire.protocol.PaymentOnion.FinalTlvPayload import fr.acinq.eclair.wire.protocol.PaymentOnion.FinalTlvPayload
import fr.acinq.eclair.wire.protocol._ import fr.acinq.eclair.wire.protocol._
import fr.acinq.eclair.{CltvExpiryDelta, Features, MilliSatoshiLong, NodeParams, TestConstants, TestKitBaseClass, randomBytes32, randomKey} import fr.acinq.eclair.{CltvExpiryDelta, Feature, FeatureSupport, Features, InvoiceFeature, MilliSatoshiLong, NodeParams, TestConstants, TestKitBaseClass, TimestampSecond, randomBytes32, randomKey}
import org.scalatest.funsuite.FixtureAnyFunSuiteLike import org.scalatest.funsuite.FixtureAnyFunSuiteLike
import org.scalatest.{Outcome, Tag} import org.scalatest.{Outcome, Tag}
import scodec.bits.HexStringSyntax import scodec.bits.{BinStringSyntax, ByteVector, HexStringSyntax}
import java.util.UUID import java.util.UUID
import scala.concurrent.duration._ import scala.concurrent.duration._
@ -51,17 +52,24 @@ class PaymentInitiatorSpec extends TestKitBaseClass with FixtureAnyFunSuiteLike
case class FixtureParam(nodeParams: NodeParams, initiator: TestActorRef[PaymentInitiator], payFsm: TestProbe, multiPartPayFsm: TestProbe, sender: TestProbe, eventListener: TestProbe) case class FixtureParam(nodeParams: NodeParams, initiator: TestActorRef[PaymentInitiator], payFsm: TestProbe, multiPartPayFsm: TestProbe, sender: TestProbe, eventListener: TestProbe)
val featuresWithoutMpp = Features( val featuresWithoutMpp: Map[Feature with InvoiceFeature, FeatureSupport] = Map(
VariableLengthOnion -> Mandatory, VariableLengthOnion -> Mandatory,
PaymentSecret -> Mandatory PaymentSecret -> Mandatory
) )
val featuresWithMpp = Features( val featuresWithMpp: Map[Feature with InvoiceFeature, FeatureSupport] = Map(
VariableLengthOnion -> Mandatory, VariableLengthOnion -> Mandatory,
PaymentSecret -> Mandatory, PaymentSecret -> Mandatory,
BasicMultiPartPayment -> Optional, BasicMultiPartPayment -> Optional,
) )
val featuresWithTrampoline: Map[Feature with InvoiceFeature, FeatureSupport] = Map(
VariableLengthOnion -> Mandatory,
PaymentSecret -> Mandatory,
BasicMultiPartPayment -> Optional,
TrampolinePayment -> Optional,
)
case class FakePaymentFactory(payFsm: TestProbe, multiPartPayFsm: TestProbe) extends PaymentInitiator.MultiPartPaymentFactory { case class FakePaymentFactory(payFsm: TestProbe, multiPartPayFsm: TestProbe) extends PaymentInitiator.MultiPartPaymentFactory {
// @formatter:off // @formatter:off
override def spawnOutgoingPayment(context: ActorContext, cfg: SendPaymentConfig): ActorRef = { override def spawnOutgoingPayment(context: ActorContext, cfg: SendPaymentConfig): ActorRef = {
@ -77,7 +85,7 @@ class PaymentInitiatorSpec extends TestKitBaseClass with FixtureAnyFunSuiteLike
override def withFixture(test: OneArgTest): Outcome = { override def withFixture(test: OneArgTest): Outcome = {
val features = if (test.tags.contains("mpp_disabled")) featuresWithoutMpp else featuresWithMpp val features = if (test.tags.contains("mpp_disabled")) featuresWithoutMpp else featuresWithMpp
val nodeParams = TestConstants.Alice.nodeParams.copy(features = features) val nodeParams = TestConstants.Alice.nodeParams.copy(features = Features(features.collect { case (f, s) => (f: Feature, s) }))
val (sender, payFsm, multiPartPayFsm) = (TestProbe(), TestProbe(), TestProbe()) val (sender, payFsm, multiPartPayFsm) = (TestProbe(), TestProbe(), TestProbe())
val eventListener = TestProbe() val eventListener = TestProbe()
system.eventStream.subscribe(eventListener.ref, classOf[PaymentEvent]) system.eventStream.subscribe(eventListener.ref, classOf[PaymentEvent])
@ -115,13 +123,21 @@ class PaymentInitiatorSpec extends TestKitBaseClass with FixtureAnyFunSuiteLike
test("reject payment with unknown mandatory feature") { f => test("reject payment with unknown mandatory feature") { f =>
import f._ import f._
val unknownFeature = 42 val unknownFeature = 42
val pr = PaymentRequest(Block.LivenetGenesisBlock.hash, Some(finalAmount), paymentHash, randomKey(), Left("Some invoice"), CltvExpiryDelta(18), features = PaymentRequestFeatures(Features.VariableLengthOnion.mandatory, Features.PaymentSecret.mandatory, unknownFeature)) val taggedFields = List(
PaymentRequest.PaymentHash(paymentHash),
PaymentRequest.Description("Some invoice"),
PaymentRequest.PaymentSecret(randomBytes32()),
PaymentRequest.Expiry(3600),
PaymentRequest.PaymentRequestFeatures(bin"000001000000000000000000000000000100000100000000")
)
val pr = PaymentRequest("lnbc", Some(finalAmount), TimestampSecond.now(), randomKey().publicKey, taggedFields, ByteVector.empty)
val req = SendPaymentToNode(finalAmount + 100.msat, pr, 1, CltvExpiryDelta(42), routeParams = nodeParams.routerConf.pathFindingExperimentConf.getRandomConf().getDefaultRouteParams) val req = SendPaymentToNode(finalAmount + 100.msat, pr, 1, CltvExpiryDelta(42), routeParams = nodeParams.routerConf.pathFindingExperimentConf.getRandomConf().getDefaultRouteParams)
sender.send(initiator, req) sender.send(initiator, req)
val id = sender.expectMsgType[UUID] val id = sender.expectMsgType[UUID]
val fail = sender.expectMsgType[PaymentFailed] val fail = sender.expectMsgType[PaymentFailed]
assert(fail.id === id) assert(fail.id === id)
assert(fail.failures.head.isInstanceOf[LocalFailure]) assert(fail.failures.head.isInstanceOf[LocalFailure])
assert(fail.failures.head.asInstanceOf[LocalFailure].t === UnsupportedFeatures(pr.features.features))
} }
test("forward payment with pre-defined route") { f => test("forward payment with pre-defined route") { f =>
@ -134,13 +150,13 @@ class PaymentInitiatorSpec extends TestKitBaseClass with FixtureAnyFunSuiteLike
sender.send(initiator, SendPaymentToRoute(finalAmount, finalAmount, pr, ignoredFinalExpiryDelta, route, None, None, None, 0 msat, CltvExpiryDelta(0), Nil)) sender.send(initiator, SendPaymentToRoute(finalAmount, finalAmount, pr, ignoredFinalExpiryDelta, route, None, None, None, 0 msat, CltvExpiryDelta(0), Nil))
val payment = sender.expectMsgType[SendPaymentToRouteResponse] val payment = sender.expectMsgType[SendPaymentToRouteResponse]
payFsm.expectMsg(SendPaymentConfig(payment.paymentId, payment.parentId, None, paymentHash, finalAmount, c, Upstream.Local(payment.paymentId), Some(pr), storeInDb = true, publishEvent = true, recordPathFindingMetrics = false, Nil)) payFsm.expectMsg(SendPaymentConfig(payment.paymentId, payment.parentId, None, paymentHash, finalAmount, c, Upstream.Local(payment.paymentId), Some(pr), storeInDb = true, publishEvent = true, recordPathFindingMetrics = false, Nil))
payFsm.expectMsg(PaymentLifecycle.SendPaymentToRoute(sender.ref, Left(route), PaymentOnion.createSinglePartPayload(finalAmount, finalExpiryDelta.toCltvExpiry(nodeParams.currentBlockHeight + 1), pr.paymentSecret.get))) payFsm.expectMsg(PaymentLifecycle.SendPaymentToRoute(sender.ref, Left(route), PaymentOnion.createSinglePartPayload(finalAmount, finalExpiryDelta.toCltvExpiry(nodeParams.currentBlockHeight + 1), pr.paymentSecret.get, pr.paymentMetadata)))
} }
test("forward single-part payment when multi-part deactivated", Tag("mpp_disabled")) { f => test("forward single-part payment when multi-part deactivated", Tag("mpp_disabled")) { f =>
import f._ import f._
val finalExpiryDelta = CltvExpiryDelta(24) val finalExpiryDelta = CltvExpiryDelta(24)
val pr = PaymentRequest(Block.LivenetGenesisBlock.hash, Some(finalAmount), paymentHash, priv_c.privateKey, Left("Some MPP invoice"), finalExpiryDelta, features = PaymentRequestFeatures(VariableLengthOnion.mandatory, PaymentSecret.mandatory, BasicMultiPartPayment.optional)) val pr = PaymentRequest(Block.LivenetGenesisBlock.hash, Some(finalAmount), paymentHash, priv_c.privateKey, Left("Some MPP invoice"), finalExpiryDelta, features = PaymentRequestFeatures(featuresWithMpp))
val req = SendPaymentToNode(finalAmount, pr, 1, /* ignored since the invoice provides it */ CltvExpiryDelta(12), routeParams = nodeParams.routerConf.pathFindingExperimentConf.getRandomConf().getDefaultRouteParams) val req = SendPaymentToNode(finalAmount, pr, 1, /* ignored since the invoice provides it */ CltvExpiryDelta(12), routeParams = nodeParams.routerConf.pathFindingExperimentConf.getRandomConf().getDefaultRouteParams)
assert(req.finalExpiry(nodeParams.currentBlockHeight) === (finalExpiryDelta + 1).toCltvExpiry(nodeParams.currentBlockHeight)) assert(req.finalExpiry(nodeParams.currentBlockHeight) === (finalExpiryDelta + 1).toCltvExpiry(nodeParams.currentBlockHeight))
sender.send(initiator, req) sender.send(initiator, req)
@ -151,17 +167,17 @@ class PaymentInitiatorSpec extends TestKitBaseClass with FixtureAnyFunSuiteLike
test("forward multi-part payment") { f => test("forward multi-part payment") { f =>
import f._ import f._
val pr = PaymentRequest(Block.LivenetGenesisBlock.hash, Some(finalAmount), paymentHash, priv_c.privateKey, Left("Some invoice"), CltvExpiryDelta(18), features = PaymentRequestFeatures(VariableLengthOnion.mandatory, PaymentSecret.mandatory, BasicMultiPartPayment.optional)) val pr = PaymentRequest(Block.LivenetGenesisBlock.hash, Some(finalAmount), paymentHash, priv_c.privateKey, Left("Some invoice"), CltvExpiryDelta(18), features = PaymentRequestFeatures(featuresWithMpp))
val req = SendPaymentToNode(finalAmount + 100.msat, pr, 1, CltvExpiryDelta(42), routeParams = nodeParams.routerConf.pathFindingExperimentConf.getRandomConf().getDefaultRouteParams) val req = SendPaymentToNode(finalAmount + 100.msat, pr, 1, CltvExpiryDelta(42), routeParams = nodeParams.routerConf.pathFindingExperimentConf.getRandomConf().getDefaultRouteParams)
sender.send(initiator, req) sender.send(initiator, req)
val id = sender.expectMsgType[UUID] val id = sender.expectMsgType[UUID]
multiPartPayFsm.expectMsg(SendPaymentConfig(id, id, None, paymentHash, finalAmount + 100.msat, c, Upstream.Local(id), Some(pr), storeInDb = true, publishEvent = true, recordPathFindingMetrics = true, Nil)) multiPartPayFsm.expectMsg(SendPaymentConfig(id, id, None, paymentHash, finalAmount + 100.msat, c, Upstream.Local(id), Some(pr), storeInDb = true, publishEvent = true, recordPathFindingMetrics = true, Nil))
multiPartPayFsm.expectMsg(SendMultiPartPayment(sender.ref, pr.paymentSecret.get, c, finalAmount + 100.msat, req.finalExpiry(nodeParams.currentBlockHeight), 1, routeParams = nodeParams.routerConf.pathFindingExperimentConf.getRandomConf().getDefaultRouteParams)) multiPartPayFsm.expectMsg(SendMultiPartPayment(sender.ref, pr.paymentSecret.get, c, finalAmount + 100.msat, req.finalExpiry(nodeParams.currentBlockHeight), 1, pr.paymentMetadata, routeParams = nodeParams.routerConf.pathFindingExperimentConf.getRandomConf().getDefaultRouteParams))
} }
test("forward multi-part payment with pre-defined route") { f => test("forward multi-part payment with pre-defined route") { f =>
import f._ import f._
val pr = PaymentRequest(Block.LivenetGenesisBlock.hash, Some(finalAmount), paymentHash, priv_c.privateKey, Left("Some invoice"), CltvExpiryDelta(18), features = PaymentRequestFeatures(VariableLengthOnion.mandatory, PaymentSecret.mandatory, BasicMultiPartPayment.optional)) val pr = PaymentRequest(Block.LivenetGenesisBlock.hash, Some(finalAmount), paymentHash, priv_c.privateKey, Left("Some invoice"), CltvExpiryDelta(18), features = PaymentRequestFeatures(featuresWithMpp))
val route = PredefinedChannelRoute(c, Seq(channelUpdate_ab.shortChannelId, channelUpdate_bc.shortChannelId)) val route = PredefinedChannelRoute(c, Seq(channelUpdate_ab.shortChannelId, channelUpdate_bc.shortChannelId))
val req = SendPaymentToRoute(finalAmount / 2, finalAmount, pr, Channel.MIN_CLTV_EXPIRY_DELTA, route, None, None, None, 0 msat, CltvExpiryDelta(0), Nil) val req = SendPaymentToRoute(finalAmount / 2, finalAmount, pr, Channel.MIN_CLTV_EXPIRY_DELTA, route, None, None, None, 0 msat, CltvExpiryDelta(0), Nil)
sender.send(initiator, req) sender.send(initiator, req)
@ -177,9 +193,8 @@ class PaymentInitiatorSpec extends TestKitBaseClass with FixtureAnyFunSuiteLike
test("forward trampoline payment") { f => test("forward trampoline payment") { f =>
import f._ import f._
val features = PaymentRequestFeatures(VariableLengthOnion.mandatory, PaymentSecret.mandatory, BasicMultiPartPayment.optional, TrampolinePayment.optional)
val ignoredRoutingHints = List(List(ExtraHop(b, channelUpdate_bc.shortChannelId, feeBase = 10 msat, feeProportionalMillionths = 1, cltvExpiryDelta = CltvExpiryDelta(12)))) val ignoredRoutingHints = List(List(ExtraHop(b, channelUpdate_bc.shortChannelId, feeBase = 10 msat, feeProportionalMillionths = 1, cltvExpiryDelta = CltvExpiryDelta(12))))
val pr = PaymentRequest(Block.LivenetGenesisBlock.hash, Some(finalAmount), paymentHash, priv_c.privateKey, Left("Some phoenix invoice"), CltvExpiryDelta(9), features = features, extraHops = ignoredRoutingHints) val pr = PaymentRequest(Block.LivenetGenesisBlock.hash, Some(finalAmount), paymentHash, priv_c.privateKey, Left("Some phoenix invoice"), CltvExpiryDelta(9), features = PaymentRequestFeatures(featuresWithTrampoline), extraHops = ignoredRoutingHints)
val trampolineFees = 21000 msat val trampolineFees = 21000 msat
val req = SendTrampolinePayment(finalAmount, pr, b, Seq((trampolineFees, CltvExpiryDelta(12))), /* ignored since the invoice provides it */ CltvExpiryDelta(18), routeParams = nodeParams.routerConf.pathFindingExperimentConf.getRandomConf().getDefaultRouteParams) val req = SendTrampolinePayment(finalAmount, pr, b, Seq((trampolineFees, CltvExpiryDelta(12))), /* ignored since the invoice provides it */ CltvExpiryDelta(18), routeParams = nodeParams.routerConf.pathFindingExperimentConf.getRandomConf().getDefaultRouteParams)
sender.send(initiator, req) sender.send(initiator, req)
@ -250,8 +265,7 @@ class PaymentInitiatorSpec extends TestKitBaseClass with FixtureAnyFunSuiteLike
import f._ import f._
// This is disabled because it would let the trampoline node steal the whole payment (if malicious). // This is disabled because it would let the trampoline node steal the whole payment (if malicious).
val routingHints = List(List(PaymentRequest.ExtraHop(b, channelUpdate_bc.shortChannelId, 10 msat, 100, CltvExpiryDelta(144)))) val routingHints = List(List(PaymentRequest.ExtraHop(b, channelUpdate_bc.shortChannelId, 10 msat, 100, CltvExpiryDelta(144))))
val features = PaymentRequestFeatures(VariableLengthOnion.mandatory, PaymentSecret.mandatory, BasicMultiPartPayment.optional) val pr = PaymentRequest(Block.RegtestGenesisBlock.hash, None, paymentHash, priv_a.privateKey, Left("#abittooreckless"), CltvExpiryDelta(18), None, None, routingHints, features = PaymentRequestFeatures(featuresWithMpp))
val pr = PaymentRequest(Block.RegtestGenesisBlock.hash, None, paymentHash, priv_a.privateKey, Left("#abittooreckless"), CltvExpiryDelta(18), None, None, routingHints, features = features)
val trampolineFees = 21000 msat val trampolineFees = 21000 msat
val req = SendTrampolinePayment(finalAmount, pr, b, Seq((trampolineFees, CltvExpiryDelta(12))), CltvExpiryDelta(9), routeParams = nodeParams.routerConf.pathFindingExperimentConf.getRandomConf().getDefaultRouteParams) val req = SendTrampolinePayment(finalAmount, pr, b, Seq((trampolineFees, CltvExpiryDelta(12))), CltvExpiryDelta(9), routeParams = nodeParams.routerConf.pathFindingExperimentConf.getRandomConf().getDefaultRouteParams)
sender.send(initiator, req) sender.send(initiator, req)
@ -266,8 +280,7 @@ class PaymentInitiatorSpec extends TestKitBaseClass with FixtureAnyFunSuiteLike
test("retry trampoline payment") { f => test("retry trampoline payment") { f =>
import f._ import f._
val features = PaymentRequestFeatures(VariableLengthOnion.mandatory, PaymentSecret.mandatory, BasicMultiPartPayment.optional, TrampolinePayment.optional) val pr = PaymentRequest(Block.LivenetGenesisBlock.hash, Some(finalAmount), paymentHash, priv_c.privateKey, Left("Some phoenix invoice"), CltvExpiryDelta(18), features = PaymentRequestFeatures(featuresWithTrampoline))
val pr = PaymentRequest(Block.LivenetGenesisBlock.hash, Some(finalAmount), paymentHash, priv_c.privateKey, Left("Some phoenix invoice"), CltvExpiryDelta(18), features = features)
val trampolineAttempts = (21000 msat, CltvExpiryDelta(12)) :: (25000 msat, CltvExpiryDelta(24)) :: Nil val trampolineAttempts = (21000 msat, CltvExpiryDelta(12)) :: (25000 msat, CltvExpiryDelta(24)) :: Nil
val req = SendTrampolinePayment(finalAmount, pr, b, trampolineAttempts, CltvExpiryDelta(9), routeParams = nodeParams.routerConf.pathFindingExperimentConf.getRandomConf().getDefaultRouteParams) val req = SendTrampolinePayment(finalAmount, pr, b, trampolineAttempts, CltvExpiryDelta(9), routeParams = nodeParams.routerConf.pathFindingExperimentConf.getRandomConf().getDefaultRouteParams)
sender.send(initiator, req) sender.send(initiator, req)
@ -296,8 +309,7 @@ class PaymentInitiatorSpec extends TestKitBaseClass with FixtureAnyFunSuiteLike
test("retry trampoline payment and fail") { f => test("retry trampoline payment and fail") { f =>
import f._ import f._
val features = PaymentRequestFeatures(VariableLengthOnion.mandatory, PaymentSecret.mandatory, BasicMultiPartPayment.optional, TrampolinePayment.optional) val pr = PaymentRequest(Block.LivenetGenesisBlock.hash, Some(finalAmount), paymentHash, priv_c.privateKey, Left("Some phoenix invoice"), CltvExpiryDelta(18), features = PaymentRequestFeatures(featuresWithTrampoline))
val pr = PaymentRequest(Block.LivenetGenesisBlock.hash, Some(finalAmount), paymentHash, priv_c.privateKey, Left("Some phoenix invoice"), CltvExpiryDelta(18), features = features)
val trampolineAttempts = (21000 msat, CltvExpiryDelta(12)) :: (25000 msat, CltvExpiryDelta(24)) :: Nil val trampolineAttempts = (21000 msat, CltvExpiryDelta(12)) :: (25000 msat, CltvExpiryDelta(24)) :: Nil
val req = SendTrampolinePayment(finalAmount, pr, b, trampolineAttempts, CltvExpiryDelta(9), routeParams = nodeParams.routerConf.pathFindingExperimentConf.getRandomConf().getDefaultRouteParams) val req = SendTrampolinePayment(finalAmount, pr, b, trampolineAttempts, CltvExpiryDelta(9), routeParams = nodeParams.routerConf.pathFindingExperimentConf.getRandomConf().getDefaultRouteParams)
sender.send(initiator, req) sender.send(initiator, req)
@ -326,8 +338,7 @@ class PaymentInitiatorSpec extends TestKitBaseClass with FixtureAnyFunSuiteLike
test("retry trampoline payment and fail (route not found)") { f => test("retry trampoline payment and fail (route not found)") { f =>
import f._ import f._
val features = PaymentRequestFeatures(VariableLengthOnion.mandatory, PaymentSecret.mandatory, BasicMultiPartPayment.optional, TrampolinePayment.optional) val pr = PaymentRequest(Block.LivenetGenesisBlock.hash, Some(finalAmount), paymentHash, priv_c.privateKey, Left("Some phoenix invoice"), CltvExpiryDelta(18), features = PaymentRequestFeatures(featuresWithTrampoline))
val pr = PaymentRequest(Block.LivenetGenesisBlock.hash, Some(finalAmount), paymentHash, priv_c.privateKey, Left("Some phoenix invoice"), CltvExpiryDelta(18), features = features)
val trampolineAttempts = (21000 msat, CltvExpiryDelta(12)) :: (25000 msat, CltvExpiryDelta(24)) :: Nil val trampolineAttempts = (21000 msat, CltvExpiryDelta(12)) :: (25000 msat, CltvExpiryDelta(24)) :: Nil
val req = SendTrampolinePayment(finalAmount, pr, b, trampolineAttempts, CltvExpiryDelta(9), routeParams = nodeParams.routerConf.pathFindingExperimentConf.getRandomConf().getDefaultRouteParams) val req = SendTrampolinePayment(finalAmount, pr, b, trampolineAttempts, CltvExpiryDelta(9), routeParams = nodeParams.routerConf.pathFindingExperimentConf.getRandomConf().getDefaultRouteParams)
sender.send(initiator, req) sender.send(initiator, req)

View file

@ -102,7 +102,7 @@ class PaymentLifecycleSpec extends BaseRouterSpec {
// pre-computed route going from A to D // pre-computed route going from A to D
val route = Route(defaultAmountMsat, ChannelHop(a, b, update_ab) :: ChannelHop(b, c, update_bc) :: ChannelHop(c, d, update_cd) :: Nil) val route = Route(defaultAmountMsat, ChannelHop(a, b, update_ab) :: ChannelHop(b, c, update_bc) :: ChannelHop(c, d, update_cd) :: Nil)
val request = SendPaymentToRoute(sender.ref, Right(route), PaymentOnion.createSinglePartPayload(defaultAmountMsat, defaultExpiry, defaultInvoice.paymentSecret.get)) val request = SendPaymentToRoute(sender.ref, Right(route), PaymentOnion.createSinglePartPayload(defaultAmountMsat, defaultExpiry, defaultInvoice.paymentSecret.get, defaultInvoice.paymentMetadata))
sender.send(paymentFSM, request) sender.send(paymentFSM, request)
routerForwarder.expectNoMessage(100 millis) // we don't need the router, we have the pre-computed route routerForwarder.expectNoMessage(100 millis) // we don't need the router, we have the pre-computed route
val Transition(_, WAITING_FOR_REQUEST, WAITING_FOR_ROUTE) = monitor.expectMsgClass(classOf[Transition[_]]) val Transition(_, WAITING_FOR_REQUEST, WAITING_FOR_ROUTE) = monitor.expectMsgClass(classOf[Transition[_]])
@ -128,7 +128,7 @@ class PaymentLifecycleSpec extends BaseRouterSpec {
// pre-computed route going from A to D // pre-computed route going from A to D
val route = PredefinedNodeRoute(Seq(a, b, c, d)) val route = PredefinedNodeRoute(Seq(a, b, c, d))
val request = SendPaymentToRoute(sender.ref, Left(route), PaymentOnion.createSinglePartPayload(defaultAmountMsat, defaultExpiry, defaultInvoice.paymentSecret.get)) val request = SendPaymentToRoute(sender.ref, Left(route), PaymentOnion.createSinglePartPayload(defaultAmountMsat, defaultExpiry, defaultInvoice.paymentSecret.get, defaultInvoice.paymentMetadata))
sender.send(paymentFSM, request) sender.send(paymentFSM, request)
routerForwarder.expectMsg(FinalizeRoute(defaultAmountMsat, route, paymentContext = Some(cfg.paymentContext))) routerForwarder.expectMsg(FinalizeRoute(defaultAmountMsat, route, paymentContext = Some(cfg.paymentContext)))
@ -152,7 +152,7 @@ class PaymentLifecycleSpec extends BaseRouterSpec {
val payFixture = createPaymentLifecycle(recordMetrics = false) val payFixture = createPaymentLifecycle(recordMetrics = false)
import payFixture._ import payFixture._
val brokenRoute = SendPaymentToRoute(sender.ref, Left(PredefinedNodeRoute(Seq(randomKey().publicKey, randomKey().publicKey, randomKey().publicKey))), PaymentOnion.createSinglePartPayload(defaultAmountMsat, defaultExpiry, defaultInvoice.paymentSecret.get)) val brokenRoute = SendPaymentToRoute(sender.ref, Left(PredefinedNodeRoute(Seq(randomKey().publicKey, randomKey().publicKey, randomKey().publicKey))), PaymentOnion.createSinglePartPayload(defaultAmountMsat, defaultExpiry, defaultInvoice.paymentSecret.get, defaultInvoice.paymentMetadata))
sender.send(paymentFSM, brokenRoute) sender.send(paymentFSM, brokenRoute)
routerForwarder.expectMsgType[FinalizeRoute] routerForwarder.expectMsgType[FinalizeRoute]
routerForwarder.forward(routerFixture.router) routerForwarder.forward(routerFixture.router)
@ -167,7 +167,7 @@ class PaymentLifecycleSpec extends BaseRouterSpec {
val payFixture = createPaymentLifecycle(recordMetrics = false) val payFixture = createPaymentLifecycle(recordMetrics = false)
import payFixture._ import payFixture._
val brokenRoute = SendPaymentToRoute(sender.ref, Left(PredefinedChannelRoute(randomKey().publicKey, Seq(ShortChannelId(1), ShortChannelId(2)))), PaymentOnion.createSinglePartPayload(defaultAmountMsat, defaultExpiry, defaultInvoice.paymentSecret.get)) val brokenRoute = SendPaymentToRoute(sender.ref, Left(PredefinedChannelRoute(randomKey().publicKey, Seq(ShortChannelId(1), ShortChannelId(2)))), PaymentOnion.createSinglePartPayload(defaultAmountMsat, defaultExpiry, defaultInvoice.paymentSecret.get, defaultInvoice.paymentMetadata))
sender.send(paymentFSM, brokenRoute) sender.send(paymentFSM, brokenRoute)
routerForwarder.expectMsgType[FinalizeRoute] routerForwarder.expectMsgType[FinalizeRoute]
routerForwarder.forward(routerFixture.router) routerForwarder.forward(routerFixture.router)
@ -186,7 +186,7 @@ class PaymentLifecycleSpec extends BaseRouterSpec {
val recipient = randomKey().publicKey val recipient = randomKey().publicKey
val route = PredefinedNodeRoute(Seq(a, b, c, recipient)) val route = PredefinedNodeRoute(Seq(a, b, c, recipient))
val routingHint = Seq(Seq(ExtraHop(c, ShortChannelId(561), 1 msat, 100, CltvExpiryDelta(144)))) val routingHint = Seq(Seq(ExtraHop(c, ShortChannelId(561), 1 msat, 100, CltvExpiryDelta(144))))
val request = SendPaymentToRoute(sender.ref, Left(route), PaymentOnion.createSinglePartPayload(defaultAmountMsat, defaultExpiry, defaultInvoice.paymentSecret.get), routingHint) val request = SendPaymentToRoute(sender.ref, Left(route), PaymentOnion.createSinglePartPayload(defaultAmountMsat, defaultExpiry, defaultInvoice.paymentSecret.get, defaultInvoice.paymentMetadata), routingHint)
sender.send(paymentFSM, request) sender.send(paymentFSM, request)
routerForwarder.expectMsg(FinalizeRoute(defaultAmountMsat, route, routingHint, paymentContext = Some(cfg.paymentContext))) routerForwarder.expectMsg(FinalizeRoute(defaultAmountMsat, route, routingHint, paymentContext = Some(cfg.paymentContext)))
@ -210,7 +210,7 @@ class PaymentLifecycleSpec extends BaseRouterSpec {
import payFixture._ import payFixture._
import cfg._ import cfg._
val request = SendPaymentToNode(sender.ref, f, PaymentOnion.createSinglePartPayload(defaultAmountMsat, defaultExpiry, defaultInvoice.paymentSecret.get), 5, routeParams = defaultRouteParams) val request = SendPaymentToNode(sender.ref, f, PaymentOnion.createSinglePartPayload(defaultAmountMsat, defaultExpiry, defaultInvoice.paymentSecret.get, defaultInvoice.paymentMetadata), 5, routeParams = defaultRouteParams)
sender.send(paymentFSM, request) sender.send(paymentFSM, request)
val Transition(_, WAITING_FOR_REQUEST, WAITING_FOR_ROUTE) = monitor.expectMsgClass(classOf[Transition[_]]) val Transition(_, WAITING_FOR_REQUEST, WAITING_FOR_ROUTE) = monitor.expectMsgClass(classOf[Transition[_]])
val routeRequest = routerForwarder.expectMsgType[RouteRequest] val routeRequest = routerForwarder.expectMsgType[RouteRequest]
@ -241,7 +241,7 @@ class PaymentLifecycleSpec extends BaseRouterSpec {
"my-test-experiment", "my-test-experiment",
experimentPercentage = 100 experimentPercentage = 100
).getDefaultRouteParams ).getDefaultRouteParams
val request = SendPaymentToNode(sender.ref, d, PaymentOnion.createSinglePartPayload(defaultAmountMsat, defaultExpiry, defaultInvoice.paymentSecret.get), 5, routeParams = routeParams) val request = SendPaymentToNode(sender.ref, d, PaymentOnion.createSinglePartPayload(defaultAmountMsat, defaultExpiry, defaultInvoice.paymentSecret.get, defaultInvoice.paymentMetadata), 5, routeParams = routeParams)
sender.send(paymentFSM, request) sender.send(paymentFSM, request)
val routeRequest = routerForwarder.expectMsgType[RouteRequest] val routeRequest = routerForwarder.expectMsgType[RouteRequest]
val Transition(_, WAITING_FOR_REQUEST, WAITING_FOR_ROUTE) = monitor.expectMsgClass(classOf[Transition[_]]) val Transition(_, WAITING_FOR_REQUEST, WAITING_FOR_ROUTE) = monitor.expectMsgClass(classOf[Transition[_]])
@ -263,7 +263,7 @@ class PaymentLifecycleSpec extends BaseRouterSpec {
import payFixture._ import payFixture._
import cfg._ import cfg._
val request = SendPaymentToNode(sender.ref, d, PaymentOnion.createSinglePartPayload(defaultAmountMsat, defaultExpiry, defaultInvoice.paymentSecret.get), 2, routeParams = defaultRouteParams) val request = SendPaymentToNode(sender.ref, d, PaymentOnion.createSinglePartPayload(defaultAmountMsat, defaultExpiry, defaultInvoice.paymentSecret.get, defaultInvoice.paymentMetadata), 2, routeParams = defaultRouteParams)
sender.send(paymentFSM, request) sender.send(paymentFSM, request)
routerForwarder.expectMsg(defaultRouteRequest(a, d, cfg)) routerForwarder.expectMsg(defaultRouteRequest(a, d, cfg))
awaitCond(paymentFSM.stateName == WAITING_FOR_ROUTE && nodeParams.db.payments.getOutgoingPayment(id).exists(_.status === OutgoingPaymentStatus.Pending)) awaitCond(paymentFSM.stateName == WAITING_FOR_ROUTE && nodeParams.db.payments.getOutgoingPayment(id).exists(_.status === OutgoingPaymentStatus.Pending))
@ -306,7 +306,7 @@ class PaymentLifecycleSpec extends BaseRouterSpec {
import payFixture._ import payFixture._
import cfg._ import cfg._
val request = SendPaymentToNode(sender.ref, d, PaymentOnion.createSinglePartPayload(defaultAmountMsat, defaultExpiry, defaultInvoice.paymentSecret.get), 2, routeParams = defaultRouteParams) val request = SendPaymentToNode(sender.ref, d, PaymentOnion.createSinglePartPayload(defaultAmountMsat, defaultExpiry, defaultInvoice.paymentSecret.get, defaultInvoice.paymentMetadata), 2, routeParams = defaultRouteParams)
sender.send(paymentFSM, request) sender.send(paymentFSM, request)
awaitCond(paymentFSM.stateName == WAITING_FOR_ROUTE && nodeParams.db.payments.getOutgoingPayment(id).exists(_.status === OutgoingPaymentStatus.Pending)) awaitCond(paymentFSM.stateName == WAITING_FOR_ROUTE && nodeParams.db.payments.getOutgoingPayment(id).exists(_.status === OutgoingPaymentStatus.Pending))
routerForwarder.expectMsgType[RouteRequest] routerForwarder.expectMsgType[RouteRequest]
@ -327,7 +327,7 @@ class PaymentLifecycleSpec extends BaseRouterSpec {
import payFixture._ import payFixture._
import cfg._ import cfg._
val request = SendPaymentToNode(sender.ref, d, PaymentOnion.createSinglePartPayload(defaultAmountMsat, defaultExpiry, defaultInvoice.paymentSecret.get), 2, routeParams = defaultRouteParams) val request = SendPaymentToNode(sender.ref, d, PaymentOnion.createSinglePartPayload(defaultAmountMsat, defaultExpiry, defaultInvoice.paymentSecret.get, defaultInvoice.paymentMetadata), 2, routeParams = defaultRouteParams)
sender.send(paymentFSM, request) sender.send(paymentFSM, request)
awaitCond(paymentFSM.stateName == WAITING_FOR_ROUTE && nodeParams.db.payments.getOutgoingPayment(id).exists(_.status === OutgoingPaymentStatus.Pending)) awaitCond(paymentFSM.stateName == WAITING_FOR_ROUTE && nodeParams.db.payments.getOutgoingPayment(id).exists(_.status === OutgoingPaymentStatus.Pending))
routerForwarder.expectMsgType[RouteRequest] routerForwarder.expectMsgType[RouteRequest]
@ -347,7 +347,7 @@ class PaymentLifecycleSpec extends BaseRouterSpec {
import payFixture._ import payFixture._
import cfg._ import cfg._
val request = SendPaymentToNode(sender.ref, d, PaymentOnion.createSinglePartPayload(defaultAmountMsat, defaultExpiry, defaultInvoice.paymentSecret.get), 2, routeParams = defaultRouteParams) val request = SendPaymentToNode(sender.ref, d, PaymentOnion.createSinglePartPayload(defaultAmountMsat, defaultExpiry, defaultInvoice.paymentSecret.get, defaultInvoice.paymentMetadata), 2, routeParams = defaultRouteParams)
sender.send(paymentFSM, request) sender.send(paymentFSM, request)
awaitCond(paymentFSM.stateName == WAITING_FOR_ROUTE && nodeParams.db.payments.getOutgoingPayment(id).exists(_.status === OutgoingPaymentStatus.Pending)) awaitCond(paymentFSM.stateName == WAITING_FOR_ROUTE && nodeParams.db.payments.getOutgoingPayment(id).exists(_.status === OutgoingPaymentStatus.Pending))
@ -370,7 +370,7 @@ class PaymentLifecycleSpec extends BaseRouterSpec {
import payFixture._ import payFixture._
import cfg._ import cfg._
val request = SendPaymentToNode(sender.ref, d, PaymentOnion.createSinglePartPayload(defaultAmountMsat, defaultExpiry, defaultInvoice.paymentSecret.get), 2, routeParams = defaultRouteParams) val request = SendPaymentToNode(sender.ref, d, PaymentOnion.createSinglePartPayload(defaultAmountMsat, defaultExpiry, defaultInvoice.paymentSecret.get, defaultInvoice.paymentMetadata), 2, routeParams = defaultRouteParams)
sender.send(paymentFSM, request) sender.send(paymentFSM, request)
awaitCond(paymentFSM.stateName == WAITING_FOR_ROUTE && nodeParams.db.payments.getOutgoingPayment(id).exists(_.status === OutgoingPaymentStatus.Pending)) awaitCond(paymentFSM.stateName == WAITING_FOR_ROUTE && nodeParams.db.payments.getOutgoingPayment(id).exists(_.status === OutgoingPaymentStatus.Pending))
@ -393,7 +393,7 @@ class PaymentLifecycleSpec extends BaseRouterSpec {
import payFixture._ import payFixture._
import cfg._ import cfg._
val request = SendPaymentToNode(sender.ref, d, PaymentOnion.createSinglePartPayload(defaultAmountMsat, defaultExpiry, defaultInvoice.paymentSecret.get), 2, routeParams = defaultRouteParams) val request = SendPaymentToNode(sender.ref, d, PaymentOnion.createSinglePartPayload(defaultAmountMsat, defaultExpiry, defaultInvoice.paymentSecret.get, defaultInvoice.paymentMetadata), 2, routeParams = defaultRouteParams)
sender.send(paymentFSM, request) sender.send(paymentFSM, request)
awaitCond(paymentFSM.stateName == WAITING_FOR_ROUTE && nodeParams.db.payments.getOutgoingPayment(id).exists(_.status === OutgoingPaymentStatus.Pending)) awaitCond(paymentFSM.stateName == WAITING_FOR_ROUTE && nodeParams.db.payments.getOutgoingPayment(id).exists(_.status === OutgoingPaymentStatus.Pending))
@ -416,7 +416,7 @@ class PaymentLifecycleSpec extends BaseRouterSpec {
val payFixture = createPaymentLifecycle() val payFixture = createPaymentLifecycle()
import payFixture._ import payFixture._
val request = SendPaymentToNode(sender.ref, d, PaymentOnion.createSinglePartPayload(defaultAmountMsat, defaultExpiry, defaultInvoice.paymentSecret.get), 2, routeParams = defaultRouteParams) val request = SendPaymentToNode(sender.ref, d, PaymentOnion.createSinglePartPayload(defaultAmountMsat, defaultExpiry, defaultInvoice.paymentSecret.get, defaultInvoice.paymentMetadata), 2, routeParams = defaultRouteParams)
sender.send(paymentFSM, request) sender.send(paymentFSM, request)
awaitCond(paymentFSM.stateName == WAITING_FOR_ROUTE) awaitCond(paymentFSM.stateName == WAITING_FOR_ROUTE)
val WaitingForRoute(_, Nil, _) = paymentFSM.stateData val WaitingForRoute(_, Nil, _) = paymentFSM.stateData
@ -446,7 +446,7 @@ class PaymentLifecycleSpec extends BaseRouterSpec {
import payFixture._ import payFixture._
import cfg._ import cfg._
val request = SendPaymentToNode(sender.ref, d, PaymentOnion.createSinglePartPayload(defaultAmountMsat, defaultExpiry, defaultInvoice.paymentSecret.get), 5, routeParams = defaultRouteParams) val request = SendPaymentToNode(sender.ref, d, PaymentOnion.createSinglePartPayload(defaultAmountMsat, defaultExpiry, defaultInvoice.paymentSecret.get, defaultInvoice.paymentMetadata), 5, routeParams = defaultRouteParams)
sender.send(paymentFSM, request) sender.send(paymentFSM, request)
awaitCond(paymentFSM.stateName == WAITING_FOR_ROUTE && nodeParams.db.payments.getOutgoingPayment(id).exists(_.status === OutgoingPaymentStatus.Pending)) awaitCond(paymentFSM.stateName == WAITING_FOR_ROUTE && nodeParams.db.payments.getOutgoingPayment(id).exists(_.status === OutgoingPaymentStatus.Pending))
@ -498,7 +498,7 @@ class PaymentLifecycleSpec extends BaseRouterSpec {
val payFixture = createPaymentLifecycle() val payFixture = createPaymentLifecycle()
import payFixture._ import payFixture._
val request = SendPaymentToNode(sender.ref, d, PaymentOnion.createSinglePartPayload(defaultAmountMsat, defaultExpiry, defaultInvoice.paymentSecret.get), 1, routeParams = defaultRouteParams) val request = SendPaymentToNode(sender.ref, d, PaymentOnion.createSinglePartPayload(defaultAmountMsat, defaultExpiry, defaultInvoice.paymentSecret.get, defaultInvoice.paymentMetadata), 1, routeParams = defaultRouteParams)
sender.send(paymentFSM, request) sender.send(paymentFSM, request)
routerForwarder.expectMsg(defaultRouteRequest(nodeParams.nodeId, d, cfg)) routerForwarder.expectMsg(defaultRouteRequest(nodeParams.nodeId, d, cfg))
routerForwarder.forward(routerFixture.router) routerForwarder.forward(routerFixture.router)
@ -528,7 +528,7 @@ class PaymentLifecycleSpec extends BaseRouterSpec {
ExtraHop(c, channelId_cd, update_cd.feeBaseMsat, update_cd.feeProportionalMillionths, update_cd.cltvExpiryDelta) ExtraHop(c, channelId_cd, update_cd.feeBaseMsat, update_cd.feeProportionalMillionths, update_cd.cltvExpiryDelta)
)) ))
val request = SendPaymentToNode(sender.ref, d, PaymentOnion.createSinglePartPayload(defaultAmountMsat, defaultExpiry, defaultInvoice.paymentSecret.get), 5, assistedRoutes = assistedRoutes, routeParams = defaultRouteParams) val request = SendPaymentToNode(sender.ref, d, PaymentOnion.createSinglePartPayload(defaultAmountMsat, defaultExpiry, defaultInvoice.paymentSecret.get, defaultInvoice.paymentMetadata), 5, assistedRoutes = assistedRoutes, routeParams = defaultRouteParams)
sender.send(paymentFSM, request) sender.send(paymentFSM, request)
awaitCond(paymentFSM.stateName == WAITING_FOR_ROUTE && nodeParams.db.payments.getOutgoingPayment(id).exists(_.status === OutgoingPaymentStatus.Pending)) awaitCond(paymentFSM.stateName == WAITING_FOR_ROUTE && nodeParams.db.payments.getOutgoingPayment(id).exists(_.status === OutgoingPaymentStatus.Pending))
@ -569,7 +569,7 @@ class PaymentLifecycleSpec extends BaseRouterSpec {
// we build an assisted route for channel cd // we build an assisted route for channel cd
val assistedRoutes = Seq(Seq(ExtraHop(c, channelId_cd, update_cd.feeBaseMsat, update_cd.feeProportionalMillionths, update_cd.cltvExpiryDelta))) val assistedRoutes = Seq(Seq(ExtraHop(c, channelId_cd, update_cd.feeBaseMsat, update_cd.feeProportionalMillionths, update_cd.cltvExpiryDelta)))
val request = SendPaymentToNode(sender.ref, d, PaymentOnion.createSinglePartPayload(defaultAmountMsat, defaultExpiry, defaultInvoice.paymentSecret.get), 1, assistedRoutes = assistedRoutes, routeParams = defaultRouteParams) val request = SendPaymentToNode(sender.ref, d, PaymentOnion.createSinglePartPayload(defaultAmountMsat, defaultExpiry, defaultInvoice.paymentSecret.get, defaultInvoice.paymentMetadata), 1, assistedRoutes = assistedRoutes, routeParams = defaultRouteParams)
sender.send(paymentFSM, request) sender.send(paymentFSM, request)
awaitCond(paymentFSM.stateName == WAITING_FOR_ROUTE && nodeParams.db.payments.getOutgoingPayment(id).exists(_.status === OutgoingPaymentStatus.Pending)) awaitCond(paymentFSM.stateName == WAITING_FOR_ROUTE && nodeParams.db.payments.getOutgoingPayment(id).exists(_.status === OutgoingPaymentStatus.Pending))
@ -594,7 +594,7 @@ class PaymentLifecycleSpec extends BaseRouterSpec {
import payFixture._ import payFixture._
import cfg._ import cfg._
val request = SendPaymentToNode(sender.ref, d, PaymentOnion.createSinglePartPayload(defaultAmountMsat, defaultExpiry, defaultInvoice.paymentSecret.get), 2, routeParams = defaultRouteParams) val request = SendPaymentToNode(sender.ref, d, PaymentOnion.createSinglePartPayload(defaultAmountMsat, defaultExpiry, defaultInvoice.paymentSecret.get, defaultInvoice.paymentMetadata), 2, routeParams = defaultRouteParams)
sender.send(paymentFSM, request) sender.send(paymentFSM, request)
awaitCond(paymentFSM.stateName == WAITING_FOR_ROUTE && nodeParams.db.payments.getOutgoingPayment(id).exists(_.status === OutgoingPaymentStatus.Pending)) awaitCond(paymentFSM.stateName == WAITING_FOR_ROUTE && nodeParams.db.payments.getOutgoingPayment(id).exists(_.status === OutgoingPaymentStatus.Pending))
@ -632,7 +632,7 @@ class PaymentLifecycleSpec extends BaseRouterSpec {
import payFixture._ import payFixture._
import cfg._ import cfg._
val request = SendPaymentToNode(sender.ref, d, PaymentOnion.createSinglePartPayload(defaultAmountMsat, defaultExpiry, defaultInvoice.paymentSecret.get), 5, routeParams = defaultRouteParams) val request = SendPaymentToNode(sender.ref, d, PaymentOnion.createSinglePartPayload(defaultAmountMsat, defaultExpiry, defaultInvoice.paymentSecret.get, defaultInvoice.paymentMetadata), 5, routeParams = defaultRouteParams)
sender.send(paymentFSM, request) sender.send(paymentFSM, request)
routerForwarder.expectMsgType[RouteRequest] routerForwarder.expectMsgType[RouteRequest]
val Transition(_, WAITING_FOR_REQUEST, WAITING_FOR_ROUTE) = monitor.expectMsgClass(classOf[Transition[_]]) val Transition(_, WAITING_FOR_REQUEST, WAITING_FOR_ROUTE) = monitor.expectMsgClass(classOf[Transition[_]])
@ -686,7 +686,7 @@ class PaymentLifecycleSpec extends BaseRouterSpec {
import payFixture._ import payFixture._
// we send a payment to H // we send a payment to H
val request = SendPaymentToNode(sender.ref, h, PaymentOnion.createSinglePartPayload(defaultAmountMsat, defaultExpiry, defaultInvoice.paymentSecret.get), 5, routeParams = defaultRouteParams) val request = SendPaymentToNode(sender.ref, h, PaymentOnion.createSinglePartPayload(defaultAmountMsat, defaultExpiry, defaultInvoice.paymentSecret.get, defaultInvoice.paymentMetadata), 5, routeParams = defaultRouteParams)
sender.send(paymentFSM, request) sender.send(paymentFSM, request)
routerForwarder.expectMsgType[RouteRequest] routerForwarder.expectMsgType[RouteRequest]
@ -770,7 +770,7 @@ class PaymentLifecycleSpec extends BaseRouterSpec {
import payFixture._ import payFixture._
import cfg._ import cfg._
val request = SendPaymentToNode(sender.ref, d, PaymentOnion.createSinglePartPayload(defaultAmountMsat, defaultExpiry, defaultInvoice.paymentSecret.get), 3, routeParams = defaultRouteParams) val request = SendPaymentToNode(sender.ref, d, PaymentOnion.createSinglePartPayload(defaultAmountMsat, defaultExpiry, defaultInvoice.paymentSecret.get, defaultInvoice.paymentMetadata), 3, routeParams = defaultRouteParams)
sender.send(paymentFSM, request) sender.send(paymentFSM, request)
routerForwarder.expectMsgType[RouteRequest] routerForwarder.expectMsgType[RouteRequest]
val Transition(_, WAITING_FOR_REQUEST, WAITING_FOR_ROUTE) = monitor.expectMsgClass(classOf[Transition[_]]) val Transition(_, WAITING_FOR_REQUEST, WAITING_FOR_ROUTE) = monitor.expectMsgClass(classOf[Transition[_]])
@ -791,7 +791,7 @@ class PaymentLifecycleSpec extends BaseRouterSpec {
// pre-computed route going from A to D // pre-computed route going from A to D
val route = Route(defaultAmountMsat, ChannelHop(a, b, update_ab) :: ChannelHop(b, c, update_bc) :: ChannelHop(c, d, update_cd) :: Nil) val route = Route(defaultAmountMsat, ChannelHop(a, b, update_ab) :: ChannelHop(b, c, update_bc) :: ChannelHop(c, d, update_cd) :: Nil)
val request = SendPaymentToRoute(sender.ref, Right(route), PaymentOnion.createSinglePartPayload(defaultAmountMsat, defaultExpiry, defaultInvoice.paymentSecret.get)) val request = SendPaymentToRoute(sender.ref, Right(route), PaymentOnion.createSinglePartPayload(defaultAmountMsat, defaultExpiry, defaultInvoice.paymentSecret.get, defaultInvoice.paymentMetadata))
sender.send(paymentFSM, request) sender.send(paymentFSM, request)
routerForwarder.expectNoMessage(100 millis) // we don't need the router, we have the pre-computed route routerForwarder.expectNoMessage(100 millis) // we don't need the router, we have the pre-computed route
val Transition(_, WAITING_FOR_REQUEST, WAITING_FOR_ROUTE) = monitor.expectMsgClass(classOf[Transition[_]]) val Transition(_, WAITING_FOR_REQUEST, WAITING_FOR_ROUTE) = monitor.expectMsgClass(classOf[Transition[_]])

View file

@ -20,6 +20,7 @@ import akka.actor.ActorRef
import fr.acinq.bitcoin.Crypto.PublicKey import fr.acinq.bitcoin.Crypto.PublicKey
import fr.acinq.bitcoin.DeterministicWallet.ExtendedPrivateKey import fr.acinq.bitcoin.DeterministicWallet.ExtendedPrivateKey
import fr.acinq.bitcoin.{Block, ByteVector32, Crypto, DeterministicWallet, OutPoint, Satoshi, SatoshiLong, TxOut} import fr.acinq.bitcoin.{Block, ByteVector32, Crypto, DeterministicWallet, OutPoint, Satoshi, SatoshiLong, TxOut}
import fr.acinq.eclair.FeatureSupport.{Mandatory, Optional}
import fr.acinq.eclair.Features._ import fr.acinq.eclair.Features._
import fr.acinq.eclair.channel._ import fr.acinq.eclair.channel._
import fr.acinq.eclair.crypto.Sphinx import fr.acinq.eclair.crypto.Sphinx
@ -31,7 +32,7 @@ import fr.acinq.eclair.transactions.Transactions.InputInfo
import fr.acinq.eclair.wire.protocol.OnionPaymentPayloadTlv.{AmountToForward, OutgoingCltv, PaymentData} import fr.acinq.eclair.wire.protocol.OnionPaymentPayloadTlv.{AmountToForward, OutgoingCltv, PaymentData}
import fr.acinq.eclair.wire.protocol.PaymentOnion.{ChannelRelayTlvPayload, FinalTlvPayload} import fr.acinq.eclair.wire.protocol.PaymentOnion.{ChannelRelayTlvPayload, FinalTlvPayload}
import fr.acinq.eclair.wire.protocol._ import fr.acinq.eclair.wire.protocol._
import fr.acinq.eclair.{CltvExpiry, CltvExpiryDelta, MilliSatoshi, MilliSatoshiLong, ShortChannelId, TestConstants, TimestampSecondLong, nodeFee, randomBytes32, randomKey} import fr.acinq.eclair.{CltvExpiry, CltvExpiryDelta, Feature, FeatureSupport, InvoiceFeature, MilliSatoshi, MilliSatoshiLong, ShortChannelId, TestConstants, TimestampSecondLong, nodeFee, randomBytes32, randomKey}
import org.scalatest.BeforeAndAfterAll import org.scalatest.BeforeAndAfterAll
import org.scalatest.funsuite.AnyFunSuite import org.scalatest.funsuite.AnyFunSuite
import scodec.Attempt import scodec.Attempt
@ -115,7 +116,7 @@ class PaymentPacketSpec extends AnyFunSuite with BeforeAndAfterAll {
} }
test("build a command including the onion") { test("build a command including the onion") {
val (add, _) = buildCommand(ActorRef.noSender, Upstream.Local(UUID.randomUUID), paymentHash, hops, PaymentOnion.createSinglePartPayload(finalAmount, finalExpiry, paymentSecret)) val (add, _) = buildCommand(ActorRef.noSender, Upstream.Local(UUID.randomUUID), paymentHash, hops, PaymentOnion.createSinglePartPayload(finalAmount, finalExpiry, paymentSecret, None))
assert(add.amount > finalAmount) assert(add.amount > finalAmount)
assert(add.cltvExpiry === finalExpiry + channelUpdate_de.cltvExpiryDelta + channelUpdate_cd.cltvExpiryDelta + channelUpdate_bc.cltvExpiryDelta) assert(add.cltvExpiry === finalExpiry + channelUpdate_de.cltvExpiryDelta + channelUpdate_cd.cltvExpiryDelta + channelUpdate_bc.cltvExpiryDelta)
assert(add.paymentHash === paymentHash) assert(add.paymentHash === paymentHash)
@ -126,7 +127,7 @@ class PaymentPacketSpec extends AnyFunSuite with BeforeAndAfterAll {
} }
test("build a command with no hops") { test("build a command with no hops") {
val (add, _) = buildCommand(ActorRef.noSender, Upstream.Local(UUID.randomUUID()), paymentHash, hops.take(1), PaymentOnion.createSinglePartPayload(finalAmount, finalExpiry, paymentSecret)) val (add, _) = buildCommand(ActorRef.noSender, Upstream.Local(UUID.randomUUID()), paymentHash, hops.take(1), PaymentOnion.createSinglePartPayload(finalAmount, finalExpiry, paymentSecret, Some(paymentMetadata)))
assert(add.amount === finalAmount) assert(add.amount === finalAmount)
assert(add.cltvExpiry === finalExpiry) assert(add.cltvExpiry === finalExpiry)
assert(add.paymentHash === paymentHash) assert(add.paymentHash === paymentHash)
@ -140,6 +141,7 @@ class PaymentPacketSpec extends AnyFunSuite with BeforeAndAfterAll {
assert(payload_b.totalAmount === finalAmount) assert(payload_b.totalAmount === finalAmount)
assert(payload_b.expiry === finalExpiry) assert(payload_b.expiry === finalExpiry)
assert(payload_b.paymentSecret === paymentSecret) assert(payload_b.paymentSecret === paymentSecret)
assert(payload_b.paymentMetadata === Some(paymentMetadata))
} }
test("build a trampoline payment") { test("build a trampoline payment") {
@ -148,7 +150,7 @@ class PaymentPacketSpec extends AnyFunSuite with BeforeAndAfterAll {
// / \ / \ // / \ / \
// a -> b -> c d e // a -> b -> c d e
val (amount_ac, expiry_ac, trampolineOnion) = buildTrampolinePacket(paymentHash, trampolineHops, PaymentOnion.createMultiPartPayload(finalAmount, finalAmount * 3, finalExpiry, paymentSecret)) val (amount_ac, expiry_ac, trampolineOnion) = buildTrampolinePacket(paymentHash, trampolineHops, PaymentOnion.createMultiPartPayload(finalAmount, finalAmount * 3, finalExpiry, paymentSecret, Some(hex"010203")))
assert(amount_ac === amount_bc) assert(amount_ac === amount_bc)
assert(expiry_ac === expiry_bc) assert(expiry_ac === expiry_bc)
@ -173,6 +175,7 @@ class PaymentPacketSpec extends AnyFunSuite with BeforeAndAfterAll {
assert(inner_c.invoiceRoutingInfo === None) assert(inner_c.invoiceRoutingInfo === None)
assert(inner_c.invoiceFeatures === None) assert(inner_c.invoiceFeatures === None)
assert(inner_c.paymentSecret === None) assert(inner_c.paymentSecret === None)
assert(inner_c.paymentMetadata === None)
// c forwards the trampoline payment to d. // c forwards the trampoline payment to d.
val (amount_d, expiry_d, onion_d) = buildPaymentPacket(paymentHash, ChannelHop(c, d, channelUpdate_cd) :: Nil, PaymentOnion.createTrampolinePayload(amount_cd, amount_cd, expiry_cd, randomBytes32(), packet_d)) val (amount_d, expiry_d, onion_d) = buildPaymentPacket(paymentHash, ChannelHop(c, d, channelUpdate_cd) :: Nil, PaymentOnion.createTrampolinePayload(amount_cd, amount_cd, expiry_cd, randomBytes32(), packet_d))
@ -190,6 +193,7 @@ class PaymentPacketSpec extends AnyFunSuite with BeforeAndAfterAll {
assert(inner_d.invoiceRoutingInfo === None) assert(inner_d.invoiceRoutingInfo === None)
assert(inner_d.invoiceFeatures === None) assert(inner_d.invoiceFeatures === None)
assert(inner_d.paymentSecret === None) assert(inner_d.paymentSecret === None)
assert(inner_d.paymentMetadata === None)
// d forwards the trampoline payment to e. // d forwards the trampoline payment to e.
val (amount_e, expiry_e, onion_e) = buildPaymentPacket(paymentHash, ChannelHop(d, e, channelUpdate_de) :: Nil, PaymentOnion.createTrampolinePayload(amount_de, amount_de, expiry_de, randomBytes32(), packet_e)) val (amount_e, expiry_e, onion_e) = buildPaymentPacket(paymentHash, ChannelHop(d, e, channelUpdate_de) :: Nil, PaymentOnion.createTrampolinePayload(amount_de, amount_de, expiry_de, randomBytes32(), packet_e))
@ -198,7 +202,7 @@ class PaymentPacketSpec extends AnyFunSuite with BeforeAndAfterAll {
val add_e = UpdateAddHtlc(randomBytes32(), 4, amount_e, paymentHash, expiry_e, onion_e.packet) val add_e = UpdateAddHtlc(randomBytes32(), 4, amount_e, paymentHash, expiry_e, onion_e.packet)
val Right(FinalPacket(add_e2, payload_e)) = decrypt(add_e, priv_e.privateKey) val Right(FinalPacket(add_e2, payload_e)) = decrypt(add_e, priv_e.privateKey)
assert(add_e2 === add_e) assert(add_e2 === add_e)
assert(payload_e === FinalTlvPayload(TlvStream(AmountToForward(finalAmount), OutgoingCltv(finalExpiry), PaymentData(paymentSecret, finalAmount * 3)))) assert(payload_e === FinalTlvPayload(TlvStream(AmountToForward(finalAmount), OutgoingCltv(finalExpiry), PaymentData(paymentSecret, finalAmount * 3), OnionPaymentPayloadTlv.PaymentMetadata(hex"010203"))))
} }
test("build a trampoline payment with non-trampoline recipient") { test("build a trampoline payment with non-trampoline recipient") {
@ -208,9 +212,9 @@ class PaymentPacketSpec extends AnyFunSuite with BeforeAndAfterAll {
// a -> b -> c d -> e // a -> b -> c d -> e
val routingHints = List(List(PaymentRequest.ExtraHop(randomKey().publicKey, ShortChannelId(42), 10 msat, 100, CltvExpiryDelta(144)))) val routingHints = List(List(PaymentRequest.ExtraHop(randomKey().publicKey, ShortChannelId(42), 10 msat, 100, CltvExpiryDelta(144))))
val invoiceFeatures = PaymentRequestFeatures(VariableLengthOnion.mandatory, PaymentSecret.mandatory, BasicMultiPartPayment.optional) val invoiceFeatures = PaymentRequestFeatures(Map[Feature with InvoiceFeature, FeatureSupport](VariableLengthOnion -> Mandatory, PaymentSecret -> Mandatory, BasicMultiPartPayment -> Optional))
val invoice = PaymentRequest(Block.RegtestGenesisBlock.hash, Some(finalAmount), paymentHash, priv_a.privateKey, Left("#reckless"), CltvExpiryDelta(18), None, None, routingHints, features = invoiceFeatures) val invoice = PaymentRequest(Block.RegtestGenesisBlock.hash, Some(finalAmount), paymentHash, priv_a.privateKey, Left("#reckless"), CltvExpiryDelta(18), None, None, routingHints, features = invoiceFeatures, paymentMetadata = Some(hex"010203"))
val (amount_ac, expiry_ac, trampolineOnion) = buildTrampolineToLegacyPacket(invoice, trampolineHops, PaymentOnion.createSinglePartPayload(finalAmount, finalExpiry, invoice.paymentSecret.get)) val (amount_ac, expiry_ac, trampolineOnion) = buildTrampolineToLegacyPacket(invoice, trampolineHops, PaymentOnion.createSinglePartPayload(finalAmount, finalExpiry, invoice.paymentSecret.get, None))
assert(amount_ac === amount_bc) assert(amount_ac === amount_bc)
assert(expiry_ac === expiry_bc) assert(expiry_ac === expiry_bc)
@ -249,6 +253,7 @@ class PaymentPacketSpec extends AnyFunSuite with BeforeAndAfterAll {
assert(inner_d.outgoingNodeId === e) assert(inner_d.outgoingNodeId === e)
assert(inner_d.totalAmount === finalAmount) assert(inner_d.totalAmount === finalAmount)
assert(inner_d.paymentSecret === invoice.paymentSecret) assert(inner_d.paymentSecret === invoice.paymentSecret)
assert(inner_d.paymentMetadata === Some(hex"010203"))
assert(inner_d.invoiceFeatures === Some(hex"024100")) // var_onion_optin, payment_secret, basic_mpp assert(inner_d.invoiceFeatures === Some(hex"024100")) // var_onion_optin, payment_secret, basic_mpp
assert(inner_d.invoiceRoutingInfo === Some(routingHints)) assert(inner_d.invoiceRoutingInfo === Some(routingHints))
} }
@ -257,19 +262,19 @@ class PaymentPacketSpec extends AnyFunSuite with BeforeAndAfterAll {
val routingHintOverflow = List(List.fill(7)(PaymentRequest.ExtraHop(randomKey().publicKey, ShortChannelId(1), 10 msat, 100, CltvExpiryDelta(12)))) val routingHintOverflow = List(List.fill(7)(PaymentRequest.ExtraHop(randomKey().publicKey, ShortChannelId(1), 10 msat, 100, CltvExpiryDelta(12))))
val invoice = PaymentRequest(Block.RegtestGenesisBlock.hash, Some(finalAmount), paymentHash, priv_a.privateKey, Left("#reckless"), CltvExpiryDelta(18), None, None, routingHintOverflow) val invoice = PaymentRequest(Block.RegtestGenesisBlock.hash, Some(finalAmount), paymentHash, priv_a.privateKey, Left("#reckless"), CltvExpiryDelta(18), None, None, routingHintOverflow)
assertThrows[IllegalArgumentException]( assertThrows[IllegalArgumentException](
buildTrampolineToLegacyPacket(invoice, trampolineHops, PaymentOnion.createSinglePartPayload(finalAmount, finalExpiry, invoice.paymentSecret.get)) buildTrampolineToLegacyPacket(invoice, trampolineHops, PaymentOnion.createSinglePartPayload(finalAmount, finalExpiry, invoice.paymentSecret.get, invoice.paymentMetadata))
) )
} }
test("fail to decrypt when the onion is invalid") { test("fail to decrypt when the onion is invalid") {
val (firstAmount, firstExpiry, onion) = buildPaymentPacket(paymentHash, hops, PaymentOnion.createSinglePartPayload(finalAmount, finalExpiry, paymentSecret)) val (firstAmount, firstExpiry, onion) = buildPaymentPacket(paymentHash, hops, PaymentOnion.createSinglePartPayload(finalAmount, finalExpiry, paymentSecret, None))
val add = UpdateAddHtlc(randomBytes32(), 1, firstAmount, paymentHash, firstExpiry, onion.packet.copy(payload = onion.packet.payload.reverse)) val add = UpdateAddHtlc(randomBytes32(), 1, firstAmount, paymentHash, firstExpiry, onion.packet.copy(payload = onion.packet.payload.reverse))
val Left(failure) = decrypt(add, priv_b.privateKey) val Left(failure) = decrypt(add, priv_b.privateKey)
assert(failure.isInstanceOf[InvalidOnionHmac]) assert(failure.isInstanceOf[InvalidOnionHmac])
} }
test("fail to decrypt when the trampoline onion is invalid") { test("fail to decrypt when the trampoline onion is invalid") {
val (amount_ac, expiry_ac, trampolineOnion) = buildTrampolinePacket(paymentHash, trampolineHops, PaymentOnion.createMultiPartPayload(finalAmount, finalAmount * 2, finalExpiry, paymentSecret)) val (amount_ac, expiry_ac, trampolineOnion) = buildTrampolinePacket(paymentHash, trampolineHops, PaymentOnion.createMultiPartPayload(finalAmount, finalAmount * 2, finalExpiry, paymentSecret, None))
val (firstAmount, firstExpiry, onion) = buildPaymentPacket(paymentHash, trampolineChannelHops, PaymentOnion.createTrampolinePayload(amount_ac, amount_ac, expiry_ac, randomBytes32(), trampolineOnion.packet.copy(payload = trampolineOnion.packet.payload.reverse))) val (firstAmount, firstExpiry, onion) = buildPaymentPacket(paymentHash, trampolineChannelHops, PaymentOnion.createTrampolinePayload(amount_ac, amount_ac, expiry_ac, randomBytes32(), trampolineOnion.packet.copy(payload = trampolineOnion.packet.payload.reverse)))
val add_b = UpdateAddHtlc(randomBytes32(), 1, firstAmount, paymentHash, firstExpiry, onion.packet) val add_b = UpdateAddHtlc(randomBytes32(), 1, firstAmount, paymentHash, firstExpiry, onion.packet)
val Right(ChannelRelayPacket(_, _, packet_c)) = decrypt(add_b, priv_b.privateKey) val Right(ChannelRelayPacket(_, _, packet_c)) = decrypt(add_b, priv_b.privateKey)
@ -279,28 +284,28 @@ class PaymentPacketSpec extends AnyFunSuite with BeforeAndAfterAll {
} }
test("fail to decrypt when payment hash doesn't match associated data") { test("fail to decrypt when payment hash doesn't match associated data") {
val (firstAmount, firstExpiry, onion) = buildPaymentPacket(paymentHash.reverse, hops, PaymentOnion.createSinglePartPayload(finalAmount, finalExpiry, paymentSecret)) val (firstAmount, firstExpiry, onion) = buildPaymentPacket(paymentHash.reverse, hops, PaymentOnion.createSinglePartPayload(finalAmount, finalExpiry, paymentSecret, None))
val add = UpdateAddHtlc(randomBytes32(), 1, firstAmount, paymentHash, firstExpiry, onion.packet) val add = UpdateAddHtlc(randomBytes32(), 1, firstAmount, paymentHash, firstExpiry, onion.packet)
val Left(failure) = decrypt(add, priv_b.privateKey) val Left(failure) = decrypt(add, priv_b.privateKey)
assert(failure.isInstanceOf[InvalidOnionHmac]) assert(failure.isInstanceOf[InvalidOnionHmac])
} }
test("fail to decrypt at the final node when amount has been modified by next-to-last node") { test("fail to decrypt at the final node when amount has been modified by next-to-last node") {
val (firstAmount, firstExpiry, onion) = buildPaymentPacket(paymentHash, hops.take(1), PaymentOnion.createSinglePartPayload(finalAmount, finalExpiry, paymentSecret)) val (firstAmount, firstExpiry, onion) = buildPaymentPacket(paymentHash, hops.take(1), PaymentOnion.createSinglePartPayload(finalAmount, finalExpiry, paymentSecret, None))
val add = UpdateAddHtlc(randomBytes32(), 1, firstAmount - 100.msat, paymentHash, firstExpiry, onion.packet) val add = UpdateAddHtlc(randomBytes32(), 1, firstAmount - 100.msat, paymentHash, firstExpiry, onion.packet)
val Left(failure) = decrypt(add, priv_b.privateKey) val Left(failure) = decrypt(add, priv_b.privateKey)
assert(failure === FinalIncorrectHtlcAmount(firstAmount - 100.msat)) assert(failure === FinalIncorrectHtlcAmount(firstAmount - 100.msat))
} }
test("fail to decrypt at the final node when expiry has been modified by next-to-last node") { test("fail to decrypt at the final node when expiry has been modified by next-to-last node") {
val (firstAmount, firstExpiry, onion) = buildPaymentPacket(paymentHash, hops.take(1), PaymentOnion.createSinglePartPayload(finalAmount, finalExpiry, paymentSecret)) val (firstAmount, firstExpiry, onion) = buildPaymentPacket(paymentHash, hops.take(1), PaymentOnion.createSinglePartPayload(finalAmount, finalExpiry, paymentSecret, None))
val add = UpdateAddHtlc(randomBytes32(), 1, firstAmount, paymentHash, firstExpiry - CltvExpiryDelta(12), onion.packet) val add = UpdateAddHtlc(randomBytes32(), 1, firstAmount, paymentHash, firstExpiry - CltvExpiryDelta(12), onion.packet)
val Left(failure) = decrypt(add, priv_b.privateKey) val Left(failure) = decrypt(add, priv_b.privateKey)
assert(failure === FinalIncorrectCltvExpiry(firstExpiry - CltvExpiryDelta(12))) assert(failure === FinalIncorrectCltvExpiry(firstExpiry - CltvExpiryDelta(12)))
} }
test("fail to decrypt at the final trampoline node when amount has been modified by next-to-last trampoline") { test("fail to decrypt at the final trampoline node when amount has been modified by next-to-last trampoline") {
val (amount_ac, expiry_ac, trampolineOnion) = buildTrampolinePacket(paymentHash, trampolineHops, PaymentOnion.createMultiPartPayload(finalAmount, finalAmount, finalExpiry, paymentSecret)) val (amount_ac, expiry_ac, trampolineOnion) = buildTrampolinePacket(paymentHash, trampolineHops, PaymentOnion.createMultiPartPayload(finalAmount, finalAmount, finalExpiry, paymentSecret, None))
val (firstAmount, firstExpiry, onion) = buildPaymentPacket(paymentHash, trampolineChannelHops, PaymentOnion.createTrampolinePayload(amount_ac, amount_ac, expiry_ac, randomBytes32(), trampolineOnion.packet)) val (firstAmount, firstExpiry, onion) = buildPaymentPacket(paymentHash, trampolineChannelHops, PaymentOnion.createTrampolinePayload(amount_ac, amount_ac, expiry_ac, randomBytes32(), trampolineOnion.packet))
val Right(ChannelRelayPacket(_, _, packet_c)) = decrypt(UpdateAddHtlc(randomBytes32(), 1, firstAmount, paymentHash, firstExpiry, onion.packet), priv_b.privateKey) val Right(ChannelRelayPacket(_, _, packet_c)) = decrypt(UpdateAddHtlc(randomBytes32(), 1, firstAmount, paymentHash, firstExpiry, onion.packet), priv_b.privateKey)
val Right(NodeRelayPacket(_, _, _, packet_d)) = decrypt(UpdateAddHtlc(randomBytes32(), 2, amount_bc, paymentHash, expiry_bc, packet_c), priv_c.privateKey) val Right(NodeRelayPacket(_, _, _, packet_d)) = decrypt(UpdateAddHtlc(randomBytes32(), 2, amount_bc, paymentHash, expiry_bc, packet_c), priv_c.privateKey)
@ -315,7 +320,7 @@ class PaymentPacketSpec extends AnyFunSuite with BeforeAndAfterAll {
} }
test("fail to decrypt at the final trampoline node when expiry has been modified by next-to-last trampoline") { test("fail to decrypt at the final trampoline node when expiry has been modified by next-to-last trampoline") {
val (amount_ac, expiry_ac, trampolineOnion) = buildTrampolinePacket(paymentHash, trampolineHops, PaymentOnion.createMultiPartPayload(finalAmount, finalAmount, finalExpiry, paymentSecret)) val (amount_ac, expiry_ac, trampolineOnion) = buildTrampolinePacket(paymentHash, trampolineHops, PaymentOnion.createMultiPartPayload(finalAmount, finalAmount, finalExpiry, paymentSecret, None))
val (firstAmount, firstExpiry, onion) = buildPaymentPacket(paymentHash, trampolineChannelHops, PaymentOnion.createTrampolinePayload(amount_ac, amount_ac, expiry_ac, randomBytes32(), trampolineOnion.packet)) val (firstAmount, firstExpiry, onion) = buildPaymentPacket(paymentHash, trampolineChannelHops, PaymentOnion.createTrampolinePayload(amount_ac, amount_ac, expiry_ac, randomBytes32(), trampolineOnion.packet))
val Right(ChannelRelayPacket(_, _, packet_c)) = decrypt(UpdateAddHtlc(randomBytes32(), 1, firstAmount, paymentHash, firstExpiry, onion.packet), priv_b.privateKey) val Right(ChannelRelayPacket(_, _, packet_c)) = decrypt(UpdateAddHtlc(randomBytes32(), 1, firstAmount, paymentHash, firstExpiry, onion.packet), priv_b.privateKey)
val Right(NodeRelayPacket(_, _, _, packet_d)) = decrypt(UpdateAddHtlc(randomBytes32(), 2, amount_bc, paymentHash, expiry_bc, packet_c), priv_c.privateKey) val Right(NodeRelayPacket(_, _, _, packet_d)) = decrypt(UpdateAddHtlc(randomBytes32(), 2, amount_bc, paymentHash, expiry_bc, packet_c), priv_c.privateKey)
@ -330,7 +335,7 @@ class PaymentPacketSpec extends AnyFunSuite with BeforeAndAfterAll {
} }
test("fail to decrypt at intermediate trampoline node when amount is invalid") { test("fail to decrypt at intermediate trampoline node when amount is invalid") {
val (amount_ac, expiry_ac, trampolineOnion) = buildTrampolinePacket(paymentHash, trampolineHops, PaymentOnion.createSinglePartPayload(finalAmount, finalExpiry, paymentSecret)) val (amount_ac, expiry_ac, trampolineOnion) = buildTrampolinePacket(paymentHash, trampolineHops, PaymentOnion.createSinglePartPayload(finalAmount, finalExpiry, paymentSecret, None))
val (firstAmount, firstExpiry, onion) = buildPaymentPacket(paymentHash, trampolineChannelHops, PaymentOnion.createTrampolinePayload(amount_ac, amount_ac, expiry_ac, randomBytes32(), trampolineOnion.packet)) val (firstAmount, firstExpiry, onion) = buildPaymentPacket(paymentHash, trampolineChannelHops, PaymentOnion.createTrampolinePayload(amount_ac, amount_ac, expiry_ac, randomBytes32(), trampolineOnion.packet))
val Right(ChannelRelayPacket(_, _, packet_c)) = decrypt(UpdateAddHtlc(randomBytes32(), 1, firstAmount, paymentHash, firstExpiry, onion.packet), priv_b.privateKey) val Right(ChannelRelayPacket(_, _, packet_c)) = decrypt(UpdateAddHtlc(randomBytes32(), 1, firstAmount, paymentHash, firstExpiry, onion.packet), priv_b.privateKey)
// A trampoline relay is very similar to a final node: it can validate that the HTLC amount matches the onion outer amount. // A trampoline relay is very similar to a final node: it can validate that the HTLC amount matches the onion outer amount.
@ -339,7 +344,7 @@ class PaymentPacketSpec extends AnyFunSuite with BeforeAndAfterAll {
} }
test("fail to decrypt at intermediate trampoline node when expiry is invalid") { test("fail to decrypt at intermediate trampoline node when expiry is invalid") {
val (amount_ac, expiry_ac, trampolineOnion) = buildTrampolinePacket(paymentHash, trampolineHops, PaymentOnion.createSinglePartPayload(finalAmount, finalExpiry, paymentSecret)) val (amount_ac, expiry_ac, trampolineOnion) = buildTrampolinePacket(paymentHash, trampolineHops, PaymentOnion.createSinglePartPayload(finalAmount, finalExpiry, paymentSecret, None))
val (firstAmount, firstExpiry, onion) = buildPaymentPacket(paymentHash, trampolineChannelHops, PaymentOnion.createTrampolinePayload(amount_ac, amount_ac, expiry_ac, randomBytes32(), trampolineOnion.packet)) val (firstAmount, firstExpiry, onion) = buildPaymentPacket(paymentHash, trampolineChannelHops, PaymentOnion.createTrampolinePayload(amount_ac, amount_ac, expiry_ac, randomBytes32(), trampolineOnion.packet))
val Right(ChannelRelayPacket(_, _, packet_c)) = decrypt(UpdateAddHtlc(randomBytes32(), 1, firstAmount, paymentHash, firstExpiry, onion.packet), priv_b.privateKey) val Right(ChannelRelayPacket(_, _, packet_c)) = decrypt(UpdateAddHtlc(randomBytes32(), 1, firstAmount, paymentHash, firstExpiry, onion.packet), priv_b.privateKey)
// A trampoline relay is very similar to a final node: it can validate that the HTLC expiry matches the onion outer expiry. // A trampoline relay is very similar to a final node: it can validate that the HTLC expiry matches the onion outer expiry.
@ -398,6 +403,7 @@ object PaymentPacketSpec {
val paymentPreimage = randomBytes32() val paymentPreimage = randomBytes32()
val paymentHash = Crypto.sha256(paymentPreimage) val paymentHash = Crypto.sha256(paymentPreimage)
val paymentSecret = randomBytes32() val paymentSecret = randomBytes32()
val paymentMetadata = randomBytes32().bytes
val expiry_de = finalExpiry val expiry_de = finalExpiry
val amount_de = finalAmount val amount_de = finalAmount

View file

@ -18,10 +18,10 @@ package fr.acinq.eclair.payment
import fr.acinq.bitcoin.Crypto.{PrivateKey, PublicKey} import fr.acinq.bitcoin.Crypto.{PrivateKey, PublicKey}
import fr.acinq.bitcoin.{Block, BtcDouble, ByteVector32, Crypto, MilliBtcDouble, SatoshiLong} import fr.acinq.bitcoin.{Block, BtcDouble, ByteVector32, Crypto, MilliBtcDouble, SatoshiLong}
import fr.acinq.eclair.FeatureSupport.Mandatory import fr.acinq.eclair.FeatureSupport.{Mandatory, Optional}
import fr.acinq.eclair.Features.{PaymentSecret, _} import fr.acinq.eclair.Features.{PaymentMetadata, PaymentSecret, _}
import fr.acinq.eclair.payment.PaymentRequest._ import fr.acinq.eclair.payment.PaymentRequest._
import fr.acinq.eclair.{CltvExpiryDelta, FeatureSupport, Features, MilliSatoshiLong, ShortChannelId, TestConstants, TimestampSecond, TimestampSecondLong, ToMilliSatoshiConversion} import fr.acinq.eclair.{CltvExpiryDelta, Feature, FeatureSupport, Features, InvoiceFeature, MilliSatoshiLong, ShortChannelId, TestConstants, TimestampSecond, TimestampSecondLong, ToMilliSatoshiConversion}
import org.scalatest.funsuite.AnyFunSuite import org.scalatest.funsuite.AnyFunSuite
import scodec.DecodeResult import scodec.DecodeResult
import scodec.bits._ import scodec.bits._
@ -311,6 +311,22 @@ class PaymentRequestSpec extends AnyFunSuite {
assert(PaymentRequest.write(pr.sign(priv)) === ref) assert(PaymentRequest.write(pr.sign(priv)) === ref)
} }
test("On mainnet, please send 0.01 BTC with payment metadata 0x01fafaf0") {
val ref = "lnbc10m1pvjluezpp5qqqsyqcyq5rqwzqfqqqsyqcyq5rqwzqfqqqsyqcyq5rqwzqfqypqdp9wpshjmt9de6zqmt9w3skgct5vysxjmnnd9jx2mq8q8a04uqsp5zyg3zyg3zyg3zyg3zyg3zyg3zyg3zyg3zyg3zyg3zyg3zyg3zygs9q2gqqqqqqsgq7hf8he7ecf7n4ffphs6awl9t6676rrclv9ckg3d3ncn7fct63p6s365duk5wrk202cfy3aj5xnnp5gs3vrdvruverwwq7yzhkf5a3xqpd05wjc"
val pr = PaymentRequest.read(ref)
assert(pr.prefix == "lnbc")
assert(pr.amount === Some(1000000000 msat))
assert(pr.paymentHash.bytes == hex"0001020304050607080900010203040506070809000102030405060708090102")
assert(pr.features.features === Features(VariableLengthOnion -> Mandatory, PaymentSecret -> Mandatory, PaymentMetadata -> Mandatory))
assert(pr.timestamp == TimestampSecond(1496314658L))
assert(pr.nodeId == PublicKey(hex"03e7156ae33b0a208d0744199163177e909e80176e55d97a2f221ede0f934dd9ad"))
assert(pr.paymentSecret === Some(ByteVector32(hex"1111111111111111111111111111111111111111111111111111111111111111")))
assert(pr.description == Left("payment metadata inside"))
assert(pr.paymentMetadata === Some(hex"01fafaf0"))
assert(pr.tags.size == 5)
assert(PaymentRequest.write(pr.sign(priv)) == ref)
}
test("reject invalid invoices") { test("reject invalid invoices") {
val refs = Seq( val refs = Seq(
// Bech32 checksum is invalid. // Bech32 checksum is invalid.
@ -455,7 +471,7 @@ class PaymentRequestSpec extends AnyFunSuite {
test("payment secret") { test("payment secret") {
val pr = PaymentRequest(Block.LivenetGenesisBlock.hash, Some(123 msat), ByteVector32.One, priv, Left("Some invoice"), CltvExpiryDelta(18)) val pr = PaymentRequest(Block.LivenetGenesisBlock.hash, Some(123 msat), ByteVector32.One, priv, Left("Some invoice"), CltvExpiryDelta(18))
assert(pr.paymentSecret.isDefined) assert(pr.paymentSecret.isDefined)
assert(pr.features === PaymentRequestFeatures(PaymentSecret.mandatory, VariableLengthOnion.mandatory)) assert(pr.features === PaymentRequestFeatures(Map[Feature with InvoiceFeature, FeatureSupport](VariableLengthOnion -> Mandatory, PaymentSecret -> Mandatory)))
assert(pr.features.requirePaymentSecret) assert(pr.features.requirePaymentSecret)
val pr1 = PaymentRequest.read(PaymentRequest.write(pr)) val pr1 = PaymentRequest.read(PaymentRequest.write(pr))
@ -471,20 +487,23 @@ class PaymentRequestSpec extends AnyFunSuite {
) )
// A multi-part invoice must use a payment secret. // A multi-part invoice must use a payment secret.
assertThrows[IllegalArgumentException]( assertThrows[IllegalArgumentException]({
PaymentRequest(Block.LivenetGenesisBlock.hash, Some(123 msat), ByteVector32.One, priv, Left("MPP without secrets"), CltvExpiryDelta(18), features = PaymentRequestFeatures(BasicMultiPartPayment.optional, VariableLengthOnion.optional)) val features = PaymentRequestFeatures(Map[Feature with InvoiceFeature, FeatureSupport](VariableLengthOnion -> Optional, PaymentSecret -> Optional))
) PaymentRequest(Block.LivenetGenesisBlock.hash, Some(123 msat), ByteVector32.One, priv, Left("MPP without secrets"), CltvExpiryDelta(18), features = features)
})
} }
test("trampoline") { test("trampoline") {
val pr = PaymentRequest(Block.LivenetGenesisBlock.hash, Some(123 msat), ByteVector32.One, priv, Left("Some invoice"), CltvExpiryDelta(18)) val pr = PaymentRequest(Block.LivenetGenesisBlock.hash, Some(123 msat), ByteVector32.One, priv, Left("Some invoice"), CltvExpiryDelta(18))
assert(!pr.features.allowTrampoline) assert(!pr.features.allowTrampoline)
val pr1 = PaymentRequest(Block.LivenetGenesisBlock.hash, Some(123 msat), ByteVector32.One, priv, Left("Some invoice"), CltvExpiryDelta(18), features = PaymentRequestFeatures(VariableLengthOnion.mandatory, PaymentSecret.mandatory, TrampolinePayment.optional)) val features1 = PaymentRequestFeatures(Map[Feature with InvoiceFeature, FeatureSupport](VariableLengthOnion -> Mandatory, PaymentSecret -> Mandatory, TrampolinePayment -> Optional))
val pr1 = PaymentRequest(Block.LivenetGenesisBlock.hash, Some(123 msat), ByteVector32.One, priv, Left("Some invoice"), CltvExpiryDelta(18), features = features1)
assert(!pr1.features.allowMultiPart) assert(!pr1.features.allowMultiPart)
assert(pr1.features.allowTrampoline) assert(pr1.features.allowTrampoline)
val pr2 = PaymentRequest(Block.LivenetGenesisBlock.hash, Some(123 msat), ByteVector32.One, priv, Left("Some invoice"), CltvExpiryDelta(18), features = PaymentRequestFeatures(VariableLengthOnion.mandatory, PaymentSecret.mandatory, BasicMultiPartPayment.optional, TrampolinePayment.optional)) val features2 = PaymentRequestFeatures(Map[Feature with InvoiceFeature, FeatureSupport](VariableLengthOnion -> Mandatory, PaymentSecret -> Mandatory, BasicMultiPartPayment -> Optional, TrampolinePayment -> Optional))
val pr2 = PaymentRequest(Block.LivenetGenesisBlock.hash, Some(123 msat), ByteVector32.One, priv, Left("Some invoice"), CltvExpiryDelta(18), features = features2)
assert(pr2.features.allowMultiPart) assert(pr2.features.allowMultiPart)
assert(pr2.features.allowTrampoline) assert(pr2.features.allowTrampoline)

View file

@ -673,7 +673,7 @@ object PostRestartHtlcCleanerSpec {
val (paymentHash1, paymentHash2, paymentHash3) = (Crypto.sha256(preimage1), Crypto.sha256(preimage2), Crypto.sha256(preimage3)) val (paymentHash1, paymentHash2, paymentHash3) = (Crypto.sha256(preimage1), Crypto.sha256(preimage2), Crypto.sha256(preimage3))
def buildHtlc(htlcId: Long, channelId: ByteVector32, paymentHash: ByteVector32): UpdateAddHtlc = { def buildHtlc(htlcId: Long, channelId: ByteVector32, paymentHash: ByteVector32): UpdateAddHtlc = {
val (cmd, _) = buildCommand(ActorRef.noSender, Upstream.Local(UUID.randomUUID()), paymentHash, hops, PaymentOnion.createSinglePartPayload(finalAmount, finalExpiry, randomBytes32())) val (cmd, _) = buildCommand(ActorRef.noSender, Upstream.Local(UUID.randomUUID()), paymentHash, hops, PaymentOnion.createSinglePartPayload(finalAmount, finalExpiry, randomBytes32(), None))
UpdateAddHtlc(channelId, htlcId, cmd.amount, cmd.paymentHash, cmd.cltvExpiry, cmd.onion) UpdateAddHtlc(channelId, htlcId, cmd.amount, cmd.paymentHash, cmd.cltvExpiry, cmd.onion)
} }
@ -682,7 +682,7 @@ object PostRestartHtlcCleanerSpec {
def buildHtlcOut(htlcId: Long, channelId: ByteVector32, paymentHash: ByteVector32): DirectedHtlc = OutgoingHtlc(buildHtlc(htlcId, channelId, paymentHash)) def buildHtlcOut(htlcId: Long, channelId: ByteVector32, paymentHash: ByteVector32): DirectedHtlc = OutgoingHtlc(buildHtlc(htlcId, channelId, paymentHash))
def buildFinalHtlc(htlcId: Long, channelId: ByteVector32, paymentHash: ByteVector32): DirectedHtlc = { def buildFinalHtlc(htlcId: Long, channelId: ByteVector32, paymentHash: ByteVector32): DirectedHtlc = {
val (cmd, _) = buildCommand(ActorRef.noSender, Upstream.Local(UUID.randomUUID()), paymentHash, ChannelHop(a, TestConstants.Bob.nodeParams.nodeId, channelUpdate_ab) :: Nil, PaymentOnion.createSinglePartPayload(finalAmount, finalExpiry, randomBytes32())) val (cmd, _) = buildCommand(ActorRef.noSender, Upstream.Local(UUID.randomUUID()), paymentHash, ChannelHop(a, TestConstants.Bob.nodeParams.nodeId, channelUpdate_ab) :: Nil, PaymentOnion.createSinglePartPayload(finalAmount, finalExpiry, randomBytes32(), None))
IncomingHtlc(UpdateAddHtlc(channelId, htlcId, cmd.amount, cmd.paymentHash, cmd.cltvExpiry, cmd.onion)) IncomingHtlc(UpdateAddHtlc(channelId, htlcId, cmd.amount, cmd.paymentHash, cmd.cltvExpiry, cmd.onion))
} }

View file

@ -24,6 +24,7 @@ import akka.actor.typed.scaladsl.ActorContext
import akka.actor.typed.scaladsl.adapter._ import akka.actor.typed.scaladsl.adapter._
import com.typesafe.config.ConfigFactory import com.typesafe.config.ConfigFactory
import fr.acinq.bitcoin.{Block, ByteVector32, Crypto} import fr.acinq.bitcoin.{Block, ByteVector32, Crypto}
import fr.acinq.eclair.FeatureSupport.{Mandatory, Optional}
import fr.acinq.eclair.Features.{BasicMultiPartPayment, PaymentSecret, VariableLengthOnion} import fr.acinq.eclair.Features.{BasicMultiPartPayment, PaymentSecret, VariableLengthOnion}
import fr.acinq.eclair.channel.{CMD_FAIL_HTLC, CMD_FULFILL_HTLC, Register} import fr.acinq.eclair.channel.{CMD_FAIL_HTLC, CMD_FULFILL_HTLC, Register}
import fr.acinq.eclair.crypto.Sphinx import fr.acinq.eclair.crypto.Sphinx
@ -37,7 +38,7 @@ import fr.acinq.eclair.payment.send.PaymentLifecycle.SendPaymentToNode
import fr.acinq.eclair.router.Router.RouteRequest import fr.acinq.eclair.router.Router.RouteRequest
import fr.acinq.eclair.router.{BalanceTooLow, RouteNotFound} import fr.acinq.eclair.router.{BalanceTooLow, RouteNotFound}
import fr.acinq.eclair.wire.protocol._ import fr.acinq.eclair.wire.protocol._
import fr.acinq.eclair.{CltvExpiry, CltvExpiryDelta, MilliSatoshi, MilliSatoshiLong, NodeParams, ShortChannelId, TestConstants, UInt64, randomBytes, randomBytes32, randomKey} import fr.acinq.eclair.{CltvExpiry, CltvExpiryDelta, Feature, FeatureSupport, InvoiceFeature, MilliSatoshi, MilliSatoshiLong, NodeParams, ShortChannelId, TestConstants, UInt64, randomBytes, randomBytes32, randomKey}
import org.scalatest.Outcome import org.scalatest.Outcome
import org.scalatest.funsuite.FixtureAnyFunSuiteLike import org.scalatest.funsuite.FixtureAnyFunSuiteLike
import scodec.bits.HexStringSyntax import scodec.bits.HexStringSyntax
@ -211,7 +212,7 @@ class NodeRelayerSpec extends ScalaTestWithActorTestKit(ConfigFactory.load("appl
// and then one extra // and then one extra
val extra = IncomingPaymentPacket.NodeRelayPacket( val extra = IncomingPaymentPacket.NodeRelayPacket(
UpdateAddHtlc(randomBytes32(), Random.nextInt(100), 1000 msat, paymentHash, CltvExpiry(499990), TestConstants.emptyOnionPacket), UpdateAddHtlc(randomBytes32(), Random.nextInt(100), 1000 msat, paymentHash, CltvExpiry(499990), TestConstants.emptyOnionPacket),
PaymentOnion.createMultiPartPayload(1000 msat, incomingAmount, CltvExpiry(499990), incomingSecret), PaymentOnion.createMultiPartPayload(1000 msat, incomingAmount, CltvExpiry(499990), incomingSecret, None),
PaymentOnion.createNodeRelayPayload(outgoingAmount, outgoingExpiry, outgoingNodeId), PaymentOnion.createNodeRelayPayload(outgoingAmount, outgoingExpiry, outgoingNodeId),
nextTrampolinePacket) nextTrampolinePacket)
nodeRelayer ! NodeRelay.Relay(extra) nodeRelayer ! NodeRelay.Relay(extra)
@ -240,7 +241,7 @@ class NodeRelayerSpec extends ScalaTestWithActorTestKit(ConfigFactory.load("appl
// Receive new extraneous multi-part HTLC. // Receive new extraneous multi-part HTLC.
val i1 = IncomingPaymentPacket.NodeRelayPacket( val i1 = IncomingPaymentPacket.NodeRelayPacket(
UpdateAddHtlc(randomBytes32(), Random.nextInt(100), 1000 msat, paymentHash, CltvExpiry(499990), TestConstants.emptyOnionPacket), UpdateAddHtlc(randomBytes32(), Random.nextInt(100), 1000 msat, paymentHash, CltvExpiry(499990), TestConstants.emptyOnionPacket),
PaymentOnion.createMultiPartPayload(1000 msat, incomingAmount, CltvExpiry(499990), incomingSecret), PaymentOnion.createMultiPartPayload(1000 msat, incomingAmount, CltvExpiry(499990), incomingSecret, None),
PaymentOnion.createNodeRelayPayload(outgoingAmount, outgoingExpiry, outgoingNodeId), PaymentOnion.createNodeRelayPayload(outgoingAmount, outgoingExpiry, outgoingNodeId),
nextTrampolinePacket) nextTrampolinePacket)
nodeRelayer ! NodeRelay.Relay(i1) nodeRelayer ! NodeRelay.Relay(i1)
@ -253,7 +254,7 @@ class NodeRelayerSpec extends ScalaTestWithActorTestKit(ConfigFactory.load("appl
// Receive new HTLC with different details, but for the same payment hash. // Receive new HTLC with different details, but for the same payment hash.
val i2 = IncomingPaymentPacket.NodeRelayPacket( val i2 = IncomingPaymentPacket.NodeRelayPacket(
UpdateAddHtlc(randomBytes32(), Random.nextInt(100), 1500 msat, paymentHash, CltvExpiry(499990), TestConstants.emptyOnionPacket), UpdateAddHtlc(randomBytes32(), Random.nextInt(100), 1500 msat, paymentHash, CltvExpiry(499990), TestConstants.emptyOnionPacket),
PaymentOnion.createSinglePartPayload(1500 msat, CltvExpiry(499990), incomingSecret), PaymentOnion.createSinglePartPayload(1500 msat, CltvExpiry(499990), incomingSecret, None),
PaymentOnion.createNodeRelayPayload(1250 msat, outgoingExpiry, outgoingNodeId), PaymentOnion.createNodeRelayPayload(1250 msat, outgoingExpiry, outgoingNodeId),
nextTrampolinePacket) nextTrampolinePacket)
nodeRelayer ! NodeRelay.Relay(i2) nodeRelayer ! NodeRelay.Relay(i2)
@ -566,8 +567,8 @@ class NodeRelayerSpec extends ScalaTestWithActorTestKit(ConfigFactory.load("appl
// Receive an upstream multi-part payment. // Receive an upstream multi-part payment.
val hints = List(List(ExtraHop(outgoingNodeId, ShortChannelId(42), feeBase = 10 msat, feeProportionalMillionths = 1, cltvExpiryDelta = CltvExpiryDelta(12)))) val hints = List(List(ExtraHop(outgoingNodeId, ShortChannelId(42), feeBase = 10 msat, feeProportionalMillionths = 1, cltvExpiryDelta = CltvExpiryDelta(12))))
val features = PaymentRequestFeatures(VariableLengthOnion.mandatory, PaymentSecret.mandatory, BasicMultiPartPayment.optional) val features = PaymentRequestFeatures(Map[Feature with InvoiceFeature, FeatureSupport](VariableLengthOnion -> Mandatory, PaymentSecret -> Mandatory, BasicMultiPartPayment -> Optional))
val pr = PaymentRequest(Block.LivenetGenesisBlock.hash, Some(outgoingAmount * 3), paymentHash, randomKey(), Left("Some invoice"), CltvExpiryDelta(18), extraHops = hints, features = features) val pr = PaymentRequest(Block.LivenetGenesisBlock.hash, Some(outgoingAmount * 3), paymentHash, randomKey(), Left("Some invoice"), CltvExpiryDelta(18), extraHops = hints, paymentMetadata = Some(hex"123456"), features = features)
val incomingPayments = incomingMultiPart.map(incoming => incoming.copy(innerPayload = PaymentOnion.createNodeRelayToNonTrampolinePayload( val incomingPayments = incomingMultiPart.map(incoming => incoming.copy(innerPayload = PaymentOnion.createNodeRelayToNonTrampolinePayload(
incoming.innerPayload.amountToForward, outgoingAmount * 3, outgoingExpiry, outgoingNodeId, pr incoming.innerPayload.amountToForward, outgoingAmount * 3, outgoingExpiry, outgoingNodeId, pr
))) )))
@ -578,6 +579,7 @@ class NodeRelayerSpec extends ScalaTestWithActorTestKit(ConfigFactory.load("appl
validateOutgoingCfg(outgoingCfg, Upstream.Trampoline(incomingMultiPart.map(_.add))) validateOutgoingCfg(outgoingCfg, Upstream.Trampoline(incomingMultiPart.map(_.add)))
val outgoingPayment = mockPayFSM.expectMessageType[SendMultiPartPayment] val outgoingPayment = mockPayFSM.expectMessageType[SendMultiPartPayment]
assert(outgoingPayment.paymentSecret === pr.paymentSecret.get) // we should use the provided secret assert(outgoingPayment.paymentSecret === pr.paymentSecret.get) // we should use the provided secret
assert(outgoingPayment.paymentMetadata === pr.paymentMetadata) // we should use the provided metadata
assert(outgoingPayment.totalAmount === outgoingAmount) assert(outgoingPayment.totalAmount === outgoingAmount)
assert(outgoingPayment.targetExpiry === outgoingExpiry) assert(outgoingPayment.targetExpiry === outgoingExpiry)
assert(outgoingPayment.targetNodeId === outgoingNodeId) assert(outgoingPayment.targetNodeId === outgoingNodeId)
@ -607,7 +609,7 @@ class NodeRelayerSpec extends ScalaTestWithActorTestKit(ConfigFactory.load("appl
// Receive an upstream multi-part payment. // Receive an upstream multi-part payment.
val hints = List(List(ExtraHop(outgoingNodeId, ShortChannelId(42), feeBase = 10 msat, feeProportionalMillionths = 1, cltvExpiryDelta = CltvExpiryDelta(12)))) val hints = List(List(ExtraHop(outgoingNodeId, ShortChannelId(42), feeBase = 10 msat, feeProportionalMillionths = 1, cltvExpiryDelta = CltvExpiryDelta(12))))
val pr = PaymentRequest(Block.LivenetGenesisBlock.hash, Some(outgoingAmount), paymentHash, randomKey(), Left("Some invoice"), CltvExpiryDelta(18), extraHops = hints) val pr = PaymentRequest(Block.LivenetGenesisBlock.hash, Some(outgoingAmount), paymentHash, randomKey(), Left("Some invoice"), CltvExpiryDelta(18), extraHops = hints, paymentMetadata = Some(hex"123456"))
assert(!pr.features.allowMultiPart) assert(!pr.features.allowMultiPart)
val incomingPayments = incomingMultiPart.map(incoming => incoming.copy(innerPayload = PaymentOnion.createNodeRelayToNonTrampolinePayload( val incomingPayments = incomingMultiPart.map(incoming => incoming.copy(innerPayload = PaymentOnion.createNodeRelayToNonTrampolinePayload(
incoming.innerPayload.amountToForward, incoming.innerPayload.amountToForward, outgoingExpiry, outgoingNodeId, pr incoming.innerPayload.amountToForward, incoming.innerPayload.amountToForward, outgoingExpiry, outgoingNodeId, pr
@ -620,6 +622,7 @@ class NodeRelayerSpec extends ScalaTestWithActorTestKit(ConfigFactory.load("appl
val outgoingPayment = mockPayFSM.expectMessageType[SendPaymentToNode] val outgoingPayment = mockPayFSM.expectMessageType[SendPaymentToNode]
assert(outgoingPayment.finalPayload.amount === outgoingAmount) assert(outgoingPayment.finalPayload.amount === outgoingAmount)
assert(outgoingPayment.finalPayload.expiry === outgoingExpiry) assert(outgoingPayment.finalPayload.expiry === outgoingExpiry)
assert(outgoingPayment.finalPayload.paymentMetadata === pr.paymentMetadata) // we should use the provided metadata
assert(outgoingPayment.targetNodeId === outgoingNodeId) assert(outgoingPayment.targetNodeId === outgoingNodeId)
assert(outgoingPayment.assistedRoutes === hints) assert(outgoingPayment.assistedRoutes === hints)
// those are adapters for pay-fsm messages // those are adapters for pay-fsm messages
@ -718,9 +721,9 @@ object NodeRelayerSpec {
def createValidIncomingPacket(amountIn: MilliSatoshi, totalAmountIn: MilliSatoshi, expiryIn: CltvExpiry, amountOut: MilliSatoshi, expiryOut: CltvExpiry): IncomingPaymentPacket.NodeRelayPacket = { def createValidIncomingPacket(amountIn: MilliSatoshi, totalAmountIn: MilliSatoshi, expiryIn: CltvExpiry, amountOut: MilliSatoshi, expiryOut: CltvExpiry): IncomingPaymentPacket.NodeRelayPacket = {
val outerPayload = if (amountIn == totalAmountIn) { val outerPayload = if (amountIn == totalAmountIn) {
PaymentOnion.createSinglePartPayload(amountIn, expiryIn, incomingSecret) PaymentOnion.createSinglePartPayload(amountIn, expiryIn, incomingSecret, None)
} else { } else {
PaymentOnion.createMultiPartPayload(amountIn, totalAmountIn, expiryIn, incomingSecret) PaymentOnion.createMultiPartPayload(amountIn, totalAmountIn, expiryIn, incomingSecret, None)
} }
IncomingPaymentPacket.NodeRelayPacket( IncomingPaymentPacket.NodeRelayPacket(
UpdateAddHtlc(randomBytes32(), Random.nextInt(100), amountIn, paymentHash, expiryIn, TestConstants.emptyOnionPacket), UpdateAddHtlc(randomBytes32(), Random.nextInt(100), amountIn, paymentHash, expiryIn, TestConstants.emptyOnionPacket),
@ -734,7 +737,7 @@ object NodeRelayerSpec {
val amountIn = incomingAmount / 2 val amountIn = incomingAmount / 2
IncomingPaymentPacket.NodeRelayPacket( IncomingPaymentPacket.NodeRelayPacket(
UpdateAddHtlc(randomBytes32(), Random.nextInt(100), amountIn, paymentHash, expiryIn, TestConstants.emptyOnionPacket), UpdateAddHtlc(randomBytes32(), Random.nextInt(100), amountIn, paymentHash, expiryIn, TestConstants.emptyOnionPacket),
PaymentOnion.createMultiPartPayload(amountIn, incomingAmount, expiryIn, paymentSecret), PaymentOnion.createMultiPartPayload(amountIn, incomingAmount, expiryIn, paymentSecret, None),
PaymentOnion.createNodeRelayPayload(outgoingAmount, expiryOut, outgoingNodeId), PaymentOnion.createNodeRelayPayload(outgoingAmount, expiryOut, outgoingNodeId),
nextTrampolinePacket) nextTrampolinePacket)
} }

View file

@ -84,7 +84,7 @@ class RelayerSpec extends ScalaTestWithActorTestKit(ConfigFactory.load("applicat
} }
// we use this to build a valid onion // we use this to build a valid onion
val (cmd, _) = buildCommand(ActorRef.noSender, Upstream.Local(UUID.randomUUID()), paymentHash, hops, PaymentOnion.createSinglePartPayload(finalAmount, finalExpiry, paymentSecret)) val (cmd, _) = buildCommand(ActorRef.noSender, Upstream.Local(UUID.randomUUID()), paymentHash, hops, PaymentOnion.createSinglePartPayload(finalAmount, finalExpiry, paymentSecret, None))
// and then manually build an htlc // and then manually build an htlc
val add_ab = UpdateAddHtlc(channelId = randomBytes32(), id = 123456, cmd.amount, cmd.paymentHash, cmd.cltvExpiry, cmd.onion) val add_ab = UpdateAddHtlc(channelId = randomBytes32(), id = 123456, cmd.amount, cmd.paymentHash, cmd.cltvExpiry, cmd.onion)
relayer ! RelayForward(add_ab) relayer ! RelayForward(add_ab)
@ -94,14 +94,14 @@ class RelayerSpec extends ScalaTestWithActorTestKit(ConfigFactory.load("applicat
test("relay an htlc-add at the final node to the payment handler") { f => test("relay an htlc-add at the final node to the payment handler") { f =>
import f._ import f._
val (cmd, _) = buildCommand(ActorRef.noSender, Upstream.Local(UUID.randomUUID()), paymentHash, hops.take(1), PaymentOnion.createSinglePartPayload(finalAmount, finalExpiry, paymentSecret)) val (cmd, _) = buildCommand(ActorRef.noSender, Upstream.Local(UUID.randomUUID()), paymentHash, hops.take(1), PaymentOnion.createSinglePartPayload(finalAmount, finalExpiry, paymentSecret, None))
val add_ab = UpdateAddHtlc(channelId = channelId_ab, id = 123456, cmd.amount, cmd.paymentHash, cmd.cltvExpiry, cmd.onion) val add_ab = UpdateAddHtlc(channelId = channelId_ab, id = 123456, cmd.amount, cmd.paymentHash, cmd.cltvExpiry, cmd.onion)
relayer ! RelayForward(add_ab) relayer ! RelayForward(add_ab)
val fp = paymentHandler.expectMessageType[FinalPacket] val fp = paymentHandler.expectMessageType[FinalPacket]
assert(fp.add === add_ab) assert(fp.add === add_ab)
assert(fp.payload === PaymentOnion.createSinglePartPayload(finalAmount, finalExpiry, paymentSecret)) assert(fp.payload === PaymentOnion.createSinglePartPayload(finalAmount, finalExpiry, paymentSecret, None))
register.expectNoMessage(50 millis) register.expectNoMessage(50 millis)
} }
@ -114,7 +114,7 @@ class RelayerSpec extends ScalaTestWithActorTestKit(ConfigFactory.load("applicat
// We simulate a payment split between multiple trampoline routes. // We simulate a payment split between multiple trampoline routes.
val totalAmount = finalAmount * 3 val totalAmount = finalAmount * 3
val trampolineHops = NodeHop(a, b, channelUpdate_ab.cltvExpiryDelta, 0 msat) :: Nil val trampolineHops = NodeHop(a, b, channelUpdate_ab.cltvExpiryDelta, 0 msat) :: Nil
val (trampolineAmount, trampolineExpiry, trampolineOnion) = OutgoingPaymentPacket.buildTrampolinePacket(paymentHash, trampolineHops, PaymentOnion.createMultiPartPayload(finalAmount, totalAmount, finalExpiry, paymentSecret)) val (trampolineAmount, trampolineExpiry, trampolineOnion) = OutgoingPaymentPacket.buildTrampolinePacket(paymentHash, trampolineHops, PaymentOnion.createMultiPartPayload(finalAmount, totalAmount, finalExpiry, paymentSecret, None))
assert(trampolineAmount === finalAmount) assert(trampolineAmount === finalAmount)
assert(trampolineExpiry === finalExpiry) assert(trampolineExpiry === finalExpiry)
val (cmd, _) = buildCommand(ActorRef.noSender, Upstream.Local(UUID.randomUUID()), paymentHash, ChannelHop(a, b, channelUpdate_ab) :: Nil, PaymentOnion.createTrampolinePayload(trampolineAmount, trampolineAmount, trampolineExpiry, randomBytes32(), trampolineOnion.packet)) val (cmd, _) = buildCommand(ActorRef.noSender, Upstream.Local(UUID.randomUUID()), paymentHash, ChannelHop(a, b, channelUpdate_ab) :: Nil, PaymentOnion.createTrampolinePayload(trampolineAmount, trampolineAmount, trampolineExpiry, randomBytes32(), trampolineOnion.packet))
@ -138,7 +138,7 @@ class RelayerSpec extends ScalaTestWithActorTestKit(ConfigFactory.load("applicat
import f._ import f._
// we use this to build a valid onion // we use this to build a valid onion
val (cmd, _) = buildCommand(ActorRef.noSender, Upstream.Local(UUID.randomUUID()), paymentHash, hops, PaymentOnion.createSinglePartPayload(finalAmount, finalExpiry, paymentSecret)) val (cmd, _) = buildCommand(ActorRef.noSender, Upstream.Local(UUID.randomUUID()), paymentHash, hops, PaymentOnion.createSinglePartPayload(finalAmount, finalExpiry, paymentSecret, None))
// and then manually build an htlc with an invalid onion (hmac) // and then manually build an htlc with an invalid onion (hmac)
val add_ab = UpdateAddHtlc(channelId = channelId_ab, id = 123456, cmd.amount, cmd.paymentHash, cmd.cltvExpiry, cmd.onion.copy(hmac = cmd.onion.hmac.reverse)) val add_ab = UpdateAddHtlc(channelId = channelId_ab, id = 123456, cmd.amount, cmd.paymentHash, cmd.cltvExpiry, cmd.onion.copy(hmac = cmd.onion.hmac.reverse))
@ -159,7 +159,7 @@ class RelayerSpec extends ScalaTestWithActorTestKit(ConfigFactory.load("applicat
// we use this to build a valid trampoline onion inside a normal onion // we use this to build a valid trampoline onion inside a normal onion
val trampolineHops = NodeHop(a, b, channelUpdate_ab.cltvExpiryDelta, 0 msat) :: NodeHop(b, c, channelUpdate_bc.cltvExpiryDelta, fee_b) :: Nil val trampolineHops = NodeHop(a, b, channelUpdate_ab.cltvExpiryDelta, 0 msat) :: NodeHop(b, c, channelUpdate_bc.cltvExpiryDelta, fee_b) :: Nil
val (trampolineAmount, trampolineExpiry, trampolineOnion) = OutgoingPaymentPacket.buildTrampolinePacket(paymentHash, trampolineHops, PaymentOnion.createSinglePartPayload(finalAmount, finalExpiry, paymentSecret)) val (trampolineAmount, trampolineExpiry, trampolineOnion) = OutgoingPaymentPacket.buildTrampolinePacket(paymentHash, trampolineHops, PaymentOnion.createSinglePartPayload(finalAmount, finalExpiry, paymentSecret, None))
val (cmd, _) = buildCommand(ActorRef.noSender, Upstream.Local(UUID.randomUUID()), paymentHash, ChannelHop(a, b, channelUpdate_ab) :: Nil, PaymentOnion.createTrampolinePayload(trampolineAmount, trampolineAmount, trampolineExpiry, randomBytes32(), trampolineOnion.packet)) val (cmd, _) = buildCommand(ActorRef.noSender, Upstream.Local(UUID.randomUUID()), paymentHash, ChannelHop(a, b, channelUpdate_ab) :: Nil, PaymentOnion.createTrampolinePayload(trampolineAmount, trampolineAmount, trampolineExpiry, randomBytes32(), trampolineOnion.packet))
// and then manually build an htlc // and then manually build an htlc

View file

@ -53,8 +53,26 @@ class AnnouncementsSpec extends AnyFunSuite {
} }
test("create valid signed node announcement") { test("create valid signed node announcement") {
val ann = makeNodeAnnouncement(Alice.nodeParams.privateKey, Alice.nodeParams.alias, Alice.nodeParams.color, Alice.nodeParams.publicAddresses, Alice.nodeParams.features) val features = Features(
assert(ann.features.hasFeature(Features.VariableLengthOnion, Some(FeatureSupport.Mandatory))) Features.OptionDataLossProtect -> FeatureSupport.Optional,
Features.InitialRoutingSync -> FeatureSupport.Optional,
Features.ChannelRangeQueries -> FeatureSupport.Optional,
Features.ChannelRangeQueriesExtended -> FeatureSupport.Optional,
Features.VariableLengthOnion -> FeatureSupport.Mandatory,
Features.PaymentSecret -> FeatureSupport.Mandatory,
Features.BasicMultiPartPayment -> FeatureSupport.Optional,
Features.PaymentMetadata -> FeatureSupport.Optional,
)
val ann = makeNodeAnnouncement(Alice.nodeParams.privateKey, Alice.nodeParams.alias, Alice.nodeParams.color, Alice.nodeParams.publicAddresses, features)
// Features should be filtered to only include node_announcement related features.
assert(ann.features === Features(
Features.OptionDataLossProtect -> FeatureSupport.Optional,
Features.ChannelRangeQueries -> FeatureSupport.Optional,
Features.ChannelRangeQueriesExtended -> FeatureSupport.Optional,
Features.VariableLengthOnion -> FeatureSupport.Mandatory,
Features.PaymentSecret -> FeatureSupport.Mandatory,
Features.BasicMultiPartPayment -> FeatureSupport.Optional,
))
assert(checkSig(ann)) assert(checkSig(ann))
assert(checkSig(ann.copy(timestamp = 153 unixsec)) === false) assert(checkSig(ann.copy(timestamp = 153 unixsec)) === false)
} }