Merge pull request #5214 from carlaKC/4788-terminalshard
routing: handle failure to launch shard after permanent failure
This commit is contained in:
commit
4d358a84e4
@ -290,18 +290,19 @@ func (p *PaymentControl) RegisterAttempt(paymentHash lntypes.Hash,
|
|||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
// Ensure the payment is in-flight.
|
|
||||||
if err := ensureInFlight(p); err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
// We cannot register a new attempt if the payment already has
|
// We cannot register a new attempt if the payment already has
|
||||||
// reached a terminal condition:
|
// reached a terminal condition. We check this before
|
||||||
|
// ensureInFlight because it is a more general check.
|
||||||
settle, fail := p.TerminalInfo()
|
settle, fail := p.TerminalInfo()
|
||||||
if settle != nil || fail != nil {
|
if settle != nil || fail != nil {
|
||||||
return ErrPaymentTerminal
|
return ErrPaymentTerminal
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Ensure the payment is in-flight.
|
||||||
|
if err := ensureInFlight(p); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
// Make sure any existing shards match the new one with regards
|
// Make sure any existing shards match the new one with regards
|
||||||
// to MPP options.
|
// to MPP options.
|
||||||
mpp := attempt.Route.FinalHop().MPP
|
mpp := attempt.Route.FinalHop().MPP
|
||||||
|
@ -1013,19 +1013,15 @@ func TestPaymentControlMultiShard(t *testing.T) {
|
|||||||
// up in the Succeeded state. If both failed the payment should
|
// up in the Succeeded state. If both failed the payment should
|
||||||
// also be Failed at this poinnt.
|
// also be Failed at this poinnt.
|
||||||
finalStatus := StatusFailed
|
finalStatus := StatusFailed
|
||||||
expRegErr := ErrPaymentAlreadyFailed
|
|
||||||
if test.settleFirst || test.settleLast {
|
if test.settleFirst || test.settleLast {
|
||||||
finalStatus = StatusSucceeded
|
finalStatus = StatusSucceeded
|
||||||
expRegErr = ErrPaymentAlreadySucceeded
|
|
||||||
}
|
}
|
||||||
|
|
||||||
assertPaymentStatus(t, pControl, info.PaymentHash, finalStatus)
|
assertPaymentStatus(t, pControl, info.PaymentHash, finalStatus)
|
||||||
|
|
||||||
// Finally assert we cannot register more attempts.
|
// Finally assert we cannot register more attempts.
|
||||||
_, err = pControl.RegisterAttempt(info.PaymentHash, &b)
|
_, err = pControl.RegisterAttempt(info.PaymentHash, &b)
|
||||||
if err != expRegErr {
|
require.Equal(t, ErrPaymentTerminal, err)
|
||||||
t.Fatalf("expected error %v, got: %v", expRegErr, err)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
for _, test := range tests {
|
for _, test := range tests {
|
||||||
|
@ -84,6 +84,7 @@ func (m *mockPaymentAttemptDispatcher) setPaymentResult(
|
|||||||
|
|
||||||
type mockPaymentSessionSource struct {
|
type mockPaymentSessionSource struct {
|
||||||
routes []*route.Route
|
routes []*route.Route
|
||||||
|
routeRelease chan struct{}
|
||||||
}
|
}
|
||||||
|
|
||||||
var _ PaymentSessionSource = (*mockPaymentSessionSource)(nil)
|
var _ PaymentSessionSource = (*mockPaymentSessionSource)(nil)
|
||||||
@ -91,7 +92,10 @@ var _ PaymentSessionSource = (*mockPaymentSessionSource)(nil)
|
|||||||
func (m *mockPaymentSessionSource) NewPaymentSession(
|
func (m *mockPaymentSessionSource) NewPaymentSession(
|
||||||
_ *LightningPayment) (PaymentSession, error) {
|
_ *LightningPayment) (PaymentSession, error) {
|
||||||
|
|
||||||
return &mockPaymentSession{m.routes}, nil
|
return &mockPaymentSession{
|
||||||
|
routes: m.routes,
|
||||||
|
release: m.routeRelease,
|
||||||
|
}, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (m *mockPaymentSessionSource) NewPaymentSessionForRoute(
|
func (m *mockPaymentSessionSource) NewPaymentSessionForRoute(
|
||||||
@ -137,6 +141,11 @@ func (m *mockMissionControl) GetProbability(fromNode, toNode route.Vertex,
|
|||||||
|
|
||||||
type mockPaymentSession struct {
|
type mockPaymentSession struct {
|
||||||
routes []*route.Route
|
routes []*route.Route
|
||||||
|
|
||||||
|
// release is a channel that optionally blocks requesting a route
|
||||||
|
// from our mock payment channel. If this value is nil, we will just
|
||||||
|
// release the route automatically.
|
||||||
|
release chan struct{}
|
||||||
}
|
}
|
||||||
|
|
||||||
var _ PaymentSession = (*mockPaymentSession)(nil)
|
var _ PaymentSession = (*mockPaymentSession)(nil)
|
||||||
@ -144,6 +153,10 @@ var _ PaymentSession = (*mockPaymentSession)(nil)
|
|||||||
func (m *mockPaymentSession) RequestRoute(_, _ lnwire.MilliSatoshi,
|
func (m *mockPaymentSession) RequestRoute(_, _ lnwire.MilliSatoshi,
|
||||||
_, height uint32) (*route.Route, error) {
|
_, height uint32) (*route.Route, error) {
|
||||||
|
|
||||||
|
if m.release != nil {
|
||||||
|
m.release <- struct{}{}
|
||||||
|
}
|
||||||
|
|
||||||
if len(m.routes) == 0 {
|
if len(m.routes) == 0 {
|
||||||
return nil, errNoPathFound
|
return nil, errNoPathFound
|
||||||
}
|
}
|
||||||
@ -156,7 +169,6 @@ func (m *mockPaymentSession) RequestRoute(_, _ lnwire.MilliSatoshi,
|
|||||||
|
|
||||||
type mockPayer struct {
|
type mockPayer struct {
|
||||||
sendResult chan error
|
sendResult chan error
|
||||||
paymentResultErr chan error
|
|
||||||
paymentResult chan *htlcswitch.PaymentResult
|
paymentResult chan *htlcswitch.PaymentResult
|
||||||
quit chan struct{}
|
quit chan struct{}
|
||||||
}
|
}
|
||||||
@ -180,12 +192,16 @@ func (m *mockPayer) GetPaymentResult(paymentID uint64, _ lntypes.Hash,
|
|||||||
_ htlcswitch.ErrorDecrypter) (<-chan *htlcswitch.PaymentResult, error) {
|
_ htlcswitch.ErrorDecrypter) (<-chan *htlcswitch.PaymentResult, error) {
|
||||||
|
|
||||||
select {
|
select {
|
||||||
case res := <-m.paymentResult:
|
case res, ok := <-m.paymentResult:
|
||||||
resChan := make(chan *htlcswitch.PaymentResult, 1)
|
resChan := make(chan *htlcswitch.PaymentResult, 1)
|
||||||
|
if !ok {
|
||||||
|
close(resChan)
|
||||||
|
} else {
|
||||||
resChan <- res
|
resChan <- res
|
||||||
|
}
|
||||||
|
|
||||||
return resChan, nil
|
return resChan, nil
|
||||||
case err := <-m.paymentResultErr:
|
|
||||||
return nil, err
|
|
||||||
case <-m.quit:
|
case <-m.quit:
|
||||||
return nil, fmt.Errorf("test quitting")
|
return nil, fmt.Errorf("test quitting")
|
||||||
}
|
}
|
||||||
@ -248,13 +264,13 @@ func makeMockControlTower() *mockControlTower {
|
|||||||
func (m *mockControlTower) InitPayment(phash lntypes.Hash,
|
func (m *mockControlTower) InitPayment(phash lntypes.Hash,
|
||||||
c *channeldb.PaymentCreationInfo) error {
|
c *channeldb.PaymentCreationInfo) error {
|
||||||
|
|
||||||
m.Lock()
|
|
||||||
defer m.Unlock()
|
|
||||||
|
|
||||||
if m.init != nil {
|
if m.init != nil {
|
||||||
m.init <- initArgs{c}
|
m.init <- initArgs{c}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
m.Lock()
|
||||||
|
defer m.Unlock()
|
||||||
|
|
||||||
// Don't allow re-init a successful payment.
|
// Don't allow re-init a successful payment.
|
||||||
if _, ok := m.successful[phash]; ok {
|
if _, ok := m.successful[phash]; ok {
|
||||||
return channeldb.ErrAlreadyPaid
|
return channeldb.ErrAlreadyPaid
|
||||||
@ -279,27 +295,49 @@ func (m *mockControlTower) InitPayment(phash lntypes.Hash,
|
|||||||
func (m *mockControlTower) RegisterAttempt(phash lntypes.Hash,
|
func (m *mockControlTower) RegisterAttempt(phash lntypes.Hash,
|
||||||
a *channeldb.HTLCAttemptInfo) error {
|
a *channeldb.HTLCAttemptInfo) error {
|
||||||
|
|
||||||
m.Lock()
|
|
||||||
defer m.Unlock()
|
|
||||||
|
|
||||||
if m.registerAttempt != nil {
|
if m.registerAttempt != nil {
|
||||||
m.registerAttempt <- registerAttemptArgs{a}
|
m.registerAttempt <- registerAttemptArgs{a}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Cannot register attempts for successful or failed payments.
|
m.Lock()
|
||||||
if _, ok := m.successful[phash]; ok {
|
defer m.Unlock()
|
||||||
return channeldb.ErrPaymentAlreadySucceeded
|
|
||||||
}
|
|
||||||
|
|
||||||
if _, ok := m.failed[phash]; ok {
|
|
||||||
return channeldb.ErrPaymentAlreadyFailed
|
|
||||||
}
|
|
||||||
|
|
||||||
|
// Lookup payment.
|
||||||
p, ok := m.payments[phash]
|
p, ok := m.payments[phash]
|
||||||
if !ok {
|
if !ok {
|
||||||
return channeldb.ErrPaymentNotInitiated
|
return channeldb.ErrPaymentNotInitiated
|
||||||
}
|
}
|
||||||
|
|
||||||
|
var inFlight bool
|
||||||
|
for _, a := range p.attempts {
|
||||||
|
if a.Settle != nil {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
if a.Failure != nil {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
inFlight = true
|
||||||
|
}
|
||||||
|
|
||||||
|
// Cannot register attempts for successful or failed payments.
|
||||||
|
_, settled := m.successful[phash]
|
||||||
|
_, failed := m.failed[phash]
|
||||||
|
|
||||||
|
if settled || failed {
|
||||||
|
return channeldb.ErrPaymentTerminal
|
||||||
|
}
|
||||||
|
|
||||||
|
if settled && !inFlight {
|
||||||
|
return channeldb.ErrPaymentAlreadySucceeded
|
||||||
|
}
|
||||||
|
|
||||||
|
if failed && !inFlight {
|
||||||
|
return channeldb.ErrPaymentAlreadyFailed
|
||||||
|
}
|
||||||
|
|
||||||
|
// Add attempt to payment.
|
||||||
p.attempts = append(p.attempts, channeldb.HTLCAttempt{
|
p.attempts = append(p.attempts, channeldb.HTLCAttempt{
|
||||||
HTLCAttemptInfo: *a,
|
HTLCAttemptInfo: *a,
|
||||||
})
|
})
|
||||||
@ -312,13 +350,13 @@ func (m *mockControlTower) SettleAttempt(phash lntypes.Hash,
|
|||||||
pid uint64, settleInfo *channeldb.HTLCSettleInfo) (
|
pid uint64, settleInfo *channeldb.HTLCSettleInfo) (
|
||||||
*channeldb.HTLCAttempt, error) {
|
*channeldb.HTLCAttempt, error) {
|
||||||
|
|
||||||
m.Lock()
|
|
||||||
defer m.Unlock()
|
|
||||||
|
|
||||||
if m.settleAttempt != nil {
|
if m.settleAttempt != nil {
|
||||||
m.settleAttempt <- settleAttemptArgs{settleInfo.Preimage}
|
m.settleAttempt <- settleAttemptArgs{settleInfo.Preimage}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
m.Lock()
|
||||||
|
defer m.Unlock()
|
||||||
|
|
||||||
// Only allow setting attempts if the payment is known.
|
// Only allow setting attempts if the payment is known.
|
||||||
p, ok := m.payments[phash]
|
p, ok := m.payments[phash]
|
||||||
if !ok {
|
if !ok {
|
||||||
@ -353,13 +391,13 @@ func (m *mockControlTower) SettleAttempt(phash lntypes.Hash,
|
|||||||
func (m *mockControlTower) FailAttempt(phash lntypes.Hash, pid uint64,
|
func (m *mockControlTower) FailAttempt(phash lntypes.Hash, pid uint64,
|
||||||
failInfo *channeldb.HTLCFailInfo) (*channeldb.HTLCAttempt, error) {
|
failInfo *channeldb.HTLCFailInfo) (*channeldb.HTLCAttempt, error) {
|
||||||
|
|
||||||
m.Lock()
|
|
||||||
defer m.Unlock()
|
|
||||||
|
|
||||||
if m.failAttempt != nil {
|
if m.failAttempt != nil {
|
||||||
m.failAttempt <- failAttemptArgs{failInfo}
|
m.failAttempt <- failAttemptArgs{failInfo}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
m.Lock()
|
||||||
|
defer m.Unlock()
|
||||||
|
|
||||||
// Only allow failing attempts if the payment is known.
|
// Only allow failing attempts if the payment is known.
|
||||||
p, ok := m.payments[phash]
|
p, ok := m.payments[phash]
|
||||||
if !ok {
|
if !ok {
|
||||||
@ -437,13 +475,13 @@ func (m *mockControlTower) FetchPayment(phash lntypes.Hash) (
|
|||||||
func (m *mockControlTower) FetchInFlightPayments() (
|
func (m *mockControlTower) FetchInFlightPayments() (
|
||||||
[]*channeldb.InFlightPayment, error) {
|
[]*channeldb.InFlightPayment, error) {
|
||||||
|
|
||||||
m.Lock()
|
|
||||||
defer m.Unlock()
|
|
||||||
|
|
||||||
if m.fetchInFlight != nil {
|
if m.fetchInFlight != nil {
|
||||||
m.fetchInFlight <- struct{}{}
|
m.fetchInFlight <- struct{}{}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
m.Lock()
|
||||||
|
defer m.Unlock()
|
||||||
|
|
||||||
// In flight are all payments not successful or failed.
|
// In flight are all payments not successful or failed.
|
||||||
var fl []*channeldb.InFlightPayment
|
var fl []*channeldb.InFlightPayment
|
||||||
for hash, p := range m.payments {
|
for hash, p := range m.payments {
|
||||||
|
@ -115,6 +115,7 @@ func (p *paymentLifecycle) resumePayment() ([32]byte, *route.Route, error) {
|
|||||||
|
|
||||||
// We'll continue until either our payment succeeds, or we encounter a
|
// We'll continue until either our payment succeeds, or we encounter a
|
||||||
// critical error during path finding.
|
// critical error during path finding.
|
||||||
|
lifecycle:
|
||||||
for {
|
for {
|
||||||
// Start by quickly checking if there are any outcomes already
|
// Start by quickly checking if there are any outcomes already
|
||||||
// available to handle before we reevaluate our state.
|
// available to handle before we reevaluate our state.
|
||||||
@ -171,7 +172,7 @@ func (p *paymentLifecycle) resumePayment() ([32]byte, *route.Route, error) {
|
|||||||
if err := shardHandler.waitForShard(); err != nil {
|
if err := shardHandler.waitForShard(); err != nil {
|
||||||
return [32]byte{}, nil, err
|
return [32]byte{}, nil, err
|
||||||
}
|
}
|
||||||
continue
|
continue lifecycle
|
||||||
}
|
}
|
||||||
|
|
||||||
// Before we attempt any new shard, we'll check to see if
|
// Before we attempt any new shard, we'll check to see if
|
||||||
@ -195,7 +196,7 @@ func (p *paymentLifecycle) resumePayment() ([32]byte, *route.Route, error) {
|
|||||||
return [32]byte{}, nil, saveErr
|
return [32]byte{}, nil, saveErr
|
||||||
}
|
}
|
||||||
|
|
||||||
continue
|
continue lifecycle
|
||||||
|
|
||||||
case <-p.router.quit:
|
case <-p.router.quit:
|
||||||
return [32]byte{}, nil, ErrRouterShuttingDown
|
return [32]byte{}, nil, ErrRouterShuttingDown
|
||||||
@ -234,7 +235,7 @@ func (p *paymentLifecycle) resumePayment() ([32]byte, *route.Route, error) {
|
|||||||
return [32]byte{}, nil, saveErr
|
return [32]byte{}, nil, saveErr
|
||||||
}
|
}
|
||||||
|
|
||||||
continue
|
continue lifecycle
|
||||||
}
|
}
|
||||||
|
|
||||||
// We still have active shards, we'll wait for an
|
// We still have active shards, we'll wait for an
|
||||||
@ -242,12 +243,23 @@ func (p *paymentLifecycle) resumePayment() ([32]byte, *route.Route, error) {
|
|||||||
if err := shardHandler.waitForShard(); err != nil {
|
if err := shardHandler.waitForShard(); err != nil {
|
||||||
return [32]byte{}, nil, err
|
return [32]byte{}, nil, err
|
||||||
}
|
}
|
||||||
continue
|
continue lifecycle
|
||||||
}
|
}
|
||||||
|
|
||||||
// We found a route to try, launch a new shard.
|
// We found a route to try, launch a new shard.
|
||||||
attempt, outcome, err := shardHandler.launchShard(rt)
|
attempt, outcome, err := shardHandler.launchShard(rt)
|
||||||
if err != nil {
|
switch {
|
||||||
|
// We may get a terminal error if we've processed a shard with
|
||||||
|
// a terminal state (settled or permanent failure), while we
|
||||||
|
// were pathfinding. We know we're in a terminal state here,
|
||||||
|
// so we can continue and wait for our last shards to return.
|
||||||
|
case err == channeldb.ErrPaymentTerminal:
|
||||||
|
log.Infof("Payment: %v in terminal state, abandoning "+
|
||||||
|
"shard", p.paymentHash)
|
||||||
|
|
||||||
|
continue lifecycle
|
||||||
|
|
||||||
|
case err != nil:
|
||||||
return [32]byte{}, nil, err
|
return [32]byte{}, nil, err
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -270,7 +282,7 @@ func (p *paymentLifecycle) resumePayment() ([32]byte, *route.Route, error) {
|
|||||||
|
|
||||||
// Error was handled successfully, continue to make a
|
// Error was handled successfully, continue to make a
|
||||||
// new attempt.
|
// new attempt.
|
||||||
continue
|
continue lifecycle
|
||||||
}
|
}
|
||||||
|
|
||||||
// Now that the shard was successfully sent, launch a go
|
// Now that the shard was successfully sent, launch a go
|
||||||
|
@ -2,7 +2,6 @@ package routing
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"crypto/rand"
|
"crypto/rand"
|
||||||
"fmt"
|
|
||||||
"sync/atomic"
|
"sync/atomic"
|
||||||
"testing"
|
"testing"
|
||||||
"time"
|
"time"
|
||||||
@ -15,6 +14,7 @@ import (
|
|||||||
"github.com/lightningnetwork/lnd/lntypes"
|
"github.com/lightningnetwork/lnd/lntypes"
|
||||||
"github.com/lightningnetwork/lnd/lnwire"
|
"github.com/lightningnetwork/lnd/lnwire"
|
||||||
"github.com/lightningnetwork/lnd/routing/route"
|
"github.com/lightningnetwork/lnd/routing/route"
|
||||||
|
"github.com/stretchr/testify/require"
|
||||||
)
|
)
|
||||||
|
|
||||||
const stepTimeout = 5 * time.Second
|
const stepTimeout = 5 * time.Second
|
||||||
@ -48,62 +48,22 @@ func createTestRoute(amt lnwire.MilliSatoshi,
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
// TestRouterPaymentStateMachine tests that the router interacts as expected
|
// paymentLifecycleTestCase contains the steps that we expect for a payment
|
||||||
// with the ControlTower during a payment lifecycle, such that it payment
|
// lifecycle test, and the routes that pathfinding should deliver.
|
||||||
// attempts are not sent twice to the switch, and results are handled after a
|
type paymentLifecycleTestCase struct {
|
||||||
// restart.
|
name string
|
||||||
func TestRouterPaymentStateMachine(t *testing.T) {
|
|
||||||
t.Parallel()
|
|
||||||
|
|
||||||
const startingBlockHeight = 101
|
|
||||||
|
|
||||||
// Setup two simple channels such that we can mock sending along this
|
|
||||||
// route.
|
|
||||||
chanCapSat := btcutil.Amount(100000)
|
|
||||||
testChannels := []*testChannel{
|
|
||||||
symmetricTestChannel("a", "b", chanCapSat, &testChannelPolicy{
|
|
||||||
Expiry: 144,
|
|
||||||
FeeRate: 400,
|
|
||||||
MinHTLC: 1,
|
|
||||||
MaxHTLC: lnwire.NewMSatFromSatoshis(chanCapSat),
|
|
||||||
}, 1),
|
|
||||||
symmetricTestChannel("b", "c", chanCapSat, &testChannelPolicy{
|
|
||||||
Expiry: 144,
|
|
||||||
FeeRate: 400,
|
|
||||||
MinHTLC: 1,
|
|
||||||
MaxHTLC: lnwire.NewMSatFromSatoshis(chanCapSat),
|
|
||||||
}, 2),
|
|
||||||
}
|
|
||||||
|
|
||||||
testGraph, err := createTestGraphFromChannels(testChannels, "a")
|
|
||||||
if err != nil {
|
|
||||||
t.Fatalf("unable to create graph: %v", err)
|
|
||||||
}
|
|
||||||
defer testGraph.cleanUp()
|
|
||||||
|
|
||||||
paymentAmt := lnwire.NewMSatFromSatoshis(1000)
|
|
||||||
|
|
||||||
// We create a simple route that we will supply every time the router
|
|
||||||
// requests one.
|
|
||||||
rt, err := createTestRoute(paymentAmt, testGraph.aliasMap)
|
|
||||||
if err != nil {
|
|
||||||
t.Fatalf("unable to create route: %v", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
shard, err := createTestRoute(paymentAmt/4, testGraph.aliasMap)
|
|
||||||
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 is a list of steps to perform during the testcase.
|
||||||
steps []string
|
steps []string
|
||||||
|
|
||||||
// routes is the sequence of routes we will provide to the
|
// routes is the sequence of routes we will provide to the
|
||||||
// router when it requests a new route.
|
// router when it requests a new route.
|
||||||
routes []*route.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 (
|
const (
|
||||||
@ -129,6 +89,10 @@ func TestRouterPaymentStateMachine(t *testing.T) {
|
|||||||
// to call the Fail method on the control tower.
|
// to call the Fail method on the control tower.
|
||||||
routerFailPayment = "Router:fail-payment"
|
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
|
// sendToSwitchSuccess is a step where we expect the router to
|
||||||
// call send the payment attempt to the switch, and we will
|
// call send the payment attempt to the switch, and we will
|
||||||
// respond with a non-error, indicating that the payment
|
// respond with a non-error, indicating that the payment
|
||||||
@ -188,11 +152,64 @@ func TestRouterPaymentStateMachine(t *testing.T) {
|
|||||||
resentPaymentError = "ResentPaymentError"
|
resentPaymentError = "ResentPaymentError"
|
||||||
)
|
)
|
||||||
|
|
||||||
tests := []testCase{
|
// TestRouterPaymentStateMachine tests that the router interacts as expected
|
||||||
|
// with the ControlTower during a payment lifecycle, such that it payment
|
||||||
|
// attempts are not sent twice to the switch, and results are handled after a
|
||||||
|
// restart.
|
||||||
|
func TestRouterPaymentStateMachine(t *testing.T) {
|
||||||
|
t.Parallel()
|
||||||
|
|
||||||
|
const startingBlockHeight = 101
|
||||||
|
|
||||||
|
// Setup two simple channels such that we can mock sending along this
|
||||||
|
// route.
|
||||||
|
chanCapSat := btcutil.Amount(100000)
|
||||||
|
testChannels := []*testChannel{
|
||||||
|
symmetricTestChannel("a", "b", chanCapSat, &testChannelPolicy{
|
||||||
|
Expiry: 144,
|
||||||
|
FeeRate: 400,
|
||||||
|
MinHTLC: 1,
|
||||||
|
MaxHTLC: lnwire.NewMSatFromSatoshis(chanCapSat),
|
||||||
|
}, 1),
|
||||||
|
symmetricTestChannel("b", "c", chanCapSat, &testChannelPolicy{
|
||||||
|
Expiry: 144,
|
||||||
|
FeeRate: 400,
|
||||||
|
MinHTLC: 1,
|
||||||
|
MaxHTLC: lnwire.NewMSatFromSatoshis(chanCapSat),
|
||||||
|
}, 2),
|
||||||
|
}
|
||||||
|
|
||||||
|
testGraph, err := createTestGraphFromChannels(testChannels, "a")
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("unable to create graph: %v", err)
|
||||||
|
}
|
||||||
|
defer testGraph.cleanUp()
|
||||||
|
|
||||||
|
paymentAmt := lnwire.NewMSatFromSatoshis(1000)
|
||||||
|
|
||||||
|
// We create a simple route that we will supply every time the router
|
||||||
|
// requests one.
|
||||||
|
rt, err := createTestRoute(paymentAmt, testGraph.aliasMap)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("unable to create route: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
halfShard, err := createTestRoute(paymentAmt/2, testGraph.aliasMap)
|
||||||
|
require.NoError(t, err, "unable to create half route")
|
||||||
|
|
||||||
|
shard, err := createTestRoute(paymentAmt/4, testGraph.aliasMap)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("unable to create route: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
tests := []paymentLifecycleTestCase{
|
||||||
{
|
{
|
||||||
// Tests a normal payment flow that succeeds.
|
// Tests a normal payment flow that succeeds.
|
||||||
|
name: "single shot success",
|
||||||
|
|
||||||
steps: []string{
|
steps: []string{
|
||||||
routerInitPayment,
|
routerInitPayment,
|
||||||
|
routeRelease,
|
||||||
routerRegisterAttempt,
|
routerRegisterAttempt,
|
||||||
sendToSwitchSuccess,
|
sendToSwitchSuccess,
|
||||||
getPaymentResultSuccess,
|
getPaymentResultSuccess,
|
||||||
@ -204,8 +221,11 @@ func TestRouterPaymentStateMachine(t *testing.T) {
|
|||||||
{
|
{
|
||||||
// A payment flow with a failure on the first attempt,
|
// A payment flow with a failure on the first attempt,
|
||||||
// but that succeeds on the second attempt.
|
// but that succeeds on the second attempt.
|
||||||
|
name: "single shot retry",
|
||||||
|
|
||||||
steps: []string{
|
steps: []string{
|
||||||
routerInitPayment,
|
routerInitPayment,
|
||||||
|
routeRelease,
|
||||||
routerRegisterAttempt,
|
routerRegisterAttempt,
|
||||||
sendToSwitchSuccess,
|
sendToSwitchSuccess,
|
||||||
|
|
||||||
@ -214,6 +234,7 @@ func TestRouterPaymentStateMachine(t *testing.T) {
|
|||||||
routerFailAttempt,
|
routerFailAttempt,
|
||||||
|
|
||||||
// The router should retry.
|
// The router should retry.
|
||||||
|
routeRelease,
|
||||||
routerRegisterAttempt,
|
routerRegisterAttempt,
|
||||||
sendToSwitchSuccess,
|
sendToSwitchSuccess,
|
||||||
|
|
||||||
@ -228,8 +249,11 @@ func TestRouterPaymentStateMachine(t *testing.T) {
|
|||||||
// A payment flow with a forwarding failure first time
|
// A payment flow with a forwarding failure first time
|
||||||
// sending to the switch, but that succeeds on the
|
// sending to the switch, but that succeeds on the
|
||||||
// second attempt.
|
// second attempt.
|
||||||
|
name: "single shot switch failure",
|
||||||
|
|
||||||
steps: []string{
|
steps: []string{
|
||||||
routerInitPayment,
|
routerInitPayment,
|
||||||
|
routeRelease,
|
||||||
routerRegisterAttempt,
|
routerRegisterAttempt,
|
||||||
|
|
||||||
// Make the first sent attempt fail.
|
// Make the first sent attempt fail.
|
||||||
@ -237,6 +261,7 @@ func TestRouterPaymentStateMachine(t *testing.T) {
|
|||||||
routerFailAttempt,
|
routerFailAttempt,
|
||||||
|
|
||||||
// The router should retry.
|
// The router should retry.
|
||||||
|
routeRelease,
|
||||||
routerRegisterAttempt,
|
routerRegisterAttempt,
|
||||||
sendToSwitchSuccess,
|
sendToSwitchSuccess,
|
||||||
|
|
||||||
@ -251,8 +276,11 @@ func TestRouterPaymentStateMachine(t *testing.T) {
|
|||||||
// A payment that fails on the first attempt, and has
|
// A payment that fails on the first attempt, and has
|
||||||
// only one route available to try. It will therefore
|
// only one route available to try. It will therefore
|
||||||
// fail permanently.
|
// fail permanently.
|
||||||
|
name: "single shot route fails",
|
||||||
|
|
||||||
steps: []string{
|
steps: []string{
|
||||||
routerInitPayment,
|
routerInitPayment,
|
||||||
|
routeRelease,
|
||||||
routerRegisterAttempt,
|
routerRegisterAttempt,
|
||||||
sendToSwitchSuccess,
|
sendToSwitchSuccess,
|
||||||
|
|
||||||
@ -260,30 +288,40 @@ func TestRouterPaymentStateMachine(t *testing.T) {
|
|||||||
getPaymentResultTempFailure,
|
getPaymentResultTempFailure,
|
||||||
routerFailAttempt,
|
routerFailAttempt,
|
||||||
|
|
||||||
|
routeRelease,
|
||||||
|
|
||||||
// Since there are no more routes to try, the
|
// Since there are no more routes to try, the
|
||||||
// payment should fail.
|
// payment should fail.
|
||||||
routerFailPayment,
|
routerFailPayment,
|
||||||
paymentError,
|
paymentError,
|
||||||
},
|
},
|
||||||
routes: []*route.Route{rt},
|
routes: []*route.Route{rt},
|
||||||
|
paymentErr: channeldb.FailureReasonNoRoute,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
// We expect the payment to fail immediately if we have
|
// We expect the payment to fail immediately if we have
|
||||||
// no routes to try.
|
// no routes to try.
|
||||||
|
name: "single shot no route",
|
||||||
|
|
||||||
steps: []string{
|
steps: []string{
|
||||||
routerInitPayment,
|
routerInitPayment,
|
||||||
|
routeRelease,
|
||||||
routerFailPayment,
|
routerFailPayment,
|
||||||
paymentError,
|
paymentError,
|
||||||
},
|
},
|
||||||
routes: []*route.Route{},
|
routes: []*route.Route{},
|
||||||
|
paymentErr: channeldb.FailureReasonNoRoute,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
// A normal payment flow, where we attempt to resend
|
// A normal payment flow, where we attempt to resend
|
||||||
// the same payment after each step. This ensures that
|
// the same payment after each step. This ensures that
|
||||||
// the router don't attempt to resend a payment already
|
// the router don't attempt to resend a payment already
|
||||||
// in flight.
|
// in flight.
|
||||||
|
name: "single shot resend",
|
||||||
|
|
||||||
steps: []string{
|
steps: []string{
|
||||||
routerInitPayment,
|
routerInitPayment,
|
||||||
|
routeRelease,
|
||||||
routerRegisterAttempt,
|
routerRegisterAttempt,
|
||||||
|
|
||||||
// Manually resend the payment, the router
|
// Manually resend the payment, the router
|
||||||
@ -322,8 +360,11 @@ func TestRouterPaymentStateMachine(t *testing.T) {
|
|||||||
{
|
{
|
||||||
// Tests that the router is able to handle the
|
// Tests that the router is able to handle the
|
||||||
// receieved payment result after a restart.
|
// receieved payment result after a restart.
|
||||||
|
name: "single shot restart",
|
||||||
|
|
||||||
steps: []string{
|
steps: []string{
|
||||||
routerInitPayment,
|
routerInitPayment,
|
||||||
|
routeRelease,
|
||||||
routerRegisterAttempt,
|
routerRegisterAttempt,
|
||||||
sendToSwitchSuccess,
|
sendToSwitchSuccess,
|
||||||
|
|
||||||
@ -340,12 +381,16 @@ func TestRouterPaymentStateMachine(t *testing.T) {
|
|||||||
routerSettleAttempt,
|
routerSettleAttempt,
|
||||||
},
|
},
|
||||||
routes: []*route.Route{rt},
|
routes: []*route.Route{rt},
|
||||||
|
paymentErr: ErrRouterShuttingDown,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
// Tests that we are allowed to resend a payment after
|
// Tests that we are allowed to resend a payment after
|
||||||
// it has permanently failed.
|
// it has permanently failed.
|
||||||
|
name: "single shot resend fail",
|
||||||
|
|
||||||
steps: []string{
|
steps: []string{
|
||||||
routerInitPayment,
|
routerInitPayment,
|
||||||
|
routeRelease,
|
||||||
routerRegisterAttempt,
|
routerRegisterAttempt,
|
||||||
sendToSwitchSuccess,
|
sendToSwitchSuccess,
|
||||||
|
|
||||||
@ -361,6 +406,7 @@ func TestRouterPaymentStateMachine(t *testing.T) {
|
|||||||
|
|
||||||
// Since we have no more routes to try, the
|
// Since we have no more routes to try, the
|
||||||
// original payment should fail.
|
// original payment should fail.
|
||||||
|
routeRelease,
|
||||||
routerFailPayment,
|
routerFailPayment,
|
||||||
paymentError,
|
paymentError,
|
||||||
|
|
||||||
@ -368,6 +414,7 @@ func TestRouterPaymentStateMachine(t *testing.T) {
|
|||||||
// allowed, since the payment has failed.
|
// allowed, since the payment has failed.
|
||||||
resendPayment,
|
resendPayment,
|
||||||
routerInitPayment,
|
routerInitPayment,
|
||||||
|
routeRelease,
|
||||||
routerRegisterAttempt,
|
routerRegisterAttempt,
|
||||||
sendToSwitchSuccess,
|
sendToSwitchSuccess,
|
||||||
getPaymentResultSuccess,
|
getPaymentResultSuccess,
|
||||||
@ -375,6 +422,7 @@ func TestRouterPaymentStateMachine(t *testing.T) {
|
|||||||
resentPaymentSuccess,
|
resentPaymentSuccess,
|
||||||
},
|
},
|
||||||
routes: []*route.Route{rt},
|
routes: []*route.Route{rt},
|
||||||
|
paymentErr: channeldb.FailureReasonNoRoute,
|
||||||
},
|
},
|
||||||
|
|
||||||
// =====================================
|
// =====================================
|
||||||
@ -382,22 +430,28 @@ func TestRouterPaymentStateMachine(t *testing.T) {
|
|||||||
// =====================================
|
// =====================================
|
||||||
{
|
{
|
||||||
// Tests a simple successful MP payment of 4 shards.
|
// Tests a simple successful MP payment of 4 shards.
|
||||||
|
name: "MP success",
|
||||||
|
|
||||||
steps: []string{
|
steps: []string{
|
||||||
routerInitPayment,
|
routerInitPayment,
|
||||||
|
|
||||||
// shard 0
|
// shard 0
|
||||||
|
routeRelease,
|
||||||
routerRegisterAttempt,
|
routerRegisterAttempt,
|
||||||
sendToSwitchSuccess,
|
sendToSwitchSuccess,
|
||||||
|
|
||||||
// shard 1
|
// shard 1
|
||||||
|
routeRelease,
|
||||||
routerRegisterAttempt,
|
routerRegisterAttempt,
|
||||||
sendToSwitchSuccess,
|
sendToSwitchSuccess,
|
||||||
|
|
||||||
// shard 2
|
// shard 2
|
||||||
|
routeRelease,
|
||||||
routerRegisterAttempt,
|
routerRegisterAttempt,
|
||||||
sendToSwitchSuccess,
|
sendToSwitchSuccess,
|
||||||
|
|
||||||
// shard 3
|
// shard 3
|
||||||
|
routeRelease,
|
||||||
routerRegisterAttempt,
|
routerRegisterAttempt,
|
||||||
sendToSwitchSuccess,
|
sendToSwitchSuccess,
|
||||||
|
|
||||||
@ -422,22 +476,28 @@ func TestRouterPaymentStateMachine(t *testing.T) {
|
|||||||
{
|
{
|
||||||
// An MP payment scenario where we need several extra
|
// An MP payment scenario where we need several extra
|
||||||
// attempts before the payment finally settle.
|
// attempts before the payment finally settle.
|
||||||
|
name: "MP failed shards",
|
||||||
|
|
||||||
steps: []string{
|
steps: []string{
|
||||||
routerInitPayment,
|
routerInitPayment,
|
||||||
|
|
||||||
// shard 0
|
// shard 0
|
||||||
|
routeRelease,
|
||||||
routerRegisterAttempt,
|
routerRegisterAttempt,
|
||||||
sendToSwitchSuccess,
|
sendToSwitchSuccess,
|
||||||
|
|
||||||
// shard 1
|
// shard 1
|
||||||
|
routeRelease,
|
||||||
routerRegisterAttempt,
|
routerRegisterAttempt,
|
||||||
sendToSwitchSuccess,
|
sendToSwitchSuccess,
|
||||||
|
|
||||||
// shard 2
|
// shard 2
|
||||||
|
routeRelease,
|
||||||
routerRegisterAttempt,
|
routerRegisterAttempt,
|
||||||
sendToSwitchSuccess,
|
sendToSwitchSuccess,
|
||||||
|
|
||||||
// shard 3
|
// shard 3
|
||||||
|
routeRelease,
|
||||||
routerRegisterAttempt,
|
routerRegisterAttempt,
|
||||||
sendToSwitchSuccess,
|
sendToSwitchSuccess,
|
||||||
|
|
||||||
@ -447,8 +507,10 @@ func TestRouterPaymentStateMachine(t *testing.T) {
|
|||||||
routerFailAttempt,
|
routerFailAttempt,
|
||||||
routerFailAttempt,
|
routerFailAttempt,
|
||||||
|
|
||||||
|
routeRelease,
|
||||||
routerRegisterAttempt,
|
routerRegisterAttempt,
|
||||||
sendToSwitchSuccess,
|
sendToSwitchSuccess,
|
||||||
|
routeRelease,
|
||||||
routerRegisterAttempt,
|
routerRegisterAttempt,
|
||||||
sendToSwitchSuccess,
|
sendToSwitchSuccess,
|
||||||
|
|
||||||
@ -470,65 +532,65 @@ func TestRouterPaymentStateMachine(t *testing.T) {
|
|||||||
},
|
},
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
// An MP payment scenario where 3 of the shards fail.
|
// An MP payment scenario where one of the shards fails,
|
||||||
// However the last shard settle, which means we get
|
// but we still receive a single success shard.
|
||||||
// the preimage and should consider the overall payment
|
name: "MP one shard success",
|
||||||
// a success.
|
|
||||||
steps: []string{
|
steps: []string{
|
||||||
routerInitPayment,
|
routerInitPayment,
|
||||||
|
|
||||||
// shard 0
|
// shard 0
|
||||||
|
routeRelease,
|
||||||
routerRegisterAttempt,
|
routerRegisterAttempt,
|
||||||
sendToSwitchSuccess,
|
sendToSwitchSuccess,
|
||||||
|
|
||||||
// shard 1
|
// shard 1
|
||||||
|
routeRelease,
|
||||||
routerRegisterAttempt,
|
routerRegisterAttempt,
|
||||||
sendToSwitchSuccess,
|
sendToSwitchSuccess,
|
||||||
|
|
||||||
// shard 2
|
// shard 0 fails, and should be failed by the
|
||||||
routerRegisterAttempt,
|
|
||||||
sendToSwitchSuccess,
|
|
||||||
|
|
||||||
// shard 3
|
|
||||||
routerRegisterAttempt,
|
|
||||||
sendToSwitchSuccess,
|
|
||||||
|
|
||||||
// 3 shards fail, and should be failed by the
|
|
||||||
// router.
|
// router.
|
||||||
getPaymentResultTempFailure,
|
getPaymentResultTempFailure,
|
||||||
getPaymentResultTempFailure,
|
|
||||||
getPaymentResultTempFailure,
|
|
||||||
routerFailAttempt,
|
|
||||||
routerFailAttempt,
|
|
||||||
routerFailAttempt,
|
routerFailAttempt,
|
||||||
|
|
||||||
// The fourth shard succeed against all odds,
|
// We will try one more shard because we haven't
|
||||||
|
// sent the full payment amount.
|
||||||
|
routeRelease,
|
||||||
|
|
||||||
|
// The second shard succeed against all odds,
|
||||||
// making the overall payment succeed.
|
// making the overall payment succeed.
|
||||||
getPaymentResultSuccess,
|
getPaymentResultSuccess,
|
||||||
routerSettleAttempt,
|
routerSettleAttempt,
|
||||||
paymentSuccess,
|
paymentSuccess,
|
||||||
},
|
},
|
||||||
routes: []*route.Route{shard, shard, shard, shard},
|
routes: []*route.Route{halfShard, halfShard},
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
// An MP payment scenario a shard fail with a terminal
|
// An MP payment scenario a shard fail with a terminal
|
||||||
// error, causing the router to stop attempting.
|
// error, causing the router to stop attempting.
|
||||||
|
name: "MP terminal",
|
||||||
|
|
||||||
steps: []string{
|
steps: []string{
|
||||||
routerInitPayment,
|
routerInitPayment,
|
||||||
|
|
||||||
// shard 0
|
// shard 0
|
||||||
|
routeRelease,
|
||||||
routerRegisterAttempt,
|
routerRegisterAttempt,
|
||||||
sendToSwitchSuccess,
|
sendToSwitchSuccess,
|
||||||
|
|
||||||
// shard 1
|
// shard 1
|
||||||
|
routeRelease,
|
||||||
routerRegisterAttempt,
|
routerRegisterAttempt,
|
||||||
sendToSwitchSuccess,
|
sendToSwitchSuccess,
|
||||||
|
|
||||||
// shard 2
|
// shard 2
|
||||||
|
routeRelease,
|
||||||
routerRegisterAttempt,
|
routerRegisterAttempt,
|
||||||
sendToSwitchSuccess,
|
sendToSwitchSuccess,
|
||||||
|
|
||||||
// shard 3
|
// shard 3
|
||||||
|
routeRelease,
|
||||||
routerRegisterAttempt,
|
routerRegisterAttempt,
|
||||||
sendToSwitchSuccess,
|
sendToSwitchSuccess,
|
||||||
|
|
||||||
@ -551,26 +613,122 @@ func TestRouterPaymentStateMachine(t *testing.T) {
|
|||||||
routes: []*route.Route{
|
routes: []*route.Route{
|
||||||
shard, shard, shard, shard, shard, shard,
|
shard, shard, shard, shard, shard, shard,
|
||||||
},
|
},
|
||||||
|
paymentErr: channeldb.FailureReasonPaymentDetails,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
// A MP payment scenario when our path finding returns
|
||||||
|
// after we've just received a terminal failure, and
|
||||||
|
// attempts to dispatch a new shard. Testing that we
|
||||||
|
// correctly abandon the shard and conclude the payment.
|
||||||
|
name: "MP path found after failure",
|
||||||
|
|
||||||
|
steps: []string{
|
||||||
|
routerInitPayment,
|
||||||
|
|
||||||
|
// shard 0
|
||||||
|
routeRelease,
|
||||||
|
routerRegisterAttempt,
|
||||||
|
sendToSwitchSuccess,
|
||||||
|
|
||||||
|
// The first shard fail with a terminal error.
|
||||||
|
getPaymentResultTerminalFailure,
|
||||||
|
routerFailAttempt,
|
||||||
|
routerFailPayment,
|
||||||
|
|
||||||
|
// shard 1 fails because we've had a terminal
|
||||||
|
// failure.
|
||||||
|
routeRelease,
|
||||||
|
routerRegisterAttempt,
|
||||||
|
|
||||||
|
// Payment fails.
|
||||||
|
paymentError,
|
||||||
|
},
|
||||||
|
routes: []*route.Route{
|
||||||
|
shard, shard,
|
||||||
|
},
|
||||||
|
paymentErr: channeldb.FailureReasonPaymentDetails,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
// A MP payment scenario when our path finding returns
|
||||||
|
// after we've just received a terminal failure, and
|
||||||
|
// we have another shard still in flight.
|
||||||
|
name: "MP shard in flight after terminal",
|
||||||
|
|
||||||
|
steps: []string{
|
||||||
|
routerInitPayment,
|
||||||
|
|
||||||
|
// shard 0
|
||||||
|
routeRelease,
|
||||||
|
routerRegisterAttempt,
|
||||||
|
sendToSwitchSuccess,
|
||||||
|
|
||||||
|
// shard 1
|
||||||
|
routeRelease,
|
||||||
|
routerRegisterAttempt,
|
||||||
|
sendToSwitchSuccess,
|
||||||
|
|
||||||
|
// shard 2
|
||||||
|
routeRelease,
|
||||||
|
routerRegisterAttempt,
|
||||||
|
sendToSwitchSuccess,
|
||||||
|
|
||||||
|
// We find a path for another shard.
|
||||||
|
routeRelease,
|
||||||
|
|
||||||
|
// shard 0 fails with a terminal error.
|
||||||
|
getPaymentResultTerminalFailure,
|
||||||
|
routerFailAttempt,
|
||||||
|
routerFailPayment,
|
||||||
|
|
||||||
|
// We try to register our final shard after
|
||||||
|
// processing a terminal failure.
|
||||||
|
routerRegisterAttempt,
|
||||||
|
|
||||||
|
// Our in-flight shards fail.
|
||||||
|
getPaymentResultTempFailure,
|
||||||
|
getPaymentResultTempFailure,
|
||||||
|
routerFailAttempt,
|
||||||
|
routerFailAttempt,
|
||||||
|
|
||||||
|
// Payment fails.
|
||||||
|
paymentError,
|
||||||
|
},
|
||||||
|
routes: []*route.Route{
|
||||||
|
shard, shard, shard, shard,
|
||||||
|
},
|
||||||
|
paymentErr: channeldb.FailureReasonPaymentDetails,
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
|
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
|
// Create a mock control tower with channels set up, that we use to
|
||||||
// synchronize and listen for events.
|
// synchronize and listen for events.
|
||||||
control := makeMockControlTower()
|
control := makeMockControlTower()
|
||||||
control.init = make(chan initArgs, 20)
|
control.init = make(chan initArgs)
|
||||||
control.registerAttempt = make(chan registerAttemptArgs, 20)
|
control.registerAttempt = make(chan registerAttemptArgs)
|
||||||
control.settleAttempt = make(chan settleAttemptArgs, 20)
|
control.settleAttempt = make(chan settleAttemptArgs)
|
||||||
control.failAttempt = make(chan failAttemptArgs, 20)
|
control.failAttempt = make(chan failAttemptArgs)
|
||||||
control.failPayment = make(chan failPaymentArgs, 20)
|
control.failPayment = make(chan failPaymentArgs)
|
||||||
control.fetchInFlight = make(chan struct{}, 20)
|
control.fetchInFlight = make(chan struct{})
|
||||||
|
|
||||||
quit := make(chan struct{})
|
|
||||||
defer close(quit)
|
|
||||||
|
|
||||||
// setupRouter is a helper method that creates and starts the router in
|
// setupRouter is a helper method that creates and starts the router in
|
||||||
// the desired configuration for this test.
|
// the desired configuration for this test.
|
||||||
setupRouter := func() (*ChannelRouter, chan error,
|
setupRouter := func() (*ChannelRouter, chan error,
|
||||||
chan *htlcswitch.PaymentResult, chan error) {
|
chan *htlcswitch.PaymentResult) {
|
||||||
|
|
||||||
chain := newMockChain(startingBlockHeight)
|
chain := newMockChain(startingBlockHeight)
|
||||||
chainView := newMockChainView(chain)
|
chainView := newMockChainView(chain)
|
||||||
@ -578,13 +736,11 @@ func TestRouterPaymentStateMachine(t *testing.T) {
|
|||||||
// We set uo the use the following channels and a mock Payer to
|
// We set uo the use the following channels and a mock Payer to
|
||||||
// synchonize with the interaction to the Switch.
|
// synchonize with the interaction to the Switch.
|
||||||
sendResult := make(chan error)
|
sendResult := make(chan error)
|
||||||
paymentResultErr := make(chan error)
|
|
||||||
paymentResult := make(chan *htlcswitch.PaymentResult)
|
paymentResult := make(chan *htlcswitch.PaymentResult)
|
||||||
|
|
||||||
payer := &mockPayer{
|
payer := &mockPayer{
|
||||||
sendResult: sendResult,
|
sendResult: sendResult,
|
||||||
paymentResult: paymentResult,
|
paymentResult: paymentResult,
|
||||||
paymentResultErr: paymentResultErr,
|
|
||||||
}
|
}
|
||||||
|
|
||||||
router, err := New(Config{
|
router, err := New(Config{
|
||||||
@ -637,17 +793,16 @@ func TestRouterPaymentStateMachine(t *testing.T) {
|
|||||||
t.Fatalf("did not fetch in flight payments at startup")
|
t.Fatalf("did not fetch in flight payments at startup")
|
||||||
}
|
}
|
||||||
|
|
||||||
return router, sendResult, paymentResult, paymentResultErr
|
return router, sendResult, paymentResult
|
||||||
}
|
}
|
||||||
|
|
||||||
router, sendResult, getPaymentResult, getPaymentResultErr := setupRouter()
|
router, sendResult, getPaymentResult := setupRouter()
|
||||||
defer func() {
|
defer func() {
|
||||||
if err := router.Stop(); err != nil {
|
if err := router.Stop(); err != nil {
|
||||||
t.Fatal(err)
|
t.Fatal(err)
|
||||||
}
|
}
|
||||||
}()
|
}()
|
||||||
|
|
||||||
for _, test := range tests {
|
|
||||||
// Craft a LightningPayment struct.
|
// Craft a LightningPayment struct.
|
||||||
var preImage lntypes.Preimage
|
var preImage lntypes.Preimage
|
||||||
if _, err := rand.Read(preImage[:]); err != nil {
|
if _, err := rand.Read(preImage[:]); err != nil {
|
||||||
@ -663,8 +818,12 @@ func TestRouterPaymentStateMachine(t *testing.T) {
|
|||||||
PaymentHash: payHash,
|
PaymentHash: payHash,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Setup our payment session source to block on release of
|
||||||
|
// routes.
|
||||||
|
routeChan := make(chan struct{})
|
||||||
router.cfg.SessionSource = &mockPaymentSessionSource{
|
router.cfg.SessionSource = &mockPaymentSessionSource{
|
||||||
routes: test.routes,
|
routes: test.routes,
|
||||||
|
routeRelease: routeChan,
|
||||||
}
|
}
|
||||||
|
|
||||||
router.cfg.MissionControl = &mockMissionControl{}
|
router.cfg.MissionControl = &mockMissionControl{}
|
||||||
@ -672,9 +831,11 @@ func TestRouterPaymentStateMachine(t *testing.T) {
|
|||||||
// Send the payment. Since this is new payment hash, the
|
// Send the payment. Since this is new payment hash, the
|
||||||
// information should be registered with the ControlTower.
|
// information should be registered with the ControlTower.
|
||||||
paymentResult := make(chan error)
|
paymentResult := make(chan error)
|
||||||
|
done := make(chan struct{})
|
||||||
go func() {
|
go func() {
|
||||||
_, _, err := router.SendPayment(&payment)
|
_, _, err := router.SendPayment(&payment)
|
||||||
paymentResult <- err
|
paymentResult <- err
|
||||||
|
close(done)
|
||||||
}()
|
}()
|
||||||
|
|
||||||
var resendResult chan error
|
var resendResult chan error
|
||||||
@ -693,6 +854,14 @@ func TestRouterPaymentStateMachine(t *testing.T) {
|
|||||||
t.Fatalf("expected non-nil CreationInfo")
|
t.Fatalf("expected non-nil CreationInfo")
|
||||||
}
|
}
|
||||||
|
|
||||||
|
case routeRelease:
|
||||||
|
select {
|
||||||
|
case <-routeChan:
|
||||||
|
|
||||||
|
case <-time.After(stepTimeout):
|
||||||
|
t.Fatalf("no route requested")
|
||||||
|
}
|
||||||
|
|
||||||
// In this step we expect the router to make a call to
|
// In this step we expect the router to make a call to
|
||||||
// register a new attempt with the ControlTower.
|
// register a new attempt with the ControlTower.
|
||||||
case routerRegisterAttempt:
|
case routerRegisterAttempt:
|
||||||
@ -820,13 +989,9 @@ func TestRouterPaymentStateMachine(t *testing.T) {
|
|||||||
|
|
||||||
// In this step we manually stop the router.
|
// In this step we manually stop the router.
|
||||||
case stopRouter:
|
case stopRouter:
|
||||||
select {
|
// On shutdown, the switch closes our result channel.
|
||||||
case getPaymentResultErr <- fmt.Errorf(
|
// Mimic this behavior in our mock.
|
||||||
"shutting down"):
|
close(getPaymentResult)
|
||||||
case <-time.After(stepTimeout):
|
|
||||||
t.Fatalf("unable to send payment " +
|
|
||||||
"result error")
|
|
||||||
}
|
|
||||||
|
|
||||||
if err := router.Stop(); err != nil {
|
if err := router.Stop(); err != nil {
|
||||||
t.Fatalf("unable to restart: %v", err)
|
t.Fatalf("unable to restart: %v", err)
|
||||||
@ -834,17 +999,17 @@ func TestRouterPaymentStateMachine(t *testing.T) {
|
|||||||
|
|
||||||
// In this step we manually start the router.
|
// In this step we manually start the router.
|
||||||
case startRouter:
|
case startRouter:
|
||||||
router, sendResult, getPaymentResult,
|
router, sendResult, getPaymentResult = setupRouter()
|
||||||
getPaymentResultErr = setupRouter()
|
|
||||||
|
|
||||||
// In this state we expect to receive an error for the
|
// In this state we expect to receive an error for the
|
||||||
// original payment made.
|
// original payment made.
|
||||||
case paymentError:
|
case paymentError:
|
||||||
|
require.Error(t, test.paymentErr,
|
||||||
|
"paymentError not set")
|
||||||
|
|
||||||
select {
|
select {
|
||||||
case err := <-paymentResult:
|
case err := <-paymentResult:
|
||||||
if err == nil {
|
require.Equal(t, test.paymentErr, err)
|
||||||
t.Fatalf("expected error")
|
|
||||||
}
|
|
||||||
|
|
||||||
case <-time.After(stepTimeout):
|
case <-time.After(stepTimeout):
|
||||||
t.Fatalf("got no payment result")
|
t.Fatalf("got no payment result")
|
||||||
@ -853,6 +1018,8 @@ func TestRouterPaymentStateMachine(t *testing.T) {
|
|||||||
// In this state we expect the original payment to
|
// In this state we expect the original payment to
|
||||||
// succeed.
|
// succeed.
|
||||||
case paymentSuccess:
|
case paymentSuccess:
|
||||||
|
require.Nil(t, test.paymentErr)
|
||||||
|
|
||||||
select {
|
select {
|
||||||
case err := <-paymentResult:
|
case err := <-paymentResult:
|
||||||
if err != nil {
|
if err != nil {
|
||||||
@ -894,5 +1061,10 @@ func TestRouterPaymentStateMachine(t *testing.T) {
|
|||||||
t.Fatalf("unknown step %v", step)
|
t.Fatalf("unknown step %v", step)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
select {
|
||||||
|
case <-done:
|
||||||
|
case <-time.After(testTimeout):
|
||||||
|
t.Fatalf("SendPayment didn't exit")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
Loading…
Reference in New Issue
Block a user