mirror of
https://github.com/lightningnetwork/lnd.git
synced 2025-01-18 13:27:56 +01:00
9f54ec90aa
All the structs defined in the `channeldb/models` package are graph related. So once we move all the graph CRUD code to the graph package, it makes sense to have the schema structs there too. So this just moves the `models` package over to `graph/db/models`.
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/feature"
|
|
"github.com/lightningnetwork/lnd/fn"
|
|
"github.com/lightningnetwork/lnd/graph/db/models"
|
|
"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
|
|
}
|