Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
17 changes: 17 additions & 0 deletions cmd/commands/cmd_payments.go
Original file line number Diff line number Diff line change
Expand Up @@ -1974,6 +1974,14 @@ var estimateRouteFeeCommand = cli.Command{
"applicable if pay_req is specified.",
Comment thread
asheswook marked this conversation as resolved.
Value: paymentTimeout,
},
cli.StringSliceFlag{
Name: "outgoing_chan_id",
Usage: "short channel id of the outgoing channel to " +
"use for the first hop of the fee estimation; " +
"can be specified multiple times in the same " +
"command",
Value: &cli.StringSlice{},
},
},
}

Expand Down Expand Up @@ -2020,6 +2028,15 @@ func estimateRouteFee(ctx *cli.Context) error {
return fmt.Errorf("fee estimation arguments missing")
}

var (
err error
outChanIDs = ctx.StringSlice("outgoing_chan_id")
)
req.OutgoingChanIds, err = parseChanIDs(outChanIDs)
if err != nil {
return fmt.Errorf("unable to decode outgoing_chan_id: %w", err)
}

resp, err := client.EstimateRouteFee(ctxc, req)
if err != nil {
return err
Expand Down
46 changes: 41 additions & 5 deletions itest/lnd_estimate_route_fee_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,8 @@ var (
)

const (
ErrNoRouteInGraph = "unable to find a path to destination"
ErrNoRouteInGraph = "unable to find a path to destination"
ErrInsufficientBalance = "insufficient local balance"
)

type estimateRouteFeeTestCase struct {
Expand All @@ -41,6 +42,10 @@ type estimateRouteFeeTestCase struct {
// routeHints are the route hints that will be used for the probe.
routeHints []*lnrpc.RouteHint

// outgoingChanIds is the list of channel IDs allowed for the first
// hop. If empty, any channel may be used.
outgoingChanIds []uint64

// expectedRoutingFeesMsat are the expected routing fees that will be
// returned by the probe.
expectedRoutingFeesMsat int64
Expand Down Expand Up @@ -154,6 +159,11 @@ func testEstimateRouteFee(ht *lntest.HarnessTest) {
require.Len(ht, davesPrivChannels.Channels, 1)
daveFrankChanID := davesPrivChannels.Channels[0].ChanId

channelAliceCarol := ht.QueryChannelByChanPoint(
mts.alice, mts.channelPoints[0],
)
aliceCarolChanID := channelAliceCarol.ChanId

// Let's disable the paths from Alice to Bob through Dave and Eve with
// high fees. This ensures that the path estimates are based on Carol's
// channel to Bob for the first set of tests.
Expand Down Expand Up @@ -441,6 +451,30 @@ func testEstimateRouteFee(ht *lntest.HarnessTest) {
highestFeeRouteDelta,
expectedFailureReason: failureReasonNone,
},
// Test graph-based estimation with a specific outgoing channel.
// We specify the Alice-Carol channel, so the route should go
// through Carol to Bob.
{
name: "graph based estimate with " +
"outgoing channel",
probing: false,
destination: mts.bob,
outgoingChanIds: []uint64{aliceCarolChanID},
expectedRoutingFeesMsat: feeStandardSingleHop,
expectedCltvDelta: locktime + deltaCB,
expectedFailureReason: failureReasonNone,
},
// Test graph-based estimation with a non-existent outgoing
// channel. This should fail because the specified channel
// doesn't exist and has no balance.
{
name: "graph based estimate with " +
"invalid outgoing channel",
probing: false,
destination: mts.bob,
outgoingChanIds: []uint64{999999999},
expectedError: ErrInsufficientBalance,
},
}

for _, testCase := range testCases {
Expand All @@ -467,13 +501,15 @@ func runFeeEstimationTestCase(ht *lntest.HarnessTest,
tc.destination, probeAmount, 1, tc.routeHints...,
)
feeReq = &routerrpc.RouteFeeRequest{
PaymentRequest: payReqs[0],
Timeout: uint32(wait.PaymentTimeout.Seconds()),
PaymentRequest: payReqs[0],
Timeout: uint32(wait.PaymentTimeout.Seconds()),
OutgoingChanIds: tc.outgoingChanIds,
}
} else {
feeReq = &routerrpc.RouteFeeRequest{
Dest: tc.destination.PubKey[:],
AmtSat: int64(probeAmount),
Dest: tc.destination.PubKey[:],
AmtSat: int64(probeAmount),
OutgoingChanIds: tc.outgoingChanIds,
}
}

Expand Down
8 changes: 8 additions & 0 deletions lnrpc/routerrpc/router.proto
Original file line number Diff line number Diff line change
Expand Up @@ -431,6 +431,14 @@ message RouteFeeRequest {
controlled by the timeout parameter.
*/
uint32 timeout = 4;

/*
The channel ids of the channels that are allowed for the first hop. If
empty, any channel may be used. This field is applicable to both
graph-based fee estimation (using dest + amt_sat) and probe-based
estimation (using payment_request).
*/
repeated uint64 outgoing_chan_ids = 5;
}

message RouteFeeResponse {
Expand Down
8 changes: 8 additions & 0 deletions lnrpc/routerrpc/router.swagger.json
Original file line number Diff line number Diff line change
Expand Up @@ -1906,6 +1906,14 @@
"type": "integer",
"format": "int64",
"description": "A user preference of how long a probe payment should maximally be allowed to\ntake, denoted in seconds. The probing payment loop is aborted if this\ntimeout is reached. Note that the probing process itself can take longer\nthan the timeout if the HTLC becomes delayed or stuck. Canceling the context\nof this call will not cancel the payment loop, the duration is only\ncontrolled by the timeout parameter."
},
"outgoing_chan_ids": {
"type": "array",
"items": {
"type": "string",
"format": "uint64"
},
"description": "The channel ids of the channels that are allowed for the first hop. If\nempty, any channel may be used. This field is applicable to both\ngraph-based fee estimation (using dest + amt_sat) and probe-based\nestimation (using payment_request)."
}
}
},
Expand Down
19 changes: 12 additions & 7 deletions lnrpc/routerrpc/router_server.go
Original file line number Diff line number Diff line change
Expand Up @@ -444,12 +444,15 @@ func (s *Server) EstimateRouteFee(ctx context.Context,
return nil, errors.New("amount must be greater than 0")

default:
return s.probeDestination(req.Dest, req.AmtSat)
return s.probeDestination(
req.Dest, req.AmtSat, req.OutgoingChanIds,
)
Comment thread
asheswook marked this conversation as resolved.
}

case isProbeInvoice:
return s.probePaymentRequest(
ctx, req.PaymentRequest, req.Timeout,
req.OutgoingChanIds,
)
}

