mirror of
https://github.com/lightningnetwork/lnd.git
synced 2024-11-19 18:10:34 +01:00
7dfe4018ce
This commit was previously split into the following parts to ease review: - 2d746f68: replace imports - 4008f0fd: use ecdsa.Signature - 849e33d1: remove btcec.S256() - b8f6ebbd: use v2 library correctly - fa80bca9: bump go modules
1133 lines
31 KiB
Go
1133 lines
31 KiB
Go
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"
|
|
"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(true, 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
|
|
// 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)),
|
|
})
|
|
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, 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.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.fetchPaymentState()
|
|
|
|
// 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},
|
|
},
|
|
}
|
|
}
|