1
0
Fork 0
mirror of https://github.com/ACINQ/eclair.git synced 2025-03-13 19:37:35 +01:00

handle min pay-to-open in Phoenix

This commit is contained in:
pm47 2021-02-01 14:25:19 +01:00
parent 0b3ba78c7f
commit b59f9d640b
No known key found for this signature in database
GPG key ID: E434ED292E85643A
6 changed files with 115 additions and 32 deletions

View file

@ -110,6 +110,11 @@ object PaymentReceived {
}
/**
* We may reject an incoming payment, because it requires the creation of a new channel, but the amount is too low.
*/
case class MissedPayToOpenPayment(paymentHash: ByteVector32, amount: MilliSatoshi, minAmount: MilliSatoshi)
case class PaymentSettlingOnChain(id: UUID, amount: MilliSatoshi, paymentHash: ByteVector32, timestamp: Long = System.currentTimeMillis) extends PaymentEvent
sealed trait PaymentFailure {

View file

@ -16,18 +16,17 @@
package fr.acinq.eclair.payment.receive
import java.util.concurrent.TimeUnit
import akka.actor.{ActorRef, Props}
import akka.event.Logging.MDC
import fr.acinq.bitcoin.ByteVector32
import fr.acinq.eclair.channel.ChannelCommandResponse
import fr.acinq.eclair.payment.MissedPayToOpenPayment
import fr.acinq.eclair.payment.Monitoring.{Metrics, Tags}
import fr.acinq.eclair.wire.{FailureMessage, IncorrectOrUnknownPaymentDetails, PayToOpenRequest, UpdateAddHtlc}
import fr.acinq.eclair.{FSMDiagnosticActorLogging, Logs, MilliSatoshi, NodeParams, wire}
import java.util.concurrent.TimeUnit
import scala.collection.immutable.Queue
import scala.compat.Platform
/**
* Created by t-bast on 18/07/2019.
@ -61,7 +60,23 @@ class MultiPartPaymentFSM(nodeParams: NodeParams, paymentHash: ByteVector32, tot
log.warning("multi-part payment total amount mismatch: previously {}, now {}", totalAmount, part.totalAmount)
goto(PAYMENT_FAILED) using PaymentFailed(IncorrectOrUnknownPaymentDetails(part.totalAmount, nodeParams.currentBlockHeight), updatedParts)
} else if (d.paidAmount + part.amount >= totalAmount) {
goto(PAYMENT_SUCCEEDED) using PaymentSucceeded(updatedParts)
// Because of the cost of opening a new channel, there is a minimum amount for incoming payments to trigger
// a pay-to-open. Given that the total amount of a payment is included in each payment part, we could have
// rejected pay-to-open parts as they arrived, but it would have caused two issues:
// - in case there is a mix of htlc parts and pay-to-open parts, the htlc parts would have been accepted and we
// would have waited for a timeout before failing them (since the payment would never complete)
// - if we rejected each pay-to-open part individually, we wouldn't have been able to emit a single event
// regarding the failed pay-to-open
// That is why, instead, we wait for all parts to arrive. Then, if there is at least one pay-to-open part, and if
// the total received amount is less than the minimum amount required for a pay-to-open, we fail the payment.
updatedParts
.collectFirst { case p: PayToOpenPart => p } match {
case Some(p) if p.totalAmount < p.payToOpen.payToOpenMinAmount =>
context.system.eventStream.publish(MissedPayToOpenPayment(p.paymentHash, p.totalAmount, p.payToOpen.payToOpenMinAmount))
goto(PAYMENT_FAILED) using PaymentFailed(IncorrectOrUnknownPaymentDetails(part.totalAmount, nodeParams.currentBlockHeight), updatedParts)
case _ =>
goto(PAYMENT_SUCCEEDED) using PaymentSucceeded(updatedParts)
}
} else {
stay using d.copy(parts = updatedParts)
}

View file

@ -303,7 +303,8 @@ object LightningMessageCodecs {
("feeThresholdSatoshis" | satoshi.unit(Satoshi(0))) ::
("feeProportionalMillionths" | uint32.unit(0)) ::
("expireAt" | uint32) ::
("htlc_opt" | optional(bool(8), updateAddHtlcCodec))).as[PayToOpenRequest]
("htlc_opt" | optional(bool(8), updateAddHtlcCodec)) ::
("payToOpenMinAmount" | millisatoshi)).as[PayToOpenRequest]
val payToOpenResponseCodec: Codec[PayToOpenResponse] = (
("chainHash" | bytes32) ::

View file

@ -303,7 +303,8 @@ case class PayToOpenRequest(chainHash: ByteVector32,
payToOpenFee: Satoshi,
paymentHash: ByteVector32,
expireAt: Long,
htlc_opt: Option[UpdateAddHtlc]
htlc_opt: Option[UpdateAddHtlc],
payToOpenMinAmount: MilliSatoshi
) extends LightningMessage with HasChainHash {
def denied(nodeSecret: PrivateKey, failure_opt: Option[FailureMessage]): PayToOpenResponse = {
// if we have the necessary information, we include a properly onion-encrypted failure reason
@ -329,13 +330,15 @@ object PayToOpenRequest {
require(requests.nonEmpty, "there needs to be at least one pay-to-open request")
require(requests.map(_.chainHash).toSet.size == 1, "all pay-to-open chain hash must be equal")
require(requests.map(_.paymentHash).toSet.size == 1, "all pay-to-open payment hash must be equal")
require(requests.map(_.payToOpenMinAmount).toSet.size == 1, "all pay-to-open min amount must be equal")
val chainHash = requests.head.chainHash
val paymentHash = requests.head.paymentHash
val totalAmount = requests.map(_.amountMsat).sum
val payToOpenFees = requests.map(_.payToOpenFee).sum
val fundingAmount = PayToOpenRequest.computeFunding(totalAmount, payToOpenFees)
val expireAt = requests.map(_.expireAt).min // the aggregate request expires when the first of the underlying request expires
PayToOpenRequest(chainHash, fundingAmount, totalAmount, payToOpenFees, paymentHash, expireAt, None)
val payToOpenMinAmount = requests.head.payToOpenMinAmount
PayToOpenRequest(chainHash, fundingAmount, totalAmount, payToOpenFees, paymentHash, expireAt, None, payToOpenMinAmount)
}
}

View file

@ -505,7 +505,7 @@ class MultiPartHandlerSpec extends TestKitBaseClass with FixtureAnyFunSuiteLike
f.sender.send(handler, ReceivePayment(Some(1000 msat), "1 fast coffee"))
val pr = f.sender.expectMsgType[PaymentRequest]
val p1 = PayToOpenRequest(Block.RegtestGenesisBlock.hash, 1 mbtc, 1000 msat, 0 sat, pr.paymentHash, secondsFromNow(60), None)
val p1 = PayToOpenRequest(Block.RegtestGenesisBlock.hash, 1 mbtc, 1000 msat, 0 sat, pr.paymentHash, secondsFromNow(60), None, 0.msat)
f.sender.send(handler, p1)
val r1 = f.sender.expectMsgType[PayToOpenResponse]
@ -522,7 +522,7 @@ class MultiPartHandlerSpec extends TestKitBaseClass with FixtureAnyFunSuiteLike
})
// Extraneous pay-to-opens will be ignored
val pExtra = PayToOpenRequest(Block.RegtestGenesisBlock.hash, 1 mbtc, 200 msat, 0 sat, pr.paymentHash, secondsFromNow(60), None)
val pExtra = PayToOpenRequest(Block.RegtestGenesisBlock.hash, 1 mbtc, 200 msat, 0 sat, pr.paymentHash, secondsFromNow(60), None, 0.msat)
f.sender.send(handler, MultiPartPaymentFSM.ExtraPaymentReceived(pr.paymentHash, PayToOpenPart(1000 msat, pExtra, f.sender.ref), None))
f.sender.expectNoMsg()
@ -543,7 +543,7 @@ class MultiPartHandlerSpec extends TestKitBaseClass with FixtureAnyFunSuiteLike
f.sender.send(handler, ReceivePayment(Some(20000000 msat), "1 fast coffee"))
val pr = f.sender.expectMsgType[PaymentRequest]
val p1 = PayToOpenRequest(Block.RegtestGenesisBlock.hash, funding, amount, fee, pr.paymentHash, secondsFromNow(60), None)
val p1 = PayToOpenRequest(Block.RegtestGenesisBlock.hash, funding, amount, fee, pr.paymentHash, secondsFromNow(60), None, 0.msat)
f.sender.send(handler, p1)
val e1 = eventListener.expectMsgType[PayToOpenRequestEvent]
@ -581,7 +581,7 @@ class MultiPartHandlerSpec extends TestKitBaseClass with FixtureAnyFunSuiteLike
f.sender.send(handler, ReceivePayment(Some(20000000 msat), "1 fast coffee"))
val pr = f.sender.expectMsgType[PaymentRequest]
val p1 = PayToOpenRequest(Block.RegtestGenesisBlock.hash, funding, amount, fee, pr.paymentHash, secondsFromNow(60), None)
val p1 = PayToOpenRequest(Block.RegtestGenesisBlock.hash, funding, amount, fee, pr.paymentHash, secondsFromNow(60), None, 0.msat)
f.sender.send(handler, p1)
val e1 = eventListener.expectMsgType[PayToOpenRequestEvent]
@ -609,7 +609,7 @@ class MultiPartHandlerSpec extends TestKitBaseClass with FixtureAnyFunSuiteLike
f.sender.send(handler, ReceivePayment(Some(20000000 msat), "1 fast coffee"))
val pr = f.sender.expectMsgType[PaymentRequest]
val p1 = PayToOpenRequest(Block.RegtestGenesisBlock.hash, funding, amount, fee, pr.paymentHash, secondsFromNow(2), None)
val p1 = PayToOpenRequest(Block.RegtestGenesisBlock.hash, funding, amount, fee, pr.paymentHash, secondsFromNow(2), None, 0.msat)
f.sender.send(handler, p1)
val e1 = eventListener.expectMsgType[PayToOpenRequestEvent]
@ -624,6 +624,67 @@ class MultiPartHandlerSpec extends TestKitBaseClass with FixtureAnyFunSuiteLike
f.sender.expectMsgType[PendingPayments].paymentHashes.isEmpty
}
test("reject single-part payment with pay-to-open if min amount not reached") { f =>
val nodeParams = Alice.nodeParams.copy(multiPartPaymentExpiry = 500 millis, features = Features(hex"028a8a"))
val handler = TestActorRef[PaymentHandler](PaymentHandler.props(nodeParams, f.register.ref))
val eventListener = TestProbe()
system.eventStream.subscribe(eventListener.ref, classOf[PayToOpenRequestEvent])
system.eventStream.subscribe(eventListener.ref, classOf[MissedPayToOpenPayment])
val amount = 9000000 msat
val fee = 1000 sat
val funding = PayToOpenRequest.computeFunding(amount, fee)
f.sender.send(handler, ReceivePayment(Some(9000000 msat), "1 fast coffee"))
val pr = f.sender.expectMsgType[PaymentRequest]
val p1 = PayToOpenRequest(Block.RegtestGenesisBlock.hash, funding, amount, fee, pr.paymentHash, secondsFromNow(60), None, 10000000.msat)
f.sender.send(handler, p1)
eventListener.expectMsg(MissedPayToOpenPayment(pr.paymentHash, amount, p1.payToOpenMinAmount))
val r1 = f.sender.expectMsgType[PayToOpenResponse]
assert(r1.paymentPreimage === ByteVector32.Zeroes)
f.sender.send(handler, GetPendingPayments)
f.sender.expectMsgType[PendingPayments].paymentHashes.isEmpty
}
test("reject multi-part payment with pay-to-open if min amount not reached") { f =>
val nodeParams = Alice.nodeParams.copy(multiPartPaymentExpiry = 500 millis, features = Features(hex"028a8a"))
val handler = TestActorRef[PaymentHandler](PaymentHandler.props(nodeParams, f.register.ref))
val eventListener = TestProbe()
system.eventStream.subscribe(eventListener.ref, classOf[PayToOpenRequestEvent])
system.eventStream.subscribe(eventListener.ref, classOf[MissedPayToOpenPayment])
f.sender.send(handler, ReceivePayment(Some(9000000 msat), "1 fast coffee"))
val pr = f.sender.expectMsgType[PaymentRequest]
val add = UpdateAddHtlc(ByteVector32.One, 0, 4000000 msat, pr.paymentHash, f.defaultExpiry, TestConstants.emptyOnionPacket)
f.sender.send(handler, IncomingPacket.FinalPacket(add, Onion.createMultiPartPayload(add.amountMsat, 9000000 msat, add.cltvExpiry, pr.paymentSecret.get)))
val amount = 5000000 msat
val fee = 1000 sat
val funding = PayToOpenRequest.computeFunding(amount, fee)
val payload = Onion.createMultiPartPayload(amount, 9000000 msat, f.defaultExpiry, pr.paymentSecret.get)
val onion = buildOnion(Sphinx.PaymentPacket)(nodeParams.nodeId :: Nil, payload :: Nil, pr.paymentHash).packet
val htlc = UpdateAddHtlc(ByteVector32.Zeroes, 0, payload.amount, pr.paymentHash, payload.expiry, onion)
val p1 = PayToOpenRequest(Block.RegtestGenesisBlock.hash, funding, amount, fee, pr.paymentHash, secondsFromNow(45), Some(htlc), 10000000.msat)
f.sender.send(handler, p1)
eventListener.expectMsg(MissedPayToOpenPayment(pr.paymentHash, 9000000 msat, p1.payToOpenMinAmount))
val cmd = f.register.expectMsgType[Register.Forward[CMD_FAIL_HTLC]].message
assert(cmd.reason == Right(IncorrectOrUnknownPaymentDetails(9000000 msat, nodeParams.currentBlockHeight)))
assert(nodeParams.db.payments.getIncomingPayment(pr.paymentHash).get.status === IncomingPaymentStatus.Pending)
val r1 = f.sender.expectMsgType[PayToOpenResponse]
assert(r1.paymentPreimage === ByteVector32.Zeroes)
f.sender.send(handler, GetPendingPayments)
f.sender.expectMsgType[PendingPayments].paymentHashes.isEmpty
}
def mixPaymentSuccess(f: FixtureParam, invoiceAmount: Option[MilliSatoshi]) = {
val nodeParams = Alice.nodeParams.copy(multiPartPaymentExpiry = 500 millis, features = featuresWithMpp)
val handler = TestActorRef[PaymentHandler](PaymentHandler.props(nodeParams, f.register.ref))
@ -645,7 +706,7 @@ class MultiPartHandlerSpec extends TestKitBaseClass with FixtureAnyFunSuiteLike
val payload1 = Onion.createMultiPartPayload(amount1, 100000000 msat, CltvExpiry(420000), pr.paymentSecret.get)
val onion1 = buildOnion(Sphinx.PaymentPacket)(nodeParams.nodeId :: Nil, payload1 :: Nil, pr.paymentHash).packet
val htlc1 = UpdateAddHtlc(ByteVector32.Zeroes, 0, payload1.amount, pr.paymentHash, payload1.expiry, onion1)
val p1 = PayToOpenRequest(Block.RegtestGenesisBlock.hash, funding1, amount1, fee1, pr.paymentHash, secondsFromNow(45), Some(htlc1))
val p1 = PayToOpenRequest(Block.RegtestGenesisBlock.hash, funding1, amount1, fee1, pr.paymentHash, secondsFromNow(45), Some(htlc1), 0.msat)
f.sender.send(handler, p1)
val amount2 = 20000000 msat
@ -654,7 +715,7 @@ class MultiPartHandlerSpec extends TestKitBaseClass with FixtureAnyFunSuiteLike
val payload2 = Onion.createMultiPartPayload(amount2, 100000000 msat, CltvExpiry(420000), pr.paymentSecret.get)
val onion2 = buildOnion(Sphinx.PaymentPacket)(nodeParams.nodeId :: Nil, payload2 :: Nil, pr.paymentHash).packet
val htlc2 = UpdateAddHtlc(ByteVector32.Zeroes, 0, payload2.amount, pr.paymentHash, payload2.expiry, onion2)
val p2 = PayToOpenRequest(Block.RegtestGenesisBlock.hash, funding2, amount2, fee2, pr.paymentHash, secondsFromNow(50), Some(htlc2))
val p2 = PayToOpenRequest(Block.RegtestGenesisBlock.hash, funding2, amount2, fee2, pr.paymentHash, secondsFromNow(50), Some(htlc2), 0.msat)
f.sender.send(handler, p2)
val amount3 = 10000000 msat
@ -663,7 +724,7 @@ class MultiPartHandlerSpec extends TestKitBaseClass with FixtureAnyFunSuiteLike
val payload3 = Onion.createMultiPartPayload(amount3, 100000000 msat, CltvExpiry(420000), pr.paymentSecret.get)
val onion3 = buildOnion(Sphinx.PaymentPacket)(nodeParams.nodeId :: Nil, payload3 :: Nil, pr.paymentHash).packet
val htlc3 = UpdateAddHtlc(ByteVector32.Zeroes, 0, payload3.amount, pr.paymentHash, payload3.expiry, onion3)
val p3 = PayToOpenRequest(Block.RegtestGenesisBlock.hash, funding3, amount3, fee3, pr.paymentHash, secondsFromNow(60), Some(htlc3))
val p3 = PayToOpenRequest(Block.RegtestGenesisBlock.hash, funding3, amount3, fee3, pr.paymentHash, secondsFromNow(60), Some(htlc3), 0.msat)
f.sender.send(handler, p3)
val payToOpenAmount = amount1 + amount2 + amount3
@ -678,7 +739,8 @@ class MultiPartHandlerSpec extends TestKitBaseClass with FixtureAnyFunSuiteLike
payToOpenFee = payToOpenFee,
paymentHash = p1.paymentHash,
expireAt = p1.expireAt,
htlc_opt = None
htlc_opt = None,
payToOpenMinAmount = 0.msat
))
assert(e1.peer === f.sender.ref)
e1.decision.success(true)

View file

@ -417,7 +417,7 @@ class LightningMessageCodecsSpec extends AnyFunSuite {
assert(bin === bin2)
}
test("non-reg pay-to-open") {
test("non-reg pay-to-open (optional htlc)") {
// we just need to make sure that old phoenix can decode new pay-to-open requests
case class OldPayToOpenRequest(chainHash: ByteVector32,
fundingSatoshis: Satoshi,
@ -434,25 +434,22 @@ class LightningMessageCodecsSpec extends AnyFunSuite {
("payToOpenFee" | satoshi) ::
("paymentHash" | bytes32)).as[OldPayToOpenRequest]
val p = PayToOpenRequest(randomBytes32, 12 mbtc, 12345 msat, 7 sat, randomBytes32, 1234567890L, Some(UpdateAddHtlc(randomBytes32, 42, 12345 msat, randomBytes32, CltvExpiry(420), TestConstants.emptyOnionPacket)))
val p = PayToOpenRequest(randomBytes32, 12 mbtc, 12345 msat, 7 sat, randomBytes32, 1234567890L, Some(UpdateAddHtlc(randomBytes32, 42, 12345 msat, randomBytes32, CltvExpiry(420), TestConstants.emptyOnionPacket)), 15000000 msat)
val bits = payToOpenRequestCodec.encode(p).require
val DecodeResult(oldp, remainder) = oldPayToOpenRequestCodec.decode(bits).require
assert(oldp === OldPayToOpenRequest(p.chainHash, p.fundingSatoshis, p.amountMsat, p.payToOpenFee, p.paymentHash))
assert(remainder.nonEmpty)
}
test("non-reg pay-to-open 2") {
// we just need to make sure that old phoenix can decode new pay-to-open requests
test("non-reg pay-to-open (min amount)") {
case class OldPayToOpenRequest(chainHash: ByteVector32,
fundingSatoshis: Satoshi,
amountMsat: MilliSatoshi,
feeSatoshis: Satoshi,
payToOpenFee: Satoshi,
paymentHash: ByteVector32,
feeThresholdSatoshis: Satoshi,
feeProportionalMillionths: Long,
expireAt: Long,
htlc_opt: Option[UpdateAddHtlc]
)
htlc_opt: Option[UpdateAddHtlc])
import fr.acinq.eclair.wire.CommonCodecs._
import scodec.codecs._
@ -462,16 +459,16 @@ class LightningMessageCodecsSpec extends AnyFunSuite {
("pushMsat" | millisatoshi) ::
("feeSatoshis" | satoshi) ::
("paymentHash" | bytes32) ::
("feeThresholdSatoshis" | satoshi) ::
("feeProportionalMillionths" | uint32) ::
("feeThresholdSatoshis" | satoshi.unit(Satoshi(0))) ::
("feeProportionalMillionths" | uint32.unit(0)) ::
("expireAt" | uint32) ::
("htlc_opt" | optional(bool(8), updateAddHtlcCodec))).as[OldPayToOpenRequest]
val p = OldPayToOpenRequest(randomBytes32, 12 mbtc, 12345 msat, 7 sat, randomBytes32, 10000 sat, 1000, 1234567890L, Some(UpdateAddHtlc(randomBytes32, 42, 12345 msat, randomBytes32, CltvExpiry(420), TestConstants.emptyOnionPacket)))
val bits = oldPayToOpenRequestCodec.encode(p).require
val DecodeResult(newp, remainder) = payToOpenRequestCodec.decode(bits).require
assert(newp === PayToOpenRequest(p.chainHash, p.fundingSatoshis, p.amountMsat, p.feeSatoshis, p.paymentHash, p.expireAt, p.htlc_opt))
assert(remainder.isEmpty)
val p = PayToOpenRequest(randomBytes32, 12 mbtc, 12345 msat, 7 sat, randomBytes32, 1234567890L, Some(UpdateAddHtlc(randomBytes32, 42, 12345 msat, randomBytes32, CltvExpiry(420), TestConstants.emptyOnionPacket)), 15000000 msat)
val bits = payToOpenRequestCodec.encode(p).require
val DecodeResult(oldp, remainder) = oldPayToOpenRequestCodec.decode(bits).require
assert(oldp === OldPayToOpenRequest(p.chainHash, p.fundingSatoshis, p.amountMsat, p.payToOpenFee, p.paymentHash, p.expireAt, p.htlc_opt))
assert(remainder.nonEmpty)
}
}