mirror of
https://github.com/lightningnetwork/lnd.git
synced 2024-11-19 01:43:16 +01:00
routerrpc+zpay32: EstimateRouteFee overhaul
In this commit the mission control based fee estimation is supplemented with a payment probe estimation which can lead to more accurate estimation results. The probing utilizes a hop-hint heurisic to detect routes through LSPs in order to manually estimate fees to destinations behind an LSP that would otherwise block the payment probe.
This commit is contained in:
parent
24080c51f9
commit
7d9589ecbf
@ -1175,8 +1175,18 @@ func unmarshallHopHint(rpcHint *lnrpc.HopHint) (zpay32.HopHint, error) {
|
||||
}, nil
|
||||
}
|
||||
|
||||
// MarshalFeatures converts a feature vector into a list of uint32's.
|
||||
func MarshalFeatures(feats *lnwire.FeatureVector) []lnrpc.FeatureBit {
|
||||
var featureBits []lnrpc.FeatureBit
|
||||
for feature := range feats.Features() {
|
||||
featureBits = append(featureBits, lnrpc.FeatureBit(feature))
|
||||
}
|
||||
|
||||
return featureBits
|
||||
}
|
||||
|
||||
// UnmarshalFeatures converts a list of uint32's into a valid feature vector.
|
||||
// This method checks that feature bit pairs aren't assigned toegether, and
|
||||
// This method checks that feature bit pairs aren't assigned together, and
|
||||
// validates transitive dependencies.
|
||||
func UnmarshalFeatures(
|
||||
rpcFeatures []lnrpc.FeatureBit) (*lnwire.FeatureVector, error) {
|
||||
|
@ -1,7 +1,9 @@
|
||||
package routerrpc
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"context"
|
||||
crand "crypto/rand"
|
||||
"errors"
|
||||
"fmt"
|
||||
"io/ioutil"
|
||||
@ -15,11 +17,13 @@ import (
|
||||
"github.com/grpc-ecosystem/grpc-gateway/v2/runtime"
|
||||
"github.com/lightningnetwork/lnd/channeldb"
|
||||
"github.com/lightningnetwork/lnd/lnrpc"
|
||||
"github.com/lightningnetwork/lnd/lnrpc/invoicesrpc"
|
||||
"github.com/lightningnetwork/lnd/lntypes"
|
||||
"github.com/lightningnetwork/lnd/lnwire"
|
||||
"github.com/lightningnetwork/lnd/macaroons"
|
||||
"github.com/lightningnetwork/lnd/routing"
|
||||
"github.com/lightningnetwork/lnd/routing/route"
|
||||
"github.com/lightningnetwork/lnd/zpay32"
|
||||
"google.golang.org/grpc"
|
||||
"google.golang.org/grpc/codes"
|
||||
"google.golang.org/grpc/status"
|
||||
@ -31,16 +35,26 @@ const (
|
||||
// to register ourselves, and we also require that the main
|
||||
// SubServerConfigDispatcher instance recognize as the name of our
|
||||
subServerName = "RouterRPC"
|
||||
|
||||
// routeFeeLimitSat is the maximum routing fee that we allow to occur
|
||||
// when estimating a routing fee.
|
||||
routeFeeLimitSat = 100_000_000
|
||||
)
|
||||
|
||||
var (
|
||||
errServerShuttingDown = errors.New("routerrpc server shutting down")
|
||||
|
||||
// ErrInterceptorAlreadyExists is an error returned when the a new stream
|
||||
// is opened and there is already one active interceptor.
|
||||
// The user must disconnect prior to open another stream.
|
||||
// ErrInterceptorAlreadyExists is an error returned when a new stream is
|
||||
// opened and there is already one active interceptor. The user must
|
||||
// disconnect prior to open another stream.
|
||||
ErrInterceptorAlreadyExists = errors.New("interceptor already exists")
|
||||
|
||||
errMissingPaymentAttempt = errors.New("missing payment attempt")
|
||||
|
||||
errMissingRoute = errors.New("missing route")
|
||||
|
||||
errUnexpectedFailureSource = errors.New("unexpected failure source")
|
||||
|
||||
// macaroonOps are the set of capabilities that our minted macaroon (if
|
||||
// it doesn't already exist) will have.
|
||||
macaroonOps = []bakery.Op{
|
||||
@ -356,25 +370,58 @@ func (s *Server) SendPaymentV2(req *SendPaymentRequest,
|
||||
)
|
||||
}
|
||||
|
||||
// EstimateRouteFee allows callers to obtain a lower bound w.r.t how much it
|
||||
// may cost to send an HTLC to the target end destination.
|
||||
// EstimateRouteFee allows callers to obtain an expected value w.r.t how much it
|
||||
// may cost to send an HTLC to the target end destination. This method sends
|
||||
// probe payments to the target node, based on target invoice parameters and a
|
||||
// random payment hash that makes it impossible for the target to settle the
|
||||
// htlc. The probing stops if a user-provided timeout is reached. If provided
|
||||
// with a destination key and amount, this method will perform a local graph
|
||||
// based fee estimation.
|
||||
func (s *Server) EstimateRouteFee(ctx context.Context,
|
||||
req *RouteFeeRequest) (*RouteFeeResponse, error) {
|
||||
|
||||
if len(req.Dest) != 33 {
|
||||
isProbeDestination := len(req.Dest) > 0
|
||||
isProbeInvoice := len(req.PaymentRequest) > 0
|
||||
|
||||
switch {
|
||||
case isProbeDestination == isProbeInvoice:
|
||||
return nil, errors.New("specify either a destination or an " +
|
||||
"invoice")
|
||||
|
||||
case isProbeDestination:
|
||||
switch {
|
||||
case len(req.Dest) != 33:
|
||||
return nil, errors.New("invalid length destination key")
|
||||
|
||||
case req.AmtSat <= 0:
|
||||
return nil, errors.New("amount must be greater than 0")
|
||||
|
||||
default:
|
||||
return s.probeDestination(req.Dest, req.AmtSat)
|
||||
}
|
||||
|
||||
case isProbeInvoice:
|
||||
return s.probePaymentRequest(
|
||||
ctx, req.PaymentRequest, req.Timeout,
|
||||
)
|
||||
}
|
||||
|
||||
return &RouteFeeResponse{}, nil
|
||||
}
|
||||
|
||||
// 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) {
|
||||
|
||||
destNode, err := route.NewVertexFromBytes(dest)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
var destNode route.Vertex
|
||||
copy(destNode[:], req.Dest)
|
||||
|
||||
// Next, we'll convert the amount in satoshis to mSAT, which are the
|
||||
// native unit of LN.
|
||||
amtMsat := lnwire.NewMSatFromSatoshis(btcutil.Amount(req.AmtSat))
|
||||
|
||||
// Pick a fee limit
|
||||
//
|
||||
// TODO: Change this into behaviour that makes more sense.
|
||||
feeLimit := lnwire.NewMSatFromSatoshis(btcutil.SatoshiPerBitcoin)
|
||||
amtMsat := lnwire.NewMSatFromSatoshis(btcutil.Amount(amtSat))
|
||||
|
||||
// Finally, we'll query for a route to the destination that can carry
|
||||
// that target amount, we'll only request a single route. Set a
|
||||
@ -384,7 +431,7 @@ func (s *Server) EstimateRouteFee(ctx context.Context,
|
||||
routeReq, err := routing.NewRouteRequest(
|
||||
s.cfg.RouterBackend.SelfNode, &destNode, amtMsat, 0,
|
||||
&routing.RestrictParams{
|
||||
FeeLimit: feeLimit,
|
||||
FeeLimit: routeFeeLimitSat,
|
||||
CltvLimit: s.cfg.RouterBackend.MaxTotalTimelock,
|
||||
ProbabilitySource: mc.GetProbability,
|
||||
}, nil, nil, nil, s.cfg.RouterBackend.DefaultFinalCltvDelta,
|
||||
@ -398,14 +445,379 @@ func (s *Server) EstimateRouteFee(ctx context.Context,
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// We are adding a block padding to the total time lock to account for
|
||||
// the safety buffer that the payment session will add to the last hop's
|
||||
// cltv delta. This is to prevent the htlc from failing if blocks are
|
||||
// mined while it is in flight.
|
||||
timeLockDelay := route.TotalTimeLock + uint32(routing.BlockPadding)
|
||||
|
||||
return &RouteFeeResponse{
|
||||
RoutingFeeMsat: int64(route.TotalFees()),
|
||||
TimeLockDelay: int64(route.TotalTimeLock),
|
||||
TimeLockDelay: int64(timeLockDelay),
|
||||
FailureReason: lnrpc.PaymentFailureReason_FAILURE_REASON_NONE,
|
||||
}, nil
|
||||
}
|
||||
|
||||
// SendToRouteV2 sends a payment through a predefined route. The response of this
|
||||
// call contains structured error information.
|
||||
// probePaymentRequest estimates fees along a route to a destination that is
|
||||
// specified in an invoice. The estimation duration is limited by a timeout. In
|
||||
// case that route hints are provided, this method applies a heuristic to
|
||||
// identify LSPs which might block probe payments. In that case, fees are
|
||||
// manually calculated and added to the probed fee estimation up until the LSP
|
||||
// node. If the route hints don't indicate an LSP, they are passed as arguments
|
||||
// to the SendPayment_V2 method, which enable it to send probe payments to the
|
||||
// payment request destination.
|
||||
func (s *Server) probePaymentRequest(ctx context.Context, paymentRequest string,
|
||||
timeout uint32) (*RouteFeeResponse, error) {
|
||||
|
||||
payReq, err := zpay32.Decode(
|
||||
paymentRequest, s.cfg.RouterBackend.ActiveNetParams,
|
||||
)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
if *payReq.MilliSat <= 0 {
|
||||
return nil, errors.New("payment request amount must be " +
|
||||
"greater than 0")
|
||||
}
|
||||
|
||||
// Generate random payment hash, so we can be sure that the target of
|
||||
// the probe payment doesn't have the preimage to settle the htlc.
|
||||
var paymentHash lntypes.Hash
|
||||
_, err = crand.Read(paymentHash[:])
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("cannot generate random probe "+
|
||||
"preimage: %w", err)
|
||||
}
|
||||
|
||||
amtMsat := int64(*payReq.MilliSat)
|
||||
probeRequest := &SendPaymentRequest{
|
||||
TimeoutSeconds: int32(timeout),
|
||||
Dest: payReq.Destination.SerializeCompressed(),
|
||||
MaxParts: 1,
|
||||
AllowSelfPayment: false,
|
||||
AmtMsat: amtMsat,
|
||||
PaymentHash: paymentHash[:],
|
||||
FeeLimitSat: routeFeeLimitSat,
|
||||
PaymentAddr: payReq.PaymentAddr[:],
|
||||
FinalCltvDelta: int32(payReq.MinFinalCLTVExpiry()),
|
||||
DestFeatures: MarshalFeatures(payReq.Features),
|
||||
}
|
||||
|
||||
hints := payReq.RouteHints
|
||||
|
||||
// If the hints don't indicate an LSP then chances are that our probe
|
||||
// payment won't be blocked along the route to the destination. We send
|
||||
// a probe payment with unmodified route hints.
|
||||
if !isLSP(hints) {
|
||||
probeRequest.RouteHints = invoicesrpc.CreateRPCRouteHints(hints)
|
||||
return s.sendProbePayment(ctx, probeRequest)
|
||||
}
|
||||
|
||||
// If the heuristic indicates an LSP we modify the route hints to allow
|
||||
// probing the LSP.
|
||||
lspAdjustedRouteHints, lspHint, err := prepareLspRouteHints(
|
||||
hints, *payReq.MilliSat,
|
||||
)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// The adjusted route hints serve the payment probe to find the last
|
||||
// public hop to the LSP on the route.
|
||||
probeRequest.Dest = lspHint.NodeID.SerializeCompressed()
|
||||
if len(lspAdjustedRouteHints) > 0 {
|
||||
probeRequest.RouteHints = invoicesrpc.CreateRPCRouteHints(
|
||||
lspAdjustedRouteHints,
|
||||
)
|
||||
}
|
||||
|
||||
// The payment probe will be able to calculate the fee up until the LSP
|
||||
// node. The fee of the last hop has to be calculated manually. Since
|
||||
// the last hop's fee amount has to be sent across the payment path we
|
||||
// have to add it to the original payment amount. Only then will the
|
||||
// payment probe be able to determine the correct fee to the last hop
|
||||
// prior to the private destination. For example, if the user wants to
|
||||
// send 1000 sats to a private destination and the last hop's fee is 10
|
||||
// sats, then 1010 sats will have to arrive at the last hop. This means
|
||||
// that the probe has to be dispatched with 1010 sats to correctly
|
||||
// calculate the routing fee.
|
||||
//
|
||||
// Calculate the hop fee for the last hop manually.
|
||||
hopFee := lspHint.HopFee(*payReq.MilliSat)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// Add the last hop's fee to the requested payment amount that we want
|
||||
// to get an estimate for.
|
||||
probeRequest.AmtMsat += int64(hopFee)
|
||||
|
||||
// Use the hop hint's cltv delta as the payment request's final cltv
|
||||
// delta. The actual final cltv delta of the invoice will be added to
|
||||
// the payment probe's cltv delta.
|
||||
probeRequest.FinalCltvDelta = int32(lspHint.CLTVExpiryDelta)
|
||||
|
||||
// Dispatch the payment probe with adjusted fee amount.
|
||||
resp, err := s.sendProbePayment(ctx, probeRequest)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// If the payment probe failed we only return the failure reason and
|
||||
// leave the probe result params unaltered.
|
||||
if resp.FailureReason != lnrpc.PaymentFailureReason_FAILURE_REASON_NONE { //nolint:lll
|
||||
return resp, nil
|
||||
}
|
||||
|
||||
// The probe succeeded, so we can add the last hop's fee to fee the
|
||||
// payment probe returned.
|
||||
resp.RoutingFeeMsat += int64(hopFee)
|
||||
|
||||
// Add the final cltv delta of the invoice to the payment probe's total
|
||||
// cltv delta. This is the cltv delta for the hop behind the LSP.
|
||||
resp.TimeLockDelay += int64(payReq.MinFinalCLTVExpiry())
|
||||
|
||||
return resp, nil
|
||||
}
|
||||
|
||||
// isLSP checks if the route hints indicate an LSP. An LSP is indicated with
|
||||
// true if the last node in each route hint has the same node id, false
|
||||
// otherwise.
|
||||
func isLSP(routeHints [][]zpay32.HopHint) bool {
|
||||
if len(routeHints) == 0 || len(routeHints[0]) == 0 {
|
||||
return false
|
||||
}
|
||||
|
||||
refNodeID := routeHints[0][len(routeHints[0])-1].NodeID
|
||||
for i := 1; i < len(routeHints); i++ {
|
||||
// Skip empty route hints.
|
||||
if len(routeHints[i]) == 0 {
|
||||
continue
|
||||
}
|
||||
|
||||
lastHop := routeHints[i][len(routeHints[i])-1]
|
||||
idMatchesRefNode := bytes.Equal(
|
||||
lastHop.NodeID.SerializeCompressed(),
|
||||
refNodeID.SerializeCompressed(),
|
||||
)
|
||||
if !idMatchesRefNode {
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
return true
|
||||
}
|
||||
|
||||
// prepareLspRouteHints assumes that the isLsp heuristic returned true for the
|
||||
// route hints passed in here. It constructs a modified list of route hints that
|
||||
// allows the caller to probe the LSP, which itself is returned as a separate
|
||||
// hop hint.
|
||||
func prepareLspRouteHints(routeHints [][]zpay32.HopHint,
|
||||
amt lnwire.MilliSatoshi) ([][]zpay32.HopHint, *zpay32.HopHint, error) {
|
||||
|
||||
if len(routeHints) == 0 {
|
||||
return nil, nil, fmt.Errorf("no route hints provided")
|
||||
}
|
||||
|
||||
// Create the LSP hop hint. We are probing for the worst case fee and
|
||||
// cltv delta. So we look for the max values amongst all LSP hop hints.
|
||||
refHint := routeHints[0][len(routeHints[0])-1]
|
||||
refHint.CLTVExpiryDelta = maxLspCltvDelta(routeHints)
|
||||
refHint.FeeBaseMSat, refHint.FeeProportionalMillionths = maxLspFee(
|
||||
routeHints, amt,
|
||||
)
|
||||
|
||||
// We construct a modified list of route hints that allows the caller to
|
||||
// probe the LSP.
|
||||
adjustedHints := make([][]zpay32.HopHint, 0, len(routeHints))
|
||||
|
||||
// Strip off the LSP hop hint from all route hints.
|
||||
for i := 0; i < len(routeHints); i++ {
|
||||
hint := routeHints[i]
|
||||
if len(hint) > 1 {
|
||||
adjustedHints = append(
|
||||
adjustedHints, hint[:len(hint)-1],
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
return adjustedHints, &refHint, nil
|
||||
}
|
||||
|
||||
// maxLspFee returns base fee and fee rate amongst all LSP route hints that
|
||||
// results in the overall highest fee for the given amount.
|
||||
func maxLspFee(routeHints [][]zpay32.HopHint, amt lnwire.MilliSatoshi) (uint32,
|
||||
uint32) {
|
||||
|
||||
var maxFeePpm uint32
|
||||
var maxBaseFee uint32
|
||||
var maxTotalFee lnwire.MilliSatoshi
|
||||
for _, rh := range routeHints {
|
||||
lastHop := rh[len(rh)-1]
|
||||
lastHopFee := lastHop.HopFee(amt)
|
||||
if lastHopFee > maxTotalFee {
|
||||
maxTotalFee = lastHopFee
|
||||
maxBaseFee = lastHop.FeeBaseMSat
|
||||
maxFeePpm = lastHop.FeeProportionalMillionths
|
||||
}
|
||||
}
|
||||
|
||||
return maxBaseFee, maxFeePpm
|
||||
}
|
||||
|
||||
// maxLspCltvDelta returns the maximum cltv delta amongst all LSP route hints.
|
||||
func maxLspCltvDelta(routeHints [][]zpay32.HopHint) uint16 {
|
||||
var maxCltvDelta uint16
|
||||
for _, rh := range routeHints {
|
||||
rhLastHop := rh[len(rh)-1]
|
||||
if rhLastHop.CLTVExpiryDelta > maxCltvDelta {
|
||||
maxCltvDelta = rhLastHop.CLTVExpiryDelta
|
||||
}
|
||||
}
|
||||
|
||||
return maxCltvDelta
|
||||
}
|
||||
|
||||
// probePaymentStream is a custom implementation of the grpc.ServerStream
|
||||
// interface. It is used to send payment status updates to the caller on the
|
||||
// stream channel.
|
||||
type probePaymentStream struct {
|
||||
Router_SendPaymentV2Server
|
||||
|
||||
stream chan *lnrpc.Payment
|
||||
ctx context.Context //nolint:containedctx
|
||||
}
|
||||
|
||||
// Send sends a payment status update to a payment stream that the caller can
|
||||
// evaluate.
|
||||
func (p *probePaymentStream) Send(response *lnrpc.Payment) error {
|
||||
select {
|
||||
case p.stream <- response:
|
||||
|
||||
case <-p.ctx.Done():
|
||||
return p.ctx.Err()
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// Context returns the context of the stream.
|
||||
func (p *probePaymentStream) Context() context.Context {
|
||||
return p.ctx
|
||||
}
|
||||
|
||||
// sendProbePayment sends a payment to a target node in order to obtain
|
||||
// potential routing fees for it. The payment request has to contain a payment
|
||||
// hash that is guaranteed to be unknown to the target node, so it cannot settle
|
||||
// the payment. This method invokes a payment request loop in a goroutine and
|
||||
// awaits payment status updates.
|
||||
func (s *Server) sendProbePayment(ctx context.Context,
|
||||
req *SendPaymentRequest) (*RouteFeeResponse, error) {
|
||||
|
||||
// We'll launch a goroutine to send the payment probes.
|
||||
errChan := make(chan error, 1)
|
||||
defer close(errChan)
|
||||
|
||||
paymentStream := &probePaymentStream{
|
||||
stream: make(chan *lnrpc.Payment),
|
||||
ctx: ctx,
|
||||
}
|
||||
go func() {
|
||||
err := s.SendPaymentV2(req, paymentStream)
|
||||
if err != nil {
|
||||
select {
|
||||
case errChan <- err:
|
||||
|
||||
case <-paymentStream.ctx.Done():
|
||||
return
|
||||
}
|
||||
}
|
||||
}()
|
||||
|
||||
for {
|
||||
select {
|
||||
case payment := <-paymentStream.stream:
|
||||
switch payment.Status {
|
||||
case lnrpc.Payment_INITIATED:
|
||||
case lnrpc.Payment_IN_FLIGHT:
|
||||
case lnrpc.Payment_SUCCEEDED:
|
||||
return nil, errors.New("warning, the fee " +
|
||||
"estimation payment probe " +
|
||||
"unexpectedly succeeded. Please reach" +
|
||||
"out to the probe destination to " +
|
||||
"negotiate a refund. Otherwise the " +
|
||||
"payment probe amount is lost forever")
|
||||
|
||||
case lnrpc.Payment_FAILED:
|
||||
// Incorrect payment details point to a
|
||||
// successful probe.
|
||||
//nolint:lll
|
||||
if payment.FailureReason == lnrpc.PaymentFailureReason_FAILURE_REASON_INCORRECT_PAYMENT_DETAILS {
|
||||
return paymentDetails(payment)
|
||||
}
|
||||
|
||||
return &RouteFeeResponse{
|
||||
RoutingFeeMsat: 0,
|
||||
TimeLockDelay: 0,
|
||||
FailureReason: payment.FailureReason,
|
||||
}, nil
|
||||
|
||||
default:
|
||||
return nil, errors.New("unexpected payment " +
|
||||
"status")
|
||||
}
|
||||
|
||||
case err := <-errChan:
|
||||
return nil, err
|
||||
|
||||
case <-s.quit:
|
||||
return nil, errServerShuttingDown
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func paymentDetails(payment *lnrpc.Payment) (*RouteFeeResponse, error) {
|
||||
fee, timeLock, err := timelockAndFee(payment)
|
||||
if errors.Is(err, errUnexpectedFailureSource) {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return &RouteFeeResponse{
|
||||
RoutingFeeMsat: fee,
|
||||
TimeLockDelay: timeLock,
|
||||
FailureReason: lnrpc.PaymentFailureReason_FAILURE_REASON_NONE,
|
||||
}, nil
|
||||
}
|
||||
|
||||
// timelockAndFee returns the fee and total time lock of the last payment
|
||||
// attempt.
|
||||
func timelockAndFee(p *lnrpc.Payment) (int64, int64, error) {
|
||||
if len(p.Htlcs) == 0 {
|
||||
return 0, 0, nil
|
||||
}
|
||||
|
||||
lastAttempt := p.Htlcs[len(p.Htlcs)-1]
|
||||
if lastAttempt == nil {
|
||||
return 0, 0, errMissingPaymentAttempt
|
||||
}
|
||||
|
||||
lastRoute := lastAttempt.Route
|
||||
if lastRoute == nil {
|
||||
return 0, 0, errMissingRoute
|
||||
}
|
||||
|
||||
hopFailureIndex := lastAttempt.Failure.FailureSourceIndex
|
||||
finalHopIndex := uint32(len(lastRoute.Hops))
|
||||
if hopFailureIndex != finalHopIndex {
|
||||
return 0, 0, errUnexpectedFailureSource
|
||||
}
|
||||
|
||||
return lastRoute.TotalFeesMsat, int64(lastRoute.TotalTimeLock), nil
|
||||
}
|
||||
|
||||
// SendToRouteV2 sends a payment through a predefined route. The response of
|
||||
// this call contains structured error information.
|
||||
func (s *Server) SendToRouteV2(ctx context.Context,
|
||||
req *SendToRouteRequest) (*lnrpc.HTLCAttempt, error) {
|
||||
|
||||
@ -867,7 +1279,6 @@ func (s *Server) trackPayment(subscription routing.ControlTowerSubscriber,
|
||||
identifier lntypes.Hash, stream Router_TrackPaymentV2Server,
|
||||
noInflightUpdates bool) error {
|
||||
|
||||
// Stream updates to the client.
|
||||
err := s.trackPaymentStream(
|
||||
stream.Context(), subscription, noInflightUpdates, stream.Send,
|
||||
)
|
||||
|
@ -5,10 +5,13 @@ import (
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"github.com/btcsuite/btcd/btcec/v2"
|
||||
"github.com/lightningnetwork/lnd/channeldb"
|
||||
"github.com/lightningnetwork/lnd/lnrpc"
|
||||
"github.com/lightningnetwork/lnd/lnwire"
|
||||
"github.com/lightningnetwork/lnd/queue"
|
||||
"github.com/lightningnetwork/lnd/routing"
|
||||
"github.com/lightningnetwork/lnd/zpay32"
|
||||
"github.com/stretchr/testify/require"
|
||||
"google.golang.org/grpc"
|
||||
)
|
||||
@ -214,3 +217,186 @@ func TestTrackPaymentsNoInflightUpdates(t *testing.T) {
|
||||
payment := <-stream.sentFromServer
|
||||
require.Equal(t, lnrpc.Payment_SUCCEEDED, payment.Status)
|
||||
}
|
||||
|
||||
// TestIsLsp tests the isLSP heuristic. Combinations of different route hints
|
||||
// with different fees and cltv deltas are tested to ensure that the heuristic
|
||||
// correctly identifies whether a route leads to an LSP or not.
|
||||
func TestIsLsp(t *testing.T) {
|
||||
probeAmtMsat := lnwire.MilliSatoshi(1_000_000)
|
||||
|
||||
alicePrivKey, err := btcec.NewPrivateKey()
|
||||
require.NoError(t, err)
|
||||
alicePubKey := alicePrivKey.PubKey()
|
||||
|
||||
bobPrivKey, err := btcec.NewPrivateKey()
|
||||
require.NoError(t, err)
|
||||
bobPubKey := bobPrivKey.PubKey()
|
||||
|
||||
carolPrivKey, err := btcec.NewPrivateKey()
|
||||
require.NoError(t, err)
|
||||
carolPubKey := carolPrivKey.PubKey()
|
||||
|
||||
davePrivKey, err := btcec.NewPrivateKey()
|
||||
require.NoError(t, err)
|
||||
davePubKey := davePrivKey.PubKey()
|
||||
|
||||
var (
|
||||
aliceHopHint = zpay32.HopHint{
|
||||
NodeID: alicePubKey,
|
||||
FeeBaseMSat: 100,
|
||||
FeeProportionalMillionths: 1_000,
|
||||
ChannelID: 421337,
|
||||
}
|
||||
|
||||
bobHopHint = zpay32.HopHint{
|
||||
NodeID: bobPubKey,
|
||||
FeeBaseMSat: 2_000,
|
||||
FeeProportionalMillionths: 2_000,
|
||||
CLTVExpiryDelta: 288,
|
||||
ChannelID: 815,
|
||||
}
|
||||
|
||||
carolHopHint = zpay32.HopHint{
|
||||
NodeID: carolPubKey,
|
||||
FeeBaseMSat: 2_000,
|
||||
FeeProportionalMillionths: 2_000,
|
||||
ChannelID: 815,
|
||||
}
|
||||
|
||||
daveHopHint = zpay32.HopHint{
|
||||
NodeID: davePubKey,
|
||||
FeeBaseMSat: 2_000,
|
||||
FeeProportionalMillionths: 2_000,
|
||||
ChannelID: 815,
|
||||
}
|
||||
)
|
||||
|
||||
bobExpensiveCopy := bobHopHint.Copy()
|
||||
bobExpensiveCopy.FeeBaseMSat = 1_000_000
|
||||
bobExpensiveCopy.FeeProportionalMillionths = 1_000_000
|
||||
bobExpensiveCopy.CLTVExpiryDelta = bobHopHint.CLTVExpiryDelta - 1
|
||||
|
||||
//nolint:lll
|
||||
lspTestCases := []struct {
|
||||
name string
|
||||
routeHints [][]zpay32.HopHint
|
||||
probeAmtMsat lnwire.MilliSatoshi
|
||||
isLsp bool
|
||||
expectedHints [][]zpay32.HopHint
|
||||
expectedLspHop *zpay32.HopHint
|
||||
}{
|
||||
{
|
||||
name: "empty route hints",
|
||||
routeHints: [][]zpay32.HopHint{{}},
|
||||
probeAmtMsat: probeAmtMsat,
|
||||
isLsp: false,
|
||||
expectedHints: [][]zpay32.HopHint{},
|
||||
expectedLspHop: nil,
|
||||
},
|
||||
{
|
||||
name: "single route hint",
|
||||
routeHints: [][]zpay32.HopHint{{daveHopHint}},
|
||||
probeAmtMsat: probeAmtMsat,
|
||||
isLsp: true,
|
||||
expectedHints: [][]zpay32.HopHint{},
|
||||
expectedLspHop: &daveHopHint,
|
||||
},
|
||||
{
|
||||
name: "single route, multiple hints",
|
||||
routeHints: [][]zpay32.HopHint{{
|
||||
aliceHopHint, bobHopHint,
|
||||
}},
|
||||
probeAmtMsat: probeAmtMsat,
|
||||
isLsp: true,
|
||||
expectedHints: [][]zpay32.HopHint{{aliceHopHint}},
|
||||
expectedLspHop: &bobHopHint,
|
||||
},
|
||||
{
|
||||
name: "multiple routes, multiple hints",
|
||||
routeHints: [][]zpay32.HopHint{
|
||||
{
|
||||
aliceHopHint, bobHopHint,
|
||||
},
|
||||
{
|
||||
carolHopHint, bobHopHint,
|
||||
},
|
||||
},
|
||||
probeAmtMsat: probeAmtMsat,
|
||||
isLsp: true,
|
||||
expectedHints: [][]zpay32.HopHint{
|
||||
{aliceHopHint}, {carolHopHint},
|
||||
},
|
||||
expectedLspHop: &bobHopHint,
|
||||
},
|
||||
{
|
||||
name: "multiple routes, multiple hints with min length",
|
||||
routeHints: [][]zpay32.HopHint{
|
||||
{
|
||||
bobHopHint,
|
||||
},
|
||||
{
|
||||
carolHopHint, bobHopHint,
|
||||
},
|
||||
},
|
||||
probeAmtMsat: probeAmtMsat,
|
||||
isLsp: true,
|
||||
expectedHints: [][]zpay32.HopHint{
|
||||
{carolHopHint},
|
||||
},
|
||||
expectedLspHop: &bobHopHint,
|
||||
},
|
||||
{
|
||||
name: "multiple routes, multiple hints, diff fees+cltv",
|
||||
routeHints: [][]zpay32.HopHint{
|
||||
{
|
||||
bobHopHint,
|
||||
},
|
||||
{
|
||||
carolHopHint, bobExpensiveCopy,
|
||||
},
|
||||
},
|
||||
probeAmtMsat: probeAmtMsat,
|
||||
isLsp: true,
|
||||
expectedHints: [][]zpay32.HopHint{
|
||||
{carolHopHint},
|
||||
},
|
||||
expectedLspHop: &zpay32.HopHint{
|
||||
NodeID: bobHopHint.NodeID,
|
||||
ChannelID: bobHopHint.ChannelID,
|
||||
FeeBaseMSat: bobExpensiveCopy.FeeBaseMSat,
|
||||
FeeProportionalMillionths: bobExpensiveCopy.FeeProportionalMillionths,
|
||||
CLTVExpiryDelta: bobHopHint.CLTVExpiryDelta,
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "multiple routes, different final hops",
|
||||
routeHints: [][]zpay32.HopHint{
|
||||
{
|
||||
aliceHopHint, bobHopHint,
|
||||
},
|
||||
{
|
||||
carolHopHint, daveHopHint,
|
||||
},
|
||||
},
|
||||
probeAmtMsat: probeAmtMsat,
|
||||
isLsp: false,
|
||||
expectedHints: [][]zpay32.HopHint{},
|
||||
expectedLspHop: nil,
|
||||
},
|
||||
}
|
||||
|
||||
for _, tc := range lspTestCases {
|
||||
t.Run(tc.name, func(t *testing.T) {
|
||||
require.Equal(t, tc.isLsp, isLSP(tc.routeHints))
|
||||
if !tc.isLsp {
|
||||
return
|
||||
}
|
||||
|
||||
adjustedHints, lspHint, _ := prepareLspRouteHints(
|
||||
tc.routeHints, tc.probeAmtMsat,
|
||||
)
|
||||
require.Equal(t, tc.expectedHints, adjustedHints)
|
||||
require.Equal(t, tc.expectedLspHop, lspHint)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
@ -1,6 +1,9 @@
|
||||
package zpay32
|
||||
|
||||
import "github.com/btcsuite/btcd/btcec/v2"
|
||||
import (
|
||||
"github.com/btcsuite/btcd/btcec/v2"
|
||||
"github.com/lightningnetwork/lnd/lnwire"
|
||||
)
|
||||
|
||||
const (
|
||||
// DefaultAssumedFinalCLTVDelta is the default value to be used as the
|
||||
@ -10,6 +13,9 @@ const (
|
||||
// See also:
|
||||
// https://github.com/lightning/bolts/blob/master/02-peer-protocol.md
|
||||
DefaultAssumedFinalCLTVDelta = 18
|
||||
|
||||
// feeRateParts is the total number of parts used to express fee rates.
|
||||
feeRateParts = 1e6
|
||||
)
|
||||
|
||||
// HopHint is a routing hint that contains the minimum information of a channel
|
||||
@ -45,3 +51,13 @@ func (h HopHint) Copy() HopHint {
|
||||
CLTVExpiryDelta: h.CLTVExpiryDelta,
|
||||
}
|
||||
}
|
||||
|
||||
// HopFee calculates the fee for a given amount that is forwarded over a hop.
|
||||
// The amount has to be denoted in milli satoshi. The returned fee is also
|
||||
// denoted in milli satoshi.
|
||||
func (h HopHint) HopFee(amt lnwire.MilliSatoshi) lnwire.MilliSatoshi {
|
||||
baseFee := lnwire.MilliSatoshi(h.FeeBaseMSat)
|
||||
feeRate := lnwire.MilliSatoshi(h.FeeProportionalMillionths)
|
||||
|
||||
return baseFee + (amt*feeRate)/feeRateParts
|
||||
}
|
||||
|
Loading…
Reference in New Issue
Block a user