mirror of
https://github.com/lightningnetwork/lnd.git
synced 2024-11-19 09:53:54 +01:00
7dc86acb8c
Since it is allowed to not be set and so can lead to nil deref panics if it is a pointer.
1471 lines
48 KiB
Go
1471 lines
48 KiB
Go
package routing
|
|
|
|
import (
|
|
"bytes"
|
|
"container/heap"
|
|
"errors"
|
|
"fmt"
|
|
"math"
|
|
"sort"
|
|
"time"
|
|
|
|
"github.com/btcsuite/btcd/btcutil"
|
|
sphinx "github.com/lightningnetwork/lightning-onion"
|
|
"github.com/lightningnetwork/lnd/channeldb"
|
|
"github.com/lightningnetwork/lnd/channeldb/models"
|
|
"github.com/lightningnetwork/lnd/feature"
|
|
"github.com/lightningnetwork/lnd/fn"
|
|
"github.com/lightningnetwork/lnd/lnutils"
|
|
"github.com/lightningnetwork/lnd/lnwire"
|
|
"github.com/lightningnetwork/lnd/record"
|
|
"github.com/lightningnetwork/lnd/routing/route"
|
|
)
|
|
|
|
const (
|
|
// infinity is used as a starting distance in our shortest path search.
|
|
infinity = math.MaxInt64
|
|
|
|
// RiskFactorBillionths controls the influence of time lock delta
|
|
// of a channel on route selection. It is expressed as billionths
|
|
// of msat per msat sent through the channel per time lock delta
|
|
// block. See edgeWeight function below for more details.
|
|
// The chosen value is based on the previous incorrect weight function
|
|
// 1 + timelock + fee * fee. In this function, the fee penalty
|
|
// diminishes the time lock penalty for all but the smallest amounts.
|
|
// To not change the behaviour of path finding too drastically, a
|
|
// relatively small value is chosen which is still big enough to give
|
|
// some effect with smaller time lock values. The value may need
|
|
// tweaking and/or be made configurable in the future.
|
|
RiskFactorBillionths = 15
|
|
|
|
// estimatedNodeCount is used to preallocate the path finding structures
|
|
// to avoid resizing and copies. It should be number on the same order as
|
|
// the number of active nodes in the network.
|
|
estimatedNodeCount = 10000
|
|
|
|
// fakeHopHintCapacity is the capacity we assume for hop hint channels.
|
|
// This is a high number, which expresses that a hop hint channel should
|
|
// be able to route payments.
|
|
fakeHopHintCapacity = btcutil.Amount(10 * btcutil.SatoshiPerBitcoin)
|
|
)
|
|
|
|
// pathFinder defines the interface of a path finding algorithm.
|
|
type pathFinder = func(g *graphParams, r *RestrictParams,
|
|
cfg *PathFindingConfig, self, source, target route.Vertex,
|
|
amt lnwire.MilliSatoshi, timePref float64, finalHtlcExpiry int32) (
|
|
[]*unifiedEdge, float64, error)
|
|
|
|
var (
|
|
// DefaultEstimator is the default estimator used for computing
|
|
// probabilities in pathfinding.
|
|
DefaultEstimator = AprioriEstimatorName
|
|
|
|
// DefaultAttemptCost is the default fixed virtual cost in path finding
|
|
// of a failed payment attempt. It is used to trade off potentially
|
|
// better routes against their probability of succeeding.
|
|
DefaultAttemptCost = lnwire.NewMSatFromSatoshis(100)
|
|
|
|
// DefaultAttemptCostPPM is the default proportional virtual cost in
|
|
// path finding weight units of executing a payment attempt that fails.
|
|
// It is used to trade off potentially better routes against their
|
|
// probability of succeeding. This parameter is expressed in parts per
|
|
// million of the payment amount.
|
|
//
|
|
// It is impossible to pick a perfect default value. The current value
|
|
// of 0.1% is based on the idea that a transaction fee of 1% is within
|
|
// reasonable territory and that a payment shouldn't need more than 10
|
|
// attempts.
|
|
DefaultAttemptCostPPM = int64(1000)
|
|
|
|
// DefaultMinRouteProbability is the default minimum probability for routes
|
|
// returned from findPath.
|
|
DefaultMinRouteProbability = float64(0.01)
|
|
|
|
// DefaultAprioriHopProbability is the default a priori probability for
|
|
// a hop.
|
|
DefaultAprioriHopProbability = float64(0.6)
|
|
)
|
|
|
|
// edgePolicyWithSource is a helper struct to keep track of the source node
|
|
// of a channel edge. ChannelEdgePolicy only contains to destination node
|
|
// of the edge.
|
|
type edgePolicyWithSource struct {
|
|
sourceNode route.Vertex
|
|
edge AdditionalEdge
|
|
}
|
|
|
|
// finalHopParams encapsulates various parameters for route construction that
|
|
// apply to the final hop in a route. These features include basic payment data
|
|
// such as amounts and cltvs, as well as more complex features like destination
|
|
// custom records and payment address.
|
|
type finalHopParams struct {
|
|
amt lnwire.MilliSatoshi
|
|
totalAmt lnwire.MilliSatoshi
|
|
|
|
// cltvDelta is the final hop's minimum CLTV expiry delta.
|
|
//
|
|
// NOTE that in the case of paying to a blinded path, this value will
|
|
// be set to a duplicate of the blinded path's accumulated CLTV value.
|
|
// We would then only need to use this value in the case where the
|
|
// introduction node of the path is also the destination node.
|
|
cltvDelta uint16
|
|
|
|
records record.CustomSet
|
|
paymentAddr fn.Option[[32]byte]
|
|
|
|
// metadata is additional data that is sent along with the payment to
|
|
// the payee.
|
|
metadata []byte
|
|
}
|
|
|
|
// newRoute constructs a route using the provided path and final hop constraints.
|
|
// Any destination specific fields from the final hop params will be attached
|
|
// assuming the destination's feature vector signals support, otherwise this
|
|
// method will fail. If the route is too long, or the selected path cannot
|
|
// support the fully payment including fees, then a non-nil error is returned.
|
|
// If the route is to a blinded path, the blindedPath parameter is used to
|
|
// back fill additional fields that are required for a blinded payment. This is
|
|
// done in a separate pass to keep our route construction simple, as blinded
|
|
// paths require zero expiry and amount values for intermediate hops (which
|
|
// makes calculating the totals during route construction difficult if we
|
|
// include blinded paths on the first pass).
|
|
//
|
|
// NOTE: The passed slice of unified edges MUST be sorted in forward order: from
|
|
// the source to the target node of the path finding attempt. It is assumed that
|
|
// any feature vectors on all hops have been validated for transitive
|
|
// dependencies.
|
|
// NOTE: If a non-nil blinded path is provided it is assumed to have been
|
|
// validated by the caller.
|
|
func newRoute(sourceVertex route.Vertex,
|
|
pathEdges []*unifiedEdge, currentHeight uint32, finalHop finalHopParams,
|
|
blindedPathSet *BlindedPaymentPathSet) (*route.Route, error) {
|
|
|
|
var (
|
|
hops []*route.Hop
|
|
|
|
// totalTimeLock will accumulate the cumulative time lock
|
|
// across the entire route. This value represents how long the
|
|
// sender will need to wait in the *worst* case.
|
|
totalTimeLock = currentHeight
|
|
|
|
// nextIncomingAmount is the amount that will need to flow into
|
|
// the *next* hop. Since we're going to be walking the route
|
|
// backwards below, this next hop gets closer and closer to the
|
|
// sender of the payment.
|
|
nextIncomingAmount lnwire.MilliSatoshi
|
|
|
|
blindedPayment *BlindedPayment
|
|
)
|
|
|
|
pathLength := len(pathEdges)
|
|
for i := pathLength - 1; i >= 0; i-- {
|
|
// Now we'll start to calculate the items within the per-hop
|
|
// payload for the hop this edge is leading to.
|
|
edge := pathEdges[i].policy
|
|
|
|
// If this is an edge from a blinded path and the
|
|
// blindedPayment variable has not been set yet, then set it now
|
|
// by extracting the corresponding blinded payment from the
|
|
// edge.
|
|
isBlindedEdge := pathEdges[i].blindedPayment != nil
|
|
if isBlindedEdge && blindedPayment == nil {
|
|
blindedPayment = pathEdges[i].blindedPayment
|
|
}
|
|
|
|
// We'll calculate the amounts, timelocks, and fees for each hop
|
|
// in the route. The base case is the final hop which includes
|
|
// their amount and timelocks. These values will accumulate
|
|
// contributions from the preceding hops back to the sender as
|
|
// we compute the route in reverse.
|
|
var (
|
|
amtToForward lnwire.MilliSatoshi
|
|
fee int64
|
|
totalAmtMsatBlinded lnwire.MilliSatoshi
|
|
outgoingTimeLock uint32
|
|
customRecords record.CustomSet
|
|
mpp *record.MPP
|
|
metadata []byte
|
|
)
|
|
|
|
// Define a helper function that checks this edge's feature
|
|
// vector for support for a given feature. We assume at this
|
|
// point that the feature vectors transitive dependencies have
|
|
// been validated.
|
|
supports := func(feature lnwire.FeatureBit) bool {
|
|
// If this edge comes from router hints, the features
|
|
// could be nil.
|
|
if edge.ToNodeFeatures == nil {
|
|
return false
|
|
}
|
|
return edge.ToNodeFeatures.HasFeature(feature)
|
|
}
|
|
|
|
if i == len(pathEdges)-1 {
|
|
// If this is the last hop, then the hop payload will
|
|
// contain the exact amount. In BOLT #4: Onion Routing
|
|
// Protocol / "Payload for the Last Node", this is
|
|
// detailed.
|
|
amtToForward = finalHop.amt
|
|
|
|
// Fee is not part of the hop payload, but only used for
|
|
// reporting through RPC. Set to zero for the final hop.
|
|
fee = 0
|
|
|
|
if blindedPathSet == nil {
|
|
totalTimeLock += uint32(finalHop.cltvDelta)
|
|
} else {
|
|
totalTimeLock += uint32(
|
|
blindedPathSet.FinalCLTVDelta(),
|
|
)
|
|
}
|
|
outgoingTimeLock = totalTimeLock
|
|
|
|
// Attach any custom records to the final hop.
|
|
customRecords = finalHop.records
|
|
|
|
// If we're attaching a payment addr but the receiver
|
|
// doesn't support both TLV and payment addrs, fail.
|
|
payAddr := supports(lnwire.PaymentAddrOptional)
|
|
if !payAddr && finalHop.paymentAddr.IsSome() {
|
|
return nil, errors.New("cannot attach " +
|
|
"payment addr")
|
|
}
|
|
|
|
// Otherwise attach the mpp record if it exists.
|
|
// TODO(halseth): move this to payment life cycle,
|
|
// where AMP options are set.
|
|
finalHop.paymentAddr.WhenSome(func(addr [32]byte) {
|
|
mpp = record.NewMPP(finalHop.totalAmt, addr)
|
|
})
|
|
|
|
metadata = finalHop.metadata
|
|
|
|
if blindedPathSet != nil {
|
|
totalAmtMsatBlinded = finalHop.totalAmt
|
|
}
|
|
} else {
|
|
// The amount that the current hop needs to forward is
|
|
// equal to the incoming amount of the next hop.
|
|
amtToForward = nextIncomingAmount
|
|
|
|
// The fee that needs to be paid to the current hop is
|
|
// based on the amount that this hop needs to forward
|
|
// and its policy for the outgoing channel. This policy
|
|
// is stored as part of the incoming channel of
|
|
// the next hop.
|
|
outboundFee := pathEdges[i+1].policy.ComputeFee(
|
|
amtToForward,
|
|
)
|
|
|
|
inboundFee := pathEdges[i].inboundFees.CalcFee(
|
|
amtToForward + outboundFee,
|
|
)
|
|
|
|
fee = int64(outboundFee) + inboundFee
|
|
if fee < 0 {
|
|
fee = 0
|
|
}
|
|
|
|
// We'll take the total timelock of the preceding hop as
|
|
// the outgoing timelock or this hop. Then we'll
|
|
// increment the total timelock incurred by this hop.
|
|
outgoingTimeLock = totalTimeLock
|
|
totalTimeLock += uint32(
|
|
pathEdges[i+1].policy.TimeLockDelta,
|
|
)
|
|
}
|
|
|
|
// Since we're traversing the path backwards atm, we prepend
|
|
// each new hop such that, the final slice of hops will be in
|
|
// the forwards order.
|
|
currentHop := &route.Hop{
|
|
PubKeyBytes: edge.ToNodePubKey(),
|
|
ChannelID: edge.ChannelID,
|
|
AmtToForward: amtToForward,
|
|
OutgoingTimeLock: outgoingTimeLock,
|
|
CustomRecords: customRecords,
|
|
MPP: mpp,
|
|
Metadata: metadata,
|
|
TotalAmtMsat: totalAmtMsatBlinded,
|
|
}
|
|
|
|
hops = append([]*route.Hop{currentHop}, hops...)
|
|
|
|
// Finally, we update the amount that needs to flow into the
|
|
// *next* hop, which is the amount this hop needs to forward,
|
|
// accounting for the fee that it takes.
|
|
nextIncomingAmount = amtToForward + lnwire.MilliSatoshi(fee)
|
|
}
|
|
|
|
// If we are creating a route to a blinded path, we need to add some
|
|
// additional data to the route that is required for blinded forwarding.
|
|
// We do another pass on our edges to append this data.
|
|
if blindedPathSet != nil {
|
|
// If the passed in BlindedPaymentPathSet is non-nil but no
|
|
// edge had a BlindedPayment attached, it means that the path
|
|
// chosen was an introduction-node-only path. So in this case,
|
|
// we can assume the relevant payment is the only one in the
|
|
// payment set.
|
|
if blindedPayment == nil {
|
|
var err error
|
|
blindedPayment, err = blindedPathSet.IntroNodeOnlyPath()
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
}
|
|
|
|
var (
|
|
inBlindedRoute bool
|
|
dataIndex = 0
|
|
|
|
blindedPath = blindedPayment.BlindedPath
|
|
numHops = len(blindedPath.BlindedHops)
|
|
realFinal = blindedPath.BlindedHops[numHops-1].
|
|
BlindedNodePub
|
|
|
|
introVertex = route.NewVertex(
|
|
blindedPath.IntroductionPoint,
|
|
)
|
|
)
|
|
|
|
for i, hop := range hops {
|
|
// Once we locate our introduction node, we know that
|
|
// every hop after this is part of the blinded route.
|
|
if bytes.Equal(hop.PubKeyBytes[:], introVertex[:]) {
|
|
inBlindedRoute = true
|
|
hop.BlindingPoint = blindedPath.BlindingPoint
|
|
}
|
|
|
|
// We don't need to modify edges outside of our blinded
|
|
// route.
|
|
if !inBlindedRoute {
|
|
continue
|
|
}
|
|
|
|
payload := blindedPath.BlindedHops[dataIndex].CipherText
|
|
hop.EncryptedData = payload
|
|
|
|
// All of the hops in a blinded route *except* the
|
|
// final hop should have zero amounts / time locks.
|
|
if i != len(hops)-1 {
|
|
hop.AmtToForward = 0
|
|
hop.OutgoingTimeLock = 0
|
|
} else {
|
|
// For the final hop, we swap out the pub key
|
|
// bytes to the original destination node pub
|
|
// key for that payment path.
|
|
hop.PubKeyBytes = route.NewVertex(realFinal)
|
|
}
|
|
|
|
dataIndex++
|
|
}
|
|
}
|
|
|
|
// With the base routing data expressed as hops, build the full route
|
|
newRoute, err := route.NewRouteFromHops(
|
|
nextIncomingAmount, totalTimeLock, route.Vertex(sourceVertex),
|
|
hops,
|
|
)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
return newRoute, nil
|
|
}
|
|
|
|
// edgeWeight computes the weight of an edge. This value is used when searching
|
|
// for the shortest path within the channel graph between two nodes. Weight is
|
|
// is the fee itself plus a time lock penalty added to it. This benefits
|
|
// channels with shorter time lock deltas and shorter (hops) routes in general.
|
|
// RiskFactor controls the influence of time lock on route selection. This is
|
|
// currently a fixed value, but might be configurable in the future.
|
|
func edgeWeight(lockedAmt lnwire.MilliSatoshi, fee lnwire.MilliSatoshi,
|
|
timeLockDelta uint16) int64 {
|
|
// timeLockPenalty is the penalty for the time lock delta of this channel.
|
|
// It is controlled by RiskFactorBillionths and scales proportional
|
|
// to the amount that will pass through channel. Rationale is that it if
|
|
// a twice as large amount gets locked up, it is twice as bad.
|
|
timeLockPenalty := int64(lockedAmt) * int64(timeLockDelta) *
|
|
RiskFactorBillionths / 1000000000
|
|
|
|
return int64(fee) + timeLockPenalty
|
|
}
|
|
|
|
// graphParams wraps the set of graph parameters passed to findPath.
|
|
type graphParams struct {
|
|
// graph is the ChannelGraph to be used during path finding.
|
|
graph Graph
|
|
|
|
// additionalEdges is an optional set of edges that should be
|
|
// considered during path finding, that is not already found in the
|
|
// channel graph. These can either be private edges for bolt 11 invoices
|
|
// or blinded edges when a payment to a blinded path is made.
|
|
additionalEdges map[route.Vertex][]AdditionalEdge
|
|
|
|
// bandwidthHints is an interface that provides bandwidth hints that
|
|
// can provide a better estimate of the current channel bandwidth than
|
|
// what is found in the graph. It will override the capacities and
|
|
// disabled flags found in the graph for local channels when doing
|
|
// path finding if it has updated values for that channel. In
|
|
// particular, it should be set to the current available sending
|
|
// bandwidth for active local channels, and 0 for inactive channels.
|
|
bandwidthHints bandwidthHints
|
|
}
|
|
|
|
// RestrictParams wraps the set of restrictions passed to findPath that the
|
|
// found path must adhere to.
|
|
type RestrictParams struct {
|
|
// ProbabilitySource is a callback that is expected to return the
|
|
// success probability of traversing the channel from the node.
|
|
ProbabilitySource func(route.Vertex, route.Vertex,
|
|
lnwire.MilliSatoshi, btcutil.Amount) float64
|
|
|
|
// FeeLimit is a maximum fee amount allowed to be used on the path from
|
|
// the source to the target.
|
|
FeeLimit lnwire.MilliSatoshi
|
|
|
|
// OutgoingChannelIDs is the list of channels that are allowed for the
|
|
// first hop. If nil, any channel may be used.
|
|
OutgoingChannelIDs []uint64
|
|
|
|
// LastHop is the pubkey of the last node before the final destination
|
|
// is reached. If nil, any node may be used.
|
|
LastHop *route.Vertex
|
|
|
|
// CltvLimit is the maximum time lock of the route excluding the final
|
|
// ctlv. After path finding is complete, the caller needs to increase
|
|
// all cltv expiry heights with the required final cltv delta.
|
|
CltvLimit uint32
|
|
|
|
// DestCustomRecords contains the custom records to drop off at the
|
|
// final hop, if any.
|
|
DestCustomRecords record.CustomSet
|
|
|
|
// DestFeatures is a feature vector describing what the final hop
|
|
// supports. If none are provided, pathfinding will try to inspect any
|
|
// features on the node announcement instead.
|
|
DestFeatures *lnwire.FeatureVector
|
|
|
|
// PaymentAddr is a random 32-byte value generated by the receiver to
|
|
// mitigate probing vectors and payment sniping attacks on overpaid
|
|
// invoices.
|
|
PaymentAddr fn.Option[[32]byte]
|
|
|
|
// Amp signals to the pathfinder that this payment is an AMP payment
|
|
// and therefore it needs to account for additional AMP data in the
|
|
// final hop payload size calculation.
|
|
Amp *AMPOptions
|
|
|
|
// Metadata is additional data that is sent along with the payment to
|
|
// the payee.
|
|
Metadata []byte
|
|
|
|
// BlindedPaymentPathSet is necessary to determine the hop size of the
|
|
// last/exit hop.
|
|
BlindedPaymentPathSet *BlindedPaymentPathSet
|
|
|
|
// FirstHopCustomRecords includes any records that should be included in
|
|
// the update_add_htlc message towards our peer.
|
|
FirstHopCustomRecords lnwire.CustomRecords
|
|
}
|
|
|
|
// PathFindingConfig defines global parameters that control the trade-off in
|
|
// path finding between fees and probability.
|
|
type PathFindingConfig struct {
|
|
// AttemptCost is the fixed virtual cost in path finding of a failed
|
|
// payment attempt. It is used to trade off potentially better routes
|
|
// against their probability of succeeding.
|
|
AttemptCost lnwire.MilliSatoshi
|
|
|
|
// AttemptCostPPM is the proportional virtual cost in path finding of a
|
|
// failed payment attempt. It is used to trade off potentially better
|
|
// routes against their probability of succeeding. This parameter is
|
|
// expressed in parts per million of the total payment amount.
|
|
AttemptCostPPM int64
|
|
|
|
// MinProbability defines the minimum success probability of the
|
|
// returned route.
|
|
MinProbability float64
|
|
}
|
|
|
|
// getOutgoingBalance returns the maximum available balance in any of the
|
|
// channels of the given node. The second return parameters is the total
|
|
// available balance.
|
|
func getOutgoingBalance(node route.Vertex, outgoingChans map[uint64]struct{},
|
|
bandwidthHints bandwidthHints,
|
|
g Graph) (lnwire.MilliSatoshi, lnwire.MilliSatoshi, error) {
|
|
|
|
var max, total lnwire.MilliSatoshi
|
|
cb := func(channel *channeldb.DirectedChannel) error {
|
|
if !channel.OutPolicySet {
|
|
return nil
|
|
}
|
|
|
|
chanID := channel.ChannelID
|
|
|
|
// Enforce outgoing channel restriction.
|
|
if outgoingChans != nil {
|
|
if _, ok := outgoingChans[chanID]; !ok {
|
|
return nil
|
|
}
|
|
}
|
|
|
|
bandwidth, ok := bandwidthHints.availableChanBandwidth(
|
|
chanID, 0,
|
|
)
|
|
|
|
// If the bandwidth is not available, use the channel capacity.
|
|
// This can happen when a channel is added to the graph after
|
|
// we've already queried the bandwidth hints.
|
|
if !ok {
|
|
bandwidth = lnwire.NewMSatFromSatoshis(channel.Capacity)
|
|
}
|
|
|
|
if bandwidth > max {
|
|
max = bandwidth
|
|
}
|
|
|
|
var overflow bool
|
|
total, overflow = overflowSafeAdd(total, bandwidth)
|
|
if overflow {
|
|
// If the current total and the bandwidth would
|
|
// overflow the maximum value, we set the total to the
|
|
// maximum value. Which is more milli-satoshis than are
|
|
// in existence anyway, so the actual value is
|
|
// irrelevant.
|
|
total = lnwire.MilliSatoshi(math.MaxUint64)
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
// Iterate over all channels of the to node.
|
|
err := g.ForEachNodeChannel(node, cb)
|
|
if err != nil {
|
|
return 0, 0, err
|
|
}
|
|
return max, total, err
|
|
}
|
|
|
|
// findPath attempts to find a path from the source node within the ChannelGraph
|
|
// to the target node that's capable of supporting a payment of `amt` value. The
|
|
// current approach implemented is modified version of Dijkstra's algorithm to
|
|
// find a single shortest path between the source node and the destination. The
|
|
// distance metric used for edges is related to the time-lock+fee costs along a
|
|
// particular edge. If a path is found, this function returns a slice of
|
|
// ChannelHop structs which encoded the chosen path from the target to the
|
|
// source. The search is performed backwards from destination node back to
|
|
// source. This is to properly accumulate fees that need to be paid along the
|
|
// path and accurately check the amount to forward at every node against the
|
|
// available bandwidth.
|
|
func findPath(g *graphParams, r *RestrictParams, cfg *PathFindingConfig,
|
|
self, source, target route.Vertex, amt lnwire.MilliSatoshi,
|
|
timePref float64, finalHtlcExpiry int32) ([]*unifiedEdge, float64,
|
|
error) {
|
|
|
|
// Pathfinding can be a significant portion of the total payment
|
|
// latency, especially on low-powered devices. Log several metrics to
|
|
// aid in the analysis performance problems in this area.
|
|
start := time.Now()
|
|
nodesVisited := 0
|
|
edgesExpanded := 0
|
|
defer func() {
|
|
timeElapsed := time.Since(start)
|
|
log.Debugf("Pathfinding perf metrics: nodes=%v, edges=%v, "+
|
|
"time=%v", nodesVisited, edgesExpanded, timeElapsed)
|
|
}()
|
|
|
|
// If no destination features are provided, we will load what features
|
|
// we have for the target node from our graph.
|
|
features := r.DestFeatures
|
|
if features == nil {
|
|
var err error
|
|
features, err = g.graph.FetchNodeFeatures(target)
|
|
if err != nil {
|
|
return nil, 0, err
|
|
}
|
|
}
|
|
|
|
// Ensure that the destination's features don't include unknown
|
|
// required features.
|
|
err := feature.ValidateRequired(features)
|
|
if err != nil {
|
|
log.Warnf("Pathfinding destination node features: %v", err)
|
|
return nil, 0, errUnknownRequiredFeature
|
|
}
|
|
|
|
// Ensure that all transitive dependencies are set.
|
|
err = feature.ValidateDeps(features)
|
|
if err != nil {
|
|
log.Warnf("Pathfinding destination node features: %v", err)
|
|
return nil, 0, errMissingDependentFeature
|
|
}
|
|
|
|
// Now that we know the feature vector is well-formed, we'll proceed in
|
|
// checking that it supports the features we need. If the caller has a
|
|
// payment address to attach, check that our destination feature vector
|
|
// supports them.
|
|
if r.PaymentAddr.IsSome() &&
|
|
!features.HasFeature(lnwire.PaymentAddrOptional) {
|
|
|
|
return nil, 0, errNoPaymentAddr
|
|
}
|
|
|
|
// Set up outgoing channel map for quicker access.
|
|
var outgoingChanMap map[uint64]struct{}
|
|
if len(r.OutgoingChannelIDs) > 0 {
|
|
outgoingChanMap = make(map[uint64]struct{})
|
|
for _, outChan := range r.OutgoingChannelIDs {
|
|
outgoingChanMap[outChan] = struct{}{}
|
|
}
|
|
}
|
|
|
|
// If we are routing from ourselves, check that we have enough local
|
|
// balance available.
|
|
if source == self {
|
|
max, total, err := getOutgoingBalance(
|
|
self, outgoingChanMap, g.bandwidthHints, g.graph,
|
|
)
|
|
if err != nil {
|
|
return nil, 0, err
|
|
}
|
|
|
|
// If the total outgoing balance isn't sufficient, it will be
|
|
// impossible to complete the payment.
|
|
if total < amt {
|
|
log.Warnf("Not enough outbound balance to send "+
|
|
"htlc of amount: %v, only have local "+
|
|
"balance: %v", amt, total)
|
|
|
|
return nil, 0, errInsufficientBalance
|
|
}
|
|
|
|
// If there is only not enough capacity on a single route, it
|
|
// may still be possible to complete the payment by splitting.
|
|
if max < amt {
|
|
return nil, 0, errNoPathFound
|
|
}
|
|
}
|
|
|
|
// First we'll initialize an empty heap which'll help us to quickly
|
|
// locate the next edge we should visit next during our graph
|
|
// traversal.
|
|
nodeHeap := newDistanceHeap(estimatedNodeCount)
|
|
|
|
// Holds the current best distance for a given node.
|
|
distance := make(map[route.Vertex]*nodeWithDist, estimatedNodeCount)
|
|
|
|
additionalEdgesWithSrc := make(map[route.Vertex][]*edgePolicyWithSource)
|
|
for vertex, additionalEdges := range g.additionalEdges {
|
|
// Edges connected to self are always included in the graph,
|
|
// therefore can be skipped. This prevents us from trying
|
|
// routes to malformed hop hints.
|
|
if vertex == self {
|
|
continue
|
|
}
|
|
|
|
// Build reverse lookup to find incoming edges. Needed because
|
|
// search is taken place from target to source.
|
|
for _, additionalEdge := range additionalEdges {
|
|
outgoingEdgePolicy := additionalEdge.EdgePolicy()
|
|
toVertex := outgoingEdgePolicy.ToNodePubKey()
|
|
|
|
incomingEdgePolicy := &edgePolicyWithSource{
|
|
sourceNode: vertex,
|
|
edge: additionalEdge,
|
|
}
|
|
|
|
additionalEdgesWithSrc[toVertex] =
|
|
append(additionalEdgesWithSrc[toVertex],
|
|
incomingEdgePolicy)
|
|
}
|
|
}
|
|
|
|
// The payload size of the final hop differ from intermediate hops
|
|
// and depends on whether the destination is blinded or not.
|
|
lastHopPayloadSize := lastHopPayloadSize(r, finalHtlcExpiry, amt)
|
|
|
|
// We can't always assume that the end destination is publicly
|
|
// advertised to the network so we'll manually include the target node.
|
|
// The target node charges no fee. Distance is set to 0, because this is
|
|
// the starting point of the graph traversal. We are searching backwards
|
|
// to get the fees first time right and correctly match channel
|
|
// bandwidth.
|
|
//
|
|
// Don't record the initial partial path in the distance map and reserve
|
|
// that key for the source key in the case we route to ourselves.
|
|
partialPath := &nodeWithDist{
|
|
dist: 0,
|
|
weight: 0,
|
|
node: target,
|
|
netAmountReceived: amt,
|
|
incomingCltv: finalHtlcExpiry,
|
|
probability: 1,
|
|
routingInfoSize: lastHopPayloadSize,
|
|
}
|
|
|
|
// Calculate the absolute cltv limit. Use uint64 to prevent an overflow
|
|
// if the cltv limit is MaxUint32.
|
|
absoluteCltvLimit := uint64(r.CltvLimit) + uint64(finalHtlcExpiry)
|
|
|
|
// Calculate the default attempt cost as configured globally.
|
|
defaultAttemptCost := float64(
|
|
cfg.AttemptCost +
|
|
amt*lnwire.MilliSatoshi(cfg.AttemptCostPPM)/1000000,
|
|
)
|
|
|
|
// Validate time preference value.
|
|
if math.Abs(timePref) > 1 {
|
|
return nil, 0, fmt.Errorf("time preference %v out of range "+
|
|
"[-1, 1]", timePref)
|
|
}
|
|
|
|
// Scale to avoid the extremes -1 and 1 which run into infinity issues.
|
|
timePref *= 0.9
|
|
|
|
// Apply time preference. At 0, the default attempt cost will
|
|
// be used.
|
|
absoluteAttemptCost := defaultAttemptCost * (1/(0.5-timePref/2) - 1)
|
|
|
|
log.Debugf("Pathfinding absolute attempt cost: %v sats",
|
|
absoluteAttemptCost/1000)
|
|
|
|
// processEdge is a helper closure that will be used to make sure edges
|
|
// satisfy our specific requirements.
|
|
processEdge := func(fromVertex route.Vertex,
|
|
edge *unifiedEdge, toNodeDist *nodeWithDist) {
|
|
|
|
edgesExpanded++
|
|
|
|
// Calculate inbound fee charged by "to" node. The exit hop
|
|
// doesn't charge inbound fees. If the "to" node is the exit
|
|
// hop, its inbound fees have already been set to zero by
|
|
// nodeEdgeUnifier.
|
|
inboundFee := edge.inboundFees.CalcFee(
|
|
toNodeDist.netAmountReceived,
|
|
)
|
|
|
|
// Make sure that the node total fee is never negative.
|
|
// Routing nodes treat a total fee that turns out
|
|
// negative as a zero fee and pathfinding should do the
|
|
// same.
|
|
minInboundFee := -int64(toNodeDist.outboundFee)
|
|
if inboundFee < minInboundFee {
|
|
inboundFee = minInboundFee
|
|
}
|
|
|
|
// Calculate amount that the candidate node would have to send
|
|
// out.
|
|
amountToSend := toNodeDist.netAmountReceived +
|
|
lnwire.MilliSatoshi(inboundFee)
|
|
|
|
// Check if accumulated fees would exceed fee limit when this
|
|
// node would be added to the path.
|
|
totalFee := int64(amountToSend) - int64(amt)
|
|
|
|
log.Trace(lnutils.NewLogClosure(func() string {
|
|
return fmt.Sprintf(
|
|
"Checking fromVertex (%v) with "+
|
|
"minInboundFee=%v, inboundFee=%v, "+
|
|
"amountToSend=%v, amt=%v, totalFee=%v",
|
|
fromVertex, minInboundFee, inboundFee,
|
|
amountToSend, amt, totalFee,
|
|
)
|
|
}))
|
|
|
|
if totalFee > 0 && lnwire.MilliSatoshi(totalFee) > r.FeeLimit {
|
|
return
|
|
}
|
|
|
|
// Request the success probability for this edge.
|
|
edgeProbability := r.ProbabilitySource(
|
|
fromVertex, toNodeDist.node, amountToSend,
|
|
edge.capacity,
|
|
)
|
|
|
|
log.Trace(lnutils.NewLogClosure(func() string {
|
|
return fmt.Sprintf("path finding probability: fromnode=%v,"+
|
|
" tonode=%v, amt=%v, cap=%v, probability=%v",
|
|
fromVertex, toNodeDist.node, amountToSend,
|
|
edge.capacity, edgeProbability)
|
|
}))
|
|
|
|
// If the probability is zero, there is no point in trying.
|
|
if edgeProbability == 0 {
|
|
return
|
|
}
|
|
|
|
// Compute fee that fromVertex is charging. It is based on the
|
|
// amount that needs to be sent to the next node in the route.
|
|
//
|
|
// Source node has no predecessor to pay a fee. Therefore set
|
|
// fee to zero, because it should not be included in the fee
|
|
// limit check and edge weight.
|
|
//
|
|
// Also determine the time lock delta that will be added to the
|
|
// route if fromVertex is selected. If fromVertex is the source
|
|
// node, no additional timelock is required.
|
|
var (
|
|
timeLockDelta uint16
|
|
outboundFee int64
|
|
)
|
|
|
|
if fromVertex != source {
|
|
outboundFee = int64(
|
|
edge.policy.ComputeFee(amountToSend),
|
|
)
|
|
timeLockDelta = edge.policy.TimeLockDelta
|
|
}
|
|
|
|
incomingCltv := toNodeDist.incomingCltv + int32(timeLockDelta)
|
|
|
|
// Check that we are within our CLTV limit.
|
|
if uint64(incomingCltv) > absoluteCltvLimit {
|
|
return
|
|
}
|
|
|
|
// netAmountToReceive is the amount that the node that is added
|
|
// to the distance map needs to receive from a (to be found)
|
|
// previous node in the route. The inbound fee of the receiving
|
|
// node is already subtracted from this value. The previous node
|
|
// will need to pay the amount that this node forwards plus the
|
|
// fee it charges plus this node's inbound fee.
|
|
netAmountToReceive := amountToSend +
|
|
lnwire.MilliSatoshi(outboundFee)
|
|
|
|
// Calculate total probability of successfully reaching target
|
|
// by multiplying the probabilities. Both this edge and the rest
|
|
// of the route must succeed.
|
|
probability := toNodeDist.probability * edgeProbability
|
|
|
|
// If the probability is below the specified lower bound, we can
|
|
// abandon this direction. Adding further nodes can only lower
|
|
// the probability more.
|
|
if probability < cfg.MinProbability {
|
|
return
|
|
}
|
|
|
|
// Calculate the combined fee for this edge. Dijkstra does not
|
|
// support negative edge weights. Because this fee feeds into
|
|
// the edge weight calculation, we don't allow it to be
|
|
// negative.
|
|
signedFee := inboundFee + outboundFee
|
|
fee := lnwire.MilliSatoshi(0)
|
|
if signedFee > 0 {
|
|
fee = lnwire.MilliSatoshi(signedFee)
|
|
}
|
|
|
|
// By adding fromVertex in the route, there will be an extra
|
|
// weight composed of the fee that this node will charge and
|
|
// the amount that will be locked for timeLockDelta blocks in
|
|
// the HTLC that is handed out to fromVertex.
|
|
weight := edgeWeight(amountToSend, fee, timeLockDelta)
|
|
|
|
// Compute the tentative weight to this new channel/edge
|
|
// which is the weight from our toNode to the target node
|
|
// plus the weight of this edge.
|
|
tempWeight := toNodeDist.weight + weight
|
|
|
|
// Add an extra factor to the weight to take into account the
|
|
// probability. Another reason why we rounded the fee up to zero
|
|
// is to prevent a highly negative fee from cancelling out the
|
|
// extra factor. We don't want an always-failing node to attract
|
|
// traffic using a highly negative fee and escape penalization.
|
|
tempDist := getProbabilityBasedDist(
|
|
tempWeight, probability,
|
|
absoluteAttemptCost,
|
|
)
|
|
|
|
// If there is already a best route stored, compare this
|
|
// candidate route with the best route so far.
|
|
current, ok := distance[fromVertex]
|
|
if ok {
|
|
// If this route is worse than what we already found,
|
|
// skip this route.
|
|
if tempDist > current.dist {
|
|
return
|
|
}
|
|
|
|
// If the route is equally good and the probability
|
|
// isn't better, skip this route. It is important to
|
|
// also return if both cost and probability are equal,
|
|
// because otherwise the algorithm could run into an
|
|
// endless loop.
|
|
probNotBetter := probability <= current.probability
|
|
if tempDist == current.dist && probNotBetter {
|
|
return
|
|
}
|
|
}
|
|
|
|
// Calculate the total routing info size if this hop were to be
|
|
// included. If we are coming from the source hop, the payload
|
|
// size is zero, because the original htlc isn't in the onion
|
|
// blob.
|
|
var payloadSize uint64
|
|
if fromVertex != source {
|
|
// In case the unifiedEdge does not have a payload size
|
|
// function supplied we request a graceful shutdown
|
|
// because this should never happen.
|
|
if edge.hopPayloadSizeFn == nil {
|
|
log.Criticalf("No payload size function "+
|
|
"available for edge=%v unable to "+
|
|
"determine payload size: %v", edge,
|
|
ErrNoPayLoadSizeFunc)
|
|
|
|
return
|
|
}
|
|
|
|
payloadSize = edge.hopPayloadSizeFn(
|
|
amountToSend,
|
|
uint32(toNodeDist.incomingCltv),
|
|
edge.policy.ChannelID,
|
|
)
|
|
}
|
|
|
|
routingInfoSize := toNodeDist.routingInfoSize + payloadSize
|
|
// Skip paths that would exceed the maximum routing info size.
|
|
if routingInfoSize > sphinx.MaxPayloadSize {
|
|
return
|
|
}
|
|
|
|
// All conditions are met and this new tentative distance is
|
|
// better than the current best known distance to this node.
|
|
// The new better distance is recorded, and also our "next hop"
|
|
// map is populated with this edge.
|
|
withDist := &nodeWithDist{
|
|
dist: tempDist,
|
|
weight: tempWeight,
|
|
node: fromVertex,
|
|
netAmountReceived: netAmountToReceive,
|
|
outboundFee: lnwire.MilliSatoshi(outboundFee),
|
|
incomingCltv: incomingCltv,
|
|
probability: probability,
|
|
nextHop: edge,
|
|
routingInfoSize: routingInfoSize,
|
|
}
|
|
distance[fromVertex] = withDist
|
|
|
|
// Either push withDist onto the heap if the node
|
|
// represented by fromVertex is not already on the heap OR adjust
|
|
// its position within the heap via heap.Fix.
|
|
nodeHeap.PushOrFix(withDist)
|
|
}
|
|
|
|
// TODO(roasbeef): also add path caching
|
|
// * similar to route caching, but doesn't factor in the amount
|
|
|
|
// Cache features because we visit nodes multiple times.
|
|
featureCache := make(map[route.Vertex]*lnwire.FeatureVector)
|
|
|
|
// getGraphFeatures returns (cached) node features from the graph.
|
|
getGraphFeatures := func(node route.Vertex) (*lnwire.FeatureVector,
|
|
error) {
|
|
|
|
// Check cache for features of the fromNode.
|
|
fromFeatures, ok := featureCache[node]
|
|
if ok {
|
|
return fromFeatures, nil
|
|
}
|
|
|
|
// Fetch node features fresh from the graph.
|
|
fromFeatures, err := g.graph.FetchNodeFeatures(node)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
// Don't route through nodes that contain unknown required
|
|
// features and mark as nil in the cache.
|
|
err = feature.ValidateRequired(fromFeatures)
|
|
if err != nil {
|
|
featureCache[node] = nil
|
|
return nil, nil
|
|
}
|
|
|
|
// Don't route through nodes that don't properly set all
|
|
// transitive feature dependencies and mark as nil in the cache.
|
|
err = feature.ValidateDeps(fromFeatures)
|
|
if err != nil {
|
|
featureCache[node] = nil
|
|
return nil, nil
|
|
}
|
|
|
|
// Update cache.
|
|
featureCache[node] = fromFeatures
|
|
|
|
return fromFeatures, nil
|
|
}
|
|
|
|
routeToSelf := source == target
|
|
for {
|
|
nodesVisited++
|
|
|
|
pivot := partialPath.node
|
|
isExitHop := partialPath.nextHop == nil
|
|
|
|
// Create unified policies for all incoming connections. Don't
|
|
// use inbound fees for the exit hop.
|
|
u := newNodeEdgeUnifier(
|
|
self, pivot, !isExitHop, outgoingChanMap,
|
|
)
|
|
|
|
err := u.addGraphPolicies(g.graph)
|
|
if err != nil {
|
|
return nil, 0, err
|
|
}
|
|
|
|
// We add hop hints that were supplied externally.
|
|
for _, reverseEdge := range additionalEdgesWithSrc[pivot] {
|
|
// Assume zero inbound fees for route hints. If inbound
|
|
// fees would apply, they couldn't be communicated in
|
|
// bolt11 invoices currently.
|
|
inboundFee := models.InboundFee{}
|
|
|
|
// Hop hints don't contain a capacity. We set one here,
|
|
// since a capacity is needed for probability
|
|
// calculations. We set a high capacity to act as if
|
|
// there is enough liquidity, otherwise the hint would
|
|
// not have been added by a wallet.
|
|
// We also pass the payload size function to the
|
|
// graph data so that we calculate the exact payload
|
|
// size when evaluating this hop for a route.
|
|
u.addPolicy(
|
|
reverseEdge.sourceNode,
|
|
reverseEdge.edge.EdgePolicy(),
|
|
inboundFee,
|
|
fakeHopHintCapacity,
|
|
reverseEdge.edge.IntermediatePayloadSize,
|
|
reverseEdge.edge.BlindedPayment(),
|
|
)
|
|
}
|
|
|
|
netAmountReceived := partialPath.netAmountReceived
|
|
|
|
// Expand all connections using the optimal policy for each
|
|
// connection.
|
|
for fromNode, edgeUnifier := range u.edgeUnifiers {
|
|
// The target node is not recorded in the distance map.
|
|
// Therefore we need to have this check to prevent
|
|
// creating a cycle. Only when we intend to route to
|
|
// self, we allow this cycle to form. In that case we'll
|
|
// also break out of the search loop below.
|
|
if !routeToSelf && fromNode == target {
|
|
continue
|
|
}
|
|
|
|
// Apply last hop restriction if set.
|
|
if r.LastHop != nil &&
|
|
pivot == target && fromNode != *r.LastHop {
|
|
|
|
continue
|
|
}
|
|
|
|
edge := edgeUnifier.getEdge(
|
|
netAmountReceived, g.bandwidthHints,
|
|
partialPath.outboundFee,
|
|
)
|
|
|
|
if edge == nil {
|
|
continue
|
|
}
|
|
|
|
// Get feature vector for fromNode.
|
|
fromFeatures, err := getGraphFeatures(fromNode)
|
|
if err != nil {
|
|
return nil, 0, err
|
|
}
|
|
|
|
// If there are no valid features, skip this node.
|
|
if fromFeatures == nil {
|
|
continue
|
|
}
|
|
|
|
// Check if this candidate node is better than what we
|
|
// already have.
|
|
processEdge(fromNode, edge, partialPath)
|
|
}
|
|
|
|
if nodeHeap.Len() == 0 {
|
|
break
|
|
}
|
|
|
|
// Fetch the node within the smallest distance from our source
|
|
// from the heap.
|
|
partialPath = heap.Pop(&nodeHeap).(*nodeWithDist)
|
|
|
|
// If we've reached our source (or we don't have any incoming
|
|
// edges), then we're done here and can exit the graph
|
|
// traversal early.
|
|
if partialPath.node == source {
|
|
break
|
|
}
|
|
}
|
|
|
|
// Use the distance map to unravel the forward path from source to
|
|
// target.
|
|
var pathEdges []*unifiedEdge
|
|
currentNode := source
|
|
for {
|
|
// Determine the next hop forward using the next map.
|
|
currentNodeWithDist, ok := distance[currentNode]
|
|
if !ok {
|
|
// If the node doesn't have a next hop it means we
|
|
// didn't find a path.
|
|
return nil, 0, errNoPathFound
|
|
}
|
|
|
|
// Add the next hop to the list of path edges.
|
|
pathEdges = append(pathEdges, currentNodeWithDist.nextHop)
|
|
|
|
// Advance current node.
|
|
currentNode = currentNodeWithDist.nextHop.policy.ToNodePubKey()
|
|
|
|
// Check stop condition at the end of this loop. This prevents
|
|
// breaking out too soon for self-payments that have target set
|
|
// to source.
|
|
if currentNode == target {
|
|
break
|
|
}
|
|
}
|
|
|
|
// For the final hop, we'll set the node features to those determined
|
|
// above. These are either taken from the destination features, e.g.
|
|
// virtual or invoice features, or loaded as a fallback from the graph.
|
|
// The transitive dependencies were already validated above, so no need
|
|
// to do so now.
|
|
//
|
|
// NOTE: This may overwrite features loaded from the graph if
|
|
// destination features were provided. This is fine though, since our
|
|
// route construction does not care where the features are actually
|
|
// taken from. In the future we may wish to do route construction within
|
|
// findPath, and avoid using ChannelEdgePolicy altogether.
|
|
pathEdges[len(pathEdges)-1].policy.ToNodeFeatures = features
|
|
|
|
log.Debugf("Found route: probability=%v, hops=%v, fee=%v",
|
|
distance[source].probability, len(pathEdges),
|
|
distance[source].netAmountReceived-amt)
|
|
|
|
return pathEdges, distance[source].probability, nil
|
|
}
|
|
|
|
// blindedPathRestrictions are a set of constraints to adhere to when
|
|
// choosing a set of blinded paths to this node.
|
|
type blindedPathRestrictions struct {
|
|
// minNumHops is the minimum number of hops to include in a blinded
|
|
// path. This doesn't include our node, so if the minimum is 1, then
|
|
// the path will contain at minimum our node along with an introduction
|
|
// node hop. A minimum of 0 will include paths where this node is the
|
|
// introduction node and so should be used with caution.
|
|
minNumHops uint8
|
|
|
|
// maxNumHops is the maximum number of hops to include in a blinded
|
|
// path. This doesn't include our node, so if the maximum is 1, then
|
|
// the path will contain our node along with an introduction node hop.
|
|
maxNumHops uint8
|
|
|
|
// nodeOmissionSet holds a set of node IDs of nodes that we should
|
|
// ignore during blinded path selection.
|
|
nodeOmissionSet fn.Set[route.Vertex]
|
|
}
|
|
|
|
// blindedHop holds the information about a hop we have selected for a blinded
|
|
// path.
|
|
type blindedHop struct {
|
|
vertex route.Vertex
|
|
channelID uint64
|
|
edgeCapacity btcutil.Amount
|
|
}
|
|
|
|
// findBlindedPaths does a depth first search from the target node to find a set
|
|
// of blinded paths to the target node given the set of restrictions. This
|
|
// function will select and return any candidate path. A candidate path is a
|
|
// path to the target node with a size determined by the given hop number
|
|
// constraints where all the nodes on the path signal the route blinding feature
|
|
// _and_ the introduction node for the path has more than one public channel.
|
|
// Any filtering of paths based on payment value or success probabilities is
|
|
// left to the caller.
|
|
func findBlindedPaths(g Graph, target route.Vertex,
|
|
restrictions *blindedPathRestrictions) ([][]blindedHop, error) {
|
|
|
|
// Sanity check the restrictions.
|
|
if restrictions.minNumHops > restrictions.maxNumHops {
|
|
return nil, fmt.Errorf("maximum number of blinded path hops "+
|
|
"(%d) must be greater than or equal to the minimum "+
|
|
"number of hops (%d)", restrictions.maxNumHops,
|
|
restrictions.minNumHops)
|
|
}
|
|
|
|
// If the node is not the destination node, then it is required that the
|
|
// node advertise the route blinding feature-bit in order for it to be
|
|
// chosen as a node on the blinded path.
|
|
supportsRouteBlinding := func(node route.Vertex) (bool, error) {
|
|
if node == target {
|
|
return true, nil
|
|
}
|
|
|
|
features, err := g.FetchNodeFeatures(node)
|
|
if err != nil {
|
|
return false, err
|
|
}
|
|
|
|
return features.HasFeature(lnwire.RouteBlindingOptional), nil
|
|
}
|
|
|
|
// This function will have some recursion. We will spin out from the
|
|
// target node & append edges to the paths until we reach various exit
|
|
// conditions such as: The maxHops number being reached or reaching
|
|
// a node that doesn't have any other edges - in that final case, the
|
|
// whole path should be ignored.
|
|
paths, _, err := processNodeForBlindedPath(
|
|
g, target, supportsRouteBlinding, nil, restrictions,
|
|
)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
// Reverse each path so that the order is correct (from introduction
|
|
// node to last hop node) and then append this node on as the
|
|
// destination of each path.
|
|
orderedPaths := make([][]blindedHop, len(paths))
|
|
for i, path := range paths {
|
|
sort.Slice(path, func(i, j int) bool {
|
|
return j < i
|
|
})
|
|
|
|
orderedPaths[i] = append(path, blindedHop{vertex: target})
|
|
}
|
|
|
|
// Handle the special case that allows a blinded path with the
|
|
// introduction node as the destination node.
|
|
if restrictions.minNumHops == 0 {
|
|
singleHopPath := [][]blindedHop{{{vertex: target}}}
|
|
|
|
//nolint:makezero
|
|
orderedPaths = append(
|
|
orderedPaths, singleHopPath...,
|
|
)
|
|
}
|
|
|
|
return orderedPaths, err
|
|
}
|
|
|
|
// processNodeForBlindedPath is a recursive function that traverses the graph
|
|
// in a depth first manner searching for a set of blinded paths to the given
|
|
// node.
|
|
func processNodeForBlindedPath(g Graph, node route.Vertex,
|
|
supportsRouteBlinding func(vertex route.Vertex) (bool, error),
|
|
alreadyVisited map[route.Vertex]bool,
|
|
restrictions *blindedPathRestrictions) ([][]blindedHop, bool, error) {
|
|
|
|
// If we have already visited the maximum number of hops, then this path
|
|
// is complete and we can exit now.
|
|
if len(alreadyVisited) > int(restrictions.maxNumHops) {
|
|
return nil, false, nil
|
|
}
|
|
|
|
// If we have already visited this peer on this path, then we skip
|
|
// processing it again.
|
|
if alreadyVisited[node] {
|
|
return nil, false, nil
|
|
}
|
|
|
|
// If we have explicitly been told to ignore this node for blinded paths
|
|
// then we skip it too.
|
|
if restrictions.nodeOmissionSet.Contains(node) {
|
|
return nil, false, nil
|
|
}
|
|
|
|
supports, err := supportsRouteBlinding(node)
|
|
if err != nil {
|
|
return nil, false, err
|
|
}
|
|
if !supports {
|
|
return nil, false, nil
|
|
}
|
|
|
|
// At this point, copy the alreadyVisited map.
|
|
visited := make(map[route.Vertex]bool, len(alreadyVisited))
|
|
for r := range alreadyVisited {
|
|
visited[r] = true
|
|
}
|
|
|
|
// Add this node the visited set.
|
|
visited[node] = true
|
|
|
|
var (
|
|
hopSets [][]blindedHop
|
|
chanCount int
|
|
)
|
|
|
|
// Now, iterate over the node's channels in search for paths to this
|
|
// node that can be used for blinded paths
|
|
err = g.ForEachNodeChannel(node,
|
|
func(channel *channeldb.DirectedChannel) error {
|
|
// Keep track of how many incoming channels this node
|
|
// has. We only use a node as an introduction node if it
|
|
// has channels other than the one that lead us to it.
|
|
chanCount++
|
|
|
|
// Process each channel peer to gather any paths that
|
|
// lead to the peer.
|
|
nextPaths, hasMoreChans, err := processNodeForBlindedPath( //nolint:lll
|
|
g, channel.OtherNode, supportsRouteBlinding,
|
|
visited, restrictions,
|
|
)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
hop := blindedHop{
|
|
vertex: channel.OtherNode,
|
|
channelID: channel.ChannelID,
|
|
edgeCapacity: channel.Capacity,
|
|
}
|
|
|
|
// For each of the paths returned, unwrap them and
|
|
// append this hop to them.
|
|
for _, path := range nextPaths {
|
|
hopSets = append(
|
|
hopSets,
|
|
append([]blindedHop{hop}, path...),
|
|
)
|
|
}
|
|
|
|
// If this node does have channels other than the one
|
|
// that lead to it, and if the hop count up to this node
|
|
// meets the minHop requirement, then we also add a
|
|
// path that starts at this node.
|
|
if hasMoreChans &&
|
|
len(visited) >= int(restrictions.minNumHops) {
|
|
|
|
hopSets = append(hopSets, []blindedHop{hop})
|
|
}
|
|
|
|
return nil
|
|
},
|
|
)
|
|
if err != nil {
|
|
return nil, false, err
|
|
}
|
|
|
|
return hopSets, chanCount > 1, nil
|
|
}
|
|
|
|
// getProbabilityBasedDist converts a weight into a distance that takes into
|
|
// account the success probability and the (virtual) cost of a failed payment
|
|
// attempt.
|
|
//
|
|
// Derivation:
|
|
//
|
|
// Suppose there are two routes A and B with fees Fa and Fb and success
|
|
// probabilities Pa and Pb.
|
|
//
|
|
// Is the expected cost of trying route A first and then B lower than trying the
|
|
// other way around?
|
|
//
|
|
// The expected cost of A-then-B is: Pa*Fa + (1-Pa)*Pb*(c+Fb)
|
|
//
|
|
// The expected cost of B-then-A is: Pb*Fb + (1-Pb)*Pa*(c+Fa)
|
|
//
|
|
// In these equations, the term representing the case where both A and B fail is
|
|
// left out because its value would be the same in both cases.
|
|
//
|
|
// Pa*Fa + (1-Pa)*Pb*(c+Fb) < Pb*Fb + (1-Pb)*Pa*(c+Fa)
|
|
//
|
|
// Pa*Fa + Pb*c + Pb*Fb - Pa*Pb*c - Pa*Pb*Fb < Pb*Fb + Pa*c + Pa*Fa - Pa*Pb*c - Pa*Pb*Fa
|
|
//
|
|
// Removing terms that cancel out:
|
|
// Pb*c - Pa*Pb*Fb < Pa*c - Pa*Pb*Fa
|
|
//
|
|
// Divide by Pa*Pb:
|
|
// c/Pa - Fb < c/Pb - Fa
|
|
//
|
|
// Move terms around:
|
|
// Fa + c/Pa < Fb + c/Pb
|
|
//
|
|
// So the value of F + c/P can be used to compare routes.
|
|
func getProbabilityBasedDist(weight int64, probability float64,
|
|
penalty float64) int64 {
|
|
|
|
// Prevent divide by zero by returning early.
|
|
if probability == 0 {
|
|
return infinity
|
|
}
|
|
|
|
// Calculate distance.
|
|
dist := float64(weight) + penalty/probability
|
|
|
|
// Avoid cast if an overflow would occur. The maxFloat constant is
|
|
// chosen to stay well below the maximum float64 value that is still
|
|
// convertible to int64.
|
|
const maxFloat = 9000000000000000000
|
|
if dist > maxFloat {
|
|
return infinity
|
|
}
|
|
|
|
return int64(dist)
|
|
}
|
|
|
|
// lastHopPayloadSize calculates the payload size of the final hop in a route.
|
|
// It depends on the tlv types which are present and also whether the hop is
|
|
// part of a blinded route or not.
|
|
func lastHopPayloadSize(r *RestrictParams, finalHtlcExpiry int32,
|
|
amount lnwire.MilliSatoshi) uint64 {
|
|
|
|
if r.BlindedPaymentPathSet != nil {
|
|
paymentPath := r.BlindedPaymentPathSet.
|
|
LargestLastHopPayloadPath()
|
|
blindedPath := paymentPath.BlindedPath.BlindedHops
|
|
blindedPoint := paymentPath.BlindedPath.BlindingPoint
|
|
|
|
encryptedData := blindedPath[len(blindedPath)-1].CipherText
|
|
finalHop := route.Hop{
|
|
AmtToForward: amount,
|
|
OutgoingTimeLock: uint32(finalHtlcExpiry),
|
|
EncryptedData: encryptedData,
|
|
}
|
|
if len(blindedPath) == 1 {
|
|
finalHop.BlindingPoint = blindedPoint
|
|
}
|
|
|
|
// The final hop does not have a short chanID set.
|
|
return finalHop.PayloadSize(0)
|
|
}
|
|
|
|
var mpp *record.MPP
|
|
r.PaymentAddr.WhenSome(func(addr [32]byte) {
|
|
mpp = record.NewMPP(amount, addr)
|
|
})
|
|
|
|
var amp *record.AMP
|
|
if r.Amp != nil {
|
|
// The AMP payload is not easy accessible at this point but we
|
|
// are only interested in the size of the payload so we just use
|
|
// the AMP record dummy.
|
|
amp = &record.MaxAmpPayLoadSize
|
|
}
|
|
|
|
finalHop := route.Hop{
|
|
AmtToForward: amount,
|
|
OutgoingTimeLock: uint32(finalHtlcExpiry),
|
|
CustomRecords: r.DestCustomRecords,
|
|
MPP: mpp,
|
|
AMP: amp,
|
|
Metadata: r.Metadata,
|
|
}
|
|
|
|
// The final hop does not have a short chanID set.
|
|
return finalHop.PayloadSize(0)
|
|
}
|
|
|
|
// overflowSafeAdd adds two MilliSatoshi values and returns the result. If an
|
|
// overflow could occur, zero is returned instead and the boolean is set to
|
|
// true.
|
|
func overflowSafeAdd(x, y lnwire.MilliSatoshi) (lnwire.MilliSatoshi, bool) {
|
|
if y > math.MaxUint64-x {
|
|
// Overflow would occur, return 0 and set overflow flag.
|
|
return 0, true
|
|
}
|
|
|
|
return x + y, false
|
|
}
|