routing tests: move TestRouterPaymentStateMachine to own file
(almost) PURE CODE MOVE The only code change is to change a few select cases from case _ <- channel: to case <- channel: to please the linter. The test is testing the payment lifecycle, so move it to payment_lifecycle_test.go
This commit is contained in:
parent
3610824abd
commit
4d343bbb46
653
routing/payment_lifecycle_test.go
Normal file
653
routing/payment_lifecycle_test.go
Normal file
@ -0,0 +1,653 @@
|
||||
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"
|
||||
)
|
||||
|
||||
// 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()
|
||||
|
||||
hop1 := testGraph.aliasMap["b"]
|
||||
hop2 := testGraph.aliasMap["c"]
|
||||
hops := []*route.Hop{
|
||||
{
|
||||
ChannelID: 1,
|
||||
PubKeyBytes: hop1,
|
||||
LegacyPayload: true,
|
||||
},
|
||||
{
|
||||
ChannelID: 2,
|
||||
PubKeyBytes: hop2,
|
||||
LegacyPayload: true,
|
||||
},
|
||||
}
|
||||
|
||||
// We create a simple route that we will supply every time the router
|
||||
// requests one.
|
||||
rt, err := route.NewRouteFromHops(
|
||||
lnwire.MilliSatoshi(10000), 100, testGraph.aliasMap["a"], hops,
|
||||
)
|
||||
if err != nil {
|
||||
t.Fatalf("unable to create route: %v", err)
|
||||
}
|
||||
|
||||
// A payment state machine test case consists of several ordered steps,
|
||||
// that we use for driving the scenario.
|
||||
type testCase struct {
|
||||
// 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
|
||||
}
|
||||
|
||||
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"
|
||||
|
||||
// routerSuccess is a test step where we expect the router to
|
||||
// call the Success method on the control tower.
|
||||
routerSuccess = "Router:success"
|
||||
|
||||
// routerFail is a test step where we expect the router to call
|
||||
// the Fail method on the control tower.
|
||||
routerFail = "Router:fail"
|
||||
|
||||
// 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"
|
||||
|
||||
// getPaymentResultFailure is a test step where we expect the
|
||||
// router to call the GetPaymentResult method, and we will
|
||||
// respond with a forwarding error.
|
||||
getPaymentResultFailure = "GetPaymentResult: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"
|
||||
)
|
||||
|
||||
tests := []testCase{
|
||||
{
|
||||
// Tests a normal payment flow that succeeds.
|
||||
steps: []string{
|
||||
routerInitPayment,
|
||||
routerRegisterAttempt,
|
||||
sendToSwitchSuccess,
|
||||
getPaymentResultSuccess,
|
||||
routerSuccess,
|
||||
paymentSuccess,
|
||||
},
|
||||
routes: []*route.Route{rt},
|
||||
},
|
||||
{
|
||||
// A payment flow with a failure on the first attempt,
|
||||
// but that succeeds on the second attempt.
|
||||
steps: []string{
|
||||
routerInitPayment,
|
||||
routerRegisterAttempt,
|
||||
sendToSwitchSuccess,
|
||||
|
||||
// Make the first sent attempt fail.
|
||||
getPaymentResultFailure,
|
||||
|
||||
// The router should retry.
|
||||
routerRegisterAttempt,
|
||||
sendToSwitchSuccess,
|
||||
|
||||
// Make the second sent attempt succeed.
|
||||
getPaymentResultSuccess,
|
||||
routerSuccess,
|
||||
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.
|
||||
steps: []string{
|
||||
routerInitPayment,
|
||||
routerRegisterAttempt,
|
||||
|
||||
// Make the first sent attempt fail.
|
||||
sendToSwitchResultFailure,
|
||||
|
||||
// The router should retry.
|
||||
routerRegisterAttempt,
|
||||
sendToSwitchSuccess,
|
||||
|
||||
// Make the second sent attempt succeed.
|
||||
getPaymentResultSuccess,
|
||||
routerSuccess,
|
||||
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.
|
||||
steps: []string{
|
||||
routerInitPayment,
|
||||
routerRegisterAttempt,
|
||||
sendToSwitchSuccess,
|
||||
|
||||
// Make the first sent attempt fail.
|
||||
getPaymentResultFailure,
|
||||
|
||||
// Since there are no more routes to try, the
|
||||
// payment should fail.
|
||||
routerFail,
|
||||
paymentError,
|
||||
},
|
||||
routes: []*route.Route{rt},
|
||||
},
|
||||
{
|
||||
// We expect the payment to fail immediately if we have
|
||||
// no routes to try.
|
||||
steps: []string{
|
||||
routerInitPayment,
|
||||
routerFail,
|
||||
paymentError,
|
||||
},
|
||||
routes: []*route.Route{},
|
||||
},
|
||||
{
|
||||
// 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.
|
||||
steps: []string{
|
||||
routerInitPayment,
|
||||
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,
|
||||
routerSuccess,
|
||||
|
||||
// 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.
|
||||
steps: []string{
|
||||
routerInitPayment,
|
||||
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,
|
||||
routerSuccess,
|
||||
},
|
||||
routes: []*route.Route{rt},
|
||||
},
|
||||
{
|
||||
// Tests that we are allowed to resend a payment after
|
||||
// it has permanently failed.
|
||||
steps: []string{
|
||||
routerInitPayment,
|
||||
routerRegisterAttempt,
|
||||
sendToSwitchSuccess,
|
||||
|
||||
// Resending the payment at this stage should
|
||||
// not be allowed.
|
||||
resendPayment,
|
||||
routerInitPayment,
|
||||
resentPaymentError,
|
||||
|
||||
// Make the first attempt fail.
|
||||
getPaymentResultFailure,
|
||||
routerFail,
|
||||
|
||||
// Since we have no more routes to try, the
|
||||
// original payment should fail.
|
||||
paymentError,
|
||||
|
||||
// Now resend the payment again. This should be
|
||||
// allowed, since the payment has failed.
|
||||
resendPayment,
|
||||
routerInitPayment,
|
||||
routerRegisterAttempt,
|
||||
sendToSwitchSuccess,
|
||||
getPaymentResultSuccess,
|
||||
routerSuccess,
|
||||
resentPaymentSuccess,
|
||||
},
|
||||
routes: []*route.Route{rt},
|
||||
},
|
||||
}
|
||||
|
||||
// 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.register = make(chan registerArgs)
|
||||
control.success = make(chan successArgs)
|
||||
control.fail = make(chan failArgs)
|
||||
control.fetchInFlight = make(chan struct{})
|
||||
|
||||
quit := make(chan struct{})
|
||||
defer close(quit)
|
||||
|
||||
// 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, chan error) {
|
||||
|
||||
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)
|
||||
paymentResultErr := make(chan error)
|
||||
paymentResult := make(chan *htlcswitch.PaymentResult)
|
||||
|
||||
payer := &mockPayer{
|
||||
sendResult: sendResult,
|
||||
paymentResult: paymentResult,
|
||||
paymentResultErr: paymentResultErr,
|
||||
}
|
||||
|
||||
router, err := New(Config{
|
||||
Graph: testGraph.graph,
|
||||
Chain: chain,
|
||||
ChainView: chainView,
|
||||
Control: control,
|
||||
SessionSource: &mockPaymentSessionSource{},
|
||||
MissionControl: &mockMissionControl{},
|
||||
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, paymentResultErr
|
||||
}
|
||||
|
||||
router, sendResult, getPaymentResult, getPaymentResultErr := setupRouter()
|
||||
defer func() {
|
||||
if err := router.Stop(); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
}()
|
||||
|
||||
for _, test := range tests {
|
||||
// Craft a LightningPayment struct.
|
||||
var preImage lntypes.Preimage
|
||||
if _, err := rand.Read(preImage[:]); err != nil {
|
||||
t.Fatalf("unable to generate preimage")
|
||||
}
|
||||
|
||||
payHash := preImage.Hash()
|
||||
|
||||
paymentAmt := lnwire.NewMSatFromSatoshis(1000)
|
||||
payment := LightningPayment{
|
||||
Target: testGraph.aliasMap["c"],
|
||||
Amount: paymentAmt,
|
||||
FeeLimit: noFeeLimit,
|
||||
PaymentHash: payHash,
|
||||
}
|
||||
|
||||
router.cfg.SessionSource = &mockPaymentSessionSource{
|
||||
routes: test.routes,
|
||||
}
|
||||
|
||||
router.cfg.MissionControl = &mockMissionControl{}
|
||||
|
||||
// Send the payment. Since this is new payment hash, the
|
||||
// information should be registered with the ControlTower.
|
||||
paymentResult := make(chan error)
|
||||
go func() {
|
||||
_, _, err := router.SendPayment(&payment)
|
||||
paymentResult <- err
|
||||
}()
|
||||
|
||||
var resendResult chan error
|
||||
for _, step := range test.steps {
|
||||
switch step {
|
||||
|
||||
case routerInitPayment:
|
||||
var args initArgs
|
||||
select {
|
||||
case args = <-control.init:
|
||||
case <-time.After(1 * time.Second):
|
||||
t.Fatalf("no init payment with control")
|
||||
}
|
||||
|
||||
if args.c == nil {
|
||||
t.Fatalf("expected non-nil CreationInfo")
|
||||
}
|
||||
|
||||
// In this step we expect the router to make a call to
|
||||
// register a new attempt with the ControlTower.
|
||||
case routerRegisterAttempt:
|
||||
var args registerArgs
|
||||
select {
|
||||
case args = <-control.register:
|
||||
case <-time.After(1 * time.Second):
|
||||
t.Fatalf("not registered with control")
|
||||
}
|
||||
|
||||
if args.a == nil {
|
||||
t.Fatalf("expected non-nil AttemptInfo")
|
||||
}
|
||||
|
||||
// In this step we expect the router to call the
|
||||
// ControlTower's Succcess method with the preimage.
|
||||
case routerSuccess:
|
||||
select {
|
||||
case <-control.success:
|
||||
case <-time.After(1 * time.Second):
|
||||
t.Fatalf("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 routerFail:
|
||||
select {
|
||||
case <-control.fail:
|
||||
case <-time.After(1 * time.Second):
|
||||
t.Fatalf("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(1 * time.Second):
|
||||
t.Fatalf("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(1 * time.Second):
|
||||
t.Fatalf("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(1 * time.Second):
|
||||
t.Fatalf("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 getPaymentResultFailure:
|
||||
failure := htlcswitch.NewForwardingError(
|
||||
&lnwire.FailTemporaryChannelFailure{},
|
||||
1,
|
||||
)
|
||||
|
||||
select {
|
||||
case getPaymentResult <- &htlcswitch.PaymentResult{
|
||||
Error: failure,
|
||||
}:
|
||||
case <-time.After(1 * time.Second):
|
||||
t.Fatalf("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:
|
||||
select {
|
||||
case getPaymentResultErr <- fmt.Errorf(
|
||||
"shutting down"):
|
||||
case <-time.After(1 * time.Second):
|
||||
t.Fatalf("unable to send payment " +
|
||||
"result error")
|
||||
}
|
||||
|
||||
if err := router.Stop(); err != nil {
|
||||
t.Fatalf("unable to restart: %v", err)
|
||||
}
|
||||
|
||||
// In this step we manually start the router.
|
||||
case startRouter:
|
||||
router, sendResult, getPaymentResult,
|
||||
getPaymentResultErr = setupRouter()
|
||||
|
||||
// In this state we expect to receive an error for the
|
||||
// original payment made.
|
||||
case paymentError:
|
||||
select {
|
||||
case err := <-paymentResult:
|
||||
if err == nil {
|
||||
t.Fatalf("expected error")
|
||||
}
|
||||
|
||||
case <-time.After(1 * time.Second):
|
||||
t.Fatalf("got no payment result")
|
||||
}
|
||||
|
||||
// In this state we expect the original payment to
|
||||
// succeed.
|
||||
case paymentSuccess:
|
||||
select {
|
||||
case err := <-paymentResult:
|
||||
if err != nil {
|
||||
t.Fatalf("did not expecte error %v", err)
|
||||
}
|
||||
|
||||
case <-time.After(1 * time.Second):
|
||||
t.Fatalf("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(1 * time.Second):
|
||||
t.Fatalf("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(1 * time.Second):
|
||||
t.Fatalf("got no payment result")
|
||||
}
|
||||
|
||||
default:
|
||||
t.Fatalf("unknown step %v", step)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
@ -2,7 +2,6 @@ package routing
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"errors"
|
||||
"fmt"
|
||||
"image/color"
|
||||
"math"
|
||||
@ -2616,637 +2615,6 @@ func assertChannelsPruned(t *testing.T, graph *channeldb.ChannelGraph,
|
||||
}
|
||||
}
|
||||
|
||||
// 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()
|
||||
|
||||
hop1 := testGraph.aliasMap["b"]
|
||||
hop2 := testGraph.aliasMap["c"]
|
||||
hops := []*route.Hop{
|
||||
{
|
||||
ChannelID: 1,
|
||||
PubKeyBytes: hop1,
|
||||
LegacyPayload: true,
|
||||
},
|
||||
{
|
||||
ChannelID: 2,
|
||||
PubKeyBytes: hop2,
|
||||
LegacyPayload: true,
|
||||
},
|
||||
}
|
||||
|
||||
// We create a simple route that we will supply every time the router
|
||||
// requests one.
|
||||
rt, err := route.NewRouteFromHops(
|
||||
lnwire.MilliSatoshi(10000), 100, testGraph.aliasMap["a"], hops,
|
||||
)
|
||||
if err != nil {
|
||||
t.Fatalf("unable to create route: %v", err)
|
||||
}
|
||||
|
||||
// A payment state machine test case consists of several ordered steps,
|
||||
// that we use for driving the scenario.
|
||||
type testCase struct {
|
||||
// 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
|
||||
}
|
||||
|
||||
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"
|
||||
|
||||
// routerSuccess is a test step where we expect the router to
|
||||
// call the Success method on the control tower.
|
||||
routerSuccess = "Router:success"
|
||||
|
||||
// routerFail is a test step where we expect the router to call
|
||||
// the Fail method on the control tower.
|
||||
routerFail = "Router:fail"
|
||||
|
||||
// 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"
|
||||
|
||||
// getPaymentResultFailure is a test step where we expect the
|
||||
// router to call the GetPaymentResult method, and we will
|
||||
// respond with a forwarding error.
|
||||
getPaymentResultFailure = "GetPaymentResult: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 alreayd 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"
|
||||
)
|
||||
|
||||
tests := []testCase{
|
||||
{
|
||||
// Tests a normal payment flow that succeeds.
|
||||
steps: []string{
|
||||
routerInitPayment,
|
||||
routerRegisterAttempt,
|
||||
sendToSwitchSuccess,
|
||||
getPaymentResultSuccess,
|
||||
routerSuccess,
|
||||
paymentSuccess,
|
||||
},
|
||||
routes: []*route.Route{rt},
|
||||
},
|
||||
{
|
||||
// A payment flow with a failure on the first attempt,
|
||||
// but that succeeds on the second attempt.
|
||||
steps: []string{
|
||||
routerInitPayment,
|
||||
routerRegisterAttempt,
|
||||
sendToSwitchSuccess,
|
||||
|
||||
// Make the first sent attempt fail.
|
||||
getPaymentResultFailure,
|
||||
|
||||
// The router should retry.
|
||||
routerRegisterAttempt,
|
||||
sendToSwitchSuccess,
|
||||
|
||||
// Make the second sent attempt succeed.
|
||||
getPaymentResultSuccess,
|
||||
routerSuccess,
|
||||
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.
|
||||
steps: []string{
|
||||
routerInitPayment,
|
||||
routerRegisterAttempt,
|
||||
|
||||
// Make the first sent attempt fail.
|
||||
sendToSwitchResultFailure,
|
||||
|
||||
// The router should retry.
|
||||
routerRegisterAttempt,
|
||||
sendToSwitchSuccess,
|
||||
|
||||
// Make the second sent attempt succeed.
|
||||
getPaymentResultSuccess,
|
||||
routerSuccess,
|
||||
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.
|
||||
steps: []string{
|
||||
routerInitPayment,
|
||||
routerRegisterAttempt,
|
||||
sendToSwitchSuccess,
|
||||
|
||||
// Make the first sent attempt fail.
|
||||
getPaymentResultFailure,
|
||||
|
||||
// Since there are no more routes to try, the
|
||||
// payment should fail.
|
||||
routerFail,
|
||||
paymentError,
|
||||
},
|
||||
routes: []*route.Route{rt},
|
||||
},
|
||||
{
|
||||
// We expect the payment to fail immediately if we have
|
||||
// no routes to try.
|
||||
steps: []string{
|
||||
routerInitPayment,
|
||||
routerFail,
|
||||
paymentError,
|
||||
},
|
||||
routes: []*route.Route{},
|
||||
},
|
||||
{
|
||||
// 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.
|
||||
steps: []string{
|
||||
routerInitPayment,
|
||||
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,
|
||||
routerSuccess,
|
||||
|
||||
// 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.
|
||||
steps: []string{
|
||||
routerInitPayment,
|
||||
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,
|
||||
routerSuccess,
|
||||
},
|
||||
routes: []*route.Route{rt},
|
||||
},
|
||||
{
|
||||
// Tests that we are allowed to resend a payment after
|
||||
// it has permanently failed.
|
||||
steps: []string{
|
||||
routerInitPayment,
|
||||
routerRegisterAttempt,
|
||||
sendToSwitchSuccess,
|
||||
|
||||
// Resending the payment at this stage should
|
||||
// not be allowed.
|
||||
resendPayment,
|
||||
routerInitPayment,
|
||||
resentPaymentError,
|
||||
|
||||
// Make the first attempt fail.
|
||||
getPaymentResultFailure,
|
||||
routerFail,
|
||||
|
||||
// Since we have no more routes to try, the
|
||||
// original payment should fail.
|
||||
paymentError,
|
||||
|
||||
// Now resend the payment again. This should be
|
||||
// allowed, since the payment has failed.
|
||||
resendPayment,
|
||||
routerInitPayment,
|
||||
routerRegisterAttempt,
|
||||
sendToSwitchSuccess,
|
||||
getPaymentResultSuccess,
|
||||
routerSuccess,
|
||||
resentPaymentSuccess,
|
||||
},
|
||||
routes: []*route.Route{rt},
|
||||
},
|
||||
}
|
||||
|
||||
// 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.register = make(chan registerArgs)
|
||||
control.success = make(chan successArgs)
|
||||
control.fail = make(chan failArgs)
|
||||
control.fetchInFlight = make(chan struct{})
|
||||
|
||||
quit := make(chan struct{})
|
||||
defer close(quit)
|
||||
|
||||
// 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, chan error) {
|
||||
|
||||
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)
|
||||
paymentResultErr := make(chan error)
|
||||
paymentResult := make(chan *htlcswitch.PaymentResult)
|
||||
|
||||
payer := &mockPayer{
|
||||
sendResult: sendResult,
|
||||
paymentResult: paymentResult,
|
||||
paymentResultErr: paymentResultErr,
|
||||
}
|
||||
|
||||
router, err := New(Config{
|
||||
Graph: testGraph.graph,
|
||||
Chain: chain,
|
||||
ChainView: chainView,
|
||||
Control: control,
|
||||
SessionSource: &mockPaymentSessionSource{},
|
||||
MissionControl: &mockMissionControl{},
|
||||
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, paymentResultErr
|
||||
}
|
||||
|
||||
router, sendResult, getPaymentResult, getPaymentResultErr := setupRouter()
|
||||
defer router.Stop()
|
||||
|
||||
for _, test := range tests {
|
||||
// Craft a LightningPayment struct.
|
||||
var preImage lntypes.Preimage
|
||||
if _, err := rand.Read(preImage[:]); err != nil {
|
||||
t.Fatalf("unable to generate preimage")
|
||||
}
|
||||
|
||||
payHash := preImage.Hash()
|
||||
|
||||
paymentAmt := lnwire.NewMSatFromSatoshis(1000)
|
||||
payment := LightningPayment{
|
||||
Target: testGraph.aliasMap["c"],
|
||||
Amount: paymentAmt,
|
||||
FeeLimit: noFeeLimit,
|
||||
PaymentHash: payHash,
|
||||
}
|
||||
|
||||
router.cfg.SessionSource = &mockPaymentSessionSource{
|
||||
routes: test.routes,
|
||||
}
|
||||
|
||||
router.cfg.MissionControl = &mockMissionControl{}
|
||||
|
||||
// Send the payment. Since this is new payment hash, the
|
||||
// information should be registered with the ControlTower.
|
||||
paymentResult := make(chan error)
|
||||
go func() {
|
||||
_, _, err := router.SendPayment(&payment)
|
||||
paymentResult <- err
|
||||
}()
|
||||
|
||||
var resendResult chan error
|
||||
for _, step := range test.steps {
|
||||
switch step {
|
||||
|
||||
case routerInitPayment:
|
||||
var args initArgs
|
||||
select {
|
||||
case args = <-control.init:
|
||||
case <-time.After(1 * time.Second):
|
||||
t.Fatalf("no init payment with control")
|
||||
}
|
||||
|
||||
if args.c == nil {
|
||||
t.Fatalf("expected non-nil CreationInfo")
|
||||
}
|
||||
|
||||
// In this step we expect the router to make a call to
|
||||
// register a new attempt with the ControlTower.
|
||||
case routerRegisterAttempt:
|
||||
var args registerArgs
|
||||
select {
|
||||
case args = <-control.register:
|
||||
case <-time.After(1 * time.Second):
|
||||
t.Fatalf("not registered with control")
|
||||
}
|
||||
|
||||
if args.a == nil {
|
||||
t.Fatalf("expected non-nil AttemptInfo")
|
||||
}
|
||||
|
||||
// In this step we expect the router to call the
|
||||
// ControlTower's Succcess method with the preimage.
|
||||
case routerSuccess:
|
||||
select {
|
||||
case _ = <-control.success:
|
||||
case <-time.After(1 * time.Second):
|
||||
t.Fatalf("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 routerFail:
|
||||
select {
|
||||
case _ = <-control.fail:
|
||||
case <-time.After(1 * time.Second):
|
||||
t.Fatalf("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(1 * time.Second):
|
||||
t.Fatalf("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(1 * time.Second):
|
||||
t.Fatalf("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(1 * time.Second):
|
||||
t.Fatalf("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 getPaymentResultFailure:
|
||||
failure := htlcswitch.NewForwardingError(
|
||||
&lnwire.FailTemporaryChannelFailure{},
|
||||
1,
|
||||
)
|
||||
|
||||
select {
|
||||
case getPaymentResult <- &htlcswitch.PaymentResult{
|
||||
Error: failure,
|
||||
}:
|
||||
case <-time.After(1 * time.Second):
|
||||
t.Fatalf("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 alreayd 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:
|
||||
select {
|
||||
case getPaymentResultErr <- fmt.Errorf(
|
||||
"shutting down"):
|
||||
case <-time.After(1 * time.Second):
|
||||
t.Fatalf("unable to send payment " +
|
||||
"result error")
|
||||
}
|
||||
|
||||
if err := router.Stop(); err != nil {
|
||||
t.Fatalf("unable to restart: %v", err)
|
||||
}
|
||||
|
||||
// In this step we manually start the router.
|
||||
case startRouter:
|
||||
router, sendResult, getPaymentResult,
|
||||
getPaymentResultErr = setupRouter()
|
||||
|
||||
// In this state we expect to receive an error for the
|
||||
// original payment made.
|
||||
case paymentError:
|
||||
select {
|
||||
case err := <-paymentResult:
|
||||
if err == nil {
|
||||
t.Fatalf("expected error")
|
||||
}
|
||||
|
||||
case <-time.After(1 * time.Second):
|
||||
t.Fatalf("got no payment result")
|
||||
}
|
||||
|
||||
// In this state we expect the original payment to
|
||||
// succeed.
|
||||
case paymentSuccess:
|
||||
select {
|
||||
case err := <-paymentResult:
|
||||
if err != nil {
|
||||
t.Fatalf("did not expecte error %v", err)
|
||||
}
|
||||
|
||||
case <-time.After(1 * time.Second):
|
||||
t.Fatalf("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(1 * time.Second):
|
||||
t.Fatalf("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(1 * time.Second):
|
||||
t.Fatalf("got no payment result")
|
||||
}
|
||||
|
||||
default:
|
||||
t.Fatalf("unknown step %v", step)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// TestSendToRouteStructuredError asserts that SendToRoute returns a structured
|
||||
// error.
|
||||
func TestSendToRouteStructuredError(t *testing.T) {
|
||||
|
Loading…
Reference in New Issue
Block a user