mirror of
https://github.com/ACINQ/eclair.git
synced 2025-02-24 06:47:46 +01:00
Add support for option_payment_metadata
(#2063)
* Filter init, node and invoice features We should explicitly filter features based on where they can be included (`init`, `node_announcement` or `invoice`) as specified in Bolt 9. * Add support for sending `payment_metadata` Whenever we find a payment metadata field in an invoice, we send it in the onion payload for the final recipient. * Include `payment_metadata` in all invoices We include a payment metadata in every invoice we generate. This lets us see whether our payers support it or not, which is important data to have before we make it mandatory and use it for storage-less invoices. See https://github.com/lightning/bolts/pull/912 for reference.
This commit is contained in:
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.
|
It can also send onion messages with the `sendonionmessage` API.
|
||||||
Messages sent to Eclair can be read with the websocket API.
|
Messages sent to Eclair can be read with the websocket API.
|
||||||
|
|
||||||
|
### Support for `option_payment_metadata`
|
||||||
|
|
||||||
|
Eclair now supports the `option_payment_metadata` feature (see https://github.com/lightning/bolts/pull/912).
|
||||||
|
This feature will let recipients generate "light" invoices that don't need to be stored locally until they're paid.
|
||||||
|
This is particularly useful for payment hubs that generate a lot of invoices (e.g. to be displayed on a website) but expect only a fraction of them to actually be paid.
|
||||||
|
|
||||||
|
Eclair includes a small `payment_metadata` field in all invoices it generates.
|
||||||
|
This lets node operators verify that payers actually support that feature.
|
||||||
|
|
||||||
### API changes
|
### API changes
|
||||||
|
|
||||||
#### Timestamps
|
#### Timestamps
|
||||||
|
|
|
@ -58,6 +58,7 @@ eclair {
|
||||||
option_shutdown_anysegwit = optional
|
option_shutdown_anysegwit = optional
|
||||||
option_onion_messages = disabled
|
option_onion_messages = disabled
|
||||||
option_channel_type = optional
|
option_channel_type = optional
|
||||||
|
option_payment_metadata = optional
|
||||||
trampoline_payment = disabled
|
trampoline_payment = disabled
|
||||||
keysend = disabled
|
keysend = disabled
|
||||||
}
|
}
|
||||||
|
|
|
@ -34,6 +34,8 @@ object FeatureSupport {
|
||||||
|
|
||||||
trait Feature {
|
trait Feature {
|
||||||
|
|
||||||
|
this: FeatureScope =>
|
||||||
|
|
||||||
def rfcName: String
|
def rfcName: String
|
||||||
def mandatory: Int
|
def mandatory: Int
|
||||||
def optional: Int = mandatory + 1
|
def optional: Int = mandatory + 1
|
||||||
|
@ -46,6 +48,15 @@ trait Feature {
|
||||||
override def toString = rfcName
|
override def toString = rfcName
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/** Feature scope as defined in Bolt 9. */
|
||||||
|
sealed trait FeatureScope
|
||||||
|
/** Feature that should be advertised in init messages. */
|
||||||
|
trait InitFeature extends FeatureScope
|
||||||
|
/** Feature that should be advertised in node announcements. */
|
||||||
|
trait NodeFeature extends FeatureScope
|
||||||
|
/** Feature that should be advertised in invoices. */
|
||||||
|
trait InvoiceFeature extends FeatureScope
|
||||||
// @formatter:on
|
// @formatter:on
|
||||||
|
|
||||||
case class UnknownFeature(bitIndex: Int)
|
case class UnknownFeature(bitIndex: Int)
|
||||||
|
@ -71,6 +82,13 @@ case class Features(activated: Map[Feature, FeatureSupport], unknown: Set[Unknow
|
||||||
unknownFeaturesOk && knownFeaturesOk
|
unknownFeaturesOk && knownFeaturesOk
|
||||||
}
|
}
|
||||||
|
|
||||||
|
def initFeatures(): Features = Features(activated.collect { case (f: InitFeature, s) => (f: Feature, s) }, unknown)
|
||||||
|
|
||||||
|
def nodeAnnouncementFeatures(): Features = Features(activated.collect { case (f: NodeFeature, s) => (f: Feature, s) }, unknown)
|
||||||
|
|
||||||
|
// NB: we don't include unknown features in invoices, which means plugins cannot inject invoice features.
|
||||||
|
def invoiceFeatures(): Map[Feature with InvoiceFeature, FeatureSupport] = activated.collect { case (f: InvoiceFeature, s) => (f, s) }
|
||||||
|
|
||||||
def toByteVector: ByteVector = {
|
def toByteVector: ByteVector = {
|
||||||
val activatedFeatureBytes = toByteVectorFromIndex(activated.map { case (feature, support) => feature.supportBit(support) }.toSet)
|
val activatedFeatureBytes = toByteVectorFromIndex(activated.map { case (feature, support) => feature.supportBit(support) }.toSet)
|
||||||
val unknownFeatureBytes = toByteVectorFromIndex(unknown.map(_.bitIndex))
|
val unknownFeatureBytes = toByteVectorFromIndex(unknown.map(_.bitIndex))
|
||||||
|
@ -137,91 +155,96 @@ object Features {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
case object OptionDataLossProtect extends Feature {
|
case object OptionDataLossProtect extends Feature with InitFeature with NodeFeature {
|
||||||
val rfcName = "option_data_loss_protect"
|
val rfcName = "option_data_loss_protect"
|
||||||
val mandatory = 0
|
val mandatory = 0
|
||||||
}
|
}
|
||||||
|
|
||||||
case object InitialRoutingSync extends Feature {
|
case object InitialRoutingSync extends Feature with InitFeature {
|
||||||
val rfcName = "initial_routing_sync"
|
val rfcName = "initial_routing_sync"
|
||||||
// reserved but not used as per lightningnetwork/lightning-rfc/pull/178
|
// reserved but not used as per lightningnetwork/lightning-rfc/pull/178
|
||||||
val mandatory = 2
|
val mandatory = 2
|
||||||
}
|
}
|
||||||
|
|
||||||
case object OptionUpfrontShutdownScript extends Feature {
|
case object OptionUpfrontShutdownScript extends Feature with InitFeature with NodeFeature {
|
||||||
val rfcName = "option_upfront_shutdown_script"
|
val rfcName = "option_upfront_shutdown_script"
|
||||||
val mandatory = 4
|
val mandatory = 4
|
||||||
}
|
}
|
||||||
|
|
||||||
case object ChannelRangeQueries extends Feature {
|
case object ChannelRangeQueries extends Feature with InitFeature with NodeFeature {
|
||||||
val rfcName = "gossip_queries"
|
val rfcName = "gossip_queries"
|
||||||
val mandatory = 6
|
val mandatory = 6
|
||||||
}
|
}
|
||||||
|
|
||||||
case object VariableLengthOnion extends Feature {
|
case object VariableLengthOnion extends Feature with InitFeature with NodeFeature with InvoiceFeature {
|
||||||
val rfcName = "var_onion_optin"
|
val rfcName = "var_onion_optin"
|
||||||
val mandatory = 8
|
val mandatory = 8
|
||||||
}
|
}
|
||||||
|
|
||||||
case object ChannelRangeQueriesExtended extends Feature {
|
case object ChannelRangeQueriesExtended extends Feature with InitFeature with NodeFeature {
|
||||||
val rfcName = "gossip_queries_ex"
|
val rfcName = "gossip_queries_ex"
|
||||||
val mandatory = 10
|
val mandatory = 10
|
||||||
}
|
}
|
||||||
|
|
||||||
case object StaticRemoteKey extends Feature {
|
case object StaticRemoteKey extends Feature with InitFeature with NodeFeature {
|
||||||
val rfcName = "option_static_remotekey"
|
val rfcName = "option_static_remotekey"
|
||||||
val mandatory = 12
|
val mandatory = 12
|
||||||
}
|
}
|
||||||
|
|
||||||
case object PaymentSecret extends Feature {
|
case object PaymentSecret extends Feature with InitFeature with NodeFeature with InvoiceFeature {
|
||||||
val rfcName = "payment_secret"
|
val rfcName = "payment_secret"
|
||||||
val mandatory = 14
|
val mandatory = 14
|
||||||
}
|
}
|
||||||
|
|
||||||
case object BasicMultiPartPayment extends Feature {
|
case object BasicMultiPartPayment extends Feature with InitFeature with NodeFeature with InvoiceFeature {
|
||||||
val rfcName = "basic_mpp"
|
val rfcName = "basic_mpp"
|
||||||
val mandatory = 16
|
val mandatory = 16
|
||||||
}
|
}
|
||||||
|
|
||||||
case object Wumbo extends Feature {
|
case object Wumbo extends Feature with InitFeature with NodeFeature {
|
||||||
val rfcName = "option_support_large_channel"
|
val rfcName = "option_support_large_channel"
|
||||||
val mandatory = 18
|
val mandatory = 18
|
||||||
}
|
}
|
||||||
|
|
||||||
case object AnchorOutputs extends Feature {
|
case object AnchorOutputs extends Feature with InitFeature with NodeFeature {
|
||||||
val rfcName = "option_anchor_outputs"
|
val rfcName = "option_anchor_outputs"
|
||||||
val mandatory = 20
|
val mandatory = 20
|
||||||
}
|
}
|
||||||
|
|
||||||
case object AnchorOutputsZeroFeeHtlcTx extends Feature {
|
case object AnchorOutputsZeroFeeHtlcTx extends Feature with InitFeature with NodeFeature {
|
||||||
val rfcName = "option_anchors_zero_fee_htlc_tx"
|
val rfcName = "option_anchors_zero_fee_htlc_tx"
|
||||||
val mandatory = 22
|
val mandatory = 22
|
||||||
}
|
}
|
||||||
|
|
||||||
case object ShutdownAnySegwit extends Feature {
|
case object ShutdownAnySegwit extends Feature with InitFeature with NodeFeature {
|
||||||
val rfcName = "option_shutdown_anysegwit"
|
val rfcName = "option_shutdown_anysegwit"
|
||||||
val mandatory = 26
|
val mandatory = 26
|
||||||
}
|
}
|
||||||
|
|
||||||
case object OnionMessages extends Feature {
|
case object OnionMessages extends Feature with InitFeature with NodeFeature {
|
||||||
val rfcName = "option_onion_messages"
|
val rfcName = "option_onion_messages"
|
||||||
val mandatory = 38
|
val mandatory = 38
|
||||||
}
|
}
|
||||||
|
|
||||||
case object ChannelType extends Feature {
|
case object ChannelType extends Feature with InitFeature with NodeFeature {
|
||||||
val rfcName = "option_channel_type"
|
val rfcName = "option_channel_type"
|
||||||
val mandatory = 44
|
val mandatory = 44
|
||||||
}
|
}
|
||||||
|
|
||||||
|
case object PaymentMetadata extends Feature with InvoiceFeature {
|
||||||
|
val rfcName = "option_payment_metadata"
|
||||||
|
val mandatory = 48
|
||||||
|
}
|
||||||
|
|
||||||
// TODO: @t-bast: update feature bits once spec-ed (currently reserved here: https://github.com/lightningnetwork/lightning-rfc/issues/605)
|
// TODO: @t-bast: update feature bits once spec-ed (currently reserved here: https://github.com/lightningnetwork/lightning-rfc/issues/605)
|
||||||
// We're not advertising these bits yet in our announcements, clients have to assume support.
|
// We're not advertising these bits yet in our announcements, clients have to assume support.
|
||||||
// This is why we haven't added them yet to `areSupported`.
|
// This is why we haven't added them yet to `areSupported`.
|
||||||
case object TrampolinePayment extends Feature {
|
case object TrampolinePayment extends Feature with InitFeature with NodeFeature with InvoiceFeature {
|
||||||
val rfcName = "trampoline_payment"
|
val rfcName = "trampoline_payment"
|
||||||
val mandatory = 50
|
val mandatory = 50
|
||||||
}
|
}
|
||||||
|
|
||||||
case object KeySend extends Feature {
|
case object KeySend extends Feature with NodeFeature {
|
||||||
val rfcName = "keysend"
|
val rfcName = "keysend"
|
||||||
val mandatory = 54
|
val mandatory = 54
|
||||||
}
|
}
|
||||||
|
@ -242,6 +265,7 @@ object Features {
|
||||||
ShutdownAnySegwit,
|
ShutdownAnySegwit,
|
||||||
OnionMessages,
|
OnionMessages,
|
||||||
ChannelType,
|
ChannelType,
|
||||||
|
PaymentMetadata,
|
||||||
TrampolinePayment,
|
TrampolinePayment,
|
||||||
KeySend
|
KeySend
|
||||||
)
|
)
|
||||||
|
|
|
@ -110,7 +110,8 @@ case class NodeParams(nodeKeyManager: NodeKeyManager,
|
||||||
|
|
||||||
def currentBlockHeight: Long = blockCount.get
|
def currentBlockHeight: Long = blockCount.get
|
||||||
|
|
||||||
def featuresFor(nodeId: PublicKey): Features = overrideFeatures.getOrElse(nodeId, features)
|
/** Returns the features that should be used in our init message with the given peer. */
|
||||||
|
def initFeaturesFor(nodeId: PublicKey): Features = overrideFeatures.getOrElse(nodeId, features).initFeatures()
|
||||||
}
|
}
|
||||||
|
|
||||||
object NodeParams extends Logging {
|
object NodeParams extends Logging {
|
||||||
|
|
|
@ -92,7 +92,7 @@ class Switchboard(nodeParams: NodeParams, peerFactory: Switchboard.PeerFactory)
|
||||||
case authenticated: PeerConnection.Authenticated =>
|
case authenticated: PeerConnection.Authenticated =>
|
||||||
// if this is an incoming connection, we might not yet have created the peer
|
// if this is an incoming connection, we might not yet have created the peer
|
||||||
val peer = createOrGetPeer(authenticated.remoteNodeId, offlineChannels = Set.empty)
|
val peer = createOrGetPeer(authenticated.remoteNodeId, offlineChannels = Set.empty)
|
||||||
val features = nodeParams.featuresFor(authenticated.remoteNodeId)
|
val features = nodeParams.initFeaturesFor(authenticated.remoteNodeId)
|
||||||
// if the peer is whitelisted, we sync with them, otherwise we only sync with peers with whom we have at least one channel
|
// if the peer is whitelisted, we sync with them, otherwise we only sync with peers with whom we have at least one channel
|
||||||
val doSync = nodeParams.syncWhitelist.contains(authenticated.remoteNodeId) || (nodeParams.syncWhitelist.isEmpty && peersWithChannels.contains(authenticated.remoteNodeId))
|
val doSync = nodeParams.syncWhitelist.contains(authenticated.remoteNodeId) || (nodeParams.syncWhitelist.isEmpty && peersWithChannels.contains(authenticated.remoteNodeId))
|
||||||
authenticated.peerConnection ! PeerConnection.InitializeConnection(peer, nodeParams.chainHash, features, doSync)
|
authenticated.peerConnection ! PeerConnection.InitializeConnection(peer, nodeParams.chainHash, features, doSync)
|
||||||
|
|
|
@ -335,6 +335,7 @@ object PaymentRequestSerializer extends MinimalSerializer({
|
||||||
FeatureSupportSerializer +
|
FeatureSupportSerializer +
|
||||||
UnknownFeatureSerializer
|
UnknownFeatureSerializer
|
||||||
))
|
))
|
||||||
|
val paymentMetadata = p.paymentMetadata.map(m => JField("paymentMetadata", JString(m.toHex))).toSeq
|
||||||
val routingInfo = JField("routingInfo", Extraction.decompose(p.routingInfo)(
|
val routingInfo = JField("routingInfo", Extraction.decompose(p.routingInfo)(
|
||||||
DefaultFormats +
|
DefaultFormats +
|
||||||
ByteVector32Serializer +
|
ByteVector32Serializer +
|
||||||
|
@ -344,12 +345,14 @@ object PaymentRequestSerializer extends MinimalSerializer({
|
||||||
MilliSatoshiSerializer +
|
MilliSatoshiSerializer +
|
||||||
CltvExpiryDeltaSerializer
|
CltvExpiryDeltaSerializer
|
||||||
))
|
))
|
||||||
val fieldList = List(JField("prefix", JString(p.prefix)),
|
val fieldList = List(
|
||||||
|
JField("prefix", JString(p.prefix)),
|
||||||
JField("timestamp", JLong(p.timestamp.toLong)),
|
JField("timestamp", JLong(p.timestamp.toLong)),
|
||||||
JField("nodeId", JString(p.nodeId.toString())),
|
JField("nodeId", JString(p.nodeId.toString())),
|
||||||
JField("serialized", JString(PaymentRequest.write(p))),
|
JField("serialized", JString(PaymentRequest.write(p))),
|
||||||
p.description.fold(string => JField("description", JString(string)), hash => JField("descriptionHash", JString(hash.toHex))),
|
p.description.fold(string => JField("description", JString(string)), hash => JField("descriptionHash", JString(hash.toHex))),
|
||||||
JField("paymentHash", JString(p.paymentHash.toString()))) ++
|
JField("paymentHash", JString(p.paymentHash.toString()))) ++
|
||||||
|
paymentMetadata ++
|
||||||
expiry ++
|
expiry ++
|
||||||
minFinalCltvExpiry ++
|
minFinalCltvExpiry ++
|
||||||
amount :+
|
amount :+
|
||||||
|
|
|
@ -27,6 +27,7 @@ object Monitoring {
|
||||||
val PaymentAmount = Kamon.histogram("payment.amount", "Payment amount (satoshi)")
|
val PaymentAmount = Kamon.histogram("payment.amount", "Payment amount (satoshi)")
|
||||||
val PaymentFees = Kamon.histogram("payment.fees", "Payment fees (satoshi)")
|
val PaymentFees = Kamon.histogram("payment.fees", "Payment fees (satoshi)")
|
||||||
val PaymentParts = Kamon.histogram("payment.parts", "Number of HTLCs per payment (MPP)")
|
val PaymentParts = Kamon.histogram("payment.parts", "Number of HTLCs per payment (MPP)")
|
||||||
|
val PaymentHtlcReceived = Kamon.counter("payment.received", "Number of valid htlcs received")
|
||||||
val PaymentFailed = Kamon.counter("payment.failed", "Number of failed payment")
|
val PaymentFailed = Kamon.counter("payment.failed", "Number of failed payment")
|
||||||
val PaymentError = Kamon.counter("payment.error", "Non-fatal errors encountered during payment attempts")
|
val PaymentError = Kamon.counter("payment.error", "Non-fatal errors encountered during payment attempts")
|
||||||
val PaymentAttempt = Kamon.histogram("payment.attempt", "Number of attempts before a payment succeeds")
|
val PaymentAttempt = Kamon.histogram("payment.attempt", "Number of attempts before a payment succeeds")
|
||||||
|
@ -71,6 +72,7 @@ object Monitoring {
|
||||||
val PaymentId = "paymentId"
|
val PaymentId = "paymentId"
|
||||||
val ParentId = "parentPaymentId"
|
val ParentId = "parentPaymentId"
|
||||||
val PaymentHash = "paymentHash"
|
val PaymentHash = "paymentHash"
|
||||||
|
val PaymentMetadataIncluded = "paymentMetadataIncluded"
|
||||||
|
|
||||||
val Amount = "amount"
|
val Amount = "amount"
|
||||||
val TotalAmount = "totalAmount"
|
val TotalAmount = "totalAmount"
|
||||||
|
|
|
@ -25,6 +25,7 @@ import fr.acinq.eclair.router.Announcements
|
||||||
import fr.acinq.eclair.router.Router.{ChannelDesc, ChannelHop, Hop, Ignore}
|
import fr.acinq.eclair.router.Router.{ChannelDesc, ChannelHop, Hop, Ignore}
|
||||||
import fr.acinq.eclair.wire.protocol.{ChannelDisabled, ChannelUpdate, Node, TemporaryChannelFailure}
|
import fr.acinq.eclair.wire.protocol.{ChannelDisabled, ChannelUpdate, Node, TemporaryChannelFailure}
|
||||||
import fr.acinq.eclair.{MilliSatoshi, ShortChannelId, TimestampMilli}
|
import fr.acinq.eclair.{MilliSatoshi, ShortChannelId, TimestampMilli}
|
||||||
|
import scodec.bits.ByteVector
|
||||||
|
|
||||||
import java.util.UUID
|
import java.util.UUID
|
||||||
import scala.concurrent.duration.FiniteDuration
|
import scala.concurrent.duration.FiniteDuration
|
||||||
|
@ -114,6 +115,8 @@ object PaymentReceived {
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
case class PaymentMetadataReceived(paymentHash: ByteVector32, paymentMetadata: ByteVector)
|
||||||
|
|
||||||
case class PaymentSettlingOnChain(id: UUID, amount: MilliSatoshi, paymentHash: ByteVector32, timestamp: TimestampMilli = TimestampMilli.now()) extends PaymentEvent
|
case class PaymentSettlingOnChain(id: UUID, amount: MilliSatoshi, paymentHash: ByteVector32, timestamp: TimestampMilli = TimestampMilli.now()) extends PaymentEvent
|
||||||
|
|
||||||
sealed trait PaymentFailure {
|
sealed trait PaymentFailure {
|
||||||
|
|
|
@ -117,7 +117,7 @@ object IncomingPaymentPacket {
|
||||||
} else {
|
} else {
|
||||||
// We merge contents from the outer and inner payloads.
|
// We merge contents from the outer and inner payloads.
|
||||||
// We must use the inner payload's total amount and payment secret because the payment may be split between multiple trampoline payments (#reckless).
|
// We must use the inner payload's total amount and payment secret because the payment may be split between multiple trampoline payments (#reckless).
|
||||||
Right(FinalPacket(add, PaymentOnion.createMultiPartPayload(outerPayload.amount, innerPayload.totalAmount, outerPayload.expiry, innerPayload.paymentSecret)))
|
Right(FinalPacket(add, PaymentOnion.createMultiPartPayload(outerPayload.amount, innerPayload.totalAmount, outerPayload.expiry, innerPayload.paymentSecret, innerPayload.paymentMetadata)))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -213,9 +213,12 @@ object OutgoingPaymentPacket {
|
||||||
* - the trampoline onion to include in final payload of a normal onion
|
* - the trampoline onion to include in final payload of a normal onion
|
||||||
*/
|
*/
|
||||||
def buildTrampolineToLegacyPacket(invoice: PaymentRequest, hops: Seq[NodeHop], finalPayload: PaymentOnion.FinalPayload): (MilliSatoshi, CltvExpiry, Sphinx.PacketAndSecrets) = {
|
def buildTrampolineToLegacyPacket(invoice: PaymentRequest, hops: Seq[NodeHop], finalPayload: PaymentOnion.FinalPayload): (MilliSatoshi, CltvExpiry, Sphinx.PacketAndSecrets) = {
|
||||||
val (firstAmount, firstExpiry, payloads) = hops.drop(1).reverse.foldLeft((finalPayload.amount, finalPayload.expiry, Seq[PaymentOnion.PerHopPayload](finalPayload))) {
|
// NB: the final payload will never reach the recipient, since the next-to-last node in the trampoline route will convert that to a non-trampoline payment.
|
||||||
|
// We use the smallest final payload possible, otherwise we may overflow the trampoline onion size.
|
||||||
|
val dummyFinalPayload = PaymentOnion.createSinglePartPayload(finalPayload.amount, finalPayload.expiry, finalPayload.paymentSecret, None)
|
||||||
|
val (firstAmount, firstExpiry, payloads) = hops.drop(1).reverse.foldLeft((finalPayload.amount, finalPayload.expiry, Seq[PaymentOnion.PerHopPayload](dummyFinalPayload))) {
|
||||||
case ((amount, expiry, payloads), hop) =>
|
case ((amount, expiry, payloads), hop) =>
|
||||||
// The next-to-last trampoline hop must include invoice data to indicate the conversion to a legacy payment.
|
// The next-to-last node in the trampoline route must receive invoice data to indicate the conversion to a non-trampoline payment.
|
||||||
val payload = if (payloads.length == 1) {
|
val payload = if (payloads.length == 1) {
|
||||||
PaymentOnion.createNodeRelayToNonTrampolinePayload(finalPayload.amount, finalPayload.totalAmount, finalPayload.expiry, hop.nextNodeId, invoice)
|
PaymentOnion.createNodeRelayToNonTrampolinePayload(finalPayload.amount, finalPayload.totalAmount, finalPayload.expiry, hop.nextNodeId, invoice)
|
||||||
} else {
|
} else {
|
||||||
|
|
|
@ -19,12 +19,11 @@ package fr.acinq.eclair.payment
|
||||||
import fr.acinq.bitcoin.Crypto.{PrivateKey, PublicKey}
|
import fr.acinq.bitcoin.Crypto.{PrivateKey, PublicKey}
|
||||||
import fr.acinq.bitcoin.{Base58, Base58Check, Bech32, Block, ByteVector32, ByteVector64, Crypto}
|
import fr.acinq.bitcoin.{Base58, Base58Check, Bech32, Block, ByteVector32, ByteVector64, Crypto}
|
||||||
import fr.acinq.eclair.payment.PaymentRequest._
|
import fr.acinq.eclair.payment.PaymentRequest._
|
||||||
import fr.acinq.eclair.{CltvExpiryDelta, FeatureSupport, Features, MilliSatoshi, MilliSatoshiLong, NodeParams, ShortChannelId, TimestampSecond, randomBytes32}
|
import fr.acinq.eclair.{CltvExpiryDelta, Feature, FeatureSupport, Features, InvoiceFeature, MilliSatoshi, MilliSatoshiLong, NodeParams, ShortChannelId, TimestampSecond, randomBytes32}
|
||||||
import scodec.bits.{BitVector, ByteOrdering, ByteVector}
|
import scodec.bits.{BitVector, ByteOrdering, ByteVector}
|
||||||
import scodec.codecs.{list, ubyte}
|
import scodec.codecs.{list, ubyte}
|
||||||
import scodec.{Codec, Err}
|
import scodec.{Codec, Err}
|
||||||
|
|
||||||
import scala.concurrent.duration._
|
|
||||||
import scala.util.{Failure, Success, Try}
|
import scala.util.{Failure, Success, Try}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@ -67,6 +66,11 @@ case class PaymentRequest(prefix: String, amount: Option[MilliSatoshi], timestam
|
||||||
case PaymentRequest.DescriptionHash(h) => Right(h)
|
case PaymentRequest.DescriptionHash(h) => Right(h)
|
||||||
}.get
|
}.get
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @return metadata about the payment (see option_payment_metadata).
|
||||||
|
*/
|
||||||
|
lazy val paymentMetadata: Option[ByteVector] = tags.collectFirst { case m: PaymentRequest.PaymentMetadata => m.data }
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @return the fallback address if any. It could be a script address, pubkey address, ..
|
* @return the fallback address if any. It could be a script address, pubkey address, ..
|
||||||
*/
|
*/
|
||||||
|
@ -126,6 +130,11 @@ object PaymentRequest {
|
||||||
Block.LivenetGenesisBlock.hash -> "lnbc"
|
Block.LivenetGenesisBlock.hash -> "lnbc"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
val defaultFeatures: Map[Feature with InvoiceFeature, FeatureSupport] = Map(
|
||||||
|
Features.VariableLengthOnion -> FeatureSupport.Mandatory,
|
||||||
|
Features.PaymentSecret -> FeatureSupport.Mandatory,
|
||||||
|
)
|
||||||
|
|
||||||
def apply(chainHash: ByteVector32,
|
def apply(chainHash: ByteVector32,
|
||||||
amount: Option[MilliSatoshi],
|
amount: Option[MilliSatoshi],
|
||||||
paymentHash: ByteVector32,
|
paymentHash: ByteVector32,
|
||||||
|
@ -137,7 +146,8 @@ object PaymentRequest {
|
||||||
extraHops: List[List[ExtraHop]] = Nil,
|
extraHops: List[List[ExtraHop]] = Nil,
|
||||||
timestamp: TimestampSecond = TimestampSecond.now(),
|
timestamp: TimestampSecond = TimestampSecond.now(),
|
||||||
paymentSecret: ByteVector32 = randomBytes32(),
|
paymentSecret: ByteVector32 = randomBytes32(),
|
||||||
features: PaymentRequestFeatures = PaymentRequestFeatures(Features.VariableLengthOnion.mandatory, Features.PaymentSecret.mandatory)): PaymentRequest = {
|
paymentMetadata: Option[ByteVector] = None,
|
||||||
|
features: PaymentRequestFeatures = PaymentRequestFeatures(defaultFeatures)): PaymentRequest = {
|
||||||
require(features.requirePaymentSecret, "invoices must require a payment secret")
|
require(features.requirePaymentSecret, "invoices must require a payment secret")
|
||||||
val prefix = prefixes(chainHash)
|
val prefix = prefixes(chainHash)
|
||||||
val tags = {
|
val tags = {
|
||||||
|
@ -145,6 +155,7 @@ object PaymentRequest {
|
||||||
Some(PaymentHash(paymentHash)),
|
Some(PaymentHash(paymentHash)),
|
||||||
Some(description.fold(Description, DescriptionHash)),
|
Some(description.fold(Description, DescriptionHash)),
|
||||||
Some(PaymentSecret(paymentSecret)),
|
Some(PaymentSecret(paymentSecret)),
|
||||||
|
paymentMetadata.map(PaymentMetadata),
|
||||||
fallbackAddress.map(FallbackAddress(_)),
|
fallbackAddress.map(FallbackAddress(_)),
|
||||||
expirySeconds.map(Expiry(_)),
|
expirySeconds.map(Expiry(_)),
|
||||||
Some(MinFinalCltvExpiry(minFinalCltvExpiryDelta.toInt)),
|
Some(MinFinalCltvExpiry(minFinalCltvExpiryDelta.toInt)),
|
||||||
|
@ -193,7 +204,6 @@ object PaymentRequest {
|
||||||
case class InvalidTag23(data: BitVector) extends InvalidTaggedField
|
case class InvalidTag23(data: BitVector) extends InvalidTaggedField
|
||||||
case class UnknownTag25(data: BitVector) extends UnknownTaggedField
|
case class UnknownTag25(data: BitVector) extends UnknownTaggedField
|
||||||
case class UnknownTag26(data: BitVector) extends UnknownTaggedField
|
case class UnknownTag26(data: BitVector) extends UnknownTaggedField
|
||||||
case class UnknownTag27(data: BitVector) extends UnknownTaggedField
|
|
||||||
case class UnknownTag28(data: BitVector) extends UnknownTaggedField
|
case class UnknownTag28(data: BitVector) extends UnknownTaggedField
|
||||||
case class UnknownTag29(data: BitVector) extends UnknownTaggedField
|
case class UnknownTag29(data: BitVector) extends UnknownTaggedField
|
||||||
case class UnknownTag30(data: BitVector) extends UnknownTaggedField
|
case class UnknownTag30(data: BitVector) extends UnknownTaggedField
|
||||||
|
@ -229,6 +239,11 @@ object PaymentRequest {
|
||||||
*/
|
*/
|
||||||
case class DescriptionHash(hash: ByteVector32) extends TaggedField
|
case class DescriptionHash(hash: ByteVector32) extends TaggedField
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Additional metadata to attach to the payment.
|
||||||
|
*/
|
||||||
|
case class PaymentMetadata(data: ByteVector) extends TaggedField
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Fallback Payment that specifies a fallback payment address to be used if LN payment cannot be processed
|
* Fallback Payment that specifies a fallback payment address to be used if LN payment cannot be processed
|
||||||
*/
|
*/
|
||||||
|
@ -355,8 +370,8 @@ object PaymentRequest {
|
||||||
}
|
}
|
||||||
|
|
||||||
object PaymentRequestFeatures {
|
object PaymentRequestFeatures {
|
||||||
def apply(features: Int*): PaymentRequestFeatures = PaymentRequestFeatures(long2bits(features.foldLeft(0L) {
|
def apply(features: Map[Feature with InvoiceFeature, FeatureSupport]): PaymentRequestFeatures = PaymentRequestFeatures(long2bits(features.foldLeft(0L) {
|
||||||
case (current, feature) => current + (1L << feature)
|
case (current, (feature, support)) => current + (1L << feature.supportBit(support))
|
||||||
}))
|
}))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -429,7 +444,7 @@ object PaymentRequest {
|
||||||
.typecase(24, dataCodec(bits).as[MinFinalCltvExpiry])
|
.typecase(24, dataCodec(bits).as[MinFinalCltvExpiry])
|
||||||
.typecase(25, dataCodec(bits).as[UnknownTag25])
|
.typecase(25, dataCodec(bits).as[UnknownTag25])
|
||||||
.typecase(26, dataCodec(bits).as[UnknownTag26])
|
.typecase(26, dataCodec(bits).as[UnknownTag26])
|
||||||
.typecase(27, dataCodec(bits).as[UnknownTag27])
|
.typecase(27, dataCodec(alignedBytesCodec(bytes)).as[PaymentMetadata])
|
||||||
.typecase(28, dataCodec(bits).as[UnknownTag28])
|
.typecase(28, dataCodec(bits).as[UnknownTag28])
|
||||||
.typecase(29, dataCodec(bits).as[UnknownTag29])
|
.typecase(29, dataCodec(bits).as[UnknownTag29])
|
||||||
.typecase(30, dataCodec(bits).as[UnknownTag30])
|
.typecase(30, dataCodec(bits).as[UnknownTag30])
|
||||||
|
|
|
@ -27,9 +27,10 @@ import fr.acinq.eclair.channel.{CMD_FAIL_HTLC, CMD_FULFILL_HTLC, RES_SUCCESS}
|
||||||
import fr.acinq.eclair.db._
|
import fr.acinq.eclair.db._
|
||||||
import fr.acinq.eclair.payment.Monitoring.{Metrics, Tags}
|
import fr.acinq.eclair.payment.Monitoring.{Metrics, Tags}
|
||||||
import fr.acinq.eclair.payment.PaymentRequest.{ExtraHop, PaymentRequestFeatures}
|
import fr.acinq.eclair.payment.PaymentRequest.{ExtraHop, PaymentRequestFeatures}
|
||||||
import fr.acinq.eclair.payment.{IncomingPaymentPacket, PaymentReceived, PaymentRequest}
|
import fr.acinq.eclair.payment.{IncomingPaymentPacket, PaymentMetadataReceived, PaymentReceived, PaymentRequest}
|
||||||
import fr.acinq.eclair.wire.protocol._
|
import fr.acinq.eclair.wire.protocol._
|
||||||
import fr.acinq.eclair.{Features, Logs, MilliSatoshi, NodeParams, randomBytes32}
|
import fr.acinq.eclair.{Feature, FeatureSupport, Features, InvoiceFeature, Logs, MilliSatoshi, NodeParams, randomBytes32}
|
||||||
|
import scodec.bits.HexStringSyntax
|
||||||
|
|
||||||
import scala.util.{Failure, Success, Try}
|
import scala.util.{Failure, Success, Try}
|
||||||
|
|
||||||
|
@ -71,7 +72,12 @@ class MultiPartHandler(nodeParams: NodeParams, register: ActorRef, db: IncomingP
|
||||||
Metrics.PaymentFailed.withTag(Tags.Direction, Tags.Directions.Received).withTag(Tags.Failure, Tags.FailureType(cmdFail)).increment()
|
Metrics.PaymentFailed.withTag(Tags.Direction, Tags.Directions.Received).withTag(Tags.Failure, Tags.FailureType(cmdFail)).increment()
|
||||||
PendingCommandsDb.safeSend(register, nodeParams.db.pendingCommands, p.add.channelId, cmdFail)
|
PendingCommandsDb.safeSend(register, nodeParams.db.pendingCommands, p.add.channelId, cmdFail)
|
||||||
case None =>
|
case None =>
|
||||||
log.info("received payment for amount={} totalAmount={}", p.add.amountMsat, p.payload.totalAmount)
|
// We log whether the sender included the payment metadata field.
|
||||||
|
// We always set it in our invoices to test whether senders support it.
|
||||||
|
// Once all incoming payments correctly set that field, we can make it mandatory.
|
||||||
|
log.info("received payment for amount={} totalAmount={} paymentMetadata={}", p.add.amountMsat, p.payload.totalAmount, p.payload.paymentMetadata.map(_.toHex).getOrElse("none"))
|
||||||
|
Metrics.PaymentHtlcReceived.withTag(Tags.PaymentMetadataIncluded, p.payload.paymentMetadata.nonEmpty).increment()
|
||||||
|
p.payload.paymentMetadata.foreach(metadata => ctx.system.eventStream.publish(PaymentMetadataReceived(p.add.paymentHash, metadata)))
|
||||||
pendingPayments.get(p.add.paymentHash) match {
|
pendingPayments.get(p.add.paymentHash) match {
|
||||||
case Some((_, handler)) =>
|
case Some((_, handler)) =>
|
||||||
handler ! MultiPartPaymentFSM.HtlcPart(p.payload.totalAmount, p.add)
|
handler ! MultiPartPaymentFSM.HtlcPart(p.payload.totalAmount, p.add)
|
||||||
|
@ -86,13 +92,13 @@ class MultiPartHandler(nodeParams: NodeParams, register: ActorRef, db: IncomingP
|
||||||
val amount = Some(p.payload.totalAmount)
|
val amount = Some(p.payload.totalAmount)
|
||||||
val paymentHash = Crypto.sha256(paymentPreimage)
|
val paymentHash = Crypto.sha256(paymentPreimage)
|
||||||
val desc = Left("Donation")
|
val desc = Left("Donation")
|
||||||
val features = if (nodeParams.features.hasFeature(Features.BasicMultiPartPayment)) {
|
val features: Map[Feature with InvoiceFeature, FeatureSupport] = if (nodeParams.features.hasFeature(Features.BasicMultiPartPayment)) {
|
||||||
PaymentRequestFeatures(Features.BasicMultiPartPayment.optional, Features.PaymentSecret.mandatory, Features.VariableLengthOnion.mandatory)
|
Map(Features.BasicMultiPartPayment -> FeatureSupport.Optional, Features.PaymentSecret -> FeatureSupport.Mandatory, Features.VariableLengthOnion -> FeatureSupport.Mandatory)
|
||||||
} else {
|
} else {
|
||||||
PaymentRequestFeatures(Features.PaymentSecret.mandatory, Features.VariableLengthOnion.mandatory)
|
Map(Features.PaymentSecret -> FeatureSupport.Mandatory, Features.VariableLengthOnion -> FeatureSupport.Mandatory)
|
||||||
}
|
}
|
||||||
// Insert a fake invoice and then restart the incoming payment handler
|
// Insert a fake invoice and then restart the incoming payment handler
|
||||||
val paymentRequest = PaymentRequest(nodeParams.chainHash, amount, paymentHash, nodeParams.privateKey, desc, nodeParams.minFinalExpiryDelta, paymentSecret = p.payload.paymentSecret, features = features)
|
val paymentRequest = PaymentRequest(nodeParams.chainHash, amount, paymentHash, nodeParams.privateKey, desc, nodeParams.minFinalExpiryDelta, paymentSecret = p.payload.paymentSecret, features = PaymentRequestFeatures(features))
|
||||||
log.debug("generated fake payment request={} from amount={} (KeySend)", PaymentRequest.write(paymentRequest), amount)
|
log.debug("generated fake payment request={} from amount={} (KeySend)", PaymentRequest.write(paymentRequest), amount)
|
||||||
db.addIncomingPayment(paymentRequest, paymentPreimage, paymentType = PaymentType.KeySend)
|
db.addIncomingPayment(paymentRequest, paymentPreimage, paymentType = PaymentType.KeySend)
|
||||||
ctx.self ! p
|
ctx.self ! p
|
||||||
|
@ -223,14 +229,25 @@ object MultiPartHandler {
|
||||||
val paymentPreimage = paymentPreimage_opt.getOrElse(randomBytes32())
|
val paymentPreimage = paymentPreimage_opt.getOrElse(randomBytes32())
|
||||||
val paymentHash = Crypto.sha256(paymentPreimage)
|
val paymentHash = Crypto.sha256(paymentPreimage)
|
||||||
val expirySeconds = expirySeconds_opt.getOrElse(nodeParams.paymentRequestExpiry.toSeconds)
|
val expirySeconds = expirySeconds_opt.getOrElse(nodeParams.paymentRequestExpiry.toSeconds)
|
||||||
val features = {
|
val paymentMetadata = hex"2a"
|
||||||
val f1 = Seq(Features.PaymentSecret.mandatory, Features.VariableLengthOnion.mandatory)
|
val invoiceFeatures = if (nodeParams.enableTrampolinePayment) {
|
||||||
val allowMultiPart = nodeParams.features.hasFeature(Features.BasicMultiPartPayment)
|
nodeParams.features.invoiceFeatures() + (Features.TrampolinePayment -> FeatureSupport.Optional)
|
||||||
val f2 = if (allowMultiPart) Seq(Features.BasicMultiPartPayment.optional) else Nil
|
} else {
|
||||||
val f3 = if (nodeParams.enableTrampolinePayment) Seq(Features.TrampolinePayment.optional) else Nil
|
nodeParams.features.invoiceFeatures()
|
||||||
PaymentRequest.PaymentRequestFeatures(f1 ++ f2 ++ f3: _*)
|
|
||||||
}
|
}
|
||||||
val paymentRequest = PaymentRequest(nodeParams.chainHash, amount_opt, paymentHash, nodeParams.privateKey, description, nodeParams.minFinalExpiryDelta, fallbackAddress_opt, expirySeconds = Some(expirySeconds), extraHops = extraHops, features = features)
|
val paymentRequest = PaymentRequest(
|
||||||
|
nodeParams.chainHash,
|
||||||
|
amount_opt,
|
||||||
|
paymentHash,
|
||||||
|
nodeParams.privateKey,
|
||||||
|
description,
|
||||||
|
nodeParams.minFinalExpiryDelta,
|
||||||
|
fallbackAddress_opt,
|
||||||
|
expirySeconds = Some(expirySeconds),
|
||||||
|
extraHops = extraHops,
|
||||||
|
paymentMetadata = Some(paymentMetadata),
|
||||||
|
features = PaymentRequestFeatures(invoiceFeatures)
|
||||||
|
)
|
||||||
context.log.debug("generated payment request={} from amount={}", PaymentRequest.write(paymentRequest), amount_opt)
|
context.log.debug("generated payment request={} from amount={}", PaymentRequest.write(paymentRequest), amount_opt)
|
||||||
nodeParams.db.payments.addIncomingPayment(paymentRequest, paymentPreimage, paymentType)
|
nodeParams.db.payments.addIncomingPayment(paymentRequest, paymentPreimage, paymentType)
|
||||||
paymentRequest
|
paymentRequest
|
||||||
|
|
|
@ -271,13 +271,13 @@ class NodeRelay private(nodeParams: NodeParams,
|
||||||
val paymentSecret = payloadOut.paymentSecret.get // NB: we've verified that there was a payment secret in validateRelay
|
val paymentSecret = payloadOut.paymentSecret.get // NB: we've verified that there was a payment secret in validateRelay
|
||||||
if (Features(features).hasFeature(Features.BasicMultiPartPayment)) {
|
if (Features(features).hasFeature(Features.BasicMultiPartPayment)) {
|
||||||
context.log.debug("sending the payment to non-trampoline recipient using MPP")
|
context.log.debug("sending the payment to non-trampoline recipient using MPP")
|
||||||
val payment = SendMultiPartPayment(payFsmAdapters, paymentSecret, payloadOut.outgoingNodeId, payloadOut.amountToForward, payloadOut.outgoingCltv, nodeParams.maxPaymentAttempts, routingHints, routeParams)
|
val payment = SendMultiPartPayment(payFsmAdapters, paymentSecret, payloadOut.outgoingNodeId, payloadOut.amountToForward, payloadOut.outgoingCltv, nodeParams.maxPaymentAttempts, payloadOut.paymentMetadata, routingHints, routeParams)
|
||||||
val payFSM = outgoingPaymentFactory.spawnOutgoingPayFSM(context, paymentCfg, multiPart = true)
|
val payFSM = outgoingPaymentFactory.spawnOutgoingPayFSM(context, paymentCfg, multiPart = true)
|
||||||
payFSM ! payment
|
payFSM ! payment
|
||||||
payFSM
|
payFSM
|
||||||
} else {
|
} else {
|
||||||
context.log.debug("sending the payment to non-trampoline recipient without MPP")
|
context.log.debug("sending the payment to non-trampoline recipient without MPP")
|
||||||
val finalPayload = PaymentOnion.createSinglePartPayload(payloadOut.amountToForward, payloadOut.outgoingCltv, paymentSecret)
|
val finalPayload = PaymentOnion.createSinglePartPayload(payloadOut.amountToForward, payloadOut.outgoingCltv, paymentSecret, payloadOut.paymentMetadata)
|
||||||
val payment = SendPaymentToNode(payFsmAdapters, payloadOut.outgoingNodeId, finalPayload, nodeParams.maxPaymentAttempts, routingHints, routeParams)
|
val payment = SendPaymentToNode(payFsmAdapters, payloadOut.outgoingNodeId, finalPayload, nodeParams.maxPaymentAttempts, routingHints, routeParams)
|
||||||
val payFSM = outgoingPaymentFactory.spawnOutgoingPayFSM(context, paymentCfg, multiPart = false)
|
val payFSM = outgoingPaymentFactory.spawnOutgoingPayFSM(context, paymentCfg, multiPart = false)
|
||||||
payFSM ! payment
|
payFSM ! payment
|
||||||
|
@ -287,7 +287,7 @@ class NodeRelay private(nodeParams: NodeParams,
|
||||||
context.log.debug("sending the payment to the next trampoline node")
|
context.log.debug("sending the payment to the next trampoline node")
|
||||||
val payFSM = outgoingPaymentFactory.spawnOutgoingPayFSM(context, paymentCfg, multiPart = true)
|
val payFSM = outgoingPaymentFactory.spawnOutgoingPayFSM(context, paymentCfg, multiPart = true)
|
||||||
val paymentSecret = randomBytes32() // we generate a new secret to protect against probing attacks
|
val paymentSecret = randomBytes32() // we generate a new secret to protect against probing attacks
|
||||||
val payment = SendMultiPartPayment(payFsmAdapters, paymentSecret, payloadOut.outgoingNodeId, payloadOut.amountToForward, payloadOut.outgoingCltv, nodeParams.maxPaymentAttempts, routeParams = routeParams, additionalTlvs = Seq(OnionPaymentPayloadTlv.TrampolineOnion(packetOut)))
|
val payment = SendMultiPartPayment(payFsmAdapters, paymentSecret, payloadOut.outgoingNodeId, payloadOut.amountToForward, payloadOut.outgoingCltv, nodeParams.maxPaymentAttempts, None, routeParams = routeParams, additionalTlvs = Seq(OnionPaymentPayloadTlv.TrampolineOnion(packetOut)))
|
||||||
payFSM ! payment
|
payFSM ! payment
|
||||||
payFSM
|
payFSM
|
||||||
}
|
}
|
||||||
|
|
|
@ -32,6 +32,7 @@ import fr.acinq.eclair.payment.send.PaymentLifecycle.SendPaymentToRoute
|
||||||
import fr.acinq.eclair.router.Router._
|
import fr.acinq.eclair.router.Router._
|
||||||
import fr.acinq.eclair.wire.protocol._
|
import fr.acinq.eclair.wire.protocol._
|
||||||
import fr.acinq.eclair.{CltvExpiry, FSMDiagnosticActorLogging, Logs, MilliSatoshi, MilliSatoshiLong, NodeParams, TimestampMilli}
|
import fr.acinq.eclair.{CltvExpiry, FSMDiagnosticActorLogging, Logs, MilliSatoshi, MilliSatoshiLong, NodeParams, TimestampMilli}
|
||||||
|
import scodec.bits.ByteVector
|
||||||
|
|
||||||
import java.util.UUID
|
import java.util.UUID
|
||||||
import java.util.concurrent.TimeUnit
|
import java.util.concurrent.TimeUnit
|
||||||
|
@ -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
|
* 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.
|
* algorithm will run to find suitable payment routes.
|
||||||
*
|
*
|
||||||
* @param paymentSecret payment secret to protect against probing (usually from a Bolt 11 invoice).
|
* @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
|
* @param targetNodeId target node (may be the final recipient when using source-routing, or the first trampoline
|
||||||
* node when using trampoline).
|
* node when using trampoline).
|
||||||
* @param totalAmount total amount to send to the target node.
|
* @param totalAmount total amount to send to the target node.
|
||||||
* @param targetExpiry expiry at the target node (CLTV for the target node's received HTLCs).
|
* @param targetExpiry expiry at the target node (CLTV for the target node's received HTLCs).
|
||||||
* @param maxAttempts maximum number of retries.
|
* @param maxAttempts maximum number of retries.
|
||||||
* @param assistedRoutes routing hints (usually from a Bolt 11 invoice).
|
* @param paymentMetadata payment metadata (usually from the Bolt 11 invoice).
|
||||||
* @param routeParams parameters to fine-tune the routing algorithm.
|
* @param assistedRoutes routing hints (usually from a Bolt 11 invoice).
|
||||||
* @param additionalTlvs when provided, additional tlvs that will be added to the onion sent to the target node.
|
* @param routeParams parameters to fine-tune the routing algorithm.
|
||||||
* @param userCustomTlvs when provided, additional user-defined custom tlvs that will be added to the onion sent to the target node.
|
* @param additionalTlvs when provided, additional tlvs that will be added to the onion sent to the target node.
|
||||||
|
* @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,
|
case class SendMultiPartPayment(replyTo: ActorRef,
|
||||||
paymentSecret: ByteVector32,
|
paymentSecret: ByteVector32,
|
||||||
|
@ -318,6 +320,7 @@ object MultiPartPaymentLifecycle {
|
||||||
totalAmount: MilliSatoshi,
|
totalAmount: MilliSatoshi,
|
||||||
targetExpiry: CltvExpiry,
|
targetExpiry: CltvExpiry,
|
||||||
maxAttempts: Int,
|
maxAttempts: Int,
|
||||||
|
paymentMetadata: Option[ByteVector],
|
||||||
assistedRoutes: Seq[Seq[ExtraHop]] = Nil,
|
assistedRoutes: Seq[Seq[ExtraHop]] = Nil,
|
||||||
routeParams: RouteParams,
|
routeParams: RouteParams,
|
||||||
additionalTlvs: Seq[OnionPaymentPayloadTlv] = Nil,
|
additionalTlvs: Seq[OnionPaymentPayloadTlv] = Nil,
|
||||||
|
@ -400,7 +403,7 @@ object MultiPartPaymentLifecycle {
|
||||||
Some(cfg.paymentContext))
|
Some(cfg.paymentContext))
|
||||||
|
|
||||||
private def createChildPayment(replyTo: ActorRef, route: Route, request: SendMultiPartPayment): SendPaymentToRoute = {
|
private def createChildPayment(replyTo: ActorRef, route: Route, request: SendMultiPartPayment): SendPaymentToRoute = {
|
||||||
val finalPayload = PaymentOnion.createMultiPartPayload(route.amount, request.totalAmount, request.targetExpiry, request.paymentSecret, request.additionalTlvs, request.userCustomTlvs)
|
val finalPayload = PaymentOnion.createMultiPartPayload(route.amount, request.totalAmount, request.targetExpiry, request.paymentSecret, request.paymentMetadata, request.additionalTlvs, request.userCustomTlvs)
|
||||||
SendPaymentToRoute(replyTo, Right(route), finalPayload)
|
SendPaymentToRoute(replyTo, Right(route), finalPayload)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -59,9 +59,9 @@ class PaymentInitiator(nodeParams: NodeParams, outgoingPaymentFactory: PaymentIn
|
||||||
sender() ! PaymentFailed(paymentId, r.paymentHash, LocalFailure(r.recipientAmount, Nil, PaymentSecretMissing) :: Nil)
|
sender() ! PaymentFailed(paymentId, r.paymentHash, LocalFailure(r.recipientAmount, Nil, PaymentSecretMissing) :: Nil)
|
||||||
case Some(paymentSecret) if r.paymentRequest.features.allowMultiPart && nodeParams.features.hasFeature(BasicMultiPartPayment) =>
|
case Some(paymentSecret) if r.paymentRequest.features.allowMultiPart && nodeParams.features.hasFeature(BasicMultiPartPayment) =>
|
||||||
val fsm = outgoingPaymentFactory.spawnOutgoingMultiPartPayment(context, paymentCfg)
|
val fsm = outgoingPaymentFactory.spawnOutgoingMultiPartPayment(context, paymentCfg)
|
||||||
fsm ! SendMultiPartPayment(sender(), paymentSecret, r.recipientNodeId, r.recipientAmount, finalExpiry, r.maxAttempts, r.assistedRoutes, r.routeParams, userCustomTlvs = r.userCustomTlvs)
|
fsm ! SendMultiPartPayment(sender(), paymentSecret, r.recipientNodeId, r.recipientAmount, finalExpiry, r.maxAttempts, r.paymentRequest.paymentMetadata, r.assistedRoutes, r.routeParams, userCustomTlvs = r.userCustomTlvs)
|
||||||
case Some(paymentSecret) =>
|
case Some(paymentSecret) =>
|
||||||
val finalPayload = PaymentOnion.createSinglePartPayload(r.recipientAmount, finalExpiry, paymentSecret, r.userCustomTlvs)
|
val finalPayload = PaymentOnion.createSinglePartPayload(r.recipientAmount, finalExpiry, paymentSecret, r.paymentRequest.paymentMetadata, r.userCustomTlvs)
|
||||||
val fsm = outgoingPaymentFactory.spawnOutgoingPayment(context, paymentCfg)
|
val fsm = outgoingPaymentFactory.spawnOutgoingPayment(context, paymentCfg)
|
||||||
fsm ! PaymentLifecycle.SendPaymentToNode(sender(), r.recipientNodeId, finalPayload, r.maxAttempts, r.assistedRoutes, r.routeParams)
|
fsm ! PaymentLifecycle.SendPaymentToNode(sender(), r.recipientNodeId, finalPayload, r.maxAttempts, r.assistedRoutes, r.routeParams)
|
||||||
}
|
}
|
||||||
|
@ -140,11 +140,11 @@ class PaymentInitiator(nodeParams: NodeParams, outgoingPaymentFactory: PaymentIn
|
||||||
sender() ! SendPaymentToRouteResponse(paymentId, parentPaymentId, Some(trampolineSecret))
|
sender() ! SendPaymentToRouteResponse(paymentId, parentPaymentId, Some(trampolineSecret))
|
||||||
val payFsm = outgoingPaymentFactory.spawnOutgoingPayment(context, paymentCfg)
|
val payFsm = outgoingPaymentFactory.spawnOutgoingPayment(context, paymentCfg)
|
||||||
val (trampolineAmount, trampolineExpiry, trampolineOnion) = buildTrampolinePayment(r, trampoline, r.trampolineFees, r.trampolineExpiryDelta)
|
val (trampolineAmount, trampolineExpiry, trampolineOnion) = buildTrampolinePayment(r, trampoline, r.trampolineFees, r.trampolineExpiryDelta)
|
||||||
payFsm ! PaymentLifecycle.SendPaymentToRoute(sender(), Left(r.route), PaymentOnion.createMultiPartPayload(r.amount, trampolineAmount, trampolineExpiry, trampolineSecret, Seq(OnionPaymentPayloadTlv.TrampolineOnion(trampolineOnion))), r.paymentRequest.routingInfo)
|
payFsm ! PaymentLifecycle.SendPaymentToRoute(sender(), Left(r.route), PaymentOnion.createMultiPartPayload(r.amount, trampolineAmount, trampolineExpiry, trampolineSecret, r.paymentRequest.paymentMetadata, Seq(OnionPaymentPayloadTlv.TrampolineOnion(trampolineOnion))), r.paymentRequest.routingInfo)
|
||||||
case Nil =>
|
case Nil =>
|
||||||
sender() ! SendPaymentToRouteResponse(paymentId, parentPaymentId, None)
|
sender() ! SendPaymentToRouteResponse(paymentId, parentPaymentId, None)
|
||||||
val payFsm = outgoingPaymentFactory.spawnOutgoingPayment(context, paymentCfg)
|
val payFsm = outgoingPaymentFactory.spawnOutgoingPayment(context, paymentCfg)
|
||||||
payFsm ! PaymentLifecycle.SendPaymentToRoute(sender(), Left(r.route), PaymentOnion.createMultiPartPayload(r.amount, r.recipientAmount, finalExpiry, r.paymentRequest.paymentSecret.get), r.paymentRequest.routingInfo)
|
payFsm ! PaymentLifecycle.SendPaymentToRoute(sender(), Left(r.route), PaymentOnion.createMultiPartPayload(r.amount, r.recipientAmount, finalExpiry, r.paymentRequest.paymentSecret.get, r.paymentRequest.paymentMetadata), r.paymentRequest.routingInfo)
|
||||||
case _ =>
|
case _ =>
|
||||||
sender() ! PaymentFailed(paymentId, r.paymentHash, LocalFailure(r.recipientAmount, Nil, TrampolineMultiNodeNotSupported) :: Nil)
|
sender() ! PaymentFailed(paymentId, r.paymentHash, LocalFailure(r.recipientAmount, Nil, TrampolineMultiNodeNotSupported) :: Nil)
|
||||||
}
|
}
|
||||||
|
@ -156,9 +156,9 @@ class PaymentInitiator(nodeParams: NodeParams, outgoingPaymentFactory: PaymentIn
|
||||||
NodeHop(trampolineNodeId, r.recipientNodeId, trampolineExpiryDelta, trampolineFees) // for now we only use a single trampoline hop
|
NodeHop(trampolineNodeId, r.recipientNodeId, trampolineExpiryDelta, trampolineFees) // for now we only use a single trampoline hop
|
||||||
)
|
)
|
||||||
val finalPayload = if (r.paymentRequest.features.allowMultiPart) {
|
val finalPayload = if (r.paymentRequest.features.allowMultiPart) {
|
||||||
PaymentOnion.createMultiPartPayload(r.recipientAmount, r.recipientAmount, r.finalExpiry(nodeParams.currentBlockHeight), r.paymentRequest.paymentSecret.get)
|
PaymentOnion.createMultiPartPayload(r.recipientAmount, r.recipientAmount, r.finalExpiry(nodeParams.currentBlockHeight), r.paymentRequest.paymentSecret.get, r.paymentRequest.paymentMetadata)
|
||||||
} else {
|
} else {
|
||||||
PaymentOnion.createSinglePartPayload(r.recipientAmount, r.finalExpiry(nodeParams.currentBlockHeight), r.paymentRequest.paymentSecret.get)
|
PaymentOnion.createSinglePartPayload(r.recipientAmount, r.finalExpiry(nodeParams.currentBlockHeight), r.paymentRequest.paymentSecret.get, r.paymentRequest.paymentMetadata)
|
||||||
}
|
}
|
||||||
// We assume that the trampoline node supports multi-part payments (it should).
|
// We assume that the trampoline node supports multi-part payments (it should).
|
||||||
val (trampolineAmount, trampolineExpiry, trampolineOnion) = if (r.paymentRequest.features.allowTrampoline) {
|
val (trampolineAmount, trampolineExpiry, trampolineOnion) = if (r.paymentRequest.features.allowTrampoline) {
|
||||||
|
@ -175,7 +175,7 @@ class PaymentInitiator(nodeParams: NodeParams, outgoingPaymentFactory: PaymentIn
|
||||||
val trampolineSecret = randomBytes32()
|
val trampolineSecret = randomBytes32()
|
||||||
val (trampolineAmount, trampolineExpiry, trampolineOnion) = buildTrampolinePayment(r, r.trampolineNodeId, trampolineFees, trampolineExpiryDelta)
|
val (trampolineAmount, trampolineExpiry, trampolineOnion) = buildTrampolinePayment(r, r.trampolineNodeId, trampolineFees, trampolineExpiryDelta)
|
||||||
val fsm = outgoingPaymentFactory.spawnOutgoingMultiPartPayment(context, paymentCfg)
|
val fsm = outgoingPaymentFactory.spawnOutgoingMultiPartPayment(context, paymentCfg)
|
||||||
fsm ! SendMultiPartPayment(self, trampolineSecret, r.trampolineNodeId, trampolineAmount, trampolineExpiry, nodeParams.maxPaymentAttempts, r.paymentRequest.routingInfo, r.routeParams, Seq(OnionPaymentPayloadTlv.TrampolineOnion(trampolineOnion)))
|
fsm ! SendMultiPartPayment(self, trampolineSecret, r.trampolineNodeId, trampolineAmount, trampolineExpiry, nodeParams.maxPaymentAttempts, r.paymentRequest.paymentMetadata, r.paymentRequest.routingInfo, r.routeParams, Seq(OnionPaymentPayloadTlv.TrampolineOnion(trampolineOnion)))
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
|
@ -76,7 +76,8 @@ object Announcements {
|
||||||
case address@(_: Tor2) => (3, address)
|
case address@(_: Tor2) => (3, address)
|
||||||
case address@(_: Tor3) => (4, address)
|
case address@(_: Tor3) => (4, address)
|
||||||
}.sortBy(_._1).map(_._2)
|
}.sortBy(_._1).map(_._2)
|
||||||
val witness = nodeAnnouncementWitnessEncode(timestamp, nodeSecret.publicKey, color, alias, features, sortedAddresses, TlvStream.empty)
|
val nodeAnnouncementFeatures = features.nodeAnnouncementFeatures()
|
||||||
|
val witness = nodeAnnouncementWitnessEncode(timestamp, nodeSecret.publicKey, color, alias, nodeAnnouncementFeatures, sortedAddresses, TlvStream.empty)
|
||||||
val sig = Crypto.sign(witness, nodeSecret)
|
val sig = Crypto.sign(witness, nodeSecret)
|
||||||
NodeAnnouncement(
|
NodeAnnouncement(
|
||||||
signature = sig,
|
signature = sig,
|
||||||
|
@ -84,7 +85,7 @@ object Announcements {
|
||||||
nodeId = nodeSecret.publicKey,
|
nodeId = nodeSecret.publicKey,
|
||||||
rgbColor = color,
|
rgbColor = color,
|
||||||
alias = alias,
|
alias = alias,
|
||||||
features = features,
|
features = nodeAnnouncementFeatures,
|
||||||
addresses = sortedAddresses
|
addresses = sortedAddresses
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
|
@ -155,6 +155,12 @@ object OnionPaymentPayloadTlv {
|
||||||
/** Id of the next node. */
|
/** Id of the next node. */
|
||||||
case class OutgoingNodeId(nodeId: PublicKey) extends OnionPaymentPayloadTlv
|
case class OutgoingNodeId(nodeId: PublicKey) extends OnionPaymentPayloadTlv
|
||||||
|
|
||||||
|
/**
|
||||||
|
* When payment metadata is included in a Bolt 11 invoice, we should send it as-is to the recipient.
|
||||||
|
* This lets recipients generate invoices without having to store anything on their side until the invoice is paid.
|
||||||
|
*/
|
||||||
|
case class PaymentMetadata(data: ByteVector) extends OnionPaymentPayloadTlv
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Invoice feature bits. Only included for intermediate trampoline nodes when they should convert to a legacy payment
|
* Invoice feature bits. Only included for intermediate trampoline nodes when they should convert to a legacy payment
|
||||||
* because the final recipient doesn't support trampoline.
|
* because the final recipient doesn't support trampoline.
|
||||||
|
@ -242,6 +248,7 @@ object PaymentOnion {
|
||||||
val paymentSecret: ByteVector32
|
val paymentSecret: ByteVector32
|
||||||
val totalAmount: MilliSatoshi
|
val totalAmount: MilliSatoshi
|
||||||
val paymentPreimage: Option[ByteVector32]
|
val paymentPreimage: Option[ByteVector32]
|
||||||
|
val paymentMetadata: Option[ByteVector]
|
||||||
}
|
}
|
||||||
|
|
||||||
case class RelayLegacyPayload(outgoingChannelId: ShortChannelId, amountToForward: MilliSatoshi, outgoingCltv: CltvExpiry) extends ChannelRelayPayload with LegacyFormat
|
case class RelayLegacyPayload(outgoingChannelId: ShortChannelId, amountToForward: MilliSatoshi, outgoingCltv: CltvExpiry) extends ChannelRelayPayload with LegacyFormat
|
||||||
|
@ -267,6 +274,7 @@ object PaymentOnion {
|
||||||
case totalAmount => totalAmount
|
case totalAmount => totalAmount
|
||||||
}).getOrElse(amountToForward)
|
}).getOrElse(amountToForward)
|
||||||
val paymentSecret = records.get[PaymentData].map(_.secret)
|
val paymentSecret = records.get[PaymentData].map(_.secret)
|
||||||
|
val paymentMetadata = records.get[PaymentMetadata].map(_.data)
|
||||||
val invoiceFeatures = records.get[InvoiceFeatures].map(_.features)
|
val invoiceFeatures = records.get[InvoiceFeatures].map(_.features)
|
||||||
val invoiceRoutingInfo = records.get[InvoiceRoutingInfo].map(_.extraHops)
|
val invoiceRoutingInfo = records.get[InvoiceRoutingInfo].map(_.extraHops)
|
||||||
}
|
}
|
||||||
|
@ -280,6 +288,7 @@ object PaymentOnion {
|
||||||
case totalAmount => totalAmount
|
case totalAmount => totalAmount
|
||||||
}).getOrElse(amount)
|
}).getOrElse(amount)
|
||||||
override val paymentPreimage = records.get[KeySend].map(_.paymentPreimage)
|
override val paymentPreimage = records.get[KeySend].map(_.paymentPreimage)
|
||||||
|
override val paymentMetadata = records.get[PaymentMetadata].map(_.data)
|
||||||
}
|
}
|
||||||
|
|
||||||
def createNodeRelayPayload(amount: MilliSatoshi, expiry: CltvExpiry, nextNodeId: PublicKey): NodeRelayPayload =
|
def createNodeRelayPayload(amount: MilliSatoshi, expiry: CltvExpiry, nextNodeId: PublicKey): NodeRelayPayload =
|
||||||
|
@ -287,16 +296,37 @@ object PaymentOnion {
|
||||||
|
|
||||||
/** Create a trampoline inner payload instructing the trampoline node to relay via a non-trampoline payment. */
|
/** Create a trampoline inner payload instructing the trampoline node to relay via a non-trampoline payment. */
|
||||||
def createNodeRelayToNonTrampolinePayload(amount: MilliSatoshi, totalAmount: MilliSatoshi, expiry: CltvExpiry, targetNodeId: PublicKey, invoice: PaymentRequest): NodeRelayPayload = {
|
def createNodeRelayToNonTrampolinePayload(amount: MilliSatoshi, totalAmount: MilliSatoshi, expiry: CltvExpiry, targetNodeId: PublicKey, invoice: PaymentRequest): NodeRelayPayload = {
|
||||||
val tlvs = Seq[OnionPaymentPayloadTlv](AmountToForward(amount), OutgoingCltv(expiry), OutgoingNodeId(targetNodeId), InvoiceFeatures(invoice.features.toByteVector), InvoiceRoutingInfo(invoice.routingInfo.toList.map(_.toList)))
|
val tlvs = Seq(
|
||||||
val tlvs2 = invoice.paymentSecret.map(s => tlvs :+ PaymentData(s, totalAmount)).getOrElse(tlvs)
|
Some(AmountToForward(amount)),
|
||||||
NodeRelayPayload(TlvStream(tlvs2))
|
Some(OutgoingCltv(expiry)),
|
||||||
|
invoice.paymentSecret.map(s => PaymentData(s, totalAmount)),
|
||||||
|
invoice.paymentMetadata.map(m => PaymentMetadata(m)),
|
||||||
|
Some(OutgoingNodeId(targetNodeId)),
|
||||||
|
Some(InvoiceFeatures(invoice.features.toByteVector)),
|
||||||
|
Some(InvoiceRoutingInfo(invoice.routingInfo.toList.map(_.toList)))
|
||||||
|
).flatten
|
||||||
|
NodeRelayPayload(TlvStream(tlvs))
|
||||||
}
|
}
|
||||||
|
|
||||||
def createSinglePartPayload(amount: MilliSatoshi, expiry: CltvExpiry, paymentSecret: ByteVector32, userCustomTlvs: Seq[GenericTlv] = Nil): FinalPayload =
|
def createSinglePartPayload(amount: MilliSatoshi, expiry: CltvExpiry, paymentSecret: ByteVector32, paymentMetadata: Option[ByteVector], userCustomTlvs: Seq[GenericTlv] = Nil): FinalPayload = {
|
||||||
FinalTlvPayload(TlvStream(Seq(AmountToForward(amount), OutgoingCltv(expiry), PaymentData(paymentSecret, amount)), userCustomTlvs))
|
val tlvs = Seq(
|
||||||
|
Some(AmountToForward(amount)),
|
||||||
|
Some(OutgoingCltv(expiry)),
|
||||||
|
Some(PaymentData(paymentSecret, amount)),
|
||||||
|
paymentMetadata.map(m => PaymentMetadata(m))
|
||||||
|
).flatten
|
||||||
|
FinalTlvPayload(TlvStream(tlvs, userCustomTlvs))
|
||||||
|
}
|
||||||
|
|
||||||
def createMultiPartPayload(amount: MilliSatoshi, totalAmount: MilliSatoshi, expiry: CltvExpiry, paymentSecret: ByteVector32, additionalTlvs: Seq[OnionPaymentPayloadTlv] = Nil, userCustomTlvs: Seq[GenericTlv] = Nil): FinalPayload =
|
def createMultiPartPayload(amount: MilliSatoshi, totalAmount: MilliSatoshi, expiry: CltvExpiry, paymentSecret: ByteVector32, paymentMetadata: Option[ByteVector], additionalTlvs: Seq[OnionPaymentPayloadTlv] = Nil, userCustomTlvs: Seq[GenericTlv] = Nil): FinalPayload = {
|
||||||
FinalTlvPayload(TlvStream(AmountToForward(amount) +: OutgoingCltv(expiry) +: PaymentData(paymentSecret, totalAmount) +: additionalTlvs, userCustomTlvs))
|
val tlvs = Seq(
|
||||||
|
Some(AmountToForward(amount)),
|
||||||
|
Some(OutgoingCltv(expiry)),
|
||||||
|
Some(PaymentData(paymentSecret, totalAmount)),
|
||||||
|
paymentMetadata.map(m => PaymentMetadata(m))
|
||||||
|
).flatten
|
||||||
|
FinalTlvPayload(TlvStream(tlvs ++ additionalTlvs, userCustomTlvs))
|
||||||
|
}
|
||||||
|
|
||||||
/** Create a trampoline outer payload. */
|
/** Create a trampoline outer payload. */
|
||||||
def createTrampolinePayload(amount: MilliSatoshi, totalAmount: MilliSatoshi, expiry: CltvExpiry, paymentSecret: ByteVector32, trampolinePacket: OnionRoutingPacket): FinalPayload = {
|
def createTrampolinePayload(amount: MilliSatoshi, totalAmount: MilliSatoshi, expiry: CltvExpiry, paymentSecret: ByteVector32, trampolinePacket: OnionRoutingPacket): FinalPayload = {
|
||||||
|
@ -340,6 +370,8 @@ object PaymentOnionCodecs {
|
||||||
|
|
||||||
private val outgoingNodeId: Codec[OutgoingNodeId] = (("length" | constant(hex"21")) :: ("node_id" | publicKey)).as[OutgoingNodeId]
|
private val outgoingNodeId: Codec[OutgoingNodeId] = (("length" | constant(hex"21")) :: ("node_id" | publicKey)).as[OutgoingNodeId]
|
||||||
|
|
||||||
|
private val paymentMetadata: Codec[PaymentMetadata] = variableSizeBytesLong(varintoverflow, "payment_metadata" | bytes).as[PaymentMetadata]
|
||||||
|
|
||||||
private val invoiceFeatures: Codec[InvoiceFeatures] = variableSizeBytesLong(varintoverflow, bytes).as[InvoiceFeatures]
|
private val invoiceFeatures: Codec[InvoiceFeatures] = variableSizeBytesLong(varintoverflow, bytes).as[InvoiceFeatures]
|
||||||
|
|
||||||
private val invoiceRoutingInfo: Codec[InvoiceRoutingInfo] = variableSizeBytesLong(varintoverflow, list(listOfN(uint8, PaymentRequest.Codecs.extraHopCodec))).as[InvoiceRoutingInfo]
|
private val invoiceRoutingInfo: Codec[InvoiceRoutingInfo] = variableSizeBytesLong(varintoverflow, list(listOfN(uint8, PaymentRequest.Codecs.extraHopCodec))).as[InvoiceRoutingInfo]
|
||||||
|
@ -355,6 +387,7 @@ object PaymentOnionCodecs {
|
||||||
.typecase(UInt64(8), paymentData)
|
.typecase(UInt64(8), paymentData)
|
||||||
.typecase(UInt64(10), encryptedRecipientData)
|
.typecase(UInt64(10), encryptedRecipientData)
|
||||||
.typecase(UInt64(12), blindingPoint)
|
.typecase(UInt64(12), blindingPoint)
|
||||||
|
.typecase(UInt64(16), paymentMetadata)
|
||||||
// Types below aren't specified - use cautiously when deploying (be careful with backwards-compatibility).
|
// Types below aren't specified - use cautiously when deploying (be careful with backwards-compatibility).
|
||||||
.typecase(UInt64(66097), invoiceFeatures)
|
.typecase(UInt64(66097), invoiceFeatures)
|
||||||
.typecase(UInt64(66098), outgoingNodeId)
|
.typecase(UInt64(66098), outgoingNodeId)
|
||||||
|
|
|
@ -211,6 +211,22 @@ class FeaturesSpec extends AnyFunSuite {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
test("filter features based on their usage") {
|
||||||
|
val features = Features(
|
||||||
|
Map[Feature, FeatureSupport](OptionDataLossProtect -> Optional, InitialRoutingSync -> Optional, VariableLengthOnion -> Mandatory, PaymentMetadata -> Optional),
|
||||||
|
Set(UnknownFeature(753), UnknownFeature(852))
|
||||||
|
)
|
||||||
|
assert(features.initFeatures() === Features(
|
||||||
|
Map[Feature, FeatureSupport](OptionDataLossProtect -> Optional, InitialRoutingSync -> Optional, VariableLengthOnion -> Mandatory),
|
||||||
|
Set(UnknownFeature(753), UnknownFeature(852))
|
||||||
|
))
|
||||||
|
assert(features.nodeAnnouncementFeatures() === Features(
|
||||||
|
Map[Feature, FeatureSupport](OptionDataLossProtect -> Optional, VariableLengthOnion -> Mandatory),
|
||||||
|
Set(UnknownFeature(753), UnknownFeature(852))
|
||||||
|
))
|
||||||
|
assert(features.invoiceFeatures() === Map[Feature, FeatureSupport](VariableLengthOnion -> Mandatory, PaymentMetadata -> Optional))
|
||||||
|
}
|
||||||
|
|
||||||
test("features to bytes") {
|
test("features to bytes") {
|
||||||
val testCases = Map(
|
val testCases = Map(
|
||||||
hex"" -> Features.empty,
|
hex"" -> Features.empty,
|
||||||
|
|
|
@ -181,10 +181,47 @@ class StartupSpec extends AnyFunSuite {
|
||||||
)
|
)
|
||||||
|
|
||||||
val nodeParams = makeNodeParamsWithDefaults(perNodeConf.withFallback(defaultConf))
|
val nodeParams = makeNodeParamsWithDefaults(perNodeConf.withFallback(defaultConf))
|
||||||
val perNodeFeatures = nodeParams.featuresFor(PublicKey(ByteVector.fromValidHex("02aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa")))
|
val perNodeFeatures = nodeParams.initFeaturesFor(PublicKey(ByteVector.fromValidHex("02aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa")))
|
||||||
assert(perNodeFeatures === Features(VariableLengthOnion -> Mandatory, PaymentSecret -> Mandatory, BasicMultiPartPayment -> Mandatory, ChannelType -> Optional))
|
assert(perNodeFeatures === Features(VariableLengthOnion -> Mandatory, PaymentSecret -> Mandatory, BasicMultiPartPayment -> Mandatory, ChannelType -> Optional))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
test("filter out non-init features in node override") {
|
||||||
|
val perNodeConf = ConfigFactory.parseString(
|
||||||
|
"""
|
||||||
|
| override-features = [ // optional per-node features
|
||||||
|
| {
|
||||||
|
| nodeid = "02aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa",
|
||||||
|
| features {
|
||||||
|
| var_onion_optin = mandatory
|
||||||
|
| payment_secret = mandatory
|
||||||
|
| option_channel_type = optional
|
||||||
|
| option_payment_metadata = disabled
|
||||||
|
| }
|
||||||
|
| },
|
||||||
|
| {
|
||||||
|
| nodeid = "02bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb",
|
||||||
|
| features {
|
||||||
|
| var_onion_optin = mandatory
|
||||||
|
| payment_secret = mandatory
|
||||||
|
| option_channel_type = optional
|
||||||
|
| option_payment_metadata = mandatory
|
||||||
|
| }
|
||||||
|
| }
|
||||||
|
| ]
|
||||||
|
""".stripMargin
|
||||||
|
)
|
||||||
|
|
||||||
|
val nodeParams = makeNodeParamsWithDefaults(perNodeConf.withFallback(defaultConf))
|
||||||
|
val perNodeFeaturesA = nodeParams.initFeaturesFor(PublicKey(ByteVector.fromValidHex("02aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa")))
|
||||||
|
val perNodeFeaturesB = nodeParams.initFeaturesFor(PublicKey(ByteVector.fromValidHex("02bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb")))
|
||||||
|
val defaultNodeFeatures = nodeParams.initFeaturesFor(PublicKey(ByteVector.fromValidHex("02cccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccc")))
|
||||||
|
// Some features should never be sent in init messages.
|
||||||
|
assert(nodeParams.features.hasFeature(PaymentMetadata))
|
||||||
|
assert(!perNodeFeaturesA.hasFeature(PaymentMetadata))
|
||||||
|
assert(!perNodeFeaturesB.hasFeature(PaymentMetadata))
|
||||||
|
assert(!defaultNodeFeatures.hasFeature(PaymentMetadata))
|
||||||
|
}
|
||||||
|
|
||||||
test("override feerate mismatch tolerance") {
|
test("override feerate mismatch tolerance") {
|
||||||
val perNodeConf = ConfigFactory.parseString(
|
val perNodeConf = ConfigFactory.parseString(
|
||||||
"""
|
"""
|
||||||
|
|
|
@ -64,7 +64,7 @@ object TestConstants {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
case object TestFeature extends Feature {
|
case object TestFeature extends Feature with InitFeature with NodeFeature {
|
||||||
val rfcName = "test_feature"
|
val rfcName = "test_feature"
|
||||||
val mandatory = 50000
|
val mandatory = 50000
|
||||||
}
|
}
|
||||||
|
@ -106,7 +106,8 @@ object TestConstants {
|
||||||
ChannelRangeQueriesExtended -> Optional,
|
ChannelRangeQueriesExtended -> Optional,
|
||||||
VariableLengthOnion -> Mandatory,
|
VariableLengthOnion -> Mandatory,
|
||||||
PaymentSecret -> Mandatory,
|
PaymentSecret -> Mandatory,
|
||||||
BasicMultiPartPayment -> Optional
|
BasicMultiPartPayment -> Optional,
|
||||||
|
PaymentMetadata -> Optional,
|
||||||
),
|
),
|
||||||
Set(UnknownFeature(TestFeature.optional))
|
Set(UnknownFeature(TestFeature.optional))
|
||||||
),
|
),
|
||||||
|
@ -240,7 +241,8 @@ object TestConstants {
|
||||||
ChannelRangeQueriesExtended -> Optional,
|
ChannelRangeQueriesExtended -> Optional,
|
||||||
VariableLengthOnion -> Mandatory,
|
VariableLengthOnion -> Mandatory,
|
||||||
PaymentSecret -> Mandatory,
|
PaymentSecret -> Mandatory,
|
||||||
BasicMultiPartPayment -> Optional
|
BasicMultiPartPayment -> Optional,
|
||||||
|
PaymentMetadata -> Optional,
|
||||||
),
|
),
|
||||||
pluginParams = Nil,
|
pluginParams = Nil,
|
||||||
overrideFeatures = Map.empty,
|
overrideFeatures = Map.empty,
|
||||||
|
|
|
@ -122,7 +122,7 @@ class FuzzySpec extends TestKitBaseClass with FixtureAnyFunSuiteLike with Channe
|
||||||
// allow overpaying (no more than 2 times the required amount)
|
// allow overpaying (no more than 2 times the required amount)
|
||||||
val amount = requiredAmount + Random.nextInt(requiredAmount.toLong.toInt).msat
|
val amount = requiredAmount + Random.nextInt(requiredAmount.toLong.toInt).msat
|
||||||
val expiry = (Channel.MIN_CLTV_EXPIRY_DELTA + 1).toCltvExpiry(blockHeight = 400000)
|
val expiry = (Channel.MIN_CLTV_EXPIRY_DELTA + 1).toCltvExpiry(blockHeight = 400000)
|
||||||
OutgoingPaymentPacket.buildCommand(self, Upstream.Local(UUID.randomUUID()), paymentHash, ChannelHop(null, dest, null) :: Nil, PaymentOnion.createSinglePartPayload(amount, expiry, paymentSecret))._1
|
OutgoingPaymentPacket.buildCommand(self, Upstream.Local(UUID.randomUUID()), paymentHash, ChannelHop(null, dest, null) :: Nil, PaymentOnion.createSinglePartPayload(amount, expiry, paymentSecret, None))._1
|
||||||
}
|
}
|
||||||
|
|
||||||
def initiatePaymentOrStop(remaining: Int): Unit =
|
def initiatePaymentOrStop(remaining: Int): Unit =
|
||||||
|
|
|
@ -246,7 +246,7 @@ trait ChannelStateTestsHelperMethods extends TestKitBase {
|
||||||
def makeCmdAdd(amount: MilliSatoshi, cltvExpiryDelta: CltvExpiryDelta, destination: PublicKey, paymentPreimage: ByteVector32, currentBlockHeight: Long, upstream: Upstream, replyTo: ActorRef = TestProbe().ref): (ByteVector32, CMD_ADD_HTLC) = {
|
def makeCmdAdd(amount: MilliSatoshi, cltvExpiryDelta: CltvExpiryDelta, destination: PublicKey, paymentPreimage: ByteVector32, currentBlockHeight: Long, upstream: Upstream, replyTo: ActorRef = TestProbe().ref): (ByteVector32, CMD_ADD_HTLC) = {
|
||||||
val paymentHash: ByteVector32 = Crypto.sha256(paymentPreimage)
|
val paymentHash: ByteVector32 = Crypto.sha256(paymentPreimage)
|
||||||
val expiry = cltvExpiryDelta.toCltvExpiry(currentBlockHeight)
|
val expiry = cltvExpiryDelta.toCltvExpiry(currentBlockHeight)
|
||||||
val cmd = OutgoingPaymentPacket.buildCommand(replyTo, upstream, paymentHash, ChannelHop(null, destination, null) :: Nil, PaymentOnion.createSinglePartPayload(amount, expiry, randomBytes32()))._1.copy(commit = false)
|
val cmd = OutgoingPaymentPacket.buildCommand(replyTo, upstream, paymentHash, ChannelHop(null, destination, null) :: Nil, PaymentOnion.createSinglePartPayload(amount, expiry, randomBytes32(), None))._1.copy(commit = false)
|
||||||
(paymentPreimage, cmd)
|
(paymentPreimage, cmd)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -59,7 +59,7 @@ class ShutdownStateSpec extends TestKitBaseClass with FixtureAnyFunSuiteLike wit
|
||||||
val h1 = Crypto.sha256(r1)
|
val h1 = Crypto.sha256(r1)
|
||||||
val amount1 = 300000000 msat
|
val amount1 = 300000000 msat
|
||||||
val expiry1 = CltvExpiryDelta(144).toCltvExpiry(currentBlockHeight)
|
val expiry1 = CltvExpiryDelta(144).toCltvExpiry(currentBlockHeight)
|
||||||
val cmd1 = OutgoingPaymentPacket.buildCommand(sender.ref, Upstream.Local(UUID.randomUUID), h1, ChannelHop(null, TestConstants.Bob.nodeParams.nodeId, null) :: Nil, PaymentOnion.createSinglePartPayload(amount1, expiry1, randomBytes32()))._1.copy(commit = false)
|
val cmd1 = OutgoingPaymentPacket.buildCommand(sender.ref, Upstream.Local(UUID.randomUUID), h1, ChannelHop(null, TestConstants.Bob.nodeParams.nodeId, null) :: Nil, PaymentOnion.createSinglePartPayload(amount1, expiry1, randomBytes32(), None))._1.copy(commit = false)
|
||||||
alice ! cmd1
|
alice ! cmd1
|
||||||
sender.expectMsgType[RES_SUCCESS[CMD_ADD_HTLC]]
|
sender.expectMsgType[RES_SUCCESS[CMD_ADD_HTLC]]
|
||||||
val htlc1 = alice2bob.expectMsgType[UpdateAddHtlc]
|
val htlc1 = alice2bob.expectMsgType[UpdateAddHtlc]
|
||||||
|
@ -69,7 +69,7 @@ class ShutdownStateSpec extends TestKitBaseClass with FixtureAnyFunSuiteLike wit
|
||||||
val h2 = Crypto.sha256(r2)
|
val h2 = Crypto.sha256(r2)
|
||||||
val amount2 = 200000000 msat
|
val amount2 = 200000000 msat
|
||||||
val expiry2 = CltvExpiryDelta(144).toCltvExpiry(currentBlockHeight)
|
val expiry2 = CltvExpiryDelta(144).toCltvExpiry(currentBlockHeight)
|
||||||
val cmd2 = OutgoingPaymentPacket.buildCommand(sender.ref, Upstream.Local(UUID.randomUUID), h2, ChannelHop(null, TestConstants.Bob.nodeParams.nodeId, null) :: Nil, PaymentOnion.createSinglePartPayload(amount2, expiry2, randomBytes32()))._1.copy(commit = false)
|
val cmd2 = OutgoingPaymentPacket.buildCommand(sender.ref, Upstream.Local(UUID.randomUUID), h2, ChannelHop(null, TestConstants.Bob.nodeParams.nodeId, null) :: Nil, PaymentOnion.createSinglePartPayload(amount2, expiry2, randomBytes32(), None))._1.copy(commit = false)
|
||||||
alice ! cmd2
|
alice ! cmd2
|
||||||
sender.expectMsgType[RES_SUCCESS[CMD_ADD_HTLC]]
|
sender.expectMsgType[RES_SUCCESS[CMD_ADD_HTLC]]
|
||||||
val htlc2 = alice2bob.expectMsgType[UpdateAddHtlc]
|
val htlc2 = alice2bob.expectMsgType[UpdateAddHtlc]
|
||||||
|
|
|
@ -154,11 +154,15 @@ class PaymentIntegrationSpec extends IntegrationSpec {
|
||||||
}
|
}
|
||||||
|
|
||||||
test("send an HTLC A->D") {
|
test("send an HTLC A->D") {
|
||||||
val sender = TestProbe()
|
val (sender, eventListener) = (TestProbe(), TestProbe())
|
||||||
val amountMsat = 4200000.msat
|
nodes("D").system.eventStream.subscribe(eventListener.ref, classOf[PaymentMetadataReceived])
|
||||||
|
|
||||||
// first we retrieve a payment hash from D
|
// first we retrieve a payment hash from D
|
||||||
|
val amountMsat = 4200000.msat
|
||||||
sender.send(nodes("D").paymentHandler, ReceivePayment(Some(amountMsat), Left("1 coffee")))
|
sender.send(nodes("D").paymentHandler, ReceivePayment(Some(amountMsat), Left("1 coffee")))
|
||||||
val pr = sender.expectMsgType[PaymentRequest]
|
val pr = sender.expectMsgType[PaymentRequest]
|
||||||
|
assert(pr.paymentMetadata.nonEmpty)
|
||||||
|
|
||||||
// then we make the actual payment
|
// then we make the actual payment
|
||||||
sender.send(nodes("A").paymentInitiator, SendPaymentToNode(amountMsat, pr, fallbackFinalExpiryDelta = finalCltvExpiryDelta, routeParams = integrationTestRouteParams, maxAttempts = 1))
|
sender.send(nodes("A").paymentInitiator, SendPaymentToNode(amountMsat, pr, fallbackFinalExpiryDelta = finalCltvExpiryDelta, routeParams = integrationTestRouteParams, maxAttempts = 1))
|
||||||
val paymentId = sender.expectMsgType[UUID]
|
val paymentId = sender.expectMsgType[UUID]
|
||||||
|
@ -166,6 +170,7 @@ class PaymentIntegrationSpec extends IntegrationSpec {
|
||||||
assert(Crypto.sha256(preimage) === pr.paymentHash)
|
assert(Crypto.sha256(preimage) === pr.paymentHash)
|
||||||
val ps = sender.expectMsgType[PaymentSent]
|
val ps = sender.expectMsgType[PaymentSent]
|
||||||
assert(ps.id == paymentId)
|
assert(ps.id == paymentId)
|
||||||
|
eventListener.expectMsg(PaymentMetadataReceived(pr.paymentHash, pr.paymentMetadata.get))
|
||||||
}
|
}
|
||||||
|
|
||||||
test("send an HTLC A->D with an invalid expiry delta for B") {
|
test("send an HTLC A->D with an invalid expiry delta for B") {
|
||||||
|
@ -503,12 +508,14 @@ class PaymentIntegrationSpec extends IntegrationSpec {
|
||||||
|
|
||||||
test("send a trampoline payment D->B (via trampoline C)") {
|
test("send a trampoline payment D->B (via trampoline C)") {
|
||||||
val start = TimestampMilli.now()
|
val start = TimestampMilli.now()
|
||||||
val sender = TestProbe()
|
val (sender, eventListener) = (TestProbe(), TestProbe())
|
||||||
|
nodes("B").system.eventStream.subscribe(eventListener.ref, classOf[PaymentMetadataReceived])
|
||||||
val amount = 2500000000L.msat
|
val amount = 2500000000L.msat
|
||||||
sender.send(nodes("B").paymentHandler, ReceivePayment(Some(amount), Left("trampoline-MPP is so #reckless")))
|
sender.send(nodes("B").paymentHandler, ReceivePayment(Some(amount), Left("trampoline-MPP is so #reckless")))
|
||||||
val pr = sender.expectMsgType[PaymentRequest]
|
val pr = sender.expectMsgType[PaymentRequest]
|
||||||
assert(pr.features.allowMultiPart)
|
assert(pr.features.allowMultiPart)
|
||||||
assert(pr.features.allowTrampoline)
|
assert(pr.features.allowTrampoline)
|
||||||
|
assert(pr.paymentMetadata.nonEmpty)
|
||||||
|
|
||||||
val payment = SendTrampolinePayment(amount, pr, nodes("C").nodeParams.nodeId, Seq((350000 msat, CltvExpiryDelta(288))), routeParams = integrationTestRouteParams)
|
val payment = SendTrampolinePayment(amount, pr, nodes("C").nodeParams.nodeId, Seq((350000 msat, CltvExpiryDelta(288))), routeParams = integrationTestRouteParams)
|
||||||
sender.send(nodes("D").paymentInitiator, payment)
|
sender.send(nodes("D").paymentInitiator, payment)
|
||||||
|
@ -523,6 +530,7 @@ class PaymentIntegrationSpec extends IntegrationSpec {
|
||||||
awaitCond(nodes("B").nodeParams.db.payments.getIncomingPayment(pr.paymentHash).exists(_.status.isInstanceOf[IncomingPaymentStatus.Received]))
|
awaitCond(nodes("B").nodeParams.db.payments.getIncomingPayment(pr.paymentHash).exists(_.status.isInstanceOf[IncomingPaymentStatus.Received]))
|
||||||
val Some(IncomingPayment(_, _, _, _, IncomingPaymentStatus.Received(receivedAmount, _))) = nodes("B").nodeParams.db.payments.getIncomingPayment(pr.paymentHash)
|
val Some(IncomingPayment(_, _, _, _, IncomingPaymentStatus.Received(receivedAmount, _))) = nodes("B").nodeParams.db.payments.getIncomingPayment(pr.paymentHash)
|
||||||
assert(receivedAmount === amount)
|
assert(receivedAmount === amount)
|
||||||
|
eventListener.expectMsg(PaymentMetadataReceived(pr.paymentHash, pr.paymentMetadata.get))
|
||||||
|
|
||||||
awaitCond({
|
awaitCond({
|
||||||
val relayed = nodes("C").nodeParams.db.audit.listRelayed(start, TimestampMilli.now()).filter(_.paymentHash == pr.paymentHash)
|
val relayed = nodes("C").nodeParams.db.audit.listRelayed(start, TimestampMilli.now()).filter(_.paymentHash == pr.paymentHash)
|
||||||
|
@ -548,7 +556,8 @@ class PaymentIntegrationSpec extends IntegrationSpec {
|
||||||
test("send a trampoline payment F1->A (via trampoline C, non-trampoline recipient)") {
|
test("send a trampoline payment F1->A (via trampoline C, non-trampoline recipient)") {
|
||||||
// The A -> B channel is not announced.
|
// The A -> B channel is not announced.
|
||||||
val start = TimestampMilli.now()
|
val start = TimestampMilli.now()
|
||||||
val sender = TestProbe()
|
val (sender, eventListener) = (TestProbe(), TestProbe())
|
||||||
|
nodes("A").system.eventStream.subscribe(eventListener.ref, classOf[PaymentMetadataReceived])
|
||||||
sender.send(nodes("B").relayer, Relayer.GetOutgoingChannels())
|
sender.send(nodes("B").relayer, Relayer.GetOutgoingChannels())
|
||||||
val channelUpdate_ba = sender.expectMsgType[Relayer.OutgoingChannels].channels.filter(c => c.nextNodeId == nodes("A").nodeParams.nodeId).head.channelUpdate
|
val channelUpdate_ba = sender.expectMsgType[Relayer.OutgoingChannels].channels.filter(c => c.nextNodeId == nodes("A").nodeParams.nodeId).head.channelUpdate
|
||||||
val routingHints = List(List(ExtraHop(nodes("B").nodeParams.nodeId, channelUpdate_ba.shortChannelId, channelUpdate_ba.feeBaseMsat, channelUpdate_ba.feeProportionalMillionths, channelUpdate_ba.cltvExpiryDelta)))
|
val routingHints = List(List(ExtraHop(nodes("B").nodeParams.nodeId, channelUpdate_ba.shortChannelId, channelUpdate_ba.feeBaseMsat, channelUpdate_ba.feeProportionalMillionths, channelUpdate_ba.cltvExpiryDelta)))
|
||||||
|
@ -558,6 +567,7 @@ class PaymentIntegrationSpec extends IntegrationSpec {
|
||||||
val pr = sender.expectMsgType[PaymentRequest]
|
val pr = sender.expectMsgType[PaymentRequest]
|
||||||
assert(pr.features.allowMultiPart)
|
assert(pr.features.allowMultiPart)
|
||||||
assert(!pr.features.allowTrampoline)
|
assert(!pr.features.allowTrampoline)
|
||||||
|
assert(pr.paymentMetadata.nonEmpty)
|
||||||
|
|
||||||
val payment = SendTrampolinePayment(amount, pr, nodes("C").nodeParams.nodeId, Seq((1000000 msat, CltvExpiryDelta(432))), routeParams = integrationTestRouteParams)
|
val payment = SendTrampolinePayment(amount, pr, nodes("C").nodeParams.nodeId, Seq((1000000 msat, CltvExpiryDelta(432))), routeParams = integrationTestRouteParams)
|
||||||
sender.send(nodes("F").paymentInitiator, payment)
|
sender.send(nodes("F").paymentInitiator, payment)
|
||||||
|
@ -571,6 +581,7 @@ class PaymentIntegrationSpec extends IntegrationSpec {
|
||||||
awaitCond(nodes("A").nodeParams.db.payments.getIncomingPayment(pr.paymentHash).exists(_.status.isInstanceOf[IncomingPaymentStatus.Received]))
|
awaitCond(nodes("A").nodeParams.db.payments.getIncomingPayment(pr.paymentHash).exists(_.status.isInstanceOf[IncomingPaymentStatus.Received]))
|
||||||
val Some(IncomingPayment(_, _, _, _, IncomingPaymentStatus.Received(receivedAmount, _))) = nodes("A").nodeParams.db.payments.getIncomingPayment(pr.paymentHash)
|
val Some(IncomingPayment(_, _, _, _, IncomingPaymentStatus.Received(receivedAmount, _))) = nodes("A").nodeParams.db.payments.getIncomingPayment(pr.paymentHash)
|
||||||
assert(receivedAmount === amount)
|
assert(receivedAmount === amount)
|
||||||
|
eventListener.expectMsg(PaymentMetadataReceived(pr.paymentHash, pr.paymentMetadata.get))
|
||||||
|
|
||||||
awaitCond({
|
awaitCond({
|
||||||
val relayed = nodes("C").nodeParams.db.audit.listRelayed(start, TimestampMilli.now()).filter(_.paymentHash == pr.paymentHash)
|
val relayed = nodes("C").nodeParams.db.audit.listRelayed(start, TimestampMilli.now()).filter(_.paymentHash == pr.paymentHash)
|
||||||
|
|
|
@ -77,7 +77,7 @@ class SwitchboardSpec extends TestKitBaseClass with AnyFunSuiteLike {
|
||||||
val nodeParams = Alice.nodeParams.copy(syncWhitelist = Set.empty)
|
val nodeParams = Alice.nodeParams.copy(syncWhitelist = Set.empty)
|
||||||
val remoteNodeId = ChannelCodecsSpec.normal.commitments.remoteParams.nodeId
|
val remoteNodeId = ChannelCodecsSpec.normal.commitments.remoteParams.nodeId
|
||||||
nodeParams.db.channels.addOrUpdateChannel(ChannelCodecsSpec.normal)
|
nodeParams.db.channels.addOrUpdateChannel(ChannelCodecsSpec.normal)
|
||||||
sendFeatures(nodeParams, remoteNodeId, nodeParams.features, expectedSync = true)
|
sendFeatures(nodeParams, remoteNodeId, nodeParams.features.initFeatures(), expectedSync = true)
|
||||||
}
|
}
|
||||||
|
|
||||||
test("sync if no whitelist is defined and peer creates a channel") {
|
test("sync if no whitelist is defined and peer creates a channel") {
|
||||||
|
@ -91,7 +91,7 @@ class SwitchboardSpec extends TestKitBaseClass with AnyFunSuiteLike {
|
||||||
switchboard ! PeerConnection.Authenticated(peerConnection.ref, remoteNodeId)
|
switchboard ! PeerConnection.Authenticated(peerConnection.ref, remoteNodeId)
|
||||||
val initConnection1 = peerConnection.expectMsgType[PeerConnection.InitializeConnection]
|
val initConnection1 = peerConnection.expectMsgType[PeerConnection.InitializeConnection]
|
||||||
assert(initConnection1.chainHash === nodeParams.chainHash)
|
assert(initConnection1.chainHash === nodeParams.chainHash)
|
||||||
assert(initConnection1.features === nodeParams.features)
|
assert(initConnection1.features === nodeParams.features.initFeatures())
|
||||||
assert(initConnection1.doSync)
|
assert(initConnection1.doSync)
|
||||||
|
|
||||||
// We don't have channels with our peer, so we won't trigger a sync when connecting.
|
// We don't have channels with our peer, so we won't trigger a sync when connecting.
|
||||||
|
@ -99,26 +99,26 @@ class SwitchboardSpec extends TestKitBaseClass with AnyFunSuiteLike {
|
||||||
switchboard ! PeerConnection.Authenticated(peerConnection.ref, remoteNodeId)
|
switchboard ! PeerConnection.Authenticated(peerConnection.ref, remoteNodeId)
|
||||||
val initConnection2 = peerConnection.expectMsgType[PeerConnection.InitializeConnection]
|
val initConnection2 = peerConnection.expectMsgType[PeerConnection.InitializeConnection]
|
||||||
assert(initConnection2.chainHash === nodeParams.chainHash)
|
assert(initConnection2.chainHash === nodeParams.chainHash)
|
||||||
assert(initConnection2.features === nodeParams.features)
|
assert(initConnection2.features === nodeParams.features.initFeatures())
|
||||||
assert(!initConnection2.doSync)
|
assert(!initConnection2.doSync)
|
||||||
}
|
}
|
||||||
|
|
||||||
test("don't sync if no whitelist is defined and peer does not have channels") {
|
test("don't sync if no whitelist is defined and peer does not have channels") {
|
||||||
val nodeParams = Alice.nodeParams.copy(syncWhitelist = Set.empty)
|
val nodeParams = Alice.nodeParams.copy(syncWhitelist = Set.empty)
|
||||||
sendFeatures(nodeParams, randomKey().publicKey, nodeParams.features, expectedSync = false)
|
sendFeatures(nodeParams, randomKey().publicKey, nodeParams.features.initFeatures(), expectedSync = false)
|
||||||
}
|
}
|
||||||
|
|
||||||
test("sync if whitelist contains peer") {
|
test("sync if whitelist contains peer") {
|
||||||
val remoteNodeId = randomKey().publicKey
|
val remoteNodeId = randomKey().publicKey
|
||||||
val nodeParams = Alice.nodeParams.copy(syncWhitelist = Set(remoteNodeId, randomKey().publicKey, randomKey().publicKey))
|
val nodeParams = Alice.nodeParams.copy(syncWhitelist = Set(remoteNodeId, randomKey().publicKey, randomKey().publicKey))
|
||||||
sendFeatures(nodeParams, remoteNodeId, nodeParams.features, expectedSync = true)
|
sendFeatures(nodeParams, remoteNodeId, nodeParams.features.initFeatures(), expectedSync = true)
|
||||||
}
|
}
|
||||||
|
|
||||||
test("don't sync if whitelist doesn't contain peer") {
|
test("don't sync if whitelist doesn't contain peer") {
|
||||||
val nodeParams = Alice.nodeParams.copy(syncWhitelist = Set(randomKey().publicKey, randomKey().publicKey, randomKey().publicKey))
|
val nodeParams = Alice.nodeParams.copy(syncWhitelist = Set(randomKey().publicKey, randomKey().publicKey, randomKey().publicKey))
|
||||||
val remoteNodeId = ChannelCodecsSpec.normal.commitments.remoteParams.nodeId
|
val remoteNodeId = ChannelCodecsSpec.normal.commitments.remoteParams.nodeId
|
||||||
nodeParams.db.channels.addOrUpdateChannel(ChannelCodecsSpec.normal)
|
nodeParams.db.channels.addOrUpdateChannel(ChannelCodecsSpec.normal)
|
||||||
sendFeatures(nodeParams, remoteNodeId, nodeParams.features, expectedSync = false)
|
sendFeatures(nodeParams, remoteNodeId, nodeParams.features.initFeatures(), expectedSync = false)
|
||||||
}
|
}
|
||||||
|
|
||||||
test("get peer info") {
|
test("get peer info") {
|
||||||
|
|
|
@ -179,6 +179,12 @@ class JsonSerializersSpec extends AnyFunSuite with Matchers {
|
||||||
JsonSerializers.serialization.write(pr)(JsonSerializers.formats) shouldBe """{"prefix":"lntb","timestamp":1622474982,"nodeId":"03e89e4c3d41dc5332c2fb6cc66d12bfb9257ba681945a242f27a08d5ad210d891","serialized":"lntb1pst2q8xpp5qysan6j5xeq97tytxf7pfr0n75na8rztqhh03glmlgsqsyuqzgnqdqqxqrrss9qy9qsqsp5qq67gcxrn2drj5p0lc6p8wgdpqwxnc2h4s9kra5489q0fqsvhumsrzjqfqnj4upt5z6hdludky9vgk4ehzmwu2dk9rcevzczw5ywstehq79c83xr5qqqkqqqqqqqqlgqqqqqeqqjqrzjqwfn3p9278ttzzpe0e00uhyxhned3j5d9acqak5emwfpflp8z2cng838tqqqqxgqqqqqqqlgqqqqqeqqjqkxs4223x2r6sat65asfp0k2pze2rswe9np9vq08waqvsp832ffgymzgx8hgzejasesfxwcw6jj93azwq9klwuzmef3llns3n95pztgqpawp7an","description":"","paymentHash":"0121d9ea5436405f2c8b327c148df3f527d38c4b05eef8a3fbfa200813801226","expiry":3600,"features":{"activated":{"var_onion_optin":"optional","payment_secret":"optional","basic_mpp":"optional"},"unknown":[]},"routingInfo":[[{"nodeId":"02413957815d05abb7fc6d885622d5cdc5b7714db1478cb05813a8474179b83c5c","shortChannelId":"1975837x88x0","feeBase":1000,"feeProportionalMillionths":100,"cltvExpiryDelta":144}],[{"nodeId":"03933884aaf1d6b108397e5efe5c86bcf2d8ca8d2f700eda99db9214fc2712b134","shortChannelId":"1976152x25x0","feeBase":1000,"feeProportionalMillionths":100,"cltvExpiryDelta":144}]]}"""
|
JsonSerializers.serialization.write(pr)(JsonSerializers.formats) shouldBe """{"prefix":"lntb","timestamp":1622474982,"nodeId":"03e89e4c3d41dc5332c2fb6cc66d12bfb9257ba681945a242f27a08d5ad210d891","serialized":"lntb1pst2q8xpp5qysan6j5xeq97tytxf7pfr0n75na8rztqhh03glmlgsqsyuqzgnqdqqxqrrss9qy9qsqsp5qq67gcxrn2drj5p0lc6p8wgdpqwxnc2h4s9kra5489q0fqsvhumsrzjqfqnj4upt5z6hdludky9vgk4ehzmwu2dk9rcevzczw5ywstehq79c83xr5qqqkqqqqqqqqlgqqqqqeqqjqrzjqwfn3p9278ttzzpe0e00uhyxhned3j5d9acqak5emwfpflp8z2cng838tqqqqxgqqqqqqqlgqqqqqeqqjqkxs4223x2r6sat65asfp0k2pze2rswe9np9vq08waqvsp832ffgymzgx8hgzejasesfxwcw6jj93azwq9klwuzmef3llns3n95pztgqpawp7an","description":"","paymentHash":"0121d9ea5436405f2c8b327c148df3f527d38c4b05eef8a3fbfa200813801226","expiry":3600,"features":{"activated":{"var_onion_optin":"optional","payment_secret":"optional","basic_mpp":"optional"},"unknown":[]},"routingInfo":[[{"nodeId":"02413957815d05abb7fc6d885622d5cdc5b7714db1478cb05813a8474179b83c5c","shortChannelId":"1975837x88x0","feeBase":1000,"feeProportionalMillionths":100,"cltvExpiryDelta":144}],[{"nodeId":"03933884aaf1d6b108397e5efe5c86bcf2d8ca8d2f700eda99db9214fc2712b134","shortChannelId":"1976152x25x0","feeBase":1000,"feeProportionalMillionths":100,"cltvExpiryDelta":144}]]}"""
|
||||||
}
|
}
|
||||||
|
|
||||||
|
test("Payment Request with metadata") {
|
||||||
|
val ref = "lnbc10m1pvjluezpp5qqqsyqcyq5rqwzqfqqqsyqcyq5rqwzqfqqqsyqcyq5rqwzqfqypqdp9wpshjmt9de6zqmt9w3skgct5vysxjmnnd9jx2mq8q8a04uqnp4q0n326hr8v9zprg8gsvezcch06gfaqqhde2aj730yg0durunfhv66sp5zyg3zyg3zyg3zyg3zyg3zyg3zyg3zyg3zyg3zyg3zyg3zyg3zygs9q2gqqqqqqsgqy9gw6ymamd20jumvdgpfphkhp8fzhhdhycw36egcmla5vlrtrmhs9t7psfy3hkkdqzm9eq64fjg558znccds5nhsfmxveha5xe0dykgpspdha0"
|
||||||
|
val pr = PaymentRequest.read(ref)
|
||||||
|
JsonSerializers.serialization.write(pr)(JsonSerializers.formats) shouldBe """{"prefix":"lnbc","timestamp":1496314658,"nodeId":"03e7156ae33b0a208d0744199163177e909e80176e55d97a2f221ede0f934dd9ad","serialized":"lnbc10m1pvjluezpp5qqqsyqcyq5rqwzqfqqqsyqcyq5rqwzqfqqqsyqcyq5rqwzqfqypqdp9wpshjmt9de6zqmt9w3skgct5vysxjmnnd9jx2mq8q8a04uqnp4q0n326hr8v9zprg8gsvezcch06gfaqqhde2aj730yg0durunfhv66sp5zyg3zyg3zyg3zyg3zyg3zyg3zyg3zyg3zyg3zyg3zyg3zyg3zygs9q2gqqqqqqsgqy9gw6ymamd20jumvdgpfphkhp8fzhhdhycw36egcmla5vlrtrmhs9t7psfy3hkkdqzm9eq64fjg558znccds5nhsfmxveha5xe0dykgpspdha0","description":"payment metadata inside","paymentHash":"0001020304050607080900010203040506070809000102030405060708090102","paymentMetadata":"01fafaf0","amount":1000000000,"features":{"activated":{"var_onion_optin":"mandatory","payment_secret":"mandatory","option_payment_metadata":"mandatory"},"unknown":[]},"routingInfo":[]}"""
|
||||||
|
}
|
||||||
|
|
||||||
test("GlobalBalance serializer") {
|
test("GlobalBalance serializer") {
|
||||||
val gb = GlobalBalance(
|
val gb = GlobalBalance(
|
||||||
onChain = CheckBalance.CorrectedOnChainBalance(Btc(0.4), Btc(0.05)),
|
onChain = CheckBalance.CorrectedOnChainBalance(Btc(0.4), Btc(0.05)),
|
||||||
|
|
|
@ -35,6 +35,7 @@ import fr.acinq.eclair.wire.protocol._
|
||||||
import fr.acinq.eclair.{CltvExpiry, CltvExpiryDelta, Features, MilliSatoshiLong, NodeParams, ShortChannelId, TestConstants, TestKitBaseClass, TimestampMilliLong, randomBytes32, randomKey}
|
import fr.acinq.eclair.{CltvExpiry, CltvExpiryDelta, Features, MilliSatoshiLong, NodeParams, ShortChannelId, TestConstants, TestKitBaseClass, TimestampMilliLong, randomBytes32, randomKey}
|
||||||
import org.scalatest.Outcome
|
import org.scalatest.Outcome
|
||||||
import org.scalatest.funsuite.FixtureAnyFunSuiteLike
|
import org.scalatest.funsuite.FixtureAnyFunSuiteLike
|
||||||
|
import scodec.bits.HexStringSyntax
|
||||||
|
|
||||||
import scala.collection.immutable.Queue
|
import scala.collection.immutable.Queue
|
||||||
import scala.concurrent.duration._
|
import scala.concurrent.duration._
|
||||||
|
@ -93,7 +94,7 @@ class MultiPartHandlerSpec extends TestKitBaseClass with FixtureAnyFunSuiteLike
|
||||||
assert(Crypto.sha256(incoming.get.paymentPreimage) === pr.paymentHash)
|
assert(Crypto.sha256(incoming.get.paymentPreimage) === pr.paymentHash)
|
||||||
|
|
||||||
val add = UpdateAddHtlc(ByteVector32.One, 0, amountMsat, pr.paymentHash, defaultExpiry, TestConstants.emptyOnionPacket)
|
val add = UpdateAddHtlc(ByteVector32.One, 0, amountMsat, pr.paymentHash, defaultExpiry, TestConstants.emptyOnionPacket)
|
||||||
sender.send(handlerWithoutMpp, IncomingPaymentPacket.FinalPacket(add, PaymentOnion.createSinglePartPayload(add.amountMsat, add.cltvExpiry, pr.paymentSecret.get)))
|
sender.send(handlerWithoutMpp, IncomingPaymentPacket.FinalPacket(add, PaymentOnion.createSinglePartPayload(add.amountMsat, add.cltvExpiry, pr.paymentSecret.get, pr.paymentMetadata)))
|
||||||
register.expectMsgType[Register.Forward[CMD_FULFILL_HTLC]]
|
register.expectMsgType[Register.Forward[CMD_FULFILL_HTLC]]
|
||||||
|
|
||||||
val paymentReceived = eventListener.expectMsgType[PaymentReceived]
|
val paymentReceived = eventListener.expectMsgType[PaymentReceived]
|
||||||
|
@ -112,7 +113,7 @@ class MultiPartHandlerSpec extends TestKitBaseClass with FixtureAnyFunSuiteLike
|
||||||
assert(nodeParams.db.payments.getIncomingPayment(pr.paymentHash).get.status === IncomingPaymentStatus.Pending)
|
assert(nodeParams.db.payments.getIncomingPayment(pr.paymentHash).get.status === IncomingPaymentStatus.Pending)
|
||||||
|
|
||||||
val add = UpdateAddHtlc(ByteVector32.One, 0, amountMsat, pr.paymentHash, defaultExpiry, TestConstants.emptyOnionPacket)
|
val add = UpdateAddHtlc(ByteVector32.One, 0, amountMsat, pr.paymentHash, defaultExpiry, TestConstants.emptyOnionPacket)
|
||||||
sender.send(handlerWithMpp, IncomingPaymentPacket.FinalPacket(add, PaymentOnion.createSinglePartPayload(add.amountMsat, add.cltvExpiry, pr.paymentSecret.get)))
|
sender.send(handlerWithMpp, IncomingPaymentPacket.FinalPacket(add, PaymentOnion.createSinglePartPayload(add.amountMsat, add.cltvExpiry, pr.paymentSecret.get, pr.paymentMetadata)))
|
||||||
register.expectMsgType[Register.Forward[CMD_FULFILL_HTLC]]
|
register.expectMsgType[Register.Forward[CMD_FULFILL_HTLC]]
|
||||||
|
|
||||||
val paymentReceived = eventListener.expectMsgType[PaymentReceived]
|
val paymentReceived = eventListener.expectMsgType[PaymentReceived]
|
||||||
|
@ -130,7 +131,7 @@ class MultiPartHandlerSpec extends TestKitBaseClass with FixtureAnyFunSuiteLike
|
||||||
assert(nodeParams.db.payments.getIncomingPayment(pr.paymentHash).get.status === IncomingPaymentStatus.Pending)
|
assert(nodeParams.db.payments.getIncomingPayment(pr.paymentHash).get.status === IncomingPaymentStatus.Pending)
|
||||||
|
|
||||||
val add = UpdateAddHtlc(ByteVector32.One, 0, amountMsat, pr.paymentHash, CltvExpiryDelta(3).toCltvExpiry(nodeParams.currentBlockHeight), TestConstants.emptyOnionPacket)
|
val add = UpdateAddHtlc(ByteVector32.One, 0, amountMsat, pr.paymentHash, CltvExpiryDelta(3).toCltvExpiry(nodeParams.currentBlockHeight), TestConstants.emptyOnionPacket)
|
||||||
sender.send(handlerWithMpp, IncomingPaymentPacket.FinalPacket(add, PaymentOnion.createSinglePartPayload(add.amountMsat, add.cltvExpiry, pr.paymentSecret.get)))
|
sender.send(handlerWithMpp, IncomingPaymentPacket.FinalPacket(add, PaymentOnion.createSinglePartPayload(add.amountMsat, add.cltvExpiry, pr.paymentSecret.get, pr.paymentMetadata)))
|
||||||
val cmd = register.expectMsgType[Register.Forward[CMD_FAIL_HTLC]].message
|
val cmd = register.expectMsgType[Register.Forward[CMD_FAIL_HTLC]].message
|
||||||
assert(cmd.reason == Right(IncorrectOrUnknownPaymentDetails(amountMsat, nodeParams.currentBlockHeight)))
|
assert(cmd.reason == Right(IncorrectOrUnknownPaymentDetails(amountMsat, nodeParams.currentBlockHeight)))
|
||||||
assert(nodeParams.db.payments.getIncomingPayment(pr.paymentHash).get.status === IncomingPaymentStatus.Pending)
|
assert(nodeParams.db.payments.getIncomingPayment(pr.paymentHash).get.status === IncomingPaymentStatus.Pending)
|
||||||
|
@ -191,7 +192,6 @@ class MultiPartHandlerSpec extends TestKitBaseClass with FixtureAnyFunSuiteLike
|
||||||
assert(!pr.features.allowMultiPart)
|
assert(!pr.features.allowMultiPart)
|
||||||
assert(!pr.features.allowTrampoline)
|
assert(!pr.features.allowTrampoline)
|
||||||
}
|
}
|
||||||
|
|
||||||
{
|
{
|
||||||
val handler = TestActorRef[PaymentHandler](PaymentHandler.props(Alice.nodeParams.copy(enableTrampolinePayment = false, features = featuresWithMpp), TestProbe().ref))
|
val handler = TestActorRef[PaymentHandler](PaymentHandler.props(Alice.nodeParams.copy(enableTrampolinePayment = false, features = featuresWithMpp), TestProbe().ref))
|
||||||
sender.send(handler, ReceivePayment(Some(42 msat), Left("1 coffee")))
|
sender.send(handler, ReceivePayment(Some(42 msat), Left("1 coffee")))
|
||||||
|
@ -199,7 +199,6 @@ class MultiPartHandlerSpec extends TestKitBaseClass with FixtureAnyFunSuiteLike
|
||||||
assert(pr.features.allowMultiPart)
|
assert(pr.features.allowMultiPart)
|
||||||
assert(!pr.features.allowTrampoline)
|
assert(!pr.features.allowTrampoline)
|
||||||
}
|
}
|
||||||
|
|
||||||
{
|
{
|
||||||
val handler = TestActorRef[PaymentHandler](PaymentHandler.props(Alice.nodeParams.copy(enableTrampolinePayment = true, features = featuresWithoutMpp), TestProbe().ref))
|
val handler = TestActorRef[PaymentHandler](PaymentHandler.props(Alice.nodeParams.copy(enableTrampolinePayment = true, features = featuresWithoutMpp), TestProbe().ref))
|
||||||
sender.send(handler, ReceivePayment(Some(42 msat), Left("1 coffee")))
|
sender.send(handler, ReceivePayment(Some(42 msat), Left("1 coffee")))
|
||||||
|
@ -207,7 +206,6 @@ class MultiPartHandlerSpec extends TestKitBaseClass with FixtureAnyFunSuiteLike
|
||||||
assert(!pr.features.allowMultiPart)
|
assert(!pr.features.allowMultiPart)
|
||||||
assert(pr.features.allowTrampoline)
|
assert(pr.features.allowTrampoline)
|
||||||
}
|
}
|
||||||
|
|
||||||
{
|
{
|
||||||
val handler = TestActorRef[PaymentHandler](PaymentHandler.props(Alice.nodeParams.copy(enableTrampolinePayment = true, features = featuresWithMpp), TestProbe().ref))
|
val handler = TestActorRef[PaymentHandler](PaymentHandler.props(Alice.nodeParams.copy(enableTrampolinePayment = true, features = featuresWithMpp), TestProbe().ref))
|
||||||
sender.send(handler, ReceivePayment(Some(42 msat), Left("1 coffee")))
|
sender.send(handler, ReceivePayment(Some(42 msat), Left("1 coffee")))
|
||||||
|
@ -244,7 +242,7 @@ class MultiPartHandlerSpec extends TestKitBaseClass with FixtureAnyFunSuiteLike
|
||||||
assert(pr.isExpired)
|
assert(pr.isExpired)
|
||||||
|
|
||||||
val add = UpdateAddHtlc(ByteVector32.One, 0, 1000 msat, pr.paymentHash, defaultExpiry, TestConstants.emptyOnionPacket)
|
val add = UpdateAddHtlc(ByteVector32.One, 0, 1000 msat, pr.paymentHash, defaultExpiry, TestConstants.emptyOnionPacket)
|
||||||
sender.send(handlerWithoutMpp, IncomingPaymentPacket.FinalPacket(add, PaymentOnion.createSinglePartPayload(add.amountMsat, add.cltvExpiry, pr.paymentSecret.get)))
|
sender.send(handlerWithoutMpp, IncomingPaymentPacket.FinalPacket(add, PaymentOnion.createSinglePartPayload(add.amountMsat, add.cltvExpiry, pr.paymentSecret.get, pr.paymentMetadata)))
|
||||||
register.expectMsgType[Register.Forward[CMD_FAIL_HTLC]]
|
register.expectMsgType[Register.Forward[CMD_FAIL_HTLC]]
|
||||||
val Some(incoming) = nodeParams.db.payments.getIncomingPayment(pr.paymentHash)
|
val Some(incoming) = nodeParams.db.payments.getIncomingPayment(pr.paymentHash)
|
||||||
assert(incoming.paymentRequest.isExpired && incoming.status === IncomingPaymentStatus.Expired)
|
assert(incoming.paymentRequest.isExpired && incoming.status === IncomingPaymentStatus.Expired)
|
||||||
|
@ -259,7 +257,7 @@ class MultiPartHandlerSpec extends TestKitBaseClass with FixtureAnyFunSuiteLike
|
||||||
assert(pr.isExpired)
|
assert(pr.isExpired)
|
||||||
|
|
||||||
val add = UpdateAddHtlc(ByteVector32.One, 0, 800 msat, pr.paymentHash, defaultExpiry, TestConstants.emptyOnionPacket)
|
val add = UpdateAddHtlc(ByteVector32.One, 0, 800 msat, pr.paymentHash, defaultExpiry, TestConstants.emptyOnionPacket)
|
||||||
sender.send(handlerWithMpp, IncomingPaymentPacket.FinalPacket(add, PaymentOnion.createMultiPartPayload(add.amountMsat, 1000 msat, add.cltvExpiry, pr.paymentSecret.get)))
|
sender.send(handlerWithMpp, IncomingPaymentPacket.FinalPacket(add, PaymentOnion.createMultiPartPayload(add.amountMsat, 1000 msat, add.cltvExpiry, pr.paymentSecret.get, pr.paymentMetadata)))
|
||||||
val cmd = register.expectMsgType[Register.Forward[CMD_FAIL_HTLC]].message
|
val cmd = register.expectMsgType[Register.Forward[CMD_FAIL_HTLC]].message
|
||||||
assert(cmd.reason == Right(IncorrectOrUnknownPaymentDetails(1000 msat, nodeParams.currentBlockHeight)))
|
assert(cmd.reason == Right(IncorrectOrUnknownPaymentDetails(1000 msat, nodeParams.currentBlockHeight)))
|
||||||
val Some(incoming) = nodeParams.db.payments.getIncomingPayment(pr.paymentHash)
|
val Some(incoming) = nodeParams.db.payments.getIncomingPayment(pr.paymentHash)
|
||||||
|
@ -274,7 +272,7 @@ class MultiPartHandlerSpec extends TestKitBaseClass with FixtureAnyFunSuiteLike
|
||||||
assert(!pr.features.allowMultiPart)
|
assert(!pr.features.allowMultiPart)
|
||||||
|
|
||||||
val add = UpdateAddHtlc(ByteVector32.One, 0, 800 msat, pr.paymentHash, defaultExpiry, TestConstants.emptyOnionPacket)
|
val add = UpdateAddHtlc(ByteVector32.One, 0, 800 msat, pr.paymentHash, defaultExpiry, TestConstants.emptyOnionPacket)
|
||||||
sender.send(handlerWithoutMpp, IncomingPaymentPacket.FinalPacket(add, PaymentOnion.createMultiPartPayload(add.amountMsat, 1000 msat, add.cltvExpiry, pr.paymentSecret.get)))
|
sender.send(handlerWithoutMpp, IncomingPaymentPacket.FinalPacket(add, PaymentOnion.createMultiPartPayload(add.amountMsat, 1000 msat, add.cltvExpiry, pr.paymentSecret.get, pr.paymentMetadata)))
|
||||||
val cmd = register.expectMsgType[Register.Forward[CMD_FAIL_HTLC]].message
|
val cmd = register.expectMsgType[Register.Forward[CMD_FAIL_HTLC]].message
|
||||||
assert(cmd.reason == Right(IncorrectOrUnknownPaymentDetails(1000 msat, nodeParams.currentBlockHeight)))
|
assert(cmd.reason == Right(IncorrectOrUnknownPaymentDetails(1000 msat, nodeParams.currentBlockHeight)))
|
||||||
assert(nodeParams.db.payments.getIncomingPayment(pr.paymentHash).get.status === IncomingPaymentStatus.Pending)
|
assert(nodeParams.db.payments.getIncomingPayment(pr.paymentHash).get.status === IncomingPaymentStatus.Pending)
|
||||||
|
@ -289,7 +287,7 @@ class MultiPartHandlerSpec extends TestKitBaseClass with FixtureAnyFunSuiteLike
|
||||||
|
|
||||||
val lowCltvExpiry = nodeParams.fulfillSafetyBeforeTimeout.toCltvExpiry(nodeParams.currentBlockHeight)
|
val lowCltvExpiry = nodeParams.fulfillSafetyBeforeTimeout.toCltvExpiry(nodeParams.currentBlockHeight)
|
||||||
val add = UpdateAddHtlc(ByteVector32.One, 0, 800 msat, pr.paymentHash, lowCltvExpiry, TestConstants.emptyOnionPacket)
|
val add = UpdateAddHtlc(ByteVector32.One, 0, 800 msat, pr.paymentHash, lowCltvExpiry, TestConstants.emptyOnionPacket)
|
||||||
sender.send(handlerWithMpp, IncomingPaymentPacket.FinalPacket(add, PaymentOnion.createMultiPartPayload(add.amountMsat, 1000 msat, add.cltvExpiry, pr.paymentSecret.get)))
|
sender.send(handlerWithMpp, IncomingPaymentPacket.FinalPacket(add, PaymentOnion.createMultiPartPayload(add.amountMsat, 1000 msat, add.cltvExpiry, pr.paymentSecret.get, pr.paymentMetadata)))
|
||||||
val cmd = register.expectMsgType[Register.Forward[CMD_FAIL_HTLC]].message
|
val cmd = register.expectMsgType[Register.Forward[CMD_FAIL_HTLC]].message
|
||||||
assert(cmd.reason == Right(IncorrectOrUnknownPaymentDetails(1000 msat, nodeParams.currentBlockHeight)))
|
assert(cmd.reason == Right(IncorrectOrUnknownPaymentDetails(1000 msat, nodeParams.currentBlockHeight)))
|
||||||
assert(nodeParams.db.payments.getIncomingPayment(pr.paymentHash).get.status === IncomingPaymentStatus.Pending)
|
assert(nodeParams.db.payments.getIncomingPayment(pr.paymentHash).get.status === IncomingPaymentStatus.Pending)
|
||||||
|
@ -303,7 +301,7 @@ class MultiPartHandlerSpec extends TestKitBaseClass with FixtureAnyFunSuiteLike
|
||||||
assert(pr.features.allowMultiPart)
|
assert(pr.features.allowMultiPart)
|
||||||
|
|
||||||
val add = UpdateAddHtlc(ByteVector32.One, 0, 800 msat, pr.paymentHash.reverse, defaultExpiry, TestConstants.emptyOnionPacket)
|
val add = UpdateAddHtlc(ByteVector32.One, 0, 800 msat, pr.paymentHash.reverse, defaultExpiry, TestConstants.emptyOnionPacket)
|
||||||
sender.send(handlerWithMpp, IncomingPaymentPacket.FinalPacket(add, PaymentOnion.createMultiPartPayload(add.amountMsat, 1000 msat, add.cltvExpiry, pr.paymentSecret.get)))
|
sender.send(handlerWithMpp, IncomingPaymentPacket.FinalPacket(add, PaymentOnion.createMultiPartPayload(add.amountMsat, 1000 msat, add.cltvExpiry, pr.paymentSecret.get, pr.paymentMetadata)))
|
||||||
val cmd = register.expectMsgType[Register.Forward[CMD_FAIL_HTLC]].message
|
val cmd = register.expectMsgType[Register.Forward[CMD_FAIL_HTLC]].message
|
||||||
assert(cmd.reason == Right(IncorrectOrUnknownPaymentDetails(1000 msat, nodeParams.currentBlockHeight)))
|
assert(cmd.reason == Right(IncorrectOrUnknownPaymentDetails(1000 msat, nodeParams.currentBlockHeight)))
|
||||||
assert(nodeParams.db.payments.getIncomingPayment(pr.paymentHash).get.status === IncomingPaymentStatus.Pending)
|
assert(nodeParams.db.payments.getIncomingPayment(pr.paymentHash).get.status === IncomingPaymentStatus.Pending)
|
||||||
|
@ -317,7 +315,7 @@ class MultiPartHandlerSpec extends TestKitBaseClass with FixtureAnyFunSuiteLike
|
||||||
assert(pr.features.allowMultiPart)
|
assert(pr.features.allowMultiPart)
|
||||||
|
|
||||||
val add = UpdateAddHtlc(ByteVector32.One, 0, 800 msat, pr.paymentHash, defaultExpiry, TestConstants.emptyOnionPacket)
|
val add = UpdateAddHtlc(ByteVector32.One, 0, 800 msat, pr.paymentHash, defaultExpiry, TestConstants.emptyOnionPacket)
|
||||||
sender.send(handlerWithMpp, IncomingPaymentPacket.FinalPacket(add, PaymentOnion.createMultiPartPayload(add.amountMsat, 999 msat, add.cltvExpiry, pr.paymentSecret.get)))
|
sender.send(handlerWithMpp, IncomingPaymentPacket.FinalPacket(add, PaymentOnion.createMultiPartPayload(add.amountMsat, 999 msat, add.cltvExpiry, pr.paymentSecret.get, pr.paymentMetadata)))
|
||||||
val cmd = register.expectMsgType[Register.Forward[CMD_FAIL_HTLC]].message
|
val cmd = register.expectMsgType[Register.Forward[CMD_FAIL_HTLC]].message
|
||||||
assert(cmd.reason == Right(IncorrectOrUnknownPaymentDetails(999 msat, nodeParams.currentBlockHeight)))
|
assert(cmd.reason == Right(IncorrectOrUnknownPaymentDetails(999 msat, nodeParams.currentBlockHeight)))
|
||||||
assert(nodeParams.db.payments.getIncomingPayment(pr.paymentHash).get.status === IncomingPaymentStatus.Pending)
|
assert(nodeParams.db.payments.getIncomingPayment(pr.paymentHash).get.status === IncomingPaymentStatus.Pending)
|
||||||
|
@ -331,7 +329,7 @@ class MultiPartHandlerSpec extends TestKitBaseClass with FixtureAnyFunSuiteLike
|
||||||
assert(pr.features.allowMultiPart)
|
assert(pr.features.allowMultiPart)
|
||||||
|
|
||||||
val add = UpdateAddHtlc(ByteVector32.One, 0, 800 msat, pr.paymentHash, defaultExpiry, TestConstants.emptyOnionPacket)
|
val add = UpdateAddHtlc(ByteVector32.One, 0, 800 msat, pr.paymentHash, defaultExpiry, TestConstants.emptyOnionPacket)
|
||||||
sender.send(handlerWithMpp, IncomingPaymentPacket.FinalPacket(add, PaymentOnion.createMultiPartPayload(add.amountMsat, 2001 msat, add.cltvExpiry, pr.paymentSecret.get)))
|
sender.send(handlerWithMpp, IncomingPaymentPacket.FinalPacket(add, PaymentOnion.createMultiPartPayload(add.amountMsat, 2001 msat, add.cltvExpiry, pr.paymentSecret.get, pr.paymentMetadata)))
|
||||||
val cmd = register.expectMsgType[Register.Forward[CMD_FAIL_HTLC]].message
|
val cmd = register.expectMsgType[Register.Forward[CMD_FAIL_HTLC]].message
|
||||||
assert(cmd.reason == Right(IncorrectOrUnknownPaymentDetails(2001 msat, nodeParams.currentBlockHeight)))
|
assert(cmd.reason == Right(IncorrectOrUnknownPaymentDetails(2001 msat, nodeParams.currentBlockHeight)))
|
||||||
assert(nodeParams.db.payments.getIncomingPayment(pr.paymentHash).get.status === IncomingPaymentStatus.Pending)
|
assert(nodeParams.db.payments.getIncomingPayment(pr.paymentHash).get.status === IncomingPaymentStatus.Pending)
|
||||||
|
@ -346,7 +344,7 @@ class MultiPartHandlerSpec extends TestKitBaseClass with FixtureAnyFunSuiteLike
|
||||||
|
|
||||||
// Invalid payment secret.
|
// Invalid payment secret.
|
||||||
val add = UpdateAddHtlc(ByteVector32.One, 0, 800 msat, pr.paymentHash, defaultExpiry, TestConstants.emptyOnionPacket)
|
val add = UpdateAddHtlc(ByteVector32.One, 0, 800 msat, pr.paymentHash, defaultExpiry, TestConstants.emptyOnionPacket)
|
||||||
sender.send(handlerWithMpp, IncomingPaymentPacket.FinalPacket(add, PaymentOnion.createMultiPartPayload(add.amountMsat, 1000 msat, add.cltvExpiry, pr.paymentSecret.get.reverse)))
|
sender.send(handlerWithMpp, IncomingPaymentPacket.FinalPacket(add, PaymentOnion.createMultiPartPayload(add.amountMsat, 1000 msat, add.cltvExpiry, pr.paymentSecret.get.reverse, pr.paymentMetadata)))
|
||||||
val cmd = register.expectMsgType[Register.Forward[CMD_FAIL_HTLC]].message
|
val cmd = register.expectMsgType[Register.Forward[CMD_FAIL_HTLC]].message
|
||||||
assert(cmd.reason == Right(IncorrectOrUnknownPaymentDetails(1000 msat, nodeParams.currentBlockHeight)))
|
assert(cmd.reason == Right(IncorrectOrUnknownPaymentDetails(1000 msat, nodeParams.currentBlockHeight)))
|
||||||
assert(nodeParams.db.payments.getIncomingPayment(pr.paymentHash).get.status === IncomingPaymentStatus.Pending)
|
assert(nodeParams.db.payments.getIncomingPayment(pr.paymentHash).get.status === IncomingPaymentStatus.Pending)
|
||||||
|
@ -360,13 +358,13 @@ class MultiPartHandlerSpec extends TestKitBaseClass with FixtureAnyFunSuiteLike
|
||||||
f.sender.send(handler, ReceivePayment(Some(1000 msat), Left("1 slow coffee")))
|
f.sender.send(handler, ReceivePayment(Some(1000 msat), Left("1 slow coffee")))
|
||||||
val pr1 = f.sender.expectMsgType[PaymentRequest]
|
val pr1 = f.sender.expectMsgType[PaymentRequest]
|
||||||
val add1 = UpdateAddHtlc(ByteVector32.One, 0, 800 msat, pr1.paymentHash, f.defaultExpiry, TestConstants.emptyOnionPacket)
|
val add1 = UpdateAddHtlc(ByteVector32.One, 0, 800 msat, pr1.paymentHash, f.defaultExpiry, TestConstants.emptyOnionPacket)
|
||||||
f.sender.send(handler, IncomingPaymentPacket.FinalPacket(add1, PaymentOnion.createMultiPartPayload(add1.amountMsat, 1000 msat, add1.cltvExpiry, pr1.paymentSecret.get)))
|
f.sender.send(handler, IncomingPaymentPacket.FinalPacket(add1, PaymentOnion.createMultiPartPayload(add1.amountMsat, 1000 msat, add1.cltvExpiry, pr1.paymentSecret.get, pr1.paymentMetadata)))
|
||||||
|
|
||||||
// Partial payment exceeding the invoice amount, but incomplete because it promises to overpay.
|
// Partial payment exceeding the invoice amount, but incomplete because it promises to overpay.
|
||||||
f.sender.send(handler, ReceivePayment(Some(1500 msat), Left("1 slow latte")))
|
f.sender.send(handler, ReceivePayment(Some(1500 msat), Left("1 slow latte")))
|
||||||
val pr2 = f.sender.expectMsgType[PaymentRequest]
|
val pr2 = f.sender.expectMsgType[PaymentRequest]
|
||||||
val add2 = UpdateAddHtlc(ByteVector32.One, 1, 1600 msat, pr2.paymentHash, f.defaultExpiry, TestConstants.emptyOnionPacket)
|
val add2 = UpdateAddHtlc(ByteVector32.One, 1, 1600 msat, pr2.paymentHash, f.defaultExpiry, TestConstants.emptyOnionPacket)
|
||||||
f.sender.send(handler, IncomingPaymentPacket.FinalPacket(add2, PaymentOnion.createMultiPartPayload(add2.amountMsat, 2000 msat, add2.cltvExpiry, pr2.paymentSecret.get)))
|
f.sender.send(handler, IncomingPaymentPacket.FinalPacket(add2, PaymentOnion.createMultiPartPayload(add2.amountMsat, 2000 msat, add2.cltvExpiry, pr2.paymentSecret.get, pr2.paymentMetadata)))
|
||||||
|
|
||||||
awaitCond {
|
awaitCond {
|
||||||
f.sender.send(handler, GetPendingPayments)
|
f.sender.send(handler, GetPendingPayments)
|
||||||
|
@ -401,12 +399,12 @@ class MultiPartHandlerSpec extends TestKitBaseClass with FixtureAnyFunSuiteLike
|
||||||
val pr = f.sender.expectMsgType[PaymentRequest]
|
val pr = f.sender.expectMsgType[PaymentRequest]
|
||||||
|
|
||||||
val add1 = UpdateAddHtlc(ByteVector32.One, 0, 800 msat, pr.paymentHash, f.defaultExpiry, TestConstants.emptyOnionPacket)
|
val add1 = UpdateAddHtlc(ByteVector32.One, 0, 800 msat, pr.paymentHash, f.defaultExpiry, TestConstants.emptyOnionPacket)
|
||||||
f.sender.send(handler, IncomingPaymentPacket.FinalPacket(add1, PaymentOnion.createMultiPartPayload(add1.amountMsat, 1000 msat, add1.cltvExpiry, pr.paymentSecret.get)))
|
f.sender.send(handler, IncomingPaymentPacket.FinalPacket(add1, PaymentOnion.createMultiPartPayload(add1.amountMsat, 1000 msat, add1.cltvExpiry, pr.paymentSecret.get, pr.paymentMetadata)))
|
||||||
// Invalid payment secret -> should be rejected.
|
// Invalid payment secret -> should be rejected.
|
||||||
val add2 = UpdateAddHtlc(ByteVector32.Zeroes, 42, 200 msat, pr.paymentHash, f.defaultExpiry, TestConstants.emptyOnionPacket)
|
val add2 = UpdateAddHtlc(ByteVector32.Zeroes, 42, 200 msat, pr.paymentHash, f.defaultExpiry, TestConstants.emptyOnionPacket)
|
||||||
f.sender.send(handler, IncomingPaymentPacket.FinalPacket(add2, PaymentOnion.createMultiPartPayload(add2.amountMsat, 1000 msat, add2.cltvExpiry, pr.paymentSecret.get.reverse)))
|
f.sender.send(handler, IncomingPaymentPacket.FinalPacket(add2, PaymentOnion.createMultiPartPayload(add2.amountMsat, 1000 msat, add2.cltvExpiry, pr.paymentSecret.get.reverse, pr.paymentMetadata)))
|
||||||
val add3 = add2.copy(id = 43)
|
val add3 = add2.copy(id = 43)
|
||||||
f.sender.send(handler, IncomingPaymentPacket.FinalPacket(add3, PaymentOnion.createMultiPartPayload(add3.amountMsat, 1000 msat, add3.cltvExpiry, pr.paymentSecret.get)))
|
f.sender.send(handler, IncomingPaymentPacket.FinalPacket(add3, PaymentOnion.createMultiPartPayload(add3.amountMsat, 1000 msat, add3.cltvExpiry, pr.paymentSecret.get, pr.paymentMetadata)))
|
||||||
|
|
||||||
f.register.expectMsgAllOf(
|
f.register.expectMsgAllOf(
|
||||||
Register.Forward(ActorRef.noSender, add2.channelId, CMD_FAIL_HTLC(add2.id, Right(IncorrectOrUnknownPaymentDetails(1000 msat, nodeParams.currentBlockHeight)), commit = true)),
|
Register.Forward(ActorRef.noSender, add2.channelId, CMD_FAIL_HTLC(add2.id, Right(IncorrectOrUnknownPaymentDetails(1000 msat, nodeParams.currentBlockHeight)), commit = true)),
|
||||||
|
@ -446,7 +444,7 @@ class MultiPartHandlerSpec extends TestKitBaseClass with FixtureAnyFunSuiteLike
|
||||||
assert(pr.paymentHash == Crypto.sha256(preimage))
|
assert(pr.paymentHash == Crypto.sha256(preimage))
|
||||||
|
|
||||||
val add1 = UpdateAddHtlc(ByteVector32.One, 0, 800 msat, pr.paymentHash, f.defaultExpiry, TestConstants.emptyOnionPacket)
|
val add1 = UpdateAddHtlc(ByteVector32.One, 0, 800 msat, pr.paymentHash, f.defaultExpiry, TestConstants.emptyOnionPacket)
|
||||||
f.sender.send(handler, IncomingPaymentPacket.FinalPacket(add1, PaymentOnion.createMultiPartPayload(add1.amountMsat, 1000 msat, add1.cltvExpiry, pr.paymentSecret.get)))
|
f.sender.send(handler, IncomingPaymentPacket.FinalPacket(add1, PaymentOnion.createMultiPartPayload(add1.amountMsat, 1000 msat, add1.cltvExpiry, pr.paymentSecret.get, pr.paymentMetadata)))
|
||||||
f.register.expectMsg(Register.Forward(ActorRef.noSender, ByteVector32.One, CMD_FAIL_HTLC(0, Right(PaymentTimeout), commit = true)))
|
f.register.expectMsg(Register.Forward(ActorRef.noSender, ByteVector32.One, CMD_FAIL_HTLC(0, Right(PaymentTimeout), commit = true)))
|
||||||
awaitCond({
|
awaitCond({
|
||||||
f.sender.send(handler, GetPendingPayments)
|
f.sender.send(handler, GetPendingPayments)
|
||||||
|
@ -454,9 +452,9 @@ class MultiPartHandlerSpec extends TestKitBaseClass with FixtureAnyFunSuiteLike
|
||||||
})
|
})
|
||||||
|
|
||||||
val add2 = UpdateAddHtlc(ByteVector32.One, 2, 300 msat, pr.paymentHash, f.defaultExpiry, TestConstants.emptyOnionPacket)
|
val add2 = UpdateAddHtlc(ByteVector32.One, 2, 300 msat, pr.paymentHash, f.defaultExpiry, TestConstants.emptyOnionPacket)
|
||||||
f.sender.send(handler, IncomingPaymentPacket.FinalPacket(add2, PaymentOnion.createMultiPartPayload(add2.amountMsat, 1000 msat, add2.cltvExpiry, pr.paymentSecret.get)))
|
f.sender.send(handler, IncomingPaymentPacket.FinalPacket(add2, PaymentOnion.createMultiPartPayload(add2.amountMsat, 1000 msat, add2.cltvExpiry, pr.paymentSecret.get, pr.paymentMetadata)))
|
||||||
val add3 = UpdateAddHtlc(ByteVector32.Zeroes, 5, 700 msat, pr.paymentHash, f.defaultExpiry, TestConstants.emptyOnionPacket)
|
val add3 = UpdateAddHtlc(ByteVector32.Zeroes, 5, 700 msat, pr.paymentHash, f.defaultExpiry, TestConstants.emptyOnionPacket)
|
||||||
f.sender.send(handler, IncomingPaymentPacket.FinalPacket(add3, PaymentOnion.createMultiPartPayload(add3.amountMsat, 1000 msat, add3.cltvExpiry, pr.paymentSecret.get)))
|
f.sender.send(handler, IncomingPaymentPacket.FinalPacket(add3, PaymentOnion.createMultiPartPayload(add3.amountMsat, 1000 msat, add3.cltvExpiry, pr.paymentSecret.get, pr.paymentMetadata)))
|
||||||
|
|
||||||
// the fulfill are not necessarily in the same order as the commands
|
// the fulfill are not necessarily in the same order as the commands
|
||||||
f.register.expectMsgAllOf(
|
f.register.expectMsgAllOf(
|
||||||
|
@ -524,7 +522,7 @@ class MultiPartHandlerSpec extends TestKitBaseClass with FixtureAnyFunSuiteLike
|
||||||
assert(nodeParams.db.payments.getIncomingPayment(paymentHash) === None)
|
assert(nodeParams.db.payments.getIncomingPayment(paymentHash) === None)
|
||||||
|
|
||||||
val add = UpdateAddHtlc(ByteVector32.One, 0, 1000 msat, paymentHash, defaultExpiry, TestConstants.emptyOnionPacket)
|
val add = UpdateAddHtlc(ByteVector32.One, 0, 1000 msat, paymentHash, defaultExpiry, TestConstants.emptyOnionPacket)
|
||||||
sender.send(handlerWithoutMpp, IncomingPaymentPacket.FinalPacket(add, PaymentOnion.createSinglePartPayload(add.amountMsat, add.cltvExpiry, paymentSecret)))
|
sender.send(handlerWithoutMpp, IncomingPaymentPacket.FinalPacket(add, PaymentOnion.createSinglePartPayload(add.amountMsat, add.cltvExpiry, paymentSecret, None)))
|
||||||
val cmd = register.expectMsgType[Register.Forward[CMD_FAIL_HTLC]].message
|
val cmd = register.expectMsgType[Register.Forward[CMD_FAIL_HTLC]].message
|
||||||
assert(cmd.id === add.id)
|
assert(cmd.id === add.id)
|
||||||
assert(cmd.reason === Right(IncorrectOrUnknownPaymentDetails(1000 msat, nodeParams.currentBlockHeight)))
|
assert(cmd.reason === Right(IncorrectOrUnknownPaymentDetails(1000 msat, nodeParams.currentBlockHeight)))
|
||||||
|
@ -538,7 +536,7 @@ class MultiPartHandlerSpec extends TestKitBaseClass with FixtureAnyFunSuiteLike
|
||||||
assert(nodeParams.db.payments.getIncomingPayment(paymentHash) === None)
|
assert(nodeParams.db.payments.getIncomingPayment(paymentHash) === None)
|
||||||
|
|
||||||
val add = UpdateAddHtlc(ByteVector32.One, 0, 800 msat, paymentHash, defaultExpiry, TestConstants.emptyOnionPacket)
|
val add = UpdateAddHtlc(ByteVector32.One, 0, 800 msat, paymentHash, defaultExpiry, TestConstants.emptyOnionPacket)
|
||||||
sender.send(handlerWithMpp, IncomingPaymentPacket.FinalPacket(add, PaymentOnion.createMultiPartPayload(add.amountMsat, 1000 msat, add.cltvExpiry, paymentSecret)))
|
sender.send(handlerWithMpp, IncomingPaymentPacket.FinalPacket(add, PaymentOnion.createMultiPartPayload(add.amountMsat, 1000 msat, add.cltvExpiry, paymentSecret, Some(hex"012345"))))
|
||||||
val cmd = register.expectMsgType[Register.Forward[CMD_FAIL_HTLC]].message
|
val cmd = register.expectMsgType[Register.Forward[CMD_FAIL_HTLC]].message
|
||||||
assert(cmd.id === add.id)
|
assert(cmd.id === add.id)
|
||||||
assert(cmd.reason === Right(IncorrectOrUnknownPaymentDetails(1000 msat, nodeParams.currentBlockHeight)))
|
assert(cmd.reason === Right(IncorrectOrUnknownPaymentDetails(1000 msat, nodeParams.currentBlockHeight)))
|
||||||
|
|
|
@ -78,7 +78,7 @@ class MultiPartPaymentLifecycleSpec extends TestKitBaseClass with FixtureAnyFunS
|
||||||
import f._
|
import f._
|
||||||
|
|
||||||
assert(payFsm.stateName === WAIT_FOR_PAYMENT_REQUEST)
|
assert(payFsm.stateName === WAIT_FOR_PAYMENT_REQUEST)
|
||||||
val payment = SendMultiPartPayment(sender.ref, randomBytes32(), e, finalAmount, expiry, 1, routeParams = routeParams.copy(randomize = true))
|
val payment = SendMultiPartPayment(sender.ref, randomBytes32(), e, finalAmount, expiry, 1, None, routeParams = routeParams.copy(randomize = true))
|
||||||
sender.send(payFsm, payment)
|
sender.send(payFsm, payment)
|
||||||
|
|
||||||
router.expectMsg(RouteRequest(nodeParams.nodeId, e, finalAmount, maxFee, routeParams = routeParams.copy(randomize = false), allowMultiPart = true, paymentContext = Some(cfg.paymentContext)))
|
router.expectMsg(RouteRequest(nodeParams.nodeId, e, finalAmount, maxFee, routeParams = routeParams.copy(randomize = false), allowMultiPart = true, paymentContext = Some(cfg.paymentContext)))
|
||||||
|
@ -111,7 +111,7 @@ class MultiPartPaymentLifecycleSpec extends TestKitBaseClass with FixtureAnyFunS
|
||||||
import f._
|
import f._
|
||||||
|
|
||||||
assert(payFsm.stateName === WAIT_FOR_PAYMENT_REQUEST)
|
assert(payFsm.stateName === WAIT_FOR_PAYMENT_REQUEST)
|
||||||
val payment = SendMultiPartPayment(sender.ref, randomBytes32(), e, 1200000 msat, expiry, 1, routeParams = routeParams.copy(randomize = false))
|
val payment = SendMultiPartPayment(sender.ref, randomBytes32(), e, 1200000 msat, expiry, 1, Some(hex"012345"), routeParams = routeParams.copy(randomize = false))
|
||||||
sender.send(payFsm, payment)
|
sender.send(payFsm, payment)
|
||||||
|
|
||||||
router.expectMsg(RouteRequest(nodeParams.nodeId, e, 1200000 msat, maxFee, routeParams = routeParams.copy(randomize = false), allowMultiPart = true, paymentContext = Some(cfg.paymentContext)))
|
router.expectMsg(RouteRequest(nodeParams.nodeId, e, 1200000 msat, maxFee, routeParams = routeParams.copy(randomize = false), allowMultiPart = true, paymentContext = Some(cfg.paymentContext)))
|
||||||
|
@ -126,6 +126,7 @@ class MultiPartPaymentLifecycleSpec extends TestKitBaseClass with FixtureAnyFunS
|
||||||
assert(childPayments.map(_.route).toSet === routes.map(r => Right(r)).toSet)
|
assert(childPayments.map(_.route).toSet === routes.map(r => Right(r)).toSet)
|
||||||
assert(childPayments.map(_.finalPayload.expiry).toSet === Set(expiry))
|
assert(childPayments.map(_.finalPayload.expiry).toSet === Set(expiry))
|
||||||
assert(childPayments.map(_.finalPayload.paymentSecret).toSet === Set(payment.paymentSecret))
|
assert(childPayments.map(_.finalPayload.paymentSecret).toSet === Set(payment.paymentSecret))
|
||||||
|
assert(childPayments.map(_.finalPayload.paymentMetadata).toSet === Set(Some(hex"012345")))
|
||||||
assert(childPayments.map(_.finalPayload.amount).toSet === Set(500000 msat, 700000 msat))
|
assert(childPayments.map(_.finalPayload.amount).toSet === Set(500000 msat, 700000 msat))
|
||||||
assert(childPayments.map(_.finalPayload.totalAmount).toSet === Set(1200000 msat))
|
assert(childPayments.map(_.finalPayload.totalAmount).toSet === Set(1200000 msat))
|
||||||
assert(payFsm.stateName === PAYMENT_IN_PROGRESS)
|
assert(payFsm.stateName === PAYMENT_IN_PROGRESS)
|
||||||
|
@ -149,7 +150,7 @@ class MultiPartPaymentLifecycleSpec extends TestKitBaseClass with FixtureAnyFunS
|
||||||
// We include a bunch of additional tlv records.
|
// We include a bunch of additional tlv records.
|
||||||
val trampolineTlv = OnionPaymentPayloadTlv.TrampolineOnion(OnionRoutingPacket(0, ByteVector.fill(33)(0), ByteVector.fill(400)(0), randomBytes32()))
|
val trampolineTlv = OnionPaymentPayloadTlv.TrampolineOnion(OnionRoutingPacket(0, ByteVector.fill(33)(0), ByteVector.fill(400)(0), randomBytes32()))
|
||||||
val userCustomTlv = GenericTlv(UInt64(561), hex"deadbeef")
|
val userCustomTlv = GenericTlv(UInt64(561), hex"deadbeef")
|
||||||
val payment = SendMultiPartPayment(sender.ref, randomBytes32(), e, finalAmount + 1000.msat, expiry, 1, routeParams = routeParams, additionalTlvs = Seq(trampolineTlv), userCustomTlvs = Seq(userCustomTlv))
|
val payment = SendMultiPartPayment(sender.ref, randomBytes32(), e, finalAmount + 1000.msat, expiry, 1, None, routeParams = routeParams, additionalTlvs = Seq(trampolineTlv), userCustomTlvs = Seq(userCustomTlv))
|
||||||
sender.send(payFsm, payment)
|
sender.send(payFsm, payment)
|
||||||
router.expectMsgType[RouteRequest]
|
router.expectMsgType[RouteRequest]
|
||||||
router.send(payFsm, RouteResponse(Seq(Route(500000 msat, hop_ab_1 :: hop_be :: Nil), Route(501000 msat, hop_ac_1 :: hop_ce :: Nil))))
|
router.send(payFsm, RouteResponse(Seq(Route(500000 msat, hop_ab_1 :: hop_be :: Nil), Route(501000 msat, hop_ac_1 :: hop_ce :: Nil))))
|
||||||
|
@ -173,7 +174,7 @@ class MultiPartPaymentLifecycleSpec extends TestKitBaseClass with FixtureAnyFunS
|
||||||
test("successful retry") { f =>
|
test("successful retry") { f =>
|
||||||
import f._
|
import f._
|
||||||
|
|
||||||
val payment = SendMultiPartPayment(sender.ref, randomBytes32(), e, finalAmount, expiry, 3, routeParams = routeParams)
|
val payment = SendMultiPartPayment(sender.ref, randomBytes32(), e, finalAmount, expiry, 3, None, routeParams = routeParams)
|
||||||
sender.send(payFsm, payment)
|
sender.send(payFsm, payment)
|
||||||
router.expectMsgType[RouteRequest]
|
router.expectMsgType[RouteRequest]
|
||||||
val failingRoute = Route(finalAmount, hop_ab_1 :: hop_be :: Nil)
|
val failingRoute = Route(finalAmount, hop_ab_1 :: hop_be :: Nil)
|
||||||
|
@ -206,7 +207,7 @@ class MultiPartPaymentLifecycleSpec extends TestKitBaseClass with FixtureAnyFunS
|
||||||
test("retry failures while waiting for routes") { f =>
|
test("retry failures while waiting for routes") { f =>
|
||||||
import f._
|
import f._
|
||||||
|
|
||||||
val payment = SendMultiPartPayment(sender.ref, randomBytes32(), e, finalAmount, expiry, 3, routeParams = routeParams)
|
val payment = SendMultiPartPayment(sender.ref, randomBytes32(), e, finalAmount, expiry, 3, None, routeParams = routeParams)
|
||||||
sender.send(payFsm, payment)
|
sender.send(payFsm, payment)
|
||||||
router.expectMsgType[RouteRequest]
|
router.expectMsgType[RouteRequest]
|
||||||
router.send(payFsm, RouteResponse(Seq(Route(400000 msat, hop_ab_1 :: hop_be :: Nil), Route(600000 msat, hop_ab_2 :: hop_be :: Nil))))
|
router.send(payFsm, RouteResponse(Seq(Route(400000 msat, hop_ab_1 :: hop_be :: Nil), Route(600000 msat, hop_ab_2 :: hop_be :: Nil))))
|
||||||
|
@ -248,7 +249,7 @@ class MultiPartPaymentLifecycleSpec extends TestKitBaseClass with FixtureAnyFunS
|
||||||
test("retry local channel failures") { f =>
|
test("retry local channel failures") { f =>
|
||||||
import f._
|
import f._
|
||||||
|
|
||||||
val payment = SendMultiPartPayment(sender.ref, randomBytes32(), e, finalAmount, expiry, 3, routeParams = routeParams)
|
val payment = SendMultiPartPayment(sender.ref, randomBytes32(), e, finalAmount, expiry, 3, None, routeParams = routeParams)
|
||||||
sender.send(payFsm, payment)
|
sender.send(payFsm, payment)
|
||||||
router.expectMsgType[RouteRequest]
|
router.expectMsgType[RouteRequest]
|
||||||
router.send(payFsm, RouteResponse(Seq(Route(finalAmount, hop_ab_1 :: hop_be :: Nil))))
|
router.send(payFsm, RouteResponse(Seq(Route(finalAmount, hop_ab_1 :: hop_be :: Nil))))
|
||||||
|
@ -273,7 +274,7 @@ class MultiPartPaymentLifecycleSpec extends TestKitBaseClass with FixtureAnyFunS
|
||||||
test("retry without ignoring channels") { f =>
|
test("retry without ignoring channels") { f =>
|
||||||
import f._
|
import f._
|
||||||
|
|
||||||
val payment = SendMultiPartPayment(sender.ref, randomBytes32(), e, finalAmount, expiry, 3, routeParams = routeParams)
|
val payment = SendMultiPartPayment(sender.ref, randomBytes32(), e, finalAmount, expiry, 3, None, routeParams = routeParams)
|
||||||
sender.send(payFsm, payment)
|
sender.send(payFsm, payment)
|
||||||
router.expectMsgType[RouteRequest]
|
router.expectMsgType[RouteRequest]
|
||||||
router.send(payFsm, RouteResponse(Seq(Route(500000 msat, hop_ab_1 :: hop_be :: Nil), Route(500000 msat, hop_ab_1 :: hop_be :: Nil))))
|
router.send(payFsm, RouteResponse(Seq(Route(500000 msat, hop_ab_1 :: hop_be :: Nil), Route(500000 msat, hop_ab_1 :: hop_be :: Nil))))
|
||||||
|
@ -317,7 +318,7 @@ class MultiPartPaymentLifecycleSpec extends TestKitBaseClass with FixtureAnyFunS
|
||||||
|
|
||||||
// The B -> E channel is private and provided in the invoice routing hints.
|
// The B -> E channel is private and provided in the invoice routing hints.
|
||||||
val routingHint = ExtraHop(b, hop_be.lastUpdate.shortChannelId, hop_be.lastUpdate.feeBaseMsat, hop_be.lastUpdate.feeProportionalMillionths, hop_be.lastUpdate.cltvExpiryDelta)
|
val routingHint = ExtraHop(b, hop_be.lastUpdate.shortChannelId, hop_be.lastUpdate.feeBaseMsat, hop_be.lastUpdate.feeProportionalMillionths, hop_be.lastUpdate.cltvExpiryDelta)
|
||||||
val payment = SendMultiPartPayment(sender.ref, randomBytes32(), e, finalAmount, expiry, 3, routeParams = routeParams, assistedRoutes = List(List(routingHint)))
|
val payment = SendMultiPartPayment(sender.ref, randomBytes32(), e, finalAmount, expiry, 3, None, routeParams = routeParams, assistedRoutes = List(List(routingHint)))
|
||||||
sender.send(payFsm, payment)
|
sender.send(payFsm, payment)
|
||||||
assert(router.expectMsgType[RouteRequest].assistedRoutes.head.head === routingHint)
|
assert(router.expectMsgType[RouteRequest].assistedRoutes.head.head === routingHint)
|
||||||
val route = Route(finalAmount, hop_ab_1 :: hop_be :: Nil)
|
val route = Route(finalAmount, hop_ab_1 :: hop_be :: Nil)
|
||||||
|
@ -338,7 +339,7 @@ class MultiPartPaymentLifecycleSpec extends TestKitBaseClass with FixtureAnyFunS
|
||||||
|
|
||||||
// The B -> E channel is private and provided in the invoice routing hints.
|
// The B -> E channel is private and provided in the invoice routing hints.
|
||||||
val routingHint = ExtraHop(b, hop_be.lastUpdate.shortChannelId, hop_be.lastUpdate.feeBaseMsat, hop_be.lastUpdate.feeProportionalMillionths, hop_be.lastUpdate.cltvExpiryDelta)
|
val routingHint = ExtraHop(b, hop_be.lastUpdate.shortChannelId, hop_be.lastUpdate.feeBaseMsat, hop_be.lastUpdate.feeProportionalMillionths, hop_be.lastUpdate.cltvExpiryDelta)
|
||||||
val payment = SendMultiPartPayment(sender.ref, randomBytes32(), e, finalAmount, expiry, 3, routeParams = routeParams, assistedRoutes = List(List(routingHint)))
|
val payment = SendMultiPartPayment(sender.ref, randomBytes32(), e, finalAmount, expiry, 3, None, routeParams = routeParams, assistedRoutes = List(List(routingHint)))
|
||||||
sender.send(payFsm, payment)
|
sender.send(payFsm, payment)
|
||||||
assert(router.expectMsgType[RouteRequest].assistedRoutes.head.head === routingHint)
|
assert(router.expectMsgType[RouteRequest].assistedRoutes.head.head === routingHint)
|
||||||
val route = Route(finalAmount, hop_ab_1 :: hop_be :: Nil)
|
val route = Route(finalAmount, hop_ab_1 :: hop_be :: Nil)
|
||||||
|
@ -397,7 +398,7 @@ class MultiPartPaymentLifecycleSpec extends TestKitBaseClass with FixtureAnyFunS
|
||||||
test("abort after too many failed attempts") { f =>
|
test("abort after too many failed attempts") { f =>
|
||||||
import f._
|
import f._
|
||||||
|
|
||||||
val payment = SendMultiPartPayment(sender.ref, randomBytes32(), e, finalAmount, expiry, 2, routeParams = routeParams)
|
val payment = SendMultiPartPayment(sender.ref, randomBytes32(), e, finalAmount, expiry, 2, None, routeParams = routeParams)
|
||||||
sender.send(payFsm, payment)
|
sender.send(payFsm, payment)
|
||||||
router.expectMsgType[RouteRequest]
|
router.expectMsgType[RouteRequest]
|
||||||
router.send(payFsm, RouteResponse(Seq(Route(500000 msat, hop_ab_1 :: hop_be :: Nil), Route(500000 msat, hop_ac_1 :: hop_ce :: Nil))))
|
router.send(payFsm, RouteResponse(Seq(Route(500000 msat, hop_ab_1 :: hop_be :: Nil), Route(500000 msat, hop_ac_1 :: hop_ce :: Nil))))
|
||||||
|
@ -428,7 +429,7 @@ class MultiPartPaymentLifecycleSpec extends TestKitBaseClass with FixtureAnyFunS
|
||||||
import f._
|
import f._
|
||||||
|
|
||||||
sender.watch(payFsm)
|
sender.watch(payFsm)
|
||||||
val payment = SendMultiPartPayment(sender.ref, randomBytes32(), e, finalAmount, expiry, 5, routeParams = routeParams)
|
val payment = SendMultiPartPayment(sender.ref, randomBytes32(), e, finalAmount, expiry, 5, None, routeParams = routeParams)
|
||||||
sender.send(payFsm, payment)
|
sender.send(payFsm, payment)
|
||||||
router.expectMsgType[RouteRequest]
|
router.expectMsgType[RouteRequest]
|
||||||
router.send(payFsm, Status.Failure(RouteNotFound))
|
router.send(payFsm, Status.Failure(RouteNotFound))
|
||||||
|
@ -458,7 +459,7 @@ class MultiPartPaymentLifecycleSpec extends TestKitBaseClass with FixtureAnyFunS
|
||||||
test("abort if recipient sends error") { f =>
|
test("abort if recipient sends error") { f =>
|
||||||
import f._
|
import f._
|
||||||
|
|
||||||
val payment = SendMultiPartPayment(sender.ref, randomBytes32(), e, finalAmount, expiry, 5, routeParams = routeParams)
|
val payment = SendMultiPartPayment(sender.ref, randomBytes32(), e, finalAmount, expiry, 5, None, routeParams = routeParams)
|
||||||
sender.send(payFsm, payment)
|
sender.send(payFsm, payment)
|
||||||
router.expectMsgType[RouteRequest]
|
router.expectMsgType[RouteRequest]
|
||||||
router.send(payFsm, RouteResponse(Seq(Route(finalAmount, hop_ab_1 :: hop_be :: Nil))))
|
router.send(payFsm, RouteResponse(Seq(Route(finalAmount, hop_ab_1 :: hop_be :: Nil))))
|
||||||
|
@ -479,7 +480,7 @@ class MultiPartPaymentLifecycleSpec extends TestKitBaseClass with FixtureAnyFunS
|
||||||
test("abort if payment gets settled on chain") { f =>
|
test("abort if payment gets settled on chain") { f =>
|
||||||
import f._
|
import f._
|
||||||
|
|
||||||
val payment = SendMultiPartPayment(sender.ref, randomBytes32(), e, finalAmount, expiry, 5, routeParams = routeParams)
|
val payment = SendMultiPartPayment(sender.ref, randomBytes32(), e, finalAmount, expiry, 5, None, routeParams = routeParams)
|
||||||
sender.send(payFsm, payment)
|
sender.send(payFsm, payment)
|
||||||
router.expectMsgType[RouteRequest]
|
router.expectMsgType[RouteRequest]
|
||||||
router.send(payFsm, RouteResponse(Seq(Route(finalAmount, hop_ab_1 :: hop_be :: Nil))))
|
router.send(payFsm, RouteResponse(Seq(Route(finalAmount, hop_ab_1 :: hop_be :: Nil))))
|
||||||
|
@ -493,7 +494,7 @@ class MultiPartPaymentLifecycleSpec extends TestKitBaseClass with FixtureAnyFunS
|
||||||
test("abort if recipient sends error during retry") { f =>
|
test("abort if recipient sends error during retry") { f =>
|
||||||
import f._
|
import f._
|
||||||
|
|
||||||
val payment = SendMultiPartPayment(sender.ref, randomBytes32(), e, finalAmount, expiry, 5, routeParams = routeParams)
|
val payment = SendMultiPartPayment(sender.ref, randomBytes32(), e, finalAmount, expiry, 5, None, routeParams = routeParams)
|
||||||
sender.send(payFsm, payment)
|
sender.send(payFsm, payment)
|
||||||
router.expectMsgType[RouteRequest]
|
router.expectMsgType[RouteRequest]
|
||||||
router.send(payFsm, RouteResponse(Seq(Route(400000 msat, hop_ab_1 :: hop_be :: Nil), Route(600000 msat, hop_ac_1 :: hop_ce :: Nil))))
|
router.send(payFsm, RouteResponse(Seq(Route(400000 msat, hop_ab_1 :: hop_be :: Nil), Route(600000 msat, hop_ac_1 :: hop_ce :: Nil))))
|
||||||
|
@ -511,7 +512,7 @@ class MultiPartPaymentLifecycleSpec extends TestKitBaseClass with FixtureAnyFunS
|
||||||
test("receive partial success after retriable failure (recipient spec violation)") { f =>
|
test("receive partial success after retriable failure (recipient spec violation)") { f =>
|
||||||
import f._
|
import f._
|
||||||
|
|
||||||
val payment = SendMultiPartPayment(sender.ref, randomBytes32(), e, finalAmount, expiry, 5, routeParams = routeParams)
|
val payment = SendMultiPartPayment(sender.ref, randomBytes32(), e, finalAmount, expiry, 5, None, routeParams = routeParams)
|
||||||
sender.send(payFsm, payment)
|
sender.send(payFsm, payment)
|
||||||
router.expectMsgType[RouteRequest]
|
router.expectMsgType[RouteRequest]
|
||||||
router.send(payFsm, RouteResponse(Seq(Route(400000 msat, hop_ab_1 :: hop_be :: Nil), Route(600000 msat, hop_ac_1 :: hop_ce :: Nil))))
|
router.send(payFsm, RouteResponse(Seq(Route(400000 msat, hop_ab_1 :: hop_be :: Nil), Route(600000 msat, hop_ac_1 :: hop_ce :: Nil))))
|
||||||
|
@ -531,7 +532,7 @@ class MultiPartPaymentLifecycleSpec extends TestKitBaseClass with FixtureAnyFunS
|
||||||
test("receive partial success after abort (recipient spec violation)") { f =>
|
test("receive partial success after abort (recipient spec violation)") { f =>
|
||||||
import f._
|
import f._
|
||||||
|
|
||||||
val payment = SendMultiPartPayment(sender.ref, randomBytes32(), e, finalAmount, expiry, 5, routeParams = routeParams)
|
val payment = SendMultiPartPayment(sender.ref, randomBytes32(), e, finalAmount, expiry, 5, None, routeParams = routeParams)
|
||||||
sender.send(payFsm, payment)
|
sender.send(payFsm, payment)
|
||||||
router.expectMsgType[RouteRequest]
|
router.expectMsgType[RouteRequest]
|
||||||
router.send(payFsm, RouteResponse(Seq(Route(400000 msat, hop_ab_1 :: hop_be :: Nil), Route(600000 msat, hop_ac_1 :: hop_ce :: Nil))))
|
router.send(payFsm, RouteResponse(Seq(Route(400000 msat, hop_ab_1 :: hop_be :: Nil), Route(600000 msat, hop_ac_1 :: hop_ce :: Nil))))
|
||||||
|
@ -564,7 +565,7 @@ class MultiPartPaymentLifecycleSpec extends TestKitBaseClass with FixtureAnyFunS
|
||||||
test("receive partial failure after success (recipient spec violation)") { f =>
|
test("receive partial failure after success (recipient spec violation)") { f =>
|
||||||
import f._
|
import f._
|
||||||
|
|
||||||
val payment = SendMultiPartPayment(sender.ref, randomBytes32(), e, finalAmount, expiry, 5, routeParams = routeParams)
|
val payment = SendMultiPartPayment(sender.ref, randomBytes32(), e, finalAmount, expiry, 5, None, routeParams = routeParams)
|
||||||
sender.send(payFsm, payment)
|
sender.send(payFsm, payment)
|
||||||
router.expectMsgType[RouteRequest]
|
router.expectMsgType[RouteRequest]
|
||||||
router.send(payFsm, RouteResponse(Seq(Route(400000 msat, hop_ab_1 :: hop_be :: Nil), Route(600000 msat, hop_ac_1 :: hop_ce :: Nil))))
|
router.send(payFsm, RouteResponse(Seq(Route(400000 msat, hop_ab_1 :: hop_be :: Nil), Route(600000 msat, hop_ac_1 :: hop_ce :: Nil))))
|
||||||
|
|
|
@ -28,6 +28,7 @@ import fr.acinq.eclair.payment.OutgoingPaymentPacket.Upstream
|
||||||
import fr.acinq.eclair.payment.PaymentPacketSpec._
|
import fr.acinq.eclair.payment.PaymentPacketSpec._
|
||||||
import fr.acinq.eclair.payment.PaymentRequest.{ExtraHop, PaymentRequestFeatures}
|
import fr.acinq.eclair.payment.PaymentRequest.{ExtraHop, PaymentRequestFeatures}
|
||||||
import fr.acinq.eclair.payment.send.MultiPartPaymentLifecycle.SendMultiPartPayment
|
import fr.acinq.eclair.payment.send.MultiPartPaymentLifecycle.SendMultiPartPayment
|
||||||
|
import fr.acinq.eclair.payment.send.PaymentError.UnsupportedFeatures
|
||||||
import fr.acinq.eclair.payment.send.PaymentInitiator._
|
import fr.acinq.eclair.payment.send.PaymentInitiator._
|
||||||
import fr.acinq.eclair.payment.send.{PaymentError, PaymentInitiator, PaymentLifecycle}
|
import fr.acinq.eclair.payment.send.{PaymentError, PaymentInitiator, PaymentLifecycle}
|
||||||
import fr.acinq.eclair.router.RouteNotFound
|
import fr.acinq.eclair.router.RouteNotFound
|
||||||
|
@ -35,10 +36,10 @@ import fr.acinq.eclair.router.Router._
|
||||||
import fr.acinq.eclair.wire.protocol.OnionPaymentPayloadTlv.{AmountToForward, KeySend, OutgoingCltv}
|
import fr.acinq.eclair.wire.protocol.OnionPaymentPayloadTlv.{AmountToForward, KeySend, OutgoingCltv}
|
||||||
import fr.acinq.eclair.wire.protocol.PaymentOnion.FinalTlvPayload
|
import fr.acinq.eclair.wire.protocol.PaymentOnion.FinalTlvPayload
|
||||||
import fr.acinq.eclair.wire.protocol._
|
import fr.acinq.eclair.wire.protocol._
|
||||||
import fr.acinq.eclair.{CltvExpiryDelta, Features, MilliSatoshiLong, NodeParams, TestConstants, TestKitBaseClass, randomBytes32, randomKey}
|
import fr.acinq.eclair.{CltvExpiryDelta, Feature, FeatureSupport, Features, InvoiceFeature, MilliSatoshiLong, NodeParams, TestConstants, TestKitBaseClass, TimestampSecond, randomBytes32, randomKey}
|
||||||
import org.scalatest.funsuite.FixtureAnyFunSuiteLike
|
import org.scalatest.funsuite.FixtureAnyFunSuiteLike
|
||||||
import org.scalatest.{Outcome, Tag}
|
import org.scalatest.{Outcome, Tag}
|
||||||
import scodec.bits.HexStringSyntax
|
import scodec.bits.{BinStringSyntax, ByteVector, HexStringSyntax}
|
||||||
|
|
||||||
import java.util.UUID
|
import java.util.UUID
|
||||||
import scala.concurrent.duration._
|
import scala.concurrent.duration._
|
||||||
|
@ -51,17 +52,24 @@ class PaymentInitiatorSpec extends TestKitBaseClass with FixtureAnyFunSuiteLike
|
||||||
|
|
||||||
case class FixtureParam(nodeParams: NodeParams, initiator: TestActorRef[PaymentInitiator], payFsm: TestProbe, multiPartPayFsm: TestProbe, sender: TestProbe, eventListener: TestProbe)
|
case class FixtureParam(nodeParams: NodeParams, initiator: TestActorRef[PaymentInitiator], payFsm: TestProbe, multiPartPayFsm: TestProbe, sender: TestProbe, eventListener: TestProbe)
|
||||||
|
|
||||||
val featuresWithoutMpp = Features(
|
val featuresWithoutMpp: Map[Feature with InvoiceFeature, FeatureSupport] = Map(
|
||||||
VariableLengthOnion -> Mandatory,
|
VariableLengthOnion -> Mandatory,
|
||||||
PaymentSecret -> Mandatory
|
PaymentSecret -> Mandatory
|
||||||
)
|
)
|
||||||
|
|
||||||
val featuresWithMpp = Features(
|
val featuresWithMpp: Map[Feature with InvoiceFeature, FeatureSupport] = Map(
|
||||||
VariableLengthOnion -> Mandatory,
|
VariableLengthOnion -> Mandatory,
|
||||||
PaymentSecret -> Mandatory,
|
PaymentSecret -> Mandatory,
|
||||||
BasicMultiPartPayment -> Optional,
|
BasicMultiPartPayment -> Optional,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
val featuresWithTrampoline: Map[Feature with InvoiceFeature, FeatureSupport] = Map(
|
||||||
|
VariableLengthOnion -> Mandatory,
|
||||||
|
PaymentSecret -> Mandatory,
|
||||||
|
BasicMultiPartPayment -> Optional,
|
||||||
|
TrampolinePayment -> Optional,
|
||||||
|
)
|
||||||
|
|
||||||
case class FakePaymentFactory(payFsm: TestProbe, multiPartPayFsm: TestProbe) extends PaymentInitiator.MultiPartPaymentFactory {
|
case class FakePaymentFactory(payFsm: TestProbe, multiPartPayFsm: TestProbe) extends PaymentInitiator.MultiPartPaymentFactory {
|
||||||
// @formatter:off
|
// @formatter:off
|
||||||
override def spawnOutgoingPayment(context: ActorContext, cfg: SendPaymentConfig): ActorRef = {
|
override def spawnOutgoingPayment(context: ActorContext, cfg: SendPaymentConfig): ActorRef = {
|
||||||
|
@ -77,7 +85,7 @@ class PaymentInitiatorSpec extends TestKitBaseClass with FixtureAnyFunSuiteLike
|
||||||
|
|
||||||
override def withFixture(test: OneArgTest): Outcome = {
|
override def withFixture(test: OneArgTest): Outcome = {
|
||||||
val features = if (test.tags.contains("mpp_disabled")) featuresWithoutMpp else featuresWithMpp
|
val features = if (test.tags.contains("mpp_disabled")) featuresWithoutMpp else featuresWithMpp
|
||||||
val nodeParams = TestConstants.Alice.nodeParams.copy(features = features)
|
val nodeParams = TestConstants.Alice.nodeParams.copy(features = Features(features.collect { case (f, s) => (f: Feature, s) }))
|
||||||
val (sender, payFsm, multiPartPayFsm) = (TestProbe(), TestProbe(), TestProbe())
|
val (sender, payFsm, multiPartPayFsm) = (TestProbe(), TestProbe(), TestProbe())
|
||||||
val eventListener = TestProbe()
|
val eventListener = TestProbe()
|
||||||
system.eventStream.subscribe(eventListener.ref, classOf[PaymentEvent])
|
system.eventStream.subscribe(eventListener.ref, classOf[PaymentEvent])
|
||||||
|
@ -115,13 +123,21 @@ class PaymentInitiatorSpec extends TestKitBaseClass with FixtureAnyFunSuiteLike
|
||||||
test("reject payment with unknown mandatory feature") { f =>
|
test("reject payment with unknown mandatory feature") { f =>
|
||||||
import f._
|
import f._
|
||||||
val unknownFeature = 42
|
val unknownFeature = 42
|
||||||
val pr = PaymentRequest(Block.LivenetGenesisBlock.hash, Some(finalAmount), paymentHash, randomKey(), Left("Some invoice"), CltvExpiryDelta(18), features = PaymentRequestFeatures(Features.VariableLengthOnion.mandatory, Features.PaymentSecret.mandatory, unknownFeature))
|
val taggedFields = List(
|
||||||
|
PaymentRequest.PaymentHash(paymentHash),
|
||||||
|
PaymentRequest.Description("Some invoice"),
|
||||||
|
PaymentRequest.PaymentSecret(randomBytes32()),
|
||||||
|
PaymentRequest.Expiry(3600),
|
||||||
|
PaymentRequest.PaymentRequestFeatures(bin"000001000000000000000000000000000100000100000000")
|
||||||
|
)
|
||||||
|
val pr = PaymentRequest("lnbc", Some(finalAmount), TimestampSecond.now(), randomKey().publicKey, taggedFields, ByteVector.empty)
|
||||||
val req = SendPaymentToNode(finalAmount + 100.msat, pr, 1, CltvExpiryDelta(42), routeParams = nodeParams.routerConf.pathFindingExperimentConf.getRandomConf().getDefaultRouteParams)
|
val req = SendPaymentToNode(finalAmount + 100.msat, pr, 1, CltvExpiryDelta(42), routeParams = nodeParams.routerConf.pathFindingExperimentConf.getRandomConf().getDefaultRouteParams)
|
||||||
sender.send(initiator, req)
|
sender.send(initiator, req)
|
||||||
val id = sender.expectMsgType[UUID]
|
val id = sender.expectMsgType[UUID]
|
||||||
val fail = sender.expectMsgType[PaymentFailed]
|
val fail = sender.expectMsgType[PaymentFailed]
|
||||||
assert(fail.id === id)
|
assert(fail.id === id)
|
||||||
assert(fail.failures.head.isInstanceOf[LocalFailure])
|
assert(fail.failures.head.isInstanceOf[LocalFailure])
|
||||||
|
assert(fail.failures.head.asInstanceOf[LocalFailure].t === UnsupportedFeatures(pr.features.features))
|
||||||
}
|
}
|
||||||
|
|
||||||
test("forward payment with pre-defined route") { f =>
|
test("forward payment with pre-defined route") { f =>
|
||||||
|
@ -134,13 +150,13 @@ class PaymentInitiatorSpec extends TestKitBaseClass with FixtureAnyFunSuiteLike
|
||||||
sender.send(initiator, SendPaymentToRoute(finalAmount, finalAmount, pr, ignoredFinalExpiryDelta, route, None, None, None, 0 msat, CltvExpiryDelta(0), Nil))
|
sender.send(initiator, SendPaymentToRoute(finalAmount, finalAmount, pr, ignoredFinalExpiryDelta, route, None, None, None, 0 msat, CltvExpiryDelta(0), Nil))
|
||||||
val payment = sender.expectMsgType[SendPaymentToRouteResponse]
|
val payment = sender.expectMsgType[SendPaymentToRouteResponse]
|
||||||
payFsm.expectMsg(SendPaymentConfig(payment.paymentId, payment.parentId, None, paymentHash, finalAmount, c, Upstream.Local(payment.paymentId), Some(pr), storeInDb = true, publishEvent = true, recordPathFindingMetrics = false, Nil))
|
payFsm.expectMsg(SendPaymentConfig(payment.paymentId, payment.parentId, None, paymentHash, finalAmount, c, Upstream.Local(payment.paymentId), Some(pr), storeInDb = true, publishEvent = true, recordPathFindingMetrics = false, Nil))
|
||||||
payFsm.expectMsg(PaymentLifecycle.SendPaymentToRoute(sender.ref, Left(route), PaymentOnion.createSinglePartPayload(finalAmount, finalExpiryDelta.toCltvExpiry(nodeParams.currentBlockHeight + 1), pr.paymentSecret.get)))
|
payFsm.expectMsg(PaymentLifecycle.SendPaymentToRoute(sender.ref, Left(route), PaymentOnion.createSinglePartPayload(finalAmount, finalExpiryDelta.toCltvExpiry(nodeParams.currentBlockHeight + 1), pr.paymentSecret.get, pr.paymentMetadata)))
|
||||||
}
|
}
|
||||||
|
|
||||||
test("forward single-part payment when multi-part deactivated", Tag("mpp_disabled")) { f =>
|
test("forward single-part payment when multi-part deactivated", Tag("mpp_disabled")) { f =>
|
||||||
import f._
|
import f._
|
||||||
val finalExpiryDelta = CltvExpiryDelta(24)
|
val finalExpiryDelta = CltvExpiryDelta(24)
|
||||||
val pr = PaymentRequest(Block.LivenetGenesisBlock.hash, Some(finalAmount), paymentHash, priv_c.privateKey, Left("Some MPP invoice"), finalExpiryDelta, features = PaymentRequestFeatures(VariableLengthOnion.mandatory, PaymentSecret.mandatory, BasicMultiPartPayment.optional))
|
val pr = PaymentRequest(Block.LivenetGenesisBlock.hash, Some(finalAmount), paymentHash, priv_c.privateKey, Left("Some MPP invoice"), finalExpiryDelta, features = PaymentRequestFeatures(featuresWithMpp))
|
||||||
val req = SendPaymentToNode(finalAmount, pr, 1, /* ignored since the invoice provides it */ CltvExpiryDelta(12), routeParams = nodeParams.routerConf.pathFindingExperimentConf.getRandomConf().getDefaultRouteParams)
|
val req = SendPaymentToNode(finalAmount, pr, 1, /* ignored since the invoice provides it */ CltvExpiryDelta(12), routeParams = nodeParams.routerConf.pathFindingExperimentConf.getRandomConf().getDefaultRouteParams)
|
||||||
assert(req.finalExpiry(nodeParams.currentBlockHeight) === (finalExpiryDelta + 1).toCltvExpiry(nodeParams.currentBlockHeight))
|
assert(req.finalExpiry(nodeParams.currentBlockHeight) === (finalExpiryDelta + 1).toCltvExpiry(nodeParams.currentBlockHeight))
|
||||||
sender.send(initiator, req)
|
sender.send(initiator, req)
|
||||||
|
@ -151,17 +167,17 @@ class PaymentInitiatorSpec extends TestKitBaseClass with FixtureAnyFunSuiteLike
|
||||||
|
|
||||||
test("forward multi-part payment") { f =>
|
test("forward multi-part payment") { f =>
|
||||||
import f._
|
import f._
|
||||||
val pr = PaymentRequest(Block.LivenetGenesisBlock.hash, Some(finalAmount), paymentHash, priv_c.privateKey, Left("Some invoice"), CltvExpiryDelta(18), features = PaymentRequestFeatures(VariableLengthOnion.mandatory, PaymentSecret.mandatory, BasicMultiPartPayment.optional))
|
val pr = PaymentRequest(Block.LivenetGenesisBlock.hash, Some(finalAmount), paymentHash, priv_c.privateKey, Left("Some invoice"), CltvExpiryDelta(18), features = PaymentRequestFeatures(featuresWithMpp))
|
||||||
val req = SendPaymentToNode(finalAmount + 100.msat, pr, 1, CltvExpiryDelta(42), routeParams = nodeParams.routerConf.pathFindingExperimentConf.getRandomConf().getDefaultRouteParams)
|
val req = SendPaymentToNode(finalAmount + 100.msat, pr, 1, CltvExpiryDelta(42), routeParams = nodeParams.routerConf.pathFindingExperimentConf.getRandomConf().getDefaultRouteParams)
|
||||||
sender.send(initiator, req)
|
sender.send(initiator, req)
|
||||||
val id = sender.expectMsgType[UUID]
|
val id = sender.expectMsgType[UUID]
|
||||||
multiPartPayFsm.expectMsg(SendPaymentConfig(id, id, None, paymentHash, finalAmount + 100.msat, c, Upstream.Local(id), Some(pr), storeInDb = true, publishEvent = true, recordPathFindingMetrics = true, Nil))
|
multiPartPayFsm.expectMsg(SendPaymentConfig(id, id, None, paymentHash, finalAmount + 100.msat, c, Upstream.Local(id), Some(pr), storeInDb = true, publishEvent = true, recordPathFindingMetrics = true, Nil))
|
||||||
multiPartPayFsm.expectMsg(SendMultiPartPayment(sender.ref, pr.paymentSecret.get, c, finalAmount + 100.msat, req.finalExpiry(nodeParams.currentBlockHeight), 1, routeParams = nodeParams.routerConf.pathFindingExperimentConf.getRandomConf().getDefaultRouteParams))
|
multiPartPayFsm.expectMsg(SendMultiPartPayment(sender.ref, pr.paymentSecret.get, c, finalAmount + 100.msat, req.finalExpiry(nodeParams.currentBlockHeight), 1, pr.paymentMetadata, routeParams = nodeParams.routerConf.pathFindingExperimentConf.getRandomConf().getDefaultRouteParams))
|
||||||
}
|
}
|
||||||
|
|
||||||
test("forward multi-part payment with pre-defined route") { f =>
|
test("forward multi-part payment with pre-defined route") { f =>
|
||||||
import f._
|
import f._
|
||||||
val pr = PaymentRequest(Block.LivenetGenesisBlock.hash, Some(finalAmount), paymentHash, priv_c.privateKey, Left("Some invoice"), CltvExpiryDelta(18), features = PaymentRequestFeatures(VariableLengthOnion.mandatory, PaymentSecret.mandatory, BasicMultiPartPayment.optional))
|
val pr = PaymentRequest(Block.LivenetGenesisBlock.hash, Some(finalAmount), paymentHash, priv_c.privateKey, Left("Some invoice"), CltvExpiryDelta(18), features = PaymentRequestFeatures(featuresWithMpp))
|
||||||
val route = PredefinedChannelRoute(c, Seq(channelUpdate_ab.shortChannelId, channelUpdate_bc.shortChannelId))
|
val route = PredefinedChannelRoute(c, Seq(channelUpdate_ab.shortChannelId, channelUpdate_bc.shortChannelId))
|
||||||
val req = SendPaymentToRoute(finalAmount / 2, finalAmount, pr, Channel.MIN_CLTV_EXPIRY_DELTA, route, None, None, None, 0 msat, CltvExpiryDelta(0), Nil)
|
val req = SendPaymentToRoute(finalAmount / 2, finalAmount, pr, Channel.MIN_CLTV_EXPIRY_DELTA, route, None, None, None, 0 msat, CltvExpiryDelta(0), Nil)
|
||||||
sender.send(initiator, req)
|
sender.send(initiator, req)
|
||||||
|
@ -177,9 +193,8 @@ class PaymentInitiatorSpec extends TestKitBaseClass with FixtureAnyFunSuiteLike
|
||||||
|
|
||||||
test("forward trampoline payment") { f =>
|
test("forward trampoline payment") { f =>
|
||||||
import f._
|
import f._
|
||||||
val features = PaymentRequestFeatures(VariableLengthOnion.mandatory, PaymentSecret.mandatory, BasicMultiPartPayment.optional, TrampolinePayment.optional)
|
|
||||||
val ignoredRoutingHints = List(List(ExtraHop(b, channelUpdate_bc.shortChannelId, feeBase = 10 msat, feeProportionalMillionths = 1, cltvExpiryDelta = CltvExpiryDelta(12))))
|
val ignoredRoutingHints = List(List(ExtraHop(b, channelUpdate_bc.shortChannelId, feeBase = 10 msat, feeProportionalMillionths = 1, cltvExpiryDelta = CltvExpiryDelta(12))))
|
||||||
val pr = PaymentRequest(Block.LivenetGenesisBlock.hash, Some(finalAmount), paymentHash, priv_c.privateKey, Left("Some phoenix invoice"), CltvExpiryDelta(9), features = features, extraHops = ignoredRoutingHints)
|
val pr = PaymentRequest(Block.LivenetGenesisBlock.hash, Some(finalAmount), paymentHash, priv_c.privateKey, Left("Some phoenix invoice"), CltvExpiryDelta(9), features = PaymentRequestFeatures(featuresWithTrampoline), extraHops = ignoredRoutingHints)
|
||||||
val trampolineFees = 21000 msat
|
val trampolineFees = 21000 msat
|
||||||
val req = SendTrampolinePayment(finalAmount, pr, b, Seq((trampolineFees, CltvExpiryDelta(12))), /* ignored since the invoice provides it */ CltvExpiryDelta(18), routeParams = nodeParams.routerConf.pathFindingExperimentConf.getRandomConf().getDefaultRouteParams)
|
val req = SendTrampolinePayment(finalAmount, pr, b, Seq((trampolineFees, CltvExpiryDelta(12))), /* ignored since the invoice provides it */ CltvExpiryDelta(18), routeParams = nodeParams.routerConf.pathFindingExperimentConf.getRandomConf().getDefaultRouteParams)
|
||||||
sender.send(initiator, req)
|
sender.send(initiator, req)
|
||||||
|
@ -250,8 +265,7 @@ class PaymentInitiatorSpec extends TestKitBaseClass with FixtureAnyFunSuiteLike
|
||||||
import f._
|
import f._
|
||||||
// This is disabled because it would let the trampoline node steal the whole payment (if malicious).
|
// This is disabled because it would let the trampoline node steal the whole payment (if malicious).
|
||||||
val routingHints = List(List(PaymentRequest.ExtraHop(b, channelUpdate_bc.shortChannelId, 10 msat, 100, CltvExpiryDelta(144))))
|
val routingHints = List(List(PaymentRequest.ExtraHop(b, channelUpdate_bc.shortChannelId, 10 msat, 100, CltvExpiryDelta(144))))
|
||||||
val features = PaymentRequestFeatures(VariableLengthOnion.mandatory, PaymentSecret.mandatory, BasicMultiPartPayment.optional)
|
val pr = PaymentRequest(Block.RegtestGenesisBlock.hash, None, paymentHash, priv_a.privateKey, Left("#abittooreckless"), CltvExpiryDelta(18), None, None, routingHints, features = PaymentRequestFeatures(featuresWithMpp))
|
||||||
val pr = PaymentRequest(Block.RegtestGenesisBlock.hash, None, paymentHash, priv_a.privateKey, Left("#abittooreckless"), CltvExpiryDelta(18), None, None, routingHints, features = features)
|
|
||||||
val trampolineFees = 21000 msat
|
val trampolineFees = 21000 msat
|
||||||
val req = SendTrampolinePayment(finalAmount, pr, b, Seq((trampolineFees, CltvExpiryDelta(12))), CltvExpiryDelta(9), routeParams = nodeParams.routerConf.pathFindingExperimentConf.getRandomConf().getDefaultRouteParams)
|
val req = SendTrampolinePayment(finalAmount, pr, b, Seq((trampolineFees, CltvExpiryDelta(12))), CltvExpiryDelta(9), routeParams = nodeParams.routerConf.pathFindingExperimentConf.getRandomConf().getDefaultRouteParams)
|
||||||
sender.send(initiator, req)
|
sender.send(initiator, req)
|
||||||
|
@ -266,8 +280,7 @@ class PaymentInitiatorSpec extends TestKitBaseClass with FixtureAnyFunSuiteLike
|
||||||
|
|
||||||
test("retry trampoline payment") { f =>
|
test("retry trampoline payment") { f =>
|
||||||
import f._
|
import f._
|
||||||
val features = PaymentRequestFeatures(VariableLengthOnion.mandatory, PaymentSecret.mandatory, BasicMultiPartPayment.optional, TrampolinePayment.optional)
|
val pr = PaymentRequest(Block.LivenetGenesisBlock.hash, Some(finalAmount), paymentHash, priv_c.privateKey, Left("Some phoenix invoice"), CltvExpiryDelta(18), features = PaymentRequestFeatures(featuresWithTrampoline))
|
||||||
val pr = PaymentRequest(Block.LivenetGenesisBlock.hash, Some(finalAmount), paymentHash, priv_c.privateKey, Left("Some phoenix invoice"), CltvExpiryDelta(18), features = features)
|
|
||||||
val trampolineAttempts = (21000 msat, CltvExpiryDelta(12)) :: (25000 msat, CltvExpiryDelta(24)) :: Nil
|
val trampolineAttempts = (21000 msat, CltvExpiryDelta(12)) :: (25000 msat, CltvExpiryDelta(24)) :: Nil
|
||||||
val req = SendTrampolinePayment(finalAmount, pr, b, trampolineAttempts, CltvExpiryDelta(9), routeParams = nodeParams.routerConf.pathFindingExperimentConf.getRandomConf().getDefaultRouteParams)
|
val req = SendTrampolinePayment(finalAmount, pr, b, trampolineAttempts, CltvExpiryDelta(9), routeParams = nodeParams.routerConf.pathFindingExperimentConf.getRandomConf().getDefaultRouteParams)
|
||||||
sender.send(initiator, req)
|
sender.send(initiator, req)
|
||||||
|
@ -296,8 +309,7 @@ class PaymentInitiatorSpec extends TestKitBaseClass with FixtureAnyFunSuiteLike
|
||||||
|
|
||||||
test("retry trampoline payment and fail") { f =>
|
test("retry trampoline payment and fail") { f =>
|
||||||
import f._
|
import f._
|
||||||
val features = PaymentRequestFeatures(VariableLengthOnion.mandatory, PaymentSecret.mandatory, BasicMultiPartPayment.optional, TrampolinePayment.optional)
|
val pr = PaymentRequest(Block.LivenetGenesisBlock.hash, Some(finalAmount), paymentHash, priv_c.privateKey, Left("Some phoenix invoice"), CltvExpiryDelta(18), features = PaymentRequestFeatures(featuresWithTrampoline))
|
||||||
val pr = PaymentRequest(Block.LivenetGenesisBlock.hash, Some(finalAmount), paymentHash, priv_c.privateKey, Left("Some phoenix invoice"), CltvExpiryDelta(18), features = features)
|
|
||||||
val trampolineAttempts = (21000 msat, CltvExpiryDelta(12)) :: (25000 msat, CltvExpiryDelta(24)) :: Nil
|
val trampolineAttempts = (21000 msat, CltvExpiryDelta(12)) :: (25000 msat, CltvExpiryDelta(24)) :: Nil
|
||||||
val req = SendTrampolinePayment(finalAmount, pr, b, trampolineAttempts, CltvExpiryDelta(9), routeParams = nodeParams.routerConf.pathFindingExperimentConf.getRandomConf().getDefaultRouteParams)
|
val req = SendTrampolinePayment(finalAmount, pr, b, trampolineAttempts, CltvExpiryDelta(9), routeParams = nodeParams.routerConf.pathFindingExperimentConf.getRandomConf().getDefaultRouteParams)
|
||||||
sender.send(initiator, req)
|
sender.send(initiator, req)
|
||||||
|
@ -326,8 +338,7 @@ class PaymentInitiatorSpec extends TestKitBaseClass with FixtureAnyFunSuiteLike
|
||||||
|
|
||||||
test("retry trampoline payment and fail (route not found)") { f =>
|
test("retry trampoline payment and fail (route not found)") { f =>
|
||||||
import f._
|
import f._
|
||||||
val features = PaymentRequestFeatures(VariableLengthOnion.mandatory, PaymentSecret.mandatory, BasicMultiPartPayment.optional, TrampolinePayment.optional)
|
val pr = PaymentRequest(Block.LivenetGenesisBlock.hash, Some(finalAmount), paymentHash, priv_c.privateKey, Left("Some phoenix invoice"), CltvExpiryDelta(18), features = PaymentRequestFeatures(featuresWithTrampoline))
|
||||||
val pr = PaymentRequest(Block.LivenetGenesisBlock.hash, Some(finalAmount), paymentHash, priv_c.privateKey, Left("Some phoenix invoice"), CltvExpiryDelta(18), features = features)
|
|
||||||
val trampolineAttempts = (21000 msat, CltvExpiryDelta(12)) :: (25000 msat, CltvExpiryDelta(24)) :: Nil
|
val trampolineAttempts = (21000 msat, CltvExpiryDelta(12)) :: (25000 msat, CltvExpiryDelta(24)) :: Nil
|
||||||
val req = SendTrampolinePayment(finalAmount, pr, b, trampolineAttempts, CltvExpiryDelta(9), routeParams = nodeParams.routerConf.pathFindingExperimentConf.getRandomConf().getDefaultRouteParams)
|
val req = SendTrampolinePayment(finalAmount, pr, b, trampolineAttempts, CltvExpiryDelta(9), routeParams = nodeParams.routerConf.pathFindingExperimentConf.getRandomConf().getDefaultRouteParams)
|
||||||
sender.send(initiator, req)
|
sender.send(initiator, req)
|
||||||
|
|
|
@ -102,7 +102,7 @@ class PaymentLifecycleSpec extends BaseRouterSpec {
|
||||||
|
|
||||||
// pre-computed route going from A to D
|
// pre-computed route going from A to D
|
||||||
val route = Route(defaultAmountMsat, ChannelHop(a, b, update_ab) :: ChannelHop(b, c, update_bc) :: ChannelHop(c, d, update_cd) :: Nil)
|
val route = Route(defaultAmountMsat, ChannelHop(a, b, update_ab) :: ChannelHop(b, c, update_bc) :: ChannelHop(c, d, update_cd) :: Nil)
|
||||||
val request = SendPaymentToRoute(sender.ref, Right(route), PaymentOnion.createSinglePartPayload(defaultAmountMsat, defaultExpiry, defaultInvoice.paymentSecret.get))
|
val request = SendPaymentToRoute(sender.ref, Right(route), PaymentOnion.createSinglePartPayload(defaultAmountMsat, defaultExpiry, defaultInvoice.paymentSecret.get, defaultInvoice.paymentMetadata))
|
||||||
sender.send(paymentFSM, request)
|
sender.send(paymentFSM, request)
|
||||||
routerForwarder.expectNoMessage(100 millis) // we don't need the router, we have the pre-computed route
|
routerForwarder.expectNoMessage(100 millis) // we don't need the router, we have the pre-computed route
|
||||||
val Transition(_, WAITING_FOR_REQUEST, WAITING_FOR_ROUTE) = monitor.expectMsgClass(classOf[Transition[_]])
|
val Transition(_, WAITING_FOR_REQUEST, WAITING_FOR_ROUTE) = monitor.expectMsgClass(classOf[Transition[_]])
|
||||||
|
@ -128,7 +128,7 @@ class PaymentLifecycleSpec extends BaseRouterSpec {
|
||||||
|
|
||||||
// pre-computed route going from A to D
|
// pre-computed route going from A to D
|
||||||
val route = PredefinedNodeRoute(Seq(a, b, c, d))
|
val route = PredefinedNodeRoute(Seq(a, b, c, d))
|
||||||
val request = SendPaymentToRoute(sender.ref, Left(route), PaymentOnion.createSinglePartPayload(defaultAmountMsat, defaultExpiry, defaultInvoice.paymentSecret.get))
|
val request = SendPaymentToRoute(sender.ref, Left(route), PaymentOnion.createSinglePartPayload(defaultAmountMsat, defaultExpiry, defaultInvoice.paymentSecret.get, defaultInvoice.paymentMetadata))
|
||||||
|
|
||||||
sender.send(paymentFSM, request)
|
sender.send(paymentFSM, request)
|
||||||
routerForwarder.expectMsg(FinalizeRoute(defaultAmountMsat, route, paymentContext = Some(cfg.paymentContext)))
|
routerForwarder.expectMsg(FinalizeRoute(defaultAmountMsat, route, paymentContext = Some(cfg.paymentContext)))
|
||||||
|
@ -152,7 +152,7 @@ class PaymentLifecycleSpec extends BaseRouterSpec {
|
||||||
val payFixture = createPaymentLifecycle(recordMetrics = false)
|
val payFixture = createPaymentLifecycle(recordMetrics = false)
|
||||||
import payFixture._
|
import payFixture._
|
||||||
|
|
||||||
val brokenRoute = SendPaymentToRoute(sender.ref, Left(PredefinedNodeRoute(Seq(randomKey().publicKey, randomKey().publicKey, randomKey().publicKey))), PaymentOnion.createSinglePartPayload(defaultAmountMsat, defaultExpiry, defaultInvoice.paymentSecret.get))
|
val brokenRoute = SendPaymentToRoute(sender.ref, Left(PredefinedNodeRoute(Seq(randomKey().publicKey, randomKey().publicKey, randomKey().publicKey))), PaymentOnion.createSinglePartPayload(defaultAmountMsat, defaultExpiry, defaultInvoice.paymentSecret.get, defaultInvoice.paymentMetadata))
|
||||||
sender.send(paymentFSM, brokenRoute)
|
sender.send(paymentFSM, brokenRoute)
|
||||||
routerForwarder.expectMsgType[FinalizeRoute]
|
routerForwarder.expectMsgType[FinalizeRoute]
|
||||||
routerForwarder.forward(routerFixture.router)
|
routerForwarder.forward(routerFixture.router)
|
||||||
|
@ -167,7 +167,7 @@ class PaymentLifecycleSpec extends BaseRouterSpec {
|
||||||
val payFixture = createPaymentLifecycle(recordMetrics = false)
|
val payFixture = createPaymentLifecycle(recordMetrics = false)
|
||||||
import payFixture._
|
import payFixture._
|
||||||
|
|
||||||
val brokenRoute = SendPaymentToRoute(sender.ref, Left(PredefinedChannelRoute(randomKey().publicKey, Seq(ShortChannelId(1), ShortChannelId(2)))), PaymentOnion.createSinglePartPayload(defaultAmountMsat, defaultExpiry, defaultInvoice.paymentSecret.get))
|
val brokenRoute = SendPaymentToRoute(sender.ref, Left(PredefinedChannelRoute(randomKey().publicKey, Seq(ShortChannelId(1), ShortChannelId(2)))), PaymentOnion.createSinglePartPayload(defaultAmountMsat, defaultExpiry, defaultInvoice.paymentSecret.get, defaultInvoice.paymentMetadata))
|
||||||
sender.send(paymentFSM, brokenRoute)
|
sender.send(paymentFSM, brokenRoute)
|
||||||
routerForwarder.expectMsgType[FinalizeRoute]
|
routerForwarder.expectMsgType[FinalizeRoute]
|
||||||
routerForwarder.forward(routerFixture.router)
|
routerForwarder.forward(routerFixture.router)
|
||||||
|
@ -186,7 +186,7 @@ class PaymentLifecycleSpec extends BaseRouterSpec {
|
||||||
val recipient = randomKey().publicKey
|
val recipient = randomKey().publicKey
|
||||||
val route = PredefinedNodeRoute(Seq(a, b, c, recipient))
|
val route = PredefinedNodeRoute(Seq(a, b, c, recipient))
|
||||||
val routingHint = Seq(Seq(ExtraHop(c, ShortChannelId(561), 1 msat, 100, CltvExpiryDelta(144))))
|
val routingHint = Seq(Seq(ExtraHop(c, ShortChannelId(561), 1 msat, 100, CltvExpiryDelta(144))))
|
||||||
val request = SendPaymentToRoute(sender.ref, Left(route), PaymentOnion.createSinglePartPayload(defaultAmountMsat, defaultExpiry, defaultInvoice.paymentSecret.get), routingHint)
|
val request = SendPaymentToRoute(sender.ref, Left(route), PaymentOnion.createSinglePartPayload(defaultAmountMsat, defaultExpiry, defaultInvoice.paymentSecret.get, defaultInvoice.paymentMetadata), routingHint)
|
||||||
|
|
||||||
sender.send(paymentFSM, request)
|
sender.send(paymentFSM, request)
|
||||||
routerForwarder.expectMsg(FinalizeRoute(defaultAmountMsat, route, routingHint, paymentContext = Some(cfg.paymentContext)))
|
routerForwarder.expectMsg(FinalizeRoute(defaultAmountMsat, route, routingHint, paymentContext = Some(cfg.paymentContext)))
|
||||||
|
@ -210,7 +210,7 @@ class PaymentLifecycleSpec extends BaseRouterSpec {
|
||||||
import payFixture._
|
import payFixture._
|
||||||
import cfg._
|
import cfg._
|
||||||
|
|
||||||
val request = SendPaymentToNode(sender.ref, f, PaymentOnion.createSinglePartPayload(defaultAmountMsat, defaultExpiry, defaultInvoice.paymentSecret.get), 5, routeParams = defaultRouteParams)
|
val request = SendPaymentToNode(sender.ref, f, PaymentOnion.createSinglePartPayload(defaultAmountMsat, defaultExpiry, defaultInvoice.paymentSecret.get, defaultInvoice.paymentMetadata), 5, routeParams = defaultRouteParams)
|
||||||
sender.send(paymentFSM, request)
|
sender.send(paymentFSM, request)
|
||||||
val Transition(_, WAITING_FOR_REQUEST, WAITING_FOR_ROUTE) = monitor.expectMsgClass(classOf[Transition[_]])
|
val Transition(_, WAITING_FOR_REQUEST, WAITING_FOR_ROUTE) = monitor.expectMsgClass(classOf[Transition[_]])
|
||||||
val routeRequest = routerForwarder.expectMsgType[RouteRequest]
|
val routeRequest = routerForwarder.expectMsgType[RouteRequest]
|
||||||
|
@ -241,7 +241,7 @@ class PaymentLifecycleSpec extends BaseRouterSpec {
|
||||||
"my-test-experiment",
|
"my-test-experiment",
|
||||||
experimentPercentage = 100
|
experimentPercentage = 100
|
||||||
).getDefaultRouteParams
|
).getDefaultRouteParams
|
||||||
val request = SendPaymentToNode(sender.ref, d, PaymentOnion.createSinglePartPayload(defaultAmountMsat, defaultExpiry, defaultInvoice.paymentSecret.get), 5, routeParams = routeParams)
|
val request = SendPaymentToNode(sender.ref, d, PaymentOnion.createSinglePartPayload(defaultAmountMsat, defaultExpiry, defaultInvoice.paymentSecret.get, defaultInvoice.paymentMetadata), 5, routeParams = routeParams)
|
||||||
sender.send(paymentFSM, request)
|
sender.send(paymentFSM, request)
|
||||||
val routeRequest = routerForwarder.expectMsgType[RouteRequest]
|
val routeRequest = routerForwarder.expectMsgType[RouteRequest]
|
||||||
val Transition(_, WAITING_FOR_REQUEST, WAITING_FOR_ROUTE) = monitor.expectMsgClass(classOf[Transition[_]])
|
val Transition(_, WAITING_FOR_REQUEST, WAITING_FOR_ROUTE) = monitor.expectMsgClass(classOf[Transition[_]])
|
||||||
|
@ -263,7 +263,7 @@ class PaymentLifecycleSpec extends BaseRouterSpec {
|
||||||
import payFixture._
|
import payFixture._
|
||||||
import cfg._
|
import cfg._
|
||||||
|
|
||||||
val request = SendPaymentToNode(sender.ref, d, PaymentOnion.createSinglePartPayload(defaultAmountMsat, defaultExpiry, defaultInvoice.paymentSecret.get), 2, routeParams = defaultRouteParams)
|
val request = SendPaymentToNode(sender.ref, d, PaymentOnion.createSinglePartPayload(defaultAmountMsat, defaultExpiry, defaultInvoice.paymentSecret.get, defaultInvoice.paymentMetadata), 2, routeParams = defaultRouteParams)
|
||||||
sender.send(paymentFSM, request)
|
sender.send(paymentFSM, request)
|
||||||
routerForwarder.expectMsg(defaultRouteRequest(a, d, cfg))
|
routerForwarder.expectMsg(defaultRouteRequest(a, d, cfg))
|
||||||
awaitCond(paymentFSM.stateName == WAITING_FOR_ROUTE && nodeParams.db.payments.getOutgoingPayment(id).exists(_.status === OutgoingPaymentStatus.Pending))
|
awaitCond(paymentFSM.stateName == WAITING_FOR_ROUTE && nodeParams.db.payments.getOutgoingPayment(id).exists(_.status === OutgoingPaymentStatus.Pending))
|
||||||
|
@ -306,7 +306,7 @@ class PaymentLifecycleSpec extends BaseRouterSpec {
|
||||||
import payFixture._
|
import payFixture._
|
||||||
import cfg._
|
import cfg._
|
||||||
|
|
||||||
val request = SendPaymentToNode(sender.ref, d, PaymentOnion.createSinglePartPayload(defaultAmountMsat, defaultExpiry, defaultInvoice.paymentSecret.get), 2, routeParams = defaultRouteParams)
|
val request = SendPaymentToNode(sender.ref, d, PaymentOnion.createSinglePartPayload(defaultAmountMsat, defaultExpiry, defaultInvoice.paymentSecret.get, defaultInvoice.paymentMetadata), 2, routeParams = defaultRouteParams)
|
||||||
sender.send(paymentFSM, request)
|
sender.send(paymentFSM, request)
|
||||||
awaitCond(paymentFSM.stateName == WAITING_FOR_ROUTE && nodeParams.db.payments.getOutgoingPayment(id).exists(_.status === OutgoingPaymentStatus.Pending))
|
awaitCond(paymentFSM.stateName == WAITING_FOR_ROUTE && nodeParams.db.payments.getOutgoingPayment(id).exists(_.status === OutgoingPaymentStatus.Pending))
|
||||||
routerForwarder.expectMsgType[RouteRequest]
|
routerForwarder.expectMsgType[RouteRequest]
|
||||||
|
@ -327,7 +327,7 @@ class PaymentLifecycleSpec extends BaseRouterSpec {
|
||||||
import payFixture._
|
import payFixture._
|
||||||
import cfg._
|
import cfg._
|
||||||
|
|
||||||
val request = SendPaymentToNode(sender.ref, d, PaymentOnion.createSinglePartPayload(defaultAmountMsat, defaultExpiry, defaultInvoice.paymentSecret.get), 2, routeParams = defaultRouteParams)
|
val request = SendPaymentToNode(sender.ref, d, PaymentOnion.createSinglePartPayload(defaultAmountMsat, defaultExpiry, defaultInvoice.paymentSecret.get, defaultInvoice.paymentMetadata), 2, routeParams = defaultRouteParams)
|
||||||
sender.send(paymentFSM, request)
|
sender.send(paymentFSM, request)
|
||||||
awaitCond(paymentFSM.stateName == WAITING_FOR_ROUTE && nodeParams.db.payments.getOutgoingPayment(id).exists(_.status === OutgoingPaymentStatus.Pending))
|
awaitCond(paymentFSM.stateName == WAITING_FOR_ROUTE && nodeParams.db.payments.getOutgoingPayment(id).exists(_.status === OutgoingPaymentStatus.Pending))
|
||||||
routerForwarder.expectMsgType[RouteRequest]
|
routerForwarder.expectMsgType[RouteRequest]
|
||||||
|
@ -347,7 +347,7 @@ class PaymentLifecycleSpec extends BaseRouterSpec {
|
||||||
import payFixture._
|
import payFixture._
|
||||||
import cfg._
|
import cfg._
|
||||||
|
|
||||||
val request = SendPaymentToNode(sender.ref, d, PaymentOnion.createSinglePartPayload(defaultAmountMsat, defaultExpiry, defaultInvoice.paymentSecret.get), 2, routeParams = defaultRouteParams)
|
val request = SendPaymentToNode(sender.ref, d, PaymentOnion.createSinglePartPayload(defaultAmountMsat, defaultExpiry, defaultInvoice.paymentSecret.get, defaultInvoice.paymentMetadata), 2, routeParams = defaultRouteParams)
|
||||||
sender.send(paymentFSM, request)
|
sender.send(paymentFSM, request)
|
||||||
awaitCond(paymentFSM.stateName == WAITING_FOR_ROUTE && nodeParams.db.payments.getOutgoingPayment(id).exists(_.status === OutgoingPaymentStatus.Pending))
|
awaitCond(paymentFSM.stateName == WAITING_FOR_ROUTE && nodeParams.db.payments.getOutgoingPayment(id).exists(_.status === OutgoingPaymentStatus.Pending))
|
||||||
|
|
||||||
|
@ -370,7 +370,7 @@ class PaymentLifecycleSpec extends BaseRouterSpec {
|
||||||
import payFixture._
|
import payFixture._
|
||||||
import cfg._
|
import cfg._
|
||||||
|
|
||||||
val request = SendPaymentToNode(sender.ref, d, PaymentOnion.createSinglePartPayload(defaultAmountMsat, defaultExpiry, defaultInvoice.paymentSecret.get), 2, routeParams = defaultRouteParams)
|
val request = SendPaymentToNode(sender.ref, d, PaymentOnion.createSinglePartPayload(defaultAmountMsat, defaultExpiry, defaultInvoice.paymentSecret.get, defaultInvoice.paymentMetadata), 2, routeParams = defaultRouteParams)
|
||||||
sender.send(paymentFSM, request)
|
sender.send(paymentFSM, request)
|
||||||
awaitCond(paymentFSM.stateName == WAITING_FOR_ROUTE && nodeParams.db.payments.getOutgoingPayment(id).exists(_.status === OutgoingPaymentStatus.Pending))
|
awaitCond(paymentFSM.stateName == WAITING_FOR_ROUTE && nodeParams.db.payments.getOutgoingPayment(id).exists(_.status === OutgoingPaymentStatus.Pending))
|
||||||
|
|
||||||
|
@ -393,7 +393,7 @@ class PaymentLifecycleSpec extends BaseRouterSpec {
|
||||||
import payFixture._
|
import payFixture._
|
||||||
import cfg._
|
import cfg._
|
||||||
|
|
||||||
val request = SendPaymentToNode(sender.ref, d, PaymentOnion.createSinglePartPayload(defaultAmountMsat, defaultExpiry, defaultInvoice.paymentSecret.get), 2, routeParams = defaultRouteParams)
|
val request = SendPaymentToNode(sender.ref, d, PaymentOnion.createSinglePartPayload(defaultAmountMsat, defaultExpiry, defaultInvoice.paymentSecret.get, defaultInvoice.paymentMetadata), 2, routeParams = defaultRouteParams)
|
||||||
sender.send(paymentFSM, request)
|
sender.send(paymentFSM, request)
|
||||||
awaitCond(paymentFSM.stateName == WAITING_FOR_ROUTE && nodeParams.db.payments.getOutgoingPayment(id).exists(_.status === OutgoingPaymentStatus.Pending))
|
awaitCond(paymentFSM.stateName == WAITING_FOR_ROUTE && nodeParams.db.payments.getOutgoingPayment(id).exists(_.status === OutgoingPaymentStatus.Pending))
|
||||||
|
|
||||||
|
@ -416,7 +416,7 @@ class PaymentLifecycleSpec extends BaseRouterSpec {
|
||||||
val payFixture = createPaymentLifecycle()
|
val payFixture = createPaymentLifecycle()
|
||||||
import payFixture._
|
import payFixture._
|
||||||
|
|
||||||
val request = SendPaymentToNode(sender.ref, d, PaymentOnion.createSinglePartPayload(defaultAmountMsat, defaultExpiry, defaultInvoice.paymentSecret.get), 2, routeParams = defaultRouteParams)
|
val request = SendPaymentToNode(sender.ref, d, PaymentOnion.createSinglePartPayload(defaultAmountMsat, defaultExpiry, defaultInvoice.paymentSecret.get, defaultInvoice.paymentMetadata), 2, routeParams = defaultRouteParams)
|
||||||
sender.send(paymentFSM, request)
|
sender.send(paymentFSM, request)
|
||||||
awaitCond(paymentFSM.stateName == WAITING_FOR_ROUTE)
|
awaitCond(paymentFSM.stateName == WAITING_FOR_ROUTE)
|
||||||
val WaitingForRoute(_, Nil, _) = paymentFSM.stateData
|
val WaitingForRoute(_, Nil, _) = paymentFSM.stateData
|
||||||
|
@ -446,7 +446,7 @@ class PaymentLifecycleSpec extends BaseRouterSpec {
|
||||||
import payFixture._
|
import payFixture._
|
||||||
import cfg._
|
import cfg._
|
||||||
|
|
||||||
val request = SendPaymentToNode(sender.ref, d, PaymentOnion.createSinglePartPayload(defaultAmountMsat, defaultExpiry, defaultInvoice.paymentSecret.get), 5, routeParams = defaultRouteParams)
|
val request = SendPaymentToNode(sender.ref, d, PaymentOnion.createSinglePartPayload(defaultAmountMsat, defaultExpiry, defaultInvoice.paymentSecret.get, defaultInvoice.paymentMetadata), 5, routeParams = defaultRouteParams)
|
||||||
sender.send(paymentFSM, request)
|
sender.send(paymentFSM, request)
|
||||||
awaitCond(paymentFSM.stateName == WAITING_FOR_ROUTE && nodeParams.db.payments.getOutgoingPayment(id).exists(_.status === OutgoingPaymentStatus.Pending))
|
awaitCond(paymentFSM.stateName == WAITING_FOR_ROUTE && nodeParams.db.payments.getOutgoingPayment(id).exists(_.status === OutgoingPaymentStatus.Pending))
|
||||||
|
|
||||||
|
@ -498,7 +498,7 @@ class PaymentLifecycleSpec extends BaseRouterSpec {
|
||||||
val payFixture = createPaymentLifecycle()
|
val payFixture = createPaymentLifecycle()
|
||||||
import payFixture._
|
import payFixture._
|
||||||
|
|
||||||
val request = SendPaymentToNode(sender.ref, d, PaymentOnion.createSinglePartPayload(defaultAmountMsat, defaultExpiry, defaultInvoice.paymentSecret.get), 1, routeParams = defaultRouteParams)
|
val request = SendPaymentToNode(sender.ref, d, PaymentOnion.createSinglePartPayload(defaultAmountMsat, defaultExpiry, defaultInvoice.paymentSecret.get, defaultInvoice.paymentMetadata), 1, routeParams = defaultRouteParams)
|
||||||
sender.send(paymentFSM, request)
|
sender.send(paymentFSM, request)
|
||||||
routerForwarder.expectMsg(defaultRouteRequest(nodeParams.nodeId, d, cfg))
|
routerForwarder.expectMsg(defaultRouteRequest(nodeParams.nodeId, d, cfg))
|
||||||
routerForwarder.forward(routerFixture.router)
|
routerForwarder.forward(routerFixture.router)
|
||||||
|
@ -528,7 +528,7 @@ class PaymentLifecycleSpec extends BaseRouterSpec {
|
||||||
ExtraHop(c, channelId_cd, update_cd.feeBaseMsat, update_cd.feeProportionalMillionths, update_cd.cltvExpiryDelta)
|
ExtraHop(c, channelId_cd, update_cd.feeBaseMsat, update_cd.feeProportionalMillionths, update_cd.cltvExpiryDelta)
|
||||||
))
|
))
|
||||||
|
|
||||||
val request = SendPaymentToNode(sender.ref, d, PaymentOnion.createSinglePartPayload(defaultAmountMsat, defaultExpiry, defaultInvoice.paymentSecret.get), 5, assistedRoutes = assistedRoutes, routeParams = defaultRouteParams)
|
val request = SendPaymentToNode(sender.ref, d, PaymentOnion.createSinglePartPayload(defaultAmountMsat, defaultExpiry, defaultInvoice.paymentSecret.get, defaultInvoice.paymentMetadata), 5, assistedRoutes = assistedRoutes, routeParams = defaultRouteParams)
|
||||||
sender.send(paymentFSM, request)
|
sender.send(paymentFSM, request)
|
||||||
awaitCond(paymentFSM.stateName == WAITING_FOR_ROUTE && nodeParams.db.payments.getOutgoingPayment(id).exists(_.status === OutgoingPaymentStatus.Pending))
|
awaitCond(paymentFSM.stateName == WAITING_FOR_ROUTE && nodeParams.db.payments.getOutgoingPayment(id).exists(_.status === OutgoingPaymentStatus.Pending))
|
||||||
|
|
||||||
|
@ -569,7 +569,7 @@ class PaymentLifecycleSpec extends BaseRouterSpec {
|
||||||
|
|
||||||
// we build an assisted route for channel cd
|
// we build an assisted route for channel cd
|
||||||
val assistedRoutes = Seq(Seq(ExtraHop(c, channelId_cd, update_cd.feeBaseMsat, update_cd.feeProportionalMillionths, update_cd.cltvExpiryDelta)))
|
val assistedRoutes = Seq(Seq(ExtraHop(c, channelId_cd, update_cd.feeBaseMsat, update_cd.feeProportionalMillionths, update_cd.cltvExpiryDelta)))
|
||||||
val request = SendPaymentToNode(sender.ref, d, PaymentOnion.createSinglePartPayload(defaultAmountMsat, defaultExpiry, defaultInvoice.paymentSecret.get), 1, assistedRoutes = assistedRoutes, routeParams = defaultRouteParams)
|
val request = SendPaymentToNode(sender.ref, d, PaymentOnion.createSinglePartPayload(defaultAmountMsat, defaultExpiry, defaultInvoice.paymentSecret.get, defaultInvoice.paymentMetadata), 1, assistedRoutes = assistedRoutes, routeParams = defaultRouteParams)
|
||||||
sender.send(paymentFSM, request)
|
sender.send(paymentFSM, request)
|
||||||
awaitCond(paymentFSM.stateName == WAITING_FOR_ROUTE && nodeParams.db.payments.getOutgoingPayment(id).exists(_.status === OutgoingPaymentStatus.Pending))
|
awaitCond(paymentFSM.stateName == WAITING_FOR_ROUTE && nodeParams.db.payments.getOutgoingPayment(id).exists(_.status === OutgoingPaymentStatus.Pending))
|
||||||
|
|
||||||
|
@ -594,7 +594,7 @@ class PaymentLifecycleSpec extends BaseRouterSpec {
|
||||||
import payFixture._
|
import payFixture._
|
||||||
import cfg._
|
import cfg._
|
||||||
|
|
||||||
val request = SendPaymentToNode(sender.ref, d, PaymentOnion.createSinglePartPayload(defaultAmountMsat, defaultExpiry, defaultInvoice.paymentSecret.get), 2, routeParams = defaultRouteParams)
|
val request = SendPaymentToNode(sender.ref, d, PaymentOnion.createSinglePartPayload(defaultAmountMsat, defaultExpiry, defaultInvoice.paymentSecret.get, defaultInvoice.paymentMetadata), 2, routeParams = defaultRouteParams)
|
||||||
sender.send(paymentFSM, request)
|
sender.send(paymentFSM, request)
|
||||||
awaitCond(paymentFSM.stateName == WAITING_FOR_ROUTE && nodeParams.db.payments.getOutgoingPayment(id).exists(_.status === OutgoingPaymentStatus.Pending))
|
awaitCond(paymentFSM.stateName == WAITING_FOR_ROUTE && nodeParams.db.payments.getOutgoingPayment(id).exists(_.status === OutgoingPaymentStatus.Pending))
|
||||||
|
|
||||||
|
@ -632,7 +632,7 @@ class PaymentLifecycleSpec extends BaseRouterSpec {
|
||||||
import payFixture._
|
import payFixture._
|
||||||
import cfg._
|
import cfg._
|
||||||
|
|
||||||
val request = SendPaymentToNode(sender.ref, d, PaymentOnion.createSinglePartPayload(defaultAmountMsat, defaultExpiry, defaultInvoice.paymentSecret.get), 5, routeParams = defaultRouteParams)
|
val request = SendPaymentToNode(sender.ref, d, PaymentOnion.createSinglePartPayload(defaultAmountMsat, defaultExpiry, defaultInvoice.paymentSecret.get, defaultInvoice.paymentMetadata), 5, routeParams = defaultRouteParams)
|
||||||
sender.send(paymentFSM, request)
|
sender.send(paymentFSM, request)
|
||||||
routerForwarder.expectMsgType[RouteRequest]
|
routerForwarder.expectMsgType[RouteRequest]
|
||||||
val Transition(_, WAITING_FOR_REQUEST, WAITING_FOR_ROUTE) = monitor.expectMsgClass(classOf[Transition[_]])
|
val Transition(_, WAITING_FOR_REQUEST, WAITING_FOR_ROUTE) = monitor.expectMsgClass(classOf[Transition[_]])
|
||||||
|
@ -686,7 +686,7 @@ class PaymentLifecycleSpec extends BaseRouterSpec {
|
||||||
import payFixture._
|
import payFixture._
|
||||||
|
|
||||||
// we send a payment to H
|
// we send a payment to H
|
||||||
val request = SendPaymentToNode(sender.ref, h, PaymentOnion.createSinglePartPayload(defaultAmountMsat, defaultExpiry, defaultInvoice.paymentSecret.get), 5, routeParams = defaultRouteParams)
|
val request = SendPaymentToNode(sender.ref, h, PaymentOnion.createSinglePartPayload(defaultAmountMsat, defaultExpiry, defaultInvoice.paymentSecret.get, defaultInvoice.paymentMetadata), 5, routeParams = defaultRouteParams)
|
||||||
sender.send(paymentFSM, request)
|
sender.send(paymentFSM, request)
|
||||||
routerForwarder.expectMsgType[RouteRequest]
|
routerForwarder.expectMsgType[RouteRequest]
|
||||||
|
|
||||||
|
@ -770,7 +770,7 @@ class PaymentLifecycleSpec extends BaseRouterSpec {
|
||||||
import payFixture._
|
import payFixture._
|
||||||
import cfg._
|
import cfg._
|
||||||
|
|
||||||
val request = SendPaymentToNode(sender.ref, d, PaymentOnion.createSinglePartPayload(defaultAmountMsat, defaultExpiry, defaultInvoice.paymentSecret.get), 3, routeParams = defaultRouteParams)
|
val request = SendPaymentToNode(sender.ref, d, PaymentOnion.createSinglePartPayload(defaultAmountMsat, defaultExpiry, defaultInvoice.paymentSecret.get, defaultInvoice.paymentMetadata), 3, routeParams = defaultRouteParams)
|
||||||
sender.send(paymentFSM, request)
|
sender.send(paymentFSM, request)
|
||||||
routerForwarder.expectMsgType[RouteRequest]
|
routerForwarder.expectMsgType[RouteRequest]
|
||||||
val Transition(_, WAITING_FOR_REQUEST, WAITING_FOR_ROUTE) = monitor.expectMsgClass(classOf[Transition[_]])
|
val Transition(_, WAITING_FOR_REQUEST, WAITING_FOR_ROUTE) = monitor.expectMsgClass(classOf[Transition[_]])
|
||||||
|
@ -791,7 +791,7 @@ class PaymentLifecycleSpec extends BaseRouterSpec {
|
||||||
|
|
||||||
// pre-computed route going from A to D
|
// pre-computed route going from A to D
|
||||||
val route = Route(defaultAmountMsat, ChannelHop(a, b, update_ab) :: ChannelHop(b, c, update_bc) :: ChannelHop(c, d, update_cd) :: Nil)
|
val route = Route(defaultAmountMsat, ChannelHop(a, b, update_ab) :: ChannelHop(b, c, update_bc) :: ChannelHop(c, d, update_cd) :: Nil)
|
||||||
val request = SendPaymentToRoute(sender.ref, Right(route), PaymentOnion.createSinglePartPayload(defaultAmountMsat, defaultExpiry, defaultInvoice.paymentSecret.get))
|
val request = SendPaymentToRoute(sender.ref, Right(route), PaymentOnion.createSinglePartPayload(defaultAmountMsat, defaultExpiry, defaultInvoice.paymentSecret.get, defaultInvoice.paymentMetadata))
|
||||||
sender.send(paymentFSM, request)
|
sender.send(paymentFSM, request)
|
||||||
routerForwarder.expectNoMessage(100 millis) // we don't need the router, we have the pre-computed route
|
routerForwarder.expectNoMessage(100 millis) // we don't need the router, we have the pre-computed route
|
||||||
val Transition(_, WAITING_FOR_REQUEST, WAITING_FOR_ROUTE) = monitor.expectMsgClass(classOf[Transition[_]])
|
val Transition(_, WAITING_FOR_REQUEST, WAITING_FOR_ROUTE) = monitor.expectMsgClass(classOf[Transition[_]])
|
||||||
|
|
|
@ -20,6 +20,7 @@ import akka.actor.ActorRef
|
||||||
import fr.acinq.bitcoin.Crypto.PublicKey
|
import fr.acinq.bitcoin.Crypto.PublicKey
|
||||||
import fr.acinq.bitcoin.DeterministicWallet.ExtendedPrivateKey
|
import fr.acinq.bitcoin.DeterministicWallet.ExtendedPrivateKey
|
||||||
import fr.acinq.bitcoin.{Block, ByteVector32, Crypto, DeterministicWallet, OutPoint, Satoshi, SatoshiLong, TxOut}
|
import fr.acinq.bitcoin.{Block, ByteVector32, Crypto, DeterministicWallet, OutPoint, Satoshi, SatoshiLong, TxOut}
|
||||||
|
import fr.acinq.eclair.FeatureSupport.{Mandatory, Optional}
|
||||||
import fr.acinq.eclair.Features._
|
import fr.acinq.eclair.Features._
|
||||||
import fr.acinq.eclair.channel._
|
import fr.acinq.eclair.channel._
|
||||||
import fr.acinq.eclair.crypto.Sphinx
|
import fr.acinq.eclair.crypto.Sphinx
|
||||||
|
@ -31,7 +32,7 @@ import fr.acinq.eclair.transactions.Transactions.InputInfo
|
||||||
import fr.acinq.eclair.wire.protocol.OnionPaymentPayloadTlv.{AmountToForward, OutgoingCltv, PaymentData}
|
import fr.acinq.eclair.wire.protocol.OnionPaymentPayloadTlv.{AmountToForward, OutgoingCltv, PaymentData}
|
||||||
import fr.acinq.eclair.wire.protocol.PaymentOnion.{ChannelRelayTlvPayload, FinalTlvPayload}
|
import fr.acinq.eclair.wire.protocol.PaymentOnion.{ChannelRelayTlvPayload, FinalTlvPayload}
|
||||||
import fr.acinq.eclair.wire.protocol._
|
import fr.acinq.eclair.wire.protocol._
|
||||||
import fr.acinq.eclair.{CltvExpiry, CltvExpiryDelta, MilliSatoshi, MilliSatoshiLong, ShortChannelId, TestConstants, TimestampSecondLong, nodeFee, randomBytes32, randomKey}
|
import fr.acinq.eclair.{CltvExpiry, CltvExpiryDelta, Feature, FeatureSupport, InvoiceFeature, MilliSatoshi, MilliSatoshiLong, ShortChannelId, TestConstants, TimestampSecondLong, nodeFee, randomBytes32, randomKey}
|
||||||
import org.scalatest.BeforeAndAfterAll
|
import org.scalatest.BeforeAndAfterAll
|
||||||
import org.scalatest.funsuite.AnyFunSuite
|
import org.scalatest.funsuite.AnyFunSuite
|
||||||
import scodec.Attempt
|
import scodec.Attempt
|
||||||
|
@ -115,7 +116,7 @@ class PaymentPacketSpec extends AnyFunSuite with BeforeAndAfterAll {
|
||||||
}
|
}
|
||||||
|
|
||||||
test("build a command including the onion") {
|
test("build a command including the onion") {
|
||||||
val (add, _) = buildCommand(ActorRef.noSender, Upstream.Local(UUID.randomUUID), paymentHash, hops, PaymentOnion.createSinglePartPayload(finalAmount, finalExpiry, paymentSecret))
|
val (add, _) = buildCommand(ActorRef.noSender, Upstream.Local(UUID.randomUUID), paymentHash, hops, PaymentOnion.createSinglePartPayload(finalAmount, finalExpiry, paymentSecret, None))
|
||||||
assert(add.amount > finalAmount)
|
assert(add.amount > finalAmount)
|
||||||
assert(add.cltvExpiry === finalExpiry + channelUpdate_de.cltvExpiryDelta + channelUpdate_cd.cltvExpiryDelta + channelUpdate_bc.cltvExpiryDelta)
|
assert(add.cltvExpiry === finalExpiry + channelUpdate_de.cltvExpiryDelta + channelUpdate_cd.cltvExpiryDelta + channelUpdate_bc.cltvExpiryDelta)
|
||||||
assert(add.paymentHash === paymentHash)
|
assert(add.paymentHash === paymentHash)
|
||||||
|
@ -126,7 +127,7 @@ class PaymentPacketSpec extends AnyFunSuite with BeforeAndAfterAll {
|
||||||
}
|
}
|
||||||
|
|
||||||
test("build a command with no hops") {
|
test("build a command with no hops") {
|
||||||
val (add, _) = buildCommand(ActorRef.noSender, Upstream.Local(UUID.randomUUID()), paymentHash, hops.take(1), PaymentOnion.createSinglePartPayload(finalAmount, finalExpiry, paymentSecret))
|
val (add, _) = buildCommand(ActorRef.noSender, Upstream.Local(UUID.randomUUID()), paymentHash, hops.take(1), PaymentOnion.createSinglePartPayload(finalAmount, finalExpiry, paymentSecret, Some(paymentMetadata)))
|
||||||
assert(add.amount === finalAmount)
|
assert(add.amount === finalAmount)
|
||||||
assert(add.cltvExpiry === finalExpiry)
|
assert(add.cltvExpiry === finalExpiry)
|
||||||
assert(add.paymentHash === paymentHash)
|
assert(add.paymentHash === paymentHash)
|
||||||
|
@ -140,6 +141,7 @@ class PaymentPacketSpec extends AnyFunSuite with BeforeAndAfterAll {
|
||||||
assert(payload_b.totalAmount === finalAmount)
|
assert(payload_b.totalAmount === finalAmount)
|
||||||
assert(payload_b.expiry === finalExpiry)
|
assert(payload_b.expiry === finalExpiry)
|
||||||
assert(payload_b.paymentSecret === paymentSecret)
|
assert(payload_b.paymentSecret === paymentSecret)
|
||||||
|
assert(payload_b.paymentMetadata === Some(paymentMetadata))
|
||||||
}
|
}
|
||||||
|
|
||||||
test("build a trampoline payment") {
|
test("build a trampoline payment") {
|
||||||
|
@ -148,7 +150,7 @@ class PaymentPacketSpec extends AnyFunSuite with BeforeAndAfterAll {
|
||||||
// / \ / \
|
// / \ / \
|
||||||
// a -> b -> c d e
|
// a -> b -> c d e
|
||||||
|
|
||||||
val (amount_ac, expiry_ac, trampolineOnion) = buildTrampolinePacket(paymentHash, trampolineHops, PaymentOnion.createMultiPartPayload(finalAmount, finalAmount * 3, finalExpiry, paymentSecret))
|
val (amount_ac, expiry_ac, trampolineOnion) = buildTrampolinePacket(paymentHash, trampolineHops, PaymentOnion.createMultiPartPayload(finalAmount, finalAmount * 3, finalExpiry, paymentSecret, Some(hex"010203")))
|
||||||
assert(amount_ac === amount_bc)
|
assert(amount_ac === amount_bc)
|
||||||
assert(expiry_ac === expiry_bc)
|
assert(expiry_ac === expiry_bc)
|
||||||
|
|
||||||
|
@ -173,6 +175,7 @@ class PaymentPacketSpec extends AnyFunSuite with BeforeAndAfterAll {
|
||||||
assert(inner_c.invoiceRoutingInfo === None)
|
assert(inner_c.invoiceRoutingInfo === None)
|
||||||
assert(inner_c.invoiceFeatures === None)
|
assert(inner_c.invoiceFeatures === None)
|
||||||
assert(inner_c.paymentSecret === None)
|
assert(inner_c.paymentSecret === None)
|
||||||
|
assert(inner_c.paymentMetadata === None)
|
||||||
|
|
||||||
// c forwards the trampoline payment to d.
|
// c forwards the trampoline payment to d.
|
||||||
val (amount_d, expiry_d, onion_d) = buildPaymentPacket(paymentHash, ChannelHop(c, d, channelUpdate_cd) :: Nil, PaymentOnion.createTrampolinePayload(amount_cd, amount_cd, expiry_cd, randomBytes32(), packet_d))
|
val (amount_d, expiry_d, onion_d) = buildPaymentPacket(paymentHash, ChannelHop(c, d, channelUpdate_cd) :: Nil, PaymentOnion.createTrampolinePayload(amount_cd, amount_cd, expiry_cd, randomBytes32(), packet_d))
|
||||||
|
@ -190,6 +193,7 @@ class PaymentPacketSpec extends AnyFunSuite with BeforeAndAfterAll {
|
||||||
assert(inner_d.invoiceRoutingInfo === None)
|
assert(inner_d.invoiceRoutingInfo === None)
|
||||||
assert(inner_d.invoiceFeatures === None)
|
assert(inner_d.invoiceFeatures === None)
|
||||||
assert(inner_d.paymentSecret === None)
|
assert(inner_d.paymentSecret === None)
|
||||||
|
assert(inner_d.paymentMetadata === None)
|
||||||
|
|
||||||
// d forwards the trampoline payment to e.
|
// d forwards the trampoline payment to e.
|
||||||
val (amount_e, expiry_e, onion_e) = buildPaymentPacket(paymentHash, ChannelHop(d, e, channelUpdate_de) :: Nil, PaymentOnion.createTrampolinePayload(amount_de, amount_de, expiry_de, randomBytes32(), packet_e))
|
val (amount_e, expiry_e, onion_e) = buildPaymentPacket(paymentHash, ChannelHop(d, e, channelUpdate_de) :: Nil, PaymentOnion.createTrampolinePayload(amount_de, amount_de, expiry_de, randomBytes32(), packet_e))
|
||||||
|
@ -198,7 +202,7 @@ class PaymentPacketSpec extends AnyFunSuite with BeforeAndAfterAll {
|
||||||
val add_e = UpdateAddHtlc(randomBytes32(), 4, amount_e, paymentHash, expiry_e, onion_e.packet)
|
val add_e = UpdateAddHtlc(randomBytes32(), 4, amount_e, paymentHash, expiry_e, onion_e.packet)
|
||||||
val Right(FinalPacket(add_e2, payload_e)) = decrypt(add_e, priv_e.privateKey)
|
val Right(FinalPacket(add_e2, payload_e)) = decrypt(add_e, priv_e.privateKey)
|
||||||
assert(add_e2 === add_e)
|
assert(add_e2 === add_e)
|
||||||
assert(payload_e === FinalTlvPayload(TlvStream(AmountToForward(finalAmount), OutgoingCltv(finalExpiry), PaymentData(paymentSecret, finalAmount * 3))))
|
assert(payload_e === FinalTlvPayload(TlvStream(AmountToForward(finalAmount), OutgoingCltv(finalExpiry), PaymentData(paymentSecret, finalAmount * 3), OnionPaymentPayloadTlv.PaymentMetadata(hex"010203"))))
|
||||||
}
|
}
|
||||||
|
|
||||||
test("build a trampoline payment with non-trampoline recipient") {
|
test("build a trampoline payment with non-trampoline recipient") {
|
||||||
|
@ -208,9 +212,9 @@ class PaymentPacketSpec extends AnyFunSuite with BeforeAndAfterAll {
|
||||||
// a -> b -> c d -> e
|
// a -> b -> c d -> e
|
||||||
|
|
||||||
val routingHints = List(List(PaymentRequest.ExtraHop(randomKey().publicKey, ShortChannelId(42), 10 msat, 100, CltvExpiryDelta(144))))
|
val routingHints = List(List(PaymentRequest.ExtraHop(randomKey().publicKey, ShortChannelId(42), 10 msat, 100, CltvExpiryDelta(144))))
|
||||||
val invoiceFeatures = PaymentRequestFeatures(VariableLengthOnion.mandatory, PaymentSecret.mandatory, BasicMultiPartPayment.optional)
|
val invoiceFeatures = PaymentRequestFeatures(Map[Feature with InvoiceFeature, FeatureSupport](VariableLengthOnion -> Mandatory, PaymentSecret -> Mandatory, BasicMultiPartPayment -> Optional))
|
||||||
val invoice = PaymentRequest(Block.RegtestGenesisBlock.hash, Some(finalAmount), paymentHash, priv_a.privateKey, Left("#reckless"), CltvExpiryDelta(18), None, None, routingHints, features = invoiceFeatures)
|
val invoice = PaymentRequest(Block.RegtestGenesisBlock.hash, Some(finalAmount), paymentHash, priv_a.privateKey, Left("#reckless"), CltvExpiryDelta(18), None, None, routingHints, features = invoiceFeatures, paymentMetadata = Some(hex"010203"))
|
||||||
val (amount_ac, expiry_ac, trampolineOnion) = buildTrampolineToLegacyPacket(invoice, trampolineHops, PaymentOnion.createSinglePartPayload(finalAmount, finalExpiry, invoice.paymentSecret.get))
|
val (amount_ac, expiry_ac, trampolineOnion) = buildTrampolineToLegacyPacket(invoice, trampolineHops, PaymentOnion.createSinglePartPayload(finalAmount, finalExpiry, invoice.paymentSecret.get, None))
|
||||||
assert(amount_ac === amount_bc)
|
assert(amount_ac === amount_bc)
|
||||||
assert(expiry_ac === expiry_bc)
|
assert(expiry_ac === expiry_bc)
|
||||||
|
|
||||||
|
@ -249,6 +253,7 @@ class PaymentPacketSpec extends AnyFunSuite with BeforeAndAfterAll {
|
||||||
assert(inner_d.outgoingNodeId === e)
|
assert(inner_d.outgoingNodeId === e)
|
||||||
assert(inner_d.totalAmount === finalAmount)
|
assert(inner_d.totalAmount === finalAmount)
|
||||||
assert(inner_d.paymentSecret === invoice.paymentSecret)
|
assert(inner_d.paymentSecret === invoice.paymentSecret)
|
||||||
|
assert(inner_d.paymentMetadata === Some(hex"010203"))
|
||||||
assert(inner_d.invoiceFeatures === Some(hex"024100")) // var_onion_optin, payment_secret, basic_mpp
|
assert(inner_d.invoiceFeatures === Some(hex"024100")) // var_onion_optin, payment_secret, basic_mpp
|
||||||
assert(inner_d.invoiceRoutingInfo === Some(routingHints))
|
assert(inner_d.invoiceRoutingInfo === Some(routingHints))
|
||||||
}
|
}
|
||||||
|
@ -257,19 +262,19 @@ class PaymentPacketSpec extends AnyFunSuite with BeforeAndAfterAll {
|
||||||
val routingHintOverflow = List(List.fill(7)(PaymentRequest.ExtraHop(randomKey().publicKey, ShortChannelId(1), 10 msat, 100, CltvExpiryDelta(12))))
|
val routingHintOverflow = List(List.fill(7)(PaymentRequest.ExtraHop(randomKey().publicKey, ShortChannelId(1), 10 msat, 100, CltvExpiryDelta(12))))
|
||||||
val invoice = PaymentRequest(Block.RegtestGenesisBlock.hash, Some(finalAmount), paymentHash, priv_a.privateKey, Left("#reckless"), CltvExpiryDelta(18), None, None, routingHintOverflow)
|
val invoice = PaymentRequest(Block.RegtestGenesisBlock.hash, Some(finalAmount), paymentHash, priv_a.privateKey, Left("#reckless"), CltvExpiryDelta(18), None, None, routingHintOverflow)
|
||||||
assertThrows[IllegalArgumentException](
|
assertThrows[IllegalArgumentException](
|
||||||
buildTrampolineToLegacyPacket(invoice, trampolineHops, PaymentOnion.createSinglePartPayload(finalAmount, finalExpiry, invoice.paymentSecret.get))
|
buildTrampolineToLegacyPacket(invoice, trampolineHops, PaymentOnion.createSinglePartPayload(finalAmount, finalExpiry, invoice.paymentSecret.get, invoice.paymentMetadata))
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
test("fail to decrypt when the onion is invalid") {
|
test("fail to decrypt when the onion is invalid") {
|
||||||
val (firstAmount, firstExpiry, onion) = buildPaymentPacket(paymentHash, hops, PaymentOnion.createSinglePartPayload(finalAmount, finalExpiry, paymentSecret))
|
val (firstAmount, firstExpiry, onion) = buildPaymentPacket(paymentHash, hops, PaymentOnion.createSinglePartPayload(finalAmount, finalExpiry, paymentSecret, None))
|
||||||
val add = UpdateAddHtlc(randomBytes32(), 1, firstAmount, paymentHash, firstExpiry, onion.packet.copy(payload = onion.packet.payload.reverse))
|
val add = UpdateAddHtlc(randomBytes32(), 1, firstAmount, paymentHash, firstExpiry, onion.packet.copy(payload = onion.packet.payload.reverse))
|
||||||
val Left(failure) = decrypt(add, priv_b.privateKey)
|
val Left(failure) = decrypt(add, priv_b.privateKey)
|
||||||
assert(failure.isInstanceOf[InvalidOnionHmac])
|
assert(failure.isInstanceOf[InvalidOnionHmac])
|
||||||
}
|
}
|
||||||
|
|
||||||
test("fail to decrypt when the trampoline onion is invalid") {
|
test("fail to decrypt when the trampoline onion is invalid") {
|
||||||
val (amount_ac, expiry_ac, trampolineOnion) = buildTrampolinePacket(paymentHash, trampolineHops, PaymentOnion.createMultiPartPayload(finalAmount, finalAmount * 2, finalExpiry, paymentSecret))
|
val (amount_ac, expiry_ac, trampolineOnion) = buildTrampolinePacket(paymentHash, trampolineHops, PaymentOnion.createMultiPartPayload(finalAmount, finalAmount * 2, finalExpiry, paymentSecret, None))
|
||||||
val (firstAmount, firstExpiry, onion) = buildPaymentPacket(paymentHash, trampolineChannelHops, PaymentOnion.createTrampolinePayload(amount_ac, amount_ac, expiry_ac, randomBytes32(), trampolineOnion.packet.copy(payload = trampolineOnion.packet.payload.reverse)))
|
val (firstAmount, firstExpiry, onion) = buildPaymentPacket(paymentHash, trampolineChannelHops, PaymentOnion.createTrampolinePayload(amount_ac, amount_ac, expiry_ac, randomBytes32(), trampolineOnion.packet.copy(payload = trampolineOnion.packet.payload.reverse)))
|
||||||
val add_b = UpdateAddHtlc(randomBytes32(), 1, firstAmount, paymentHash, firstExpiry, onion.packet)
|
val add_b = UpdateAddHtlc(randomBytes32(), 1, firstAmount, paymentHash, firstExpiry, onion.packet)
|
||||||
val Right(ChannelRelayPacket(_, _, packet_c)) = decrypt(add_b, priv_b.privateKey)
|
val Right(ChannelRelayPacket(_, _, packet_c)) = decrypt(add_b, priv_b.privateKey)
|
||||||
|
@ -279,28 +284,28 @@ class PaymentPacketSpec extends AnyFunSuite with BeforeAndAfterAll {
|
||||||
}
|
}
|
||||||
|
|
||||||
test("fail to decrypt when payment hash doesn't match associated data") {
|
test("fail to decrypt when payment hash doesn't match associated data") {
|
||||||
val (firstAmount, firstExpiry, onion) = buildPaymentPacket(paymentHash.reverse, hops, PaymentOnion.createSinglePartPayload(finalAmount, finalExpiry, paymentSecret))
|
val (firstAmount, firstExpiry, onion) = buildPaymentPacket(paymentHash.reverse, hops, PaymentOnion.createSinglePartPayload(finalAmount, finalExpiry, paymentSecret, None))
|
||||||
val add = UpdateAddHtlc(randomBytes32(), 1, firstAmount, paymentHash, firstExpiry, onion.packet)
|
val add = UpdateAddHtlc(randomBytes32(), 1, firstAmount, paymentHash, firstExpiry, onion.packet)
|
||||||
val Left(failure) = decrypt(add, priv_b.privateKey)
|
val Left(failure) = decrypt(add, priv_b.privateKey)
|
||||||
assert(failure.isInstanceOf[InvalidOnionHmac])
|
assert(failure.isInstanceOf[InvalidOnionHmac])
|
||||||
}
|
}
|
||||||
|
|
||||||
test("fail to decrypt at the final node when amount has been modified by next-to-last node") {
|
test("fail to decrypt at the final node when amount has been modified by next-to-last node") {
|
||||||
val (firstAmount, firstExpiry, onion) = buildPaymentPacket(paymentHash, hops.take(1), PaymentOnion.createSinglePartPayload(finalAmount, finalExpiry, paymentSecret))
|
val (firstAmount, firstExpiry, onion) = buildPaymentPacket(paymentHash, hops.take(1), PaymentOnion.createSinglePartPayload(finalAmount, finalExpiry, paymentSecret, None))
|
||||||
val add = UpdateAddHtlc(randomBytes32(), 1, firstAmount - 100.msat, paymentHash, firstExpiry, onion.packet)
|
val add = UpdateAddHtlc(randomBytes32(), 1, firstAmount - 100.msat, paymentHash, firstExpiry, onion.packet)
|
||||||
val Left(failure) = decrypt(add, priv_b.privateKey)
|
val Left(failure) = decrypt(add, priv_b.privateKey)
|
||||||
assert(failure === FinalIncorrectHtlcAmount(firstAmount - 100.msat))
|
assert(failure === FinalIncorrectHtlcAmount(firstAmount - 100.msat))
|
||||||
}
|
}
|
||||||
|
|
||||||
test("fail to decrypt at the final node when expiry has been modified by next-to-last node") {
|
test("fail to decrypt at the final node when expiry has been modified by next-to-last node") {
|
||||||
val (firstAmount, firstExpiry, onion) = buildPaymentPacket(paymentHash, hops.take(1), PaymentOnion.createSinglePartPayload(finalAmount, finalExpiry, paymentSecret))
|
val (firstAmount, firstExpiry, onion) = buildPaymentPacket(paymentHash, hops.take(1), PaymentOnion.createSinglePartPayload(finalAmount, finalExpiry, paymentSecret, None))
|
||||||
val add = UpdateAddHtlc(randomBytes32(), 1, firstAmount, paymentHash, firstExpiry - CltvExpiryDelta(12), onion.packet)
|
val add = UpdateAddHtlc(randomBytes32(), 1, firstAmount, paymentHash, firstExpiry - CltvExpiryDelta(12), onion.packet)
|
||||||
val Left(failure) = decrypt(add, priv_b.privateKey)
|
val Left(failure) = decrypt(add, priv_b.privateKey)
|
||||||
assert(failure === FinalIncorrectCltvExpiry(firstExpiry - CltvExpiryDelta(12)))
|
assert(failure === FinalIncorrectCltvExpiry(firstExpiry - CltvExpiryDelta(12)))
|
||||||
}
|
}
|
||||||
|
|
||||||
test("fail to decrypt at the final trampoline node when amount has been modified by next-to-last trampoline") {
|
test("fail to decrypt at the final trampoline node when amount has been modified by next-to-last trampoline") {
|
||||||
val (amount_ac, expiry_ac, trampolineOnion) = buildTrampolinePacket(paymentHash, trampolineHops, PaymentOnion.createMultiPartPayload(finalAmount, finalAmount, finalExpiry, paymentSecret))
|
val (amount_ac, expiry_ac, trampolineOnion) = buildTrampolinePacket(paymentHash, trampolineHops, PaymentOnion.createMultiPartPayload(finalAmount, finalAmount, finalExpiry, paymentSecret, None))
|
||||||
val (firstAmount, firstExpiry, onion) = buildPaymentPacket(paymentHash, trampolineChannelHops, PaymentOnion.createTrampolinePayload(amount_ac, amount_ac, expiry_ac, randomBytes32(), trampolineOnion.packet))
|
val (firstAmount, firstExpiry, onion) = buildPaymentPacket(paymentHash, trampolineChannelHops, PaymentOnion.createTrampolinePayload(amount_ac, amount_ac, expiry_ac, randomBytes32(), trampolineOnion.packet))
|
||||||
val Right(ChannelRelayPacket(_, _, packet_c)) = decrypt(UpdateAddHtlc(randomBytes32(), 1, firstAmount, paymentHash, firstExpiry, onion.packet), priv_b.privateKey)
|
val Right(ChannelRelayPacket(_, _, packet_c)) = decrypt(UpdateAddHtlc(randomBytes32(), 1, firstAmount, paymentHash, firstExpiry, onion.packet), priv_b.privateKey)
|
||||||
val Right(NodeRelayPacket(_, _, _, packet_d)) = decrypt(UpdateAddHtlc(randomBytes32(), 2, amount_bc, paymentHash, expiry_bc, packet_c), priv_c.privateKey)
|
val Right(NodeRelayPacket(_, _, _, packet_d)) = decrypt(UpdateAddHtlc(randomBytes32(), 2, amount_bc, paymentHash, expiry_bc, packet_c), priv_c.privateKey)
|
||||||
|
@ -315,7 +320,7 @@ class PaymentPacketSpec extends AnyFunSuite with BeforeAndAfterAll {
|
||||||
}
|
}
|
||||||
|
|
||||||
test("fail to decrypt at the final trampoline node when expiry has been modified by next-to-last trampoline") {
|
test("fail to decrypt at the final trampoline node when expiry has been modified by next-to-last trampoline") {
|
||||||
val (amount_ac, expiry_ac, trampolineOnion) = buildTrampolinePacket(paymentHash, trampolineHops, PaymentOnion.createMultiPartPayload(finalAmount, finalAmount, finalExpiry, paymentSecret))
|
val (amount_ac, expiry_ac, trampolineOnion) = buildTrampolinePacket(paymentHash, trampolineHops, PaymentOnion.createMultiPartPayload(finalAmount, finalAmount, finalExpiry, paymentSecret, None))
|
||||||
val (firstAmount, firstExpiry, onion) = buildPaymentPacket(paymentHash, trampolineChannelHops, PaymentOnion.createTrampolinePayload(amount_ac, amount_ac, expiry_ac, randomBytes32(), trampolineOnion.packet))
|
val (firstAmount, firstExpiry, onion) = buildPaymentPacket(paymentHash, trampolineChannelHops, PaymentOnion.createTrampolinePayload(amount_ac, amount_ac, expiry_ac, randomBytes32(), trampolineOnion.packet))
|
||||||
val Right(ChannelRelayPacket(_, _, packet_c)) = decrypt(UpdateAddHtlc(randomBytes32(), 1, firstAmount, paymentHash, firstExpiry, onion.packet), priv_b.privateKey)
|
val Right(ChannelRelayPacket(_, _, packet_c)) = decrypt(UpdateAddHtlc(randomBytes32(), 1, firstAmount, paymentHash, firstExpiry, onion.packet), priv_b.privateKey)
|
||||||
val Right(NodeRelayPacket(_, _, _, packet_d)) = decrypt(UpdateAddHtlc(randomBytes32(), 2, amount_bc, paymentHash, expiry_bc, packet_c), priv_c.privateKey)
|
val Right(NodeRelayPacket(_, _, _, packet_d)) = decrypt(UpdateAddHtlc(randomBytes32(), 2, amount_bc, paymentHash, expiry_bc, packet_c), priv_c.privateKey)
|
||||||
|
@ -330,7 +335,7 @@ class PaymentPacketSpec extends AnyFunSuite with BeforeAndAfterAll {
|
||||||
}
|
}
|
||||||
|
|
||||||
test("fail to decrypt at intermediate trampoline node when amount is invalid") {
|
test("fail to decrypt at intermediate trampoline node when amount is invalid") {
|
||||||
val (amount_ac, expiry_ac, trampolineOnion) = buildTrampolinePacket(paymentHash, trampolineHops, PaymentOnion.createSinglePartPayload(finalAmount, finalExpiry, paymentSecret))
|
val (amount_ac, expiry_ac, trampolineOnion) = buildTrampolinePacket(paymentHash, trampolineHops, PaymentOnion.createSinglePartPayload(finalAmount, finalExpiry, paymentSecret, None))
|
||||||
val (firstAmount, firstExpiry, onion) = buildPaymentPacket(paymentHash, trampolineChannelHops, PaymentOnion.createTrampolinePayload(amount_ac, amount_ac, expiry_ac, randomBytes32(), trampolineOnion.packet))
|
val (firstAmount, firstExpiry, onion) = buildPaymentPacket(paymentHash, trampolineChannelHops, PaymentOnion.createTrampolinePayload(amount_ac, amount_ac, expiry_ac, randomBytes32(), trampolineOnion.packet))
|
||||||
val Right(ChannelRelayPacket(_, _, packet_c)) = decrypt(UpdateAddHtlc(randomBytes32(), 1, firstAmount, paymentHash, firstExpiry, onion.packet), priv_b.privateKey)
|
val Right(ChannelRelayPacket(_, _, packet_c)) = decrypt(UpdateAddHtlc(randomBytes32(), 1, firstAmount, paymentHash, firstExpiry, onion.packet), priv_b.privateKey)
|
||||||
// A trampoline relay is very similar to a final node: it can validate that the HTLC amount matches the onion outer amount.
|
// A trampoline relay is very similar to a final node: it can validate that the HTLC amount matches the onion outer amount.
|
||||||
|
@ -339,7 +344,7 @@ class PaymentPacketSpec extends AnyFunSuite with BeforeAndAfterAll {
|
||||||
}
|
}
|
||||||
|
|
||||||
test("fail to decrypt at intermediate trampoline node when expiry is invalid") {
|
test("fail to decrypt at intermediate trampoline node when expiry is invalid") {
|
||||||
val (amount_ac, expiry_ac, trampolineOnion) = buildTrampolinePacket(paymentHash, trampolineHops, PaymentOnion.createSinglePartPayload(finalAmount, finalExpiry, paymentSecret))
|
val (amount_ac, expiry_ac, trampolineOnion) = buildTrampolinePacket(paymentHash, trampolineHops, PaymentOnion.createSinglePartPayload(finalAmount, finalExpiry, paymentSecret, None))
|
||||||
val (firstAmount, firstExpiry, onion) = buildPaymentPacket(paymentHash, trampolineChannelHops, PaymentOnion.createTrampolinePayload(amount_ac, amount_ac, expiry_ac, randomBytes32(), trampolineOnion.packet))
|
val (firstAmount, firstExpiry, onion) = buildPaymentPacket(paymentHash, trampolineChannelHops, PaymentOnion.createTrampolinePayload(amount_ac, amount_ac, expiry_ac, randomBytes32(), trampolineOnion.packet))
|
||||||
val Right(ChannelRelayPacket(_, _, packet_c)) = decrypt(UpdateAddHtlc(randomBytes32(), 1, firstAmount, paymentHash, firstExpiry, onion.packet), priv_b.privateKey)
|
val Right(ChannelRelayPacket(_, _, packet_c)) = decrypt(UpdateAddHtlc(randomBytes32(), 1, firstAmount, paymentHash, firstExpiry, onion.packet), priv_b.privateKey)
|
||||||
// A trampoline relay is very similar to a final node: it can validate that the HTLC expiry matches the onion outer expiry.
|
// A trampoline relay is very similar to a final node: it can validate that the HTLC expiry matches the onion outer expiry.
|
||||||
|
@ -398,6 +403,7 @@ object PaymentPacketSpec {
|
||||||
val paymentPreimage = randomBytes32()
|
val paymentPreimage = randomBytes32()
|
||||||
val paymentHash = Crypto.sha256(paymentPreimage)
|
val paymentHash = Crypto.sha256(paymentPreimage)
|
||||||
val paymentSecret = randomBytes32()
|
val paymentSecret = randomBytes32()
|
||||||
|
val paymentMetadata = randomBytes32().bytes
|
||||||
|
|
||||||
val expiry_de = finalExpiry
|
val expiry_de = finalExpiry
|
||||||
val amount_de = finalAmount
|
val amount_de = finalAmount
|
||||||
|
|
|
@ -18,10 +18,10 @@ package fr.acinq.eclair.payment
|
||||||
|
|
||||||
import fr.acinq.bitcoin.Crypto.{PrivateKey, PublicKey}
|
import fr.acinq.bitcoin.Crypto.{PrivateKey, PublicKey}
|
||||||
import fr.acinq.bitcoin.{Block, BtcDouble, ByteVector32, Crypto, MilliBtcDouble, SatoshiLong}
|
import fr.acinq.bitcoin.{Block, BtcDouble, ByteVector32, Crypto, MilliBtcDouble, SatoshiLong}
|
||||||
import fr.acinq.eclair.FeatureSupport.Mandatory
|
import fr.acinq.eclair.FeatureSupport.{Mandatory, Optional}
|
||||||
import fr.acinq.eclair.Features.{PaymentSecret, _}
|
import fr.acinq.eclair.Features.{PaymentMetadata, PaymentSecret, _}
|
||||||
import fr.acinq.eclair.payment.PaymentRequest._
|
import fr.acinq.eclair.payment.PaymentRequest._
|
||||||
import fr.acinq.eclair.{CltvExpiryDelta, FeatureSupport, Features, MilliSatoshiLong, ShortChannelId, TestConstants, TimestampSecond, TimestampSecondLong, ToMilliSatoshiConversion}
|
import fr.acinq.eclair.{CltvExpiryDelta, Feature, FeatureSupport, Features, InvoiceFeature, MilliSatoshiLong, ShortChannelId, TestConstants, TimestampSecond, TimestampSecondLong, ToMilliSatoshiConversion}
|
||||||
import org.scalatest.funsuite.AnyFunSuite
|
import org.scalatest.funsuite.AnyFunSuite
|
||||||
import scodec.DecodeResult
|
import scodec.DecodeResult
|
||||||
import scodec.bits._
|
import scodec.bits._
|
||||||
|
@ -311,6 +311,22 @@ class PaymentRequestSpec extends AnyFunSuite {
|
||||||
assert(PaymentRequest.write(pr.sign(priv)) === ref)
|
assert(PaymentRequest.write(pr.sign(priv)) === ref)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
test("On mainnet, please send 0.01 BTC with payment metadata 0x01fafaf0") {
|
||||||
|
val ref = "lnbc10m1pvjluezpp5qqqsyqcyq5rqwzqfqqqsyqcyq5rqwzqfqqqsyqcyq5rqwzqfqypqdp9wpshjmt9de6zqmt9w3skgct5vysxjmnnd9jx2mq8q8a04uqsp5zyg3zyg3zyg3zyg3zyg3zyg3zyg3zyg3zyg3zyg3zyg3zyg3zygs9q2gqqqqqqsgq7hf8he7ecf7n4ffphs6awl9t6676rrclv9ckg3d3ncn7fct63p6s365duk5wrk202cfy3aj5xnnp5gs3vrdvruverwwq7yzhkf5a3xqpd05wjc"
|
||||||
|
val pr = PaymentRequest.read(ref)
|
||||||
|
assert(pr.prefix == "lnbc")
|
||||||
|
assert(pr.amount === Some(1000000000 msat))
|
||||||
|
assert(pr.paymentHash.bytes == hex"0001020304050607080900010203040506070809000102030405060708090102")
|
||||||
|
assert(pr.features.features === Features(VariableLengthOnion -> Mandatory, PaymentSecret -> Mandatory, PaymentMetadata -> Mandatory))
|
||||||
|
assert(pr.timestamp == TimestampSecond(1496314658L))
|
||||||
|
assert(pr.nodeId == PublicKey(hex"03e7156ae33b0a208d0744199163177e909e80176e55d97a2f221ede0f934dd9ad"))
|
||||||
|
assert(pr.paymentSecret === Some(ByteVector32(hex"1111111111111111111111111111111111111111111111111111111111111111")))
|
||||||
|
assert(pr.description == Left("payment metadata inside"))
|
||||||
|
assert(pr.paymentMetadata === Some(hex"01fafaf0"))
|
||||||
|
assert(pr.tags.size == 5)
|
||||||
|
assert(PaymentRequest.write(pr.sign(priv)) == ref)
|
||||||
|
}
|
||||||
|
|
||||||
test("reject invalid invoices") {
|
test("reject invalid invoices") {
|
||||||
val refs = Seq(
|
val refs = Seq(
|
||||||
// Bech32 checksum is invalid.
|
// Bech32 checksum is invalid.
|
||||||
|
@ -455,7 +471,7 @@ class PaymentRequestSpec extends AnyFunSuite {
|
||||||
test("payment secret") {
|
test("payment secret") {
|
||||||
val pr = PaymentRequest(Block.LivenetGenesisBlock.hash, Some(123 msat), ByteVector32.One, priv, Left("Some invoice"), CltvExpiryDelta(18))
|
val pr = PaymentRequest(Block.LivenetGenesisBlock.hash, Some(123 msat), ByteVector32.One, priv, Left("Some invoice"), CltvExpiryDelta(18))
|
||||||
assert(pr.paymentSecret.isDefined)
|
assert(pr.paymentSecret.isDefined)
|
||||||
assert(pr.features === PaymentRequestFeatures(PaymentSecret.mandatory, VariableLengthOnion.mandatory))
|
assert(pr.features === PaymentRequestFeatures(Map[Feature with InvoiceFeature, FeatureSupport](VariableLengthOnion -> Mandatory, PaymentSecret -> Mandatory)))
|
||||||
assert(pr.features.requirePaymentSecret)
|
assert(pr.features.requirePaymentSecret)
|
||||||
|
|
||||||
val pr1 = PaymentRequest.read(PaymentRequest.write(pr))
|
val pr1 = PaymentRequest.read(PaymentRequest.write(pr))
|
||||||
|
@ -471,20 +487,23 @@ class PaymentRequestSpec extends AnyFunSuite {
|
||||||
)
|
)
|
||||||
|
|
||||||
// A multi-part invoice must use a payment secret.
|
// A multi-part invoice must use a payment secret.
|
||||||
assertThrows[IllegalArgumentException](
|
assertThrows[IllegalArgumentException]({
|
||||||
PaymentRequest(Block.LivenetGenesisBlock.hash, Some(123 msat), ByteVector32.One, priv, Left("MPP without secrets"), CltvExpiryDelta(18), features = PaymentRequestFeatures(BasicMultiPartPayment.optional, VariableLengthOnion.optional))
|
val features = PaymentRequestFeatures(Map[Feature with InvoiceFeature, FeatureSupport](VariableLengthOnion -> Optional, PaymentSecret -> Optional))
|
||||||
)
|
PaymentRequest(Block.LivenetGenesisBlock.hash, Some(123 msat), ByteVector32.One, priv, Left("MPP without secrets"), CltvExpiryDelta(18), features = features)
|
||||||
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
test("trampoline") {
|
test("trampoline") {
|
||||||
val pr = PaymentRequest(Block.LivenetGenesisBlock.hash, Some(123 msat), ByteVector32.One, priv, Left("Some invoice"), CltvExpiryDelta(18))
|
val pr = PaymentRequest(Block.LivenetGenesisBlock.hash, Some(123 msat), ByteVector32.One, priv, Left("Some invoice"), CltvExpiryDelta(18))
|
||||||
assert(!pr.features.allowTrampoline)
|
assert(!pr.features.allowTrampoline)
|
||||||
|
|
||||||
val pr1 = PaymentRequest(Block.LivenetGenesisBlock.hash, Some(123 msat), ByteVector32.One, priv, Left("Some invoice"), CltvExpiryDelta(18), features = PaymentRequestFeatures(VariableLengthOnion.mandatory, PaymentSecret.mandatory, TrampolinePayment.optional))
|
val features1 = PaymentRequestFeatures(Map[Feature with InvoiceFeature, FeatureSupport](VariableLengthOnion -> Mandatory, PaymentSecret -> Mandatory, TrampolinePayment -> Optional))
|
||||||
|
val pr1 = PaymentRequest(Block.LivenetGenesisBlock.hash, Some(123 msat), ByteVector32.One, priv, Left("Some invoice"), CltvExpiryDelta(18), features = features1)
|
||||||
assert(!pr1.features.allowMultiPart)
|
assert(!pr1.features.allowMultiPart)
|
||||||
assert(pr1.features.allowTrampoline)
|
assert(pr1.features.allowTrampoline)
|
||||||
|
|
||||||
val pr2 = PaymentRequest(Block.LivenetGenesisBlock.hash, Some(123 msat), ByteVector32.One, priv, Left("Some invoice"), CltvExpiryDelta(18), features = PaymentRequestFeatures(VariableLengthOnion.mandatory, PaymentSecret.mandatory, BasicMultiPartPayment.optional, TrampolinePayment.optional))
|
val features2 = PaymentRequestFeatures(Map[Feature with InvoiceFeature, FeatureSupport](VariableLengthOnion -> Mandatory, PaymentSecret -> Mandatory, BasicMultiPartPayment -> Optional, TrampolinePayment -> Optional))
|
||||||
|
val pr2 = PaymentRequest(Block.LivenetGenesisBlock.hash, Some(123 msat), ByteVector32.One, priv, Left("Some invoice"), CltvExpiryDelta(18), features = features2)
|
||||||
assert(pr2.features.allowMultiPart)
|
assert(pr2.features.allowMultiPart)
|
||||||
assert(pr2.features.allowTrampoline)
|
assert(pr2.features.allowTrampoline)
|
||||||
|
|
||||||
|
|
|
@ -673,7 +673,7 @@ object PostRestartHtlcCleanerSpec {
|
||||||
val (paymentHash1, paymentHash2, paymentHash3) = (Crypto.sha256(preimage1), Crypto.sha256(preimage2), Crypto.sha256(preimage3))
|
val (paymentHash1, paymentHash2, paymentHash3) = (Crypto.sha256(preimage1), Crypto.sha256(preimage2), Crypto.sha256(preimage3))
|
||||||
|
|
||||||
def buildHtlc(htlcId: Long, channelId: ByteVector32, paymentHash: ByteVector32): UpdateAddHtlc = {
|
def buildHtlc(htlcId: Long, channelId: ByteVector32, paymentHash: ByteVector32): UpdateAddHtlc = {
|
||||||
val (cmd, _) = buildCommand(ActorRef.noSender, Upstream.Local(UUID.randomUUID()), paymentHash, hops, PaymentOnion.createSinglePartPayload(finalAmount, finalExpiry, randomBytes32()))
|
val (cmd, _) = buildCommand(ActorRef.noSender, Upstream.Local(UUID.randomUUID()), paymentHash, hops, PaymentOnion.createSinglePartPayload(finalAmount, finalExpiry, randomBytes32(), None))
|
||||||
UpdateAddHtlc(channelId, htlcId, cmd.amount, cmd.paymentHash, cmd.cltvExpiry, cmd.onion)
|
UpdateAddHtlc(channelId, htlcId, cmd.amount, cmd.paymentHash, cmd.cltvExpiry, cmd.onion)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -682,7 +682,7 @@ object PostRestartHtlcCleanerSpec {
|
||||||
def buildHtlcOut(htlcId: Long, channelId: ByteVector32, paymentHash: ByteVector32): DirectedHtlc = OutgoingHtlc(buildHtlc(htlcId, channelId, paymentHash))
|
def buildHtlcOut(htlcId: Long, channelId: ByteVector32, paymentHash: ByteVector32): DirectedHtlc = OutgoingHtlc(buildHtlc(htlcId, channelId, paymentHash))
|
||||||
|
|
||||||
def buildFinalHtlc(htlcId: Long, channelId: ByteVector32, paymentHash: ByteVector32): DirectedHtlc = {
|
def buildFinalHtlc(htlcId: Long, channelId: ByteVector32, paymentHash: ByteVector32): DirectedHtlc = {
|
||||||
val (cmd, _) = buildCommand(ActorRef.noSender, Upstream.Local(UUID.randomUUID()), paymentHash, ChannelHop(a, TestConstants.Bob.nodeParams.nodeId, channelUpdate_ab) :: Nil, PaymentOnion.createSinglePartPayload(finalAmount, finalExpiry, randomBytes32()))
|
val (cmd, _) = buildCommand(ActorRef.noSender, Upstream.Local(UUID.randomUUID()), paymentHash, ChannelHop(a, TestConstants.Bob.nodeParams.nodeId, channelUpdate_ab) :: Nil, PaymentOnion.createSinglePartPayload(finalAmount, finalExpiry, randomBytes32(), None))
|
||||||
IncomingHtlc(UpdateAddHtlc(channelId, htlcId, cmd.amount, cmd.paymentHash, cmd.cltvExpiry, cmd.onion))
|
IncomingHtlc(UpdateAddHtlc(channelId, htlcId, cmd.amount, cmd.paymentHash, cmd.cltvExpiry, cmd.onion))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -24,6 +24,7 @@ import akka.actor.typed.scaladsl.ActorContext
|
||||||
import akka.actor.typed.scaladsl.adapter._
|
import akka.actor.typed.scaladsl.adapter._
|
||||||
import com.typesafe.config.ConfigFactory
|
import com.typesafe.config.ConfigFactory
|
||||||
import fr.acinq.bitcoin.{Block, ByteVector32, Crypto}
|
import fr.acinq.bitcoin.{Block, ByteVector32, Crypto}
|
||||||
|
import fr.acinq.eclair.FeatureSupport.{Mandatory, Optional}
|
||||||
import fr.acinq.eclair.Features.{BasicMultiPartPayment, PaymentSecret, VariableLengthOnion}
|
import fr.acinq.eclair.Features.{BasicMultiPartPayment, PaymentSecret, VariableLengthOnion}
|
||||||
import fr.acinq.eclair.channel.{CMD_FAIL_HTLC, CMD_FULFILL_HTLC, Register}
|
import fr.acinq.eclair.channel.{CMD_FAIL_HTLC, CMD_FULFILL_HTLC, Register}
|
||||||
import fr.acinq.eclair.crypto.Sphinx
|
import fr.acinq.eclair.crypto.Sphinx
|
||||||
|
@ -37,7 +38,7 @@ import fr.acinq.eclair.payment.send.PaymentLifecycle.SendPaymentToNode
|
||||||
import fr.acinq.eclair.router.Router.RouteRequest
|
import fr.acinq.eclair.router.Router.RouteRequest
|
||||||
import fr.acinq.eclair.router.{BalanceTooLow, RouteNotFound}
|
import fr.acinq.eclair.router.{BalanceTooLow, RouteNotFound}
|
||||||
import fr.acinq.eclair.wire.protocol._
|
import fr.acinq.eclair.wire.protocol._
|
||||||
import fr.acinq.eclair.{CltvExpiry, CltvExpiryDelta, MilliSatoshi, MilliSatoshiLong, NodeParams, ShortChannelId, TestConstants, UInt64, randomBytes, randomBytes32, randomKey}
|
import fr.acinq.eclair.{CltvExpiry, CltvExpiryDelta, Feature, FeatureSupport, InvoiceFeature, MilliSatoshi, MilliSatoshiLong, NodeParams, ShortChannelId, TestConstants, UInt64, randomBytes, randomBytes32, randomKey}
|
||||||
import org.scalatest.Outcome
|
import org.scalatest.Outcome
|
||||||
import org.scalatest.funsuite.FixtureAnyFunSuiteLike
|
import org.scalatest.funsuite.FixtureAnyFunSuiteLike
|
||||||
import scodec.bits.HexStringSyntax
|
import scodec.bits.HexStringSyntax
|
||||||
|
@ -211,7 +212,7 @@ class NodeRelayerSpec extends ScalaTestWithActorTestKit(ConfigFactory.load("appl
|
||||||
// and then one extra
|
// and then one extra
|
||||||
val extra = IncomingPaymentPacket.NodeRelayPacket(
|
val extra = IncomingPaymentPacket.NodeRelayPacket(
|
||||||
UpdateAddHtlc(randomBytes32(), Random.nextInt(100), 1000 msat, paymentHash, CltvExpiry(499990), TestConstants.emptyOnionPacket),
|
UpdateAddHtlc(randomBytes32(), Random.nextInt(100), 1000 msat, paymentHash, CltvExpiry(499990), TestConstants.emptyOnionPacket),
|
||||||
PaymentOnion.createMultiPartPayload(1000 msat, incomingAmount, CltvExpiry(499990), incomingSecret),
|
PaymentOnion.createMultiPartPayload(1000 msat, incomingAmount, CltvExpiry(499990), incomingSecret, None),
|
||||||
PaymentOnion.createNodeRelayPayload(outgoingAmount, outgoingExpiry, outgoingNodeId),
|
PaymentOnion.createNodeRelayPayload(outgoingAmount, outgoingExpiry, outgoingNodeId),
|
||||||
nextTrampolinePacket)
|
nextTrampolinePacket)
|
||||||
nodeRelayer ! NodeRelay.Relay(extra)
|
nodeRelayer ! NodeRelay.Relay(extra)
|
||||||
|
@ -240,7 +241,7 @@ class NodeRelayerSpec extends ScalaTestWithActorTestKit(ConfigFactory.load("appl
|
||||||
// Receive new extraneous multi-part HTLC.
|
// Receive new extraneous multi-part HTLC.
|
||||||
val i1 = IncomingPaymentPacket.NodeRelayPacket(
|
val i1 = IncomingPaymentPacket.NodeRelayPacket(
|
||||||
UpdateAddHtlc(randomBytes32(), Random.nextInt(100), 1000 msat, paymentHash, CltvExpiry(499990), TestConstants.emptyOnionPacket),
|
UpdateAddHtlc(randomBytes32(), Random.nextInt(100), 1000 msat, paymentHash, CltvExpiry(499990), TestConstants.emptyOnionPacket),
|
||||||
PaymentOnion.createMultiPartPayload(1000 msat, incomingAmount, CltvExpiry(499990), incomingSecret),
|
PaymentOnion.createMultiPartPayload(1000 msat, incomingAmount, CltvExpiry(499990), incomingSecret, None),
|
||||||
PaymentOnion.createNodeRelayPayload(outgoingAmount, outgoingExpiry, outgoingNodeId),
|
PaymentOnion.createNodeRelayPayload(outgoingAmount, outgoingExpiry, outgoingNodeId),
|
||||||
nextTrampolinePacket)
|
nextTrampolinePacket)
|
||||||
nodeRelayer ! NodeRelay.Relay(i1)
|
nodeRelayer ! NodeRelay.Relay(i1)
|
||||||
|
@ -253,7 +254,7 @@ class NodeRelayerSpec extends ScalaTestWithActorTestKit(ConfigFactory.load("appl
|
||||||
// Receive new HTLC with different details, but for the same payment hash.
|
// Receive new HTLC with different details, but for the same payment hash.
|
||||||
val i2 = IncomingPaymentPacket.NodeRelayPacket(
|
val i2 = IncomingPaymentPacket.NodeRelayPacket(
|
||||||
UpdateAddHtlc(randomBytes32(), Random.nextInt(100), 1500 msat, paymentHash, CltvExpiry(499990), TestConstants.emptyOnionPacket),
|
UpdateAddHtlc(randomBytes32(), Random.nextInt(100), 1500 msat, paymentHash, CltvExpiry(499990), TestConstants.emptyOnionPacket),
|
||||||
PaymentOnion.createSinglePartPayload(1500 msat, CltvExpiry(499990), incomingSecret),
|
PaymentOnion.createSinglePartPayload(1500 msat, CltvExpiry(499990), incomingSecret, None),
|
||||||
PaymentOnion.createNodeRelayPayload(1250 msat, outgoingExpiry, outgoingNodeId),
|
PaymentOnion.createNodeRelayPayload(1250 msat, outgoingExpiry, outgoingNodeId),
|
||||||
nextTrampolinePacket)
|
nextTrampolinePacket)
|
||||||
nodeRelayer ! NodeRelay.Relay(i2)
|
nodeRelayer ! NodeRelay.Relay(i2)
|
||||||
|
@ -566,8 +567,8 @@ class NodeRelayerSpec extends ScalaTestWithActorTestKit(ConfigFactory.load("appl
|
||||||
|
|
||||||
// Receive an upstream multi-part payment.
|
// Receive an upstream multi-part payment.
|
||||||
val hints = List(List(ExtraHop(outgoingNodeId, ShortChannelId(42), feeBase = 10 msat, feeProportionalMillionths = 1, cltvExpiryDelta = CltvExpiryDelta(12))))
|
val hints = List(List(ExtraHop(outgoingNodeId, ShortChannelId(42), feeBase = 10 msat, feeProportionalMillionths = 1, cltvExpiryDelta = CltvExpiryDelta(12))))
|
||||||
val features = PaymentRequestFeatures(VariableLengthOnion.mandatory, PaymentSecret.mandatory, BasicMultiPartPayment.optional)
|
val features = PaymentRequestFeatures(Map[Feature with InvoiceFeature, FeatureSupport](VariableLengthOnion -> Mandatory, PaymentSecret -> Mandatory, BasicMultiPartPayment -> Optional))
|
||||||
val pr = PaymentRequest(Block.LivenetGenesisBlock.hash, Some(outgoingAmount * 3), paymentHash, randomKey(), Left("Some invoice"), CltvExpiryDelta(18), extraHops = hints, features = features)
|
val pr = PaymentRequest(Block.LivenetGenesisBlock.hash, Some(outgoingAmount * 3), paymentHash, randomKey(), Left("Some invoice"), CltvExpiryDelta(18), extraHops = hints, paymentMetadata = Some(hex"123456"), features = features)
|
||||||
val incomingPayments = incomingMultiPart.map(incoming => incoming.copy(innerPayload = PaymentOnion.createNodeRelayToNonTrampolinePayload(
|
val incomingPayments = incomingMultiPart.map(incoming => incoming.copy(innerPayload = PaymentOnion.createNodeRelayToNonTrampolinePayload(
|
||||||
incoming.innerPayload.amountToForward, outgoingAmount * 3, outgoingExpiry, outgoingNodeId, pr
|
incoming.innerPayload.amountToForward, outgoingAmount * 3, outgoingExpiry, outgoingNodeId, pr
|
||||||
)))
|
)))
|
||||||
|
@ -578,6 +579,7 @@ class NodeRelayerSpec extends ScalaTestWithActorTestKit(ConfigFactory.load("appl
|
||||||
validateOutgoingCfg(outgoingCfg, Upstream.Trampoline(incomingMultiPart.map(_.add)))
|
validateOutgoingCfg(outgoingCfg, Upstream.Trampoline(incomingMultiPart.map(_.add)))
|
||||||
val outgoingPayment = mockPayFSM.expectMessageType[SendMultiPartPayment]
|
val outgoingPayment = mockPayFSM.expectMessageType[SendMultiPartPayment]
|
||||||
assert(outgoingPayment.paymentSecret === pr.paymentSecret.get) // we should use the provided secret
|
assert(outgoingPayment.paymentSecret === pr.paymentSecret.get) // we should use the provided secret
|
||||||
|
assert(outgoingPayment.paymentMetadata === pr.paymentMetadata) // we should use the provided metadata
|
||||||
assert(outgoingPayment.totalAmount === outgoingAmount)
|
assert(outgoingPayment.totalAmount === outgoingAmount)
|
||||||
assert(outgoingPayment.targetExpiry === outgoingExpiry)
|
assert(outgoingPayment.targetExpiry === outgoingExpiry)
|
||||||
assert(outgoingPayment.targetNodeId === outgoingNodeId)
|
assert(outgoingPayment.targetNodeId === outgoingNodeId)
|
||||||
|
@ -607,7 +609,7 @@ class NodeRelayerSpec extends ScalaTestWithActorTestKit(ConfigFactory.load("appl
|
||||||
|
|
||||||
// Receive an upstream multi-part payment.
|
// Receive an upstream multi-part payment.
|
||||||
val hints = List(List(ExtraHop(outgoingNodeId, ShortChannelId(42), feeBase = 10 msat, feeProportionalMillionths = 1, cltvExpiryDelta = CltvExpiryDelta(12))))
|
val hints = List(List(ExtraHop(outgoingNodeId, ShortChannelId(42), feeBase = 10 msat, feeProportionalMillionths = 1, cltvExpiryDelta = CltvExpiryDelta(12))))
|
||||||
val pr = PaymentRequest(Block.LivenetGenesisBlock.hash, Some(outgoingAmount), paymentHash, randomKey(), Left("Some invoice"), CltvExpiryDelta(18), extraHops = hints)
|
val pr = PaymentRequest(Block.LivenetGenesisBlock.hash, Some(outgoingAmount), paymentHash, randomKey(), Left("Some invoice"), CltvExpiryDelta(18), extraHops = hints, paymentMetadata = Some(hex"123456"))
|
||||||
assert(!pr.features.allowMultiPart)
|
assert(!pr.features.allowMultiPart)
|
||||||
val incomingPayments = incomingMultiPart.map(incoming => incoming.copy(innerPayload = PaymentOnion.createNodeRelayToNonTrampolinePayload(
|
val incomingPayments = incomingMultiPart.map(incoming => incoming.copy(innerPayload = PaymentOnion.createNodeRelayToNonTrampolinePayload(
|
||||||
incoming.innerPayload.amountToForward, incoming.innerPayload.amountToForward, outgoingExpiry, outgoingNodeId, pr
|
incoming.innerPayload.amountToForward, incoming.innerPayload.amountToForward, outgoingExpiry, outgoingNodeId, pr
|
||||||
|
@ -620,6 +622,7 @@ class NodeRelayerSpec extends ScalaTestWithActorTestKit(ConfigFactory.load("appl
|
||||||
val outgoingPayment = mockPayFSM.expectMessageType[SendPaymentToNode]
|
val outgoingPayment = mockPayFSM.expectMessageType[SendPaymentToNode]
|
||||||
assert(outgoingPayment.finalPayload.amount === outgoingAmount)
|
assert(outgoingPayment.finalPayload.amount === outgoingAmount)
|
||||||
assert(outgoingPayment.finalPayload.expiry === outgoingExpiry)
|
assert(outgoingPayment.finalPayload.expiry === outgoingExpiry)
|
||||||
|
assert(outgoingPayment.finalPayload.paymentMetadata === pr.paymentMetadata) // we should use the provided metadata
|
||||||
assert(outgoingPayment.targetNodeId === outgoingNodeId)
|
assert(outgoingPayment.targetNodeId === outgoingNodeId)
|
||||||
assert(outgoingPayment.assistedRoutes === hints)
|
assert(outgoingPayment.assistedRoutes === hints)
|
||||||
// those are adapters for pay-fsm messages
|
// those are adapters for pay-fsm messages
|
||||||
|
@ -718,9 +721,9 @@ object NodeRelayerSpec {
|
||||||
|
|
||||||
def createValidIncomingPacket(amountIn: MilliSatoshi, totalAmountIn: MilliSatoshi, expiryIn: CltvExpiry, amountOut: MilliSatoshi, expiryOut: CltvExpiry): IncomingPaymentPacket.NodeRelayPacket = {
|
def createValidIncomingPacket(amountIn: MilliSatoshi, totalAmountIn: MilliSatoshi, expiryIn: CltvExpiry, amountOut: MilliSatoshi, expiryOut: CltvExpiry): IncomingPaymentPacket.NodeRelayPacket = {
|
||||||
val outerPayload = if (amountIn == totalAmountIn) {
|
val outerPayload = if (amountIn == totalAmountIn) {
|
||||||
PaymentOnion.createSinglePartPayload(amountIn, expiryIn, incomingSecret)
|
PaymentOnion.createSinglePartPayload(amountIn, expiryIn, incomingSecret, None)
|
||||||
} else {
|
} else {
|
||||||
PaymentOnion.createMultiPartPayload(amountIn, totalAmountIn, expiryIn, incomingSecret)
|
PaymentOnion.createMultiPartPayload(amountIn, totalAmountIn, expiryIn, incomingSecret, None)
|
||||||
}
|
}
|
||||||
IncomingPaymentPacket.NodeRelayPacket(
|
IncomingPaymentPacket.NodeRelayPacket(
|
||||||
UpdateAddHtlc(randomBytes32(), Random.nextInt(100), amountIn, paymentHash, expiryIn, TestConstants.emptyOnionPacket),
|
UpdateAddHtlc(randomBytes32(), Random.nextInt(100), amountIn, paymentHash, expiryIn, TestConstants.emptyOnionPacket),
|
||||||
|
@ -734,7 +737,7 @@ object NodeRelayerSpec {
|
||||||
val amountIn = incomingAmount / 2
|
val amountIn = incomingAmount / 2
|
||||||
IncomingPaymentPacket.NodeRelayPacket(
|
IncomingPaymentPacket.NodeRelayPacket(
|
||||||
UpdateAddHtlc(randomBytes32(), Random.nextInt(100), amountIn, paymentHash, expiryIn, TestConstants.emptyOnionPacket),
|
UpdateAddHtlc(randomBytes32(), Random.nextInt(100), amountIn, paymentHash, expiryIn, TestConstants.emptyOnionPacket),
|
||||||
PaymentOnion.createMultiPartPayload(amountIn, incomingAmount, expiryIn, paymentSecret),
|
PaymentOnion.createMultiPartPayload(amountIn, incomingAmount, expiryIn, paymentSecret, None),
|
||||||
PaymentOnion.createNodeRelayPayload(outgoingAmount, expiryOut, outgoingNodeId),
|
PaymentOnion.createNodeRelayPayload(outgoingAmount, expiryOut, outgoingNodeId),
|
||||||
nextTrampolinePacket)
|
nextTrampolinePacket)
|
||||||
}
|
}
|
||||||
|
|
|
@ -84,7 +84,7 @@ class RelayerSpec extends ScalaTestWithActorTestKit(ConfigFactory.load("applicat
|
||||||
}
|
}
|
||||||
|
|
||||||
// we use this to build a valid onion
|
// we use this to build a valid onion
|
||||||
val (cmd, _) = buildCommand(ActorRef.noSender, Upstream.Local(UUID.randomUUID()), paymentHash, hops, PaymentOnion.createSinglePartPayload(finalAmount, finalExpiry, paymentSecret))
|
val (cmd, _) = buildCommand(ActorRef.noSender, Upstream.Local(UUID.randomUUID()), paymentHash, hops, PaymentOnion.createSinglePartPayload(finalAmount, finalExpiry, paymentSecret, None))
|
||||||
// and then manually build an htlc
|
// and then manually build an htlc
|
||||||
val add_ab = UpdateAddHtlc(channelId = randomBytes32(), id = 123456, cmd.amount, cmd.paymentHash, cmd.cltvExpiry, cmd.onion)
|
val add_ab = UpdateAddHtlc(channelId = randomBytes32(), id = 123456, cmd.amount, cmd.paymentHash, cmd.cltvExpiry, cmd.onion)
|
||||||
relayer ! RelayForward(add_ab)
|
relayer ! RelayForward(add_ab)
|
||||||
|
@ -94,14 +94,14 @@ class RelayerSpec extends ScalaTestWithActorTestKit(ConfigFactory.load("applicat
|
||||||
test("relay an htlc-add at the final node to the payment handler") { f =>
|
test("relay an htlc-add at the final node to the payment handler") { f =>
|
||||||
import f._
|
import f._
|
||||||
|
|
||||||
val (cmd, _) = buildCommand(ActorRef.noSender, Upstream.Local(UUID.randomUUID()), paymentHash, hops.take(1), PaymentOnion.createSinglePartPayload(finalAmount, finalExpiry, paymentSecret))
|
val (cmd, _) = buildCommand(ActorRef.noSender, Upstream.Local(UUID.randomUUID()), paymentHash, hops.take(1), PaymentOnion.createSinglePartPayload(finalAmount, finalExpiry, paymentSecret, None))
|
||||||
val add_ab = UpdateAddHtlc(channelId = channelId_ab, id = 123456, cmd.amount, cmd.paymentHash, cmd.cltvExpiry, cmd.onion)
|
val add_ab = UpdateAddHtlc(channelId = channelId_ab, id = 123456, cmd.amount, cmd.paymentHash, cmd.cltvExpiry, cmd.onion)
|
||||||
|
|
||||||
relayer ! RelayForward(add_ab)
|
relayer ! RelayForward(add_ab)
|
||||||
|
|
||||||
val fp = paymentHandler.expectMessageType[FinalPacket]
|
val fp = paymentHandler.expectMessageType[FinalPacket]
|
||||||
assert(fp.add === add_ab)
|
assert(fp.add === add_ab)
|
||||||
assert(fp.payload === PaymentOnion.createSinglePartPayload(finalAmount, finalExpiry, paymentSecret))
|
assert(fp.payload === PaymentOnion.createSinglePartPayload(finalAmount, finalExpiry, paymentSecret, None))
|
||||||
|
|
||||||
register.expectNoMessage(50 millis)
|
register.expectNoMessage(50 millis)
|
||||||
}
|
}
|
||||||
|
@ -114,7 +114,7 @@ class RelayerSpec extends ScalaTestWithActorTestKit(ConfigFactory.load("applicat
|
||||||
// We simulate a payment split between multiple trampoline routes.
|
// We simulate a payment split between multiple trampoline routes.
|
||||||
val totalAmount = finalAmount * 3
|
val totalAmount = finalAmount * 3
|
||||||
val trampolineHops = NodeHop(a, b, channelUpdate_ab.cltvExpiryDelta, 0 msat) :: Nil
|
val trampolineHops = NodeHop(a, b, channelUpdate_ab.cltvExpiryDelta, 0 msat) :: Nil
|
||||||
val (trampolineAmount, trampolineExpiry, trampolineOnion) = OutgoingPaymentPacket.buildTrampolinePacket(paymentHash, trampolineHops, PaymentOnion.createMultiPartPayload(finalAmount, totalAmount, finalExpiry, paymentSecret))
|
val (trampolineAmount, trampolineExpiry, trampolineOnion) = OutgoingPaymentPacket.buildTrampolinePacket(paymentHash, trampolineHops, PaymentOnion.createMultiPartPayload(finalAmount, totalAmount, finalExpiry, paymentSecret, None))
|
||||||
assert(trampolineAmount === finalAmount)
|
assert(trampolineAmount === finalAmount)
|
||||||
assert(trampolineExpiry === finalExpiry)
|
assert(trampolineExpiry === finalExpiry)
|
||||||
val (cmd, _) = buildCommand(ActorRef.noSender, Upstream.Local(UUID.randomUUID()), paymentHash, ChannelHop(a, b, channelUpdate_ab) :: Nil, PaymentOnion.createTrampolinePayload(trampolineAmount, trampolineAmount, trampolineExpiry, randomBytes32(), trampolineOnion.packet))
|
val (cmd, _) = buildCommand(ActorRef.noSender, Upstream.Local(UUID.randomUUID()), paymentHash, ChannelHop(a, b, channelUpdate_ab) :: Nil, PaymentOnion.createTrampolinePayload(trampolineAmount, trampolineAmount, trampolineExpiry, randomBytes32(), trampolineOnion.packet))
|
||||||
|
@ -138,7 +138,7 @@ class RelayerSpec extends ScalaTestWithActorTestKit(ConfigFactory.load("applicat
|
||||||
import f._
|
import f._
|
||||||
|
|
||||||
// we use this to build a valid onion
|
// we use this to build a valid onion
|
||||||
val (cmd, _) = buildCommand(ActorRef.noSender, Upstream.Local(UUID.randomUUID()), paymentHash, hops, PaymentOnion.createSinglePartPayload(finalAmount, finalExpiry, paymentSecret))
|
val (cmd, _) = buildCommand(ActorRef.noSender, Upstream.Local(UUID.randomUUID()), paymentHash, hops, PaymentOnion.createSinglePartPayload(finalAmount, finalExpiry, paymentSecret, None))
|
||||||
// and then manually build an htlc with an invalid onion (hmac)
|
// and then manually build an htlc with an invalid onion (hmac)
|
||||||
val add_ab = UpdateAddHtlc(channelId = channelId_ab, id = 123456, cmd.amount, cmd.paymentHash, cmd.cltvExpiry, cmd.onion.copy(hmac = cmd.onion.hmac.reverse))
|
val add_ab = UpdateAddHtlc(channelId = channelId_ab, id = 123456, cmd.amount, cmd.paymentHash, cmd.cltvExpiry, cmd.onion.copy(hmac = cmd.onion.hmac.reverse))
|
||||||
|
|
||||||
|
@ -159,7 +159,7 @@ class RelayerSpec extends ScalaTestWithActorTestKit(ConfigFactory.load("applicat
|
||||||
|
|
||||||
// we use this to build a valid trampoline onion inside a normal onion
|
// we use this to build a valid trampoline onion inside a normal onion
|
||||||
val trampolineHops = NodeHop(a, b, channelUpdate_ab.cltvExpiryDelta, 0 msat) :: NodeHop(b, c, channelUpdate_bc.cltvExpiryDelta, fee_b) :: Nil
|
val trampolineHops = NodeHop(a, b, channelUpdate_ab.cltvExpiryDelta, 0 msat) :: NodeHop(b, c, channelUpdate_bc.cltvExpiryDelta, fee_b) :: Nil
|
||||||
val (trampolineAmount, trampolineExpiry, trampolineOnion) = OutgoingPaymentPacket.buildTrampolinePacket(paymentHash, trampolineHops, PaymentOnion.createSinglePartPayload(finalAmount, finalExpiry, paymentSecret))
|
val (trampolineAmount, trampolineExpiry, trampolineOnion) = OutgoingPaymentPacket.buildTrampolinePacket(paymentHash, trampolineHops, PaymentOnion.createSinglePartPayload(finalAmount, finalExpiry, paymentSecret, None))
|
||||||
val (cmd, _) = buildCommand(ActorRef.noSender, Upstream.Local(UUID.randomUUID()), paymentHash, ChannelHop(a, b, channelUpdate_ab) :: Nil, PaymentOnion.createTrampolinePayload(trampolineAmount, trampolineAmount, trampolineExpiry, randomBytes32(), trampolineOnion.packet))
|
val (cmd, _) = buildCommand(ActorRef.noSender, Upstream.Local(UUID.randomUUID()), paymentHash, ChannelHop(a, b, channelUpdate_ab) :: Nil, PaymentOnion.createTrampolinePayload(trampolineAmount, trampolineAmount, trampolineExpiry, randomBytes32(), trampolineOnion.packet))
|
||||||
|
|
||||||
// and then manually build an htlc
|
// and then manually build an htlc
|
||||||
|
|
|
@ -53,8 +53,26 @@ class AnnouncementsSpec extends AnyFunSuite {
|
||||||
}
|
}
|
||||||
|
|
||||||
test("create valid signed node announcement") {
|
test("create valid signed node announcement") {
|
||||||
val ann = makeNodeAnnouncement(Alice.nodeParams.privateKey, Alice.nodeParams.alias, Alice.nodeParams.color, Alice.nodeParams.publicAddresses, Alice.nodeParams.features)
|
val features = Features(
|
||||||
assert(ann.features.hasFeature(Features.VariableLengthOnion, Some(FeatureSupport.Mandatory)))
|
Features.OptionDataLossProtect -> FeatureSupport.Optional,
|
||||||
|
Features.InitialRoutingSync -> FeatureSupport.Optional,
|
||||||
|
Features.ChannelRangeQueries -> FeatureSupport.Optional,
|
||||||
|
Features.ChannelRangeQueriesExtended -> FeatureSupport.Optional,
|
||||||
|
Features.VariableLengthOnion -> FeatureSupport.Mandatory,
|
||||||
|
Features.PaymentSecret -> FeatureSupport.Mandatory,
|
||||||
|
Features.BasicMultiPartPayment -> FeatureSupport.Optional,
|
||||||
|
Features.PaymentMetadata -> FeatureSupport.Optional,
|
||||||
|
)
|
||||||
|
val ann = makeNodeAnnouncement(Alice.nodeParams.privateKey, Alice.nodeParams.alias, Alice.nodeParams.color, Alice.nodeParams.publicAddresses, features)
|
||||||
|
// Features should be filtered to only include node_announcement related features.
|
||||||
|
assert(ann.features === Features(
|
||||||
|
Features.OptionDataLossProtect -> FeatureSupport.Optional,
|
||||||
|
Features.ChannelRangeQueries -> FeatureSupport.Optional,
|
||||||
|
Features.ChannelRangeQueriesExtended -> FeatureSupport.Optional,
|
||||||
|
Features.VariableLengthOnion -> FeatureSupport.Mandatory,
|
||||||
|
Features.PaymentSecret -> FeatureSupport.Mandatory,
|
||||||
|
Features.BasicMultiPartPayment -> FeatureSupport.Optional,
|
||||||
|
))
|
||||||
assert(checkSig(ann))
|
assert(checkSig(ann))
|
||||||
assert(checkSig(ann.copy(timestamp = 153 unixsec)) === false)
|
assert(checkSig(ann.copy(timestamp = 153 unixsec)) === false)
|
||||||
}
|
}
|
||||||
|
|
Loading…
Add table
Reference in a new issue