Expand All @@ -458,8 +461,8 @@ func (s *Server) EstimateRouteFee(ctx context.Context,

// probeDestination estimates fees along a route to a destination based on the
// contents of the local graph.
func (s *Server) probeDestination(dest []byte, amtSat int64) (*RouteFeeResponse,
error) {
func (s *Server) probeDestination(dest []byte, amtSat int64,
outgoingChanIDs []uint64) (*RouteFeeResponse, error) {
Comment thread
asheswook marked this conversation as resolved.

destNode, err := route.NewVertexFromBytes(dest)
if err != nil {
Expand All @@ -478,9 +481,10 @@ func (s *Server) probeDestination(dest []byte, amtSat int64) (*RouteFeeResponse,
routeReq, err := routing.NewRouteRequest(
s.cfg.RouterBackend.SelfNode, &destNode, amtMsat, 0,
&routing.RestrictParams{
FeeLimit: routeFeeLimitSat,
CltvLimit: s.cfg.RouterBackend.MaxTotalTimelock,
ProbabilitySource: mc.GetProbability,
FeeLimit: routeFeeLimitSat,
CltvLimit: s.cfg.RouterBackend.MaxTotalTimelock,
ProbabilitySource: mc.GetProbability,
OutgoingChannelIDs: outgoingChanIDs,
}, nil, nil, nil, s.cfg.RouterBackend.DefaultFinalCltvDelta,
)
if err != nil {
Expand Down Expand Up @@ -518,7 +522,7 @@ func (s *Server) probeDestination(dest []byte, amtSat int64) (*RouteFeeResponse,
// identify LSPs, the probe payment might use a different node id as the
// final destination (the assumed LSP node id).
func (s *Server) probePaymentRequest(ctx context.Context, paymentRequest string,
timeout uint32) (*RouteFeeResponse, error) {
timeout uint32, outgoingChanIDs []uint64) (*RouteFeeResponse, error) {

payReq, err := zpay32.Decode(
paymentRequest, s.cfg.RouterBackend.ActiveNetParams,
Expand Down Expand Up @@ -552,6 +556,7 @@ func (s *Server) probePaymentRequest(ctx context.Context, paymentRequest string,
FeeLimitSat: routeFeeLimitSat,
FinalCltvDelta: int32(payReq.MinFinalCLTVExpiry()),
DestFeatures: MarshalFeatures(payReq.Features),
OutgoingChanIds: outgoingChanIDs,
}

// If the payment addresses is specified, then we'll also populate that
Expand Down
Loading