Merge pull request #8170 from yyforyongyu/0-18-staging-missed

Add missing commits from `0-18-staging`
This commit is contained in:
Olaoluwa Osuntokun authored and GitHub committed 2023-11-13 10:38:23 -08:00
commit e02fd39ce6
12 files changed
+2580 -2243

No files matched your search

+46 -5
View File
@@ -219,10 +219,10 @@ func (m *MPPayment) Terminated() bool {
// TerminalInfo returns any HTLC settle info recorded. If no settle info is // TerminalInfo returns any HTLC settle info recorded. If no settle info is
// recorded, any payment level failure will be returned. If neither a settle // recorded, any payment level failure will be returned. If neither a settle
// nor a failure is recorded, both return values will be nil. // nor a failure is recorded, both return values will be nil.
func (m *MPPayment) TerminalInfo() (*HTLCSettleInfo, *FailureReason) { func (m *MPPayment) TerminalInfo() (*HTLCAttempt, *FailureReason) {
for _, h := range m.HTLCs { for _, h := range m.HTLCs {
if h.Settle != nil { if h.Settle != nil {
return h.Settle, nil return &h, nil
} }
} }
@@ -264,6 +264,7 @@ func (m *MPPayment) InFlightHTLCs() []HTLCAttempt {
// GetAttempt returns the specified htlc attempt on the payment. // GetAttempt returns the specified htlc attempt on the payment.
func (m *MPPayment) GetAttempt(id uint64) (*HTLCAttempt, error) { func (m *MPPayment) GetAttempt(id uint64) (*HTLCAttempt, error) {
// TODO(yy): iteration can be slow, make it into a tree or use BS.
for _, htlc := range m.HTLCs { for _, htlc := range m.HTLCs {
htlc := htlc htlc := htlc
if htlc.AttemptID == id { if htlc.AttemptID == id {
@@ -463,9 +464,49 @@ func (m *MPPayment) GetHTLCs() []HTLCAttempt {
return m.HTLCs return m.HTLCs
} }
// GetFailureReason returns the failure reason. // AllowMoreAttempts is used to decide whether we can safely attempt more HTLCs
func (m *MPPayment) GetFailureReason() *FailureReason { // for a given payment state. Return an error if the payment is in an
return m.FailureReason // unexpected state.
func (m *MPPayment) AllowMoreAttempts() (bool, error) {
// Now check whether the remainingAmt is zero or not. If we don't have
// any remainingAmt, no more HTLCs should be made.
if m.State.RemainingAmt == 0 {
// If the payment is newly created, yet we don't have any
// remainingAmt, return an error.
if m.Status == StatusInitiated {
return false, fmt.Errorf("%w: initiated payment has "+
"zero remainingAmt", ErrPaymentInternal)
}
// Otherwise, exit early since all other statuses with zero
// remainingAmt indicate no more HTLCs can be made.
return false, nil
}
// Otherwise, the remaining amount is not zero, we now decide whether
// to make more attempts based on the payment's current status.
//
// If at least one of the payment's attempts is settled, yet we haven't
// sent all the amount, it indicates something is wrong with the peer
// as the preimage is received. In this case, return an error state.
if m.Status == StatusSucceeded {
return false, fmt.Errorf("%w: payment already succeeded but "+
"still have remaining amount %v", ErrPaymentInternal,
m.State.RemainingAmt)
}
// Now check if we can register a new HTLC.
err := m.Registrable()
if err != nil {
log.Warnf("Payment(%v): cannot register HTLC attempt: %v, "+
"current status: %s", m.Info.PaymentIdentifier,
err, m.Status)
return false, nil
}
// Now we know we can register new HTLCs.
return true, nil
} }
// serializeHTLCSettleInfo serializes the details of a settled htlc. // serializeHTLCSettleInfo serializes the details of a settled htlc.
+177
View File
@@ -368,6 +368,183 @@ func TestNeedWaitAttempts(t *testing.T) {
} }
} }
// TestAllowMoreAttempts checks whether more attempts can be created against
// ALL possible payment statuses.
func TestAllowMoreAttempts(t *testing.T) {
t.Parallel()
testCases := []struct {
status PaymentStatus
remainingAmt lnwire.MilliSatoshi
hasSettledHTLC bool
paymentFailed bool
allowMore bool
expectedErr error
}{
{
// A newly created payment with zero remainingAmt
// indicates an error.
status: StatusInitiated,
remainingAmt: 0,
allowMore: false,
expectedErr: ErrPaymentInternal,
},
{
// With zero remainingAmt we don't allow more HTLC
// attempts.
status: StatusInFlight,
remainingAmt: 0,
allowMore: false,
expectedErr: nil,
},
{
// With zero remainingAmt we don't allow more HTLC
// attempts.
status: StatusSucceeded,
remainingAmt: 0,
allowMore: false,
expectedErr: nil,
},
{
// With zero remainingAmt we don't allow more HTLC
// attempts.
status: StatusFailed,
remainingAmt: 0,
allowMore: false,
expectedErr: nil,
},
{
// With zero remainingAmt and settled HTLCs we don't
// allow more HTLC attempts.
status: StatusInFlight,
remainingAmt: 0,
hasSettledHTLC: true,
allowMore: false,
expectedErr: nil,
},
{
// With zero remainingAmt and failed payment we don't
// allow more HTLC attempts.
status: StatusInFlight,
remainingAmt: 0,
paymentFailed: true,
allowMore: false,
expectedErr: nil,
},
{
// With zero remainingAmt and both settled HTLCs and
// failed payment, we don't allow more HTLC attempts.
status: StatusInFlight,
remainingAmt: 0,
hasSettledHTLC: true,
paymentFailed: true,
allowMore: false,
expectedErr: nil,
},
{
// A newly created payment can have more attempts.
status: StatusInitiated,
remainingAmt: 1000,
allowMore: true,
expectedErr: nil,
},
{
// With HTLCs inflight we can have more attempts when
// the remainingAmt is not zero and we have neither
// failed payment or settled HTLCs.
status: StatusInFlight,
remainingAmt: 1000,
allowMore: true,
expectedErr: nil,
},
{
// With HTLCs inflight we cannot have more attempts
// though the remainingAmt is not zero but we have
// settled HTLCs.
status: StatusInFlight,
remainingAmt: 1000,
hasSettledHTLC: true,
allowMore: false,
expectedErr: nil,
},
{
// With HTLCs inflight we cannot have more attempts
// though the remainingAmt is not zero but we have
// failed payment.
status: StatusInFlight,
remainingAmt: 1000,
paymentFailed: true,
allowMore: false,
expectedErr: nil,
},
{
// With HTLCs inflight we cannot have more attempts
// though the remainingAmt is not zero but we have
// settled HTLCs and failed payment.
status: StatusInFlight,
remainingAmt: 1000,
hasSettledHTLC: true,
paymentFailed: true,
allowMore: false,
expectedErr: nil,
},
{
// With the payment settled, but the remainingAmt is
// not zero, we have an error state.
status: StatusSucceeded,
remainingAmt: 1000,
hasSettledHTLC: true,
allowMore: false,
expectedErr: ErrPaymentInternal,
},
{
// With the payment failed with no inflight HTLCs, we
// don't allow more attempts to be made.
status: StatusFailed,
remainingAmt: 1000,
paymentFailed: true,
allowMore: false,
expectedErr: nil,
},
{
// With the payment in an unknown state, we don't allow
// more attempts to be made.
status: 0,
remainingAmt: 1000,
allowMore: false,
expectedErr: nil,
},
}
for i, tc := range testCases {
tc := tc
p := &MPPayment{
Info: &PaymentCreationInfo{
PaymentIdentifier: [32]byte{1, 2, 3},
},
Status: tc.status,
State: &MPPaymentState{
RemainingAmt: tc.remainingAmt,
HasSettledHTLC: tc.hasSettledHTLC,
PaymentFailed: tc.paymentFailed,
},
}
name := fmt.Sprintf("test_%d|status=%s|remainingAmt=%v", i,
tc.status, tc.remainingAmt)
t.Run(name, func(t *testing.T) {
t.Parallel()
result, err := p.AllowMoreAttempts()
require.ErrorIs(t, err, tc.expectedErr)
require.Equalf(t, tc.allowMore, result, "status=%v, "+
"remainingAmt=%v", tc.status, tc.remainingAmt)
})
}
}
func makeActiveAttempt(total, fee int) HTLCAttempt { func makeActiveAttempt(total, fee int) HTLCAttempt {
return HTLCAttempt{ return HTLCAttempt{
HTLCAttemptInfo: makeAttemptInfo(total, total-fee), HTLCAttemptInfo: makeAttemptInfo(total, total-fee),
@@ -37,6 +37,9 @@
might panic due to empty witness data found in a transaction. More details might panic due to empty witness data found in a transaction. More details
can be found [here](https://github.com/bitcoin/bitcoin/issues/28730). can be found [here](https://github.com/bitcoin/bitcoin/issues/28730).
* [Fixed a case](https://github.com/lightningnetwork/lnd/pull/7503) where it's
possible a failed payment might be stuck in pending.
# New Features # New Features
## Functional Enhancements ## Functional Enhancements
@@ -58,6 +61,13 @@
[http-header-timeout](https://github.com/lightningnetwork/lnd/pull/7715), is added so users can specify the amount of time the http server will wait for a request to complete before closing the connection. The default value is 5 seconds. [http-header-timeout](https://github.com/lightningnetwork/lnd/pull/7715), is added so users can specify the amount of time the http server will wait for a request to complete before closing the connection. The default value is 5 seconds.
## RPC Additions ## RPC Additions
* [Deprecated](https://github.com/lightningnetwork/lnd/pull/7175)
`StatusUnknown` from the payment's rpc response in its status and added a new
status, `StatusInitiated`, to explicitly report its current state. Before
running this new version, please make sure to upgrade your client application
to include this new status so it can understand the RPC response properly.
## lncli Additions ## lncli Additions
# Improvements # Improvements
@@ -81,6 +91,12 @@
hash](https://github.com/lightningnetwork/lnd/pull/8106) to the hash](https://github.com/lightningnetwork/lnd/pull/8106) to the
signer.SignMessage/signer.VerifyMessage RPCs. signer.SignMessage/signer.VerifyMessage RPCs.
* `sendtoroute` will return an error when it's called using the flag
`--skip_temp_err` on a payment that's not a MPP. This is needed as a temp
error is defined as a routing error found in one of a MPP's HTLC attempts.
If, however, there's only one HTLC attempt, when it's failed, this payment is
considered failed, thus there's no such thing as temp error for a non-MPP.
## lncli Updates ## lncli Updates
## Code Health ## Code Health
@@ -90,6 +106,11 @@
`lnrpc.GetInfoResponse` message along with the `chain` field in the `lnrpc.GetInfoResponse` message along with the `chain` field in the
`lnrpc.Chain` message have also been deprecated for the same reason. `lnrpc.Chain` message have also been deprecated for the same reason.
* The payment lifecycle code has been refactored to improve its maintainablity.
In particular, the complexity involved in the lifecycle loop has been
decoupled into logical steps, with each step having its own responsibility,
making it easier to reason about the payment flow.
## Breaking Changes ## Breaking Changes
## Performance Improvements ## Performance Improvements
+2
View File
@@ -1000,6 +1000,8 @@ func (s *Switch) extractResult(deobfuscator ErrorDecrypter, n *networkResult,
// We've received a fail update which means we can finalize the // We've received a fail update which means we can finalize the
// user payment and return fail response. // user payment and return fail response.
case *lnwire.UpdateFailHTLC: case *lnwire.UpdateFailHTLC:
// TODO(yy): construct deobfuscator here to avoid creating it
// in paymentLifecycle even for settled HTLCs.
paymentErr := s.parseFailedPayment( paymentErr := s.parseFailedPayment(
deobfuscator, attemptID, paymentHash, n.unencrypted, deobfuscator, attemptID, paymentHash, n.unencrypted,
n.isResolution, htlc, n.isResolution, htlc,
+2 -5
View File
@@ -122,8 +122,7 @@ func acceptHoldInvoice(ht *lntest.HarnessTest, idx int, sender,
invoice := receiver.RPC.AddHoldInvoice(req) invoice := receiver.RPC.AddHoldInvoice(req)
invStream := receiver.RPC.SubscribeSingleInvoice(hash[:]) invStream := receiver.RPC.SubscribeSingleInvoice(hash[:])
inv := ht.ReceiveSingleInvoice(invStream) ht.AssertInvoiceState(invStream, lnrpc.Invoice_OPEN)
require.Equal(ht, lnrpc.Invoice_OPEN, inv.State, "expect open")
sendReq := &routerrpc.SendPaymentRequest{ sendReq := &routerrpc.SendPaymentRequest{
PaymentRequest: invoice.PaymentRequest, PaymentRequest: invoice.PaymentRequest,
@@ -145,9 +144,7 @@ func acceptHoldInvoice(ht *lntest.HarnessTest, idx int, sender,
) )
require.Len(ht, payment.Htlcs, 1) require.Len(ht, payment.Htlcs, 1)
inv = ht.ReceiveSingleInvoice(invStream) ht.AssertInvoiceState(invStream, lnrpc.Invoice_ACCEPTED)
require.Equal(ht, lnrpc.Invoice_ACCEPTED, inv.State,
"expected accepted")
return &holdSubscription{ return &holdSubscription{
recipient: receiver, recipient: receiver,
+8 -2
View File
@@ -31,8 +31,14 @@ type dbMPPayment interface {
// InFlightHTLCs returns all HTLCs that are in flight. // InFlightHTLCs returns all HTLCs that are in flight.
InFlightHTLCs() []channeldb.HTLCAttempt InFlightHTLCs() []channeldb.HTLCAttempt
// GetFailureReason returns the reason the payment failed. // AllowMoreAttempts is used to decide whether we can safely attempt
GetFailureReason() *channeldb.FailureReason // more HTLCs for a given payment state. Return an error if the payment
// is in an unexpected state.
AllowMoreAttempts() (bool, error)
// TerminalInfo returns the settled HTLC attempt or the payment's
// failure reason.
TerminalInfo() (*channeldb.HTLCAttempt, *channeldb.FailureReason)
} }
// ControlTower tracks all outgoing payments made, whose primary purpose is to // ControlTower tracks all outgoing payments made, whose primary purpose is to
+6 -8
View File
@@ -134,8 +134,8 @@ func TestControlTowerSubscribeSuccess(t *testing.T) {
"subscriber %v failed, want %s, got %s", i, "subscriber %v failed, want %s, got %s", i,
channeldb.StatusSucceeded, result.GetStatus()) channeldb.StatusSucceeded, result.GetStatus())
settle, _ := result.TerminalInfo() attempt, _ := result.TerminalInfo()
if settle.Preimage != preimg { if attempt.Settle.Preimage != preimg {
t.Fatal("unexpected preimage") t.Fatal("unexpected preimage")
} }
if len(result.HTLCs) != 1 { if len(result.HTLCs) != 1 {
@@ -264,9 +264,8 @@ func TestPaymentControlSubscribeAllSuccess(t *testing.T) {
) )
settle1, _ := result1.TerminalInfo() settle1, _ := result1.TerminalInfo()
require.Equal( require.Equal(t, preimg1, settle1.Settle.Preimage,
t, preimg1, settle1.Preimage, "unexpected preimage payment 1", "unexpected preimage payment 1")
)
require.Len( require.Len(
t, result1.HTLCs, 1, "expect 1 htlc for payment 1, got %d", t, result1.HTLCs, 1, "expect 1 htlc for payment 1, got %d",
@@ -283,9 +282,8 @@ func TestPaymentControlSubscribeAllSuccess(t *testing.T) {
) )
settle2, _ := result2.TerminalInfo() settle2, _ := result2.TerminalInfo()
require.Equal( require.Equal(t, preimg2, settle2.Settle.Preimage,
t, preimg2, settle2.Preimage, "unexpected preimage payment 2", "unexpected preimage payment 2")
)
require.Len( require.Len(
t, result2.HTLCs, 1, "expect 1 htlc for payment 2, got %d", t, result2.HTLCs, 1, "expect 1 htlc for payment 2, got %d",
len(result2.HTLCs), len(result2.HTLCs),
+117 -25
View File
@@ -12,7 +12,9 @@ import (
"github.com/lightningnetwork/lnd/htlcswitch" "github.com/lightningnetwork/lnd/htlcswitch"
"github.com/lightningnetwork/lnd/lntypes" "github.com/lightningnetwork/lnd/lntypes"
"github.com/lightningnetwork/lnd/lnwire" "github.com/lightningnetwork/lnd/lnwire"
"github.com/lightningnetwork/lnd/record"
"github.com/lightningnetwork/lnd/routing/route" "github.com/lightningnetwork/lnd/routing/route"
"github.com/lightningnetwork/lnd/routing/shards"
"github.com/stretchr/testify/mock" "github.com/stretchr/testify/mock"
) )
@@ -572,8 +574,6 @@ func (m *mockControlTowerOld) SubscribeAllPayments() (
type mockPaymentAttemptDispatcher struct { type mockPaymentAttemptDispatcher struct {
mock.Mock mock.Mock
resultChan chan *htlcswitch.PaymentResult
} }
var _ PaymentAttemptDispatcher = (*mockPaymentAttemptDispatcher)(nil) var _ PaymentAttemptDispatcher = (*mockPaymentAttemptDispatcher)(nil)
@@ -589,11 +589,14 @@ func (m *mockPaymentAttemptDispatcher) GetAttemptResult(attemptID uint64,
paymentHash lntypes.Hash, deobfuscator htlcswitch.ErrorDecrypter) ( paymentHash lntypes.Hash, deobfuscator htlcswitch.ErrorDecrypter) (
<-chan *htlcswitch.PaymentResult, error) { <-chan *htlcswitch.PaymentResult, error) {
m.Called(attemptID, paymentHash, deobfuscator) args := m.Called(attemptID, paymentHash, deobfuscator)
// Instead of returning the mocked returned values, we need to return resultChan := args.Get(0)
// the chan resultChan so it can be converted into a read-only chan. if resultChan == nil {
return m.resultChan, nil return nil, args.Error(1)
}
return args.Get(0).(chan *htlcswitch.PaymentResult), args.Error(1)
} }
func (m *mockPaymentAttemptDispatcher) CleanStore( func (m *mockPaymentAttemptDispatcher) CleanStore(
@@ -673,6 +676,12 @@ func (m *mockPaymentSession) RequestRoute(maxAmt, feeLimit lnwire.MilliSatoshi,
activeShards, height uint32) (*route.Route, error) { activeShards, height uint32) (*route.Route, error) {
args := m.Called(maxAmt, feeLimit, activeShards, height) args := m.Called(maxAmt, feeLimit, activeShards, height)
// Type assertion on nil will fail, so we check and return here.
if args.Get(0) == nil {
return nil, args.Error(1)
}
return args.Get(0).(*route.Route), args.Error(1) return args.Get(0).(*route.Route), args.Error(1)
} }
@@ -692,7 +701,6 @@ func (m *mockPaymentSession) GetAdditionalEdgePolicy(pubKey *btcec.PublicKey,
type mockControlTower struct { type mockControlTower struct {
mock.Mock mock.Mock
sync.Mutex
} }
var _ ControlTower = (*mockControlTower)(nil) var _ ControlTower = (*mockControlTower)(nil)
@@ -712,9 +720,6 @@ func (m *mockControlTower) DeleteFailedAttempts(phash lntypes.Hash) error {
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()
args := m.Called(phash, a) args := m.Called(phash, a)
return args.Error(0) return args.Error(0)
} }
@@ -723,29 +728,32 @@ 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()
args := m.Called(phash, pid, settleInfo) args := m.Called(phash, pid, settleInfo)
return args.Get(0).(*channeldb.HTLCAttempt), args.Error(1)
attempt := args.Get(0)
if attempt == nil {
return nil, args.Error(1)
}
return attempt.(*channeldb.HTLCAttempt), args.Error(1)
} }
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()
args := m.Called(phash, pid, failInfo) args := m.Called(phash, pid, failInfo)
attempt := args.Get(0)
if attempt == nil {
return nil, args.Error(1)
}
return args.Get(0).(*channeldb.HTLCAttempt), args.Error(1) return args.Get(0).(*channeldb.HTLCAttempt), args.Error(1)
} }
func (m *mockControlTower) FailPayment(phash lntypes.Hash, func (m *mockControlTower) FailPayment(phash lntypes.Hash,
reason channeldb.FailureReason) error { reason channeldb.FailureReason) error {
m.Lock()
defer m.Unlock()
args := m.Called(phash, reason) args := m.Called(phash, reason)
return args.Error(0) return args.Error(0)
} }
@@ -822,15 +830,32 @@ func (m *mockMPPayment) InFlightHTLCs() []channeldb.HTLCAttempt {
return args.Get(0).([]channeldb.HTLCAttempt) return args.Get(0).([]channeldb.HTLCAttempt)
} }
func (m *mockMPPayment) GetFailureReason() *channeldb.FailureReason { func (m *mockMPPayment) AllowMoreAttempts() (bool, error) {
args := m.Called()
return args.Bool(0), args.Error(1)
}
func (m *mockMPPayment) TerminalInfo() (*channeldb.HTLCAttempt,
*channeldb.FailureReason) {
args := m.Called() args := m.Called()
reason := args.Get(0) var (
if reason == nil { settleInfo *channeldb.HTLCAttempt
return nil failureInfo *channeldb.FailureReason
)
settle := args.Get(0)
if settle != nil {
settleInfo = settle.(*channeldb.HTLCAttempt)
} }
return reason.(*channeldb.FailureReason) reason := args.Get(1)
if reason != nil {
failureInfo = reason.(*channeldb.FailureReason)
}
return settleInfo, failureInfo
} }
type mockLink struct { type mockLink struct {
@@ -854,3 +879,70 @@ func (m *mockLink) EligibleToForward() bool {
func (m *mockLink) MayAddOutgoingHtlc(_ lnwire.MilliSatoshi) error { func (m *mockLink) MayAddOutgoingHtlc(_ lnwire.MilliSatoshi) error {
return m.mayAddOutgoingErr return m.mayAddOutgoingErr
} }
type mockShardTracker struct {
mock.Mock
}
var _ shards.ShardTracker = (*mockShardTracker)(nil)
func (m *mockShardTracker) NewShard(attemptID uint64,
lastShard bool) (shards.PaymentShard, error) {
args := m.Called(attemptID, lastShard)
shard := args.Get(0)
if shard == nil {
return nil, args.Error(1)
}
return shard.(shards.PaymentShard), args.Error(1)
}
func (m *mockShardTracker) GetHash(attemptID uint64) (lntypes.Hash, error) {
args := m.Called(attemptID)
return args.Get(0).(lntypes.Hash), args.Error(1)
}
func (m *mockShardTracker) CancelShard(attemptID uint64) error {
args := m.Called(attemptID)
return args.Error(0)
}
type mockShard struct {
mock.Mock
}
var _ shards.PaymentShard = (*mockShard)(nil)
// Hash returns the hash used for the HTLC representing this shard.
func (m *mockShard) Hash() lntypes.Hash {
args := m.Called()
return args.Get(0).(lntypes.Hash)
}
// MPP returns any extra MPP records that should be set for the final
// hop on the route used by this shard.
func (m *mockShard) MPP() *record.MPP {
args := m.Called()
r := args.Get(0)
if r == nil {
return nil
}
return r.(*record.MPP)
}
// AMP returns any extra AMP records that should be set for the final
// hop on the route used by this shard.
func (m *mockShard) AMP() *record.AMP {
args := m.Called()
r := args.Get(0)
if r == nil {
return nil
}
return r.(*record.AMP)
}
File diff suppressed because it is too large. Load diff
File diff suppressed because it is too large. Load diff
+77 -63
View File
@@ -2,7 +2,6 @@ package routing
import ( import (
"bytes" "bytes"
goErrors "errors"
"fmt" "fmt"
"math" "math"
"runtime" "runtime"
@@ -120,6 +119,10 @@ var (
// provided by either a blinded route or a cleartext pubkey. // provided by either a blinded route or a cleartext pubkey.
ErrNoTarget = errors.New("destination not set in target or blinded " + ErrNoTarget = errors.New("destination not set in target or blinded " +
"path") "path")
// ErrSkipTempErr is returned when a non-MPP is made yet the
// skipTempErr flag is set.
ErrSkipTempErr = errors.New("cannot skip temp error for non-MPP")
) )
// ChannelGraphSource represents the source of information about the topology // ChannelGraphSource represents the source of information about the topology
@@ -2451,6 +2454,13 @@ func (r *ChannelRouter) sendToRoute(htlcHash lntypes.Hash, rt *route.Route,
amt = mpp.TotalMsat() amt = mpp.TotalMsat()
} }
// For non-MPP, there's no such thing as temp error as there's only one
// HTLC attempt being made. When this HTLC is failed, the payment is
// failed hence cannot be retried.
if skipTempErr && mpp == nil {
return nil, ErrSkipTempErr
}
// For non-AMP payments the overall payment identifier will be the same // For non-AMP payments the overall payment identifier will be the same
// hash as used for this HTLC. // hash as used for this HTLC.
paymentIdentifier := htlcHash paymentIdentifier := htlcHash
@@ -2496,47 +2506,73 @@ func (r *ChannelRouter) sendToRoute(htlcHash lntypes.Hash, rt *route.Route,
// shard we'll now launch. // shard we'll now launch.
shardTracker := shards.NewSimpleShardTracker(htlcHash, nil) shardTracker := shards.NewSimpleShardTracker(htlcHash, nil)
// Launch a shard along the given route. // Create a payment lifecycle using the given route with,
sh := &shardHandler{ // - zero fee limit as we are not requesting routes.
router: r, // - nil payment session (since we already have a route).
identifier: paymentIdentifier, // - no payment timeout.
shardTracker: shardTracker, // - no current block height.
} p := newPaymentLifecycle(
r, 0, paymentIdentifier, nil, shardTracker, 0, 0,
var shardError error
attempt, outcome, err := sh.launchShard(rt, false)
// With SendToRoute, it can happen that the route exceeds protocol
// constraints. Mark the payment as failed with an internal error.
if err == route.ErrMaxRouteHopsExceeded ||
err == sphinx.ErrMaxRoutingInfoSizeExceeded {
log.Debugf("Invalid route provided for payment %x: %v",
paymentIdentifier, err)
controlErr := r.cfg.Control.FailPayment(
paymentIdentifier, channeldb.FailureReasonError,
) )
if controlErr != nil {
return nil, controlErr
}
}
// In any case, don't continue if there is an error. // We found a route to try, create a new HTLC attempt to try.
//
// NOTE: we use zero `remainingAmt` here to simulate the same effect of
// setting the lastShard to be false, which is used by previous
// implementation.
attempt, err := p.registerAttempt(rt, 0)
if err != nil { if err != nil {
return nil, err return nil, err
} }
var htlcAttempt *channeldb.HTLCAttempt // Once the attempt is created, send it to the htlcswitch. Notice that
switch { // the `err` returned here has already been processed by
// Failed to launch shard. // `handleSwitchErr`, which means if there's a terminal failure, the
case outcome.err != nil: // payment has been failed.
shardError = outcome.err result, err := p.sendAttempt(attempt)
htlcAttempt = outcome.attempt if err != nil {
return nil, err
}
// Shard successfully launched, wait for the result to be available. // We now lookup the payment to see if it's already failed.
default: payment, err := p.router.cfg.Control.FetchPayment(p.identifier)
result, err := sh.collectResult(attempt) if err != nil {
return result.attempt, err
}
// Exit if the above error has caused the payment to be failed, we also
// return the error from sending attempt to mimic the old behavior of
// this method.
_, failedReason := payment.TerminalInfo()
if failedReason != nil {
return result.attempt, result.err
}
// Since for SendToRoute we won't retry in case the shard fails, we'll
// mark the payment failed with the control tower immediately if the
// skipTempErr is false.
reason := channeldb.FailureReasonError
// If we failed to send the HTLC, we need to further decide if we want
// to fail the payment.
if result.err != nil {
// If skipTempErr, we'll return the attempt and the temp error.
if skipTempErr {
return result.attempt, result.err
}
// Otherwise we need to fail the payment.
err := r.cfg.Control.FailPayment(paymentIdentifier, reason)
if err != nil {
return nil, err
}
return result.attempt, result.err
}
// The attempt was successfully sent, wait for the result to be
// available.
result, err = p.collectResult(attempt)
if err != nil { if err != nil {
return nil, err return nil, err
} }
@@ -2546,38 +2582,16 @@ func (r *ChannelRouter) sendToRoute(htlcHash lntypes.Hash, rt *route.Route,
return result.attempt, nil return result.attempt, nil
} }
// The shard failed, break switch to handle it. // An error returned from collecting the result, we'll mark the payment
shardError = result.err // as failed if we don't skip temp error.
htlcAttempt = result.attempt if !skipTempErr {
} err := r.cfg.Control.FailPayment(paymentIdentifier, reason)
// Since for SendToRoute we won't retry in case the shard fails, we'll
// 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.
var failureReason *channeldb.FailureReason
err = sh.handleSwitchErr(attempt, shardError)
switch {
// If a non-terminal error is returned and `skipTempErr` is false, then
// we'll use the normal no route error.
case err == nil && !skipTempErr:
err = r.cfg.Control.FailPayment(
paymentIdentifier, channeldb.FailureReasonNoRoute,
)
// If this is a failure reason, then we'll apply the failure directly
// to the control tower, and return the normal response to the caller.
case goErrors.As(err, &failureReason):
err = r.cfg.Control.FailPayment(
paymentIdentifier, *failureReason,
)
}
if err != nil { if err != nil {
return nil, err return nil, err
} }
}
return htlcAttempt, shardError return result.attempt, result.err
} }
// sendPayment attempts to send a payment to the passed payment hash. This // sendPayment attempts to send a payment to the passed payment hash. This
+183 -949
View File
File diff suppressed because it is too large. Load diff