Skip to content
Merged
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
35 changes: 18 additions & 17 deletions macaroon_recipes.go
Original file line number Diff line number Diff line change
Expand Up @@ -47,23 +47,24 @@ var (
// implemented in lndclient and the value is the original name of the
// RPC method defined in the proto.
renames = map[string]string{
"ChannelBackup": "ExportChannelBackup",
"ChannelBackups": "ExportAllChannelBackups",
"ConfirmedWalletBalance": "WalletBalance",
"Connect": "ConnectPeer",
"DecodePaymentRequest": "DecodePayReq",
"ListTransactions": "GetTransactions",
"UpdateChanPolicy": "UpdateChannelPolicy",
"NetworkInfo": "GetNetworkInfo",
"SubscribeGraph": "SubscribeChannelGraph",
"InterceptHtlcs": "HtlcInterceptor",
"ImportMissionControl": "XImportMissionControl",
"EstimateFeeRate": "EstimateFee",
"EstimateFeeToP2WSH": "EstimateFee",
"OpenChannelStream": "OpenChannel",
"ListSweepsVerbose": "ListSweeps",
"MinRelayFee": "EstimateFee",
"SignOutputRawKeyLocator": "SignOutputRaw",
"ChannelBackup": "ExportChannelBackup",
"ChannelBackups": "ExportAllChannelBackups",
"ConfirmedWalletBalance": "WalletBalance",
"Connect": "ConnectPeer",
"DecodePaymentRequest": "DecodePayReq",
"ListTransactions": "GetTransactions",
"UpdateChanPolicy": "UpdateChannelPolicy",
"NetworkInfo": "GetNetworkInfo",
"SubscribeGraph": "SubscribeChannelGraph",
"InterceptHtlcs": "HtlcInterceptor",
"ImportMissionControl": "XImportMissionControl",
"EstimateFeeRate": "EstimateFee",
"EstimateFeeToP2WSH": "EstimateFee",
"EstimateRouteFeeWithProbe": "EstimateRouteFee",
"OpenChannelStream": "OpenChannel",
"ListSweepsVerbose": "ListSweeps",
"MinRelayFee": "EstimateFee",
"SignOutputRawKeyLocator": "SignOutputRaw",
}

// methodOverrides maps lndclient methods to their exact backing RPC
Expand Down
86 changes: 82 additions & 4 deletions router_client.go
Original file line number Diff line number Diff line change
Expand Up @@ -49,6 +49,19 @@ type RouterClient interface {
EstimateRouteFee(ctx context.Context, dest route.Vertex,
amt btcutil.Amount) (lnwire.MilliSatoshi, error)

// EstimateRouteFeeWithProbe estimates routing costs by probing with an
// invoice. The timeout parameter bounds the probe inside lnd; a zero
// timeout lets lnd choose its default, currently 60 seconds. Do not rely
// on the context deadline or cancellation to bound the probe itself.
//
// A nil error does not imply that a route was found. Callers must check
// that the response FailureReason is FAILURE_REASON_NONE before trusting
// RoutingFee or TimeLockDelay. Zero-amount invoices are rejected by lnd
// and returned as an error. If a probe unexpectedly succeeds, lnd returns
// an RPC error instead of a response.
EstimateRouteFeeWithProbe(ctx context.Context, invoice string,
timeout time.Duration) (*EstimateRouteFeeWithProbeResponse, error)

// SubscribeHtlcEvents subscribes to a stream of htlc events from the
// router.
SubscribeHtlcEvents(ctx context.Context) (<-chan *routerrpc.HtlcEvent,
Expand Down Expand Up @@ -319,6 +332,23 @@ type SendPaymentRequest struct {
FirstHopCustomRecords map[uint64][]byte
}

// EstimateRouteFeeWithProbeResponse is the response of a route fee estimate
// probe.
type EstimateRouteFeeWithProbeResponse struct {
// RoutingFee is a lower bound of the estimated fee to the target
// destination within the network.
RoutingFee lnwire.MilliSatoshi

// TimeLockDelay is an estimate of the worst-case time delay that can
// occur. Callers still need to factor in the final CLTV delta of the
// last hop into this value.
TimeLockDelay int64

// FailureReason indicates whether a probing payment succeeded or
// whether and why it failed. FAILURE_REASON_NONE indicates success.
FailureReason lnrpc.PaymentFailureReason
}

// InterceptedHtlc contains information about a htlc that was intercepted in
// lnd's switch.
type InterceptedHtlc struct {
Expand Down Expand Up @@ -741,18 +771,66 @@ func (r *routerClient) trackPayment(ctx context.Context,
func (r *routerClient) EstimateRouteFee(ctx context.Context, dest route.Vertex,
amt btcutil.Amount) (lnwire.MilliSatoshi, error) {

rpcCtx := r.routerKitMac.WithMacaroonAuth(ctx)
rpcReq := &routerrpc.RouteFeeRequest{
res, err := r.estimateRouteFee(ctx, &routerrpc.RouteFeeRequest{
Dest: dest[:],
AmtSat: int64(amt),
})
if err != nil {
return 0, err
}

return res.RoutingFee, nil
}

// EstimateRouteFeeWithProbe estimates routing costs by probing with an invoice.
// The timeout parameter bounds the probe inside lnd; a zero timeout lets lnd
// choose its default, currently 60 seconds. Do not rely on the context deadline
// or cancellation to bound the probe itself.
//
// A nil error does not imply that a route was found. Callers must check that
// the response FailureReason is FAILURE_REASON_NONE before trusting RoutingFee
// or TimeLockDelay. Zero-amount invoices are rejected by lnd and returned as an
// error. If a probe unexpectedly succeeds, lnd returns an RPC error instead of
// a response.
func (r *routerClient) EstimateRouteFeeWithProbe(ctx context.Context,
invoice string, timeout time.Duration) (*EstimateRouteFeeWithProbeResponse,
error) {

if timeout < 0 {
return nil, fmt.Errorf("timeout must not be negative")
}

rpcReq := &routerrpc.RouteFeeRequest{
PaymentRequest: invoice,
}
if timeout > 0 {
Comment thread
hieblmi marked this conversation as resolved.
const maxTimeout = time.Duration(^uint32(0)) * time.Second
if timeout > maxTimeout {
return nil, fmt.Errorf("timeout exceeds maximum of %v",
maxTimeout)
}

rpcReq.Timeout = uint32((timeout + time.Second - 1) / time.Second)
}

return r.estimateRouteFee(ctx, rpcReq)
}

func (r *routerClient) estimateRouteFee(ctx context.Context,
rpcReq *routerrpc.RouteFeeRequest) (*EstimateRouteFeeWithProbeResponse,
error) {

rpcCtx := r.routerKitMac.WithMacaroonAuth(ctx)
rpcRes, err := r.client.EstimateRouteFee(rpcCtx, rpcReq)
if err != nil {
return 0, err
return nil, err
}

return lnwire.MilliSatoshi(rpcRes.RoutingFeeMsat), nil
return &EstimateRouteFeeWithProbeResponse{
RoutingFee: lnwire.MilliSatoshi(rpcRes.RoutingFeeMsat),
TimeLockDelay: rpcRes.TimeLockDelay,
FailureReason: rpcRes.FailureReason,
}, nil
Comment thread
hieblmi marked this conversation as resolved.
}

// unmarshallPaymentStatus converts an rpc status update to the PaymentStatus
Expand Down
137 changes: 137 additions & 0 deletions router_client_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,137 @@
package lndclient

import (
"context"
"testing"
"time"

"github.com/btcsuite/btcd/btcutil"
"github.com/lightningnetwork/lnd/lnrpc"
"github.com/lightningnetwork/lnd/lnrpc/routerrpc"
"github.com/lightningnetwork/lnd/lnwire"
"github.com/lightningnetwork/lnd/routing/route"
"github.com/stretchr/testify/require"
"google.golang.org/grpc"
)

type mockRouterRPCClient struct {
routerrpc.RouterClient

request *routerrpc.RouteFeeRequest
response *routerrpc.RouteFeeResponse
err error
}

func (m *mockRouterRPCClient) EstimateRouteFee(_ context.Context,
request *routerrpc.RouteFeeRequest, _ ...grpc.CallOption) (
*routerrpc.RouteFeeResponse, error) {

m.request = request
return m.response, m.err
}

// TestEstimateRouteFeeWithProbe checks that invoice-based route fee estimates
// are mapped to lnd's RouteFeeRequest fields and response values.
func TestEstimateRouteFeeWithProbe(t *testing.T) {
t.Parallel()

mock := &mockRouterRPCClient{
response: &routerrpc.RouteFeeResponse{
RoutingFeeMsat: 987,
TimeLockDelay: 654,
FailureReason: lnrpc.PaymentFailureReason_FAILURE_REASON_NONE,
},
}
client := &routerClient{
client: mock,
}

resp, err := client.EstimateRouteFeeWithProbe(
t.Context(), "lnbc1...", 1500*time.Millisecond,
)
require.NoError(t, err)

require.Empty(t, mock.request.Dest)
require.Zero(t, mock.request.AmtSat)
require.Equal(t, "lnbc1...", mock.request.PaymentRequest)
require.Equal(t, uint32(2), mock.request.Timeout)
require.Equal(t, lnwire.MilliSatoshi(987), resp.RoutingFee)
require.Equal(t, int64(654), resp.TimeLockDelay)
require.Equal(
t, lnrpc.PaymentFailureReason_FAILURE_REASON_NONE,
resp.FailureReason,
)
}

// TestEstimateRouteFee checks that destination and amount route fee estimates
// are mapped to lnd's RouteFeeRequest fields.
func TestEstimateRouteFee(t *testing.T) {
t.Parallel()

dest := testVertex()
mock := &mockRouterRPCClient{
response: &routerrpc.RouteFeeResponse{
RoutingFeeMsat: 4321,
},
}
client := &routerClient{
client: mock,
}

fee, err := client.EstimateRouteFee(
t.Context(), dest, btcutil.Amount(1000),
)
require.NoError(t, err)

require.Equal(t, dest[:], mock.request.Dest)
require.Equal(t, int64(1000), mock.request.AmtSat)
require.Equal(t, lnwire.MilliSatoshi(4321), fee)
}

// TestEstimateRouteFeeWithProbeRejectsInvalidTimeout checks that invalid probe
// timeout values are rejected before making the RPC.
func TestEstimateRouteFeeWithProbeRejectsInvalidTimeout(t *testing.T) {
t.Parallel()

testCases := []struct {
name string
timeout time.Duration
err string
}{
{
name: "negative",
timeout: -time.Second,
err: "timeout must not be negative",
},
{
name: "too large",
timeout: time.Duration(^uint32(0))*time.Second +
time.Nanosecond,
err: "timeout exceeds maximum",
},
}

for _, tc := range testCases {
t.Run(tc.name, func(t *testing.T) {
mock := &mockRouterRPCClient{}
client := &routerClient{
client: mock,
}

_, err := client.EstimateRouteFeeWithProbe(
t.Context(), "lnbc1...", tc.timeout,
)
require.ErrorContains(t, err, tc.err)
require.Nil(t, mock.request)
})
}
}

func testVertex() route.Vertex {
var vertex route.Vertex
for i := range vertex {
vertex[i] = byte(i + 1)
}

return vertex
}
Loading