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
|
}, 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.
|
// 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.
|
// validates transitive dependencies.
|
||||||
func UnmarshalFeatures(
|
func UnmarshalFeatures(
|
||||||
rpcFeatures []lnrpc.FeatureBit) (*lnwire.FeatureVector, error) {
|
rpcFeatures []lnrpc.FeatureBit) (*lnwire.FeatureVector, error) {
|
||||||
|
@ -1,7 +1,9 @@
|
|||||||
package routerrpc
|
package routerrpc
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"bytes"
|
||||||
"context"
|
"context"
|
||||||
|
crand "crypto/rand"
|
||||||
"errors"
|
"errors"
|
||||||
"fmt"
|
"fmt"
|
||||||
"io/ioutil"
|
"io/ioutil"
|
||||||
@ -15,11 +17,13 @@ import (
|
|||||||
"github.com/grpc-ecosystem/grpc-gateway/v2/runtime"
|
"github.com/grpc-ecosystem/grpc-gateway/v2/runtime"
|
||||||
"github.com/lightningnetwork/lnd/channeldb"
|
"github.com/lightningnetwork/lnd/channeldb"
|
||||||
"github.com/lightningnetwork/lnd/lnrpc"
|
"github.com/lightningnetwork/lnd/lnrpc"
|
||||||
|
"github.com/lightningnetwork/lnd/lnrpc/invoicesrpc"
|
||||||
"github.com/lightningnetwork/lnd/lntypes"
|
"github.com/lightningnetwork/lnd/lntypes"
|
||||||
"github.com/lightningnetwork/lnd/lnwire"
|
"github.com/lightningnetwork/lnd/lnwire"
|
||||||
"github.com/lightningnetwork/lnd/macaroons"
|
"github.com/lightningnetwork/lnd/macaroons"
|
||||||
"github.com/lightningnetwork/lnd/routing"
|
"github.com/lightningnetwork/lnd/routing"
|
||||||
"github.com/lightningnetwork/lnd/routing/route"
|
"github.com/lightningnetwork/lnd/routing/route"
|
||||||
|
"github.com/lightningnetwork/lnd/zpay32"
|
||||||
"google.golang.org/grpc"
|
"google.golang.org/grpc"
|
||||||
"google.golang.org/grpc/codes"
|
"google.golang.org/grpc/codes"
|
||||||
"google.golang.org/grpc/status"
|
"google.golang.org/grpc/status"
|
||||||
@ -31,16 +35,26 @@ const (
|
|||||||
// to register ourselves, and we also require that the main
|
// to register ourselves, and we also require that the main
|
||||||
// SubServerConfigDispatcher instance recognize as the name of our
|
// SubServerConfigDispatcher instance recognize as the name of our
|
||||||
subServerName = "RouterRPC"
|
subServerName = "RouterRPC"
|
||||||
|
|
||||||
|
// routeFeeLimitSat is the maximum routing fee that we allow to occur
|
||||||
|
// when estimating a routing fee.
|
||||||
|
routeFeeLimitSat = 100_000_000
|
||||||
)
|
)
|
||||||
|
|
||||||
var (
|
var (
|
||||||
errServerShuttingDown = errors.New("routerrpc server shutting down")
|
errServerShuttingDown = errors.New("routerrpc server shutting down")
|
||||||
|
|
||||||
// ErrInterceptorAlreadyExists is an error returned when the a new stream
|
// ErrInterceptorAlreadyExists is an error returned when a new stream is
|
||||||
// is opened and there is already one active interceptor.
|
// opened and there is already one active interceptor. The user must
|
||||||
// The user must disconnect prior to open another stream.
|
// disconnect prior to open another stream.
|
||||||
ErrInterceptorAlreadyExists = errors.New("interceptor already exists")
|
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
|
// macaroonOps are the set of capabilities that our minted macaroon (if
|
||||||
// it doesn't already exist) will have.
|
// it doesn't already exist) will have.
|
||||||
macaroonOps = []bakery.Op{
|
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
|
// 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.
|
// 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,
|
func (s *Server) EstimateRouteFee(ctx context.Context,
|
||||||
req *RouteFeeRequest) (*RouteFeeResponse, error) {
|
req *RouteFeeRequest) (*RouteFeeResponse, error) {
|
||||||
|
|
||||||
if len(req.Dest) != 33 {
|
isProbeDestination := len(req.Dest) > 0
|
||||||
return nil, errors.New("invalid length destination key")
|
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
|
// Next, we'll convert the amount in satoshis to mSAT, which are the
|
||||||
// native unit of LN.
|
// native unit of LN.
|
||||||
amtMsat := lnwire.NewMSatFromSatoshis(btcutil.Amount(req.AmtSat))
|
amtMsat := lnwire.NewMSatFromSatoshis(btcutil.Amount(amtSat))
|
||||||
|
|
||||||
// Pick a fee limit
|
|
||||||
//
|
|
||||||
// TODO: Change this into behaviour that makes more sense.
|
|
||||||
feeLimit := lnwire.NewMSatFromSatoshis(btcutil.SatoshiPerBitcoin)
|
|
||||||
|
|
||||||
// Finally, we'll query for a route to the destination that can carry
|
// 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
|
// 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(
|
routeReq, err := routing.NewRouteRequest(
|
||||||
s.cfg.RouterBackend.SelfNode, &destNode, amtMsat, 0,
|
s.cfg.RouterBackend.SelfNode, &destNode, amtMsat, 0,
|
||||||
&routing.RestrictParams{
|
&routing.RestrictParams{
|
||||||
FeeLimit: feeLimit,
|
FeeLimit: routeFeeLimitSat,
|
||||||
CltvLimit: s.cfg.RouterBackend.MaxTotalTimelock,
|
CltvLimit: s.cfg.RouterBackend.MaxTotalTimelock,
|
||||||
ProbabilitySource: mc.GetProbability,
|
ProbabilitySource: mc.GetProbability,
|
||||||
}, nil, nil, nil, s.cfg.RouterBackend.DefaultFinalCltvDelta,
|
}, nil, nil, nil, s.cfg.RouterBackend.DefaultFinalCltvDelta,
|
||||||
@ -398,14 +445,379 @@ func (s *Server) EstimateRouteFee(ctx context.Context,
|
|||||||
return nil, err
|
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{
|
return &RouteFeeResponse{
|
||||||
RoutingFeeMsat: int64(route.TotalFees()),
|
RoutingFeeMsat: int64(route.TotalFees()),
|
||||||
TimeLockDelay: int64(route.TotalTimeLock),
|
TimeLockDelay: int64(timeLockDelay),
|
||||||
|
FailureReason: lnrpc.PaymentFailureReason_FAILURE_REASON_NONE,
|
||||||
}, nil
|
}, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// SendToRouteV2 sends a payment through a predefined route. The response of this
|
// probePaymentRequest estimates fees along a route to a destination that is
|
||||||
// call contains structured error information.
|
// 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,
|
func (s *Server) SendToRouteV2(ctx context.Context,
|
||||||
req *SendToRouteRequest) (*lnrpc.HTLCAttempt, error) {
|
req *SendToRouteRequest) (*lnrpc.HTLCAttempt, error) {
|
||||||
|
|
||||||
@ -867,7 +1279,6 @@ func (s *Server) trackPayment(subscription routing.ControlTowerSubscriber,
|
|||||||
identifier lntypes.Hash, stream Router_TrackPaymentV2Server,
|
identifier lntypes.Hash, stream Router_TrackPaymentV2Server,
|
||||||
noInflightUpdates bool) error {
|
noInflightUpdates bool) error {
|
||||||
|
|
||||||
// Stream updates to the client.
|
|
||||||
err := s.trackPaymentStream(
|
err := s.trackPaymentStream(
|
||||||
stream.Context(), subscription, noInflightUpdates, stream.Send,
|
stream.Context(), subscription, noInflightUpdates, stream.Send,
|
||||||
)
|
)
|
||||||
|
@ -5,10 +5,13 @@ import (
|
|||||||
"testing"
|
"testing"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
|
"github.com/btcsuite/btcd/btcec/v2"
|
||||||
"github.com/lightningnetwork/lnd/channeldb"
|
"github.com/lightningnetwork/lnd/channeldb"
|
||||||
"github.com/lightningnetwork/lnd/lnrpc"
|
"github.com/lightningnetwork/lnd/lnrpc"
|
||||||
|
"github.com/lightningnetwork/lnd/lnwire"
|
||||||
"github.com/lightningnetwork/lnd/queue"
|
"github.com/lightningnetwork/lnd/queue"
|
||||||
"github.com/lightningnetwork/lnd/routing"
|
"github.com/lightningnetwork/lnd/routing"
|
||||||
|
"github.com/lightningnetwork/lnd/zpay32"
|
||||||
"github.com/stretchr/testify/require"
|
"github.com/stretchr/testify/require"
|
||||||
"google.golang.org/grpc"
|
"google.golang.org/grpc"
|
||||||
)
|
)
|
||||||
@ -214,3 +217,186 @@ func TestTrackPaymentsNoInflightUpdates(t *testing.T) {
|
|||||||
payment := <-stream.sentFromServer
|
payment := <-stream.sentFromServer
|
||||||
require.Equal(t, lnrpc.Payment_SUCCEEDED, payment.Status)
|
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
|
package zpay32
|
||||||
|
|
||||||
import "github.com/btcsuite/btcd/btcec/v2"
|
import (
|
||||||
|
"github.com/btcsuite/btcd/btcec/v2"
|
||||||
|
"github.com/lightningnetwork/lnd/lnwire"
|
||||||
|
)
|
||||||
|
|
||||||
const (
|
const (
|
||||||
// DefaultAssumedFinalCLTVDelta is the default value to be used as the
|
// DefaultAssumedFinalCLTVDelta is the default value to be used as the
|
||||||
@ -10,6 +13,9 @@ const (
|
|||||||
// See also:
|
// See also:
|
||||||
// https://github.com/lightning/bolts/blob/master/02-peer-protocol.md
|
// https://github.com/lightning/bolts/blob/master/02-peer-protocol.md
|
||||||
DefaultAssumedFinalCLTVDelta = 18
|
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
|
// 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,
|
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