routing: delete old payment lifecycle related unit tests

The old payment lifecycle is removed due to it's not "unit" -
maintaining these tests probably takes as much work as the actual
methods being tested, if not more so. Moreover, the usage of the old
mockers in current payment lifecycle test is removed as it re-implements
other interfaces and sometimes implements it uniquely just for the
tests. This is bad as, not only we need to work on the actual interface
implementations and test them , but also re-implement them again in the
test without testing them!
This commit is contained in:
yyforyongyu 2023-03-08 01:09:55 +08:00
parent 09a5d235ec
commit 01e3bd87ab
No known key found for this signature in database
GPG Key ID: 9BCD95C4FF296868
2 changed files with 0 additions and 1422 deletions

View File

@ -1,17 +1,11 @@
package routing
import (
"crypto/rand"
"fmt"
"sync/atomic"
"testing"
"time"
"github.com/btcsuite/btcd/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"
@ -19,783 +13,10 @@ import (
"github.com/stretchr/testify/require"
)
const stepTimeout = 5 * time.Second
var (
dummyErr = errors.New("dummy")
)
// 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 GetAttemptResult method, and we will
// respond with a successful payment result.
getPaymentResultSuccess = "GetAttemptResult:success"
// getPaymentResultTempFailure is a test step where we expect the
// router to call the GetAttemptResult method, and we will
// respond with a forwarding error, expecting the router to retry.
getPaymentResultTempFailure = "GetAttemptResult:temp-failure"
// getPaymentResultTerminalFailure is a test step where we
// expect the router to call the GetAttemptResult method, and
// we will respond with a terminal error, expecting the router
// to stop making payment attempts.
getPaymentResultTerminalFailure = "GetAttemptResult: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(t, true, testChannels, "a")
require.NoError(t, err, "unable to create graph")
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)
require.NoError(t, err, "unable to create route")
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
// received 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
// synchronize 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,
NextPaymentID: func() (uint64, error) {
next := atomic.AddUint64(&uniquePaymentID, 1)
return next, nil
},
Clock: clock.NewTestClock(time.Unix(1, 0)),
IsAlias: func(scid lnwire.ShortChannelID) bool {
return false
},
})
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()
t.Cleanup(func() {
require.NoError(t, router.Stop())
})
// 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 GetAttemptResult 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 GetAttemptResult 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
// GetAttemptResult method, and we will respond with a
// terminal error, indicating 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.ErrorIs(t, err, test.paymentErr)
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")
}
}
func makeSettledAttempt(total, fee int,
preimage lntypes.Preimage) channeldb.HTLCAttempt {

View File

@ -3400,649 +3400,6 @@ func createDummyLightningPayment(t *testing.T,
}
}
// TestSendMPPaymentSucceed tests that we can successfully send a MPPayment via
// router.SendPayment. This test mainly focuses on testing the logic of the
// method resumePayment is implemented as expected.
func TestSendMPPaymentSucceed(t *testing.T) {
const startingBlockHeight = 101
// Create mockers to initialize the router.
controlTower := &mockControlTower{}
sessionSource := &mockPaymentSessionSource{}
missionControl := &mockMissionControl{}
payer := &mockPaymentAttemptDispatcher{}
chain := newMockChain(startingBlockHeight)
chainView := newMockChainView(chain)
testGraph := createDummyTestGraph(t)
// Define the behavior of the mockers to the point where we can
// successfully start the router.
controlTower.On("FetchInFlightPayments").Return(
[]*channeldb.MPPayment{}, nil,
)
payer.On("CleanStore", mock.Anything).Return(nil)
// Create and start the router.
router, err := New(Config{
Control: controlTower,
SessionSource: sessionSource,
MissionControl: missionControl,
Payer: payer,
// TODO(yy): create new mocks for the chain and chainview.
Chain: chain,
ChainView: chainView,
// TODO(yy): mock the graph once it's changed into interface.
Graph: testGraph.graph,
Clock: clock.NewTestClock(time.Unix(1, 0)),
GraphPruneInterval: time.Hour * 2,
NextPaymentID: func() (uint64, error) {
next := atomic.AddUint64(&uniquePaymentID, 1)
return next, nil
},
IsAlias: func(scid lnwire.ShortChannelID) bool {
return false
},
})
require.NoError(t, err, "failed to create router")
// Make sure the router can start and stop without error.
require.NoError(t, router.Start(), "router failed to start")
t.Cleanup(func() {
require.NoError(t, router.Stop(), "router failed to stop")
})
// Once the router is started, check that the mocked methods are called
// as expected.
controlTower.AssertExpectations(t)
payer.AssertExpectations(t)
// Mock the methods to the point where we are inside the function
// resumePayment.
paymentAmt := lnwire.MilliSatoshi(10000)
req := createDummyLightningPayment(
t, testGraph.aliasMap["c"], paymentAmt,
)
identifier := lntypes.Hash(req.Identifier())
session := &mockPaymentSession{}
sessionSource.On("NewPaymentSession", req).Return(session, nil)
controlTower.On("InitPayment", identifier, mock.Anything).Return(nil)
// Mock the InFlightHTLCs.
var (
htlcs []channeldb.HTLCAttempt
numAttempts atomic.Uint32
settled atomic.Bool
numParts = uint32(4)
)
// Make a mock MPPayment.
payment := &mockMPPayment{}
payment.On("InFlightHTLCs").Return(htlcs).
On("GetState").Return(&channeldb.MPPaymentState{FeesPaid: 0}).
On("GetStatus").Return(channeldb.StatusInFlight)
controlTower.On("FetchPayment", identifier).Return(payment, nil).Once()
// Mock FetchPayment to return the payment.
controlTower.On("FetchPayment", identifier).Return(payment, nil).
Run(func(args mock.Arguments) {
// When number of attempts made is less than 4, we will
// mock the payment's methods to allow the lifecycle to
// continue.
if numAttempts.Load() < numParts {
payment.On("AllowMoreAttempts").Return(true, nil).Once()
return
}
if !settled.Load() {
fmt.Println("wait")
payment.On("AllowMoreAttempts").Return(false, nil).Once()
payment.On("NeedWaitAttempts").Return(true, nil).Once()
// We add another attempt to the counter to
// unblock next time.
return
}
payment.On("AllowMoreAttempts").Return(false, nil).
On("NeedWaitAttempts").Return(false, nil)
})
// Mock SettleAttempt.
preimage := lntypes.Preimage{1, 2, 3}
settledAttempt := makeSettledAttempt(
int(paymentAmt/4), 0, preimage,
)
controlTower.On("SettleAttempt",
identifier, mock.Anything, mock.Anything,
).Return(&settledAttempt, nil).Run(func(args mock.Arguments) {
// We want to at least wait for one settlement.
if numAttempts.Load() > 1 {
settled.Store(true)
}
})
// Create a route that can send 1/4 of the total amount. This value
// will be returned by calling RequestRoute.
shard, err := createTestRoute(paymentAmt/4, testGraph.aliasMap)
require.NoError(t, err, "failed to create route")
session.On("RequestRoute",
mock.Anything, mock.Anything, mock.Anything, mock.Anything,
).Return(shard, nil)
// Make a new htlc attempt with zero fee and append it to the payment's
// HTLCs when calling RegisterAttempt.
controlTower.On("RegisterAttempt",
identifier, mock.Anything,
).Return(nil).Run(func(args mock.Arguments) {
numAttempts.Add(1)
})
// Create a buffered chan and it will be returned by GetAttemptResult.
payer.resultChan = make(chan *htlcswitch.PaymentResult, 10)
payer.On("GetAttemptResult",
mock.Anything, identifier, mock.Anything,
).Run(func(args mock.Arguments) {
// Before the mock method is returned, we send the result to
// the read-only chan.
payer.resultChan <- &htlcswitch.PaymentResult{}
})
// Simple mocking the rest.
payer.On("SendHTLC",
mock.Anything, mock.Anything, mock.Anything,
).Return(nil)
missionControl.On("ReportPaymentSuccess",
mock.Anything, mock.Anything,
).Return(nil).Run(func(args mock.Arguments) {
})
controlTower.On("DeleteFailedAttempts", identifier).Return(nil)
payment.On("TerminalInfo").Return(&settledAttempt, nil)
// Call the actual method SendPayment on router. This is place inside a
// goroutine so we can set a timeout for the whole test, in case
// anything goes wrong and the test never finishes.
done := make(chan struct{})
var p lntypes.Hash
go func() {
p, _, err = router.SendPayment(req)
close(done)
}()
select {
case <-done:
case <-time.After(testTimeout):
t.Fatalf("SendPayment didn't exit")
}
// Finally, validate the returned values and check that the mock
// methods are called as expected.
require.NoError(t, err, "send payment failed")
require.EqualValues(t, preimage, p, "preimage not match")
// Note that we also implicitly check the methods such as FailAttempt,
// ReportPaymentFail, etc, are not called because we never mocked them
// in this test. If any of the unexpected methods was called, the test
// would fail.
controlTower.AssertExpectations(t)
payer.AssertExpectations(t)
sessionSource.AssertExpectations(t)
session.AssertExpectations(t)
missionControl.AssertExpectations(t)
payment.AssertExpectations(t)
}
// TestSendMPPaymentSucceedOnExtraShards tests that we need extra attempts if
// there are failed ones,so that a payment is successfully sent. This test
// mainly focuses on testing the logic of the method resumePayment is
// implemented as expected.
func TestSendMPPaymentSucceedOnExtraShards(t *testing.T) {
const startingBlockHeight = 101
// Create mockers to initialize the router.
controlTower := &mockControlTower{}
sessionSource := &mockPaymentSessionSource{}
missionControl := &mockMissionControl{}
payer := &mockPaymentAttemptDispatcher{}
chain := newMockChain(startingBlockHeight)
chainView := newMockChainView(chain)
testGraph := createDummyTestGraph(t)
// Define the behavior of the mockers to the point where we can
// successfully start the router.
controlTower.On("FetchInFlightPayments").Return(
[]*channeldb.MPPayment{}, nil,
)
payer.On("CleanStore", mock.Anything).Return(nil)
// Create and start the router.
router, err := New(Config{
Control: controlTower,
SessionSource: sessionSource,
MissionControl: missionControl,
Payer: payer,
// TODO(yy): create new mocks for the chain and chainview.
Chain: chain,
ChainView: chainView,
// TODO(yy): mock the graph once it's changed into interface.
Graph: testGraph.graph,
Clock: clock.NewTestClock(time.Unix(1, 0)),
GraphPruneInterval: time.Hour * 2,
NextPaymentID: func() (uint64, error) {
next := atomic.AddUint64(&uniquePaymentID, 1)
return next, nil
},
IsAlias: func(scid lnwire.ShortChannelID) bool {
return false
},
})
require.NoError(t, err, "failed to create router")
// Make sure the router can start and stop without error.
require.NoError(t, router.Start(), "router failed to start")
t.Cleanup(func() {
require.NoError(t, router.Stop(), "router failed to stop")
})
// Once the router is started, check that the mocked methods are called
// as expected.
controlTower.AssertExpectations(t)
payer.AssertExpectations(t)
// Mock the methods to the point where we are inside the function
// resumePayment.
paymentAmt := lnwire.MilliSatoshi(20000)
req := createDummyLightningPayment(
t, testGraph.aliasMap["c"], paymentAmt,
)
identifier := lntypes.Hash(req.Identifier())
session := &mockPaymentSession{}
sessionSource.On("NewPaymentSession", req).Return(session, nil)
controlTower.On("InitPayment", identifier, mock.Anything).Return(nil)
// Mock the InFlightHTLCs.
var (
htlcs []channeldb.HTLCAttempt
numAttempts atomic.Uint32
failAttemptCount atomic.Uint32
settled atomic.Bool
)
// Make a mock MPPayment.
payment := &mockMPPayment{}
payment.On("InFlightHTLCs").Return(htlcs).
On("GetState").Return(&channeldb.MPPaymentState{FeesPaid: 0}).
On("GetStatus").Return(channeldb.StatusInFlight)
controlTower.On("FetchPayment", identifier).Return(payment, nil).Once()
// Mock FetchPayment to return the payment.
controlTower.On("FetchPayment", identifier).Return(payment, nil).
Run(func(args mock.Arguments) {
// When number of attempts made is less than 4, we will
// mock the payment's methods to allow the lifecycle to
// continue.
attempts := numAttempts.Load()
if attempts < 6 {
payment.On("AllowMoreAttempts").Return(true, nil).Once()
return
}
if !settled.Load() {
payment.On("AllowMoreAttempts").Return(false, nil).Once()
payment.On("NeedWaitAttempts").Return(true, nil).Once()
// We add another attempt to the counter to
// unblock next time.
numAttempts.Add(1)
return
}
payment.On("AllowMoreAttempts").Return(false, nil).
On("NeedWaitAttempts").Return(false, nil)
})
// Create a route that can send 1/4 of the total amount. This value
// will be returned by calling RequestRoute.
shard, err := createTestRoute(paymentAmt/4, testGraph.aliasMap)
require.NoError(t, err, "failed to create route")
session.On("RequestRoute",
mock.Anything, mock.Anything, mock.Anything, mock.Anything,
).Return(shard, nil)
// Make a new htlc attempt with zero fee and append it to the payment's
// HTLCs when calling RegisterAttempt.
controlTower.On("RegisterAttempt",
identifier, mock.Anything,
).Return(nil).Run(func(args mock.Arguments) {
// Increase the counter whenever an attempt is made.
numAttempts.Add(1)
})
// Create a buffered chan and it will be returned by GetAttemptResult.
payer.resultChan = make(chan *htlcswitch.PaymentResult, 10)
// We use the failAttemptCount to track how many attempts we want to
// fail. Each time the following mock method is called, the count gets
// updated.
payer.On("GetAttemptResult",
mock.Anything, identifier, mock.Anything,
).Run(func(args mock.Arguments) {
// Before the mock method is returned, we send the result to
// the read-only chan.
// Update the counter.
failAttemptCount.Add(1)
// We will make the first two attempts failed with temporary
// error.
if failAttemptCount.Load() <= 2 {
payer.resultChan <- &htlcswitch.PaymentResult{
Error: htlcswitch.NewForwardingError(
&lnwire.FailTemporaryChannelFailure{},
1,
),
}
return
}
// Otherwise we will mark the attempt succeeded.
payer.resultChan <- &htlcswitch.PaymentResult{}
})
// Mock the FailAttempt method to fail one of the attempts.
var failedAttempt channeldb.HTLCAttempt
controlTower.On("FailAttempt",
identifier, mock.Anything, mock.Anything,
).Return(&failedAttempt, nil)
// Setup ReportPaymentFail to return nil reason and error so the
// payment won't fail.
missionControl.On("ReportPaymentFail",
mock.Anything, mock.Anything, mock.Anything, mock.Anything,
).Return(nil, nil)
// Simple mocking the rest.
payer.On("SendHTLC",
mock.Anything, mock.Anything, mock.Anything,
).Return(nil)
missionControl.On("ReportPaymentSuccess",
mock.Anything, mock.Anything,
).Return(nil)
// Mock SettleAttempt by changing one of the HTLCs to be settled.
preimage := lntypes.Preimage{1, 2, 3}
settledAttempt := makeSettledAttempt(
int(paymentAmt/4), 0, preimage,
)
controlTower.On("SettleAttempt",
identifier, mock.Anything, mock.Anything,
).Return(&settledAttempt, nil).Run(func(args mock.Arguments) {
if numAttempts.Load() > 1 {
settled.Store(true)
}
})
controlTower.On("DeleteFailedAttempts", identifier).Return(nil)
payment.On("TerminalInfo").Return(&settledAttempt, nil)
// Call the actual method SendPayment on router. This is place inside a
// goroutine so we can set a timeout for the whole test, in case
// anything goes wrong and the test never finishes.
done := make(chan struct{})
var p lntypes.Hash
go func() {
p, _, err = router.SendPayment(req)
close(done)
}()
select {
case <-done:
case <-time.After(testTimeout):
t.Fatalf("SendPayment didn't exit")
}
// Finally, validate the returned values and check that the mock
// methods are called as expected.
require.NoError(t, err, "send payment failed")
require.EqualValues(t, preimage, p, "preimage not match")
controlTower.AssertExpectations(t)
payer.AssertExpectations(t)
sessionSource.AssertExpectations(t)
session.AssertExpectations(t)
missionControl.AssertExpectations(t)
payment.AssertExpectations(t)
}
// TestSendMPPaymentFailed tests that when one of the shard fails with a
// terminal error, the router will stop attempting and the payment will fail.
// This test mainly focuses on testing the logic of the method resumePayment
// is implemented as expected.
func TestSendMPPaymentFailed(t *testing.T) {
const startingBlockHeight = 101
// Create mockers to initialize the router.
controlTower := &mockControlTower{}
sessionSource := &mockPaymentSessionSource{}
missionControl := &mockMissionControl{}
payer := &mockPaymentAttemptDispatcher{}
chain := newMockChain(startingBlockHeight)
chainView := newMockChainView(chain)
testGraph := createDummyTestGraph(t)
// Define the behavior of the mockers to the point where we can
// successfully start the router.
controlTower.On("FetchInFlightPayments").Return(
[]*channeldb.MPPayment{}, nil,
)
payer.On("CleanStore", mock.Anything).Return(nil)
// Create and start the router.
router, err := New(Config{
Control: controlTower,
SessionSource: sessionSource,
MissionControl: missionControl,
Payer: payer,
// TODO(yy): create new mocks for the chain and chainview.
Chain: chain,
ChainView: chainView,
// TODO(yy): mock the graph once it's changed into interface.
Graph: testGraph.graph,
Clock: clock.NewTestClock(time.Unix(1, 0)),
GraphPruneInterval: time.Hour * 2,
NextPaymentID: func() (uint64, error) {
next := atomic.AddUint64(&uniquePaymentID, 1)
return next, nil
},
IsAlias: func(scid lnwire.ShortChannelID) bool {
return false
},
})
require.NoError(t, err, "failed to create router")
// Make sure the router can start and stop without error.
require.NoError(t, router.Start(), "router failed to start")
t.Cleanup(func() {
require.NoError(t, router.Stop(), "router failed to stop")
})
// Once the router is started, check that the mocked methods are called
// as expected.
controlTower.AssertExpectations(t)
payer.AssertExpectations(t)
// Mock the methods to the point where we are inside the function
// resumePayment.
paymentAmt := lnwire.MilliSatoshi(10000)
req := createDummyLightningPayment(
t, testGraph.aliasMap["c"], paymentAmt,
)
identifier := lntypes.Hash(req.Identifier())
session := &mockPaymentSession{}
sessionSource.On("NewPaymentSession", req).Return(session, nil)
controlTower.On("InitPayment", identifier, mock.Anything).Return(nil)
// Mock the InFlightHTLCs.
var (
htlcs []channeldb.HTLCAttempt
numAttempts atomic.Uint32
failAttemptCount atomic.Uint32
failed atomic.Bool
numParts = uint32(4)
)
// Make a mock MPPayment.
payment := &mockMPPayment{}
payment.On("InFlightHTLCs").Return(htlcs).Once()
payment.On("GetState").Return(&channeldb.MPPaymentState{}).
On("GetStatus").Return(channeldb.StatusInFlight)
controlTower.On("FetchPayment", identifier).Return(payment, nil).Once()
// Mock the sequential FetchPayment to return the payment.
controlTower.On("FetchPayment", identifier).Return(payment, nil).Run(
func(_ mock.Arguments) {
// We want to at least send out all parts in order to
// wait for them later.
if numAttempts.Load() < numParts {
payment.On("AllowMoreAttempts").Return(true, nil).Once()
return
}
// Wait if the payment wasn't failed yet.
if !failed.Load() {
payment.On("AllowMoreAttempts").Return(false, nil).Once().
On("NeedWaitAttempts").Return(true, nil).Once()
return
}
payment.On("AllowMoreAttempts").Return(false, nil).
On("NeedWaitAttempts").Return(false, nil).Once()
})
// Create a route that can send 1/4 of the total amount. This value
// will be returned by calling RequestRoute.
shard, err := createTestRoute(paymentAmt/4, testGraph.aliasMap)
require.NoError(t, err, "failed to create route")
session.On("RequestRoute",
mock.Anything, mock.Anything, mock.Anything, mock.Anything,
).Return(shard, nil)
// Make a new htlc attempt with zero fee and append it to the payment's
// HTLCs when calling RegisterAttempt.
controlTower.On("RegisterAttempt",
identifier, mock.Anything,
).Return(nil).Run(func(args mock.Arguments) {
numAttempts.Add(1)
})
// Create a buffered chan and it will be returned by GetAttemptResult.
payer.resultChan = make(chan *htlcswitch.PaymentResult, 10)
// We use the failAttemptCount to track how many attempts we want to
// fail. Each time the following mock method is called, the count gets
// updated.
payer.On("GetAttemptResult",
mock.Anything, identifier, mock.Anything,
).Run(func(_ mock.Arguments) {
// Before the mock method is returned, we send the result to
// the read-only chan.
// Update the counter.
failAttemptCount.Add(1)
// We fail the first attempt with terminal error.
if failAttemptCount.Load() == 1 {
payer.resultChan <- &htlcswitch.PaymentResult{
Error: htlcswitch.NewForwardingError(
&lnwire.FailIncorrectDetails{},
1,
),
}
return
}
// We will make the rest attempts failed with temporary error.
payer.resultChan <- &htlcswitch.PaymentResult{
Error: htlcswitch.NewForwardingError(
&lnwire.FailTemporaryChannelFailure{},
1,
),
}
})
// Mock the FailAttempt method to fail one of the attempts.
var failedAttempt channeldb.HTLCAttempt
controlTower.On("FailAttempt",
identifier, mock.Anything, mock.Anything,
).Return(&failedAttempt, nil)
// Setup ReportPaymentFail to return nil reason and error so the
// payment won't fail.
failureReason := channeldb.FailureReasonPaymentDetails
missionControl.On("ReportPaymentFail",
mock.Anything, mock.Anything, mock.Anything, mock.Anything,
).Return(&failureReason, nil)
// Simple mocking the rest.
controlTower.On("FailPayment",
identifier, failureReason,
).Return(nil).Run(func(_ mock.Arguments) {
failed.Store(true)
})
// Mock the payment to return the failure reason.
payer.On("SendHTLC",
mock.Anything, mock.Anything, mock.Anything,
).Return(nil)
payment.On("TerminalInfo").Return(nil, &failureReason)
controlTower.On("DeleteFailedAttempts", identifier).Return(nil)
// Call the actual method SendPayment on router. This is place inside a
// goroutine so we can set a timeout for the whole test, in case
// anything goes wrong and the test never finishes.
done := make(chan struct{})
var p lntypes.Hash
go func() {
p, _, err = router.SendPayment(req)
close(done)
}()
select {
case <-done:
case <-time.After(testTimeout):
t.Fatalf("SendPayment didn't exit")
}
// Finally, validate the returned values and check that the mock
// methods are called as expected.
require.Error(t, err, "expected send payment error")
require.EqualValues(t, [32]byte{}, p, "preimage not match")
require.GreaterOrEqual(t, failAttemptCount.Load(), uint32(1))
controlTower.AssertExpectations(t)
payer.AssertExpectations(t)
sessionSource.AssertExpectations(t)
session.AssertExpectations(t)
missionControl.AssertExpectations(t)
payment.AssertExpectations(t)
}
// TestBlockDifferenceFix tests if when the router is behind on blocks, the
// router catches up to the best block head.
func TestBlockDifferenceFix(t *testing.T) {