mirror of
https://github.com/lightningnetwork/lnd.git
synced 2025-10-09 08:19:18 +02:00
This commit starts reusing test cases which are not dependant on the kv db backend. So they can be later used with the native db implementation as well.
1265 lines
34 KiB
Go
1265 lines
34 KiB
Go
package paymentsdb
|
|
|
|
import (
|
|
"context"
|
|
"crypto/rand"
|
|
"crypto/sha256"
|
|
"errors"
|
|
"fmt"
|
|
"io"
|
|
"reflect"
|
|
"testing"
|
|
"time"
|
|
|
|
"github.com/btcsuite/btcd/btcec/v2"
|
|
"github.com/btcsuite/btcd/chaincfg/chainhash"
|
|
"github.com/davecgh/go-spew/spew"
|
|
"github.com/lightningnetwork/lnd/lntypes"
|
|
"github.com/lightningnetwork/lnd/lnwire"
|
|
"github.com/lightningnetwork/lnd/record"
|
|
"github.com/lightningnetwork/lnd/routing/route"
|
|
"github.com/stretchr/testify/require"
|
|
)
|
|
|
|
var (
|
|
testHash = [32]byte{
|
|
0xb7, 0x94, 0x38, 0x5f, 0x2d, 0x1e, 0xf7, 0xab,
|
|
0x4d, 0x92, 0x73, 0xd1, 0x90, 0x63, 0x81, 0xb4,
|
|
0x4f, 0x2f, 0x6f, 0x25, 0x88, 0xa3, 0xef, 0xb9,
|
|
0x6a, 0x49, 0x18, 0x83, 0x31, 0x98, 0x47, 0x53,
|
|
}
|
|
|
|
rev = [chainhash.HashSize]byte{
|
|
0x51, 0xb6, 0x37, 0xd8, 0xfc, 0xd2, 0xc6, 0xda,
|
|
0x48, 0x59, 0xe6, 0x96, 0x31, 0x13, 0xa1, 0x17,
|
|
0x2d, 0xe7, 0x93, 0xe4,
|
|
}
|
|
)
|
|
|
|
var (
|
|
priv, _ = btcec.NewPrivateKey()
|
|
pub = priv.PubKey()
|
|
vertex = route.NewVertex(pub)
|
|
|
|
testHop1 = &route.Hop{
|
|
PubKeyBytes: vertex,
|
|
ChannelID: 12345,
|
|
OutgoingTimeLock: 111,
|
|
AmtToForward: 555,
|
|
CustomRecords: record.CustomSet{
|
|
65536: []byte{},
|
|
80001: []byte{},
|
|
},
|
|
MPP: record.NewMPP(32, [32]byte{0x42}),
|
|
Metadata: []byte{1, 2, 3},
|
|
}
|
|
|
|
testHop2 = &route.Hop{
|
|
PubKeyBytes: vertex,
|
|
ChannelID: 12345,
|
|
OutgoingTimeLock: 111,
|
|
AmtToForward: 555,
|
|
LegacyPayload: true,
|
|
}
|
|
|
|
testRoute = route.Route{
|
|
TotalTimeLock: 123,
|
|
TotalAmount: 1234567,
|
|
SourcePubKey: vertex,
|
|
Hops: []*route.Hop{
|
|
testHop2,
|
|
testHop1,
|
|
},
|
|
}
|
|
|
|
testBlindedRoute = route.Route{
|
|
TotalTimeLock: 150,
|
|
TotalAmount: 1000,
|
|
SourcePubKey: vertex,
|
|
Hops: []*route.Hop{
|
|
{
|
|
PubKeyBytes: vertex,
|
|
ChannelID: 9876,
|
|
OutgoingTimeLock: 120,
|
|
AmtToForward: 900,
|
|
EncryptedData: []byte{1, 3, 3},
|
|
BlindingPoint: pub,
|
|
},
|
|
{
|
|
PubKeyBytes: vertex,
|
|
EncryptedData: []byte{3, 2, 1},
|
|
},
|
|
{
|
|
PubKeyBytes: vertex,
|
|
Metadata: []byte{4, 5, 6},
|
|
AmtToForward: 500,
|
|
OutgoingTimeLock: 100,
|
|
TotalAmtMsat: 500,
|
|
},
|
|
},
|
|
}
|
|
)
|
|
|
|
// payment is a helper structure that holds basic information on a test payment,
|
|
// such as the payment id, the status and the total number of HTLCs attempted.
|
|
type payment struct {
|
|
id lntypes.Hash
|
|
status PaymentStatus
|
|
htlcs int
|
|
}
|
|
|
|
// createTestPayments registers payments depending on the provided statuses in
|
|
// the payments slice. Each payment will receive one failed HTLC and another
|
|
// HTLC depending on the final status of the payment provided.
|
|
func createTestPayments(t *testing.T, p DB, payments []*payment) {
|
|
attemptID := uint64(0)
|
|
|
|
for i := 0; i < len(payments); i++ {
|
|
info, attempt, preimg, err := genInfo(t)
|
|
require.NoError(t, err, "unable to generate htlc message")
|
|
|
|
// Set the payment id accordingly in the payments slice.
|
|
payments[i].id = info.PaymentIdentifier
|
|
|
|
attempt.AttemptID = attemptID
|
|
attemptID++
|
|
|
|
// Init the payment.
|
|
err = p.InitPayment(info.PaymentIdentifier, info)
|
|
require.NoError(t, err, "unable to send htlc message")
|
|
|
|
// Register and fail the first attempt for all payments.
|
|
_, err = p.RegisterAttempt(info.PaymentIdentifier, attempt)
|
|
require.NoError(t, err, "unable to send htlc message")
|
|
|
|
htlcFailure := HTLCFailUnreadable
|
|
_, err = p.FailAttempt(
|
|
info.PaymentIdentifier, attempt.AttemptID,
|
|
&HTLCFailInfo{
|
|
Reason: htlcFailure,
|
|
},
|
|
)
|
|
require.NoError(t, err, "unable to fail htlc")
|
|
|
|
// Increase the HTLC counter in the payments slice for the
|
|
// failed attempt.
|
|
payments[i].htlcs++
|
|
|
|
// Depending on the test case, fail or succeed the next
|
|
// attempt.
|
|
attempt.AttemptID = attemptID
|
|
attemptID++
|
|
|
|
_, err = p.RegisterAttempt(info.PaymentIdentifier, attempt)
|
|
require.NoError(t, err, "unable to send htlc message")
|
|
|
|
switch payments[i].status {
|
|
// Fail the attempt and the payment overall.
|
|
case StatusFailed:
|
|
htlcFailure := HTLCFailUnreadable
|
|
_, err = p.FailAttempt(
|
|
info.PaymentIdentifier, attempt.AttemptID,
|
|
&HTLCFailInfo{
|
|
Reason: htlcFailure,
|
|
},
|
|
)
|
|
require.NoError(t, err, "unable to fail htlc")
|
|
|
|
failReason := FailureReasonNoRoute
|
|
_, err = p.Fail(info.PaymentIdentifier,
|
|
failReason)
|
|
require.NoError(t, err, "unable to fail payment hash")
|
|
|
|
// Settle the attempt
|
|
case StatusSucceeded:
|
|
_, err := p.SettleAttempt(
|
|
info.PaymentIdentifier, attempt.AttemptID,
|
|
&HTLCSettleInfo{
|
|
Preimage: preimg,
|
|
},
|
|
)
|
|
require.NoError(t, err, "no error should have been "+
|
|
"received from settling a htlc attempt")
|
|
|
|
// We leave the attempt in-flight by doing nothing.
|
|
case StatusInFlight:
|
|
}
|
|
|
|
// Increase the HTLC counter in the payments slice for any
|
|
// attempt above.
|
|
payments[i].htlcs++
|
|
}
|
|
}
|
|
|
|
// assertRouteEquals compares to routes for equality and returns an error if
|
|
// they are not equal.
|
|
func assertRouteEqual(a, b *route.Route) error {
|
|
if !reflect.DeepEqual(a, b) {
|
|
return fmt.Errorf("HTLCAttemptInfos don't match: %v vs %v",
|
|
spew.Sdump(a), spew.Sdump(b))
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
// assertPaymentInfo retrieves the payment referred to by hash and verifies the
|
|
// expected values.
|
|
func assertPaymentInfo(t *testing.T, p DB, hash lntypes.Hash,
|
|
c *PaymentCreationInfo, f *FailureReason,
|
|
a *htlcStatus) {
|
|
|
|
t.Helper()
|
|
|
|
payment, err := p.FetchPayment(hash)
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
|
|
if !reflect.DeepEqual(payment.Info, c) {
|
|
t.Fatalf("PaymentCreationInfos don't match: %v vs %v",
|
|
spew.Sdump(payment.Info), spew.Sdump(c))
|
|
}
|
|
|
|
if f != nil {
|
|
if *payment.FailureReason != *f {
|
|
t.Fatal("unexpected failure reason")
|
|
}
|
|
} else {
|
|
if payment.FailureReason != nil {
|
|
t.Fatal("unexpected failure reason")
|
|
}
|
|
}
|
|
|
|
if a == nil {
|
|
if len(payment.HTLCs) > 0 {
|
|
t.Fatal("expected no htlcs")
|
|
}
|
|
|
|
return
|
|
}
|
|
|
|
htlc := payment.HTLCs[a.AttemptID]
|
|
if err := assertRouteEqual(&htlc.Route, &a.Route); err != nil {
|
|
t.Fatal("routes do not match")
|
|
}
|
|
|
|
if htlc.AttemptID != a.AttemptID {
|
|
t.Fatalf("unnexpected attempt ID %v, expected %v",
|
|
htlc.AttemptID, a.AttemptID)
|
|
}
|
|
|
|
if a.failure != nil {
|
|
if htlc.Failure == nil {
|
|
t.Fatalf("expected HTLC to be failed")
|
|
}
|
|
|
|
if htlc.Failure.Reason != *a.failure {
|
|
t.Fatalf("expected HTLC failure %v, had %v",
|
|
*a.failure, htlc.Failure.Reason)
|
|
}
|
|
} else if htlc.Failure != nil {
|
|
t.Fatalf("expected no HTLC failure")
|
|
}
|
|
|
|
if a.settle != nil {
|
|
if htlc.Settle.Preimage != *a.settle {
|
|
t.Fatalf("Preimages don't match: %x vs %x",
|
|
htlc.Settle.Preimage, a.settle)
|
|
}
|
|
} else if htlc.Settle != nil {
|
|
t.Fatal("expected no settle info")
|
|
}
|
|
}
|
|
|
|
// assertPaymentStatus retrieves the status of the payment referred to by hash
|
|
// and compares it with the expected state.
|
|
func assertPaymentStatus(t *testing.T, p DB, hash lntypes.Hash,
|
|
expStatus PaymentStatus) {
|
|
|
|
t.Helper()
|
|
|
|
payment, err := p.FetchPayment(hash)
|
|
if errors.Is(err, ErrPaymentNotInitiated) {
|
|
return
|
|
}
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
|
|
if payment.Status != expStatus {
|
|
t.Fatalf("payment status mismatch: expected %v, got %v",
|
|
expStatus, payment.Status)
|
|
}
|
|
}
|
|
|
|
// assertPayments is a helper function that given a slice of payment and
|
|
// indices for the slice asserts that exactly the same payments in the
|
|
// slice for the provided indices exist when fetching payments from the
|
|
// database.
|
|
func assertPayments(t *testing.T, paymentDB DB, payments []*payment) {
|
|
t.Helper()
|
|
|
|
response, err := paymentDB.QueryPayments(
|
|
context.Background(), Query{
|
|
IndexOffset: 0,
|
|
MaxPayments: uint64(len(payments)),
|
|
IncludeIncomplete: true,
|
|
},
|
|
)
|
|
require.NoError(t, err, "could not fetch payments from db")
|
|
|
|
dbPayments := response.Payments
|
|
|
|
// Make sure that the number of fetched payments is the same
|
|
// as expected.
|
|
require.Len(
|
|
t, dbPayments, len(payments), "unexpected number of payments",
|
|
)
|
|
|
|
// Convert fetched payments of type MPPayment to our helper structure.
|
|
p := make([]*payment, len(dbPayments))
|
|
for i, dbPayment := range dbPayments {
|
|
p[i] = &payment{
|
|
id: dbPayment.Info.PaymentIdentifier,
|
|
status: dbPayment.Status,
|
|
htlcs: len(dbPayment.HTLCs),
|
|
}
|
|
}
|
|
|
|
// Check that each payment we want to assert exists in the database.
|
|
require.Equal(t, payments, p)
|
|
}
|
|
|
|
func genPreimage() ([32]byte, error) {
|
|
var preimage [32]byte
|
|
if _, err := io.ReadFull(rand.Reader, preimage[:]); err != nil {
|
|
return preimage, err
|
|
}
|
|
|
|
return preimage, nil
|
|
}
|
|
|
|
func genInfo(t *testing.T) (*PaymentCreationInfo, *HTLCAttemptInfo,
|
|
lntypes.Preimage, error) {
|
|
|
|
preimage, err := genPreimage()
|
|
if err != nil {
|
|
return nil, nil, preimage, fmt.Errorf("unable to "+
|
|
"generate preimage: %v", err)
|
|
}
|
|
|
|
rhash := sha256.Sum256(preimage[:])
|
|
var hash lntypes.Hash
|
|
copy(hash[:], rhash[:])
|
|
|
|
attempt, err := NewHtlcAttempt(
|
|
0, priv, *testRoute.Copy(), time.Time{}, &hash,
|
|
)
|
|
require.NoError(t, err)
|
|
|
|
return &PaymentCreationInfo{
|
|
PaymentIdentifier: rhash,
|
|
Value: testRoute.ReceiverAmt(),
|
|
CreationTime: time.Unix(time.Now().Unix(), 0),
|
|
PaymentRequest: []byte("hola"),
|
|
}, &attempt.HTLCAttemptInfo, preimage, nil
|
|
}
|
|
|
|
func testDeleteFailedAttempts(t *testing.T, keepFailedPaymentAttempts bool) {
|
|
paymentDB := NewTestDB(
|
|
t, WithKeepFailedPaymentAttempts(keepFailedPaymentAttempts),
|
|
)
|
|
|
|
// Register three payments:
|
|
// All payments will have one failed HTLC attempt and one HTLC attempt
|
|
// according to its final status.
|
|
// 1. A payment with two failed attempts.
|
|
// 2. A payment with one failed and one in-flight attempt.
|
|
// 3. A payment with one failed and one settled attempt.
|
|
|
|
// Initiate payments, which is a slice of payment that is used as
|
|
// template to create the corresponding test payments in the database.
|
|
//
|
|
// Note: The payment id and number of htlc attempts of each payment will
|
|
// be added to this slice when creating the payments below.
|
|
// This allows the slice to be used directly for testing purposes.
|
|
payments := []*payment{
|
|
{status: StatusFailed},
|
|
{status: StatusInFlight},
|
|
{status: StatusSucceeded},
|
|
}
|
|
|
|
// Use helper function to register the test payments in the data and
|
|
// populate the data to the payments slice.
|
|
createTestPayments(t, paymentDB, payments)
|
|
|
|
// Check that all payments are there as we added them.
|
|
assertPayments(t, paymentDB, payments)
|
|
|
|
// Calling DeleteFailedAttempts on a failed payment should delete all
|
|
// HTLCs.
|
|
require.NoError(t, paymentDB.DeleteFailedAttempts(payments[0].id))
|
|
|
|
// Expect all HTLCs to be deleted if the config is set to delete them.
|
|
if !keepFailedPaymentAttempts {
|
|
payments[0].htlcs = 0
|
|
}
|
|
assertPayments(t, paymentDB, payments)
|
|
|
|
// Calling DeleteFailedAttempts on an in-flight payment should return
|
|
// an error.
|
|
//
|
|
// NOTE: In case the option keepFailedPaymentAttempts is set no delete
|
|
// operation are performed in general therefore we do NOT expect an
|
|
// error in this case.
|
|
if keepFailedPaymentAttempts {
|
|
require.NoError(
|
|
t, paymentDB.DeleteFailedAttempts(payments[1].id),
|
|
)
|
|
} else {
|
|
require.Error(t, paymentDB.DeleteFailedAttempts(payments[1].id))
|
|
}
|
|
|
|
// Since DeleteFailedAttempts returned an error, we should expect the
|
|
// payment to be unchanged.
|
|
assertPayments(t, paymentDB, payments)
|
|
|
|
// Cleaning up a successful payment should remove failed htlcs.
|
|
require.NoError(t, paymentDB.DeleteFailedAttempts(payments[2].id))
|
|
|
|
// Expect all HTLCs except for the settled one to be deleted if the
|
|
// config is set to delete them.
|
|
if !keepFailedPaymentAttempts {
|
|
payments[2].htlcs = 1
|
|
}
|
|
assertPayments(t, paymentDB, payments)
|
|
|
|
// NOTE: In case the option keepFailedPaymentAttempts is set no delete
|
|
// operation are performed in general therefore we do NOT expect an
|
|
// error in this case.
|
|
if keepFailedPaymentAttempts {
|
|
// DeleteFailedAttempts is ignored, even for non-existent
|
|
// payments, if the control tower is configured to keep failed
|
|
// HTLCs.
|
|
require.NoError(
|
|
t, paymentDB.DeleteFailedAttempts(lntypes.ZeroHash),
|
|
)
|
|
} else {
|
|
// Attempting to cleanup a non-existent payment returns an
|
|
// error.
|
|
require.Error(
|
|
t, paymentDB.DeleteFailedAttempts(lntypes.ZeroHash),
|
|
)
|
|
}
|
|
}
|
|
|
|
// TestKVPaymentsDBMPPRecordValidation tests MPP record validation.
|
|
func TestKVPaymentsDBMPPRecordValidation(t *testing.T) {
|
|
t.Parallel()
|
|
|
|
paymentDB := NewTestDB(t)
|
|
|
|
info, attempt, _, err := genInfo(t)
|
|
require.NoError(t, err, "unable to generate htlc message")
|
|
|
|
// Init the payment.
|
|
err = paymentDB.InitPayment(info.PaymentIdentifier, info)
|
|
require.NoError(t, err, "unable to send htlc message")
|
|
|
|
// Create three unique attempts we'll use for the test, and
|
|
// register them with the payment control. We set each
|
|
// attempts's value to one third of the payment amount, and
|
|
// populate the MPP options.
|
|
shardAmt := info.Value / 3
|
|
attempt.Route.FinalHop().AmtToForward = shardAmt
|
|
attempt.Route.FinalHop().MPP = record.NewMPP(
|
|
info.Value, [32]byte{1},
|
|
)
|
|
|
|
_, err = paymentDB.RegisterAttempt(info.PaymentIdentifier, attempt)
|
|
require.NoError(t, err, "unable to send htlc message")
|
|
|
|
// Now try to register a non-MPP attempt, which should fail.
|
|
b := *attempt
|
|
b.AttemptID = 1
|
|
b.Route.FinalHop().MPP = nil
|
|
_, err = paymentDB.RegisterAttempt(info.PaymentIdentifier, &b)
|
|
require.ErrorIs(t, err, ErrMPPayment)
|
|
|
|
// Try to register attempt one with a different payment address.
|
|
b.Route.FinalHop().MPP = record.NewMPP(
|
|
info.Value, [32]byte{2},
|
|
)
|
|
_, err = paymentDB.RegisterAttempt(info.PaymentIdentifier, &b)
|
|
require.ErrorIs(t, err, ErrMPPPaymentAddrMismatch)
|
|
|
|
// Try registering one with a different total amount.
|
|
b.Route.FinalHop().MPP = record.NewMPP(
|
|
info.Value/2, [32]byte{1},
|
|
)
|
|
_, err = paymentDB.RegisterAttempt(info.PaymentIdentifier, &b)
|
|
require.ErrorIs(t, err, ErrMPPTotalAmountMismatch)
|
|
|
|
// Create and init a new payment. This time we'll check that we cannot
|
|
// register an MPP attempt if we already registered a non-MPP one.
|
|
info, attempt, _, err = genInfo(t)
|
|
require.NoError(t, err, "unable to generate htlc message")
|
|
|
|
err = paymentDB.InitPayment(info.PaymentIdentifier, info)
|
|
require.NoError(t, err, "unable to send htlc message")
|
|
|
|
attempt.Route.FinalHop().MPP = nil
|
|
_, err = paymentDB.RegisterAttempt(info.PaymentIdentifier, attempt)
|
|
require.NoError(t, err, "unable to send htlc message")
|
|
|
|
// Attempt to register an MPP attempt, which should fail.
|
|
b = *attempt
|
|
b.AttemptID = 1
|
|
b.Route.FinalHop().MPP = record.NewMPP(
|
|
info.Value, [32]byte{1},
|
|
)
|
|
|
|
_, err = paymentDB.RegisterAttempt(info.PaymentIdentifier, &b)
|
|
require.ErrorIs(t, err, ErrNonMPPayment)
|
|
}
|
|
|
|
// TestDeleteSinglePayment tests that DeletePayment correctly
|
|
// deletes information about a completed payment from the database.
|
|
func TestDeleteSinglePayment(t *testing.T) {
|
|
t.Parallel()
|
|
|
|
paymentDB := NewTestDB(t)
|
|
|
|
// Register four payments:
|
|
// All payments will have one failed HTLC attempt and one HTLC attempt
|
|
// according to its final status.
|
|
// 1. A payment with two failed attempts.
|
|
// 2. Another payment with two failed attempts.
|
|
// 3. A payment with one failed and one settled attempt.
|
|
// 4. A payment with one failed and one in-flight attempt.
|
|
|
|
// Initiate payments, which is a slice of payment that is used as
|
|
// template to create the corresponding test payments in the database.
|
|
//
|
|
// Note: The payment id and number of htlc attempts of each payment will
|
|
// be added to this slice when creating the payments below.
|
|
// This allows the slice to be used directly for testing purposes.
|
|
payments := []*payment{
|
|
{status: StatusFailed},
|
|
{status: StatusFailed},
|
|
{status: StatusSucceeded},
|
|
{status: StatusInFlight},
|
|
}
|
|
|
|
// Use helper function to register the test payments in the data and
|
|
// populate the data to the payments slice.
|
|
createTestPayments(t, paymentDB, payments)
|
|
|
|
// Check that all payments are there as we added them.
|
|
assertPayments(t, paymentDB, payments)
|
|
|
|
// Delete HTLC attempts for first payment only.
|
|
require.NoError(t, paymentDB.DeletePayment(payments[0].id, true))
|
|
|
|
// The first payment is the only altered one as its failed HTLC should
|
|
// have been removed but is still present as payment.
|
|
payments[0].htlcs = 0
|
|
assertPayments(t, paymentDB, payments)
|
|
|
|
// Delete the first payment completely.
|
|
require.NoError(t, paymentDB.DeletePayment(payments[0].id, false))
|
|
|
|
// The first payment should have been deleted.
|
|
assertPayments(t, paymentDB, payments[1:])
|
|
|
|
// Now delete the second payment completely.
|
|
require.NoError(t, paymentDB.DeletePayment(payments[1].id, false))
|
|
|
|
// The Second payment should have been deleted.
|
|
assertPayments(t, paymentDB, payments[2:])
|
|
|
|
// Delete failed HTLC attempts for the third payment.
|
|
require.NoError(t, paymentDB.DeletePayment(payments[2].id, true))
|
|
|
|
// Only the successful HTLC attempt should be left for the third
|
|
// payment.
|
|
payments[2].htlcs = 1
|
|
assertPayments(t, paymentDB, payments[2:])
|
|
|
|
// Now delete the third payment completely.
|
|
require.NoError(t, paymentDB.DeletePayment(payments[2].id, false))
|
|
|
|
// Only the last payment should be left.
|
|
assertPayments(t, paymentDB, payments[3:])
|
|
|
|
// Deleting HTLC attempts from InFlight payments should not work and an
|
|
// error returned.
|
|
require.Error(t, paymentDB.DeletePayment(payments[3].id, true))
|
|
|
|
// The payment is InFlight and therefore should not have been altered.
|
|
assertPayments(t, paymentDB, payments[3:])
|
|
|
|
// Finally deleting the InFlight payment should also not work and an
|
|
// error returned.
|
|
require.Error(t, paymentDB.DeletePayment(payments[3].id, false))
|
|
|
|
// The payment is InFlight and therefore should not have been altered.
|
|
assertPayments(t, paymentDB, payments[3:])
|
|
}
|
|
|
|
// TestPaymentRegistrable checks the method `Registrable` behaves as expected
|
|
// for ALL possible payment statuses.
|
|
func TestPaymentRegistrable(t *testing.T) {
|
|
t.Parallel()
|
|
|
|
testCases := []struct {
|
|
status PaymentStatus
|
|
registryErr error
|
|
hasSettledHTLC bool
|
|
paymentFailed bool
|
|
}{
|
|
{
|
|
status: StatusInitiated,
|
|
registryErr: nil,
|
|
},
|
|
{
|
|
// Test inflight status with no settled HTLC and no
|
|
// failed payment.
|
|
status: StatusInFlight,
|
|
registryErr: nil,
|
|
},
|
|
{
|
|
// Test inflight status with settled HTLC but no failed
|
|
// payment.
|
|
status: StatusInFlight,
|
|
registryErr: ErrPaymentPendingSettled,
|
|
hasSettledHTLC: true,
|
|
},
|
|
{
|
|
// Test inflight status with no settled HTLC but failed
|
|
// payment.
|
|
status: StatusInFlight,
|
|
registryErr: ErrPaymentPendingFailed,
|
|
paymentFailed: true,
|
|
},
|
|
{
|
|
// Test error state with settled HTLC and failed
|
|
// payment.
|
|
status: 0,
|
|
registryErr: ErrUnknownPaymentStatus,
|
|
hasSettledHTLC: true,
|
|
paymentFailed: true,
|
|
},
|
|
{
|
|
status: StatusSucceeded,
|
|
registryErr: ErrPaymentAlreadySucceeded,
|
|
},
|
|
{
|
|
status: StatusFailed,
|
|
registryErr: ErrPaymentAlreadyFailed,
|
|
},
|
|
{
|
|
status: 0,
|
|
registryErr: ErrUnknownPaymentStatus,
|
|
},
|
|
}
|
|
|
|
for i, tc := range testCases {
|
|
i, tc := i, tc
|
|
|
|
p := &MPPayment{
|
|
Status: tc.status,
|
|
State: &MPPaymentState{
|
|
HasSettledHTLC: tc.hasSettledHTLC,
|
|
PaymentFailed: tc.paymentFailed,
|
|
},
|
|
}
|
|
|
|
name := fmt.Sprintf("test_%d_%s", i, p.Status.String())
|
|
t.Run(name, func(t *testing.T) {
|
|
t.Parallel()
|
|
|
|
err := p.Registrable()
|
|
require.ErrorIs(t, err, tc.registryErr,
|
|
"registrable under state %v", tc.status)
|
|
})
|
|
}
|
|
}
|
|
|
|
// TestPaymentSetState checks that the method setState creates the
|
|
// MPPaymentState as expected.
|
|
func TestPaymentSetState(t *testing.T) {
|
|
t.Parallel()
|
|
|
|
// Create a test preimage and failure reason.
|
|
preimage := lntypes.Preimage{1}
|
|
failureReasonError := FailureReasonError
|
|
|
|
testCases := []struct {
|
|
name string
|
|
payment *MPPayment
|
|
totalAmt int
|
|
|
|
expectedState *MPPaymentState
|
|
errExpected error
|
|
}{
|
|
{
|
|
// Test that when the sentAmt exceeds totalAmount, the
|
|
// error is returned.
|
|
name: "amount exceeded error",
|
|
// SentAmt returns 90, 10
|
|
// TerminalInfo returns non-nil, nil
|
|
// InFlightHTLCs returns 0
|
|
payment: &MPPayment{
|
|
HTLCs: []HTLCAttempt{
|
|
makeSettledAttempt(100, 10, preimage),
|
|
},
|
|
},
|
|
totalAmt: 1,
|
|
errExpected: ErrSentExceedsTotal,
|
|
},
|
|
{
|
|
// Test that when the htlc is failed, the fee is not
|
|
// used.
|
|
name: "fee excluded for failed htlc",
|
|
payment: &MPPayment{
|
|
// SentAmt returns 90, 10
|
|
// TerminalInfo returns nil, nil
|
|
// InFlightHTLCs returns 1
|
|
HTLCs: []HTLCAttempt{
|
|
makeActiveAttempt(100, 10),
|
|
makeFailedAttempt(100, 10),
|
|
},
|
|
},
|
|
totalAmt: 1000,
|
|
expectedState: &MPPaymentState{
|
|
NumAttemptsInFlight: 1,
|
|
RemainingAmt: 1000 - 90,
|
|
FeesPaid: 10,
|
|
HasSettledHTLC: false,
|
|
PaymentFailed: false,
|
|
},
|
|
},
|
|
{
|
|
// Test when the payment is settled, the state should
|
|
// be marked as terminated.
|
|
name: "payment settled",
|
|
// SentAmt returns 90, 10
|
|
// TerminalInfo returns non-nil, nil
|
|
// InFlightHTLCs returns 0
|
|
payment: &MPPayment{
|
|
HTLCs: []HTLCAttempt{
|
|
makeSettledAttempt(100, 10, preimage),
|
|
},
|
|
},
|
|
totalAmt: 1000,
|
|
expectedState: &MPPaymentState{
|
|
NumAttemptsInFlight: 0,
|
|
RemainingAmt: 1000 - 90,
|
|
FeesPaid: 10,
|
|
HasSettledHTLC: true,
|
|
PaymentFailed: false,
|
|
},
|
|
},
|
|
{
|
|
// Test when the payment is failed, the state should be
|
|
// marked as terminated.
|
|
name: "payment failed",
|
|
// SentAmt returns 0, 0
|
|
// TerminalInfo returns nil, non-nil
|
|
// InFlightHTLCs returns 0
|
|
payment: &MPPayment{
|
|
FailureReason: &failureReasonError,
|
|
},
|
|
totalAmt: 1000,
|
|
expectedState: &MPPaymentState{
|
|
NumAttemptsInFlight: 0,
|
|
RemainingAmt: 1000,
|
|
FeesPaid: 0,
|
|
HasSettledHTLC: false,
|
|
PaymentFailed: true,
|
|
},
|
|
},
|
|
}
|
|
|
|
for _, tc := range testCases {
|
|
tc := tc
|
|
|
|
t.Run(tc.name, func(t *testing.T) {
|
|
t.Parallel()
|
|
|
|
// Attach the payment info.
|
|
info := &PaymentCreationInfo{
|
|
Value: lnwire.MilliSatoshi(tc.totalAmt),
|
|
}
|
|
tc.payment.Info = info
|
|
|
|
// Call the method that updates the payment state.
|
|
err := tc.payment.setState()
|
|
require.ErrorIs(t, err, tc.errExpected)
|
|
|
|
require.Equal(
|
|
t, tc.expectedState, tc.payment.State,
|
|
"state not updated as expected",
|
|
)
|
|
})
|
|
}
|
|
}
|
|
|
|
// TestNeedWaitAttempts checks whether we need to wait for the results of the
|
|
// HTLC attempts against ALL possible payment statuses.
|
|
func TestNeedWaitAttempts(t *testing.T) {
|
|
t.Parallel()
|
|
|
|
testCases := []struct {
|
|
status PaymentStatus
|
|
remainingAmt lnwire.MilliSatoshi
|
|
hasSettledHTLC bool
|
|
hasFailureReason bool
|
|
needWait bool
|
|
expectedErr error
|
|
}{
|
|
{
|
|
// For a newly created payment we don't need to wait
|
|
// for results.
|
|
status: StatusInitiated,
|
|
remainingAmt: 1000,
|
|
needWait: false,
|
|
expectedErr: nil,
|
|
},
|
|
{
|
|
// With HTLCs inflight we don't need to wait when the
|
|
// remainingAmt is not zero and we have no settled
|
|
// HTLCs.
|
|
status: StatusInFlight,
|
|
remainingAmt: 1000,
|
|
needWait: false,
|
|
expectedErr: nil,
|
|
},
|
|
{
|
|
// With HTLCs inflight we need to wait when the
|
|
// remainingAmt is not zero but we have settled HTLCs.
|
|
status: StatusInFlight,
|
|
remainingAmt: 1000,
|
|
hasSettledHTLC: true,
|
|
needWait: true,
|
|
expectedErr: nil,
|
|
},
|
|
{
|
|
// With HTLCs inflight we need to wait when the
|
|
// remainingAmt is not zero and the payment is failed.
|
|
status: StatusInFlight,
|
|
remainingAmt: 1000,
|
|
needWait: true,
|
|
hasFailureReason: true,
|
|
expectedErr: nil,
|
|
},
|
|
|
|
{
|
|
// With the payment settled, but the remainingAmt is
|
|
// not zero, we have an error state.
|
|
status: StatusSucceeded,
|
|
remainingAmt: 1000,
|
|
needWait: false,
|
|
expectedErr: ErrPaymentInternal,
|
|
},
|
|
{
|
|
// Payment is in terminal state, no need to wait.
|
|
status: StatusFailed,
|
|
remainingAmt: 1000,
|
|
needWait: false,
|
|
expectedErr: nil,
|
|
},
|
|
{
|
|
// A newly created payment with zero remainingAmt
|
|
// indicates an error.
|
|
status: StatusInitiated,
|
|
remainingAmt: 0,
|
|
needWait: false,
|
|
expectedErr: ErrPaymentInternal,
|
|
},
|
|
{
|
|
// With zero remainingAmt we must wait for the results.
|
|
status: StatusInFlight,
|
|
remainingAmt: 0,
|
|
needWait: true,
|
|
expectedErr: nil,
|
|
},
|
|
{
|
|
// Payment is terminated, no need to wait for results.
|
|
status: StatusSucceeded,
|
|
remainingAmt: 0,
|
|
needWait: false,
|
|
expectedErr: nil,
|
|
},
|
|
{
|
|
// Payment is terminated, no need to wait for results.
|
|
status: StatusFailed,
|
|
remainingAmt: 0,
|
|
needWait: false,
|
|
expectedErr: ErrPaymentInternal,
|
|
},
|
|
{
|
|
// Payment is in an unknown status, return an error.
|
|
status: 0,
|
|
remainingAmt: 0,
|
|
needWait: false,
|
|
expectedErr: ErrUnknownPaymentStatus,
|
|
},
|
|
{
|
|
// Payment is in an unknown status, return an error.
|
|
status: 0,
|
|
remainingAmt: 1000,
|
|
needWait: false,
|
|
expectedErr: ErrUnknownPaymentStatus,
|
|
},
|
|
}
|
|
|
|
for _, tc := range testCases {
|
|
tc := tc
|
|
|
|
p := &MPPayment{
|
|
Info: &PaymentCreationInfo{
|
|
PaymentIdentifier: [32]byte{1, 2, 3},
|
|
},
|
|
Status: tc.status,
|
|
State: &MPPaymentState{
|
|
RemainingAmt: tc.remainingAmt,
|
|
HasSettledHTLC: tc.hasSettledHTLC,
|
|
PaymentFailed: tc.hasFailureReason,
|
|
},
|
|
}
|
|
|
|
name := fmt.Sprintf("status=%s|remainingAmt=%v|"+
|
|
"settledHTLC=%v|failureReason=%v", tc.status,
|
|
tc.remainingAmt, tc.hasSettledHTLC, tc.hasFailureReason)
|
|
|
|
t.Run(name, func(t *testing.T) {
|
|
t.Parallel()
|
|
|
|
result, err := p.NeedWaitAttempts()
|
|
require.ErrorIs(t, err, tc.expectedErr)
|
|
require.Equalf(t, tc.needWait, result, "status=%v, "+
|
|
"remainingAmt=%v", tc.status, tc.remainingAmt)
|
|
})
|
|
}
|
|
}
|
|
|
|
// TestAllowMoreAttempts checks whether more attempts can be created against
|
|
// ALL possible payment statuses.
|
|
func TestAllowMoreAttempts(t *testing.T) {
|
|
t.Parallel()
|
|
|
|
testCases := []struct {
|
|
status PaymentStatus
|
|
remainingAmt lnwire.MilliSatoshi
|
|
hasSettledHTLC bool
|
|
paymentFailed bool
|
|
allowMore bool
|
|
expectedErr error
|
|
}{
|
|
{
|
|
// A newly created payment with zero remainingAmt
|
|
// indicates an error.
|
|
status: StatusInitiated,
|
|
remainingAmt: 0,
|
|
allowMore: false,
|
|
expectedErr: ErrPaymentInternal,
|
|
},
|
|
{
|
|
// With zero remainingAmt we don't allow more HTLC
|
|
// attempts.
|
|
status: StatusInFlight,
|
|
remainingAmt: 0,
|
|
allowMore: false,
|
|
expectedErr: nil,
|
|
},
|
|
{
|
|
// With zero remainingAmt we don't allow more HTLC
|
|
// attempts.
|
|
status: StatusSucceeded,
|
|
remainingAmt: 0,
|
|
allowMore: false,
|
|
expectedErr: nil,
|
|
},
|
|
{
|
|
// With zero remainingAmt we don't allow more HTLC
|
|
// attempts.
|
|
status: StatusFailed,
|
|
remainingAmt: 0,
|
|
allowMore: false,
|
|
expectedErr: nil,
|
|
},
|
|
{
|
|
// With zero remainingAmt and settled HTLCs we don't
|
|
// allow more HTLC attempts.
|
|
status: StatusInFlight,
|
|
remainingAmt: 0,
|
|
hasSettledHTLC: true,
|
|
allowMore: false,
|
|
expectedErr: nil,
|
|
},
|
|
{
|
|
// With zero remainingAmt and failed payment we don't
|
|
// allow more HTLC attempts.
|
|
status: StatusInFlight,
|
|
remainingAmt: 0,
|
|
paymentFailed: true,
|
|
allowMore: false,
|
|
expectedErr: nil,
|
|
},
|
|
{
|
|
// With zero remainingAmt and both settled HTLCs and
|
|
// failed payment, we don't allow more HTLC attempts.
|
|
status: StatusInFlight,
|
|
remainingAmt: 0,
|
|
hasSettledHTLC: true,
|
|
paymentFailed: true,
|
|
allowMore: false,
|
|
expectedErr: nil,
|
|
},
|
|
{
|
|
// A newly created payment can have more attempts.
|
|
status: StatusInitiated,
|
|
remainingAmt: 1000,
|
|
allowMore: true,
|
|
expectedErr: nil,
|
|
},
|
|
{
|
|
// With HTLCs inflight we can have more attempts when
|
|
// the remainingAmt is not zero and we have neither
|
|
// failed payment or settled HTLCs.
|
|
status: StatusInFlight,
|
|
remainingAmt: 1000,
|
|
allowMore: true,
|
|
expectedErr: nil,
|
|
},
|
|
{
|
|
// With HTLCs inflight we cannot have more attempts
|
|
// though the remainingAmt is not zero but we have
|
|
// settled HTLCs.
|
|
status: StatusInFlight,
|
|
remainingAmt: 1000,
|
|
hasSettledHTLC: true,
|
|
allowMore: false,
|
|
expectedErr: nil,
|
|
},
|
|
{
|
|
// With HTLCs inflight we cannot have more attempts
|
|
// though the remainingAmt is not zero but we have
|
|
// failed payment.
|
|
status: StatusInFlight,
|
|
remainingAmt: 1000,
|
|
paymentFailed: true,
|
|
allowMore: false,
|
|
expectedErr: nil,
|
|
},
|
|
{
|
|
// With HTLCs inflight we cannot have more attempts
|
|
// though the remainingAmt is not zero but we have
|
|
// settled HTLCs and failed payment.
|
|
status: StatusInFlight,
|
|
remainingAmt: 1000,
|
|
hasSettledHTLC: true,
|
|
paymentFailed: true,
|
|
allowMore: false,
|
|
expectedErr: nil,
|
|
},
|
|
{
|
|
// With the payment settled, but the remainingAmt is
|
|
// not zero, we have an error state.
|
|
status: StatusSucceeded,
|
|
remainingAmt: 1000,
|
|
hasSettledHTLC: true,
|
|
allowMore: false,
|
|
expectedErr: ErrPaymentInternal,
|
|
},
|
|
{
|
|
// With the payment failed with no inflight HTLCs, we
|
|
// don't allow more attempts to be made.
|
|
status: StatusFailed,
|
|
remainingAmt: 1000,
|
|
paymentFailed: true,
|
|
allowMore: false,
|
|
expectedErr: nil,
|
|
},
|
|
{
|
|
// With the payment in an unknown state, we don't allow
|
|
// more attempts to be made.
|
|
status: 0,
|
|
remainingAmt: 1000,
|
|
allowMore: false,
|
|
expectedErr: nil,
|
|
},
|
|
}
|
|
|
|
for i, tc := range testCases {
|
|
tc := tc
|
|
|
|
p := &MPPayment{
|
|
Info: &PaymentCreationInfo{
|
|
PaymentIdentifier: [32]byte{1, 2, 3},
|
|
},
|
|
Status: tc.status,
|
|
State: &MPPaymentState{
|
|
RemainingAmt: tc.remainingAmt,
|
|
HasSettledHTLC: tc.hasSettledHTLC,
|
|
PaymentFailed: tc.paymentFailed,
|
|
},
|
|
}
|
|
|
|
name := fmt.Sprintf("test_%d|status=%s|remainingAmt=%v", i,
|
|
tc.status, tc.remainingAmt)
|
|
|
|
t.Run(name, func(t *testing.T) {
|
|
t.Parallel()
|
|
|
|
result, err := p.AllowMoreAttempts()
|
|
require.ErrorIs(t, err, tc.expectedErr)
|
|
require.Equalf(t, tc.allowMore, result, "status=%v, "+
|
|
"remainingAmt=%v", tc.status, tc.remainingAmt)
|
|
})
|
|
}
|
|
}
|
|
|
|
func makeActiveAttempt(total, fee int) HTLCAttempt {
|
|
return HTLCAttempt{
|
|
HTLCAttemptInfo: makeAttemptInfo(total, total-fee),
|
|
}
|
|
}
|
|
|
|
func makeSettledAttempt(total, fee int,
|
|
preimage lntypes.Preimage) HTLCAttempt {
|
|
|
|
return HTLCAttempt{
|
|
HTLCAttemptInfo: makeAttemptInfo(total, total-fee),
|
|
Settle: &HTLCSettleInfo{Preimage: preimage},
|
|
}
|
|
}
|
|
|
|
func makeFailedAttempt(total, fee int) HTLCAttempt {
|
|
return HTLCAttempt{
|
|
HTLCAttemptInfo: makeAttemptInfo(total, total-fee),
|
|
Failure: &HTLCFailInfo{
|
|
Reason: HTLCFailInternal,
|
|
},
|
|
}
|
|
}
|
|
|
|
func makeAttemptInfo(total, amtForwarded int) HTLCAttemptInfo {
|
|
hop := &route.Hop{AmtToForward: lnwire.MilliSatoshi(amtForwarded)}
|
|
return HTLCAttemptInfo{
|
|
Route: route.Route{
|
|
TotalAmount: lnwire.MilliSatoshi(total),
|
|
Hops: []*route.Hop{hop},
|
|
},
|
|
}
|
|
}
|
|
|
|
// TestEmptyRoutesGenerateSphinxPacket tests that the generateSphinxPacket
|
|
// function is able to gracefully handle being passed a nil set of hops for the
|
|
// route by the caller.
|
|
func TestEmptyRoutesGenerateSphinxPacket(t *testing.T) {
|
|
t.Parallel()
|
|
|
|
sessionKey, _ := btcec.NewPrivateKey()
|
|
emptyRoute := &route.Route{}
|
|
_, _, err := generateSphinxPacket(emptyRoute, testHash[:], sessionKey)
|
|
require.ErrorIs(t, err, route.ErrNoRouteHopsProvided)
|
|
}
|
|
|
|
// TestKVPaymentsDBSuccessesWithoutInFlight tests that the payment control will
|
|
// disallow calls to Success when no payment is in flight.
|
|
func TestKVPaymentsDBSuccessesWithoutInFlight(t *testing.T) {
|
|
t.Parallel()
|
|
|
|
paymentDB := NewTestDB(t)
|
|
|
|
info, _, preimg, err := genInfo(t)
|
|
require.NoError(t, err, "unable to generate htlc message")
|
|
|
|
// Attempt to complete the payment should fail.
|
|
_, err = paymentDB.SettleAttempt(
|
|
info.PaymentIdentifier, 0,
|
|
&HTLCSettleInfo{
|
|
Preimage: preimg,
|
|
},
|
|
)
|
|
require.ErrorIs(t, err, ErrPaymentNotInitiated)
|
|
}
|
|
|
|
// TestKVPaymentsDBFailsWithoutInFlight checks that a strict payment control
|
|
// will disallow calls to Fail when no payment is in flight.
|
|
func TestKVPaymentsDBFailsWithoutInFlight(t *testing.T) {
|
|
t.Parallel()
|
|
|
|
paymentDB := NewTestDB(t)
|
|
|
|
info, _, _, err := genInfo(t)
|
|
require.NoError(t, err, "unable to generate htlc message")
|
|
|
|
// Calling Fail should return an error.
|
|
_, err = paymentDB.Fail(
|
|
info.PaymentIdentifier, FailureReasonNoRoute,
|
|
)
|
|
require.ErrorIs(t, err, ErrPaymentNotInitiated)
|
|
}
|
|
|
|
// TestKVPaymentsDBDeletePayments tests that DeletePayments correctly deletes
|
|
// information about completed payments from the database.
|
|
func TestKVPaymentsDBDeletePayments(t *testing.T) {
|
|
t.Parallel()
|
|
|
|
paymentDB := NewTestDB(t)
|
|
|
|
// Register three payments:
|
|
// 1. A payment with two failed attempts.
|
|
// 2. A payment with one failed and one settled attempt.
|
|
// 3. A payment with one failed and one in-flight attempt.
|
|
payments := []*payment{
|
|
{status: StatusFailed},
|
|
{status: StatusSucceeded},
|
|
{status: StatusInFlight},
|
|
}
|
|
|
|
// Use helper function to register the test payments in the data and
|
|
// populate the data to the payments slice.
|
|
createTestPayments(t, paymentDB, payments)
|
|
|
|
// Check that all payments are there as we added them.
|
|
assertPayments(t, paymentDB, payments)
|
|
|
|
// Delete HTLC attempts for failed payments only.
|
|
numPayments, err := paymentDB.DeletePayments(true, true)
|
|
require.NoError(t, err)
|
|
require.EqualValues(t, 0, numPayments)
|
|
|
|
// The failed payment is the only altered one.
|
|
payments[0].htlcs = 0
|
|
assertPayments(t, paymentDB, payments)
|
|
|
|
// Delete failed attempts for all payments.
|
|
numPayments, err = paymentDB.DeletePayments(false, true)
|
|
require.NoError(t, err)
|
|
require.EqualValues(t, 0, numPayments)
|
|
|
|
// The failed attempts should be deleted, except for the in-flight
|
|
// payment, that shouldn't be altered until it has completed.
|
|
payments[1].htlcs = 1
|
|
assertPayments(t, paymentDB, payments)
|
|
|
|
// Now delete all failed payments.
|
|
numPayments, err = paymentDB.DeletePayments(true, false)
|
|
require.NoError(t, err)
|
|
require.EqualValues(t, 1, numPayments)
|
|
|
|
assertPayments(t, paymentDB, payments[1:])
|
|
|
|
// Finally delete all completed payments.
|
|
numPayments, err = paymentDB.DeletePayments(false, false)
|
|
require.NoError(t, err)
|
|
require.EqualValues(t, 1, numPayments)
|
|
|
|
assertPayments(t, paymentDB, payments[2:])
|
|
}
|