lnd.xprv/routing/payment_lifecycle_test.go
yyforyongyu cd35981569
routing: refactor update payment state tests
This commit refactors the resumePayment to extract some logics back to
paymentState so that the code is more testable. It also adds unit tests
for paymentState, and breaks the original MPPayment tests into independent tests
so that it's easier to maintain and debug. All the new tests are built
using mock so that the control flow is eaiser to setup and change.
2021-06-23 20:35:29 +08:00

1141 lines
31 KiB
Go

package routing
import (
"crypto/rand"
"fmt"
"sync/atomic"
"testing"
"time"
"github.com/btcsuite/btcutil"
"github.com/go-errors/errors"
"github.com/lightningnetwork/lnd/channeldb"
"github.com/lightningnetwork/lnd/clock"
"github.com/lightningnetwork/lnd/htlcswitch"
"github.com/lightningnetwork/lnd/lntypes"
"github.com/lightningnetwork/lnd/lnwire"
"github.com/lightningnetwork/lnd/routing/route"
"github.com/stretchr/testify/require"
)
const stepTimeout = 5 * time.Second
// createTestRoute builds a route a->b->c paying the given amt to c.
func createTestRoute(amt lnwire.MilliSatoshi,
aliasMap map[string]route.Vertex) (*route.Route, error) {
hopFee := lnwire.NewMSatFromSatoshis(3)
hop1 := aliasMap["b"]
hop2 := aliasMap["c"]
hops := []*route.Hop{
{
ChannelID: 1,
PubKeyBytes: hop1,
LegacyPayload: true,
AmtToForward: amt + hopFee,
},
{
ChannelID: 2,
PubKeyBytes: hop2,
LegacyPayload: true,
AmtToForward: amt,
},
}
// We create a simple route that we will supply every time the router
// requests one.
return route.NewRouteFromHops(
amt+2*hopFee, 100, aliasMap["a"], hops,
)
}
// paymentLifecycleTestCase contains the steps that we expect for a payment
// lifecycle test, and the routes that pathfinding should deliver.
type paymentLifecycleTestCase struct {
name string
// steps is a list of steps to perform during the testcase.
steps []string
// routes is the sequence of routes we will provide to the
// router when it requests a new route.
routes []*route.Route
// paymentErr is the error we expect our payment to fail with. This
// should be nil for tests with paymentSuccess steps and non-nil for
// payments with paymentError steps.
paymentErr error
}
const (
// routerInitPayment is a test step where we expect the router
// to call the InitPayment method on the control tower.
routerInitPayment = "Router:init-payment"
// routerRegisterAttempt is a test step where we expect the
// router to call the RegisterAttempt method on the control
// tower.
routerRegisterAttempt = "Router:register-attempt"
// routerSettleAttempt is a test step where we expect the
// router to call the SettleAttempt method on the control
// tower.
routerSettleAttempt = "Router:settle-attempt"
// routerFailAttempt is a test step where we expect the router
// to call the FailAttempt method on the control tower.
routerFailAttempt = "Router:fail-attempt"
// routerFailPayment is a test step where we expect the router
// to call the Fail method on the control tower.
routerFailPayment = "Router:fail-payment"
// routeRelease is a test step where we unblock pathfinding and
// allow it to respond to our test with a route.
routeRelease = "PaymentSession:release"
// sendToSwitchSuccess is a step where we expect the router to
// call send the payment attempt to the switch, and we will
// respond with a non-error, indicating that the payment
// attempt was successfully forwarded.
sendToSwitchSuccess = "SendToSwitch:success"
// sendToSwitchResultFailure is a step where we expect the
// router to send the payment attempt to the switch, and we
// will respond with a forwarding error. This can happen when
// forwarding fail on our local links.
sendToSwitchResultFailure = "SendToSwitch:failure"
// getPaymentResultSuccess is a test step where we expect the
// router to call the GetPaymentResult method, and we will
// respond with a successful payment result.
getPaymentResultSuccess = "GetPaymentResult:success"
// getPaymentResultTempFailure is a test step where we expect the
// router to call the GetPaymentResult method, and we will
// respond with a forwarding error, expecting the router to retry.
getPaymentResultTempFailure = "GetPaymentResult:temp-failure"
// getPaymentResultTerminalFailure is a test step where we
// expect the router to call the GetPaymentResult method, and
// we will respond with a terminal error, expecting the router
// to stop making payment attempts.
getPaymentResultTerminalFailure = "GetPaymentResult:terminal-failure"
// resendPayment is a test step where we manually try to resend
// the same payment, making sure the router responds with an
// error indicating that it is already in flight.
resendPayment = "ResendPayment"
// startRouter is a step where we manually start the router,
// used to test that it automatically will resume payments at
// startup.
startRouter = "StartRouter"
// stopRouter is a test step where we manually make the router
// shut down.
stopRouter = "StopRouter"
// paymentSuccess is a step where assert that we receive a
// successful result for the original payment made.
paymentSuccess = "PaymentSuccess"
// paymentError is a step where assert that we receive an error
// for the original payment made.
paymentError = "PaymentError"
// resentPaymentSuccess is a step where assert that we receive
// a successful result for a payment that was resent.
resentPaymentSuccess = "ResentPaymentSuccess"
// resentPaymentError is a step where assert that we receive an
// error for a payment that was resent.
resentPaymentError = "ResentPaymentError"
)
// TestRouterPaymentStateMachine tests that the router interacts as expected
// with the ControlTower during a payment lifecycle, such that it payment
// attempts are not sent twice to the switch, and results are handled after a
// restart.
func TestRouterPaymentStateMachine(t *testing.T) {
t.Parallel()
const startingBlockHeight = 101
// Setup two simple channels such that we can mock sending along this
// route.
chanCapSat := btcutil.Amount(100000)
testChannels := []*testChannel{
symmetricTestChannel("a", "b", chanCapSat, &testChannelPolicy{
Expiry: 144,
FeeRate: 400,
MinHTLC: 1,
MaxHTLC: lnwire.NewMSatFromSatoshis(chanCapSat),
}, 1),
symmetricTestChannel("b", "c", chanCapSat, &testChannelPolicy{
Expiry: 144,
FeeRate: 400,
MinHTLC: 1,
MaxHTLC: lnwire.NewMSatFromSatoshis(chanCapSat),
}, 2),
}
testGraph, err := createTestGraphFromChannels(testChannels, "a")
if err != nil {
t.Fatalf("unable to create graph: %v", err)
}
defer testGraph.cleanUp()
paymentAmt := lnwire.NewMSatFromSatoshis(1000)
// We create a simple route that we will supply every time the router
// requests one.
rt, err := createTestRoute(paymentAmt, testGraph.aliasMap)
if err != nil {
t.Fatalf("unable to create route: %v", err)
}
tests := []paymentLifecycleTestCase{
{
// Tests a normal payment flow that succeeds.
name: "single shot success",
steps: []string{
routerInitPayment,
routeRelease,
routerRegisterAttempt,
sendToSwitchSuccess,
getPaymentResultSuccess,
routerSettleAttempt,
paymentSuccess,
},
routes: []*route.Route{rt},
},
{
// A payment flow with a failure on the first attempt,
// but that succeeds on the second attempt.
name: "single shot retry",
steps: []string{
routerInitPayment,
routeRelease,
routerRegisterAttempt,
sendToSwitchSuccess,
// Make the first sent attempt fail.
getPaymentResultTempFailure,
routerFailAttempt,
// The router should retry.
routeRelease,
routerRegisterAttempt,
sendToSwitchSuccess,
// Make the second sent attempt succeed.
getPaymentResultSuccess,
routerSettleAttempt,
paymentSuccess,
},
routes: []*route.Route{rt, rt},
},
{
// A payment flow with a forwarding failure first time
// sending to the switch, but that succeeds on the
// second attempt.
name: "single shot switch failure",
steps: []string{
routerInitPayment,
routeRelease,
routerRegisterAttempt,
// Make the first sent attempt fail.
sendToSwitchResultFailure,
routerFailAttempt,
// The router should retry.
routeRelease,
routerRegisterAttempt,
sendToSwitchSuccess,
// Make the second sent attempt succeed.
getPaymentResultSuccess,
routerSettleAttempt,
paymentSuccess,
},
routes: []*route.Route{rt, rt},
},
{
// A payment that fails on the first attempt, and has
// only one route available to try. It will therefore
// fail permanently.
name: "single shot route fails",
steps: []string{
routerInitPayment,
routeRelease,
routerRegisterAttempt,
sendToSwitchSuccess,
// Make the first sent attempt fail.
getPaymentResultTempFailure,
routerFailAttempt,
routeRelease,
// Since there are no more routes to try, the
// payment should fail.
routerFailPayment,
paymentError,
},
routes: []*route.Route{rt},
paymentErr: channeldb.FailureReasonNoRoute,
},
{
// We expect the payment to fail immediately if we have
// no routes to try.
name: "single shot no route",
steps: []string{
routerInitPayment,
routeRelease,
routerFailPayment,
paymentError,
},
routes: []*route.Route{},
paymentErr: channeldb.FailureReasonNoRoute,
},
{
// A normal payment flow, where we attempt to resend
// the same payment after each step. This ensures that
// the router don't attempt to resend a payment already
// in flight.
name: "single shot resend",
steps: []string{
routerInitPayment,
routeRelease,
routerRegisterAttempt,
// Manually resend the payment, the router
// should attempt to init with the control
// tower, but fail since it is already in
// flight.
resendPayment,
routerInitPayment,
resentPaymentError,
// The original payment should proceed as
// normal.
sendToSwitchSuccess,
// Again resend the payment and assert it's not
// allowed.
resendPayment,
routerInitPayment,
resentPaymentError,
// Notify about a success for the original
// payment.
getPaymentResultSuccess,
routerSettleAttempt,
// Now that the original payment finished,
// resend it again to ensure this is not
// allowed.
resendPayment,
routerInitPayment,
resentPaymentError,
paymentSuccess,
},
routes: []*route.Route{rt},
},
{
// Tests that the router is able to handle the
// receieved payment result after a restart.
name: "single shot restart",
steps: []string{
routerInitPayment,
routeRelease,
routerRegisterAttempt,
sendToSwitchSuccess,
// Shut down the router. The original caller
// should get notified about this.
stopRouter,
paymentError,
// Start the router again, and ensure the
// router registers the success with the
// control tower.
startRouter,
getPaymentResultSuccess,
routerSettleAttempt,
},
routes: []*route.Route{rt},
paymentErr: ErrRouterShuttingDown,
},
{
// Tests that we are allowed to resend a payment after
// it has permanently failed.
name: "single shot resend fail",
steps: []string{
routerInitPayment,
routeRelease,
routerRegisterAttempt,
sendToSwitchSuccess,
// Resending the payment at this stage should
// not be allowed.
resendPayment,
routerInitPayment,
resentPaymentError,
// Make the first attempt fail.
getPaymentResultTempFailure,
routerFailAttempt,
// Since we have no more routes to try, the
// original payment should fail.
routeRelease,
routerFailPayment,
paymentError,
// Now resend the payment again. This should be
// allowed, since the payment has failed.
resendPayment,
routerInitPayment,
routeRelease,
routerRegisterAttempt,
sendToSwitchSuccess,
getPaymentResultSuccess,
routerSettleAttempt,
resentPaymentSuccess,
},
routes: []*route.Route{rt},
paymentErr: channeldb.FailureReasonNoRoute,
},
}
for _, test := range tests {
test := test
t.Run(test.name, func(t *testing.T) {
testPaymentLifecycle(
t, test, paymentAmt, startingBlockHeight,
testGraph,
)
})
}
}
func testPaymentLifecycle(t *testing.T, test paymentLifecycleTestCase,
paymentAmt lnwire.MilliSatoshi, startingBlockHeight uint32,
testGraph *testGraphInstance) {
// Create a mock control tower with channels set up, that we use to
// synchronize and listen for events.
control := makeMockControlTower()
control.init = make(chan initArgs)
control.registerAttempt = make(chan registerAttemptArgs)
control.settleAttempt = make(chan settleAttemptArgs)
control.failAttempt = make(chan failAttemptArgs)
control.failPayment = make(chan failPaymentArgs)
control.fetchInFlight = make(chan struct{})
// setupRouter is a helper method that creates and starts the router in
// the desired configuration for this test.
setupRouter := func() (*ChannelRouter, chan error,
chan *htlcswitch.PaymentResult) {
chain := newMockChain(startingBlockHeight)
chainView := newMockChainView(chain)
// We set uo the use the following channels and a mock Payer to
// synchonize with the interaction to the Switch.
sendResult := make(chan error)
paymentResult := make(chan *htlcswitch.PaymentResult)
payer := &mockPayerOld{
sendResult: sendResult,
paymentResult: paymentResult,
}
router, err := New(Config{
Graph: testGraph.graph,
Chain: chain,
ChainView: chainView,
Control: control,
SessionSource: &mockPaymentSessionSourceOld{},
MissionControl: &mockMissionControlOld{},
Payer: payer,
ChannelPruneExpiry: time.Hour * 24,
GraphPruneInterval: time.Hour * 2,
QueryBandwidth: func(e *channeldb.ChannelEdgeInfo) lnwire.MilliSatoshi {
return lnwire.NewMSatFromSatoshis(e.Capacity)
},
NextPaymentID: func() (uint64, error) {
next := atomic.AddUint64(&uniquePaymentID, 1)
return next, nil
},
Clock: clock.NewTestClock(time.Unix(1, 0)),
})
if err != nil {
t.Fatalf("unable to create router %v", err)
}
// On startup, the router should fetch all pending payments
// from the ControlTower, so assert that here.
errCh := make(chan error)
go func() {
close(errCh)
select {
case <-control.fetchInFlight:
return
case <-time.After(1 * time.Second):
errCh <- errors.New("router did not fetch in flight " +
"payments")
}
}()
if err := router.Start(); err != nil {
t.Fatalf("unable to start router: %v", err)
}
select {
case err := <-errCh:
if err != nil {
t.Fatalf("error in anonymous goroutine: %s", err)
}
case <-time.After(1 * time.Second):
t.Fatalf("did not fetch in flight payments at startup")
}
return router, sendResult, paymentResult
}
router, sendResult, getPaymentResult := setupRouter()
defer func() {
if err := router.Stop(); err != nil {
t.Fatal(err)
}
}()
// Craft a LightningPayment struct.
var preImage lntypes.Preimage
if _, err := rand.Read(preImage[:]); err != nil {
t.Fatalf("unable to generate preimage")
}
payHash := preImage.Hash()
payment := LightningPayment{
Target: testGraph.aliasMap["c"],
Amount: paymentAmt,
FeeLimit: noFeeLimit,
paymentHash: &payHash,
}
// Setup our payment session source to block on release of
// routes.
routeChan := make(chan struct{})
router.cfg.SessionSource = &mockPaymentSessionSourceOld{
routes: test.routes,
routeRelease: routeChan,
}
router.cfg.MissionControl = &mockMissionControlOld{}
// Send the payment. Since this is new payment hash, the
// information should be registered with the ControlTower.
paymentResult := make(chan error)
done := make(chan struct{})
go func() {
_, _, err := router.SendPayment(&payment)
paymentResult <- err
close(done)
}()
var resendResult chan error
for i, step := range test.steps {
i, step := i, step
// fatal is a helper closure that wraps the step info.
fatal := func(err string, args ...interface{}) {
if args != nil {
err = fmt.Sprintf(err, args)
}
t.Fatalf(
"test case: %s failed on step [%v:%s], err: %s",
test.name, i, step, err,
)
}
switch step {
case routerInitPayment:
var args initArgs
select {
case args = <-control.init:
case <-time.After(stepTimeout):
fatal("no init payment with control")
}
if args.c == nil {
fatal("expected non-nil CreationInfo")
}
case routeRelease:
select {
case <-routeChan:
case <-time.After(stepTimeout):
fatal("no route requested")
}
// In this step we expect the router to make a call to
// register a new attempt with the ControlTower.
case routerRegisterAttempt:
var args registerAttemptArgs
select {
case args = <-control.registerAttempt:
case <-time.After(stepTimeout):
fatal("attempt not registered with control")
}
if args.a == nil {
fatal("expected non-nil AttemptInfo")
}
// In this step we expect the router to call the
// ControlTower's SettleAttempt method with the preimage.
case routerSettleAttempt:
select {
case <-control.settleAttempt:
case <-time.After(stepTimeout):
fatal("attempt settle not " +
"registered with control")
}
// In this step we expect the router to call the
// ControlTower's FailAttempt method with a HTLC fail
// info.
case routerFailAttempt:
select {
case <-control.failAttempt:
case <-time.After(stepTimeout):
fatal("attempt fail not " +
"registered with control")
}
// In this step we expect the router to call the
// ControlTower's Fail method, to indicate that the
// payment failed.
case routerFailPayment:
select {
case <-control.failPayment:
case <-time.After(stepTimeout):
fatal("payment fail not " +
"registered with control")
}
// In this step we expect the SendToSwitch method to be
// called, and we respond with a nil-error.
case sendToSwitchSuccess:
select {
case sendResult <- nil:
case <-time.After(stepTimeout):
fatal("unable to send result")
}
// In this step we expect the SendToSwitch method to be
// called, and we respond with a forwarding error
case sendToSwitchResultFailure:
select {
case sendResult <- htlcswitch.NewForwardingError(
&lnwire.FailTemporaryChannelFailure{},
1,
):
case <-time.After(stepTimeout):
fatal("unable to send result")
}
// In this step we expect the GetPaymentResult method
// to be called, and we respond with the preimage to
// complete the payment.
case getPaymentResultSuccess:
select {
case getPaymentResult <- &htlcswitch.PaymentResult{
Preimage: preImage,
}:
case <-time.After(stepTimeout):
fatal("unable to send result")
}
// In this state we expect the GetPaymentResult method
// to be called, and we respond with a forwarding
// error, indicating that the router should retry.
case getPaymentResultTempFailure:
failure := htlcswitch.NewForwardingError(
&lnwire.FailTemporaryChannelFailure{},
1,
)
select {
case getPaymentResult <- &htlcswitch.PaymentResult{
Error: failure,
}:
case <-time.After(stepTimeout):
fatal("unable to get result")
}
// In this state we expect the router to call the
// GetPaymentResult method, and we will respond with a
// terminal error, indiating the router should stop
// making payment attempts.
case getPaymentResultTerminalFailure:
failure := htlcswitch.NewForwardingError(
&lnwire.FailIncorrectDetails{},
1,
)
select {
case getPaymentResult <- &htlcswitch.PaymentResult{
Error: failure,
}:
case <-time.After(stepTimeout):
fatal("unable to get result")
}
// In this step we manually try to resend the same
// payment, making sure the router responds with an
// error indicating that it is already in flight.
case resendPayment:
resendResult = make(chan error)
go func() {
_, _, err := router.SendPayment(&payment)
resendResult <- err
}()
// In this step we manually stop the router.
case stopRouter:
// On shutdown, the switch closes our result channel.
// Mimic this behavior in our mock.
close(getPaymentResult)
if err := router.Stop(); err != nil {
fatal("unable to restart: %v", err)
}
// In this step we manually start the router.
case startRouter:
router, sendResult, getPaymentResult = setupRouter()
// In this state we expect to receive an error for the
// original payment made.
case paymentError:
require.Error(t, test.paymentErr,
"paymentError not set")
select {
case err := <-paymentResult:
require.Equal(t, test.paymentErr, err)
case <-time.After(stepTimeout):
fatal("got no payment result")
}
// In this state we expect the original payment to
// succeed.
case paymentSuccess:
require.Nil(t, test.paymentErr)
select {
case err := <-paymentResult:
if err != nil {
t.Fatalf("did not expect "+
"error %v", err)
}
case <-time.After(stepTimeout):
fatal("got no payment result")
}
// In this state we expect to receive an error for the
// resent payment made.
case resentPaymentError:
select {
case err := <-resendResult:
if err == nil {
t.Fatalf("expected error")
}
case <-time.After(stepTimeout):
fatal("got no payment result")
}
// In this state we expect the resent payment to
// succeed.
case resentPaymentSuccess:
select {
case err := <-resendResult:
if err != nil {
t.Fatalf("did not expect error %v", err)
}
case <-time.After(stepTimeout):
fatal("got no payment result")
}
default:
fatal("unknown step %v", step)
}
}
select {
case <-done:
case <-time.After(testTimeout):
t.Fatalf("SendPayment didn't exit")
}
}
// TestPaymentState tests that the logics implemented on paymentState struct
// are as expected. In particular, that the method terminated and
// needWaitForShards return the right values.
func TestPaymentState(t *testing.T) {
t.Parallel()
testCases := []struct {
name string
// Use the following three params, each is equivalent to a bool
// statement, to construct 8 test cases so that we can
// exhaustively catch all possible states.
numShardsInFlight int
remainingAmt lnwire.MilliSatoshi
terminate bool
expectedTerminated bool
expectedNeedWaitForShards bool
}{
{
// If we have active shards and terminate is marked
// false, the state is not terminated. Since the
// remaining amount is zero, we need to wait for shards
// to be finished and launch no more shards.
name: "state 100",
numShardsInFlight: 1,
remainingAmt: lnwire.MilliSatoshi(0),
terminate: false,
expectedTerminated: false,
expectedNeedWaitForShards: true,
},
{
// If we have active shards while terminate is marked
// true, the state is not terminated, and we need to
// wait for shards to be finished and launch no more
// shards.
name: "state 101",
numShardsInFlight: 1,
remainingAmt: lnwire.MilliSatoshi(0),
terminate: true,
expectedTerminated: false,
expectedNeedWaitForShards: true,
},
{
// If we have active shards and terminate is marked
// false, the state is not terminated. Since the
// remaining amount is not zero, we don't need to wait
// for shards outcomes and should launch more shards.
name: "state 110",
numShardsInFlight: 1,
remainingAmt: lnwire.MilliSatoshi(1),
terminate: false,
expectedTerminated: false,
expectedNeedWaitForShards: false,
},
{
// If we have active shards and terminate is marked
// true, the state is not terminated. Even the
// remaining amount is not zero, we need to wait for
// shards outcomes because state is terminated.
name: "state 111",
numShardsInFlight: 1,
remainingAmt: lnwire.MilliSatoshi(1),
terminate: true,
expectedTerminated: false,
expectedNeedWaitForShards: true,
},
{
// If we have no active shards while terminate is marked
// false, the state is not terminated, and we don't
// need to wait for more shard outcomes because there
// are no active shards.
name: "state 000",
numShardsInFlight: 0,
remainingAmt: lnwire.MilliSatoshi(0),
terminate: false,
expectedTerminated: false,
expectedNeedWaitForShards: false,
},
{
// If we have no active shards while terminate is marked
// true, the state is terminated, and we don't need to
// wait for shards to be finished.
name: "state 001",
numShardsInFlight: 0,
remainingAmt: lnwire.MilliSatoshi(0),
terminate: true,
expectedTerminated: true,
expectedNeedWaitForShards: false,
},
{
// If we have no active shards while terminate is marked
// false, the state is not terminated. Since the
// remaining amount is not zero, we don't need to wait
// for shards outcomes and should launch more shards.
name: "state 010",
numShardsInFlight: 0,
remainingAmt: lnwire.MilliSatoshi(1),
terminate: false,
expectedTerminated: false,
expectedNeedWaitForShards: false,
},
{
// If we have no active shards while terminate is marked
// true, the state is terminated, and we don't need to
// wait for shards outcomes.
name: "state 011",
numShardsInFlight: 0,
remainingAmt: lnwire.MilliSatoshi(1),
terminate: true,
expectedTerminated: true,
expectedNeedWaitForShards: false,
},
}
for _, tc := range testCases {
tc := tc
t.Run(tc.name, func(t *testing.T) {
t.Parallel()
ps := &paymentState{
numShardsInFlight: tc.numShardsInFlight,
remainingAmt: tc.remainingAmt,
terminate: tc.terminate,
}
require.Equal(
t, tc.expectedTerminated, ps.terminated(),
"terminated returned wrong value",
)
require.Equal(
t, tc.expectedNeedWaitForShards,
ps.needWaitForShards(),
"needWaitForShards returned wrong value",
)
})
}
}
// TestUpdatePaymentState checks that the method updatePaymentState updates the
// paymentState as expected.
func TestUpdatePaymentState(t *testing.T) {
t.Parallel()
// paymentHash is the identifier on paymentLifecycle.
paymentHash := lntypes.Hash{}
// TODO(yy): make MPPayment into an interface so we can mock it. The
// current design implicitly tests the methods SendAmt, TerminalInfo,
// and InFlightHTLCs on channeldb.MPPayment, which is not good. Once
// MPPayment becomes an interface, we can then mock these methods here.
// SentAmt returns 90, 10
// TerminalInfo returns non-nil, nil
// InFlightHTLCs returns 0
var preimage lntypes.Preimage
paymentSettled := &channeldb.MPPayment{
HTLCs: []channeldb.HTLCAttempt{
makeSettledAttempt(100, 10, preimage),
},
}
// SentAmt returns 0, 0
// TerminalInfo returns nil, non-nil
// InFlightHTLCs returns 0
reason := channeldb.FailureReasonError
paymentFailed := &channeldb.MPPayment{
FailureReason: &reason,
}
// SentAmt returns 90, 10
// TerminalInfo returns nil, nil
// InFlightHTLCs returns 1
paymentActive := &channeldb.MPPayment{
HTLCs: []channeldb.HTLCAttempt{
makeActiveAttempt(100, 10),
makeFailedAttempt(100, 10),
},
}
testCases := []struct {
name string
payment *channeldb.MPPayment
totalAmt int
feeLimit int
expectedState *paymentState
shouldReturnError bool
}{
{
// Test that the error returned from FetchPayment is
// handled properly. We use a nil payment to indicate
// we want to return an error.
name: "fetch payment error",
payment: nil,
shouldReturnError: true,
},
{
// Test that when the sentAmt exceeds totalAmount, the
// error is returned.
name: "amount exceeded error",
payment: paymentSettled,
totalAmt: 1,
shouldReturnError: true,
},
{
// Test that when the fee budget is reached, the
// remaining fee should be zero.
name: "fee budget reached",
payment: paymentActive,
totalAmt: 1000,
feeLimit: 1,
expectedState: &paymentState{
numShardsInFlight: 1,
remainingAmt: 1000 - 90,
remainingFees: 0,
terminate: false,
},
},
{
// Test when the payment is settled, the state should
// be marked as terminated.
name: "payment settled",
payment: paymentSettled,
totalAmt: 1000,
feeLimit: 100,
expectedState: &paymentState{
numShardsInFlight: 0,
remainingAmt: 1000 - 90,
remainingFees: 100 - 10,
terminate: true,
},
},
{
// Test when the payment is failed, the state should be
// marked as terminated.
name: "payment failed",
payment: paymentFailed,
totalAmt: 1000,
feeLimit: 100,
expectedState: &paymentState{
numShardsInFlight: 0,
remainingAmt: 1000,
remainingFees: 100,
terminate: true,
},
},
}
for _, tc := range testCases {
tc := tc
t.Run(tc.name, func(t *testing.T) {
t.Parallel()
// Create mock control tower and assign it to router.
// We will then use the router and the paymentHash
// above to create our paymentLifecycle for this test.
ct := &mockControlTower{}
rt := &ChannelRouter{cfg: &Config{Control: ct}}
pl := &paymentLifecycle{
router: rt,
identifier: paymentHash,
totalAmount: lnwire.MilliSatoshi(tc.totalAmt),
feeLimit: lnwire.MilliSatoshi(tc.feeLimit),
}
if tc.payment == nil {
// A nil payment indicates we want to test an
// error returned from FetchPayment.
dummyErr := errors.New("dummy")
ct.On("FetchPayment", paymentHash).Return(
nil, dummyErr,
)
} else {
// Otherwise we will return the payment.
ct.On("FetchPayment", paymentHash).Return(
tc.payment, nil,
)
}
// Call the method that updates the payment state.
_, state, err := pl.updatePaymentState()
// Assert that the mock method is called as
// intended.
ct.AssertExpectations(t)
if tc.shouldReturnError {
require.Error(t, err, "expect an error")
return
}
require.NoError(t, err, "unexpected error")
require.Equal(
t, tc.expectedState, state,
"state not updated as expected",
)
})
}
}
func makeActiveAttempt(total, fee int) channeldb.HTLCAttempt {
return channeldb.HTLCAttempt{
HTLCAttemptInfo: makeAttemptInfo(total, total-fee),
}
}
func makeSettledAttempt(total, fee int,
preimage lntypes.Preimage) channeldb.HTLCAttempt {
return channeldb.HTLCAttempt{
HTLCAttemptInfo: makeAttemptInfo(total, total-fee),
Settle: &channeldb.HTLCSettleInfo{Preimage: preimage},
}
}
func makeFailedAttempt(total, fee int) channeldb.HTLCAttempt {
return channeldb.HTLCAttempt{
HTLCAttemptInfo: makeAttemptInfo(total, total-fee),
Failure: &channeldb.HTLCFailInfo{
Reason: channeldb.HTLCFailInternal,
},
}
}
func makeAttemptInfo(total, amtForwarded int) channeldb.HTLCAttemptInfo {
hop := &route.Hop{AmtToForward: lnwire.MilliSatoshi(amtForwarded)}
return channeldb.HTLCAttemptInfo{
Route: route.Route{
TotalAmount: lnwire.MilliSatoshi(total),
Hops: []*route.Hop{hop},
},
}
}