Merge pull request #8734 from hieblmi/cancel-estimateroutefee

routing: cancelable payment loop
This commit is contained in:
Yong authored and GitHub committed 2024-06-17 18:06:52 +08:00
commit e6f7a2d6a7
9 files changed
+316 -145

No files matched your search

+15
View File
@@ -139,6 +139,17 @@ var (
Usage: "(blinded paths) the total cltv delay for the " + Usage: "(blinded paths) the total cltv delay for the " +
"blinded portion of the route", "blinded portion of the route",
} }
cancelableFlag = cli.BoolFlag{
Name: "cancelable",
Usage: "if set to true, the payment loop can be interrupted " +
"by manually canceling the payment context, even " +
"before the payment timeout is reached. Note that " +
"the payment may still succeed after cancellation, " +
"as in-flight attempts can still settle afterwards. " +
"Canceling will only prevent further attempts from " +
"being sent",
}
) )
// paymentFlags returns common flags for sendpayment and payinvoice. // paymentFlags returns common flags for sendpayment and payinvoice.
@@ -166,6 +177,7 @@ func paymentFlags() []cli.Flag {
"after the timeout has elapsed", "after the timeout has elapsed",
Value: paymentTimeout, Value: paymentTimeout,
}, },
cancelableFlag,
cltvLimitFlag, cltvLimitFlag,
lastHopFlag, lastHopFlag,
cli.Int64SliceFlag{ cli.Int64SliceFlag{
@@ -329,6 +341,7 @@ func sendPayment(ctx *cli.Context) error {
Amt: ctx.Int64("amt"), Amt: ctx.Int64("amt"),
DestCustomRecords: make(map[uint64][]byte), DestCustomRecords: make(map[uint64][]byte),
Amp: ctx.Bool(ampFlag.Name), Amp: ctx.Bool(ampFlag.Name),
Cancelable: ctx.Bool(cancelableFlag.Name),
} }
// We'll attempt to parse a payment address as well, given that // We'll attempt to parse a payment address as well, given that
@@ -387,6 +400,7 @@ func sendPayment(ctx *cli.Context) error {
Amt: amount, Amt: amount,
DestCustomRecords: make(map[uint64][]byte), DestCustomRecords: make(map[uint64][]byte),
Amp: ctx.Bool(ampFlag.Name), Amp: ctx.Bool(ampFlag.Name),
Cancelable: ctx.Bool(cancelableFlag.Name),
} }
var rHash []byte var rHash []byte
@@ -888,6 +902,7 @@ func payInvoice(ctx *cli.Context) error {
Amt: ctx.Int64("amt"), Amt: ctx.Int64("amt"),
DestCustomRecords: make(map[uint64][]byte), DestCustomRecords: make(map[uint64][]byte),
Amp: ctx.Bool(ampFlag.Name), Amp: ctx.Bool(ampFlag.Name),
Cancelable: ctx.Bool(cancelableFlag.Name),
} }
return sendPaymentRequest(ctx, req) return sendPaymentRequest(ctx, req)
+12 -1
View File
@@ -23,14 +23,24 @@
settled balances even if the delivery address is set to an address that settled balances even if the delivery address is set to an address that
LND does not control. LND does not control.
* [SendPaymentV2](https://github.com/lightningnetwork/lnd/pull/8734) now cancels
the background payment loop if the user cancels the stream context.
# New Features # New Features
## Functional Enhancements ## Functional Enhancements
## RPC Additions ## RPC Additions
* The [SendPaymentRequest](https://github.com/lightningnetwork/lnd/pull/8734)
message receives a new flag `cancelable` which indicates if the payment loop
is cancelable. The cancellation can either occur manually by cancelling the
send payment stream context, or automatically at the end of the timeout period
if the user provided `timeout_seconds`.
## lncli Additions ## lncli Additions
* [Added](https://github.com/lightningnetwork/lnd/pull/8491) the `cltv_expiry` * [Added](https://github.com/lightningnetwork/lnd/pull/8491) the `cltv_expiry`
argument to `addinvoice` and `addholdinvoice`, allowing users to set the argument to `addinvoice` and `addholdinvoice`, allowing users to set the
`min_final_cltv_expiry_delta` `min_final_cltv_expiry_delta`.
* The [`lncli wallet estimatefeerate`](https://github.com/lightningnetwork/lnd/pull/8730) * The [`lncli wallet estimatefeerate`](https://github.com/lightningnetwork/lnd/pull/8730)
command returns the fee rate estimate for on-chain transactions in sat/kw and command returns the fee rate estimate for on-chain transactions in sat/kw and
@@ -72,3 +82,4 @@
* Andras Banki-Horvath * Andras Banki-Horvath
* Bufo * Bufo
* Slyghtning
+17 -2
View File
@@ -492,6 +492,12 @@ type SendPaymentRequest struct {
// The time preference for this payment. Set to -1 to optimize for fees // The time preference for this payment. Set to -1 to optimize for fees
// only, to 1 to optimize for reliability only or a value inbetween for a mix. // only, to 1 to optimize for reliability only or a value inbetween for a mix.
TimePref float64 `protobuf:"fixed64,23,opt,name=time_pref,json=timePref,proto3" json:"time_pref,omitempty"` TimePref float64 `protobuf:"fixed64,23,opt,name=time_pref,json=timePref,proto3" json:"time_pref,omitempty"`
// If set, the payment loop can be interrupted by manually canceling the
// payment context, even before the payment timeout is reached. Note that the
// payment may still succeed after cancellation, as in-flight attempts can
// still settle afterwards. Canceling will only prevent further attempts from
// being sent.
Cancelable bool `protobuf:"varint,24,opt,name=cancelable,proto3" json:"cancelable,omitempty"`
} }
func (x *SendPaymentRequest) Reset() { func (x *SendPaymentRequest) Reset() {
@@ -688,6 +694,13 @@ func (x *SendPaymentRequest) GetTimePref() float64 {
return 0 return 0
} }
func (x *SendPaymentRequest) GetCancelable() bool {
if x != nil {
return x.Cancelable
}
return false
}
type TrackPaymentRequest struct { type TrackPaymentRequest struct {
state protoimpl.MessageState state protoimpl.MessageState
sizeCache protoimpl.SizeCache sizeCache protoimpl.SizeCache
@@ -3339,7 +3352,7 @@ var file_routerrpc_router_proto_rawDesc = []byte{
0x0a, 0x16, 0x72, 0x6f, 0x75, 0x74, 0x65, 0x72, 0x72, 0x70, 0x63, 0x2f, 0x72, 0x6f, 0x75, 0x74, 0x0a, 0x16, 0x72, 0x6f, 0x75, 0x74, 0x65, 0x72, 0x72, 0x70, 0x63, 0x2f, 0x72, 0x6f, 0x75, 0x74,
0x65, 0x72, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x12, 0x09, 0x72, 0x6f, 0x75, 0x74, 0x65, 0x72, 0x65, 0x72, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x12, 0x09, 0x72, 0x6f, 0x75, 0x74, 0x65, 0x72,
0x72, 0x70, 0x63, 0x1a, 0x0f, 0x6c, 0x69, 0x67, 0x68, 0x74, 0x6e, 0x69, 0x6e, 0x67, 0x2e, 0x70, 0x72, 0x70, 0x63, 0x1a, 0x0f, 0x6c, 0x69, 0x67, 0x68, 0x74, 0x6e, 0x69, 0x6e, 0x67, 0x2e, 0x70,
0x72, 0x6f, 0x74, 0x6f, 0x22, 0xf4, 0x07, 0x0a, 0x12, 0x53, 0x65, 0x6e, 0x64, 0x50, 0x61, 0x79, 0x72, 0x6f, 0x74, 0x6f, 0x22, 0x94, 0x08, 0x0a, 0x12, 0x53, 0x65, 0x6e, 0x64, 0x50, 0x61, 0x79,
0x6d, 0x65, 0x6e, 0x74, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x12, 0x12, 0x0a, 0x04, 0x64, 0x6d, 0x65, 0x6e, 0x74, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x12, 0x12, 0x0a, 0x04, 0x64,
0x65, 0x73, 0x74, 0x18, 0x01, 0x20, 0x01, 0x28, 0x0c, 0x52, 0x04, 0x64, 0x65, 0x73, 0x74, 0x12, 0x65, 0x73, 0x74, 0x18, 0x01, 0x20, 0x01, 0x28, 0x0c, 0x52, 0x04, 0x64, 0x65, 0x73, 0x74, 0x12,
0x10, 0x0a, 0x03, 0x61, 0x6d, 0x74, 0x18, 0x02, 0x20, 0x01, 0x28, 0x03, 0x52, 0x03, 0x61, 0x6d, 0x10, 0x0a, 0x03, 0x61, 0x6d, 0x74, 0x18, 0x02, 0x20, 0x01, 0x28, 0x03, 0x52, 0x03, 0x61, 0x6d,
@@ -3398,7 +3411,9 @@ var file_routerrpc_router_proto_rawDesc = []byte{
0x7a, 0x65, 0x4d, 0x73, 0x61, 0x74, 0x12, 0x10, 0x0a, 0x03, 0x61, 0x6d, 0x70, 0x18, 0x16, 0x20, 0x7a, 0x65, 0x4d, 0x73, 0x61, 0x74, 0x12, 0x10, 0x0a, 0x03, 0x61, 0x6d, 0x70, 0x18, 0x16, 0x20,
0x01, 0x28, 0x08, 0x52, 0x03, 0x61, 0x6d, 0x70, 0x12, 0x1b, 0x0a, 0x09, 0x74, 0x69, 0x6d, 0x65, 0x01, 0x28, 0x08, 0x52, 0x03, 0x61, 0x6d, 0x70, 0x12, 0x1b, 0x0a, 0x09, 0x74, 0x69, 0x6d, 0x65,
0x5f, 0x70, 0x72, 0x65, 0x66, 0x18, 0x17, 0x20, 0x01, 0x28, 0x01, 0x52, 0x08, 0x74, 0x69, 0x6d, 0x5f, 0x70, 0x72, 0x65, 0x66, 0x18, 0x17, 0x20, 0x01, 0x28, 0x01, 0x52, 0x08, 0x74, 0x69, 0x6d,
0x65, 0x50, 0x72, 0x65, 0x66, 0x1a, 0x44, 0x0a, 0x16, 0x44, 0x65, 0x73, 0x74, 0x43, 0x75, 0x73, 0x65, 0x50, 0x72, 0x65, 0x66, 0x12, 0x1e, 0x0a, 0x0a, 0x63, 0x61, 0x6e, 0x63, 0x65, 0x6c, 0x61,
0x62, 0x6c, 0x65, 0x18, 0x18, 0x20, 0x01, 0x28, 0x08, 0x52, 0x0a, 0x63, 0x61, 0x6e, 0x63, 0x65,
0x6c, 0x61, 0x62, 0x6c, 0x65, 0x1a, 0x44, 0x0a, 0x16, 0x44, 0x65, 0x73, 0x74, 0x43, 0x75, 0x73,
0x74, 0x6f, 0x6d, 0x52, 0x65, 0x63, 0x6f, 0x72, 0x64, 0x73, 0x45, 0x6e, 0x74, 0x72, 0x79, 0x12, 0x74, 0x6f, 0x6d, 0x52, 0x65, 0x63, 0x6f, 0x72, 0x64, 0x73, 0x45, 0x6e, 0x74, 0x72, 0x79, 0x12,
0x10, 0x0a, 0x03, 0x6b, 0x65, 0x79, 0x18, 0x01, 0x20, 0x01, 0x28, 0x04, 0x52, 0x03, 0x6b, 0x65, 0x10, 0x0a, 0x03, 0x6b, 0x65, 0x79, 0x18, 0x01, 0x20, 0x01, 0x28, 0x04, 0x52, 0x03, 0x6b, 0x65,
0x79, 0x12, 0x14, 0x0a, 0x05, 0x76, 0x61, 0x6c, 0x75, 0x65, 0x18, 0x02, 0x20, 0x01, 0x28, 0x0c, 0x79, 0x12, 0x14, 0x0a, 0x05, 0x76, 0x61, 0x6c, 0x75, 0x65, 0x18, 0x02, 0x20, 0x01, 0x28, 0x0c,
+9
View File
@@ -330,6 +330,15 @@ message SendPaymentRequest {
only, to 1 to optimize for reliability only or a value inbetween for a mix. only, to 1 to optimize for reliability only or a value inbetween for a mix.
*/ */
double time_pref = 23; double time_pref = 23;
/*
If set, the payment loop can be interrupted by manually canceling the
payment context, even before the payment timeout is reached. Note that the
payment may still succeed after cancellation, as in-flight attempts can
still settle afterwards. Canceling will only prevent further attempts from
being sent.
*/
bool cancelable = 24;
} }
message TrackPaymentRequest { message TrackPaymentRequest {
+4
View File
@@ -1804,6 +1804,10 @@
"type": "number", "type": "number",
"format": "double", "format": "double",
"description": "The time preference for this payment. Set to -1 to optimize for fees\nonly, to 1 to optimize for reliability only or a value inbetween for a mix." "description": "The time preference for this payment. Set to -1 to optimize for fees\nonly, to 1 to optimize for reliability only or a value inbetween for a mix."
},
"cancelable": {
"type": "boolean",
"description": "If set, the payment loop can be interrupted by manually canceling the\npayment context, even before the payment timeout is reached. Note that the\npayment may still succeed after cancellation, as in-flight attempts can\nstill settle afterwards. Canceling will only prevent further attempts from\nbeing sent."
} }
} }
}, },
+29 -17
View File
@@ -149,14 +149,14 @@ var (
DefaultRouterMacFilename = "router.macaroon" DefaultRouterMacFilename = "router.macaroon"
) )
// ServerShell a is shell struct holding a reference to the actual sub-server. // ServerShell is a shell struct holding a reference to the actual sub-server.
// It is used to register the gRPC sub-server with the root server before we // It is used to register the gRPC sub-server with the root server before we
// have the necessary dependencies to populate the actual sub-server. // have the necessary dependencies to populate the actual sub-server.
type ServerShell struct { type ServerShell struct {
RouterServer RouterServer
} }
// Server is a stand alone sub RPC server which exposes functionality that // Server is a stand-alone sub RPC server which exposes functionality that
// allows clients to route arbitrary payment through the Lightning Network. // allows clients to route arbitrary payment through the Lightning Network.
type Server struct { type Server struct {
started int32 // To be used atomically. started int32 // To be used atomically.
@@ -181,7 +181,7 @@ var _ RouterServer = (*Server)(nil)
// that contains all external dependencies. If the target macaroon exists, and // that contains all external dependencies. If the target macaroon exists, and
// we're unable to create it, then an error will be returned. We also return // we're unable to create it, then an error will be returned. We also return
// the set of permissions that we require as a server. At the time of writing // the set of permissions that we require as a server. At the time of writing
// of this documentation, this is the same macaroon as as the admin macaroon. // of this documentation, this is the same macaroon as the admin macaroon.
func New(cfg *Config) (*Server, lnrpc.MacaroonPerms, error) { func New(cfg *Config) (*Server, lnrpc.MacaroonPerms, error) {
// If the path of the router macaroon wasn't generated, then we'll // If the path of the router macaroon wasn't generated, then we'll
// assume that it's found at the default network directory. // assume that it's found at the default network directory.
@@ -360,13 +360,25 @@ func (s *Server) SendPaymentV2(req *SendPaymentRequest,
return err return err
} }
// The payment context is influenced by two user-provided parameters,
// the cancelable flag and the payment attempt timeout.
// If the payment is cancelable, we will use the stream context as the
// payment context. That way, if the user ends the stream, the payment
// loop will be canceled.
// The second context parameter is the timeout. If the user provides a
// timeout, we will additionally wrap the context in a deadline. If the
// user provided 'cancelable' and ends the stream before the timeout is
// reached the payment will be canceled.
ctx := context.Background()
if req.Cancelable {
ctx = stream.Context()
}
// Send the payment asynchronously. // Send the payment asynchronously.
s.cfg.Router.SendPaymentAsync(payment, paySession, shardTracker) s.cfg.Router.SendPaymentAsync(ctx, payment, paySession, shardTracker)
// Track the payment and return. // Track the payment and return.
return s.trackPayment( return s.trackPayment(sub, payHash, stream, req.NoInflightUpdates)
sub, payHash, stream, req.NoInflightUpdates,
)
} }
// EstimateRouteFee allows callers to obtain an expected value w.r.t how much it // EstimateRouteFee allows callers to obtain an expected value w.r.t how much it
@@ -987,8 +999,7 @@ func (s *Server) SetMissionControlConfig(ctx context.Context,
req.Config.HopProbability, req.Config.HopProbability,
), ),
AprioriWeight: float64(req.Config.Weight), AprioriWeight: float64(req.Config.Weight),
CapacityFraction: float64( CapacityFraction: routing.DefaultCapacityFraction, //nolint:lll
routing.DefaultCapacityFraction),
} }
} }
@@ -1032,8 +1043,8 @@ func (s *Server) SetMissionControlConfig(ctx context.Context,
// QueryMissionControl exposes the internal mission control state to callers. It // QueryMissionControl exposes the internal mission control state to callers. It
// is a development feature. // is a development feature.
func (s *Server) QueryMissionControl(ctx context.Context, func (s *Server) QueryMissionControl(_ context.Context,
req *QueryMissionControlRequest) (*QueryMissionControlResponse, error) { _ *QueryMissionControlRequest) (*QueryMissionControlResponse, error) {
snapshot := s.cfg.RouterBackend.MissionControl.GetHistorySnapshot() snapshot := s.cfg.RouterBackend.MissionControl.GetHistorySnapshot()
@@ -1080,7 +1091,7 @@ func toRPCPairData(data *routing.TimedPairResult) *PairData {
// XImportMissionControl imports the state provided to our internal mission // XImportMissionControl imports the state provided to our internal mission
// control. Only entries that are fresher than our existing state will be used. // control. Only entries that are fresher than our existing state will be used.
func (s *Server) XImportMissionControl(ctx context.Context, func (s *Server) XImportMissionControl(_ context.Context,
req *XImportMissionControlRequest) (*XImportMissionControlResponse, req *XImportMissionControlRequest) (*XImportMissionControlResponse,
error) { error) {
@@ -1273,8 +1284,9 @@ func (s *Server) subscribePayment(identifier lntypes.Hash) (
sub, err := router.Tower.SubscribePayment(identifier) sub, err := router.Tower.SubscribePayment(identifier)
switch { switch {
case err == channeldb.ErrPaymentNotInitiated: case errors.Is(err, channeldb.ErrPaymentNotInitiated):
return nil, status.Error(codes.NotFound, err.Error()) return nil, status.Error(codes.NotFound, err.Error())
case err != nil: case err != nil:
return nil, err return nil, err
} }
@@ -1385,7 +1397,7 @@ func (s *Server) trackPaymentStream(context context.Context,
} }
// BuildRoute builds a route from a list of hop addresses. // BuildRoute builds a route from a list of hop addresses.
func (s *Server) BuildRoute(ctx context.Context, func (s *Server) BuildRoute(_ context.Context,
req *BuildRouteRequest) (*BuildRouteResponse, error) { req *BuildRouteRequest) (*BuildRouteResponse, error) {
// Unmarshall hop list. // Unmarshall hop list.
@@ -1446,7 +1458,7 @@ func (s *Server) BuildRoute(ctx context.Context,
// SubscribeHtlcEvents creates a uni-directional stream from the server to // SubscribeHtlcEvents creates a uni-directional stream from the server to
// the client which delivers a stream of htlc events. // the client which delivers a stream of htlc events.
func (s *Server) SubscribeHtlcEvents(req *SubscribeHtlcEventsRequest, func (s *Server) SubscribeHtlcEvents(_ *SubscribeHtlcEventsRequest,
stream Router_SubscribeHtlcEventsServer) error { stream Router_SubscribeHtlcEventsServer) error {
htlcClient, err := s.cfg.RouterBackend.SubscribeHtlcEvents() htlcClient, err := s.cfg.RouterBackend.SubscribeHtlcEvents()
@@ -1495,7 +1507,7 @@ func (s *Server) SubscribeHtlcEvents(req *SubscribeHtlcEventsRequest,
// HtlcInterceptor is a bidirectional stream for streaming interception // HtlcInterceptor is a bidirectional stream for streaming interception
// requests to the caller. // requests to the caller.
// Upon connection it does the following: // Upon connection, it does the following:
// 1. Check if there is already a live stream, if yes it rejects the request. // 1. Check if there is already a live stream, if yes it rejects the request.
// 2. Registered a ForwardInterceptor // 2. Registered a ForwardInterceptor
// 3. Delivers to the caller every √√ and detect his answer. // 3. Delivers to the caller every √√ and detect his answer.
@@ -1525,7 +1537,7 @@ func extractOutPoint(req *UpdateChanStatusRequest) (*wire.OutPoint, error) {
} }
// UpdateChanStatus allows channel state to be set manually. // UpdateChanStatus allows channel state to be set manually.
func (s *Server) UpdateChanStatus(ctx context.Context, func (s *Server) UpdateChanStatus(_ context.Context,
req *UpdateChanStatusRequest) (*UpdateChanStatusResponse, error) { req *UpdateChanStatusRequest) (*UpdateChanStatusResponse, error) {
outPoint, err := extractOutPoint(req) outPoint, err := extractOutPoint(req)
+48 -39
View File
@@ -1,6 +1,7 @@
package routing package routing
import ( import (
"context"
"errors" "errors"
"fmt" "fmt"
"time" "time"
@@ -29,7 +30,6 @@ type paymentLifecycle struct {
identifier lntypes.Hash identifier lntypes.Hash
paySession PaymentSession paySession PaymentSession
shardTracker shards.ShardTracker shardTracker shards.ShardTracker
timeoutChan <-chan time.Time
currentHeight int32 currentHeight int32
// quit is closed to signal the sub goroutines of the payment lifecycle // quit is closed to signal the sub goroutines of the payment lifecycle
@@ -52,7 +52,7 @@ type paymentLifecycle struct {
// newPaymentLifecycle initiates a new payment lifecycle and returns it. // newPaymentLifecycle initiates a new payment lifecycle and returns it.
func newPaymentLifecycle(r *ChannelRouter, feeLimit lnwire.MilliSatoshi, func newPaymentLifecycle(r *ChannelRouter, feeLimit lnwire.MilliSatoshi,
identifier lntypes.Hash, paySession PaymentSession, identifier lntypes.Hash, paySession PaymentSession,
shardTracker shards.ShardTracker, timeout time.Duration, shardTracker shards.ShardTracker,
currentHeight int32) *paymentLifecycle { currentHeight int32) *paymentLifecycle {
p := &paymentLifecycle{ p := &paymentLifecycle{
@@ -69,13 +69,6 @@ func newPaymentLifecycle(r *ChannelRouter, feeLimit lnwire.MilliSatoshi,
// Mount the result collector. // Mount the result collector.
p.resultCollector = p.collectResultAsync p.resultCollector = p.collectResultAsync
// If a timeout is specified, create a timeout channel. If no timeout is
// specified, the channel is left nil and will never abort the payment
// loop.
if timeout != 0 {
p.timeoutChan = time.After(timeout)
}
return p return p
} }
@@ -167,7 +160,9 @@ func (p *paymentLifecycle) decideNextStep(
} }
// resumePayment resumes the paymentLifecycle from the current state. // resumePayment resumes the paymentLifecycle from the current state.
func (p *paymentLifecycle) resumePayment() ([32]byte, *route.Route, error) { func (p *paymentLifecycle) resumePayment(ctx context.Context) ([32]byte,
*route.Route, error) {
// When the payment lifecycle loop exits, we make sure to signal any // When the payment lifecycle loop exits, we make sure to signal any
// sub goroutine of the HTLC attempt to exit, then wait for them to // sub goroutine of the HTLC attempt to exit, then wait for them to
// return. // return.
@@ -221,18 +216,17 @@ lifecycle:
// We now proceed our lifecycle with the following tasks in // We now proceed our lifecycle with the following tasks in
// order, // order,
// 1. check timeout. // 1. check context.
// 2. request route. // 2. request route.
// 3. create HTLC attempt. // 3. create HTLC attempt.
// 4. send HTLC attempt. // 4. send HTLC attempt.
// 5. collect HTLC attempt result. // 5. collect HTLC attempt result.
// //
// Before we attempt any new shard, we'll check to see if // Before we attempt any new shard, we'll check to see if we've
// either we've gone past the payment attempt timeout, or the // gone past the payment attempt timeout, or if the context was
// router is exiting. In either case, we'll stop this payment // cancelled, or the router is exiting. In any of these cases,
// attempt short. If a timeout is not applicable, timeoutChan // we'll stop this payment attempt short.
// will be nil. if err := p.checkContext(ctx); err != nil {
if err := p.checkTimeout(); err != nil {
return exitWithErr(err) return exitWithErr(err)
} }
@@ -318,19 +312,30 @@ lifecycle:
return [32]byte{}, nil, *failure return [32]byte{}, nil, *failure
} }
// checkTimeout checks whether the payment has reached its timeout. // checkContext checks whether the payment context has been canceled.
func (p *paymentLifecycle) checkTimeout() error { // Cancellation occurs manually or if the context times out.
func (p *paymentLifecycle) checkContext(ctx context.Context) error {
select { select {
case <-p.timeoutChan: case <-ctx.Done():
log.Warnf("payment attempt not completed before timeout") // If the context was canceled, we'll mark the payment as
// failed. There are two cases to distinguish here: Either a
// user-provided timeout was reached, or the context was
// canceled, either to a manual cancellation or due to an
// unknown error.
if errors.Is(ctx.Err(), context.DeadlineExceeded) {
log.Warnf("Payment attempt not completed before "+
"timeout, id=%s", p.identifier.String())
} else {
log.Warnf("Payment attempt context canceled, id=%s",
p.identifier.String())
}
// By marking the payment failed, depending on whether it has // By marking the payment failed, depending on whether it has
// inflight HTLCs or not, its status will now either be // inflight HTLCs or not, its status will now either be
// `StatusInflight` or `StatusFailed`. In either case, no more // `StatusInflight` or `StatusFailed`. In either case, no more
// HTLCs will be attempted. // HTLCs will be attempted.
err := p.router.cfg.Control.FailPayment( reason := channeldb.FailureReasonTimeout
p.identifier, channeldb.FailureReasonTimeout, err := p.router.cfg.Control.FailPayment(p.identifier, reason)
)
if err != nil { if err != nil {
return fmt.Errorf("FailPayment got %w", err) return fmt.Errorf("FailPayment got %w", err)
} }
@@ -368,7 +373,7 @@ func (p *paymentLifecycle) requestRoute(
log.Warnf("Failed to find route for payment %v: %v", p.identifier, err) log.Warnf("Failed to find route for payment %v: %v", p.identifier, err)
// If the error belongs to `noRouteError` set, it means a non-critical // If the error belongs to `noRouteError` set, it means a non-critical
// error has happened during path finding and we will mark the payment // error has happened during path finding, and we will mark the payment
// failed with this reason. Otherwise, we'll return the critical error // failed with this reason. Otherwise, we'll return the critical error
// found to abort the lifecycle. // found to abort the lifecycle.
var routeErr noRouteError var routeErr noRouteError
@@ -377,9 +382,9 @@ func (p *paymentLifecycle) requestRoute(
} }
// It's the `paymentSession`'s responsibility to find a route for us // It's the `paymentSession`'s responsibility to find a route for us
// with best effort. When it cannot find a path, we need to treat it as // with the best effort. When it cannot find a path, we need to treat it
// a terminal condition and fail the payment no matter it has inflight // as a terminal condition and fail the payment no matter it has
// HTLCs or not. // inflight HTLCs or not.
failureCode := routeErr.FailureReason() failureCode := routeErr.FailureReason()
log.Warnf("Marking payment %v permanently failed with no route: %v", log.Warnf("Marking payment %v permanently failed with no route: %v",
p.identifier, failureCode) p.identifier, failureCode)
@@ -415,7 +420,7 @@ type attemptResult struct {
// collectResultAsync launches a goroutine that will wait for the result of the // collectResultAsync launches a goroutine that will wait for the result of the
// given HTLC attempt to be available then handle its result. Once received, it // given HTLC attempt to be available then handle its result. Once received, it
// will send a nil error to channel `resultCollected` to indicate there's an // will send a nil error to channel `resultCollected` to indicate there's a
// result. // result.
func (p *paymentLifecycle) collectResultAsync(attempt *channeldb.HTLCAttempt) { func (p *paymentLifecycle) collectResultAsync(attempt *channeldb.HTLCAttempt) {
log.Debugf("Collecting result for attempt %v in payment %v", log.Debugf("Collecting result for attempt %v in payment %v",
@@ -484,7 +489,7 @@ func (p *paymentLifecycle) collectResult(attempt *channeldb.HTLCAttempt) (
return p.failAttempt(attempt.AttemptID, err) return p.failAttempt(attempt.AttemptID, err)
} }
// Using the created circuit, initialize the error decrypter so we can // Using the created circuit, initialize the error decrypter, so we can
// parse+decode any failures incurred by this payment within the // parse+decode any failures incurred by this payment within the
// switch. // switch.
errorDecryptor := &htlcswitch.SphinxErrorDecrypter{ errorDecryptor := &htlcswitch.SphinxErrorDecrypter{
@@ -786,7 +791,7 @@ func (p *paymentLifecycle) handleSwitchErr(attempt *channeldb.HTLCAttempt,
return p.failAttempt(attemptID, sendErr) return p.failAttempt(attemptID, sendErr)
} }
if sendErr == htlcswitch.ErrUnreadableFailureMessage { if errors.Is(sendErr, htlcswitch.ErrUnreadableFailureMessage) {
log.Warn("Unreadable failure when sending htlc: id=%v, hash=%v", log.Warn("Unreadable failure when sending htlc: id=%v, hash=%v",
attempt.AttemptID, attempt.Hash) attempt.AttemptID, attempt.Hash)
@@ -801,7 +806,8 @@ func (p *paymentLifecycle) handleSwitchErr(attempt *channeldb.HTLCAttempt,
// down the route. If the error is not related to the propagation of // down the route. If the error is not related to the propagation of
// our payment, we can stop trying because an internal error has // our payment, we can stop trying because an internal error has
// occurred. // occurred.
rtErr, ok := sendErr.(htlcswitch.ClearTextError) var rtErr htlcswitch.ClearTextError
ok := errors.As(sendErr, &rtErr)
if !ok { if !ok {
return p.failPaymentAndAttempt( return p.failPaymentAndAttempt(
attemptID, &internalErrorReason, sendErr, attemptID, &internalErrorReason, sendErr,
@@ -815,7 +821,8 @@ func (p *paymentLifecycle) handleSwitchErr(attempt *channeldb.HTLCAttempt,
// ForwardingError, it did not originate at our node, so we set // ForwardingError, it did not originate at our node, so we set
// failureSourceIdx to the index of the node where the failure occurred. // failureSourceIdx to the index of the node where the failure occurred.
failureSourceIdx := 0 failureSourceIdx := 0
source, ok := rtErr.(*htlcswitch.ForwardingError) var source *htlcswitch.ForwardingError
ok = errors.As(rtErr, &source)
if ok { if ok {
failureSourceIdx = source.FailureSourceIdx failureSourceIdx = source.FailureSourceIdx
} }
@@ -863,7 +870,7 @@ func (p *paymentLifecycle) handleFailureMessage(rt *route.Route,
// Parse pubkey to allow validation of the channel update. This should // Parse pubkey to allow validation of the channel update. This should
// always succeed, otherwise there is something wrong in our // always succeed, otherwise there is something wrong in our
// implementation. Therefore return an error. // implementation. Therefore, return an error.
errVertex := rt.Hops[errorSourceIdx-1].PubKeyBytes errVertex := rt.Hops[errorSourceIdx-1].PubKeyBytes
errSource, err := btcec.ParsePubKey(errVertex[:]) errSource, err := btcec.ParsePubKey(errVertex[:])
if err != nil { if err != nil {
@@ -951,17 +958,18 @@ func marshallError(sendError error, time time.Time) *channeldb.HTLCFailInfo {
FailTime: time, FailTime: time,
} }
switch sendError { switch {
case htlcswitch.ErrPaymentIDNotFound: case errors.Is(sendError, htlcswitch.ErrPaymentIDNotFound):
response.Reason = channeldb.HTLCFailInternal response.Reason = channeldb.HTLCFailInternal
return response return response
case htlcswitch.ErrUnreadableFailureMessage: case errors.Is(sendError, htlcswitch.ErrUnreadableFailureMessage):
response.Reason = channeldb.HTLCFailUnreadable response.Reason = channeldb.HTLCFailUnreadable
return response return response
} }
rtErr, ok := sendError.(htlcswitch.ClearTextError) var rtErr htlcswitch.ClearTextError
ok := errors.As(sendError, &rtErr)
if !ok { if !ok {
response.Reason = channeldb.HTLCFailInternal response.Reason = channeldb.HTLCFailInternal
return response return response
@@ -981,7 +989,8 @@ func marshallError(sendError error, time time.Time) *channeldb.HTLCFailInfo {
// failure occurred. If the error is not a ForwardingError, the failure // failure occurred. If the error is not a ForwardingError, the failure
// occurred at our node, so we leave the index as 0 to indicate that // occurred at our node, so we leave the index as 0 to indicate that
// we failed locally. // we failed locally.
fErr, ok := rtErr.(*htlcswitch.ForwardingError) var fErr *htlcswitch.ForwardingError
ok = errors.As(rtErr, &fErr)
if ok { if ok {
response.FailureSourceIndex = uint32(fErr.FailureSourceIdx) response.FailureSourceIndex = uint32(fErr.FailureSourceIdx)
} }
+90 -24
View File
@@ -1,6 +1,7 @@
package routing package routing
import ( import (
"context"
"sync/atomic" "sync/atomic"
"testing" "testing"
"time" "time"
@@ -88,7 +89,7 @@ func newTestPaymentLifecycle(t *testing.T) (*paymentLifecycle, *mockers) {
// Create a test payment lifecycle with no fee limit and no timeout. // Create a test payment lifecycle with no fee limit and no timeout.
p := newPaymentLifecycle( p := newPaymentLifecycle(
rt, noFeeLimit, paymentHash, mockPaymentSession, rt, noFeeLimit, paymentHash, mockPaymentSession,
mockShardTracker, 0, 0, mockShardTracker, 0,
) )
// Create a mock payment which is returned from mockControlTower. // Create a mock payment which is returned from mockControlTower.
@@ -151,9 +152,9 @@ type resumePaymentResult struct {
err error err error
} }
// sendPaymentAndAssertFailed calls `resumePayment` and asserts that an error // sendPaymentAndAssertError calls `resumePayment` and asserts that an error is
// is returned. // returned.
func sendPaymentAndAssertFailed(t *testing.T, func sendPaymentAndAssertError(t *testing.T, ctx context.Context,
p *paymentLifecycle, errExpected error) { p *paymentLifecycle, errExpected error) {
resultChan := make(chan *resumePaymentResult, 1) resultChan := make(chan *resumePaymentResult, 1)
@@ -161,7 +162,7 @@ func sendPaymentAndAssertFailed(t *testing.T,
// We now make a call to `resumePayment` and expect it to return the // We now make a call to `resumePayment` and expect it to return the
// error. // error.
go func() { go func() {
preimage, _, err := p.resumePayment() preimage, _, err := p.resumePayment(ctx)
resultChan <- &resumePaymentResult{ resultChan <- &resumePaymentResult{
preimage: preimage, preimage: preimage,
err: err, err: err,
@@ -189,7 +190,7 @@ func sendPaymentAndAssertSucceeded(t *testing.T,
// We now make a call to `resumePayment` and expect it to return the // We now make a call to `resumePayment` and expect it to return the
// preimage. // preimage.
go func() { go func() {
preimage, _, err := p.resumePayment() preimage, _, err := p.resumePayment(context.Background())
resultChan <- &resumePaymentResult{ resultChan <- &resumePaymentResult{
preimage: preimage, preimage: preimage,
err: err, err: err,
@@ -278,6 +279,10 @@ func makeAttemptInfo(t *testing.T, amt int) channeldb.HTLCAttemptInfo {
func TestCheckTimeoutTimedOut(t *testing.T) { func TestCheckTimeoutTimedOut(t *testing.T) {
t.Parallel() t.Parallel()
deadline := time.Now().Add(time.Nanosecond)
ctx, cancel := context.WithDeadline(context.Background(), deadline)
defer cancel()
p := createTestPaymentLifecycle() p := createTestPaymentLifecycle()
// Mock the control tower's `FailPayment` method. // Mock the control tower's `FailPayment` method.
@@ -288,14 +293,11 @@ func TestCheckTimeoutTimedOut(t *testing.T) {
// Mount the mocked control tower. // Mount the mocked control tower.
p.router.cfg.Control = ct p.router.cfg.Control = ct
// Make the timeout happens instantly.
p.timeoutChan = time.After(1 * time.Nanosecond)
// Sleep one millisecond to make sure it timed out. // Sleep one millisecond to make sure it timed out.
time.Sleep(1 * time.Millisecond) time.Sleep(1 * time.Millisecond)
// Call the function and expect no error. // Call the function and expect no error.
err := p.checkTimeout() err := p.checkContext(ctx)
require.NoError(t, err) require.NoError(t, err)
// Assert that `FailPayment` is called as expected. // Assert that `FailPayment` is called as expected.
@@ -313,13 +315,15 @@ func TestCheckTimeoutTimedOut(t *testing.T) {
p.router.cfg.Control = ct p.router.cfg.Control = ct
// Make the timeout happens instantly. // Make the timeout happens instantly.
p.timeoutChan = time.After(1 * time.Nanosecond) deadline = time.Now().Add(time.Nanosecond)
ctx, cancel = context.WithDeadline(context.Background(), deadline)
defer cancel()
// Sleep one millisecond to make sure it timed out. // Sleep one millisecond to make sure it timed out.
time.Sleep(1 * time.Millisecond) time.Sleep(1 * time.Millisecond)
// Call the function and expect an error. // Call the function and expect an error.
err = p.checkTimeout() err = p.checkContext(ctx)
require.ErrorIs(t, err, errDummy) require.ErrorIs(t, err, errDummy)
// Assert that `FailPayment` is called as expected. // Assert that `FailPayment` is called as expected.
@@ -331,10 +335,13 @@ func TestCheckTimeoutTimedOut(t *testing.T) {
func TestCheckTimeoutOnRouterQuit(t *testing.T) { func TestCheckTimeoutOnRouterQuit(t *testing.T) {
t.Parallel() t.Parallel()
ctx, cancel := context.WithCancel(context.Background())
defer cancel()
p := createTestPaymentLifecycle() p := createTestPaymentLifecycle()
close(p.router.quit) close(p.router.quit)
err := p.checkTimeout() err := p.checkContext(ctx)
require.ErrorIs(t, err, ErrRouterShuttingDown) require.ErrorIs(t, err, ErrRouterShuttingDown)
} }
@@ -627,7 +634,7 @@ func TestResumePaymentFailOnFetchPayment(t *testing.T) {
m.control.On("FetchPayment", p.identifier).Return(nil, errDummy) m.control.On("FetchPayment", p.identifier).Return(nil, errDummy)
// Send the payment and assert it failed. // Send the payment and assert it failed.
sendPaymentAndAssertFailed(t, p, errDummy) sendPaymentAndAssertError(t, context.Background(), p, errDummy)
// Expected collectResultAsync to not be called. // Expected collectResultAsync to not be called.
require.Zero(t, m.collectResultsCount) require.Zero(t, m.collectResultsCount)
@@ -656,14 +663,15 @@ func TestResumePaymentFailOnTimeout(t *testing.T) {
} }
m.payment.On("GetState").Return(ps).Once() m.payment.On("GetState").Return(ps).Once()
// NOTE: GetStatus is only used to populate the logs which is // NOTE: GetStatus is only used to populate the logs which is not
// not critical so we loosen the checks on how many times it's // critical, so we loosen the checks on how many times it's been called.
// been called.
m.payment.On("GetStatus").Return(channeldb.StatusInFlight) m.payment.On("GetStatus").Return(channeldb.StatusInFlight)
// 3. make the timeout happens instantly and sleep one millisecond to // 3. make the timeout happens instantly and sleep one millisecond to
// make sure it timed out. // make sure it timed out.
p.timeoutChan = time.After(1 * time.Nanosecond) deadline := time.Now().Add(time.Nanosecond)
ctx, cancel := context.WithDeadline(context.Background(), deadline)
defer cancel()
time.Sleep(1 * time.Millisecond) time.Sleep(1 * time.Millisecond)
// 4. the payment should be failed with reason timeout. // 4. the payment should be failed with reason timeout.
@@ -683,7 +691,7 @@ func TestResumePaymentFailOnTimeout(t *testing.T) {
m.payment.On("TerminalInfo").Return(nil, &reason) m.payment.On("TerminalInfo").Return(nil, &reason)
// Send the payment and assert it failed with the timeout reason. // Send the payment and assert it failed with the timeout reason.
sendPaymentAndAssertFailed(t, p, reason) sendPaymentAndAssertError(t, ctx, p, reason)
// Expected collectResultAsync to not be called. // Expected collectResultAsync to not be called.
require.Zero(t, m.collectResultsCount) require.Zero(t, m.collectResultsCount)
@@ -721,7 +729,65 @@ func TestResumePaymentFailOnTimeoutErr(t *testing.T) {
close(p.router.quit) close(p.router.quit)
// Send the payment and assert it failed when router is shutting down. // Send the payment and assert it failed when router is shutting down.
sendPaymentAndAssertFailed(t, p, ErrRouterShuttingDown) sendPaymentAndAssertError(
t, context.Background(), p, ErrRouterShuttingDown,
)
// Expected collectResultAsync to not be called.
require.Zero(t, m.collectResultsCount)
}
// TestResumePaymentFailContextCancel checks that the lifecycle fails when the
// context is canceled.
//
// NOTE: No parallel test because it overwrites global variables.
//
//nolint:paralleltest
func TestResumePaymentFailContextCancel(t *testing.T) {
// Create a test paymentLifecycle with the initial two calls mocked.
p, m := setupTestPaymentLifecycle(t)
// Create the cancelable payment context.
ctx, cancel := context.WithCancel(context.Background())
paymentAmt := lnwire.MilliSatoshi(10000)
// We now enter the payment lifecycle loop.
//
// 1. calls `FetchPayment` and return the payment.
m.control.On("FetchPayment", p.identifier).Return(m.payment, nil).Once()
// 2. calls `GetState` and return the state.
ps := &channeldb.MPPaymentState{
RemainingAmt: paymentAmt,
}
m.payment.On("GetState").Return(ps).Once()
// NOTE: GetStatus is only used to populate the logs which is not
// critical, so we loosen the checks on how many times it's been called.
m.payment.On("GetStatus").Return(channeldb.StatusInFlight)
// 3. Cancel the context and skip the FailPayment error to trigger the
// context cancellation of the payment.
cancel()
m.control.On(
"FailPayment", p.identifier, channeldb.FailureReasonTimeout,
).Return(nil).Once()
// 5. decideNextStep now returns stepExit.
m.payment.On("AllowMoreAttempts").Return(false, nil).Once().
On("NeedWaitAttempts").Return(false, nil).Once()
// 6. Control tower deletes failed attempts.
m.control.On("DeleteFailedAttempts", p.identifier).Return(nil).Once()
// 7. We will observe FailureReasonError if the context was cancelled.
reason := channeldb.FailureReasonError
m.payment.On("TerminalInfo").Return(nil, &reason)
// Send the payment and assert it failed with the timeout reason.
sendPaymentAndAssertError(t, ctx, p, reason)
// Expected collectResultAsync to not be called. // Expected collectResultAsync to not be called.
require.Zero(t, m.collectResultsCount) require.Zero(t, m.collectResultsCount)
@@ -759,7 +825,7 @@ func TestResumePaymentFailOnStepErr(t *testing.T) {
m.payment.On("AllowMoreAttempts").Return(false, errDummy).Once() m.payment.On("AllowMoreAttempts").Return(false, errDummy).Once()
// Send the payment and assert it failed. // Send the payment and assert it failed.
sendPaymentAndAssertFailed(t, p, errDummy) sendPaymentAndAssertError(t, context.Background(), p, errDummy)
// Expected collectResultAsync to not be called. // Expected collectResultAsync to not be called.
require.Zero(t, m.collectResultsCount) require.Zero(t, m.collectResultsCount)
@@ -803,7 +869,7 @@ func TestResumePaymentFailOnRequestRouteErr(t *testing.T) {
).Return(nil, errDummy).Once() ).Return(nil, errDummy).Once()
// Send the payment and assert it failed. // Send the payment and assert it failed.
sendPaymentAndAssertFailed(t, p, errDummy) sendPaymentAndAssertError(t, context.Background(), p, errDummy)
// Expected collectResultAsync to not be called. // Expected collectResultAsync to not be called.
require.Zero(t, m.collectResultsCount) require.Zero(t, m.collectResultsCount)
@@ -863,7 +929,7 @@ func TestResumePaymentFailOnRegisterAttemptErr(t *testing.T) {
).Return(nil, errDummy).Once() ).Return(nil, errDummy).Once()
// Send the payment and assert it failed. // Send the payment and assert it failed.
sendPaymentAndAssertFailed(t, p, errDummy) sendPaymentAndAssertError(t, context.Background(), p, errDummy)
// Expected collectResultAsync to not be called. // Expected collectResultAsync to not be called.
require.Zero(t, m.collectResultsCount) require.Zero(t, m.collectResultsCount)
@@ -955,7 +1021,7 @@ func TestResumePaymentFailOnSendAttemptErr(t *testing.T) {
).Return(nil, errDummy).Once() ).Return(nil, errDummy).Once()
// Send the payment and assert it failed. // Send the payment and assert it failed.
sendPaymentAndAssertFailed(t, p, errDummy) sendPaymentAndAssertError(t, context.Background(), p, errDummy)
// Expected collectResultAsync to not be called. // Expected collectResultAsync to not be called.
require.Zero(t, m.collectResultsCount) require.Zero(t, m.collectResultsCount)
+92 -62
View File
@@ -2,6 +2,7 @@ package routing
import ( import (
"bytes" "bytes"
"context"
"fmt" "fmt"
"math" "math"
"runtime" "runtime"
@@ -45,11 +46,11 @@ const (
// DefaultPayAttemptTimeout is the default payment attempt timeout. The // DefaultPayAttemptTimeout is the default payment attempt timeout. The
// payment attempt timeout defines the duration after which we stop // payment attempt timeout defines the duration after which we stop
// trying more routes for a payment. // trying more routes for a payment.
DefaultPayAttemptTimeout = time.Duration(time.Second * 60) DefaultPayAttemptTimeout = time.Second * 60
// DefaultChannelPruneExpiry is the default duration used to determine // DefaultChannelPruneExpiry is the default duration used to determine
// if a channel should be pruned or not. // if a channel should be pruned or not.
DefaultChannelPruneExpiry = time.Duration(time.Hour * 24 * 14) DefaultChannelPruneExpiry = time.Hour * 24 * 14
// DefaultFirstTimePruneDelay is the time we'll wait after startup // DefaultFirstTimePruneDelay is the time we'll wait after startup
// before attempting to prune the graph for zombie channels. We don't // before attempting to prune the graph for zombie channels. We don't
@@ -376,9 +377,9 @@ type Config struct {
FirstTimePruneDelay time.Duration FirstTimePruneDelay time.Duration
// QueryBandwidth is a method that allows the router to query the lower // QueryBandwidth is a method that allows the router to query the lower
// link layer to determine the up to date available bandwidth at a // link layer to determine the up-to-date available bandwidth at a
// prospective link to be traversed. If the link isn't available, then // prospective link to be traversed. If the link isn't available, then
// a value of zero should be returned. Otherwise, the current up to // a value of zero should be returned. Otherwise, the current up-to-
// date knowledge of the available bandwidth of the link should be // date knowledge of the available bandwidth of the link should be
// returned. // returned.
GetLink getLinkQuery GetLink getLinkQuery
@@ -389,7 +390,7 @@ type Config struct {
// the switch can properly handle the HTLC. // the switch can properly handle the HTLC.
NextPaymentID func() (uint64, error) NextPaymentID func() (uint64, error)
// AssumeChannelValid toggles whether or not the router will check for // AssumeChannelValid toggles whether the router will check for
// spentness of channel outpoints. For neutrino, this saves long rescans // spentness of channel outpoints. For neutrino, this saves long rescans
// from blocking initial usage of the daemon. // from blocking initial usage of the daemon.
AssumeChannelValid bool AssumeChannelValid bool
@@ -423,7 +424,7 @@ type EdgeLocator struct {
Direction uint8 Direction uint8
} }
// String returns a human readable version of the edgeLocator values. // String returns a human-readable version of the edgeLocator values.
func (e *EdgeLocator) String() string { func (e *EdgeLocator) String() string {
return fmt.Sprintf("%v:%v", e.ChannelID, e.Direction) return fmt.Sprintf("%v:%v", e.ChannelID, e.Direction)
} }
@@ -551,9 +552,10 @@ func (r *ChannelRouter) Start() error {
// then we don't treat this as an explicit error. // then we don't treat this as an explicit error.
if _, _, err := r.cfg.Graph.PruneTip(); err != nil { if _, _, err := r.cfg.Graph.PruneTip(); err != nil {
switch { switch {
case err == channeldb.ErrGraphNeverPruned: case errors.Is(err, channeldb.ErrGraphNeverPruned):
fallthrough fallthrough
case err == channeldb.ErrGraphNotFound:
case errors.Is(err, channeldb.ErrGraphNotFound):
// If the graph has never been pruned, then we'll set // If the graph has never been pruned, then we'll set
// the prune height to the current best height of the // the prune height to the current best height of the
// chain backend. // chain backend.
@@ -563,6 +565,7 @@ func (r *ChannelRouter) Start() error {
if err != nil { if err != nil {
return err return err
} }
default: default:
return err return err
} }
@@ -603,7 +606,10 @@ func (r *ChannelRouter) Start() error {
// we may miss on-chain events as the filter hasn't properly // we may miss on-chain events as the filter hasn't properly
// been applied. // been applied.
channelView, err := r.cfg.Graph.ChannelView() channelView, err := r.cfg.Graph.ChannelView()
if err != nil && err != channeldb.ErrGraphNoEdgesFound { if err != nil && !errors.Is(
err, channeldb.ErrGraphNoEdgesFound,
) {
return err return err
} }
@@ -638,7 +644,10 @@ func (r *ChannelRouter) Start() error {
// from the graph in order to ensure we maintain a tight graph // from the graph in order to ensure we maintain a tight graph
// of "useful" nodes. // of "useful" nodes.
err = r.cfg.Graph.PruneGraphNodes() err = r.cfg.Graph.PruneGraphNodes()
if err != nil && err != channeldb.ErrGraphNodesNotFound { if err != nil && !errors.Is(
err, channeldb.ErrGraphNodesNotFound,
) {
return err return err
} }
} }
@@ -707,13 +716,15 @@ func (r *ChannelRouter) Start() error {
// result for the in-flight attempt is received. // result for the in-flight attempt is received.
paySession := r.cfg.SessionSource.NewPaymentSessionEmpty() paySession := r.cfg.SessionSource.NewPaymentSessionEmpty()
// We pass in a zero timeout value, to indicate we // We pass in a non-timeout context, to indicate we
// don't need it to timeout. It will stop immediately // don't need it to timeout. It will stop immediately
// after the existing attempt has finished anyway. We // after the existing attempt has finished anyway. We
// also set a zero fee limit, as no more routes should // also set a zero fee limit, as no more routes should
// be tried. // be tried.
noTimeout := time.Duration(0)
_, _, err := r.sendPayment( _, _, err := r.sendPayment(
0, payment.Info.PaymentIdentifier, 0, context.Background(), 0,
payment.Info.PaymentIdentifier, noTimeout,
paySession, shardTracker, paySession, shardTracker,
) )
if err != nil { if err != nil {
@@ -776,8 +787,8 @@ func (r *ChannelRouter) syncGraphWithChain() error {
switch { switch {
// If the graph has never been pruned, or hasn't fully been // If the graph has never been pruned, or hasn't fully been
// created yet, then we don't treat this as an explicit error. // created yet, then we don't treat this as an explicit error.
case err == channeldb.ErrGraphNeverPruned: case errors.Is(err, channeldb.ErrGraphNeverPruned):
case err == channeldb.ErrGraphNotFound: case errors.Is(err, channeldb.ErrGraphNotFound):
default: default:
return err return err
} }
@@ -826,10 +837,12 @@ func (r *ChannelRouter) syncGraphWithChain() error {
// can exit as this entails we are back to the point // can exit as this entails we are back to the point
// where it hasn't seen any block or created channels, // where it hasn't seen any block or created channels,
// alas there's nothing left to prune. // alas there's nothing left to prune.
case err == channeldb.ErrGraphNeverPruned: case errors.Is(err, channeldb.ErrGraphNeverPruned):
return nil return nil
case err == channeldb.ErrGraphNotFound:
case errors.Is(err, channeldb.ErrGraphNotFound):
return nil return nil
default: default:
return err return err
} }
@@ -1047,7 +1060,10 @@ func (r *ChannelRouter) pruneZombieChans() error {
} }
for _, u := range oldEdges { for _, u := range oldEdges {
filterPruneChans(u.Info, u.Policy1, u.Policy2) err = filterPruneChans(u.Info, u.Policy1, u.Policy2)
if err != nil {
log.Warnf("Filter pruning channels: %w\n", err)
}
} }
log.Infof("Pruning %v zombie channels", len(chansToPrune)) log.Infof("Pruning %v zombie channels", len(chansToPrune))
@@ -1072,7 +1088,7 @@ func (r *ChannelRouter) pruneZombieChans() error {
// With the channels pruned, we'll also attempt to prune any nodes that // With the channels pruned, we'll also attempt to prune any nodes that
// were a part of them. // were a part of them.
err = r.cfg.Graph.PruneGraphNodes() err = r.cfg.Graph.PruneGraphNodes()
if err != nil && err != channeldb.ErrGraphNodesNotFound { if err != nil && !errors.Is(err, channeldb.ErrGraphNodesNotFound) {
return fmt.Errorf("unable to prune graph nodes: %w", err) return fmt.Errorf("unable to prune graph nodes: %w", err)
} }
@@ -1125,7 +1141,7 @@ func (r *ChannelRouter) handleNetworkUpdate(vb *ValidationBarrier,
if err != nil { if err != nil {
// We now decide to log an error or not. If allowDependents is // We now decide to log an error or not. If allowDependents is
// false, it means there is an error and the error is neither // false, it means there is an error and the error is neither
// ErrIgnored or ErrOutdated. In this case, we'll log an error. // ErrIgnored nor ErrOutdated. In this case, we'll log an error.
// Otherwise, we'll add debug log only. // Otherwise, we'll add debug log only.
if allowDependents { if allowDependents {
log.Debugf("process network updates got: %v", err) log.Debugf("process network updates got: %v", err)
@@ -1225,7 +1241,7 @@ func (r *ChannelRouter) networkHandler() {
// Since this block is stale, we update our best height // Since this block is stale, we update our best height
// to the previous block. // to the previous block.
blockHeight := uint32(chainUpdate.Height) blockHeight := chainUpdate.Height
atomic.StoreUint32(&r.bestHeight, blockHeight-1) atomic.StoreUint32(&r.bestHeight, blockHeight-1)
// Update the channel graph to reflect that this block // Update the channel graph to reflect that this block
@@ -1485,9 +1501,9 @@ func (r *ChannelRouter) assertNodeAnnFreshness(node route.Vertex,
} }
// addZombieEdge adds a channel that failed complete validation into the zombie // addZombieEdge adds a channel that failed complete validation into the zombie
// index so we can avoid having to re-validate it in the future. // index, so we can avoid having to re-validate it in the future.
func (r *ChannelRouter) addZombieEdge(chanID uint64) error { func (r *ChannelRouter) addZombieEdge(chanID uint64) error {
// If the edge fails validation we'll mark the edge itself as a zombie // If the edge fails validation we'll mark the edge itself as a zombie,
// so we don't continue to request it. We use the "zero key" for both // so we don't continue to request it. We use the "zero key" for both
// node pubkeys so this edge can't be resurrected. // node pubkeys so this edge can't be resurrected.
var zeroKey [33]byte var zeroKey [33]byte
@@ -1598,7 +1614,10 @@ func (r *ChannelRouter) processUpdate(msg interface{},
_, _, exists, isZombie, err := r.cfg.Graph.HasChannelEdge( _, _, exists, isZombie, err := r.cfg.Graph.HasChannelEdge(
msg.ChannelID, msg.ChannelID,
) )
if err != nil && err != channeldb.ErrGraphNoEdgesFound { if err != nil && !errors.Is(
err, channeldb.ErrGraphNoEdgesFound,
) {
return errors.Errorf("unable to check for edge "+ return errors.Errorf("unable to check for edge "+
"existence: %v", err) "existence: %v", err)
} }
@@ -1655,7 +1674,7 @@ func (r *ChannelRouter) processUpdate(msg interface{},
case strings.Contains(err.Error(), "out of range"): case strings.Contains(err.Error(), "out of range"):
// If the funding transaction isn't found at // If the funding transaction isn't found at
// all, then we'll mark the edge itself as a // all, then we'll mark the edge itself as a
// zombie so we don't continue to request it. // zombie, so we don't continue to request it.
// We use the "zero key" for both node pubkeys // We use the "zero key" for both node pubkeys
// so this edge can't be resurrected. // so this edge can't be resurrected.
zErr := r.addZombieEdge(msg.ChannelID) zErr := r.addZombieEdge(msg.ChannelID)
@@ -1681,8 +1700,8 @@ func (r *ChannelRouter) processUpdate(msg interface{},
return err return err
} }
// Next we'll validate that this channel is actually well // Next we'll validate that this channel is actually
// formed. If this check fails, then this channel either // well-formed. If this check fails, then this channel either
// doesn't exist, or isn't the one that was meant to be created // doesn't exist, or isn't the one that was meant to be created
// according to the passed channel proofs. // according to the passed channel proofs.
fundingPoint, err := chanvalidate.Validate(&chanvalidate.Context{ fundingPoint, err := chanvalidate.Validate(&chanvalidate.Context{
@@ -1693,7 +1712,7 @@ func (r *ChannelRouter) processUpdate(msg interface{},
FundingTx: fundingTx, FundingTx: fundingTx,
}) })
if err != nil { if err != nil {
// Mark the edge as a zombie so we won't try to // Mark the edge as a zombie, so we won't try to
// re-validate it on start up. // re-validate it on start up.
if err := r.addZombieEdge(msg.ChannelID); err != nil { if err := r.addZombieEdge(msg.ChannelID); err != nil {
return err return err
@@ -1705,7 +1724,7 @@ func (r *ChannelRouter) processUpdate(msg interface{},
// Now that we have the funding outpoint of the channel, ensure // Now that we have the funding outpoint of the channel, ensure
// that it hasn't yet been spent. If so, then this channel has // that it hasn't yet been spent. If so, then this channel has
// been closed so we'll ignore it. // been closed, so we'll ignore it.
chanUtxo, err := r.cfg.Chain.GetUtxo( chanUtxo, err := r.cfg.Chain.GetUtxo(
fundingPoint, fundingPkScript, channelID.BlockHeight, fundingPoint, fundingPkScript, channelID.BlockHeight,
r.quit, r.quit,
@@ -1740,7 +1759,7 @@ func (r *ChannelRouter) processUpdate(msg interface{},
// As a new edge has been added to the channel graph, we'll // As a new edge has been added to the channel graph, we'll
// update the current UTXO filter within our active // update the current UTXO filter within our active
// FilteredChainView so we are notified if/when this channel is // FilteredChainView, so we are notified if/when this channel is
// closed. // closed.
filterUpdate := []channeldb.EdgePoint{ filterUpdate := []channeldb.EdgePoint{
{ {
@@ -1768,7 +1787,10 @@ func (r *ChannelRouter) processUpdate(msg interface{},
edge1Timestamp, edge2Timestamp, exists, isZombie, err := edge1Timestamp, edge2Timestamp, exists, isZombie, err :=
r.cfg.Graph.HasChannelEdge(msg.ChannelID) r.cfg.Graph.HasChannelEdge(msg.ChannelID)
if err != nil && err != channeldb.ErrGraphNoEdgesFound { if err != nil && !errors.Is(
err, channeldb.ErrGraphNoEdgesFound,
) {
return errors.Errorf("unable to check for edge "+ return errors.Errorf("unable to check for edge "+
"existence: %v", err) "existence: %v", err)
@@ -1795,8 +1817,8 @@ func (r *ChannelRouter) processUpdate(msg interface{},
} }
// As edges are directional edge node has a unique policy for // As edges are directional edge node has a unique policy for
// the direction of the edge they control. Therefore we first // the direction of the edge they control. Therefore, we first
// check if we already have the most up to date information for // check if we already have the most up-to-date information for
// that edge. If this message has a timestamp not strictly // that edge. If this message has a timestamp not strictly
// newer than what we already know of we can exit early. // newer than what we already know of we can exit early.
switch { switch {
@@ -2099,8 +2121,8 @@ func (r *ChannelRouter) FindRoute(req *RouteRequest) (*route.Route, float64,
return nil, 0, err return nil, 0, err
} }
// We'll fetch the current block height so we can properly calculate the // We'll fetch the current block height, so we can properly calculate
// required HTLC time locks within the route. // the required HTLC time locks within the route.
_, currentHeight, err := r.cfg.Chain.GetBestBlock() _, currentHeight, err := r.cfg.Chain.GetBestBlock()
if err != nil { if err != nil {
return nil, 0, err return nil, 0, err
@@ -2172,7 +2194,7 @@ func generateSphinxPacket(rt *route.Route, paymentHash []byte,
// Now that we know we have an actual route, we'll map the route into a // Now that we know we have an actual route, we'll map the route into a
// sphinx payment path which includes per-hop payloads for each hop // sphinx payment path which includes per-hop payloads for each hop
// that give each node within the route the necessary information // that give each node within the route the necessary information
// (fees, CLTV value, etc) to properly forward the payment. // (fees, CLTV value, etc.) to properly forward the payment.
sphinxPath, err := rt.ToSphinxPath() sphinxPath, err := rt.ToSphinxPath()
if err != nil { if err != nil {
return nil, nil, err return nil, nil, err
@@ -2285,11 +2307,11 @@ type LightningPayment struct {
LastHop *route.Vertex LastHop *route.Vertex
// DestFeatures specifies the set of features we assume the final node // DestFeatures specifies the set of features we assume the final node
// has for pathfinding. Typically these will be taken directly from an // has for pathfinding. Typically, these will be taken directly from an
// invoice, but they can also be manually supplied or assumed by the // invoice, but they can also be manually supplied or assumed by the
// sender. If a nil feature vector is provided, the router will try to // sender. If a nil feature vector is provided, the router will try to
// fallback to the graph in order to load a feature vector for a node in // fall back to the graph in order to load a feature vector for a node
// the public graph. // in the public graph.
DestFeatures *lnwire.FeatureVector DestFeatures *lnwire.FeatureVector
// PaymentAddr is the payment address specified by the receiver. This // PaymentAddr is the payment address specified by the receiver. This
@@ -2387,18 +2409,16 @@ func (r *ChannelRouter) SendPayment(payment *LightningPayment) ([32]byte,
log.Tracef("Dispatching SendPayment for lightning payment: %v", log.Tracef("Dispatching SendPayment for lightning payment: %v",
spewPayment(payment)) spewPayment(payment))
// Since this is the first time this payment is being made, we pass nil
// for the existing attempt.
return r.sendPayment( return r.sendPayment(
payment.FeeLimit, payment.Identifier(), context.Background(), payment.FeeLimit, payment.Identifier(),
payment.PayAttemptTimeout, paySession, shardTracker, payment.PayAttemptTimeout, paySession, shardTracker,
) )
} }
// SendPaymentAsync is the non-blocking version of SendPayment. The payment // SendPaymentAsync is the non-blocking version of SendPayment. The payment
// result needs to be retrieved via the control tower. // result needs to be retrieved via the control tower.
func (r *ChannelRouter) SendPaymentAsync(payment *LightningPayment, func (r *ChannelRouter) SendPaymentAsync(ctx context.Context,
ps PaymentSession, st shards.ShardTracker) { payment *LightningPayment, ps PaymentSession, st shards.ShardTracker) {
// Since this is the first time this payment is being made, we pass nil // Since this is the first time this payment is being made, we pass nil
// for the existing attempt. // for the existing attempt.
@@ -2410,7 +2430,7 @@ func (r *ChannelRouter) SendPaymentAsync(payment *LightningPayment,
spewPayment(payment)) spewPayment(payment))
_, _, err := r.sendPayment( _, _, err := r.sendPayment(
payment.FeeLimit, payment.Identifier(), ctx, payment.FeeLimit, payment.Identifier(),
payment.PayAttemptTimeout, ps, st, payment.PayAttemptTimeout, ps, st,
) )
if err != nil { if err != nil {
@@ -2520,8 +2540,8 @@ func (r *ChannelRouter) sendToRoute(htlcHash lntypes.Hash, rt *route.Route,
// Calculate amount paid to receiver. // Calculate amount paid to receiver.
amt := rt.ReceiverAmt() amt := rt.ReceiverAmt()
// If this is meant as a MP payment shard, we set the amount // If this is meant as an MP payment shard, we set the amount for the
// for the creating info to the total amount of the payment. // creating info to the total amount of the payment.
finalHop := rt.Hops[len(rt.Hops)-1] finalHop := rt.Hops[len(rt.Hops)-1]
mpp := finalHop.MPP mpp := finalHop.MPP
if mpp != nil { if mpp != nil {
@@ -2575,7 +2595,7 @@ func (r *ChannelRouter) sendToRoute(htlcHash lntypes.Hash, rt *route.Route,
// Since the HTLC hashes and preimages are specified manually over the // Since the HTLC hashes and preimages are specified manually over the
// RPC for SendToRoute requests, we don't have to worry about creating // RPC for SendToRoute requests, we don't have to worry about creating
// a ShardTracker that can generate hashes for AMP payments. Instead we // a ShardTracker that can generate hashes for AMP payments. Instead, we
// create a simple tracker that can just return the hash for the single // create a simple tracker that can just return the hash for the single
// shard we'll now launch. // shard we'll now launch.
shardTracker := shards.NewSimpleShardTracker(htlcHash, nil) shardTracker := shards.NewSimpleShardTracker(htlcHash, nil)
@@ -2585,9 +2605,7 @@ func (r *ChannelRouter) sendToRoute(htlcHash lntypes.Hash, rt *route.Route,
// - nil payment session (since we already have a route). // - nil payment session (since we already have a route).
// - no payment timeout. // - no payment timeout.
// - no current block height. // - no current block height.
p := newPaymentLifecycle( p := newPaymentLifecycle(r, 0, paymentIdentifier, nil, shardTracker, 0)
r, 0, paymentIdentifier, nil, shardTracker, 0, 0,
)
// We found a route to try, create a new HTLC attempt to try. // We found a route to try, create a new HTLC attempt to try.
// //
@@ -2608,7 +2626,7 @@ func (r *ChannelRouter) sendToRoute(htlcHash lntypes.Hash, rt *route.Route,
return nil, err return nil, err
} }
// We now lookup the payment to see if it's already failed. // We now look up the payment to see if it's already failed.
payment, err := p.router.cfg.Control.FetchPayment(p.identifier) payment, err := p.router.cfg.Control.FetchPayment(p.identifier)
if err != nil { if err != nil {
return result.attempt, err return result.attempt, err
@@ -2677,15 +2695,27 @@ func (r *ChannelRouter) sendToRoute(htlcHash lntypes.Hash, rt *route.Route,
// returned. // returned.
// //
// This method relies on the ControlTower's internal payment state machine to // This method relies on the ControlTower's internal payment state machine to
// carry out its execution. After restarts it is safe, and assumed, that the // carry out its execution. After restarts, it is safe, and assumed, that the
// router will call this method for every payment still in-flight according to // router will call this method for every payment still in-flight according to
// the ControlTower. // the ControlTower.
func (r *ChannelRouter) sendPayment(feeLimit lnwire.MilliSatoshi, func (r *ChannelRouter) sendPayment(ctx context.Context,
identifier lntypes.Hash, timeout time.Duration, feeLimit lnwire.MilliSatoshi, identifier lntypes.Hash,
paySession PaymentSession, paymentAttemptTimeout time.Duration, paySession PaymentSession,
shardTracker shards.ShardTracker) ([32]byte, *route.Route, error) { shardTracker shards.ShardTracker) ([32]byte, *route.Route, error) {
// We'll also fetch the current block height so we can properly // If the user provides a timeout, we will additionally wrap the context
// in a deadline.
cancel := func() {}
if paymentAttemptTimeout > 0 {
ctx, cancel = context.WithTimeout(ctx, paymentAttemptTimeout)
}
// Since resumePayment is a blocking call, we'll cancel this
// context if the payment completes before the optional
// deadline.
defer cancel()
// We'll also fetch the current block height, so we can properly
// calculate the required HTLC time locks within the route. // calculate the required HTLC time locks within the route.
_, currentHeight, err := r.cfg.Chain.GetBestBlock() _, currentHeight, err := r.cfg.Chain.GetBestBlock()
if err != nil { if err != nil {
@@ -2695,11 +2725,11 @@ func (r *ChannelRouter) sendPayment(feeLimit lnwire.MilliSatoshi,
// Now set up a paymentLifecycle struct with these params, such that we // Now set up a paymentLifecycle struct with these params, such that we
// can resume the payment from the current state. // can resume the payment from the current state.
p := newPaymentLifecycle( p := newPaymentLifecycle(
r, feeLimit, identifier, paySession, r, feeLimit, identifier, paySession, shardTracker,
shardTracker, timeout, currentHeight, currentHeight,
) )
return p.resumePayment() return p.resumePayment(ctx)
} }
// extractChannelUpdate examines the error and extracts the channel update. // extractChannelUpdate examines the error and extracts the channel update.
@@ -3022,8 +3052,8 @@ func (r *ChannelRouter) IsStaleEdgePolicy(chanID lnwire.ShortChannelID,
} }
// As edges are directional edge node has a unique policy for the // As edges are directional edge node has a unique policy for the
// direction of the edge they control. Therefore we first check if we // direction of the edge they control. Therefore, we first check if we
// already have the most up to date information for that edge. If so, // already have the most up-to-date information for that edge. If so,
// then we can exit early. // then we can exit early.
switch { switch {
// A flag set of 0 indicates this is an announcement for the "first" // A flag set of 0 indicates this is an announcement for the "first"
@@ -3054,7 +3084,7 @@ type ErrNoChannel struct {
fromNode route.Vertex fromNode route.Vertex
} }
// Error returns a human readable string describing the error. // Error returns a human-readable string describing the error.
func (e ErrNoChannel) Error() string { func (e ErrNoChannel) Error() string {
return fmt.Sprintf("no matching outgoing channel available for "+ return fmt.Sprintf("no matching outgoing channel available for "+
"node %v (%v)", e.position, e.fromNode) "node %v (%v)", e.position, e.fromNode)