Merge pull request #5332 from yyforyongyu/5259-routing-fix-state
routing: fix payment state and refactor payment lifecycle tests
This commit is contained in:
15 files changed
+4739
-3031
No files matched your search
@@ -274,6 +274,7 @@ github.com/spf13/pflag v1.0.1 h1:aCvUg6QPl3ibpQUxyLkrEkCHtPqYJL4x9AuhqVqFis4=
|
||||
github.com/spf13/pflag v1.0.1/go.mod h1:DYY7MBk1bdzusC3SYhjObp+wFpr4gzcvqqNjLnInEg4=
|
||||
github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
|
||||
github.com/stretchr/objx v0.1.1/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
|
||||
github.com/stretchr/objx v0.2.0 h1:Hbg2NidpLE8veEBkEZTL3CvlkUIVzuU9jDplZO54c48=
|
||||
github.com/stretchr/objx v0.2.0/go.mod h1:qt09Ya8vawLte6SNmTgCsAVtYtaKzEcn8ATUoHMkEqE=
|
||||
github.com/stretchr/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs=
|
||||
github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI=
|
||||
|
||||
File diff suppressed because it is too large.
Load diff
+37
-2087
File diff suppressed because it is too large.
Load diff
@@ -107,6 +107,11 @@ var allTestCases = []*testCase{
|
||||
name: "private channels",
|
||||
test: testPrivateChannels,
|
||||
},
|
||||
{
|
||||
name: "private channel update policy",
|
||||
test: testUpdateChannelPolicyForPrivateChannel,
|
||||
},
|
||||
|
||||
{
|
||||
name: "invoice routing hints",
|
||||
test: testInvoiceRoutingHints,
|
||||
|
||||
@@ -283,3 +283,4 @@
|
||||
<time> [ERR] HSWC: ChannelLink(<chan>): failing link: process hodl queue: unable to update commitment: link shutting down with error: internal error
|
||||
<time> [ERR] INVC: SettleHodlInvoice with preimage <hex>: invoice already canceled
|
||||
<time> [ERR] RPCS: [/invoicesrpc.Invoices/SettleInvoice]: invoice already canceled
|
||||
<time> [ERR] HSWC: ChannelLink(<chan>): outgoing htlc(<hex>) has insufficient fee: expected 33000, got 1020
|
||||
+264
-40
@@ -4,25 +4,27 @@ import (
|
||||
"fmt"
|
||||
"sync"
|
||||
|
||||
"github.com/btcsuite/btcd/btcec"
|
||||
"github.com/go-errors/errors"
|
||||
"github.com/lightningnetwork/lnd/channeldb"
|
||||
"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/mock"
|
||||
)
|
||||
|
||||
type mockPaymentAttemptDispatcher struct {
|
||||
type mockPaymentAttemptDispatcherOld struct {
|
||||
onPayment func(firstHop lnwire.ShortChannelID) ([32]byte, error)
|
||||
results map[uint64]*htlcswitch.PaymentResult
|
||||
|
||||
sync.Mutex
|
||||
}
|
||||
|
||||
var _ PaymentAttemptDispatcher = (*mockPaymentAttemptDispatcher)(nil)
|
||||
var _ PaymentAttemptDispatcher = (*mockPaymentAttemptDispatcherOld)(nil)
|
||||
|
||||
func (m *mockPaymentAttemptDispatcher) SendHTLC(firstHop lnwire.ShortChannelID,
|
||||
pid uint64,
|
||||
func (m *mockPaymentAttemptDispatcherOld) SendHTLC(
|
||||
firstHop lnwire.ShortChannelID, pid uint64,
|
||||
_ *lnwire.UpdateAddHTLC) error {
|
||||
|
||||
if m.onPayment == nil {
|
||||
@@ -54,7 +56,7 @@ func (m *mockPaymentAttemptDispatcher) SendHTLC(firstHop lnwire.ShortChannelID,
|
||||
return nil
|
||||
}
|
||||
|
||||
func (m *mockPaymentAttemptDispatcher) GetPaymentResult(paymentID uint64,
|
||||
func (m *mockPaymentAttemptDispatcherOld) GetPaymentResult(paymentID uint64,
|
||||
_ lntypes.Hash, _ htlcswitch.ErrorDecrypter) (
|
||||
<-chan *htlcswitch.PaymentResult, error) {
|
||||
|
||||
@@ -72,48 +74,51 @@ func (m *mockPaymentAttemptDispatcher) GetPaymentResult(paymentID uint64,
|
||||
return c, nil
|
||||
|
||||
}
|
||||
func (m *mockPaymentAttemptDispatcher) CleanStore(map[uint64]struct{}) error {
|
||||
func (m *mockPaymentAttemptDispatcherOld) CleanStore(
|
||||
map[uint64]struct{}) error {
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (m *mockPaymentAttemptDispatcher) setPaymentResult(
|
||||
func (m *mockPaymentAttemptDispatcherOld) setPaymentResult(
|
||||
f func(firstHop lnwire.ShortChannelID) ([32]byte, error)) {
|
||||
|
||||
m.onPayment = f
|
||||
}
|
||||
|
||||
type mockPaymentSessionSource struct {
|
||||
type mockPaymentSessionSourceOld struct {
|
||||
routes []*route.Route
|
||||
routeRelease chan struct{}
|
||||
}
|
||||
|
||||
var _ PaymentSessionSource = (*mockPaymentSessionSource)(nil)
|
||||
var _ PaymentSessionSource = (*mockPaymentSessionSourceOld)(nil)
|
||||
|
||||
func (m *mockPaymentSessionSource) NewPaymentSession(
|
||||
func (m *mockPaymentSessionSourceOld) NewPaymentSession(
|
||||
_ *LightningPayment) (PaymentSession, error) {
|
||||
|
||||
return &mockPaymentSession{
|
||||
return &mockPaymentSessionOld{
|
||||
routes: m.routes,
|
||||
release: m.routeRelease,
|
||||
}, nil
|
||||
}
|
||||
|
||||
func (m *mockPaymentSessionSource) NewPaymentSessionForRoute(
|
||||
func (m *mockPaymentSessionSourceOld) NewPaymentSessionForRoute(
|
||||
preBuiltRoute *route.Route) PaymentSession {
|
||||
return nil
|
||||
}
|
||||
|
||||
func (m *mockPaymentSessionSource) NewPaymentSessionEmpty() PaymentSession {
|
||||
return &mockPaymentSession{}
|
||||
func (m *mockPaymentSessionSourceOld) NewPaymentSessionEmpty() PaymentSession {
|
||||
return &mockPaymentSessionOld{}
|
||||
}
|
||||
|
||||
type mockMissionControl struct {
|
||||
type mockMissionControlOld struct {
|
||||
MissionControl
|
||||
}
|
||||
|
||||
var _ MissionController = (*mockMissionControl)(nil)
|
||||
var _ MissionController = (*mockMissionControlOld)(nil)
|
||||
|
||||
func (m *mockMissionControl) ReportPaymentFail(paymentID uint64, rt *route.Route,
|
||||
func (m *mockMissionControlOld) ReportPaymentFail(
|
||||
paymentID uint64, rt *route.Route,
|
||||
failureSourceIdx *int, failure lnwire.FailureMessage) (
|
||||
*channeldb.FailureReason, error) {
|
||||
|
||||
@@ -127,19 +132,19 @@ func (m *mockMissionControl) ReportPaymentFail(paymentID uint64, rt *route.Route
|
||||
return nil, nil
|
||||
}
|
||||
|
||||
func (m *mockMissionControl) ReportPaymentSuccess(paymentID uint64,
|
||||
func (m *mockMissionControlOld) ReportPaymentSuccess(paymentID uint64,
|
||||
rt *route.Route) error {
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (m *mockMissionControl) GetProbability(fromNode, toNode route.Vertex,
|
||||
func (m *mockMissionControlOld) GetProbability(fromNode, toNode route.Vertex,
|
||||
amt lnwire.MilliSatoshi) float64 {
|
||||
|
||||
return 0
|
||||
}
|
||||
|
||||
type mockPaymentSession struct {
|
||||
type mockPaymentSessionOld struct {
|
||||
routes []*route.Route
|
||||
|
||||
// release is a channel that optionally blocks requesting a route
|
||||
@@ -148,9 +153,9 @@ type mockPaymentSession struct {
|
||||
release chan struct{}
|
||||
}
|
||||
|
||||
var _ PaymentSession = (*mockPaymentSession)(nil)
|
||||
var _ PaymentSession = (*mockPaymentSessionOld)(nil)
|
||||
|
||||
func (m *mockPaymentSession) RequestRoute(_, _ lnwire.MilliSatoshi,
|
||||
func (m *mockPaymentSessionOld) RequestRoute(_, _ lnwire.MilliSatoshi,
|
||||
_, height uint32) (*route.Route, error) {
|
||||
|
||||
if m.release != nil {
|
||||
@@ -167,15 +172,27 @@ func (m *mockPaymentSession) RequestRoute(_, _ lnwire.MilliSatoshi,
|
||||
return r, nil
|
||||
}
|
||||
|
||||
type mockPayer struct {
|
||||
func (m *mockPaymentSessionOld) UpdateAdditionalEdge(_ *lnwire.ChannelUpdate,
|
||||
_ *btcec.PublicKey, _ *channeldb.ChannelEdgePolicy) bool {
|
||||
|
||||
return false
|
||||
}
|
||||
|
||||
func (m *mockPaymentSessionOld) GetAdditionalEdgePolicy(_ *btcec.PublicKey,
|
||||
_ uint64) *channeldb.ChannelEdgePolicy {
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
type mockPayerOld struct {
|
||||
sendResult chan error
|
||||
paymentResult chan *htlcswitch.PaymentResult
|
||||
quit chan struct{}
|
||||
}
|
||||
|
||||
var _ PaymentAttemptDispatcher = (*mockPayer)(nil)
|
||||
var _ PaymentAttemptDispatcher = (*mockPayerOld)(nil)
|
||||
|
||||
func (m *mockPayer) SendHTLC(_ lnwire.ShortChannelID,
|
||||
func (m *mockPayerOld) SendHTLC(_ lnwire.ShortChannelID,
|
||||
paymentID uint64,
|
||||
_ *lnwire.UpdateAddHTLC) error {
|
||||
|
||||
@@ -188,7 +205,7 @@ func (m *mockPayer) SendHTLC(_ lnwire.ShortChannelID,
|
||||
|
||||
}
|
||||
|
||||
func (m *mockPayer) GetPaymentResult(paymentID uint64, _ lntypes.Hash,
|
||||
func (m *mockPayerOld) GetPaymentResult(paymentID uint64, _ lntypes.Hash,
|
||||
_ htlcswitch.ErrorDecrypter) (<-chan *htlcswitch.PaymentResult, error) {
|
||||
|
||||
select {
|
||||
@@ -207,7 +224,7 @@ func (m *mockPayer) GetPaymentResult(paymentID uint64, _ lntypes.Hash,
|
||||
}
|
||||
}
|
||||
|
||||
func (m *mockPayer) CleanStore(pids map[uint64]struct{}) error {
|
||||
func (m *mockPayerOld) CleanStore(pids map[uint64]struct{}) error {
|
||||
return nil
|
||||
}
|
||||
|
||||
@@ -236,7 +253,7 @@ type testPayment struct {
|
||||
attempts []channeldb.HTLCAttempt
|
||||
}
|
||||
|
||||
type mockControlTower struct {
|
||||
type mockControlTowerOld struct {
|
||||
payments map[lntypes.Hash]*testPayment
|
||||
successful map[lntypes.Hash]struct{}
|
||||
failed map[lntypes.Hash]channeldb.FailureReason
|
||||
@@ -251,17 +268,17 @@ type mockControlTower struct {
|
||||
sync.Mutex
|
||||
}
|
||||
|
||||
var _ ControlTower = (*mockControlTower)(nil)
|
||||
var _ ControlTower = (*mockControlTowerOld)(nil)
|
||||
|
||||
func makeMockControlTower() *mockControlTower {
|
||||
return &mockControlTower{
|
||||
func makeMockControlTower() *mockControlTowerOld {
|
||||
return &mockControlTowerOld{
|
||||
payments: make(map[lntypes.Hash]*testPayment),
|
||||
successful: make(map[lntypes.Hash]struct{}),
|
||||
failed: make(map[lntypes.Hash]channeldb.FailureReason),
|
||||
}
|
||||
}
|
||||
|
||||
func (m *mockControlTower) InitPayment(phash lntypes.Hash,
|
||||
func (m *mockControlTowerOld) InitPayment(phash lntypes.Hash,
|
||||
c *channeldb.PaymentCreationInfo) error {
|
||||
|
||||
if m.init != nil {
|
||||
@@ -292,7 +309,7 @@ func (m *mockControlTower) InitPayment(phash lntypes.Hash,
|
||||
return nil
|
||||
}
|
||||
|
||||
func (m *mockControlTower) RegisterAttempt(phash lntypes.Hash,
|
||||
func (m *mockControlTowerOld) RegisterAttempt(phash lntypes.Hash,
|
||||
a *channeldb.HTLCAttemptInfo) error {
|
||||
|
||||
if m.registerAttempt != nil {
|
||||
@@ -346,7 +363,7 @@ func (m *mockControlTower) RegisterAttempt(phash lntypes.Hash,
|
||||
return nil
|
||||
}
|
||||
|
||||
func (m *mockControlTower) SettleAttempt(phash lntypes.Hash,
|
||||
func (m *mockControlTowerOld) SettleAttempt(phash lntypes.Hash,
|
||||
pid uint64, settleInfo *channeldb.HTLCSettleInfo) (
|
||||
*channeldb.HTLCAttempt, error) {
|
||||
|
||||
@@ -388,7 +405,7 @@ func (m *mockControlTower) SettleAttempt(phash lntypes.Hash,
|
||||
return nil, fmt.Errorf("pid not found")
|
||||
}
|
||||
|
||||
func (m *mockControlTower) FailAttempt(phash lntypes.Hash, pid uint64,
|
||||
func (m *mockControlTowerOld) FailAttempt(phash lntypes.Hash, pid uint64,
|
||||
failInfo *channeldb.HTLCFailInfo) (*channeldb.HTLCAttempt, error) {
|
||||
|
||||
if m.failAttempt != nil {
|
||||
@@ -426,7 +443,7 @@ func (m *mockControlTower) FailAttempt(phash lntypes.Hash, pid uint64,
|
||||
return nil, fmt.Errorf("pid not found")
|
||||
}
|
||||
|
||||
func (m *mockControlTower) Fail(phash lntypes.Hash,
|
||||
func (m *mockControlTowerOld) Fail(phash lntypes.Hash,
|
||||
reason channeldb.FailureReason) error {
|
||||
|
||||
m.Lock()
|
||||
@@ -446,7 +463,7 @@ func (m *mockControlTower) Fail(phash lntypes.Hash,
|
||||
return nil
|
||||
}
|
||||
|
||||
func (m *mockControlTower) FetchPayment(phash lntypes.Hash) (
|
||||
func (m *mockControlTowerOld) FetchPayment(phash lntypes.Hash) (
|
||||
*channeldb.MPPayment, error) {
|
||||
|
||||
m.Lock()
|
||||
@@ -455,7 +472,7 @@ func (m *mockControlTower) FetchPayment(phash lntypes.Hash) (
|
||||
return m.fetchPayment(phash)
|
||||
}
|
||||
|
||||
func (m *mockControlTower) fetchPayment(phash lntypes.Hash) (
|
||||
func (m *mockControlTowerOld) fetchPayment(phash lntypes.Hash) (
|
||||
*channeldb.MPPayment, error) {
|
||||
|
||||
p, ok := m.payments[phash]
|
||||
@@ -477,7 +494,7 @@ func (m *mockControlTower) fetchPayment(phash lntypes.Hash) (
|
||||
return mp, nil
|
||||
}
|
||||
|
||||
func (m *mockControlTower) FetchInFlightPayments() (
|
||||
func (m *mockControlTowerOld) FetchInFlightPayments() (
|
||||
[]*channeldb.MPPayment, error) {
|
||||
|
||||
if m.fetchInFlight != nil {
|
||||
@@ -508,8 +525,215 @@ func (m *mockControlTower) FetchInFlightPayments() (
|
||||
return fl, nil
|
||||
}
|
||||
|
||||
func (m *mockControlTower) SubscribePayment(paymentHash lntypes.Hash) (
|
||||
func (m *mockControlTowerOld) SubscribePayment(paymentHash lntypes.Hash) (
|
||||
*ControlTowerSubscriber, error) {
|
||||
|
||||
return nil, errors.New("not implemented")
|
||||
}
|
||||
|
||||
type mockPaymentAttemptDispatcher struct {
|
||||
mock.Mock
|
||||
|
||||
resultChan chan *htlcswitch.PaymentResult
|
||||
}
|
||||
|
||||
var _ PaymentAttemptDispatcher = (*mockPaymentAttemptDispatcher)(nil)
|
||||
|
||||
func (m *mockPaymentAttemptDispatcher) SendHTLC(firstHop lnwire.ShortChannelID,
|
||||
pid uint64, htlcAdd *lnwire.UpdateAddHTLC) error {
|
||||
|
||||
args := m.Called(firstHop, pid, htlcAdd)
|
||||
return args.Error(0)
|
||||
}
|
||||
|
||||
func (m *mockPaymentAttemptDispatcher) GetPaymentResult(attemptID uint64,
|
||||
paymentHash lntypes.Hash, deobfuscator htlcswitch.ErrorDecrypter) (
|
||||
<-chan *htlcswitch.PaymentResult, error) {
|
||||
|
||||
m.Called(attemptID, paymentHash, deobfuscator)
|
||||
|
||||
// Instead of returning the mocked returned values, we need to return
|
||||
// the chan resultChan so it can be converted into a read-only chan.
|
||||
return m.resultChan, nil
|
||||
}
|
||||
|
||||
func (m *mockPaymentAttemptDispatcher) CleanStore(
|
||||
keepPids map[uint64]struct{}) error {
|
||||
|
||||
args := m.Called(keepPids)
|
||||
return args.Error(0)
|
||||
}
|
||||
|
||||
type mockPaymentSessionSource struct {
|
||||
mock.Mock
|
||||
}
|
||||
|
||||
var _ PaymentSessionSource = (*mockPaymentSessionSource)(nil)
|
||||
|
||||
func (m *mockPaymentSessionSource) NewPaymentSession(
|
||||
payment *LightningPayment) (PaymentSession, error) {
|
||||
|
||||
args := m.Called(payment)
|
||||
return args.Get(0).(PaymentSession), args.Error(1)
|
||||
}
|
||||
|
||||
func (m *mockPaymentSessionSource) NewPaymentSessionForRoute(
|
||||
preBuiltRoute *route.Route) PaymentSession {
|
||||
|
||||
args := m.Called(preBuiltRoute)
|
||||
return args.Get(0).(PaymentSession)
|
||||
}
|
||||
|
||||
func (m *mockPaymentSessionSource) NewPaymentSessionEmpty() PaymentSession {
|
||||
args := m.Called()
|
||||
return args.Get(0).(PaymentSession)
|
||||
}
|
||||
|
||||
type mockMissionControl struct {
|
||||
mock.Mock
|
||||
|
||||
failReason *channeldb.FailureReason
|
||||
}
|
||||
|
||||
var _ MissionController = (*mockMissionControl)(nil)
|
||||
|
||||
func (m *mockMissionControl) ReportPaymentFail(
|
||||
paymentID uint64, rt *route.Route,
|
||||
failureSourceIdx *int, failure lnwire.FailureMessage) (
|
||||
*channeldb.FailureReason, error) {
|
||||
|
||||
args := m.Called(paymentID, rt, failureSourceIdx, failure)
|
||||
return m.failReason, args.Error(1)
|
||||
}
|
||||
|
||||
func (m *mockMissionControl) ReportPaymentSuccess(paymentID uint64,
|
||||
rt *route.Route) error {
|
||||
|
||||
args := m.Called(paymentID, rt)
|
||||
return args.Error(0)
|
||||
}
|
||||
|
||||
func (m *mockMissionControl) GetProbability(fromNode, toNode route.Vertex,
|
||||
amt lnwire.MilliSatoshi) float64 {
|
||||
|
||||
args := m.Called(fromNode, toNode, amt)
|
||||
return args.Get(0).(float64)
|
||||
}
|
||||
|
||||
type mockPaymentSession struct {
|
||||
mock.Mock
|
||||
}
|
||||
|
||||
var _ PaymentSession = (*mockPaymentSession)(nil)
|
||||
|
||||
func (m *mockPaymentSession) RequestRoute(maxAmt, feeLimit lnwire.MilliSatoshi,
|
||||
activeShards, height uint32) (*route.Route, error) {
|
||||
args := m.Called(maxAmt, feeLimit, activeShards, height)
|
||||
return args.Get(0).(*route.Route), args.Error(1)
|
||||
}
|
||||
|
||||
func (m *mockPaymentSession) UpdateAdditionalEdge(msg *lnwire.ChannelUpdate,
|
||||
pubKey *btcec.PublicKey, policy *channeldb.ChannelEdgePolicy) bool {
|
||||
|
||||
args := m.Called(msg, pubKey, policy)
|
||||
return args.Bool(0)
|
||||
}
|
||||
|
||||
func (m *mockPaymentSession) GetAdditionalEdgePolicy(pubKey *btcec.PublicKey,
|
||||
channelID uint64) *channeldb.ChannelEdgePolicy {
|
||||
|
||||
args := m.Called(pubKey, channelID)
|
||||
return args.Get(0).(*channeldb.ChannelEdgePolicy)
|
||||
}
|
||||
|
||||
type mockControlTower struct {
|
||||
mock.Mock
|
||||
sync.Mutex
|
||||
}
|
||||
|
||||
var _ ControlTower = (*mockControlTower)(nil)
|
||||
|
||||
func (m *mockControlTower) InitPayment(phash lntypes.Hash,
|
||||
c *channeldb.PaymentCreationInfo) error {
|
||||
|
||||
args := m.Called(phash, c)
|
||||
return args.Error(0)
|
||||
}
|
||||
|
||||
func (m *mockControlTower) RegisterAttempt(phash lntypes.Hash,
|
||||
a *channeldb.HTLCAttemptInfo) error {
|
||||
|
||||
m.Lock()
|
||||
defer m.Unlock()
|
||||
|
||||
args := m.Called(phash, a)
|
||||
return args.Error(0)
|
||||
}
|
||||
|
||||
func (m *mockControlTower) SettleAttempt(phash lntypes.Hash,
|
||||
pid uint64, settleInfo *channeldb.HTLCSettleInfo) (
|
||||
*channeldb.HTLCAttempt, error) {
|
||||
|
||||
m.Lock()
|
||||
defer m.Unlock()
|
||||
|
||||
args := m.Called(phash, pid, settleInfo)
|
||||
return args.Get(0).(*channeldb.HTLCAttempt), args.Error(1)
|
||||
}
|
||||
|
||||
func (m *mockControlTower) FailAttempt(phash lntypes.Hash, pid uint64,
|
||||
failInfo *channeldb.HTLCFailInfo) (*channeldb.HTLCAttempt, error) {
|
||||
|
||||
m.Lock()
|
||||
defer m.Unlock()
|
||||
|
||||
args := m.Called(phash, pid, failInfo)
|
||||
return args.Get(0).(*channeldb.HTLCAttempt), args.Error(1)
|
||||
}
|
||||
|
||||
func (m *mockControlTower) Fail(phash lntypes.Hash,
|
||||
reason channeldb.FailureReason) error {
|
||||
|
||||
m.Lock()
|
||||
defer m.Unlock()
|
||||
|
||||
args := m.Called(phash, reason)
|
||||
return args.Error(0)
|
||||
}
|
||||
|
||||
func (m *mockControlTower) FetchPayment(phash lntypes.Hash) (
|
||||
*channeldb.MPPayment, error) {
|
||||
|
||||
m.Lock()
|
||||
defer m.Unlock()
|
||||
args := m.Called(phash)
|
||||
|
||||
// Type assertion on nil will fail, so we check and return here.
|
||||
if args.Get(0) == nil {
|
||||
return nil, args.Error(1)
|
||||
}
|
||||
|
||||
// Make a copy of the payment here to avoid data race.
|
||||
p := args.Get(0).(*channeldb.MPPayment)
|
||||
payment := &channeldb.MPPayment{
|
||||
FailureReason: p.FailureReason,
|
||||
}
|
||||
payment.HTLCs = make([]channeldb.HTLCAttempt, len(p.HTLCs))
|
||||
copy(payment.HTLCs, p.HTLCs)
|
||||
|
||||
return payment, args.Error(1)
|
||||
}
|
||||
|
||||
func (m *mockControlTower) FetchInFlightPayments() (
|
||||
[]*channeldb.MPPayment, error) {
|
||||
|
||||
args := m.Called()
|
||||
return args.Get(0).([]*channeldb.MPPayment), args.Error(1)
|
||||
}
|
||||
|
||||
func (m *mockControlTower) SubscribePayment(paymentHash lntypes.Hash) (
|
||||
*ControlTowerSubscriber, error) {
|
||||
|
||||
args := m.Called(paymentHash)
|
||||
return args.Get(0).(*ControlTowerSubscriber), args.Error(1)
|
||||
}
|
||||
@@ -352,11 +352,8 @@ func (m *mockChainView) Stop() error {
|
||||
func TestEdgeUpdateNotification(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
ctx, cleanUp, err := createTestCtxSingleNode(0)
|
||||
ctx, cleanUp := createTestCtxSingleNode(t, 0)
|
||||
defer cleanUp()
|
||||
if err != nil {
|
||||
t.Fatalf("unable to create router: %v", err)
|
||||
}
|
||||
|
||||
// First we'll create the utxo for the channel to be "closed"
|
||||
const chanValue = 10000
|
||||
@@ -546,11 +543,8 @@ func TestNodeUpdateNotification(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
const startingBlockHeight = 101
|
||||
ctx, cleanUp, err := createTestCtxSingleNode(startingBlockHeight)
|
||||
ctx, cleanUp := createTestCtxSingleNode(t, startingBlockHeight)
|
||||
defer cleanUp()
|
||||
if err != nil {
|
||||
t.Fatalf("unable to create router: %v", err)
|
||||
}
|
||||
|
||||
// We only accept node announcements from nodes having a known channel,
|
||||
// so create one now.
|
||||
@@ -739,11 +733,8 @@ func TestNotificationCancellation(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
const startingBlockHeight = 101
|
||||
ctx, cleanUp, err := createTestCtxSingleNode(startingBlockHeight)
|
||||
ctx, cleanUp := createTestCtxSingleNode(t, startingBlockHeight)
|
||||
defer cleanUp()
|
||||
if err != nil {
|
||||
t.Fatalf("unable to create router: %v", err)
|
||||
}
|
||||
|
||||
// Create a new client to receive notifications.
|
||||
ntfnClient, err := ctx.router.SubscribeTopology()
|
||||
@@ -831,11 +822,8 @@ func TestChannelCloseNotification(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
const startingBlockHeight = 101
|
||||
ctx, cleanUp, err := createTestCtxSingleNode(startingBlockHeight)
|
||||
ctx, cleanUp := createTestCtxSingleNode(t, startingBlockHeight)
|
||||
defer cleanUp()
|
||||
if err != nil {
|
||||
t.Fatalf("unable to create router: %v", err)
|
||||
}
|
||||
|
||||
// First we'll create the utxo for the channel to be "closed"
|
||||
const chanValue = 10000
|
||||
|
||||
+70
-12
@@ -118,11 +118,14 @@ type testGraph struct {
|
||||
|
||||
// testNode represents a node within the test graph above. We skip certain
|
||||
// information such as the node's IP address as that information isn't needed
|
||||
// for our tests.
|
||||
// for our tests. Private keys are optional. If set, they should be consistent
|
||||
// with the public key. The private key is used to sign error messages
|
||||
// sent from the node.
|
||||
type testNode struct {
|
||||
Source bool `json:"source"`
|
||||
PubKey string `json:"pubkey"`
|
||||
Alias string `json:"alias"`
|
||||
Source bool `json:"source"`
|
||||
PubKey string `json:"pubkey"`
|
||||
PrivKey string `json:"privkey"`
|
||||
Alias string `json:"alias"`
|
||||
}
|
||||
|
||||
// testChan represents the JSON version of a payment channel. This struct
|
||||
@@ -200,6 +203,8 @@ func parseTestGraph(path string) (*testGraphInstance, error) {
|
||||
}
|
||||
|
||||
aliasMap := make(map[string]route.Vertex)
|
||||
privKeyMap := make(map[string]*btcec.PrivateKey)
|
||||
channelIDs := make(map[route.Vertex]map[route.Vertex]uint64)
|
||||
var source *channeldb.LightningNode
|
||||
|
||||
// First we insert all the nodes within the graph as vertexes.
|
||||
@@ -230,6 +235,33 @@ func parseTestGraph(path string) (*testGraphInstance, error) {
|
||||
// alias map for easy lookup.
|
||||
aliasMap[node.Alias] = dbNode.PubKeyBytes
|
||||
|
||||
// private keys are needed for signing error messages. If set
|
||||
// check the consistency with the public key.
|
||||
privBytes, err := hex.DecodeString(node.PrivKey)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if len(privBytes) > 0 {
|
||||
key, derivedPub := btcec.PrivKeyFromBytes(
|
||||
btcec.S256(), privBytes,
|
||||
)
|
||||
|
||||
if !bytes.Equal(
|
||||
pubBytes, derivedPub.SerializeCompressed(),
|
||||
) {
|
||||
|
||||
return nil, fmt.Errorf("%s public key and "+
|
||||
"private key are inconsistent\n"+
|
||||
"got %x\nwant %x\n",
|
||||
node.Alias,
|
||||
derivedPub.SerializeCompressed(),
|
||||
pubBytes,
|
||||
)
|
||||
}
|
||||
|
||||
privKeyMap[node.Alias] = key
|
||||
}
|
||||
|
||||
// If the node is tagged as the source, then we create a
|
||||
// pointer to is so we can mark the source in the graph
|
||||
// properly.
|
||||
@@ -240,7 +272,8 @@ func parseTestGraph(path string) (*testGraphInstance, error) {
|
||||
// node can be the source in the graph.
|
||||
if source != nil {
|
||||
return nil, errors.New("JSON is invalid " +
|
||||
"multiple nodes are tagged as the source")
|
||||
"multiple nodes are tagged as the " +
|
||||
"source")
|
||||
}
|
||||
|
||||
source = dbNode
|
||||
@@ -324,12 +357,35 @@ func parseTestGraph(path string) (*testGraphInstance, error) {
|
||||
if err := graph.UpdateEdgePolicy(edgePolicy); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// We also store the channel IDs info for each of the node.
|
||||
node1Vertex, err := route.NewVertexFromBytes(node1Bytes)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
node2Vertex, err := route.NewVertexFromBytes(node2Bytes)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
if _, ok := channelIDs[node1Vertex]; !ok {
|
||||
channelIDs[node1Vertex] = map[route.Vertex]uint64{}
|
||||
}
|
||||
channelIDs[node1Vertex][node2Vertex] = edge.ChannelID
|
||||
|
||||
if _, ok := channelIDs[node2Vertex]; !ok {
|
||||
channelIDs[node2Vertex] = map[route.Vertex]uint64{}
|
||||
}
|
||||
channelIDs[node2Vertex][node1Vertex] = edge.ChannelID
|
||||
}
|
||||
|
||||
return &testGraphInstance{
|
||||
graph: graph,
|
||||
cleanUp: cleanUp,
|
||||
aliasMap: aliasMap,
|
||||
graph: graph,
|
||||
cleanUp: cleanUp,
|
||||
aliasMap: aliasMap,
|
||||
privKeyMap: privKeyMap,
|
||||
channelIDs: channelIDs,
|
||||
}, nil
|
||||
}
|
||||
|
||||
@@ -402,6 +458,9 @@ type testGraphInstance struct {
|
||||
// privKeyMap maps a node alias to its private key. This is used to be
|
||||
// able to mock a remote node's signing behaviour.
|
||||
privKeyMap map[string]*btcec.PrivateKey
|
||||
|
||||
// channelIDs stores the channel ID for each node.
|
||||
channelIDs map[route.Vertex]map[route.Vertex]uint64
|
||||
}
|
||||
|
||||
// createTestGraphFromChannels returns a fully populated ChannelGraph based on a set of
|
||||
@@ -2052,10 +2111,9 @@ func TestPathFindSpecExample(t *testing.T) {
|
||||
// we'll pass that in to ensure that the router uses 100 as the current
|
||||
// height.
|
||||
const startingHeight = 100
|
||||
ctx, cleanUp, err := createTestCtxFromFile(startingHeight, specExampleFilePath)
|
||||
if err != nil {
|
||||
t.Fatalf("unable to create router: %v", err)
|
||||
}
|
||||
ctx, cleanUp := createTestCtxFromFile(
|
||||
t, startingHeight, specExampleFilePath,
|
||||
)
|
||||
defer cleanUp()
|
||||
|
||||
// We'll first exercise the scenario of a direct payment from Bob to
|
||||
|
||||
+252
-76
@@ -5,6 +5,7 @@ import (
|
||||
"sync"
|
||||
"time"
|
||||
|
||||
"github.com/btcsuite/btcd/btcec"
|
||||
"github.com/davecgh/go-spew/spew"
|
||||
sphinx "github.com/lightningnetwork/lightning-onion"
|
||||
"github.com/lightningnetwork/lnd/channeldb"
|
||||
@@ -37,21 +38,53 @@ type paymentState struct {
|
||||
numShardsInFlight int
|
||||
remainingAmt lnwire.MilliSatoshi
|
||||
remainingFees lnwire.MilliSatoshi
|
||||
terminate bool
|
||||
|
||||
// terminate indicates the payment is in its final stage and no more
|
||||
// shards should be launched. This value is true if we have an HTLC
|
||||
// settled or the payment has an error.
|
||||
terminate bool
|
||||
}
|
||||
|
||||
// paymentState uses the passed payment to find the latest information we need
|
||||
// to act on every iteration of the payment loop.
|
||||
func (p *paymentLifecycle) paymentState(payment *channeldb.MPPayment) (
|
||||
// terminated returns a bool to indicate there are no further actions needed
|
||||
// and we should return what we have, either the payment preimage or the
|
||||
// payment error.
|
||||
func (ps paymentState) terminated() bool {
|
||||
// If the payment is in final stage and we have no in flight shards to
|
||||
// wait result for, we consider the whole action terminated.
|
||||
return ps.terminate && ps.numShardsInFlight == 0
|
||||
}
|
||||
|
||||
// needWaitForShards returns a bool to specify whether we need to wait for the
|
||||
// outcome of the shanrdHandler.
|
||||
func (ps paymentState) needWaitForShards() bool {
|
||||
// If we have in flight shards and the payment is in final stage, we
|
||||
// need to wait for the outcomes from the shards. Or if we have no more
|
||||
// money to be sent, we need to wait for the already launched shards.
|
||||
if ps.numShardsInFlight == 0 {
|
||||
return false
|
||||
}
|
||||
return ps.terminate || ps.remainingAmt == 0
|
||||
}
|
||||
|
||||
// updatePaymentState will fetch db for the payment to find the latest
|
||||
// information we need to act on every iteration of the payment loop and update
|
||||
// the paymentState.
|
||||
func (p *paymentLifecycle) updatePaymentState() (*channeldb.MPPayment,
|
||||
*paymentState, error) {
|
||||
|
||||
// Fetch the latest payment from db.
|
||||
payment, err := p.router.cfg.Control.FetchPayment(p.identifier)
|
||||
if err != nil {
|
||||
return nil, nil, err
|
||||
}
|
||||
|
||||
// Fetch the total amount and fees that has already been sent in
|
||||
// settled and still in-flight shards.
|
||||
sentAmt, fees := payment.SentAmt()
|
||||
|
||||
// Sanity check we haven't sent a value larger than the payment amount.
|
||||
if sentAmt > p.totalAmount {
|
||||
return nil, fmt.Errorf("amount sent %v exceeds "+
|
||||
return nil, nil, fmt.Errorf("amount sent %v exceeds "+
|
||||
"total amount %v", sentAmt, p.totalAmount)
|
||||
}
|
||||
|
||||
@@ -73,13 +106,15 @@ func (p *paymentLifecycle) paymentState(payment *channeldb.MPPayment) (
|
||||
// have returned with a result.
|
||||
terminate := settle != nil || failure != nil
|
||||
|
||||
activeShards := payment.InFlightHTLCs()
|
||||
return &paymentState{
|
||||
numShardsInFlight: len(activeShards),
|
||||
// Update the payment state.
|
||||
state := &paymentState{
|
||||
numShardsInFlight: len(payment.InFlightHTLCs()),
|
||||
remainingAmt: p.totalAmount - sentAmt,
|
||||
remainingFees: feeBudget,
|
||||
terminate: terminate,
|
||||
}, nil
|
||||
}
|
||||
|
||||
return payment, state, nil
|
||||
}
|
||||
|
||||
// resumePayment resumes the paymentLifecycle from the current state.
|
||||
@@ -90,6 +125,7 @@ func (p *paymentLifecycle) resumePayment() ([32]byte, *route.Route, error) {
|
||||
shardTracker: p.shardTracker,
|
||||
shardErrors: make(chan error),
|
||||
quit: make(chan struct{}),
|
||||
paySession: p.paySession,
|
||||
}
|
||||
|
||||
// When the payment lifecycle loop exits, we make sure to signal any
|
||||
@@ -100,9 +136,7 @@ func (p *paymentLifecycle) resumePayment() ([32]byte, *route.Route, error) {
|
||||
// If we had any existing attempts outstanding, we'll start by spinning
|
||||
// up goroutines that'll collect their results and deliver them to the
|
||||
// lifecycle loop below.
|
||||
payment, err := p.router.cfg.Control.FetchPayment(
|
||||
p.identifier,
|
||||
)
|
||||
payment, _, err := p.updatePaymentState()
|
||||
if err != nil {
|
||||
return [32]byte{}, nil, err
|
||||
}
|
||||
@@ -126,34 +160,30 @@ lifecycle:
|
||||
return [32]byte{}, nil, err
|
||||
}
|
||||
|
||||
// We start every iteration by fetching the lastest state of
|
||||
// the payment from the ControlTower. This ensures that we will
|
||||
// act on the latest available information, whether we are
|
||||
// resuming an existing payment or just sent a new attempt.
|
||||
payment, err := p.router.cfg.Control.FetchPayment(
|
||||
p.identifier,
|
||||
)
|
||||
if err != nil {
|
||||
return [32]byte{}, nil, err
|
||||
}
|
||||
|
||||
// Using this latest state of the payment, calculate
|
||||
// information about our active shards and terminal conditions.
|
||||
state, err := p.paymentState(payment)
|
||||
// We update the payment state on every iteration. Since the
|
||||
// payment state is affected by multiple goroutines (ie,
|
||||
// collectResultAsync), it is NOT guaranteed that we always
|
||||
// have the latest state here. This is fine as long as the
|
||||
// state is consistent as a whole.
|
||||
payment, currentState, err := p.updatePaymentState()
|
||||
if err != nil {
|
||||
return [32]byte{}, nil, err
|
||||
}
|
||||
|
||||
log.Debugf("Payment %v in state terminate=%v, "+
|
||||
"active_shards=%v, rem_value=%v, fee_limit=%v",
|
||||
p.identifier, state.terminate, state.numShardsInFlight,
|
||||
state.remainingAmt, state.remainingFees)
|
||||
p.identifier, currentState.terminate,
|
||||
currentState.numShardsInFlight,
|
||||
currentState.remainingAmt, currentState.remainingFees,
|
||||
)
|
||||
|
||||
// TODO(yy): sanity check all the states to make sure
|
||||
// everything is expected.
|
||||
switch {
|
||||
|
||||
// We have a terminal condition and no active shards, we are
|
||||
// ready to exit.
|
||||
case state.terminate && state.numShardsInFlight == 0:
|
||||
case currentState.terminated():
|
||||
// Find the first successful shard and return
|
||||
// the preimage and route.
|
||||
for _, a := range payment.HTLCs {
|
||||
@@ -168,7 +198,7 @@ lifecycle:
|
||||
// If we either reached a terminal error condition (but had
|
||||
// active shards still) or there is no remaining value to send,
|
||||
// we'll wait for a shard outcome.
|
||||
case state.terminate || state.remainingAmt == 0:
|
||||
case currentState.needWaitForShards():
|
||||
// We still have outstanding shards, so wait for a new
|
||||
// outcome to be available before re-evaluating our
|
||||
// state.
|
||||
@@ -210,8 +240,9 @@ lifecycle:
|
||||
|
||||
// Create a new payment attempt from the given payment session.
|
||||
rt, err := p.paySession.RequestRoute(
|
||||
state.remainingAmt, state.remainingFees,
|
||||
uint32(state.numShardsInFlight), uint32(p.currentHeight),
|
||||
currentState.remainingAmt, currentState.remainingFees,
|
||||
uint32(currentState.numShardsInFlight),
|
||||
uint32(p.currentHeight),
|
||||
)
|
||||
if err != nil {
|
||||
log.Warnf("Failed to find route for payment %v: %v",
|
||||
@@ -225,7 +256,7 @@ lifecycle:
|
||||
// There is no route to try, and we have no active
|
||||
// shards. This means that there is no way for us to
|
||||
// send the payment, so mark it failed with no route.
|
||||
if state.numShardsInFlight == 0 {
|
||||
if currentState.numShardsInFlight == 0 {
|
||||
failureCode := routeErr.FailureReason()
|
||||
log.Debugf("Marking payment %v permanently "+
|
||||
"failed with no route: %v",
|
||||
@@ -251,22 +282,11 @@ lifecycle:
|
||||
|
||||
// If this route will consume the last remeining amount to send
|
||||
// to the receiver, this will be our last shard (for now).
|
||||
lastShard := rt.ReceiverAmt() == state.remainingAmt
|
||||
lastShard := rt.ReceiverAmt() == currentState.remainingAmt
|
||||
|
||||
// We found a route to try, launch a new shard.
|
||||
attempt, outcome, err := shardHandler.launchShard(rt, lastShard)
|
||||
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.identifier)
|
||||
|
||||
continue lifecycle
|
||||
|
||||
case err != nil:
|
||||
if err != nil {
|
||||
return [32]byte{}, nil, err
|
||||
}
|
||||
|
||||
@@ -295,6 +315,7 @@ lifecycle:
|
||||
// Now that the shard was successfully sent, launch a go
|
||||
// routine that will handle its result when its back.
|
||||
shardHandler.collectResultAsync(attempt)
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
@@ -304,6 +325,7 @@ type shardHandler struct {
|
||||
identifier lntypes.Hash
|
||||
router *ChannelRouter
|
||||
shardTracker shards.ShardTracker
|
||||
paySession PaymentSession
|
||||
|
||||
// shardErrors is a channel where errors collected by calling
|
||||
// collectResultAsync will be delivered. These results are meant to be
|
||||
@@ -434,12 +456,30 @@ type shardResult struct {
|
||||
}
|
||||
|
||||
// collectResultAsync launches a goroutine that will wait for the result of the
|
||||
// given HTLC attempt to be available then handle its result. Note that it will
|
||||
// fail the payment with the control tower if a terminal error is encountered.
|
||||
// given HTLC attempt to be available then handle its result. It will fail the
|
||||
// payment with the control tower if a terminal error is encountered.
|
||||
func (p *shardHandler) collectResultAsync(attempt *channeldb.HTLCAttemptInfo) {
|
||||
|
||||
// errToSend is the error to be sent to sh.shardErrors.
|
||||
var errToSend error
|
||||
|
||||
// handleResultErr is a function closure must be called using defer. It
|
||||
// finishes collecting result by updating the payment state and send
|
||||
// the error (or nil) to sh.shardErrors.
|
||||
handleResultErr := func() {
|
||||
// Send the error or quit.
|
||||
select {
|
||||
case p.shardErrors <- errToSend:
|
||||
case <-p.router.quit:
|
||||
case <-p.quit:
|
||||
}
|
||||
|
||||
p.wg.Done()
|
||||
}
|
||||
|
||||
p.wg.Add(1)
|
||||
go func() {
|
||||
defer p.wg.Done()
|
||||
defer handleResultErr()
|
||||
|
||||
// Block until the result is available.
|
||||
result, err := p.collectResult(attempt)
|
||||
@@ -453,32 +493,18 @@ func (p *shardHandler) collectResultAsync(attempt *channeldb.HTLCAttemptInfo) {
|
||||
attempt.AttemptID, p.identifier, err)
|
||||
}
|
||||
|
||||
select {
|
||||
case p.shardErrors <- err:
|
||||
case <-p.router.quit:
|
||||
case <-p.quit:
|
||||
}
|
||||
// Overwrite errToSend and return.
|
||||
errToSend = err
|
||||
return
|
||||
}
|
||||
|
||||
// If a non-critical error was encountered handle it and mark
|
||||
// the payment failed if the failure was terminal.
|
||||
if result.err != nil {
|
||||
err := p.handleSendError(attempt, result.err)
|
||||
if err != nil {
|
||||
select {
|
||||
case p.shardErrors <- err:
|
||||
case <-p.router.quit:
|
||||
case <-p.quit:
|
||||
}
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
select {
|
||||
case p.shardErrors <- nil:
|
||||
case <-p.router.quit:
|
||||
case <-p.quit:
|
||||
// Overwrite errToSend and return. Notice that the
|
||||
// errToSend could be nil here.
|
||||
errToSend = p.handleSendError(attempt, result.err)
|
||||
return
|
||||
}
|
||||
}()
|
||||
}
|
||||
@@ -721,25 +747,175 @@ func (p *shardHandler) sendPaymentAttempt(
|
||||
// handleSendError inspects the given error from the Switch and determines
|
||||
// whether we should make another payment attempt, or if it should be
|
||||
// considered a terminal error. Terminal errors will be recorded with the
|
||||
// control tower.
|
||||
// control tower. It analyzes the sendErr for the payment attempt received from
|
||||
// the switch and updates mission control and/or channel policies. Depending on
|
||||
// the error type, the error is either the final outcome of the payment or we
|
||||
// need to continue with an alternative route. A final outcome is indicated by
|
||||
// a non-nil reason value.
|
||||
func (p *shardHandler) handleSendError(attempt *channeldb.HTLCAttemptInfo,
|
||||
sendErr error) error {
|
||||
|
||||
reason := p.router.processSendError(
|
||||
attempt.AttemptID, &attempt.Route, sendErr,
|
||||
internalErrorReason := channeldb.FailureReasonError
|
||||
|
||||
// failPayment is a helper closure that fails the payment via the
|
||||
// router's control tower, which marks the payment as failed in db.
|
||||
failPayment := func(reason *channeldb.FailureReason,
|
||||
sendErr error) error {
|
||||
|
||||
log.Infof("Payment %v failed: final_outcome=%v, raw_err=%v",
|
||||
p.identifier, *reason, sendErr)
|
||||
|
||||
// Fail the payment via control tower.
|
||||
if err := p.router.cfg.Control.Fail(
|
||||
p.identifier, *reason); err != nil {
|
||||
|
||||
return err
|
||||
}
|
||||
|
||||
return *reason
|
||||
}
|
||||
|
||||
// reportFail is a helper closure that reports the failure to the
|
||||
// mission control, which helps us to decide whether we want to retry
|
||||
// the payment or not. If a non nil reason is returned from mission
|
||||
// control, it will further fail the payment via control tower.
|
||||
reportFail := func(srcIdx *int, msg lnwire.FailureMessage) error {
|
||||
// Report outcome to mission control.
|
||||
reason, err := p.router.cfg.MissionControl.ReportPaymentFail(
|
||||
attempt.AttemptID, &attempt.Route, srcIdx, msg,
|
||||
)
|
||||
if err != nil {
|
||||
log.Errorf("Error reporting payment result to mc: %v",
|
||||
err)
|
||||
|
||||
reason = &internalErrorReason
|
||||
}
|
||||
|
||||
// Exit early if there's no reason.
|
||||
if reason == nil {
|
||||
return nil
|
||||
}
|
||||
|
||||
return failPayment(reason, sendErr)
|
||||
}
|
||||
|
||||
if sendErr == htlcswitch.ErrUnreadableFailureMessage {
|
||||
log.Tracef("Unreadable failure when sending htlc")
|
||||
|
||||
return reportFail(nil, nil)
|
||||
}
|
||||
|
||||
// If the error is a ClearTextError, we have received a valid wire
|
||||
// failure message, either from our own outgoing link or from a node
|
||||
// down the route. If the error is not related to the propagation of
|
||||
// our payment, we can stop trying because an internal error has
|
||||
// occurred.
|
||||
rtErr, ok := sendErr.(htlcswitch.ClearTextError)
|
||||
if !ok {
|
||||
return failPayment(&internalErrorReason, sendErr)
|
||||
}
|
||||
|
||||
// failureSourceIdx is the index of the node that the failure occurred
|
||||
// at. If the ClearTextError received is not a ForwardingError the
|
||||
// payment error occurred at our node, so we leave this value as 0
|
||||
// to indicate that the failure occurred locally. If the error is a
|
||||
// ForwardingError, it did not originate at our node, so we set
|
||||
// failureSourceIdx to the index of the node where the failure occurred.
|
||||
failureSourceIdx := 0
|
||||
source, ok := rtErr.(*htlcswitch.ForwardingError)
|
||||
if ok {
|
||||
failureSourceIdx = source.FailureSourceIdx
|
||||
}
|
||||
|
||||
// Extract the wire failure and apply channel update if it contains one.
|
||||
// If we received an unknown failure message from a node along the
|
||||
// route, the failure message will be nil.
|
||||
failureMessage := rtErr.WireMessage()
|
||||
err := p.handleFailureMessage(
|
||||
&attempt.Route, failureSourceIdx, failureMessage,
|
||||
)
|
||||
if reason == nil {
|
||||
if err != nil {
|
||||
return failPayment(&internalErrorReason, sendErr)
|
||||
}
|
||||
|
||||
log.Tracef("Node=%v reported failure when sending htlc",
|
||||
failureSourceIdx)
|
||||
|
||||
return reportFail(&failureSourceIdx, failureMessage)
|
||||
}
|
||||
|
||||
// handleFailureMessage tries to apply a channel update present in the failure
|
||||
// message if any.
|
||||
func (p *shardHandler) handleFailureMessage(rt *route.Route,
|
||||
errorSourceIdx int, failure lnwire.FailureMessage) error {
|
||||
|
||||
if failure == nil {
|
||||
return nil
|
||||
}
|
||||
|
||||
log.Infof("Payment %v failed: final_outcome=%v, raw_err=%v",
|
||||
p.identifier, *reason, sendErr)
|
||||
// It makes no sense to apply our own channel updates.
|
||||
if errorSourceIdx == 0 {
|
||||
log.Errorf("Channel update of ourselves received")
|
||||
|
||||
err := p.router.cfg.Control.Fail(p.identifier, *reason)
|
||||
return nil
|
||||
}
|
||||
|
||||
// Extract channel update if the error contains one.
|
||||
update := p.router.extractChannelUpdate(failure)
|
||||
if update == nil {
|
||||
return nil
|
||||
}
|
||||
|
||||
// Parse pubkey to allow validation of the channel update. This should
|
||||
// always succeed, otherwise there is something wrong in our
|
||||
// implementation. Therefore return an error.
|
||||
errVertex := rt.Hops[errorSourceIdx-1].PubKeyBytes
|
||||
errSource, err := btcec.ParsePubKey(
|
||||
errVertex[:], btcec.S256(),
|
||||
)
|
||||
if err != nil {
|
||||
log.Errorf("Cannot parse pubkey: idx=%v, pubkey=%v",
|
||||
errorSourceIdx, errVertex)
|
||||
|
||||
return err
|
||||
}
|
||||
|
||||
var (
|
||||
isAdditionalEdge bool
|
||||
policy *channeldb.ChannelEdgePolicy
|
||||
)
|
||||
|
||||
// Before we apply the channel update, we need to decide whether the
|
||||
// update is for additional (ephemeral) edge or normal edge stored in
|
||||
// db.
|
||||
//
|
||||
// Note: the p.paySession might be nil here if it's called inside
|
||||
// SendToRoute where there's no payment lifecycle.
|
||||
if p.paySession != nil {
|
||||
policy = p.paySession.GetAdditionalEdgePolicy(
|
||||
errSource, update.ShortChannelID.ToUint64(),
|
||||
)
|
||||
if policy != nil {
|
||||
isAdditionalEdge = true
|
||||
}
|
||||
}
|
||||
|
||||
// Apply channel update to additional edge policy.
|
||||
if isAdditionalEdge {
|
||||
if !p.paySession.UpdateAdditionalEdge(
|
||||
update, errSource, policy) {
|
||||
|
||||
log.Debugf("Invalid channel update received: node=%v",
|
||||
errVertex)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// Apply channel update to the channel edge policy in our db.
|
||||
if !p.router.applyChannelUpdate(update, errSource) {
|
||||
log.Debugf("Invalid channel update received: node=%v",
|
||||
errVertex)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
|
||||
+379
-309
@@ -2,6 +2,7 @@ package routing
|
||||
|
||||
import (
|
||||
"crypto/rand"
|
||||
"fmt"
|
||||
"sync/atomic"
|
||||
"testing"
|
||||
"time"
|
||||
@@ -194,14 +195,6 @@ func TestRouterPaymentStateMachine(t *testing.T) {
|
||||
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.
|
||||
@@ -424,280 +417,6 @@ func TestRouterPaymentStateMachine(t *testing.T) {
|
||||
routes: []*route.Route{rt},
|
||||
paymentErr: channeldb.FailureReasonNoRoute,
|
||||
},
|
||||
|
||||
// =====================================
|
||||
// || MPP scenarios ||
|
||||
// =====================================
|
||||
{
|
||||
// Tests a simple successful MP payment of 4 shards.
|
||||
name: "MP success",
|
||||
|
||||
steps: []string{
|
||||
routerInitPayment,
|
||||
|
||||
// shard 0
|
||||
routeRelease,
|
||||
routerRegisterAttempt,
|
||||
sendToSwitchSuccess,
|
||||
|
||||
// shard 1
|
||||
routeRelease,
|
||||
routerRegisterAttempt,
|
||||
sendToSwitchSuccess,
|
||||
|
||||
// shard 2
|
||||
routeRelease,
|
||||
routerRegisterAttempt,
|
||||
sendToSwitchSuccess,
|
||||
|
||||
// shard 3
|
||||
routeRelease,
|
||||
routerRegisterAttempt,
|
||||
sendToSwitchSuccess,
|
||||
|
||||
// All shards succeed.
|
||||
getPaymentResultSuccess,
|
||||
getPaymentResultSuccess,
|
||||
getPaymentResultSuccess,
|
||||
getPaymentResultSuccess,
|
||||
|
||||
// Router should settle them all.
|
||||
routerSettleAttempt,
|
||||
routerSettleAttempt,
|
||||
routerSettleAttempt,
|
||||
routerSettleAttempt,
|
||||
|
||||
// And the final result is obviously
|
||||
// successful.
|
||||
paymentSuccess,
|
||||
},
|
||||
routes: []*route.Route{shard, shard, shard, shard},
|
||||
},
|
||||
{
|
||||
// An MP payment scenario where we need several extra
|
||||
// attempts before the payment finally settle.
|
||||
name: "MP failed shards",
|
||||
|
||||
steps: []string{
|
||||
routerInitPayment,
|
||||
|
||||
// shard 0
|
||||
routeRelease,
|
||||
routerRegisterAttempt,
|
||||
sendToSwitchSuccess,
|
||||
|
||||
// shard 1
|
||||
routeRelease,
|
||||
routerRegisterAttempt,
|
||||
sendToSwitchSuccess,
|
||||
|
||||
// shard 2
|
||||
routeRelease,
|
||||
routerRegisterAttempt,
|
||||
sendToSwitchSuccess,
|
||||
|
||||
// shard 3
|
||||
routeRelease,
|
||||
routerRegisterAttempt,
|
||||
sendToSwitchSuccess,
|
||||
|
||||
// First two shards fail, two new ones are sent.
|
||||
getPaymentResultTempFailure,
|
||||
getPaymentResultTempFailure,
|
||||
routerFailAttempt,
|
||||
routerFailAttempt,
|
||||
|
||||
routeRelease,
|
||||
routerRegisterAttempt,
|
||||
sendToSwitchSuccess,
|
||||
routeRelease,
|
||||
routerRegisterAttempt,
|
||||
sendToSwitchSuccess,
|
||||
|
||||
// The four shards settle.
|
||||
getPaymentResultSuccess,
|
||||
getPaymentResultSuccess,
|
||||
getPaymentResultSuccess,
|
||||
getPaymentResultSuccess,
|
||||
routerSettleAttempt,
|
||||
routerSettleAttempt,
|
||||
routerSettleAttempt,
|
||||
routerSettleAttempt,
|
||||
|
||||
// Overall payment succeeds.
|
||||
paymentSuccess,
|
||||
},
|
||||
routes: []*route.Route{
|
||||
shard, shard, shard, shard, shard, shard,
|
||||
},
|
||||
},
|
||||
{
|
||||
// An MP payment scenario where one of the shards fails,
|
||||
// but we still receive a single success shard.
|
||||
name: "MP one shard success",
|
||||
|
||||
steps: []string{
|
||||
routerInitPayment,
|
||||
|
||||
// shard 0
|
||||
routeRelease,
|
||||
routerRegisterAttempt,
|
||||
sendToSwitchSuccess,
|
||||
|
||||
// shard 1
|
||||
routeRelease,
|
||||
routerRegisterAttempt,
|
||||
sendToSwitchSuccess,
|
||||
|
||||
// shard 0 fails, and should be failed by the
|
||||
// router.
|
||||
getPaymentResultTempFailure,
|
||||
routerFailAttempt,
|
||||
|
||||
// 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.
|
||||
getPaymentResultSuccess,
|
||||
routerSettleAttempt,
|
||||
paymentSuccess,
|
||||
},
|
||||
routes: []*route.Route{halfShard, halfShard},
|
||||
},
|
||||
{
|
||||
// An MP payment scenario a shard fail with a terminal
|
||||
// error, causing the router to stop attempting.
|
||||
name: "MP terminal",
|
||||
|
||||
steps: []string{
|
||||
routerInitPayment,
|
||||
|
||||
// shard 0
|
||||
routeRelease,
|
||||
routerRegisterAttempt,
|
||||
sendToSwitchSuccess,
|
||||
|
||||
// shard 1
|
||||
routeRelease,
|
||||
routerRegisterAttempt,
|
||||
sendToSwitchSuccess,
|
||||
|
||||
// shard 2
|
||||
routeRelease,
|
||||
routerRegisterAttempt,
|
||||
sendToSwitchSuccess,
|
||||
|
||||
// shard 3
|
||||
routeRelease,
|
||||
routerRegisterAttempt,
|
||||
sendToSwitchSuccess,
|
||||
|
||||
// The first shard fail with a terminal error.
|
||||
getPaymentResultTerminalFailure,
|
||||
routerFailAttempt,
|
||||
routerFailPayment,
|
||||
|
||||
// Remaining 3 shards fail.
|
||||
getPaymentResultTempFailure,
|
||||
getPaymentResultTempFailure,
|
||||
getPaymentResultTempFailure,
|
||||
routerFailAttempt,
|
||||
routerFailAttempt,
|
||||
routerFailAttempt,
|
||||
|
||||
// Payment fails.
|
||||
paymentError,
|
||||
},
|
||||
routes: []*route.Route{
|
||||
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 {
|
||||
@@ -738,7 +457,7 @@ func testPaymentLifecycle(t *testing.T, test paymentLifecycleTestCase,
|
||||
sendResult := make(chan error)
|
||||
paymentResult := make(chan *htlcswitch.PaymentResult)
|
||||
|
||||
payer := &mockPayer{
|
||||
payer := &mockPayerOld{
|
||||
sendResult: sendResult,
|
||||
paymentResult: paymentResult,
|
||||
}
|
||||
@@ -748,8 +467,8 @@ func testPaymentLifecycle(t *testing.T, test paymentLifecycleTestCase,
|
||||
Chain: chain,
|
||||
ChainView: chainView,
|
||||
Control: control,
|
||||
SessionSource: &mockPaymentSessionSource{},
|
||||
MissionControl: &mockMissionControl{},
|
||||
SessionSource: &mockPaymentSessionSourceOld{},
|
||||
MissionControl: &mockMissionControlOld{},
|
||||
Payer: payer,
|
||||
ChannelPruneExpiry: time.Hour * 24,
|
||||
GraphPruneInterval: time.Hour * 2,
|
||||
@@ -821,12 +540,12 @@ func testPaymentLifecycle(t *testing.T, test paymentLifecycleTestCase,
|
||||
// Setup our payment session source to block on release of
|
||||
// routes.
|
||||
routeChan := make(chan struct{})
|
||||
router.cfg.SessionSource = &mockPaymentSessionSource{
|
||||
router.cfg.SessionSource = &mockPaymentSessionSourceOld{
|
||||
routes: test.routes,
|
||||
routeRelease: routeChan,
|
||||
}
|
||||
|
||||
router.cfg.MissionControl = &mockMissionControl{}
|
||||
router.cfg.MissionControl = &mockMissionControlOld{}
|
||||
|
||||
// Send the payment. Since this is new payment hash, the
|
||||
// information should be registered with the ControlTower.
|
||||
@@ -839,7 +558,20 @@ func testPaymentLifecycle(t *testing.T, test paymentLifecycleTestCase,
|
||||
}()
|
||||
|
||||
var resendResult chan error
|
||||
for _, step := range test.steps {
|
||||
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:
|
||||
@@ -847,19 +579,18 @@ func testPaymentLifecycle(t *testing.T, test paymentLifecycleTestCase,
|
||||
select {
|
||||
case args = <-control.init:
|
||||
case <-time.After(stepTimeout):
|
||||
t.Fatalf("no init payment with control")
|
||||
fatal("no init payment with control")
|
||||
}
|
||||
|
||||
if args.c == nil {
|
||||
t.Fatalf("expected non-nil CreationInfo")
|
||||
fatal("expected non-nil CreationInfo")
|
||||
}
|
||||
|
||||
case routeRelease:
|
||||
select {
|
||||
case <-routeChan:
|
||||
|
||||
case <-time.After(stepTimeout):
|
||||
t.Fatalf("no route requested")
|
||||
fatal("no route requested")
|
||||
}
|
||||
|
||||
// In this step we expect the router to make a call to
|
||||
@@ -869,12 +600,11 @@ func testPaymentLifecycle(t *testing.T, test paymentLifecycleTestCase,
|
||||
select {
|
||||
case args = <-control.registerAttempt:
|
||||
case <-time.After(stepTimeout):
|
||||
t.Fatalf("attempt not registered " +
|
||||
"with control")
|
||||
fatal("attempt not registered with control")
|
||||
}
|
||||
|
||||
if args.a == nil {
|
||||
t.Fatalf("expected non-nil AttemptInfo")
|
||||
fatal("expected non-nil AttemptInfo")
|
||||
}
|
||||
|
||||
// In this step we expect the router to call the
|
||||
@@ -883,7 +613,7 @@ func testPaymentLifecycle(t *testing.T, test paymentLifecycleTestCase,
|
||||
select {
|
||||
case <-control.settleAttempt:
|
||||
case <-time.After(stepTimeout):
|
||||
t.Fatalf("attempt settle not " +
|
||||
fatal("attempt settle not " +
|
||||
"registered with control")
|
||||
}
|
||||
|
||||
@@ -894,7 +624,7 @@ func testPaymentLifecycle(t *testing.T, test paymentLifecycleTestCase,
|
||||
select {
|
||||
case <-control.failAttempt:
|
||||
case <-time.After(stepTimeout):
|
||||
t.Fatalf("attempt fail not " +
|
||||
fatal("attempt fail not " +
|
||||
"registered with control")
|
||||
}
|
||||
|
||||
@@ -905,7 +635,7 @@ func testPaymentLifecycle(t *testing.T, test paymentLifecycleTestCase,
|
||||
select {
|
||||
case <-control.failPayment:
|
||||
case <-time.After(stepTimeout):
|
||||
t.Fatalf("payment fail not " +
|
||||
fatal("payment fail not " +
|
||||
"registered with control")
|
||||
}
|
||||
|
||||
@@ -915,7 +645,7 @@ func testPaymentLifecycle(t *testing.T, test paymentLifecycleTestCase,
|
||||
select {
|
||||
case sendResult <- nil:
|
||||
case <-time.After(stepTimeout):
|
||||
t.Fatalf("unable to send result")
|
||||
fatal("unable to send result")
|
||||
}
|
||||
|
||||
// In this step we expect the SendToSwitch method to be
|
||||
@@ -927,7 +657,7 @@ func testPaymentLifecycle(t *testing.T, test paymentLifecycleTestCase,
|
||||
1,
|
||||
):
|
||||
case <-time.After(stepTimeout):
|
||||
t.Fatalf("unable to send result")
|
||||
fatal("unable to send result")
|
||||
}
|
||||
|
||||
// In this step we expect the GetPaymentResult method
|
||||
@@ -939,7 +669,7 @@ func testPaymentLifecycle(t *testing.T, test paymentLifecycleTestCase,
|
||||
Preimage: preImage,
|
||||
}:
|
||||
case <-time.After(stepTimeout):
|
||||
t.Fatalf("unable to send result")
|
||||
fatal("unable to send result")
|
||||
}
|
||||
|
||||
// In this state we expect the GetPaymentResult method
|
||||
@@ -956,7 +686,7 @@ func testPaymentLifecycle(t *testing.T, test paymentLifecycleTestCase,
|
||||
Error: failure,
|
||||
}:
|
||||
case <-time.After(stepTimeout):
|
||||
t.Fatalf("unable to get result")
|
||||
fatal("unable to get result")
|
||||
}
|
||||
|
||||
// In this state we expect the router to call the
|
||||
@@ -974,7 +704,7 @@ func testPaymentLifecycle(t *testing.T, test paymentLifecycleTestCase,
|
||||
Error: failure,
|
||||
}:
|
||||
case <-time.After(stepTimeout):
|
||||
t.Fatalf("unable to get result")
|
||||
fatal("unable to get result")
|
||||
}
|
||||
|
||||
// In this step we manually try to resend the same
|
||||
@@ -994,7 +724,7 @@ func testPaymentLifecycle(t *testing.T, test paymentLifecycleTestCase,
|
||||
close(getPaymentResult)
|
||||
|
||||
if err := router.Stop(); err != nil {
|
||||
t.Fatalf("unable to restart: %v", err)
|
||||
fatal("unable to restart: %v", err)
|
||||
}
|
||||
|
||||
// In this step we manually start the router.
|
||||
@@ -1012,7 +742,7 @@ func testPaymentLifecycle(t *testing.T, test paymentLifecycleTestCase,
|
||||
require.Equal(t, test.paymentErr, err)
|
||||
|
||||
case <-time.After(stepTimeout):
|
||||
t.Fatalf("got no payment result")
|
||||
fatal("got no payment result")
|
||||
}
|
||||
|
||||
// In this state we expect the original payment to
|
||||
@@ -1028,7 +758,7 @@ func testPaymentLifecycle(t *testing.T, test paymentLifecycleTestCase,
|
||||
}
|
||||
|
||||
case <-time.After(stepTimeout):
|
||||
t.Fatalf("got no payment result")
|
||||
fatal("got no payment result")
|
||||
}
|
||||
|
||||
// In this state we expect to receive an error for the
|
||||
@@ -1041,7 +771,7 @@ func testPaymentLifecycle(t *testing.T, test paymentLifecycleTestCase,
|
||||
}
|
||||
|
||||
case <-time.After(stepTimeout):
|
||||
t.Fatalf("got no payment result")
|
||||
fatal("got no payment result")
|
||||
}
|
||||
|
||||
// In this state we expect the resent payment to
|
||||
@@ -1054,11 +784,11 @@ func testPaymentLifecycle(t *testing.T, test paymentLifecycleTestCase,
|
||||
}
|
||||
|
||||
case <-time.After(stepTimeout):
|
||||
t.Fatalf("got no payment result")
|
||||
fatal("got no payment result")
|
||||
}
|
||||
|
||||
default:
|
||||
t.Fatalf("unknown step %v", step)
|
||||
fatal("unknown step %v", step)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1068,3 +798,343 @@ func testPaymentLifecycle(t *testing.T, test paymentLifecycleTestCase,
|
||||
t.Fatalf("SendPayment didn't exit")
|
||||
}
|
||||
}
|
||||
|
||||
// TestPaymentState tests that the logics implemented on paymentState struct
|
||||
// are as expected. In particular, that the method terminated and
|
||||
// needWaitForShards return the right values.
|
||||
func TestPaymentState(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
testCases := []struct {
|
||||
name string
|
||||
|
||||
// Use the following three params, each is equivalent to a bool
|
||||
// statement, to construct 8 test cases so that we can
|
||||
// exhaustively catch all possible states.
|
||||
numShardsInFlight int
|
||||
remainingAmt lnwire.MilliSatoshi
|
||||
terminate bool
|
||||
|
||||
expectedTerminated bool
|
||||
expectedNeedWaitForShards bool
|
||||
}{
|
||||
{
|
||||
// If we have active shards and terminate is marked
|
||||
// false, the state is not terminated. Since the
|
||||
// remaining amount is zero, we need to wait for shards
|
||||
// to be finished and launch no more shards.
|
||||
name: "state 100",
|
||||
numShardsInFlight: 1,
|
||||
remainingAmt: lnwire.MilliSatoshi(0),
|
||||
terminate: false,
|
||||
expectedTerminated: false,
|
||||
expectedNeedWaitForShards: true,
|
||||
},
|
||||
{
|
||||
// If we have active shards while terminate is marked
|
||||
// true, the state is not terminated, and we need to
|
||||
// wait for shards to be finished and launch no more
|
||||
// shards.
|
||||
name: "state 101",
|
||||
numShardsInFlight: 1,
|
||||
remainingAmt: lnwire.MilliSatoshi(0),
|
||||
terminate: true,
|
||||
expectedTerminated: false,
|
||||
expectedNeedWaitForShards: true,
|
||||
},
|
||||
|
||||
{
|
||||
// If we have active shards and terminate is marked
|
||||
// false, the state is not terminated. Since the
|
||||
// remaining amount is not zero, we don't need to wait
|
||||
// for shards outcomes and should launch more shards.
|
||||
name: "state 110",
|
||||
numShardsInFlight: 1,
|
||||
remainingAmt: lnwire.MilliSatoshi(1),
|
||||
terminate: false,
|
||||
expectedTerminated: false,
|
||||
expectedNeedWaitForShards: false,
|
||||
},
|
||||
{
|
||||
// If we have active shards and terminate is marked
|
||||
// true, the state is not terminated. Even the
|
||||
// remaining amount is not zero, we need to wait for
|
||||
// shards outcomes because state is terminated.
|
||||
name: "state 111",
|
||||
numShardsInFlight: 1,
|
||||
remainingAmt: lnwire.MilliSatoshi(1),
|
||||
terminate: true,
|
||||
expectedTerminated: false,
|
||||
expectedNeedWaitForShards: true,
|
||||
},
|
||||
{
|
||||
// If we have no active shards while terminate is marked
|
||||
// false, the state is not terminated, and we don't
|
||||
// need to wait for more shard outcomes because there
|
||||
// are no active shards.
|
||||
name: "state 000",
|
||||
numShardsInFlight: 0,
|
||||
remainingAmt: lnwire.MilliSatoshi(0),
|
||||
terminate: false,
|
||||
expectedTerminated: false,
|
||||
expectedNeedWaitForShards: false,
|
||||
},
|
||||
{
|
||||
// If we have no active shards while terminate is marked
|
||||
// true, the state is terminated, and we don't need to
|
||||
// wait for shards to be finished.
|
||||
name: "state 001",
|
||||
numShardsInFlight: 0,
|
||||
remainingAmt: lnwire.MilliSatoshi(0),
|
||||
terminate: true,
|
||||
expectedTerminated: true,
|
||||
expectedNeedWaitForShards: false,
|
||||
},
|
||||
{
|
||||
// If we have no active shards while terminate is marked
|
||||
// false, the state is not terminated. Since the
|
||||
// remaining amount is not zero, we don't need to wait
|
||||
// for shards outcomes and should launch more shards.
|
||||
name: "state 010",
|
||||
numShardsInFlight: 0,
|
||||
remainingAmt: lnwire.MilliSatoshi(1),
|
||||
terminate: false,
|
||||
expectedTerminated: false,
|
||||
expectedNeedWaitForShards: false,
|
||||
},
|
||||
{
|
||||
// If we have no active shards while terminate is marked
|
||||
// true, the state is terminated, and we don't need to
|
||||
// wait for shards outcomes.
|
||||
name: "state 011",
|
||||
numShardsInFlight: 0,
|
||||
remainingAmt: lnwire.MilliSatoshi(1),
|
||||
terminate: true,
|
||||
expectedTerminated: true,
|
||||
expectedNeedWaitForShards: false,
|
||||
},
|
||||
}
|
||||
|
||||
for _, tc := range testCases {
|
||||
tc := tc
|
||||
t.Run(tc.name, func(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
ps := &paymentState{
|
||||
numShardsInFlight: tc.numShardsInFlight,
|
||||
remainingAmt: tc.remainingAmt,
|
||||
terminate: tc.terminate,
|
||||
}
|
||||
|
||||
require.Equal(
|
||||
t, tc.expectedTerminated, ps.terminated(),
|
||||
"terminated returned wrong value",
|
||||
)
|
||||
require.Equal(
|
||||
t, tc.expectedNeedWaitForShards,
|
||||
ps.needWaitForShards(),
|
||||
"needWaitForShards returned wrong value",
|
||||
)
|
||||
})
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
// TestUpdatePaymentState checks that the method updatePaymentState updates the
|
||||
// paymentState as expected.
|
||||
func TestUpdatePaymentState(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
// paymentHash is the identifier on paymentLifecycle.
|
||||
paymentHash := lntypes.Hash{}
|
||||
|
||||
// TODO(yy): make MPPayment into an interface so we can mock it. The
|
||||
// current design implicitly tests the methods SendAmt, TerminalInfo,
|
||||
// and InFlightHTLCs on channeldb.MPPayment, which is not good. Once
|
||||
// MPPayment becomes an interface, we can then mock these methods here.
|
||||
|
||||
// SentAmt returns 90, 10
|
||||
// TerminalInfo returns non-nil, nil
|
||||
// InFlightHTLCs returns 0
|
||||
var preimage lntypes.Preimage
|
||||
paymentSettled := &channeldb.MPPayment{
|
||||
HTLCs: []channeldb.HTLCAttempt{
|
||||
makeSettledAttempt(100, 10, preimage),
|
||||
},
|
||||
}
|
||||
|
||||
// SentAmt returns 0, 0
|
||||
// TerminalInfo returns nil, non-nil
|
||||
// InFlightHTLCs returns 0
|
||||
reason := channeldb.FailureReasonError
|
||||
paymentFailed := &channeldb.MPPayment{
|
||||
FailureReason: &reason,
|
||||
}
|
||||
|
||||
// SentAmt returns 90, 10
|
||||
// TerminalInfo returns nil, nil
|
||||
// InFlightHTLCs returns 1
|
||||
paymentActive := &channeldb.MPPayment{
|
||||
HTLCs: []channeldb.HTLCAttempt{
|
||||
makeActiveAttempt(100, 10),
|
||||
makeFailedAttempt(100, 10),
|
||||
},
|
||||
}
|
||||
|
||||
testCases := []struct {
|
||||
name string
|
||||
payment *channeldb.MPPayment
|
||||
totalAmt int
|
||||
feeLimit int
|
||||
|
||||
expectedState *paymentState
|
||||
shouldReturnError bool
|
||||
}{
|
||||
{
|
||||
// Test that the error returned from FetchPayment is
|
||||
// handled properly. We use a nil payment to indicate
|
||||
// we want to return an error.
|
||||
name: "fetch payment error",
|
||||
payment: nil,
|
||||
shouldReturnError: true,
|
||||
},
|
||||
{
|
||||
// Test that when the sentAmt exceeds totalAmount, the
|
||||
// error is returned.
|
||||
name: "amount exceeded error",
|
||||
payment: paymentSettled,
|
||||
totalAmt: 1,
|
||||
shouldReturnError: true,
|
||||
},
|
||||
{
|
||||
// Test that when the fee budget is reached, the
|
||||
// remaining fee should be zero.
|
||||
name: "fee budget reached",
|
||||
payment: paymentActive,
|
||||
totalAmt: 1000,
|
||||
feeLimit: 1,
|
||||
expectedState: &paymentState{
|
||||
numShardsInFlight: 1,
|
||||
remainingAmt: 1000 - 90,
|
||||
remainingFees: 0,
|
||||
terminate: false,
|
||||
},
|
||||
},
|
||||
{
|
||||
// Test when the payment is settled, the state should
|
||||
// be marked as terminated.
|
||||
name: "payment settled",
|
||||
payment: paymentSettled,
|
||||
totalAmt: 1000,
|
||||
feeLimit: 100,
|
||||
expectedState: &paymentState{
|
||||
numShardsInFlight: 0,
|
||||
remainingAmt: 1000 - 90,
|
||||
remainingFees: 100 - 10,
|
||||
terminate: true,
|
||||
},
|
||||
},
|
||||
{
|
||||
// Test when the payment is failed, the state should be
|
||||
// marked as terminated.
|
||||
name: "payment failed",
|
||||
payment: paymentFailed,
|
||||
totalAmt: 1000,
|
||||
feeLimit: 100,
|
||||
expectedState: &paymentState{
|
||||
numShardsInFlight: 0,
|
||||
remainingAmt: 1000,
|
||||
remainingFees: 100,
|
||||
terminate: true,
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
for _, tc := range testCases {
|
||||
tc := tc
|
||||
t.Run(tc.name, func(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
// Create mock control tower and assign it to router.
|
||||
// We will then use the router and the paymentHash
|
||||
// above to create our paymentLifecycle for this test.
|
||||
ct := &mockControlTower{}
|
||||
rt := &ChannelRouter{cfg: &Config{Control: ct}}
|
||||
pl := &paymentLifecycle{
|
||||
router: rt,
|
||||
identifier: paymentHash,
|
||||
totalAmount: lnwire.MilliSatoshi(tc.totalAmt),
|
||||
feeLimit: lnwire.MilliSatoshi(tc.feeLimit),
|
||||
}
|
||||
|
||||
if tc.payment == nil {
|
||||
// A nil payment indicates we want to test an
|
||||
// error returned from FetchPayment.
|
||||
dummyErr := errors.New("dummy")
|
||||
ct.On("FetchPayment", paymentHash).Return(
|
||||
nil, dummyErr,
|
||||
)
|
||||
|
||||
} else {
|
||||
// Otherwise we will return the payment.
|
||||
ct.On("FetchPayment", paymentHash).Return(
|
||||
tc.payment, nil,
|
||||
)
|
||||
}
|
||||
|
||||
// Call the method that updates the payment state.
|
||||
_, state, err := pl.updatePaymentState()
|
||||
|
||||
// Assert that the mock method is called as
|
||||
// intended.
|
||||
ct.AssertExpectations(t)
|
||||
|
||||
if tc.shouldReturnError {
|
||||
require.Error(t, err, "expect an error")
|
||||
return
|
||||
}
|
||||
|
||||
require.NoError(t, err, "unexpected error")
|
||||
require.Equal(
|
||||
t, tc.expectedState, state,
|
||||
"state not updated as expected",
|
||||
)
|
||||
|
||||
})
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
func makeActiveAttempt(total, fee int) channeldb.HTLCAttempt {
|
||||
return channeldb.HTLCAttempt{
|
||||
HTLCAttemptInfo: makeAttemptInfo(total, total-fee),
|
||||
}
|
||||
}
|
||||
|
||||
func makeSettledAttempt(total, fee int,
|
||||
preimage lntypes.Preimage) channeldb.HTLCAttempt {
|
||||
|
||||
return channeldb.HTLCAttempt{
|
||||
HTLCAttemptInfo: makeAttemptInfo(total, total-fee),
|
||||
Settle: &channeldb.HTLCSettleInfo{Preimage: preimage},
|
||||
}
|
||||
}
|
||||
|
||||
func makeFailedAttempt(total, fee int) channeldb.HTLCAttempt {
|
||||
return channeldb.HTLCAttempt{
|
||||
HTLCAttemptInfo: makeAttemptInfo(total, total-fee),
|
||||
Failure: &channeldb.HTLCFailInfo{
|
||||
Reason: channeldb.HTLCFailInternal,
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
func makeAttemptInfo(total, amtForwarded int) channeldb.HTLCAttemptInfo {
|
||||
hop := &route.Hop{AmtToForward: lnwire.MilliSatoshi(amtForwarded)}
|
||||
return channeldb.HTLCAttemptInfo{
|
||||
Route: route.Route{
|
||||
TotalAmount: lnwire.MilliSatoshi(total),
|
||||
Hops: []*route.Hop{hop},
|
||||
},
|
||||
}
|
||||
}
|
||||
@@ -3,7 +3,9 @@ package routing
|
||||
import (
|
||||
"fmt"
|
||||
|
||||
"github.com/btcsuite/btcd/btcec"
|
||||
"github.com/btcsuite/btclog"
|
||||
"github.com/davecgh/go-spew/spew"
|
||||
"github.com/lightningnetwork/lnd/build"
|
||||
"github.com/lightningnetwork/lnd/channeldb"
|
||||
"github.com/lightningnetwork/lnd/lnwire"
|
||||
@@ -136,6 +138,19 @@ type PaymentSession interface {
|
||||
// during path finding.
|
||||
RequestRoute(maxAmt, feeLimit lnwire.MilliSatoshi,
|
||||
activeShards, height uint32) (*route.Route, error)
|
||||
|
||||
// UpdateAdditionalEdge takes an additional channel edge policy
|
||||
// (private channels) and applies the update from the message. Returns
|
||||
// a boolean to indicate whether the update has been applied without
|
||||
// error.
|
||||
UpdateAdditionalEdge(msg *lnwire.ChannelUpdate, pubKey *btcec.PublicKey,
|
||||
policy *channeldb.ChannelEdgePolicy) bool
|
||||
|
||||
// GetAdditionalEdgePolicy uses the public key and channel ID to query
|
||||
// the ephemeral channel edge policy for additional edges. Returns a nil
|
||||
// if nothing found.
|
||||
GetAdditionalEdgePolicy(pubKey *btcec.PublicKey,
|
||||
channelID uint64) *channeldb.ChannelEdgePolicy
|
||||
}
|
||||
|
||||
// paymentSession is used during an HTLC routings session to prune the local
|
||||
@@ -382,3 +397,53 @@ func (p *paymentSession) RequestRoute(maxAmt, feeLimit lnwire.MilliSatoshi,
|
||||
return route, err
|
||||
}
|
||||
}
|
||||
|
||||
// UpdateAdditionalEdge updates the channel edge policy for a private edge. It
|
||||
// validates the message signature and checks it's up to date, then applies the
|
||||
// updates to the supplied policy. It returns a boolean to indicate whether
|
||||
// there's an error when applying the updates.
|
||||
func (p *paymentSession) UpdateAdditionalEdge(msg *lnwire.ChannelUpdate,
|
||||
pubKey *btcec.PublicKey, policy *channeldb.ChannelEdgePolicy) bool {
|
||||
|
||||
// Validate the message signature.
|
||||
if err := VerifyChannelUpdateSignature(msg, pubKey); err != nil {
|
||||
log.Errorf(
|
||||
"Unable to validate channel update signature: %v", err,
|
||||
)
|
||||
return false
|
||||
}
|
||||
|
||||
// Update channel policy for the additional edge.
|
||||
policy.TimeLockDelta = msg.TimeLockDelta
|
||||
policy.FeeBaseMSat = lnwire.MilliSatoshi(msg.BaseFee)
|
||||
policy.FeeProportionalMillionths = lnwire.MilliSatoshi(msg.FeeRate)
|
||||
|
||||
log.Debugf("New private channel update applied: %v",
|
||||
newLogClosure(func() string { return spew.Sdump(msg) }))
|
||||
|
||||
return true
|
||||
}
|
||||
|
||||
// GetAdditionalEdgePolicy uses the public key and channel ID to query the
|
||||
// ephemeral channel edge policy for additional edges. Returns a nil if nothing
|
||||
// found.
|
||||
func (p *paymentSession) GetAdditionalEdgePolicy(pubKey *btcec.PublicKey,
|
||||
channelID uint64) *channeldb.ChannelEdgePolicy {
|
||||
|
||||
target := route.NewVertex(pubKey)
|
||||
|
||||
edges, ok := p.additionalEdges[target]
|
||||
if !ok {
|
||||
return nil
|
||||
}
|
||||
|
||||
for _, edge := range edges {
|
||||
if edge.ChannelID != channelID {
|
||||
continue
|
||||
}
|
||||
|
||||
return edge
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
@@ -2,10 +2,13 @@ package routing
|
||||
|
||||
import (
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"github.com/lightningnetwork/lnd/channeldb"
|
||||
"github.com/lightningnetwork/lnd/lntypes"
|
||||
"github.com/lightningnetwork/lnd/lnwire"
|
||||
"github.com/lightningnetwork/lnd/routing/route"
|
||||
"github.com/lightningnetwork/lnd/zpay32"
|
||||
"github.com/stretchr/testify/require"
|
||||
)
|
||||
|
||||
@@ -70,6 +73,109 @@ func TestValidateCLTVLimit(t *testing.T) {
|
||||
}
|
||||
}
|
||||
|
||||
// TestUpdateAdditionalEdge checks that we can update the additional edges as
|
||||
// expected.
|
||||
func TestUpdateAdditionalEdge(t *testing.T) {
|
||||
|
||||
var (
|
||||
testChannelID = uint64(12345)
|
||||
oldFeeBaseMSat = uint32(1000)
|
||||
newFeeBaseMSat = uint32(1100)
|
||||
oldExpiryDelta = uint16(100)
|
||||
newExpiryDelta = uint16(120)
|
||||
|
||||
payHash lntypes.Hash
|
||||
)
|
||||
|
||||
// Create a minimal test node using the private key priv1.
|
||||
pub := priv1.PubKey().SerializeCompressed()
|
||||
testNode := &channeldb.LightningNode{}
|
||||
copy(testNode.PubKeyBytes[:], pub)
|
||||
|
||||
nodeID, err := testNode.PubKey()
|
||||
require.NoError(t, err, "failed to get node id")
|
||||
|
||||
// Create a payment with a route hint.
|
||||
payment := &LightningPayment{
|
||||
Target: testNode.PubKeyBytes,
|
||||
Amount: 1000,
|
||||
RouteHints: [][]zpay32.HopHint{{
|
||||
zpay32.HopHint{
|
||||
// The nodeID is actually the target itself. It
|
||||
// doesn't matter as we are not doing routing
|
||||
// in this test.
|
||||
NodeID: nodeID,
|
||||
ChannelID: testChannelID,
|
||||
FeeBaseMSat: oldFeeBaseMSat,
|
||||
CLTVExpiryDelta: oldExpiryDelta,
|
||||
},
|
||||
}},
|
||||
paymentHash: &payHash,
|
||||
}
|
||||
|
||||
// Create the paymentsession.
|
||||
session, err := newPaymentSession(
|
||||
payment,
|
||||
func() (map[uint64]lnwire.MilliSatoshi,
|
||||
error) {
|
||||
|
||||
return nil, nil
|
||||
},
|
||||
func() (routingGraph, func(), error) {
|
||||
return &sessionGraph{}, func() {}, nil
|
||||
},
|
||||
&MissionControl{},
|
||||
PathFindingConfig{},
|
||||
)
|
||||
require.NoError(t, err, "failed to create payment session")
|
||||
|
||||
// We should have 1 additional edge.
|
||||
require.Equal(t, 1, len(session.additionalEdges))
|
||||
|
||||
// The edge should use nodeID as key, and its value should have 1 edge
|
||||
// policy.
|
||||
vertex := route.NewVertex(nodeID)
|
||||
policies, ok := session.additionalEdges[vertex]
|
||||
require.True(t, ok, "cannot find policy")
|
||||
require.Equal(t, 1, len(policies), "should have 1 edge policy")
|
||||
|
||||
// Check that the policy has been created as expected.
|
||||
policy := policies[0]
|
||||
require.Equal(t, testChannelID, policy.ChannelID, "channel ID mismatch")
|
||||
require.Equal(t,
|
||||
oldExpiryDelta, policy.TimeLockDelta, "timelock delta mismatch",
|
||||
)
|
||||
require.Equal(t,
|
||||
lnwire.MilliSatoshi(oldFeeBaseMSat),
|
||||
policy.FeeBaseMSat, "fee base msat mismatch",
|
||||
)
|
||||
|
||||
// Create the channel update message and sign.
|
||||
msg := &lnwire.ChannelUpdate{
|
||||
ShortChannelID: lnwire.NewShortChanIDFromInt(testChannelID),
|
||||
Timestamp: uint32(time.Now().Unix()),
|
||||
BaseFee: newFeeBaseMSat,
|
||||
TimeLockDelta: newExpiryDelta,
|
||||
}
|
||||
signErrChanUpdate(t, priv1, msg)
|
||||
|
||||
// Apply the update.
|
||||
require.True(t,
|
||||
session.UpdateAdditionalEdge(msg, nodeID, policy),
|
||||
"failed to update additional edge",
|
||||
)
|
||||
|
||||
// Check that the policy has been updated as expected.
|
||||
require.Equal(t, testChannelID, policy.ChannelID, "channel ID mismatch")
|
||||
require.Equal(t,
|
||||
newExpiryDelta, policy.TimeLockDelta, "timelock delta mismatch",
|
||||
)
|
||||
require.Equal(t,
|
||||
lnwire.MilliSatoshi(newFeeBaseMSat),
|
||||
policy.FeeBaseMSat, "fee base msat mismatch",
|
||||
)
|
||||
}
|
||||
|
||||
func TestRequestRoute(t *testing.T) {
|
||||
const (
|
||||
height = 10
|
||||
|
||||
+5
-122
@@ -2163,15 +2163,13 @@ func (r *ChannelRouter) SendToRoute(htlcHash lntypes.Hash, rt *route.Route) (
|
||||
// mark the payment failed with the control tower immediately. Process
|
||||
// the error to check if it maps into a terminal error code, if not use
|
||||
// a generic NO_ROUTE error.
|
||||
reason := r.processSendError(
|
||||
attempt.AttemptID, &attempt.Route, shardError,
|
||||
)
|
||||
if reason == nil {
|
||||
r := channeldb.FailureReasonNoRoute
|
||||
reason = &r
|
||||
if err := sh.handleSendError(attempt, shardError); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
err = r.cfg.Control.Fail(paymentIdentifier, *reason)
|
||||
err = r.cfg.Control.Fail(
|
||||
paymentIdentifier, channeldb.FailureReasonNoRoute,
|
||||
)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
@@ -2230,121 +2228,6 @@ func (r *ChannelRouter) sendPayment(
|
||||
|
||||
}
|
||||
|
||||
// tryApplyChannelUpdate tries to apply a channel update present in the failure
|
||||
// message if any.
|
||||
func (r *ChannelRouter) tryApplyChannelUpdate(rt *route.Route,
|
||||
errorSourceIdx int, failure lnwire.FailureMessage) error {
|
||||
|
||||
// It makes no sense to apply our own channel updates.
|
||||
if errorSourceIdx == 0 {
|
||||
log.Errorf("Channel update of ourselves received")
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// Extract channel update if the error contains one.
|
||||
update := r.extractChannelUpdate(failure)
|
||||
if update == nil {
|
||||
return nil
|
||||
}
|
||||
|
||||
// Parse pubkey to allow validation of the channel update. This should
|
||||
// always succeed, otherwise there is something wrong in our
|
||||
// implementation. Therefore return an error.
|
||||
errVertex := rt.Hops[errorSourceIdx-1].PubKeyBytes
|
||||
errSource, err := btcec.ParsePubKey(
|
||||
errVertex[:], btcec.S256(),
|
||||
)
|
||||
if err != nil {
|
||||
log.Errorf("Cannot parse pubkey: idx=%v, pubkey=%v",
|
||||
errorSourceIdx, errVertex)
|
||||
|
||||
return err
|
||||
}
|
||||
|
||||
// Apply channel update.
|
||||
if !r.applyChannelUpdate(update, errSource) {
|
||||
log.Debugf("Invalid channel update received: node=%v",
|
||||
errVertex)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// processSendError analyzes the error for the payment attempt received from the
|
||||
// switch and updates mission control and/or channel policies. Depending on the
|
||||
// error type, this error is either the final outcome of the payment or we need
|
||||
// to continue with an alternative route. A final outcome is indicated by a
|
||||
// non-nil return value.
|
||||
func (r *ChannelRouter) processSendError(attemptID uint64, rt *route.Route,
|
||||
sendErr error) *channeldb.FailureReason {
|
||||
|
||||
internalErrorReason := channeldb.FailureReasonError
|
||||
|
||||
reportFail := func(srcIdx *int,
|
||||
msg lnwire.FailureMessage) *channeldb.FailureReason {
|
||||
|
||||
// Report outcome to mission control.
|
||||
reason, err := r.cfg.MissionControl.ReportPaymentFail(
|
||||
attemptID, rt, srcIdx, msg,
|
||||
)
|
||||
if err != nil {
|
||||
log.Errorf("Error reporting payment result to mc: %v",
|
||||
err)
|
||||
|
||||
return &internalErrorReason
|
||||
}
|
||||
|
||||
return reason
|
||||
}
|
||||
|
||||
if sendErr == htlcswitch.ErrUnreadableFailureMessage {
|
||||
log.Tracef("Unreadable failure when sending htlc")
|
||||
|
||||
return reportFail(nil, nil)
|
||||
}
|
||||
|
||||
// If the error is a ClearTextError, we have received a valid wire
|
||||
// failure message, either from our own outgoing link or from a node
|
||||
// down the route. If the error is not related to the propagation of
|
||||
// our payment, we can stop trying because an internal error has
|
||||
// occurred.
|
||||
rtErr, ok := sendErr.(htlcswitch.ClearTextError)
|
||||
if !ok {
|
||||
return &internalErrorReason
|
||||
}
|
||||
|
||||
// failureSourceIdx is the index of the node that the failure occurred
|
||||
// at. If the ClearTextError received is not a ForwardingError the
|
||||
// payment error occurred at our node, so we leave this value as 0
|
||||
// to indicate that the failure occurred locally. If the error is a
|
||||
// ForwardingError, it did not originate at our node, so we set
|
||||
// failureSourceIdx to the index of the node where the failure occurred.
|
||||
failureSourceIdx := 0
|
||||
source, ok := rtErr.(*htlcswitch.ForwardingError)
|
||||
if ok {
|
||||
failureSourceIdx = source.FailureSourceIdx
|
||||
}
|
||||
|
||||
// Extract the wire failure and apply channel update if it contains one.
|
||||
// If we received an unknown failure message from a node along the
|
||||
// route, the failure message will be nil.
|
||||
failureMessage := rtErr.WireMessage()
|
||||
if failureMessage != nil {
|
||||
err := r.tryApplyChannelUpdate(
|
||||
rt, failureSourceIdx, failureMessage,
|
||||
)
|
||||
if err != nil {
|
||||
return &internalErrorReason
|
||||
}
|
||||
}
|
||||
|
||||
log.Tracef("Node=%v reported failure when sending htlc",
|
||||
failureSourceIdx)
|
||||
|
||||
return reportFail(&failureSourceIdx, failureMessage)
|
||||
}
|
||||
|
||||
// extractChannelUpdate examines the error and extracts the channel update.
|
||||
func (r *ChannelRouter) extractChannelUpdate(
|
||||
failure lnwire.FailureMessage) *lnwire.ChannelUpdate {
|
||||
|
||||
+1331
-364
File diff suppressed because it is too large.
Load diff
Vendored
+6
-5
@@ -39,7 +39,8 @@
|
||||
},
|
||||
{
|
||||
"source": false,
|
||||
"pubkey": "032b480de5d002f1a8fd1fe1bbf0a0f1b07760f65f052e66d56f15d71097c01add",
|
||||
"pubkey": "026c43a8ac1cd8519985766e90748e1e06871dab0ff6b8af27e8c1a61640481318",
|
||||
"privkey": "82b266f659bd83a976bac11b2cc442baec5508e84e61085d7ec2b0fc52156c87",
|
||||
"alias": "songoku"
|
||||
},
|
||||
{
|
||||
@@ -154,7 +155,7 @@
|
||||
"capacity": 120000
|
||||
},
|
||||
{
|
||||
"node_1": "032b480de5d002f1a8fd1fe1bbf0a0f1b07760f65f052e66d56f15d71097c01add",
|
||||
"node_1": "026c43a8ac1cd8519985766e90748e1e06871dab0ff6b8af27e8c1a61640481318",
|
||||
"node_2": "0367cec75158a4129177bfb8b269cb586efe93d751b43800d456485e81c2620ca6",
|
||||
"channel_id": 12345,
|
||||
"channel_point": "89dc56859c6a082d15ba1a7f6cb6be3fea62e1746e2cb8497b1189155c21a233:0",
|
||||
@@ -168,7 +169,7 @@
|
||||
"capacity": 100000
|
||||
},
|
||||
{
|
||||
"node_1": "032b480de5d002f1a8fd1fe1bbf0a0f1b07760f65f052e66d56f15d71097c01add",
|
||||
"node_1": "026c43a8ac1cd8519985766e90748e1e06871dab0ff6b8af27e8c1a61640481318",
|
||||
"node_2": "0367cec75158a4129177bfb8b269cb586efe93d751b43800d456485e81c2620ca6",
|
||||
"channel_id": 12345,
|
||||
"channel_point": "89dc56859c6a082d15ba1a7f6cb6be3fea62e1746e2cb8497b1189155c21a233:0",
|
||||
@@ -182,7 +183,7 @@
|
||||
"capacity": 100000
|
||||
},
|
||||
{
|
||||
"node_1": "032b480de5d002f1a8fd1fe1bbf0a0f1b07760f65f052e66d56f15d71097c01add",
|
||||
"node_1": "026c43a8ac1cd8519985766e90748e1e06871dab0ff6b8af27e8c1a61640481318",
|
||||
"node_2": "036264734b40c9e91d3d990a8cdfbbe23b5b0b7ad3cd0e080a25dcd05d39eeb7eb",
|
||||
"channel_id": 3495345,
|
||||
"channel_point": "9f155756b33a0a6827713965babbd561b55f9520444ac5db0cf7cb2eb0deb5bc:0",
|
||||
@@ -196,7 +197,7 @@
|
||||
"capacity": 110000
|
||||
},
|
||||
{
|
||||
"node_1": "032b480de5d002f1a8fd1fe1bbf0a0f1b07760f65f052e66d56f15d71097c01add",
|
||||
"node_1": "026c43a8ac1cd8519985766e90748e1e06871dab0ff6b8af27e8c1a61640481318",
|
||||
"node_2": "036264734b40c9e91d3d990a8cdfbbe23b5b0b7ad3cd0e080a25dcd05d39eeb7eb",
|
||||
"channel_id": 3495345,
|
||||
"channel_point": "9f155756b33a0a6827713965babbd561b55f9520444ac5db0cf7cb2eb0deb5bc:0",
|
||||
|
||||
Reference in new issue
Block a user