routerrpc: add validation to MPP params

Adds validation to ensure that MPP parameters are compatible
with the payment amount before attempting the payment. This
prevents payments from entering a path finding loop that
would eventually timeout.
Signed-off-by: Nishant Bansal <nishant.bansal.282003@gmail.com>
This commit is contained in:
Nishant Bansal authored and Oliver Gugger committed 2025-04-10 18:17:34 +02:00
1 parent 867d27d68a
commit cfab97c8be
2 files changed
+400

No files matched your search

+369
View File
@@ -7,8 +7,10 @@ import (
"testing"
"github.com/btcsuite/btcd/btcutil"
"github.com/btcsuite/btcd/chaincfg"
"github.com/lightningnetwork/lnd/lnrpc"
"github.com/lightningnetwork/lnd/lnwire"
"github.com/lightningnetwork/lnd/record"
"github.com/lightningnetwork/lnd/routing"
"github.com/lightningnetwork/lnd/routing/route"
"github.com/stretchr/testify/require"
@@ -475,3 +477,370 @@ func testUnmarshalAMP(t *testing.T, test unmarshalAMPTest) {
t.Fatalf("test case has non-standard outcome")
}
}
// extractIntentTestCase defines a test case for the
// TestExtractIntentFromSendRequest function. It includes the test name, the
// RouterBackend instance, the SendPaymentRequest to be tested, a boolean
// indicating if the test case is valid, and the expected error message if
// applicable.
type extractIntentTestCase struct {
name string
backend *RouterBackend
sendReq *SendPaymentRequest
valid bool
expectedErrorMsg string
}
// TestExtractIntentFromSendRequest verifies that extractIntentFromSendRequest
// correctly translates a SendPaymentRequest from an RPC client into a
// LightningPayment intent.
func TestExtractIntentFromSendRequest(t *testing.T) {
const paymentAmount = btcutil.Amount(300_000)
const paymentReq = "lnbcrt500u1pnh0xflpp56w08q26t896vg2e9mtdkrem320tp" +
"wws9z9sfr7dw86dx97d90u4sdqqcqzzsxqyz5vqsp5z9945kvfy5g9afmakz" +
"yrur2t4hhn2tr87un8j0r0e6l5m5zm0fus9qxpqysgqk98c6j7qefdpdmzt4" +
"g6aykds4ydvf2x9lpngqcfux3hv8qlraan9v3s9296r5w5eh959yzadgh5ck" +
"gjydgyfxdpumxtuk3p3caugmlqpz5necs"
destNodeBytes, err := hex.DecodeString(destKey)
require.NoError(t, err)
target, err := route.NewVertexFromBytes(destNodeBytes)
require.NoError(t, err)
testCases := []extractIntentTestCase{
{
name: "Time preference out of range",
backend: &RouterBackend{},
sendReq: &SendPaymentRequest{
TimePref: 2,
},
valid: false,
expectedErrorMsg: "time preference out of range",
},
{
name: "Outgoing channel exclusivity violation",
backend: &RouterBackend{},
sendReq: &SendPaymentRequest{
OutgoingChanId: 38484,
OutgoingChanIds: []uint64{383322},
},
valid: false,
expectedErrorMsg: "outgoing_chan_id and " +
"outgoing_chan_ids are mutually exclusive",
},
{
name: "Invalid last hop pubkey length",
backend: &RouterBackend{},
sendReq: &SendPaymentRequest{
LastHopPubkey: []byte{1},
},
valid: false,
expectedErrorMsg: "invalid vertex length",
},
{
name: "total time lock exceeds max allowed",
backend: &RouterBackend{
MaxTotalTimelock: 1000,
},
sendReq: &SendPaymentRequest{
CltvLimit: 1001,
},
valid: false,
expectedErrorMsg: "total time lock of 1001 exceeds " +
"max allowed 1000",
},
{
name: "Max parts exceed allowed limit",
backend: &RouterBackend{},
sendReq: &SendPaymentRequest{
MaxParts: 1001,
MaxShardSizeMsat: 300_000,
},
valid: false,
expectedErrorMsg: "requested max_parts (1001) exceeds" +
" the allowed upper limit",
},
{
name: "Fee limit conflict, both sat and msat " +
"specified",
backend: &RouterBackend{},
sendReq: &SendPaymentRequest{
FeeLimitSat: 1000000,
FeeLimitMsat: 1000000000,
},
valid: false,
expectedErrorMsg: "sat and msat arguments are " +
"mutually exclusive",
},
{
name: "Fee limit cannot be negative",
backend: &RouterBackend{},
sendReq: &SendPaymentRequest{
FeeLimitSat: -1,
},
valid: false,
expectedErrorMsg: "amount cannot be negative",
},
{
name: "Dest custom records with type below minimum" +
" range",
backend: &RouterBackend{},
sendReq: &SendPaymentRequest{
DestCustomRecords: map[uint64][]byte{
65530: {1, 2},
},
},
valid: false,
expectedErrorMsg: "no custom records with types below",
},
{
name: "MPP params with keysend payments",
backend: &RouterBackend{},
sendReq: &SendPaymentRequest{
DestCustomRecords: map[uint64][]byte{
record.KeySendType: {1, 2},
},
MaxShardSizeMsat: 300_000,
},
valid: false,
expectedErrorMsg: "MPP not supported with keysend " +
"payments",
},
{
name: "Custom record entry with TLV type below " +
"minimum range",
backend: &RouterBackend{},
sendReq: &SendPaymentRequest{
FirstHopCustomRecords: map[uint64][]byte{
65530: {1, 2},
},
},
valid: false,
expectedErrorMsg: "custom records entry with TLV type",
},
{
name: "Amount conflict, both sat and msat specified",
backend: &RouterBackend{
ShouldSetExpEndorsement: func() bool {
return true
},
},
sendReq: &SendPaymentRequest{
Amt: int64(paymentAmount),
AmtMsat: int64(paymentAmount) * 1000,
},
valid: false,
expectedErrorMsg: "sat and msat arguments are " +
"mutually exclusive",
},
{
name: "Both dest and payment_request provided",
backend: &RouterBackend{
ShouldSetExpEndorsement: func() bool {
return false
},
},
sendReq: &SendPaymentRequest{
Amt: int64(paymentAmount),
PaymentRequest: "test",
Dest: destNodeBytes,
},
valid: false,
expectedErrorMsg: "dest and payment_request " +
"cannot appear together",
},
{
name: "Both payment_hash and payment_request provided",
backend: &RouterBackend{
ShouldSetExpEndorsement: func() bool {
return false
},
},
sendReq: &SendPaymentRequest{
Amt: int64(paymentAmount),
PaymentRequest: "test",
PaymentHash: make([]byte, 32),
},
valid: false,
expectedErrorMsg: "payment_hash and payment_request " +
"cannot appear together",
},
{
name: "Both final_cltv_delta and payment_request " +
"provided",
backend: &RouterBackend{
ShouldSetExpEndorsement: func() bool {
return false
},
},
sendReq: &SendPaymentRequest{
Amt: int64(paymentAmount),
PaymentRequest: "test",
FinalCltvDelta: 100,
},
valid: false,
expectedErrorMsg: "final_cltv_delta and " +
"payment_request cannot appear together",
},
{
name: "Invalid payment request length",
backend: &RouterBackend{
ShouldSetExpEndorsement: func() bool {
return false
},
ActiveNetParams: &chaincfg.RegressionNetParams,
},
sendReq: &SendPaymentRequest{
Amt: int64(paymentAmount),
PaymentRequest: "test",
},
valid: false,
expectedErrorMsg: "invalid bech32 string length",
},
{
name: "Expired invoice payment request",
backend: &RouterBackend{
ShouldSetExpEndorsement: func() bool {
return false
},
ActiveNetParams: &chaincfg.RegressionNetParams,
},
sendReq: &SendPaymentRequest{
Amt: int64(paymentAmount),
PaymentRequest: paymentReq,
},
valid: false,
expectedErrorMsg: "invoice expired.",
},
{
name: "Invalid dest vertex length",
backend: &RouterBackend{
ShouldSetExpEndorsement: func() bool {
return false
},
},
sendReq: &SendPaymentRequest{
Amt: int64(paymentAmount),
Dest: []byte{1},
},
valid: false,
expectedErrorMsg: "invalid vertex length",
},
{
name: "Payment request with missing amount",
backend: &RouterBackend{
ShouldSetExpEndorsement: func() bool {
return false
},
},
sendReq: &SendPaymentRequest{
Dest: destNodeBytes,
FinalCltvDelta: 100,
},
valid: false,
expectedErrorMsg: "amount must be specified",
},
{
name: "Destination lacks AMP support",
backend: &RouterBackend{
ShouldSetExpEndorsement: func() bool {
return false
},
},
sendReq: &SendPaymentRequest{
Dest: destNodeBytes,
Amt: int64(paymentAmount),
Amp: true,
DestFeatures: []lnrpc.FeatureBit{},
},
valid: false,
expectedErrorMsg: "destination doesn't " +
"support AMP payments",
},
{
name: "Invalid payment hash length",
backend: &RouterBackend{
ShouldSetExpEndorsement: func() bool {
return false
},
},
sendReq: &SendPaymentRequest{
Dest: destNodeBytes,
Amt: int64(paymentAmount),
PaymentHash: make([]byte, 1),
},
valid: false,
expectedErrorMsg: "invalid hash length",
},
{
name: "Payment amount exceeds maximum possible amount",
backend: &RouterBackend{
ShouldSetExpEndorsement: func() bool {
return false
},
},
sendReq: &SendPaymentRequest{
Dest: destNodeBytes,
Amt: int64(paymentAmount),
PaymentHash: make([]byte, 32),
MaxParts: 10,
MaxShardSizeMsat: 300_000,
},
valid: false,
expectedErrorMsg: "payment amount 300000000 mSAT " +
"exceeds maximum possible amount",
},
{
name: "Reject self-payments if not permitted",
backend: &RouterBackend{
MaxTotalTimelock: 1000,
ShouldSetExpEndorsement: func() bool {
return false
},
SelfNode: target,
},
sendReq: &SendPaymentRequest{
Dest: destNodeBytes,
Amt: int64(paymentAmount),
PaymentHash: make([]byte, 32),
},
valid: false,
expectedErrorMsg: "self-payments not allowed",
},
{
name: "Valid send req parameters, payment settled",
backend: &RouterBackend{
MaxTotalTimelock: 1000,
ShouldSetExpEndorsement: func() bool {
return false
},
},
sendReq: &SendPaymentRequest{
Dest: destNodeBytes,
Amt: int64(paymentAmount),
PaymentHash: make([]byte, 32),
MaxParts: 10,
MaxShardSizeMsat: 30_000_000,
},
valid: true,
},
}
for _, test := range testCases {
t.Run(test.name, func(t *testing.T) {
t.Parallel()
_, err := test.backend.
extractIntentFromSendRequest(test.sendReq)
if test.valid {
require.NoError(t, err)
} else {
require.ErrorContains(t, err,
test.expectedErrorMsg)
}
})
}
}