mirror of
https://github.com/ACINQ/eclair.git
synced 2025-02-23 06:35:11 +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:
parent
27579a5786
commit
6e88532d18
35 changed files with 467 additions and 224 deletions
|
@ -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.
|
||||
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
|
||||
|
||||
#### Timestamps
|
||||
|
|
|
@ -58,6 +58,7 @@ eclair {
|
|||
option_shutdown_anysegwit = optional
|
||||
option_onion_messages = disabled
|
||||
option_channel_type = optional
|
||||
option_payment_metadata = optional
|
||||
trampoline_payment = disabled
|
||||
keysend = disabled
|
||||
}
|
||||
|
|
|
@ -34,6 +34,8 @@ object FeatureSupport {
|
|||
|
||||
trait Feature {
|
||||
|
||||
this: FeatureScope =>
|
||||
|
||||
def rfcName: String
|
||||
def mandatory: Int
|
||||
def optional: Int = mandatory + 1
|
||||
|
@ -46,6 +48,15 @@ trait Feature {
|
|||
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
|
||||
|
||||
case class UnknownFeature(bitIndex: Int)
|
||||
|
@ -71,6 +82,13 @@ case class Features(activated: Map[Feature, FeatureSupport], unknown: Set[Unknow
|
|||
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 = {
|
||||
val activatedFeatureBytes = toByteVectorFromIndex(activated.map { case (feature, support) => feature.supportBit(support) }.toSet)
|
||||
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 mandatory = 0
|
||||
}
|
||||
|
||||
case object InitialRoutingSync extends Feature {
|
||||
case object InitialRoutingSync extends Feature with InitFeature {
|
||||
val rfcName = "initial_routing_sync"
|
||||
// reserved but not used as per lightningnetwork/lightning-rfc/pull/178
|
||||
val mandatory = 2
|
||||
}
|
||||
|
||||
case object OptionUpfrontShutdownScript extends Feature {
|
||||
case object OptionUpfrontShutdownScript extends Feature with InitFeature with NodeFeature {
|
||||
val rfcName = "option_upfront_shutdown_script"
|
||||
val mandatory = 4
|
||||
}
|
||||
|
||||
case object ChannelRangeQueries extends Feature {
|
||||
case object ChannelRangeQueries extends Feature with InitFeature with NodeFeature {
|
||||
val rfcName = "gossip_queries"
|
||||
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 mandatory = 8
|
||||
}
|
||||
|
||||
case object ChannelRangeQueriesExtended extends Feature {
|
||||
case object ChannelRangeQueriesExtended extends Feature with InitFeature with NodeFeature {
|
||||
val rfcName = "gossip_queries_ex"
|
||||
val mandatory = 10
|
||||
}
|
||||
|
||||
case object StaticRemoteKey extends Feature {
|
||||
case object StaticRemoteKey extends Feature with InitFeature with NodeFeature {
|
||||
val rfcName = "option_static_remotekey"
|
||||
val mandatory = 12
|
||||
}
|
||||
|
||||
case object PaymentSecret extends Feature {
|
||||
case object PaymentSecret extends Feature with InitFeature with NodeFeature with InvoiceFeature {
|
||||
val rfcName = "payment_secret"
|
||||
val mandatory = 14
|
||||
}
|
||||
|
||||
case object BasicMultiPartPayment extends Feature {
|
||||
case object BasicMultiPartPayment extends Feature with InitFeature with NodeFeature with InvoiceFeature {
|
||||
val rfcName = "basic_mpp"
|
||||
val mandatory = 16
|
||||
}
|
||||
|
||||
case object Wumbo extends Feature {
|
||||
case object Wumbo extends Feature with InitFeature with NodeFeature {
|
||||
val rfcName = "option_support_large_channel"
|
||||
val mandatory = 18
|
||||
}
|
||||
|
||||
case object AnchorOutputs extends Feature {
|
||||
case object AnchorOutputs extends Feature with InitFeature with NodeFeature {
|
||||
val rfcName = "option_anchor_outputs"
|
||||
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 mandatory = 22
|
||||
}
|
||||
|
||||
case object ShutdownAnySegwit extends Feature {
|
||||
case object ShutdownAnySegwit extends Feature with InitFeature with NodeFeature {
|
||||
val rfcName = "option_shutdown_anysegwit"
|
||||
val mandatory = 26
|
||||
}
|
||||
|
||||
case object OnionMessages extends Feature {
|
||||
case object OnionMessages extends Feature with InitFeature with NodeFeature {
|
||||
val rfcName = "option_onion_messages"
|
||||
val mandatory = 38
|
||||
}
|
||||
|
||||
case object ChannelType extends Feature {
|
||||
case object ChannelType extends Feature with InitFeature with NodeFeature {
|
||||
val rfcName = "option_channel_type"
|
||||
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)
|
||||
// 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`.
|
||||
case object TrampolinePayment extends Feature {
|
||||
case object TrampolinePayment extends Feature with InitFeature with NodeFeature with InvoiceFeature {
|
||||
val rfcName = "trampoline_payment"
|
||||
val mandatory = 50
|
||||
}
|
||||
|
||||
case object KeySend extends Feature {
|
||||
case object KeySend extends Feature with NodeFeature {
|
||||
val rfcName = "keysend"
|
||||
val mandatory = 54
|
||||
}
|
||||
|
@ -242,6 +265,7 @@ object Features {
|
|||
ShutdownAnySegwit,
|
||||
OnionMessages,
|
||||
ChannelType,
|
||||
PaymentMetadata,
|
||||
TrampolinePayment,
|
||||
KeySend
|
||||
)
|
||||
|
|
|
@ -110,7 +110,8 @@ case class NodeParams(nodeKeyManager: NodeKeyManager,
|
|||
|
||||
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 {
|
||||
|
|
|
@ -92,7 +92,7 @@ class Switchboard(nodeParams: NodeParams, peerFactory: Switchboard.PeerFactory)
|
|||
case authenticated: PeerConnection.Authenticated =>
|
||||
// if this is an incoming connection, we might not yet have created the peer
|
||||
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
|
||||
val doSync = nodeParams.syncWhitelist.contains(authenticated.remoteNodeId) || (nodeParams.syncWhitelist.isEmpty && peersWithChannels.contains(authenticated.remoteNodeId))
|
||||
authenticated.peerConnection ! PeerConnection.InitializeConnection(peer, nodeParams.chainHash, features, doSync)
|
||||
|
|
|
@ -335,6 +335,7 @@ object PaymentRequestSerializer extends MinimalSerializer({
|
|||
FeatureSupportSerializer +
|
||||
UnknownFeatureSerializer
|
||||
))
|
||||
val paymentMetadata = p.paymentMetadata.map(m => JField("paymentMetadata", JString(m.toHex))).toSeq
|
||||
val routingInfo = JField("routingInfo", Extraction.decompose(p.routingInfo)(
|
||||
DefaultFormats +
|
||||
ByteVector32Serializer +
|
||||
|
@ -344,12 +345,14 @@ object PaymentRequestSerializer extends MinimalSerializer({
|
|||
MilliSatoshiSerializer +
|
||||
CltvExpiryDeltaSerializer
|
||||
))
|
||||
val fieldList = List(JField("prefix", JString(p.prefix)),
|
||||
val fieldList = List(
|
||||
JField("prefix", JString(p.prefix)),
|
||||
JField("timestamp", JLong(p.timestamp.toLong)),
|
||||
JField("nodeId", JString(p.nodeId.toString())),
|
||||
JField("serialized", JString(PaymentRequest.write(p))),
|
||||
p.description.fold(string => JField("description", JString(string)), hash => JField("descriptionHash", JString(hash.toHex))),
|
||||
JField("paymentHash", JString(p.paymentHash.toString()))) ++
|
||||
paymentMetadata ++
|
||||
expiry ++
|
||||
minFinalCltvExpiry ++
|
||||
amount :+
|
||||
|
|
|
@ -27,6 +27,7 @@ object Monitoring {
|
|||
val PaymentAmount = Kamon.histogram("payment.amount", "Payment amount (satoshi)")
|
||||
val PaymentFees = Kamon.histogram("payment.fees", "Payment fees (satoshi)")
|
||||
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 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")
|
||||
|
@ -71,6 +72,7 @@ object Monitoring {
|
|||
val PaymentId = "paymentId"
|
||||
val ParentId = "parentPaymentId"
|
||||
val PaymentHash = "paymentHash"
|
||||
val PaymentMetadataIncluded = "paymentMetadataIncluded"
|
||||
|
||||
val Amount = "amount"
|
||||
val TotalAmount = "totalAmount"
|
||||
|
|
|
@ -25,6 +25,7 @@ import fr.acinq.eclair.router.Announcements
|
|||
import fr.acinq.eclair.router.Router.{ChannelDesc, ChannelHop, Hop, Ignore}
|
||||
import fr.acinq.eclair.wire.protocol.{ChannelDisabled, ChannelUpdate, Node, TemporaryChannelFailure}
|
||||
import fr.acinq.eclair.{MilliSatoshi, ShortChannelId, TimestampMilli}
|
||||
import scodec.bits.ByteVector
|
||||
|
||||
import java.util.UUID
|
||||
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
|
||||
|
||||
sealed trait PaymentFailure {
|
||||
|
|
|
@ -117,7 +117,7 @@ object IncomingPaymentPacket {
|
|||
} else {
|
||||
// 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).
|
||||
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
|
||||
*/
|
||||
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) =>
|
||||
// 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) {
|
||||
PaymentOnion.createNodeRelayToNonTrampolinePayload(finalPayload.amount, finalPayload.totalAmount, finalPayload.expiry, hop.nextNodeId, invoice)
|
||||
} else {
|
||||
|
|
|
@ -19,12 +19,11 @@ package fr.acinq.eclair.payment
|
|||
import fr.acinq.bitcoin.Crypto.{PrivateKey, PublicKey}
|
||||
import fr.acinq.bitcoin.{Base58, Base58Check, Bech32, Block, ByteVector32, ByteVector64, Crypto}
|
||||
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.codecs.{list, ubyte}
|
||||
import scodec.{Codec, Err}
|
||||
|
||||
import scala.concurrent.duration._
|
||||
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)
|
||||
}.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, ..
|
||||
*/
|
||||
|
@ -126,6 +130,11 @@ object PaymentRequest {
|
|||
Block.LivenetGenesisBlock.hash -> "lnbc"
|
||||
)
|
||||
|
||||
val defaultFeatures: Map[Feature with InvoiceFeature, FeatureSupport] = Map(
|
||||
Features.VariableLengthOnion -> FeatureSupport.Mandatory,
|
||||
Features.PaymentSecret -> FeatureSupport.Mandatory,
|
||||
)
|
||||
|
||||
def apply(chainHash: ByteVector32,
|
||||
amount: Option[MilliSatoshi],
|
||||
paymentHash: ByteVector32,
|
||||
|
@ -137,7 +146,8 @@ object PaymentRequest {
|
|||
extraHops: List[List[ExtraHop]] = Nil,
|
||||
timestamp: TimestampSecond = TimestampSecond.now(),
|
||||
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")
|
||||
val prefix = prefixes(chainHash)
|
||||
val tags = {
|
||||
|
@ -145,6 +155,7 @@ object PaymentRequest {
|
|||
Some(PaymentHash(paymentHash)),
|
||||
Some(description.fold(Description, DescriptionHash)),
|
||||
Some(PaymentSecret(paymentSecret)),
|
||||
paymentMetadata.map(PaymentMetadata),
|
||||
fallbackAddress.map(FallbackAddress(_)),
|
||||
expirySeconds.map(Expiry(_)),
|
||||
Some(MinFinalCltvExpiry(minFinalCltvExpiryDelta.toInt)),
|
||||
|
@ -193,7 +204,6 @@ object PaymentRequest {
|
|||
case class InvalidTag23(data: BitVector) extends InvalidTaggedField
|
||||
case class UnknownTag25(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 UnknownTag29(data: BitVector) extends UnknownTaggedField
|
||||
case class UnknownTag30(data: BitVector) extends UnknownTaggedField
|
||||
|
@ -229,6 +239,11 @@ object PaymentRequest {
|
|||
*/
|
||||
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
|
||||
*/
|
||||
|
@ -355,8 +370,8 @@ object PaymentRequest {
|
|||
}
|
||||
|
||||
object PaymentRequestFeatures {
|
||||
def apply(features: Int*): PaymentRequestFeatures = PaymentRequestFeatures(long2bits(features.foldLeft(0L) {
|
||||
case (current, feature) => current + (1L << feature)
|
||||
def apply(features: Map[Feature with InvoiceFeature, FeatureSupport]): PaymentRequestFeatures = PaymentRequestFeatures(long2bits(features.foldLeft(0L) {
|
||||
case (current, (feature, support)) => current + (1L << feature.supportBit(support))
|
||||
}))
|
||||
}
|
||||
|
||||
|
@ -429,7 +444,7 @@ object PaymentRequest {
|
|||
.typecase(24, dataCodec(bits).as[MinFinalCltvExpiry])
|
||||
.typecase(25, dataCodec(bits).as[UnknownTag25])
|
||||
.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(29, dataCodec(bits).as[UnknownTag29])
|
||||
.typecase(30, dataCodec(bits).as[UnknownTag30])
|
||||
|
|
|
@ -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.payment.Monitoring.{Metrics, Tags}
|
||||
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.{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}
|
||||
|
||||
|
@ -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()
|
||||
PendingCommandsDb.safeSend(register, nodeParams.db.pendingCommands, p.add.channelId, cmdFail)
|
||||
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 {
|
||||
case Some((_, handler)) =>
|
||||
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 paymentHash = Crypto.sha256(paymentPreimage)
|
||||
val desc = Left("Donation")
|
||||
val features = if (nodeParams.features.hasFeature(Features.BasicMultiPartPayment)) {
|
||||
PaymentRequestFeatures(Features.BasicMultiPartPayment.optional, Features.PaymentSecret.mandatory, Features.VariableLengthOnion.mandatory)
|
||||
val features: Map[Feature with InvoiceFeature, FeatureSupport] = if (nodeParams.features.hasFeature(Features.BasicMultiPartPayment)) {
|
||||
Map(Features.BasicMultiPartPayment -> FeatureSupport.Optional, Features.PaymentSecret -> FeatureSupport.Mandatory, Features.VariableLengthOnion -> FeatureSupport.Mandatory)
|
||||
} 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
|
||||
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)
|
||||
db.addIncomingPayment(paymentRequest, paymentPreimage, paymentType = PaymentType.KeySend)
|
||||
ctx.self ! p
|
||||
|
@ -223,14 +229,25 @@ object MultiPartHandler {
|
|||
val paymentPreimage = paymentPreimage_opt.getOrElse(randomBytes32())
|
||||
val paymentHash = Crypto.sha256(paymentPreimage)
|
||||
val expirySeconds = expirySeconds_opt.getOrElse(nodeParams.paymentRequestExpiry.toSeconds)
|
||||
val features = {
|
||||
val f1 = Seq(Features.PaymentSecret.mandatory, Features.VariableLengthOnion.mandatory)
|
||||
val allowMultiPart = nodeParams.features.hasFeature(Features.BasicMultiPartPayment)
|
||||
val f2 = if (allowMultiPart) Seq(Features.BasicMultiPartPayment.optional) else Nil
|
||||
val f3 = if (nodeParams.enableTrampolinePayment) Seq(Features.TrampolinePayment.optional) else Nil
|
||||
PaymentRequest.PaymentRequestFeatures(f1 ++ f2 ++ f3: _*)
|
||||
val paymentMetadata = hex"2a"
|
||||
val invoiceFeatures = if (nodeParams.enableTrampolinePayment) {
|
||||
nodeParams.features.invoiceFeatures() + (Features.TrampolinePayment -> FeatureSupport.Optional)
|
||||
} else {
|
||||
nodeParams.features.invoiceFeatures()
|
||||
}
|
||||
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)
|
||||
nodeParams.db.payments.addIncomingPayment(paymentRequest, paymentPreimage, paymentType)
|
||||
paymentRequest
|
||||
|
|
|
@ -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
|
||||
if (Features(features).hasFeature(Features.BasicMultiPartPayment)) {
|
||||
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)
|
||||
payFSM ! payment
|
||||
payFSM
|
||||
} else {
|
||||
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 payFSM = outgoingPaymentFactory.spawnOutgoingPayFSM(context, paymentCfg, multiPart = false)
|
||||
payFSM ! payment
|
||||
|
@ -287,7 +287,7 @@ class NodeRelay private(nodeParams: NodeParams,
|
|||
context.log.debug("sending the payment to the next trampoline node")
|
||||
val payFSM = outgoingPaymentFactory.spawnOutgoingPayFSM(context, paymentCfg, multiPart = true)
|
||||
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
|
||||
}
|
||||
|
|
|
@ -32,6 +32,7 @@ import fr.acinq.eclair.payment.send.PaymentLifecycle.SendPaymentToRoute
|
|||
import fr.acinq.eclair.router.Router._
|
||||
import fr.acinq.eclair.wire.protocol._
|
||||
import fr.acinq.eclair.{CltvExpiry, FSMDiagnosticActorLogging, Logs, MilliSatoshi, MilliSatoshiLong, NodeParams, TimestampMilli}
|
||||
import scodec.bits.ByteVector
|
||||
|
||||
import java.util.UUID
|
||||
import java.util.concurrent.TimeUnit
|
||||
|
@ -301,16 +302,17 @@ object MultiPartPaymentLifecycle {
|
|||
* Send a payment to a given node. The payment may be split into multiple child payments, for which a path-finding
|
||||
* algorithm will run to find suitable payment routes.
|
||||
*
|
||||
* @param paymentSecret payment secret to protect against probing (usually from a Bolt 11 invoice).
|
||||
* @param targetNodeId target node (may be the final recipient when using source-routing, or the first trampoline
|
||||
* node when using trampoline).
|
||||
* @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 maxAttempts maximum number of retries.
|
||||
* @param assistedRoutes routing hints (usually from a Bolt 11 invoice).
|
||||
* @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 userCustomTlvs when provided, additional user-defined custom tlvs that will be added to the onion sent to the target node.
|
||||
* @param paymentSecret payment secret to protect against probing (usually from a Bolt 11 invoice).
|
||||
* @param targetNodeId target node (may be the final recipient when using source-routing, or the first trampoline
|
||||
* node when using trampoline).
|
||||
* @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 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 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 userCustomTlvs when provided, additional user-defined custom tlvs that will be added to the onion sent to the target node.
|
||||
*/
|
||||
case class SendMultiPartPayment(replyTo: ActorRef,
|
||||
paymentSecret: ByteVector32,
|
||||
|
@ -318,6 +320,7 @@ object MultiPartPaymentLifecycle {
|
|||
totalAmount: MilliSatoshi,
|
||||
targetExpiry: CltvExpiry,
|
||||
maxAttempts: Int,
|
||||
paymentMetadata: Option[ByteVector],
|
||||
assistedRoutes: Seq[Seq[ExtraHop]] = Nil,
|
||||
routeParams: RouteParams,
|
||||
additionalTlvs: Seq[OnionPaymentPayloadTlv] = Nil,
|
||||
|
@ -400,7 +403,7 @@ object MultiPartPaymentLifecycle {
|
|||
Some(cfg.paymentContext))
|
||||
|
||||
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)
|
||||
}
|
||||
|
||||
|
|
|
@ -59,9 +59,9 @@ class PaymentInitiator(nodeParams: NodeParams, outgoingPaymentFactory: PaymentIn
|
|||
sender() ! PaymentFailed(paymentId, r.paymentHash, LocalFailure(r.recipientAmount, Nil, PaymentSecretMissing) :: Nil)
|
||||
case Some(paymentSecret) if r.paymentRequest.features.allowMultiPart && nodeParams.features.hasFeature(BasicMultiPartPayment) =>
|
||||
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) =>
|
||||
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)
|
||||
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))
|
||||
val payFsm = outgoingPaymentFactory.spawnOutgoingPayment(context, paymentCfg)
|
||||
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 =>
|
||||
sender() ! SendPaymentToRouteResponse(paymentId, parentPaymentId, None)
|
||||
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 _ =>
|
||||
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
|
||||
)
|
||||
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 {
|
||||
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).
|
||||
val (trampolineAmount, trampolineExpiry, trampolineOnion) = if (r.paymentRequest.features.allowTrampoline) {
|
||||
|
@ -175,7 +175,7 @@ class PaymentInitiator(nodeParams: NodeParams, outgoingPaymentFactory: PaymentIn
|
|||
val trampolineSecret = randomBytes32()
|
||||
val (trampolineAmount, trampolineExpiry, trampolineOnion) = buildTrampolinePayment(r, r.trampolineNodeId, trampolineFees, trampolineExpiryDelta)
|
||||
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)))
|
||||
}
|
||||
|
||||
}
|
||||
|
|
|
@ -76,7 +76,8 @@ object Announcements {
|
|||
case address@(_: Tor2) => (3, address)
|
||||
case address@(_: Tor3) => (4, address)
|
||||
}.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)
|
||||
NodeAnnouncement(
|
||||
signature = sig,
|
||||
|
@ -84,7 +85,7 @@ object Announcements {
|
|||
nodeId = nodeSecret.publicKey,
|
||||
rgbColor = color,
|
||||
alias = alias,
|
||||
features = features,
|
||||
features = nodeAnnouncementFeatures,
|
||||
addresses = sortedAddresses
|
||||
)
|
||||
}
|
||||
|
|
|
@ -155,6 +155,12 @@ object OnionPaymentPayloadTlv {
|
|||
/** Id of the next node. */
|
||||
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
|
||||
* because the final recipient doesn't support trampoline.
|
||||
|
@ -242,6 +248,7 @@ object PaymentOnion {
|
|||
val paymentSecret: ByteVector32
|
||||
val totalAmount: MilliSatoshi
|
||||
val paymentPreimage: Option[ByteVector32]
|
||||
val paymentMetadata: Option[ByteVector]
|
||||
}
|
||||
|
||||
case class RelayLegacyPayload(outgoingChannelId: ShortChannelId, amountToForward: MilliSatoshi, outgoingCltv: CltvExpiry) extends ChannelRelayPayload with LegacyFormat
|
||||
|
@ -267,6 +274,7 @@ object PaymentOnion {
|
|||
case totalAmount => totalAmount
|
||||
}).getOrElse(amountToForward)
|
||||
val paymentSecret = records.get[PaymentData].map(_.secret)
|
||||
val paymentMetadata = records.get[PaymentMetadata].map(_.data)
|
||||
val invoiceFeatures = records.get[InvoiceFeatures].map(_.features)
|
||||
val invoiceRoutingInfo = records.get[InvoiceRoutingInfo].map(_.extraHops)
|
||||
}
|
||||
|
@ -280,6 +288,7 @@ object PaymentOnion {
|
|||
case totalAmount => totalAmount
|
||||
}).getOrElse(amount)
|
||||
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 =
|
||||
|
@ -287,16 +296,37 @@ object PaymentOnion {
|
|||
|
||||
/** 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 = {
|
||||
val tlvs = Seq[OnionPaymentPayloadTlv](AmountToForward(amount), OutgoingCltv(expiry), OutgoingNodeId(targetNodeId), InvoiceFeatures(invoice.features.toByteVector), InvoiceRoutingInfo(invoice.routingInfo.toList.map(_.toList)))
|
||||
val tlvs2 = invoice.paymentSecret.map(s => tlvs :+ PaymentData(s, totalAmount)).getOrElse(tlvs)
|
||||
NodeRelayPayload(TlvStream(tlvs2))
|
||||
val tlvs = Seq(
|
||||
Some(AmountToForward(amount)),
|
||||
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 =
|
||||
FinalTlvPayload(TlvStream(Seq(AmountToForward(amount), OutgoingCltv(expiry), PaymentData(paymentSecret, amount)), userCustomTlvs))
|
||||
def createSinglePartPayload(amount: MilliSatoshi, expiry: CltvExpiry, paymentSecret: ByteVector32, paymentMetadata: Option[ByteVector], userCustomTlvs: Seq[GenericTlv] = Nil): FinalPayload = {
|
||||
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 =
|
||||
FinalTlvPayload(TlvStream(AmountToForward(amount) +: OutgoingCltv(expiry) +: PaymentData(paymentSecret, totalAmount) +: additionalTlvs, userCustomTlvs))
|
||||
def createMultiPartPayload(amount: MilliSatoshi, totalAmount: MilliSatoshi, expiry: CltvExpiry, paymentSecret: ByteVector32, paymentMetadata: Option[ByteVector], additionalTlvs: Seq[OnionPaymentPayloadTlv] = Nil, userCustomTlvs: Seq[GenericTlv] = Nil): FinalPayload = {
|
||||
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. */
|
||||
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 paymentMetadata: Codec[PaymentMetadata] = variableSizeBytesLong(varintoverflow, "payment_metadata" | bytes).as[PaymentMetadata]
|
||||
|
||||
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]
|
||||
|
@ -355,6 +387,7 @@ object PaymentOnionCodecs {
|
|||
.typecase(UInt64(8), paymentData)
|
||||
.typecase(UInt64(10), encryptedRecipientData)
|
||||
.typecase(UInt64(12), blindingPoint)
|
||||
.typecase(UInt64(16), paymentMetadata)
|
||||
// Types below aren't specified - use cautiously when deploying (be careful with backwards-compatibility).
|
||||
.typecase(UInt64(66097), invoiceFeatures)
|
||||
.typecase(UInt64(66098), outgoingNodeId)
|
||||
|
|
|
@ -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") {
|
||||
val testCases = Map(
|
||||
hex"" -> Features.empty,
|
||||
|
|
|
@ -181,10 +181,47 @@ class StartupSpec extends AnyFunSuite {
|
|||
)
|
||||
|
||||
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))
|
||||
}
|
||||
|
||||
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") {
|
||||
val perNodeConf = ConfigFactory.parseString(
|
||||
"""
|
||||
|
|
|
@ -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 mandatory = 50000
|
||||
}
|
||||
|
@ -106,7 +106,8 @@ object TestConstants {
|
|||
ChannelRangeQueriesExtended -> Optional,
|
||||
VariableLengthOnion -> Mandatory,
|
||||
PaymentSecret -> Mandatory,
|
||||
BasicMultiPartPayment -> Optional
|
||||
BasicMultiPartPayment -> Optional,
|
||||
PaymentMetadata -> Optional,
|
||||
),
|
||||
Set(UnknownFeature(TestFeature.optional))
|
||||
),
|
||||
|
@ -240,7 +241,8 @@ object TestConstants {
|
|||
ChannelRangeQueriesExtended -> Optional,
|
||||
VariableLengthOnion -> Mandatory,
|
||||
PaymentSecret -> Mandatory,
|
||||
BasicMultiPartPayment -> Optional
|
||||
BasicMultiPartPayment -> Optional,
|
||||
PaymentMetadata -> Optional,
|
||||
),
|
||||
pluginParams = Nil,
|
||||
overrideFeatures = Map.empty,
|
||||
|
|
|
@ -122,7 +122,7 @@ class FuzzySpec extends TestKitBaseClass with FixtureAnyFunSuiteLike with Channe
|
|||
// allow overpaying (no more than 2 times the required amount)
|
||||
val amount = requiredAmount + Random.nextInt(requiredAmount.toLong.toInt).msat
|
||||
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 =
|
||||
|
|
|
@ -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) = {
|
||||
val paymentHash: ByteVector32 = Crypto.sha256(paymentPreimage)
|
||||
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)
|
||||
}
|
||||
|
||||
|
|
|
@ -59,7 +59,7 @@ class ShutdownStateSpec extends TestKitBaseClass with FixtureAnyFunSuiteLike wit
|
|||
val h1 = Crypto.sha256(r1)
|
||||
val amount1 = 300000000 msat
|
||||
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
|
||||
sender.expectMsgType[RES_SUCCESS[CMD_ADD_HTLC]]
|
||||
val htlc1 = alice2bob.expectMsgType[UpdateAddHtlc]
|
||||
|
@ -69,7 +69,7 @@ class ShutdownStateSpec extends TestKitBaseClass with FixtureAnyFunSuiteLike wit
|
|||
val h2 = Crypto.sha256(r2)
|
||||
val amount2 = 200000000 msat
|
||||
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
|
||||
sender.expectMsgType[RES_SUCCESS[CMD_ADD_HTLC]]
|
||||
val htlc2 = alice2bob.expectMsgType[UpdateAddHtlc]
|
||||
|
|
|
@ -154,11 +154,15 @@ class PaymentIntegrationSpec extends IntegrationSpec {
|
|||
}
|
||||
|
||||
test("send an HTLC A->D") {
|
||||
val sender = TestProbe()
|
||||
val amountMsat = 4200000.msat
|
||||
val (sender, eventListener) = (TestProbe(), TestProbe())
|
||||
nodes("D").system.eventStream.subscribe(eventListener.ref, classOf[PaymentMetadataReceived])
|
||||
|
||||
// first we retrieve a payment hash from D
|
||||
val amountMsat = 4200000.msat
|
||||
sender.send(nodes("D").paymentHandler, ReceivePayment(Some(amountMsat), Left("1 coffee")))
|
||||
val pr = sender.expectMsgType[PaymentRequest]
|
||||
assert(pr.paymentMetadata.nonEmpty)
|
||||
|
||||
// then we make the actual payment
|
||||
sender.send(nodes("A").paymentInitiator, SendPaymentToNode(amountMsat, pr, fallbackFinalExpiryDelta = finalCltvExpiryDelta, routeParams = integrationTestRouteParams, maxAttempts = 1))
|
||||
val paymentId = sender.expectMsgType[UUID]
|
||||
|
@ -166,6 +170,7 @@ class PaymentIntegrationSpec extends IntegrationSpec {
|
|||
assert(Crypto.sha256(preimage) === pr.paymentHash)
|
||||
val ps = sender.expectMsgType[PaymentSent]
|
||||
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") {
|
||||
|
@ -503,12 +508,14 @@ class PaymentIntegrationSpec extends IntegrationSpec {
|
|||
|
||||
test("send a trampoline payment D->B (via trampoline C)") {
|
||||
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
|
||||
sender.send(nodes("B").paymentHandler, ReceivePayment(Some(amount), Left("trampoline-MPP is so #reckless")))
|
||||
val pr = sender.expectMsgType[PaymentRequest]
|
||||
assert(pr.features.allowMultiPart)
|
||||
assert(pr.features.allowTrampoline)
|
||||
assert(pr.paymentMetadata.nonEmpty)
|
||||
|
||||
val payment = SendTrampolinePayment(amount, pr, nodes("C").nodeParams.nodeId, Seq((350000 msat, CltvExpiryDelta(288))), routeParams = integrationTestRouteParams)
|
||||
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]))
|
||||
val Some(IncomingPayment(_, _, _, _, IncomingPaymentStatus.Received(receivedAmount, _))) = nodes("B").nodeParams.db.payments.getIncomingPayment(pr.paymentHash)
|
||||
assert(receivedAmount === amount)
|
||||
eventListener.expectMsg(PaymentMetadataReceived(pr.paymentHash, pr.paymentMetadata.get))
|
||||
|
||||
awaitCond({
|
||||
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)") {
|
||||
// The A -> B channel is not announced.
|
||||
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())
|
||||
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)))
|
||||
|
@ -558,6 +567,7 @@ class PaymentIntegrationSpec extends IntegrationSpec {
|
|||
val pr = sender.expectMsgType[PaymentRequest]
|
||||
assert(pr.features.allowMultiPart)
|
||||
assert(!pr.features.allowTrampoline)
|
||||
assert(pr.paymentMetadata.nonEmpty)
|
||||
|
||||
val payment = SendTrampolinePayment(amount, pr, nodes("C").nodeParams.nodeId, Seq((1000000 msat, CltvExpiryDelta(432))), routeParams = integrationTestRouteParams)
|
||||
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]))
|
||||
val Some(IncomingPayment(_, _, _, _, IncomingPaymentStatus.Received(receivedAmount, _))) = nodes("A").nodeParams.db.payments.getIncomingPayment(pr.paymentHash)
|
||||
assert(receivedAmount === amount)
|
||||
eventListener.expectMsg(PaymentMetadataReceived(pr.paymentHash, pr.paymentMetadata.get))
|
||||
|
||||
awaitCond({
|
||||
val relayed = nodes("C").nodeParams.db.audit.listRelayed(start, TimestampMilli.now()).filter(_.paymentHash == pr.paymentHash)
|
||||
|
|
|
@ -77,7 +77,7 @@ class SwitchboardSpec extends TestKitBaseClass with AnyFunSuiteLike {
|
|||
val nodeParams = Alice.nodeParams.copy(syncWhitelist = Set.empty)
|
||||
val remoteNodeId = ChannelCodecsSpec.normal.commitments.remoteParams.nodeId
|
||||
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") {
|
||||
|
@ -91,7 +91,7 @@ class SwitchboardSpec extends TestKitBaseClass with AnyFunSuiteLike {
|
|||
switchboard ! PeerConnection.Authenticated(peerConnection.ref, remoteNodeId)
|
||||
val initConnection1 = peerConnection.expectMsgType[PeerConnection.InitializeConnection]
|
||||
assert(initConnection1.chainHash === nodeParams.chainHash)
|
||||
assert(initConnection1.features === nodeParams.features)
|
||||
assert(initConnection1.features === nodeParams.features.initFeatures())
|
||||
assert(initConnection1.doSync)
|
||||
|
||||
// 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)
|
||||
val initConnection2 = peerConnection.expectMsgType[PeerConnection.InitializeConnection]
|
||||
assert(initConnection2.chainHash === nodeParams.chainHash)
|
||||
assert(initConnection2.features === nodeParams.features)
|
||||
assert(initConnection2.features === nodeParams.features.initFeatures())
|
||||
assert(!initConnection2.doSync)
|
||||
}
|
||||
|
||||
test("don't sync if no whitelist is defined and peer does not have channels") {
|
||||
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") {
|
||||
val remoteNodeId = 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") {
|
||||
val nodeParams = Alice.nodeParams.copy(syncWhitelist = Set(randomKey().publicKey, randomKey().publicKey, randomKey().publicKey))
|
||||
val remoteNodeId = ChannelCodecsSpec.normal.commitments.remoteParams.nodeId
|
||||
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") {
|
||||
|
|
|
@ -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}]]}"""
|
||||
}
|
||||
|
||||
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") {
|
||||
val gb = GlobalBalance(
|
||||
onChain = CheckBalance.CorrectedOnChainBalance(Btc(0.4), Btc(0.05)),
|
||||
|
|
|
@ -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 org.scalatest.Outcome
|
||||
import org.scalatest.funsuite.FixtureAnyFunSuiteLike
|
||||
import scodec.bits.HexStringSyntax
|
||||
|
||||
import scala.collection.immutable.Queue
|
||||
import scala.concurrent.duration._
|
||||
|
@ -93,7 +94,7 @@ class MultiPartHandlerSpec extends TestKitBaseClass with FixtureAnyFunSuiteLike
|
|||
assert(Crypto.sha256(incoming.get.paymentPreimage) === pr.paymentHash)
|
||||
|
||||
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]]
|
||||
|
||||
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)
|
||||
|
||||
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]]
|
||||
|
||||
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)
|
||||
|
||||
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
|
||||
assert(cmd.reason == Right(IncorrectOrUnknownPaymentDetails(amountMsat, nodeParams.currentBlockHeight)))
|
||||
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.allowTrampoline)
|
||||
}
|
||||
|
||||
{
|
||||
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")))
|
||||
|
@ -199,7 +199,6 @@ class MultiPartHandlerSpec extends TestKitBaseClass with FixtureAnyFunSuiteLike
|
|||
assert(pr.features.allowMultiPart)
|
||||
assert(!pr.features.allowTrampoline)
|
||||
}
|
||||
|
||||
{
|
||||
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")))
|
||||
|
@ -207,7 +206,6 @@ class MultiPartHandlerSpec extends TestKitBaseClass with FixtureAnyFunSuiteLike
|
|||
assert(!pr.features.allowMultiPart)
|
||||
assert(pr.features.allowTrampoline)
|
||||
}
|
||||
|
||||
{
|
||||
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")))
|
||||
|
@ -244,7 +242,7 @@ class MultiPartHandlerSpec extends TestKitBaseClass with FixtureAnyFunSuiteLike
|
|||
assert(pr.isExpired)
|
||||
|
||||
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]]
|
||||
val Some(incoming) = nodeParams.db.payments.getIncomingPayment(pr.paymentHash)
|
||||
assert(incoming.paymentRequest.isExpired && incoming.status === IncomingPaymentStatus.Expired)
|
||||
|
@ -259,7 +257,7 @@ class MultiPartHandlerSpec extends TestKitBaseClass with FixtureAnyFunSuiteLike
|
|||
assert(pr.isExpired)
|
||||
|
||||
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
|
||||
assert(cmd.reason == Right(IncorrectOrUnknownPaymentDetails(1000 msat, nodeParams.currentBlockHeight)))
|
||||
val Some(incoming) = nodeParams.db.payments.getIncomingPayment(pr.paymentHash)
|
||||
|
@ -274,7 +272,7 @@ class MultiPartHandlerSpec extends TestKitBaseClass with FixtureAnyFunSuiteLike
|
|||
assert(!pr.features.allowMultiPart)
|
||||
|
||||
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
|
||||
assert(cmd.reason == Right(IncorrectOrUnknownPaymentDetails(1000 msat, nodeParams.currentBlockHeight)))
|
||||
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 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
|
||||
assert(cmd.reason == Right(IncorrectOrUnknownPaymentDetails(1000 msat, nodeParams.currentBlockHeight)))
|
||||
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)
|
||||
|
||||
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
|
||||
assert(cmd.reason == Right(IncorrectOrUnknownPaymentDetails(1000 msat, nodeParams.currentBlockHeight)))
|
||||
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)
|
||||
|
||||
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
|
||||
assert(cmd.reason == Right(IncorrectOrUnknownPaymentDetails(999 msat, nodeParams.currentBlockHeight)))
|
||||
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)
|
||||
|
||||
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
|
||||
assert(cmd.reason == Right(IncorrectOrUnknownPaymentDetails(2001 msat, nodeParams.currentBlockHeight)))
|
||||
assert(nodeParams.db.payments.getIncomingPayment(pr.paymentHash).get.status === IncomingPaymentStatus.Pending)
|
||||
|
@ -346,7 +344,7 @@ class MultiPartHandlerSpec extends TestKitBaseClass with FixtureAnyFunSuiteLike
|
|||
|
||||
// Invalid payment secret.
|
||||
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
|
||||
assert(cmd.reason == Right(IncorrectOrUnknownPaymentDetails(1000 msat, nodeParams.currentBlockHeight)))
|
||||
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")))
|
||||
val pr1 = f.sender.expectMsgType[PaymentRequest]
|
||||
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.
|
||||
f.sender.send(handler, ReceivePayment(Some(1500 msat), Left("1 slow latte")))
|
||||
val pr2 = f.sender.expectMsgType[PaymentRequest]
|
||||
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 {
|
||||
f.sender.send(handler, GetPendingPayments)
|
||||
|
@ -401,12 +399,12 @@ class MultiPartHandlerSpec extends TestKitBaseClass with FixtureAnyFunSuiteLike
|
|||
val pr = f.sender.expectMsgType[PaymentRequest]
|
||||
|
||||
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.
|
||||
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)
|
||||
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(
|
||||
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))
|
||||
|
||||
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)))
|
||||
awaitCond({
|
||||
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)
|
||||
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)
|
||||
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
|
||||
f.register.expectMsgAllOf(
|
||||
|
@ -524,7 +522,7 @@ class MultiPartHandlerSpec extends TestKitBaseClass with FixtureAnyFunSuiteLike
|
|||
assert(nodeParams.db.payments.getIncomingPayment(paymentHash) === None)
|
||||
|
||||
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
|
||||
assert(cmd.id === add.id)
|
||||
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)
|
||||
|
||||
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
|
||||
assert(cmd.id === add.id)
|
||||
assert(cmd.reason === Right(IncorrectOrUnknownPaymentDetails(1000 msat, nodeParams.currentBlockHeight)))
|
||||
|
|
|
@ -78,7 +78,7 @@ class MultiPartPaymentLifecycleSpec extends TestKitBaseClass with FixtureAnyFunS
|
|||
import f._
|
||||
|
||||
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)
|
||||
|
||||
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._
|
||||
|
||||
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)
|
||||
|
||||
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(_.finalPayload.expiry).toSet === Set(expiry))
|
||||
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.totalAmount).toSet === Set(1200000 msat))
|
||||
assert(payFsm.stateName === PAYMENT_IN_PROGRESS)
|
||||
|
@ -149,7 +150,7 @@ class MultiPartPaymentLifecycleSpec extends TestKitBaseClass with FixtureAnyFunS
|
|||
// We include a bunch of additional tlv records.
|
||||
val trampolineTlv = OnionPaymentPayloadTlv.TrampolineOnion(OnionRoutingPacket(0, ByteVector.fill(33)(0), ByteVector.fill(400)(0), randomBytes32()))
|
||||
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)
|
||||
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))))
|
||||
|
@ -173,7 +174,7 @@ class MultiPartPaymentLifecycleSpec extends TestKitBaseClass with FixtureAnyFunS
|
|||
test("successful retry") { 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)
|
||||
router.expectMsgType[RouteRequest]
|
||||
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 =>
|
||||
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)
|
||||
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))))
|
||||
|
@ -248,7 +249,7 @@ class MultiPartPaymentLifecycleSpec extends TestKitBaseClass with FixtureAnyFunS
|
|||
test("retry local channel failures") { 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)
|
||||
router.expectMsgType[RouteRequest]
|
||||
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 =>
|
||||
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)
|
||||
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))))
|
||||
|
@ -317,7 +318,7 @@ class MultiPartPaymentLifecycleSpec extends TestKitBaseClass with FixtureAnyFunS
|
|||
|
||||
// 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 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)
|
||||
assert(router.expectMsgType[RouteRequest].assistedRoutes.head.head === routingHint)
|
||||
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.
|
||||
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)
|
||||
assert(router.expectMsgType[RouteRequest].assistedRoutes.head.head === routingHint)
|
||||
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 =>
|
||||
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)
|
||||
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))))
|
||||
|
@ -428,7 +429,7 @@ class MultiPartPaymentLifecycleSpec extends TestKitBaseClass with FixtureAnyFunS
|
|||
import f._
|
||||
|
||||
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)
|
||||
router.expectMsgType[RouteRequest]
|
||||
router.send(payFsm, Status.Failure(RouteNotFound))
|
||||
|
@ -458,7 +459,7 @@ class MultiPartPaymentLifecycleSpec extends TestKitBaseClass with FixtureAnyFunS
|
|||
test("abort if recipient sends error") { 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)
|
||||
router.expectMsgType[RouteRequest]
|
||||
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 =>
|
||||
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)
|
||||
router.expectMsgType[RouteRequest]
|
||||
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 =>
|
||||
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)
|
||||
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))))
|
||||
|
@ -511,7 +512,7 @@ class MultiPartPaymentLifecycleSpec extends TestKitBaseClass with FixtureAnyFunS
|
|||
test("receive partial success after retriable failure (recipient spec violation)") { 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)
|
||||
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))))
|
||||
|
@ -531,7 +532,7 @@ class MultiPartPaymentLifecycleSpec extends TestKitBaseClass with FixtureAnyFunS
|
|||
test("receive partial success after abort (recipient spec violation)") { 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)
|
||||
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))))
|
||||
|
@ -564,7 +565,7 @@ class MultiPartPaymentLifecycleSpec extends TestKitBaseClass with FixtureAnyFunS
|
|||
test("receive partial failure after success (recipient spec violation)") { 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)
|
||||
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))))
|
||||
|
|
|
@ -28,6 +28,7 @@ import fr.acinq.eclair.payment.OutgoingPaymentPacket.Upstream
|
|||
import fr.acinq.eclair.payment.PaymentPacketSpec._
|
||||
import fr.acinq.eclair.payment.PaymentRequest.{ExtraHop, PaymentRequestFeatures}
|
||||
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.{PaymentError, PaymentInitiator, PaymentLifecycle}
|
||||
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.PaymentOnion.FinalTlvPayload
|
||||
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.{Outcome, Tag}
|
||||
import scodec.bits.HexStringSyntax
|
||||
import scodec.bits.{BinStringSyntax, ByteVector, HexStringSyntax}
|
||||
|
||||
import java.util.UUID
|
||||
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)
|
||||
|
||||
val featuresWithoutMpp = Features(
|
||||
val featuresWithoutMpp: Map[Feature with InvoiceFeature, FeatureSupport] = Map(
|
||||
VariableLengthOnion -> Mandatory,
|
||||
PaymentSecret -> Mandatory
|
||||
)
|
||||
|
||||
val featuresWithMpp = Features(
|
||||
val featuresWithMpp: Map[Feature with InvoiceFeature, FeatureSupport] = Map(
|
||||
VariableLengthOnion -> Mandatory,
|
||||
PaymentSecret -> Mandatory,
|
||||
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 {
|
||||
// @formatter:off
|
||||
override def spawnOutgoingPayment(context: ActorContext, cfg: SendPaymentConfig): ActorRef = {
|
||||
|
@ -77,7 +85,7 @@ class PaymentInitiatorSpec extends TestKitBaseClass with FixtureAnyFunSuiteLike
|
|||
|
||||
override def withFixture(test: OneArgTest): Outcome = {
|
||||
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 eventListener = TestProbe()
|
||||
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 =>
|
||||
import f._
|
||||
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)
|
||||
sender.send(initiator, req)
|
||||
val id = sender.expectMsgType[UUID]
|
||||
val fail = sender.expectMsgType[PaymentFailed]
|
||||
assert(fail.id === id)
|
||||
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 =>
|
||||
|
@ -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))
|
||||
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(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 =>
|
||||
import f._
|
||||
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)
|
||||
assert(req.finalExpiry(nodeParams.currentBlockHeight) === (finalExpiryDelta + 1).toCltvExpiry(nodeParams.currentBlockHeight))
|
||||
sender.send(initiator, req)
|
||||
|
@ -151,17 +167,17 @@ class PaymentInitiatorSpec extends TestKitBaseClass with FixtureAnyFunSuiteLike
|
|||
|
||||
test("forward multi-part payment") { 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)
|
||||
sender.send(initiator, req)
|
||||
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(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 =>
|
||||
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 req = SendPaymentToRoute(finalAmount / 2, finalAmount, pr, Channel.MIN_CLTV_EXPIRY_DELTA, route, None, None, None, 0 msat, CltvExpiryDelta(0), Nil)
|
||||
sender.send(initiator, req)
|
||||
|
@ -177,9 +193,8 @@ class PaymentInitiatorSpec extends TestKitBaseClass with FixtureAnyFunSuiteLike
|
|||
|
||||
test("forward trampoline payment") { 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 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 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)
|
||||
|
@ -250,8 +265,7 @@ class PaymentInitiatorSpec extends TestKitBaseClass with FixtureAnyFunSuiteLike
|
|||
import f._
|
||||
// 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 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 = features)
|
||||
val pr = PaymentRequest(Block.RegtestGenesisBlock.hash, None, paymentHash, priv_a.privateKey, Left("#abittooreckless"), CltvExpiryDelta(18), None, None, routingHints, features = PaymentRequestFeatures(featuresWithMpp))
|
||||
val trampolineFees = 21000 msat
|
||||
val req = SendTrampolinePayment(finalAmount, pr, b, Seq((trampolineFees, CltvExpiryDelta(12))), CltvExpiryDelta(9), routeParams = nodeParams.routerConf.pathFindingExperimentConf.getRandomConf().getDefaultRouteParams)
|
||||
sender.send(initiator, req)
|
||||
|
@ -266,8 +280,7 @@ class PaymentInitiatorSpec extends TestKitBaseClass with FixtureAnyFunSuiteLike
|
|||
|
||||
test("retry trampoline payment") { 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 = features)
|
||||
val pr = PaymentRequest(Block.LivenetGenesisBlock.hash, Some(finalAmount), paymentHash, priv_c.privateKey, Left("Some phoenix invoice"), CltvExpiryDelta(18), features = PaymentRequestFeatures(featuresWithTrampoline))
|
||||
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)
|
||||
sender.send(initiator, req)
|
||||
|
@ -296,8 +309,7 @@ class PaymentInitiatorSpec extends TestKitBaseClass with FixtureAnyFunSuiteLike
|
|||
|
||||
test("retry trampoline payment and fail") { 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 = features)
|
||||
val pr = PaymentRequest(Block.LivenetGenesisBlock.hash, Some(finalAmount), paymentHash, priv_c.privateKey, Left("Some phoenix invoice"), CltvExpiryDelta(18), features = PaymentRequestFeatures(featuresWithTrampoline))
|
||||
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)
|
||||
sender.send(initiator, req)
|
||||
|
@ -326,8 +338,7 @@ class PaymentInitiatorSpec extends TestKitBaseClass with FixtureAnyFunSuiteLike
|
|||
|
||||
test("retry trampoline payment and fail (route not found)") { 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 = features)
|
||||
val pr = PaymentRequest(Block.LivenetGenesisBlock.hash, Some(finalAmount), paymentHash, priv_c.privateKey, Left("Some phoenix invoice"), CltvExpiryDelta(18), features = PaymentRequestFeatures(featuresWithTrampoline))
|
||||
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)
|
||||
sender.send(initiator, req)
|
||||
|
|
|
@ -102,7 +102,7 @@ class PaymentLifecycleSpec extends BaseRouterSpec {
|
|||
|
||||
// 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 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)
|
||||
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[_]])
|
||||
|
@ -128,7 +128,7 @@ class PaymentLifecycleSpec extends BaseRouterSpec {
|
|||
|
||||
// pre-computed route going from A to 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)
|
||||
routerForwarder.expectMsg(FinalizeRoute(defaultAmountMsat, route, paymentContext = Some(cfg.paymentContext)))
|
||||
|
@ -152,7 +152,7 @@ class PaymentLifecycleSpec extends BaseRouterSpec {
|
|||
val payFixture = createPaymentLifecycle(recordMetrics = false)
|
||||
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)
|
||||
routerForwarder.expectMsgType[FinalizeRoute]
|
||||
routerForwarder.forward(routerFixture.router)
|
||||
|
@ -167,7 +167,7 @@ class PaymentLifecycleSpec extends BaseRouterSpec {
|
|||
val payFixture = createPaymentLifecycle(recordMetrics = false)
|
||||
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)
|
||||
routerForwarder.expectMsgType[FinalizeRoute]
|
||||
routerForwarder.forward(routerFixture.router)
|
||||
|
@ -186,7 +186,7 @@ class PaymentLifecycleSpec extends BaseRouterSpec {
|
|||
val recipient = randomKey().publicKey
|
||||
val route = PredefinedNodeRoute(Seq(a, b, c, recipient))
|
||||
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)
|
||||
routerForwarder.expectMsg(FinalizeRoute(defaultAmountMsat, route, routingHint, paymentContext = Some(cfg.paymentContext)))
|
||||
|
@ -210,7 +210,7 @@ class PaymentLifecycleSpec extends BaseRouterSpec {
|
|||
import payFixture._
|
||||
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)
|
||||
val Transition(_, WAITING_FOR_REQUEST, WAITING_FOR_ROUTE) = monitor.expectMsgClass(classOf[Transition[_]])
|
||||
val routeRequest = routerForwarder.expectMsgType[RouteRequest]
|
||||
|
@ -241,7 +241,7 @@ class PaymentLifecycleSpec extends BaseRouterSpec {
|
|||
"my-test-experiment",
|
||||
experimentPercentage = 100
|
||||
).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)
|
||||
val routeRequest = routerForwarder.expectMsgType[RouteRequest]
|
||||
val Transition(_, WAITING_FOR_REQUEST, WAITING_FOR_ROUTE) = monitor.expectMsgClass(classOf[Transition[_]])
|
||||
|
@ -263,7 +263,7 @@ class PaymentLifecycleSpec extends BaseRouterSpec {
|
|||
import payFixture._
|
||||
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)
|
||||
routerForwarder.expectMsg(defaultRouteRequest(a, d, cfg))
|
||||
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 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)
|
||||
awaitCond(paymentFSM.stateName == WAITING_FOR_ROUTE && nodeParams.db.payments.getOutgoingPayment(id).exists(_.status === OutgoingPaymentStatus.Pending))
|
||||
routerForwarder.expectMsgType[RouteRequest]
|
||||
|
@ -327,7 +327,7 @@ class PaymentLifecycleSpec extends BaseRouterSpec {
|
|||
import payFixture._
|
||||
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)
|
||||
awaitCond(paymentFSM.stateName == WAITING_FOR_ROUTE && nodeParams.db.payments.getOutgoingPayment(id).exists(_.status === OutgoingPaymentStatus.Pending))
|
||||
routerForwarder.expectMsgType[RouteRequest]
|
||||
|
@ -347,7 +347,7 @@ class PaymentLifecycleSpec extends BaseRouterSpec {
|
|||
import payFixture._
|
||||
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)
|
||||
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 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)
|
||||
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 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)
|
||||
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()
|
||||
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)
|
||||
awaitCond(paymentFSM.stateName == WAITING_FOR_ROUTE)
|
||||
val WaitingForRoute(_, Nil, _) = paymentFSM.stateData
|
||||
|
@ -446,7 +446,7 @@ class PaymentLifecycleSpec extends BaseRouterSpec {
|
|||
import payFixture._
|
||||
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)
|
||||
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()
|
||||
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)
|
||||
routerForwarder.expectMsg(defaultRouteRequest(nodeParams.nodeId, d, cfg))
|
||||
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)
|
||||
))
|
||||
|
||||
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)
|
||||
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
|
||||
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)
|
||||
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 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)
|
||||
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 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)
|
||||
routerForwarder.expectMsgType[RouteRequest]
|
||||
val Transition(_, WAITING_FOR_REQUEST, WAITING_FOR_ROUTE) = monitor.expectMsgClass(classOf[Transition[_]])
|
||||
|
@ -686,7 +686,7 @@ class PaymentLifecycleSpec extends BaseRouterSpec {
|
|||
import payFixture._
|
||||
|
||||
// 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)
|
||||
routerForwarder.expectMsgType[RouteRequest]
|
||||
|
||||
|
@ -770,7 +770,7 @@ class PaymentLifecycleSpec extends BaseRouterSpec {
|
|||
import payFixture._
|
||||
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)
|
||||
routerForwarder.expectMsgType[RouteRequest]
|
||||
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
|
||||
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)
|
||||
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[_]])
|
||||
|
|
|
@ -20,6 +20,7 @@ import akka.actor.ActorRef
|
|||
import fr.acinq.bitcoin.Crypto.PublicKey
|
||||
import fr.acinq.bitcoin.DeterministicWallet.ExtendedPrivateKey
|
||||
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.channel._
|
||||
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.PaymentOnion.{ChannelRelayTlvPayload, FinalTlvPayload}
|
||||
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.funsuite.AnyFunSuite
|
||||
import scodec.Attempt
|
||||
|
@ -115,7 +116,7 @@ class PaymentPacketSpec extends AnyFunSuite with BeforeAndAfterAll {
|
|||
}
|
||||
|
||||
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.cltvExpiry === finalExpiry + channelUpdate_de.cltvExpiryDelta + channelUpdate_cd.cltvExpiryDelta + channelUpdate_bc.cltvExpiryDelta)
|
||||
assert(add.paymentHash === paymentHash)
|
||||
|
@ -126,7 +127,7 @@ class PaymentPacketSpec extends AnyFunSuite with BeforeAndAfterAll {
|
|||
}
|
||||
|
||||
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.cltvExpiry === finalExpiry)
|
||||
assert(add.paymentHash === paymentHash)
|
||||
|
@ -140,6 +141,7 @@ class PaymentPacketSpec extends AnyFunSuite with BeforeAndAfterAll {
|
|||
assert(payload_b.totalAmount === finalAmount)
|
||||
assert(payload_b.expiry === finalExpiry)
|
||||
assert(payload_b.paymentSecret === paymentSecret)
|
||||
assert(payload_b.paymentMetadata === Some(paymentMetadata))
|
||||
}
|
||||
|
||||
test("build a trampoline payment") {
|
||||
|
@ -148,7 +150,7 @@ class PaymentPacketSpec extends AnyFunSuite with BeforeAndAfterAll {
|
|||
// / \ / \
|
||||
// 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(expiry_ac === expiry_bc)
|
||||
|
||||
|
@ -173,6 +175,7 @@ class PaymentPacketSpec extends AnyFunSuite with BeforeAndAfterAll {
|
|||
assert(inner_c.invoiceRoutingInfo === None)
|
||||
assert(inner_c.invoiceFeatures === None)
|
||||
assert(inner_c.paymentSecret === None)
|
||||
assert(inner_c.paymentMetadata === None)
|
||||
|
||||
// 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))
|
||||
|
@ -190,6 +193,7 @@ class PaymentPacketSpec extends AnyFunSuite with BeforeAndAfterAll {
|
|||
assert(inner_d.invoiceRoutingInfo === None)
|
||||
assert(inner_d.invoiceFeatures === None)
|
||||
assert(inner_d.paymentSecret === None)
|
||||
assert(inner_d.paymentMetadata === None)
|
||||
|
||||
// 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))
|
||||
|
@ -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 Right(FinalPacket(add_e2, payload_e)) = decrypt(add_e, priv_e.privateKey)
|
||||
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") {
|
||||
|
@ -208,9 +212,9 @@ class PaymentPacketSpec extends AnyFunSuite with BeforeAndAfterAll {
|
|||
// a -> b -> c d -> e
|
||||
|
||||
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 invoice = PaymentRequest(Block.RegtestGenesisBlock.hash, Some(finalAmount), paymentHash, priv_a.privateKey, Left("#reckless"), CltvExpiryDelta(18), None, None, routingHints, features = invoiceFeatures)
|
||||
val (amount_ac, expiry_ac, trampolineOnion) = buildTrampolineToLegacyPacket(invoice, trampolineHops, PaymentOnion.createSinglePartPayload(finalAmount, finalExpiry, invoice.paymentSecret.get))
|
||||
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, paymentMetadata = Some(hex"010203"))
|
||||
val (amount_ac, expiry_ac, trampolineOnion) = buildTrampolineToLegacyPacket(invoice, trampolineHops, PaymentOnion.createSinglePartPayload(finalAmount, finalExpiry, invoice.paymentSecret.get, None))
|
||||
assert(amount_ac === amount_bc)
|
||||
assert(expiry_ac === expiry_bc)
|
||||
|
||||
|
@ -249,6 +253,7 @@ class PaymentPacketSpec extends AnyFunSuite with BeforeAndAfterAll {
|
|||
assert(inner_d.outgoingNodeId === e)
|
||||
assert(inner_d.totalAmount === finalAmount)
|
||||
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.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 invoice = PaymentRequest(Block.RegtestGenesisBlock.hash, Some(finalAmount), paymentHash, priv_a.privateKey, Left("#reckless"), CltvExpiryDelta(18), None, None, routingHintOverflow)
|
||||
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") {
|
||||
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 Left(failure) = decrypt(add, priv_b.privateKey)
|
||||
assert(failure.isInstanceOf[InvalidOnionHmac])
|
||||
}
|
||||
|
||||
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 add_b = UpdateAddHtlc(randomBytes32(), 1, firstAmount, paymentHash, firstExpiry, onion.packet)
|
||||
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") {
|
||||
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 Left(failure) = decrypt(add, priv_b.privateKey)
|
||||
assert(failure.isInstanceOf[InvalidOnionHmac])
|
||||
}
|
||||
|
||||
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 Left(failure) = decrypt(add, priv_b.privateKey)
|
||||
assert(failure === FinalIncorrectHtlcAmount(firstAmount - 100.msat))
|
||||
}
|
||||
|
||||
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 Left(failure) = decrypt(add, priv_b.privateKey)
|
||||
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") {
|
||||
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 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)
|
||||
|
@ -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") {
|
||||
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 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)
|
||||
|
@ -330,7 +335,7 @@ class PaymentPacketSpec extends AnyFunSuite with BeforeAndAfterAll {
|
|||
}
|
||||
|
||||
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 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.
|
||||
|
@ -339,7 +344,7 @@ class PaymentPacketSpec extends AnyFunSuite with BeforeAndAfterAll {
|
|||
}
|
||||
|
||||
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 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.
|
||||
|
@ -398,6 +403,7 @@ object PaymentPacketSpec {
|
|||
val paymentPreimage = randomBytes32()
|
||||
val paymentHash = Crypto.sha256(paymentPreimage)
|
||||
val paymentSecret = randomBytes32()
|
||||
val paymentMetadata = randomBytes32().bytes
|
||||
|
||||
val expiry_de = finalExpiry
|
||||
val amount_de = finalAmount
|
||||
|
|
|
@ -18,10 +18,10 @@ package fr.acinq.eclair.payment
|
|||
|
||||
import fr.acinq.bitcoin.Crypto.{PrivateKey, PublicKey}
|
||||
import fr.acinq.bitcoin.{Block, BtcDouble, ByteVector32, Crypto, MilliBtcDouble, SatoshiLong}
|
||||
import fr.acinq.eclair.FeatureSupport.Mandatory
|
||||
import fr.acinq.eclair.Features.{PaymentSecret, _}
|
||||
import fr.acinq.eclair.FeatureSupport.{Mandatory, Optional}
|
||||
import fr.acinq.eclair.Features.{PaymentMetadata, PaymentSecret, _}
|
||||
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 scodec.DecodeResult
|
||||
import scodec.bits._
|
||||
|
@ -311,6 +311,22 @@ class PaymentRequestSpec extends AnyFunSuite {
|
|||
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") {
|
||||
val refs = Seq(
|
||||
// Bech32 checksum is invalid.
|
||||
|
@ -455,7 +471,7 @@ class PaymentRequestSpec extends AnyFunSuite {
|
|||
test("payment secret") {
|
||||
val pr = PaymentRequest(Block.LivenetGenesisBlock.hash, Some(123 msat), ByteVector32.One, priv, Left("Some invoice"), CltvExpiryDelta(18))
|
||||
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)
|
||||
|
||||
val pr1 = PaymentRequest.read(PaymentRequest.write(pr))
|
||||
|
@ -471,20 +487,23 @@ class PaymentRequestSpec extends AnyFunSuite {
|
|||
)
|
||||
|
||||
// A multi-part invoice must use a payment secret.
|
||||
assertThrows[IllegalArgumentException](
|
||||
PaymentRequest(Block.LivenetGenesisBlock.hash, Some(123 msat), ByteVector32.One, priv, Left("MPP without secrets"), CltvExpiryDelta(18), features = PaymentRequestFeatures(BasicMultiPartPayment.optional, VariableLengthOnion.optional))
|
||||
)
|
||||
assertThrows[IllegalArgumentException]({
|
||||
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") {
|
||||
val pr = PaymentRequest(Block.LivenetGenesisBlock.hash, Some(123 msat), ByteVector32.One, priv, Left("Some invoice"), CltvExpiryDelta(18))
|
||||
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.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.allowTrampoline)
|
||||
|
||||
|
|
|
@ -673,7 +673,7 @@ object PostRestartHtlcCleanerSpec {
|
|||
val (paymentHash1, paymentHash2, paymentHash3) = (Crypto.sha256(preimage1), Crypto.sha256(preimage2), Crypto.sha256(preimage3))
|
||||
|
||||
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)
|
||||
}
|
||||
|
||||
|
@ -682,7 +682,7 @@ object PostRestartHtlcCleanerSpec {
|
|||
def buildHtlcOut(htlcId: Long, channelId: ByteVector32, paymentHash: ByteVector32): DirectedHtlc = OutgoingHtlc(buildHtlc(htlcId, channelId, paymentHash))
|
||||
|
||||
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))
|
||||
}
|
||||
|
||||
|
|
|
@ -24,6 +24,7 @@ import akka.actor.typed.scaladsl.ActorContext
|
|||
import akka.actor.typed.scaladsl.adapter._
|
||||
import com.typesafe.config.ConfigFactory
|
||||
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.channel.{CMD_FAIL_HTLC, CMD_FULFILL_HTLC, Register}
|
||||
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.{BalanceTooLow, RouteNotFound}
|
||||
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.funsuite.FixtureAnyFunSuiteLike
|
||||
import scodec.bits.HexStringSyntax
|
||||
|
@ -211,7 +212,7 @@ class NodeRelayerSpec extends ScalaTestWithActorTestKit(ConfigFactory.load("appl
|
|||
// and then one extra
|
||||
val extra = IncomingPaymentPacket.NodeRelayPacket(
|
||||
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),
|
||||
nextTrampolinePacket)
|
||||
nodeRelayer ! NodeRelay.Relay(extra)
|
||||
|
@ -240,7 +241,7 @@ class NodeRelayerSpec extends ScalaTestWithActorTestKit(ConfigFactory.load("appl
|
|||
// Receive new extraneous multi-part HTLC.
|
||||
val i1 = IncomingPaymentPacket.NodeRelayPacket(
|
||||
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),
|
||||
nextTrampolinePacket)
|
||||
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.
|
||||
val i2 = IncomingPaymentPacket.NodeRelayPacket(
|
||||
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),
|
||||
nextTrampolinePacket)
|
||||
nodeRelayer ! NodeRelay.Relay(i2)
|
||||
|
@ -566,8 +567,8 @@ class NodeRelayerSpec extends ScalaTestWithActorTestKit(ConfigFactory.load("appl
|
|||
|
||||
// Receive an upstream multi-part payment.
|
||||
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 pr = PaymentRequest(Block.LivenetGenesisBlock.hash, Some(outgoingAmount * 3), paymentHash, randomKey(), Left("Some invoice"), CltvExpiryDelta(18), extraHops = hints, features = features)
|
||||
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, paymentMetadata = Some(hex"123456"), features = features)
|
||||
val incomingPayments = incomingMultiPart.map(incoming => incoming.copy(innerPayload = PaymentOnion.createNodeRelayToNonTrampolinePayload(
|
||||
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)))
|
||||
val outgoingPayment = mockPayFSM.expectMessageType[SendMultiPartPayment]
|
||||
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.targetExpiry === outgoingExpiry)
|
||||
assert(outgoingPayment.targetNodeId === outgoingNodeId)
|
||||
|
@ -607,7 +609,7 @@ class NodeRelayerSpec extends ScalaTestWithActorTestKit(ConfigFactory.load("appl
|
|||
|
||||
// Receive an upstream multi-part payment.
|
||||
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)
|
||||
val incomingPayments = incomingMultiPart.map(incoming => incoming.copy(innerPayload = PaymentOnion.createNodeRelayToNonTrampolinePayload(
|
||||
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]
|
||||
assert(outgoingPayment.finalPayload.amount === outgoingAmount)
|
||||
assert(outgoingPayment.finalPayload.expiry === outgoingExpiry)
|
||||
assert(outgoingPayment.finalPayload.paymentMetadata === pr.paymentMetadata) // we should use the provided metadata
|
||||
assert(outgoingPayment.targetNodeId === outgoingNodeId)
|
||||
assert(outgoingPayment.assistedRoutes === hints)
|
||||
// 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 = {
|
||||
val outerPayload = if (amountIn == totalAmountIn) {
|
||||
PaymentOnion.createSinglePartPayload(amountIn, expiryIn, incomingSecret)
|
||||
PaymentOnion.createSinglePartPayload(amountIn, expiryIn, incomingSecret, None)
|
||||
} else {
|
||||
PaymentOnion.createMultiPartPayload(amountIn, totalAmountIn, expiryIn, incomingSecret)
|
||||
PaymentOnion.createMultiPartPayload(amountIn, totalAmountIn, expiryIn, incomingSecret, None)
|
||||
}
|
||||
IncomingPaymentPacket.NodeRelayPacket(
|
||||
UpdateAddHtlc(randomBytes32(), Random.nextInt(100), amountIn, paymentHash, expiryIn, TestConstants.emptyOnionPacket),
|
||||
|
@ -734,7 +737,7 @@ object NodeRelayerSpec {
|
|||
val amountIn = incomingAmount / 2
|
||||
IncomingPaymentPacket.NodeRelayPacket(
|
||||
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),
|
||||
nextTrampolinePacket)
|
||||
}
|
||||
|
|
|
@ -84,7 +84,7 @@ class RelayerSpec extends ScalaTestWithActorTestKit(ConfigFactory.load("applicat
|
|||
}
|
||||
|
||||
// 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
|
||||
val add_ab = UpdateAddHtlc(channelId = randomBytes32(), id = 123456, cmd.amount, cmd.paymentHash, cmd.cltvExpiry, cmd.onion)
|
||||
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 =>
|
||||
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)
|
||||
|
||||
relayer ! RelayForward(add_ab)
|
||||
|
||||
val fp = paymentHandler.expectMessageType[FinalPacket]
|
||||
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)
|
||||
}
|
||||
|
@ -114,7 +114,7 @@ class RelayerSpec extends ScalaTestWithActorTestKit(ConfigFactory.load("applicat
|
|||
// We simulate a payment split between multiple trampoline routes.
|
||||
val totalAmount = finalAmount * 3
|
||||
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(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))
|
||||
|
@ -138,7 +138,7 @@ class RelayerSpec extends ScalaTestWithActorTestKit(ConfigFactory.load("applicat
|
|||
import f._
|
||||
|
||||
// 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)
|
||||
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
|
||||
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))
|
||||
|
||||
// and then manually build an htlc
|
||||
|
|
|
@ -53,8 +53,26 @@ class AnnouncementsSpec extends AnyFunSuite {
|
|||
}
|
||||
|
||||
test("create valid signed node announcement") {
|
||||
val ann = makeNodeAnnouncement(Alice.nodeParams.privateKey, Alice.nodeParams.alias, Alice.nodeParams.color, Alice.nodeParams.publicAddresses, Alice.nodeParams.features)
|
||||
assert(ann.features.hasFeature(Features.VariableLengthOnion, Some(FeatureSupport.Mandatory)))
|
||||
val features = Features(
|
||||
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.copy(timestamp = 153 unixsec)) === false)
|
||||
}
|
||||
|
|
Loading…
Add table
Reference in a new issue