mirror of
https://github.com/lightningnetwork/lnd.git
synced 2025-01-18 13:27:56 +01:00
25f7b1c362
We will not add a buffer to the chan policy for blinded paths in case the sender amount violates the minHTLC restriction in the first place. Moreover we disgard a route fast if the payment amount is smaller than the minHTLC along the route.
1020 lines
32 KiB
Go
1020 lines
32 KiB
Go
package blindedpath
|
|
|
|
import (
|
|
"bytes"
|
|
"errors"
|
|
"fmt"
|
|
"math"
|
|
"sort"
|
|
|
|
"github.com/btcsuite/btcd/btcec/v2"
|
|
"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/lnwire"
|
|
"github.com/lightningnetwork/lnd/record"
|
|
"github.com/lightningnetwork/lnd/routing/route"
|
|
"github.com/lightningnetwork/lnd/tlv"
|
|
"github.com/lightningnetwork/lnd/zpay32"
|
|
)
|
|
|
|
const (
|
|
// oneMillion is a constant used frequently in fee rate calculations.
|
|
oneMillion = uint32(1_000_000)
|
|
)
|
|
|
|
// errInvalidBlindedPath indicates that the chosen real path is not usable as
|
|
// a blinded path.
|
|
var errInvalidBlindedPath = errors.New("the chosen path results in an " +
|
|
"unusable blinded path")
|
|
|
|
// BuildBlindedPathCfg defines the various resources and configuration values
|
|
// required to build a blinded payment path to this node.
|
|
type BuildBlindedPathCfg struct {
|
|
// FindRoutes returns a set of routes to us that can be used for the
|
|
// construction of blinded paths. These routes will consist of real
|
|
// nodes advertising the route blinding feature bit. They may be of
|
|
// various lengths and may even contain only a single hop. Any route
|
|
// shorter than MinNumHops will be padded with dummy hops during route
|
|
// construction.
|
|
FindRoutes func(value lnwire.MilliSatoshi) ([]*route.Route, error)
|
|
|
|
// FetchChannelEdgesByID attempts to look up the two directed edges for
|
|
// the channel identified by the channel ID.
|
|
FetchChannelEdgesByID func(chanID uint64) (*models.ChannelEdgeInfo,
|
|
*models.ChannelEdgePolicy, *models.ChannelEdgePolicy, error)
|
|
|
|
// FetchOurOpenChannels fetches this node's set of open channels.
|
|
FetchOurOpenChannels func() ([]*channeldb.OpenChannel, error)
|
|
|
|
// BestHeight can be used to fetch the best block height that this node
|
|
// is aware of.
|
|
BestHeight func() (uint32, error)
|
|
|
|
// AddPolicyBuffer is a function that can be used to alter the policy
|
|
// values of the given channel edge. The main reason for doing this is
|
|
// to add a safety buffer so that if the node makes small policy changes
|
|
// during the lifetime of the blinded path, then the path remains valid
|
|
// and so probing is more difficult. Note that this will only be called
|
|
// for the policies of real nodes and won't be applied to
|
|
// DefaultDummyHopPolicy.
|
|
AddPolicyBuffer func(policy *BlindedHopPolicy) (*BlindedHopPolicy,
|
|
error)
|
|
|
|
// PathID is the secret data to embed in the blinded path data that we
|
|
// will receive back as the recipient. This is the equivalent of the
|
|
// payment address used in normal payments. It lets the recipient check
|
|
// that the path is being used in the correct context.
|
|
PathID []byte
|
|
|
|
// ValueMsat is the payment amount in milli-satoshis that must be
|
|
// routed. This will be used for selecting appropriate routes to use for
|
|
// the blinded path.
|
|
ValueMsat lnwire.MilliSatoshi
|
|
|
|
// MinFinalCLTVExpiryDelta is the minimum CLTV delta that the recipient
|
|
// requires for the final hop of the payment.
|
|
//
|
|
// NOTE that the caller is responsible for adding additional block
|
|
// padding to this value to account for blocks being mined while the
|
|
// payment is in-flight.
|
|
MinFinalCLTVExpiryDelta uint32
|
|
|
|
// BlocksUntilExpiry is the number of blocks that this blinded path
|
|
// should remain valid for. This is a relative number of blocks. This
|
|
// number in addition with a potential minimum cltv delta for the last
|
|
// hop and some block padding will be the payment constraint which is
|
|
// part of the blinded hop info. Every htlc using the provided blinded
|
|
// hops cannot have a higher cltv delta otherwise it will get rejected
|
|
// by the forwarding nodes or the final node.
|
|
//
|
|
// This number should at least be greater than the invoice expiry time
|
|
// so that the blinded route is always valid as long as the invoice is
|
|
// valid.
|
|
BlocksUntilExpiry uint32
|
|
|
|
// MinNumHops is the minimum number of hops that each blinded path
|
|
// should be. If the number of hops in a path returned by FindRoutes is
|
|
// less than this number, then dummy hops will be post-fixed to the
|
|
// route.
|
|
MinNumHops uint8
|
|
|
|
// DefaultDummyHopPolicy holds the policy values that should be used for
|
|
// dummy hops in the cases where it cannot be derived via other means
|
|
// such as averaging the policy values of other hops on the path. This
|
|
// would happen in the case where the introduction node is also the
|
|
// introduction node. If these default policy values are used, then
|
|
// the MaxHTLCMsat value must be carefully chosen.
|
|
DefaultDummyHopPolicy *BlindedHopPolicy
|
|
}
|
|
|
|
// BuildBlindedPaymentPaths uses the passed config to construct a set of blinded
|
|
// payment paths that can be added to the invoice.
|
|
func BuildBlindedPaymentPaths(cfg *BuildBlindedPathCfg) (
|
|
[]*zpay32.BlindedPaymentPath, error) {
|
|
|
|
// Find some appropriate routes for the value to be routed. This will
|
|
// return a set of routes made up of real nodes.
|
|
routes, err := cfg.FindRoutes(cfg.ValueMsat)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
if len(routes) == 0 {
|
|
return nil, fmt.Errorf("could not find any routes to self to " +
|
|
"use for blinded route construction")
|
|
}
|
|
|
|
// Not every route returned will necessarily result in a usable blinded
|
|
// path and so the number of paths returned might be less than the
|
|
// number of real routes returned by FindRoutes above.
|
|
paths := make([]*zpay32.BlindedPaymentPath, 0, len(routes))
|
|
|
|
// For each route returned, we will construct the associated blinded
|
|
// payment path.
|
|
for _, route := range routes {
|
|
// Extract the information we need from the route.
|
|
candidatePath := extractCandidatePath(route)
|
|
|
|
// Pad the given route with dummy hops until the minimum number
|
|
// of hops is met.
|
|
candidatePath.padWithDummyHops(cfg.MinNumHops)
|
|
|
|
path, err := buildBlindedPaymentPath(cfg, candidatePath)
|
|
if errors.Is(err, errInvalidBlindedPath) {
|
|
log.Debugf("Not using route (%s) as a blinded path "+
|
|
"since it resulted in an invalid blinded path",
|
|
route)
|
|
|
|
continue
|
|
} else if err != nil {
|
|
log.Errorf("Not using route (%s) as a blinded path: %v",
|
|
route, err)
|
|
|
|
continue
|
|
}
|
|
|
|
log.Debugf("Route selected for blinded path: %s", candidatePath)
|
|
|
|
paths = append(paths, path)
|
|
}
|
|
|
|
if len(paths) == 0 {
|
|
return nil, fmt.Errorf("could not build any blinded paths")
|
|
}
|
|
|
|
return paths, nil
|
|
}
|
|
|
|
// buildBlindedPaymentPath takes a route from an introduction node to this node
|
|
// and uses the given config to convert it into a blinded payment path.
|
|
func buildBlindedPaymentPath(cfg *BuildBlindedPathCfg, path *candidatePath) (
|
|
*zpay32.BlindedPaymentPath, error) {
|
|
|
|
hops, minHTLC, maxHTLC, err := collectRelayInfo(cfg, path)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("could not collect blinded path relay "+
|
|
"info: %w", err)
|
|
}
|
|
|
|
relayInfo := make([]*record.PaymentRelayInfo, len(hops))
|
|
for i, hop := range hops {
|
|
relayInfo[i] = hop.relayInfo
|
|
}
|
|
|
|
// Using the collected relay info, we can calculate the aggregated
|
|
// policy values for the route.
|
|
baseFee, feeRate, cltvDelta := calcBlindedPathPolicies(
|
|
relayInfo, uint16(cfg.MinFinalCLTVExpiryDelta),
|
|
)
|
|
|
|
currentHeight, err := cfg.BestHeight()
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
// The next step is to calculate the payment constraints to communicate
|
|
// to each hop and to package up the hop info for each hop. We will
|
|
// handle the final hop first since its payload looks a bit different,
|
|
// and then we will iterate backwards through the remaining hops.
|
|
//
|
|
// Note that the +1 here is required because the route won't have the
|
|
// introduction node included in the "Hops". But since we want to create
|
|
// payloads for all the hops as well as the introduction node, we add 1
|
|
// here to get the full hop length along with the introduction node.
|
|
hopDataSet := make([]*hopData, 0, len(path.hops)+1)
|
|
|
|
// Determine the maximum CLTV expiry for the destination node.
|
|
cltvExpiry := currentHeight + cfg.BlocksUntilExpiry +
|
|
cfg.MinFinalCLTVExpiryDelta
|
|
|
|
constraints := &record.PaymentConstraints{
|
|
MaxCltvExpiry: cltvExpiry,
|
|
HtlcMinimumMsat: minHTLC,
|
|
}
|
|
|
|
// If the blinded route has only a source node (introduction node) and
|
|
// no hops, then the destination node is also the source node.
|
|
finalHopPubKey := path.introNode
|
|
if len(path.hops) > 0 {
|
|
finalHopPubKey = path.hops[len(path.hops)-1].pubKey
|
|
}
|
|
|
|
// For the final hop, we only send it the path ID and payment
|
|
// constraints.
|
|
info, err := buildFinalHopRouteData(
|
|
finalHopPubKey, cfg.PathID, constraints,
|
|
)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
hopDataSet = append(hopDataSet, info)
|
|
|
|
// Iterate through the remaining (non-final) hops, back to front.
|
|
for i := len(hops) - 1; i >= 0; i-- {
|
|
hop := hops[i]
|
|
|
|
cltvExpiry += uint32(hop.relayInfo.CltvExpiryDelta)
|
|
|
|
constraints = &record.PaymentConstraints{
|
|
MaxCltvExpiry: cltvExpiry,
|
|
HtlcMinimumMsat: minHTLC,
|
|
}
|
|
|
|
var info *hopData
|
|
if hop.nextHopIsDummy {
|
|
info, err = buildDummyRouteData(
|
|
hop.hopPubKey, hop.relayInfo, constraints,
|
|
)
|
|
} else {
|
|
info, err = buildHopRouteData(
|
|
hop.hopPubKey, hop.nextSCID, hop.relayInfo,
|
|
constraints,
|
|
)
|
|
}
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
hopDataSet = append(hopDataSet, info)
|
|
}
|
|
|
|
// Sort the hop info list in reverse order so that the data for the
|
|
// introduction node is first.
|
|
sort.Slice(hopDataSet, func(i, j int) bool {
|
|
return j < i
|
|
})
|
|
|
|
// Add padding to each route data instance until the encrypted data
|
|
// blobs are all the same size.
|
|
paymentPath, _, err := padHopInfo(
|
|
hopDataSet, true, record.AverageDummyHopPayloadSize,
|
|
)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
// Derive an ephemeral session key.
|
|
sessionKey, err := btcec.NewPrivateKey()
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
// Encrypt the hop info.
|
|
blindedPath, err := sphinx.BuildBlindedPath(sessionKey, paymentPath)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
if len(blindedPath.BlindedHops) < 1 {
|
|
return nil, fmt.Errorf("blinded path must have at least one " +
|
|
"hop")
|
|
}
|
|
|
|
// Overwrite the introduction point's blinded pub key with the real
|
|
// pub key since then we can use this more compact format in the
|
|
// invoice without needing to encode the un-used blinded node pub key of
|
|
// the intro node.
|
|
blindedPath.BlindedHops[0].BlindedNodePub =
|
|
blindedPath.IntroductionPoint
|
|
|
|
// Now construct a z32 blinded path.
|
|
return &zpay32.BlindedPaymentPath{
|
|
FeeBaseMsat: uint32(baseFee),
|
|
FeeRate: feeRate,
|
|
CltvExpiryDelta: cltvDelta,
|
|
HTLCMinMsat: uint64(minHTLC),
|
|
HTLCMaxMsat: uint64(maxHTLC),
|
|
Features: lnwire.EmptyFeatureVector(),
|
|
FirstEphemeralBlindingPoint: blindedPath.BlindingPoint,
|
|
Hops: blindedPath.BlindedHops,
|
|
}, nil
|
|
}
|
|
|
|
// hopRelayInfo packages together the relay info to send to hop on a blinded
|
|
// path along with the pub key of that hop and the SCID that the hop should
|
|
// forward the payment on to.
|
|
type hopRelayInfo struct {
|
|
hopPubKey route.Vertex
|
|
nextSCID lnwire.ShortChannelID
|
|
relayInfo *record.PaymentRelayInfo
|
|
nextHopIsDummy bool
|
|
}
|
|
|
|
// collectRelayInfo collects the relay policy rules for each relay hop on the
|
|
// route and applies any policy buffers.
|
|
//
|
|
// For the blinded route:
|
|
//
|
|
// C --chan(CB)--> B --chan(BA)--> A
|
|
//
|
|
// where C is the introduction node, the route.Route struct we are given will
|
|
// have SourcePubKey set to C's pub key, and then it will have the following
|
|
// route.Hops:
|
|
//
|
|
// - PubKeyBytes: B, ChannelID: chan(CB)
|
|
// - PubKeyBytes: A, ChannelID: chan(BA)
|
|
//
|
|
// We, however, want to collect the channel policies for the following PubKey
|
|
// and ChannelID pairs:
|
|
//
|
|
// - PubKey: C, ChannelID: chan(CB)
|
|
// - PubKey: B, ChannelID: chan(BA)
|
|
//
|
|
// Therefore, when we go through the route and its hops to collect policies, our
|
|
// index for collecting public keys will be trailing that of the channel IDs by
|
|
// 1.
|
|
//
|
|
// For any dummy hops on the route, this function also decides what to use as
|
|
// policy values for the dummy hops. If there are other real hops, then the
|
|
// dummy hop policy values are derived by taking the average of the real
|
|
// policy values. If there are no real hops (in other words we are the
|
|
// introduction node), then we use some default routing values and we use the
|
|
// average of our channel capacities for the MaxHTLC value.
|
|
func collectRelayInfo(cfg *BuildBlindedPathCfg, path *candidatePath) (
|
|
[]*hopRelayInfo, lnwire.MilliSatoshi, lnwire.MilliSatoshi, error) {
|
|
|
|
var (
|
|
// The first pub key is that of the introduction node.
|
|
hopSource = path.introNode
|
|
|
|
// A collection of the policy values of real hops on the path.
|
|
policies = make(map[uint64]*BlindedHopPolicy)
|
|
|
|
hasDummyHops bool
|
|
)
|
|
|
|
// On this first iteration, we just collect policy values of the real
|
|
// hops on the path.
|
|
for _, hop := range path.hops {
|
|
// Once we have hit a dummy hop, all hops after will be dummy
|
|
// hops too.
|
|
if hop.isDummy {
|
|
hasDummyHops = true
|
|
|
|
break
|
|
}
|
|
|
|
// For real hops, retrieve the channel policy for this hop's
|
|
// channel ID in the direction pointing away from the hopSource
|
|
// node.
|
|
policy, err := getNodeChannelPolicy(
|
|
cfg, hop.channelID, hopSource,
|
|
)
|
|
if err != nil {
|
|
return nil, 0, 0, err
|
|
}
|
|
|
|
policies[hop.channelID] = policy
|
|
|
|
// This hop's pub key will be the policy creator for the next
|
|
// hop.
|
|
hopSource = hop.pubKey
|
|
}
|
|
|
|
var (
|
|
dummyHopPolicy *BlindedHopPolicy
|
|
err error
|
|
)
|
|
|
|
// If the path does have dummy hops, we need to decide which policy
|
|
// values to use for these hops.
|
|
if hasDummyHops {
|
|
dummyHopPolicy, err = computeDummyHopPolicy(
|
|
cfg.DefaultDummyHopPolicy, cfg.FetchOurOpenChannels,
|
|
policies,
|
|
)
|
|
if err != nil {
|
|
return nil, 0, 0, err
|
|
}
|
|
}
|
|
|
|
// We iterate through the hops one more time. This time it is to
|
|
// buffer the policy values, collect the payment relay info to send to
|
|
// each hop, and to compute the min and max HTLC values for the path.
|
|
var (
|
|
hops = make([]*hopRelayInfo, 0, len(path.hops))
|
|
minHTLC lnwire.MilliSatoshi
|
|
maxHTLC lnwire.MilliSatoshi
|
|
)
|
|
// The first pub key is that of the introduction node.
|
|
hopSource = path.introNode
|
|
for _, hop := range path.hops {
|
|
var (
|
|
policy = dummyHopPolicy
|
|
ok bool
|
|
err error
|
|
)
|
|
|
|
if !hop.isDummy {
|
|
policy, ok = policies[hop.channelID]
|
|
if !ok {
|
|
return nil, 0, 0, fmt.Errorf("no cached "+
|
|
"policy found for channel ID: %d",
|
|
hop.channelID)
|
|
}
|
|
}
|
|
|
|
if policy.MinHTLCMsat > cfg.ValueMsat {
|
|
return nil, 0, 0, fmt.Errorf("%w: minHTLC of hop "+
|
|
"policy larger than payment amt: sentAmt(%v), "+
|
|
"minHTLC(%v)", errInvalidBlindedPath,
|
|
cfg.ValueMsat, policy.MinHTLCMsat)
|
|
}
|
|
|
|
bufferPolicy, err := cfg.AddPolicyBuffer(policy)
|
|
if err != nil {
|
|
return nil, 0, 0, err
|
|
}
|
|
|
|
// We only use the new buffered policy if the new minHTLC value
|
|
// does not violate the sender amount.
|
|
//
|
|
// NOTE: We don't check this for maxHTLC, because the payment
|
|
// amount can always be splitted using MPP.
|
|
if bufferPolicy.MinHTLCMsat <= cfg.ValueMsat {
|
|
policy = bufferPolicy
|
|
}
|
|
|
|
// If this is the first policy we are collecting, then use this
|
|
// policy to set the base values for min/max htlc.
|
|
if len(hops) == 0 {
|
|
minHTLC = policy.MinHTLCMsat
|
|
maxHTLC = policy.MaxHTLCMsat
|
|
} else {
|
|
if policy.MinHTLCMsat > minHTLC {
|
|
minHTLC = policy.MinHTLCMsat
|
|
}
|
|
|
|
if policy.MaxHTLCMsat < maxHTLC {
|
|
maxHTLC = policy.MaxHTLCMsat
|
|
}
|
|
}
|
|
|
|
// From the policy values for this hop, we can collect the
|
|
// payment relay info that we will send to this hop.
|
|
hops = append(hops, &hopRelayInfo{
|
|
hopPubKey: hopSource,
|
|
nextSCID: lnwire.NewShortChanIDFromInt(hop.channelID),
|
|
relayInfo: &record.PaymentRelayInfo{
|
|
FeeRate: policy.FeeRate,
|
|
BaseFee: policy.BaseFee,
|
|
CltvExpiryDelta: policy.CLTVExpiryDelta,
|
|
},
|
|
nextHopIsDummy: hop.isDummy,
|
|
})
|
|
|
|
// This hop's pub key will be the policy creator for the next
|
|
// hop.
|
|
hopSource = hop.pubKey
|
|
}
|
|
|
|
// It can happen that there is no HTLC-range overlap between the various
|
|
// hops along the path. We return errInvalidBlindedPath to indicate that
|
|
// this route was not usable
|
|
if minHTLC > maxHTLC {
|
|
return nil, 0, 0, fmt.Errorf("%w: resulting blinded path min "+
|
|
"HTLC value is larger than the resulting max HTLC "+
|
|
"value", errInvalidBlindedPath)
|
|
}
|
|
|
|
return hops, minHTLC, maxHTLC, nil
|
|
}
|
|
|
|
// buildDummyRouteData constructs the record.BlindedRouteData struct for the
|
|
// given a hop in a blinded route where the following hop is a dummy hop.
|
|
func buildDummyRouteData(node route.Vertex, relayInfo *record.PaymentRelayInfo,
|
|
constraints *record.PaymentConstraints) (*hopData, error) {
|
|
|
|
nodeID, err := btcec.ParsePubKey(node[:])
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
return &hopData{
|
|
data: record.NewDummyHopRouteData(
|
|
nodeID, *relayInfo, *constraints,
|
|
),
|
|
nodeID: nodeID,
|
|
}, nil
|
|
}
|
|
|
|
// computeDummyHopPolicy determines policy values to use for a dummy hop on a
|
|
// blinded path. If other real policy values exist, then we use the average of
|
|
// those values for the dummy hop policy values. Otherwise, in the case were
|
|
// there are no real policy values due to this node being the introduction node,
|
|
// we use the provided default policy values, and we get the average capacity of
|
|
// this node's channels to compute a MaxHTLC value.
|
|
func computeDummyHopPolicy(defaultPolicy *BlindedHopPolicy,
|
|
fetchOurChannels func() ([]*channeldb.OpenChannel, error),
|
|
policies map[uint64]*BlindedHopPolicy) (*BlindedHopPolicy, error) {
|
|
|
|
numPolicies := len(policies)
|
|
|
|
// If there are no real policies to calculate an average policy from,
|
|
// then we use the default. The only thing we need to calculate here
|
|
// though is the MaxHTLC value.
|
|
if numPolicies == 0 {
|
|
chans, err := fetchOurChannels()
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
if len(chans) == 0 {
|
|
return nil, fmt.Errorf("node has no channels to " +
|
|
"receive on")
|
|
}
|
|
|
|
// Calculate the average channel capacity and use this as the
|
|
// MaxHTLC value.
|
|
var maxHTLC btcutil.Amount
|
|
for _, c := range chans {
|
|
maxHTLC += c.Capacity
|
|
}
|
|
|
|
maxHTLC = btcutil.Amount(float64(maxHTLC) / float64(len(chans)))
|
|
|
|
return &BlindedHopPolicy{
|
|
CLTVExpiryDelta: defaultPolicy.CLTVExpiryDelta,
|
|
FeeRate: defaultPolicy.FeeRate,
|
|
BaseFee: defaultPolicy.BaseFee,
|
|
MinHTLCMsat: defaultPolicy.MinHTLCMsat,
|
|
MaxHTLCMsat: lnwire.NewMSatFromSatoshis(maxHTLC),
|
|
}, nil
|
|
}
|
|
|
|
var avgPolicy BlindedHopPolicy
|
|
|
|
for _, policy := range policies {
|
|
avgPolicy.MinHTLCMsat += policy.MinHTLCMsat
|
|
avgPolicy.MaxHTLCMsat += policy.MaxHTLCMsat
|
|
avgPolicy.BaseFee += policy.BaseFee
|
|
avgPolicy.FeeRate += policy.FeeRate
|
|
avgPolicy.CLTVExpiryDelta += policy.CLTVExpiryDelta
|
|
}
|
|
|
|
avgPolicy.MinHTLCMsat = lnwire.MilliSatoshi(
|
|
float64(avgPolicy.MinHTLCMsat) / float64(numPolicies),
|
|
)
|
|
avgPolicy.MaxHTLCMsat = lnwire.MilliSatoshi(
|
|
float64(avgPolicy.MaxHTLCMsat) / float64(numPolicies),
|
|
)
|
|
avgPolicy.BaseFee = lnwire.MilliSatoshi(
|
|
float64(avgPolicy.BaseFee) / float64(numPolicies),
|
|
)
|
|
avgPolicy.FeeRate = uint32(
|
|
float64(avgPolicy.FeeRate) / float64(numPolicies),
|
|
)
|
|
avgPolicy.CLTVExpiryDelta = uint16(
|
|
float64(avgPolicy.CLTVExpiryDelta) / float64(numPolicies),
|
|
)
|
|
|
|
return &avgPolicy, nil
|
|
}
|
|
|
|
// buildHopRouteData constructs the record.BlindedRouteData struct for the given
|
|
// non-final hop on a blinded path and packages it with the node's ID.
|
|
func buildHopRouteData(node route.Vertex, scid lnwire.ShortChannelID,
|
|
relayInfo *record.PaymentRelayInfo,
|
|
constraints *record.PaymentConstraints) (*hopData, error) {
|
|
|
|
// Wrap up the data we want to send to this hop.
|
|
blindedRouteHopData := record.NewNonFinalBlindedRouteData(
|
|
scid, nil, *relayInfo, constraints, nil,
|
|
)
|
|
|
|
nodeID, err := btcec.ParsePubKey(node[:])
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
return &hopData{
|
|
data: blindedRouteHopData,
|
|
nodeID: nodeID,
|
|
}, nil
|
|
}
|
|
|
|
// buildFinalHopRouteData constructs the record.BlindedRouteData struct for the
|
|
// final hop and packages it with the real node ID of the node it is intended
|
|
// for.
|
|
func buildFinalHopRouteData(node route.Vertex, pathID []byte,
|
|
constraints *record.PaymentConstraints) (*hopData, error) {
|
|
|
|
blindedRouteHopData := record.NewFinalHopBlindedRouteData(
|
|
constraints, pathID,
|
|
)
|
|
nodeID, err := btcec.ParsePubKey(node[:])
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
return &hopData{
|
|
data: blindedRouteHopData,
|
|
nodeID: nodeID,
|
|
}, nil
|
|
}
|
|
|
|
// getNodeChanPolicy fetches the routing policy info for the given channel and
|
|
// node pair.
|
|
func getNodeChannelPolicy(cfg *BuildBlindedPathCfg, chanID uint64,
|
|
nodeID route.Vertex) (*BlindedHopPolicy, error) {
|
|
|
|
// Attempt to fetch channel updates for the given channel. We will have
|
|
// at most two updates for a given channel.
|
|
_, update1, update2, err := cfg.FetchChannelEdgesByID(chanID)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
// Now we need to determine which of the updates was created by the
|
|
// node in question. We know the update is the correct one if the
|
|
// "ToNode" for the fetched policy is _not_ equal to the node ID in
|
|
// question.
|
|
var policy *models.ChannelEdgePolicy
|
|
switch {
|
|
case update1 != nil && !bytes.Equal(update1.ToNode[:], nodeID[:]):
|
|
policy = update1
|
|
|
|
case update2 != nil && !bytes.Equal(update2.ToNode[:], nodeID[:]):
|
|
policy = update2
|
|
|
|
default:
|
|
return nil, fmt.Errorf("no channel updates found from node "+
|
|
"%s for channel %d", nodeID, chanID)
|
|
}
|
|
|
|
return &BlindedHopPolicy{
|
|
CLTVExpiryDelta: policy.TimeLockDelta,
|
|
FeeRate: uint32(policy.FeeProportionalMillionths),
|
|
BaseFee: policy.FeeBaseMSat,
|
|
MinHTLCMsat: policy.MinHTLC,
|
|
MaxHTLCMsat: policy.MaxHTLC,
|
|
}, nil
|
|
}
|
|
|
|
// candidatePath holds all the information about a route to this node that we
|
|
// need in order to build a blinded route.
|
|
type candidatePath struct {
|
|
introNode route.Vertex
|
|
finalNodeID route.Vertex
|
|
hops []*blindedPathHop
|
|
}
|
|
|
|
// String returns a string representation of the candidatePath which can be
|
|
// useful for logging and debugging.
|
|
func (c *candidatePath) String() string {
|
|
str := fmt.Sprintf("[%s (intro node)]", c.introNode)
|
|
|
|
for _, hop := range c.hops {
|
|
if hop.isDummy {
|
|
str += "--->[dummy hop]"
|
|
continue
|
|
}
|
|
|
|
str += fmt.Sprintf("--<%d>-->[%s]", hop.channelID, hop.pubKey)
|
|
}
|
|
|
|
return str
|
|
}
|
|
|
|
// padWithDummyHops will append n dummy hops to the candidatePath hop set. The
|
|
// pub key for the dummy hop will be the same as the pub key for the final hop
|
|
// of the path. That way, the final hop will be able to decrypt the data
|
|
// encrypted for each dummy hop.
|
|
func (c *candidatePath) padWithDummyHops(n uint8) {
|
|
for len(c.hops) < int(n) {
|
|
c.hops = append(c.hops, &blindedPathHop{
|
|
pubKey: c.finalNodeID,
|
|
isDummy: true,
|
|
})
|
|
}
|
|
}
|
|
|
|
// blindedPathHop holds the information we need to know about a hop in a route
|
|
// in order to use it in the construction of a blinded path.
|
|
type blindedPathHop struct {
|
|
// pubKey is the real pub key of a node on a blinded path.
|
|
pubKey route.Vertex
|
|
|
|
// channelID is the channel along which the previous hop should forward
|
|
// their HTLC in order to reach this hop.
|
|
channelID uint64
|
|
|
|
// isDummy is true if this hop is an appended dummy hop.
|
|
isDummy bool
|
|
}
|
|
|
|
// extractCandidatePath extracts the data it needs from the given route.Route in
|
|
// order to construct a candidatePath.
|
|
func extractCandidatePath(path *route.Route) *candidatePath {
|
|
var (
|
|
hops = make([]*blindedPathHop, len(path.Hops))
|
|
finalNode = path.SourcePubKey
|
|
)
|
|
for i, hop := range path.Hops {
|
|
hops[i] = &blindedPathHop{
|
|
pubKey: hop.PubKeyBytes,
|
|
channelID: hop.ChannelID,
|
|
}
|
|
|
|
if i == len(path.Hops)-1 {
|
|
finalNode = hop.PubKeyBytes
|
|
}
|
|
}
|
|
|
|
return &candidatePath{
|
|
introNode: path.SourcePubKey,
|
|
finalNodeID: finalNode,
|
|
hops: hops,
|
|
}
|
|
}
|
|
|
|
// BlindedHopPolicy holds the set of relay policy values to use for a channel
|
|
// in a blinded path.
|
|
type BlindedHopPolicy struct {
|
|
CLTVExpiryDelta uint16
|
|
FeeRate uint32
|
|
BaseFee lnwire.MilliSatoshi
|
|
MinHTLCMsat lnwire.MilliSatoshi
|
|
MaxHTLCMsat lnwire.MilliSatoshi
|
|
}
|
|
|
|
// AddPolicyBuffer constructs the bufferedChanPolicies for a path hop by taking
|
|
// its actual policy values and multiplying them by the given multipliers.
|
|
// The base fee, fee rate and minimum HTLC msat values are adjusted via the
|
|
// incMultiplier while the maximum HTLC msat value is adjusted via the
|
|
// decMultiplier. If adjustments of the HTLC values no longer make sense
|
|
// then the original HTLC value is used.
|
|
func AddPolicyBuffer(policy *BlindedHopPolicy, incMultiplier,
|
|
decMultiplier float64) (*BlindedHopPolicy, error) {
|
|
|
|
if incMultiplier < 1 {
|
|
return nil, fmt.Errorf("blinded path policy increase " +
|
|
"multiplier must be greater than or equal to 1")
|
|
}
|
|
|
|
if decMultiplier < 0 || decMultiplier > 1 {
|
|
return nil, fmt.Errorf("blinded path policy decrease " +
|
|
"multiplier must be in the range [0;1]")
|
|
}
|
|
|
|
var (
|
|
minHTLCMsat = lnwire.MilliSatoshi(
|
|
float64(policy.MinHTLCMsat) * incMultiplier,
|
|
)
|
|
maxHTLCMsat = lnwire.MilliSatoshi(
|
|
float64(policy.MaxHTLCMsat) * decMultiplier,
|
|
)
|
|
)
|
|
|
|
// Make sure the new minimum is not more than the original maximum.
|
|
// If it is, then just stick to the original minimum.
|
|
if minHTLCMsat > policy.MaxHTLCMsat {
|
|
minHTLCMsat = policy.MinHTLCMsat
|
|
}
|
|
|
|
// Make sure the new maximum is not less than the original minimum.
|
|
// If it is, then just stick to the original maximum.
|
|
if maxHTLCMsat < policy.MinHTLCMsat {
|
|
maxHTLCMsat = policy.MaxHTLCMsat
|
|
}
|
|
|
|
// Also ensure that the new htlc bounds make sense. If the new minimum
|
|
// is greater than the new maximum, then just let both to their original
|
|
// values.
|
|
if minHTLCMsat > maxHTLCMsat {
|
|
minHTLCMsat = policy.MinHTLCMsat
|
|
maxHTLCMsat = policy.MaxHTLCMsat
|
|
}
|
|
|
|
return &BlindedHopPolicy{
|
|
CLTVExpiryDelta: uint16(
|
|
float64(policy.CLTVExpiryDelta) * incMultiplier,
|
|
),
|
|
FeeRate: uint32(
|
|
float64(policy.FeeRate) * incMultiplier,
|
|
),
|
|
BaseFee: lnwire.MilliSatoshi(
|
|
float64(policy.BaseFee) * incMultiplier,
|
|
),
|
|
MinHTLCMsat: minHTLCMsat,
|
|
MaxHTLCMsat: maxHTLCMsat,
|
|
}, nil
|
|
}
|
|
|
|
// calcBlindedPathPolicies computes the accumulated policy values for the path.
|
|
// These values include the total base fee, the total proportional fee and the
|
|
// total CLTV delta. This function assumes that all the passed relay infos have
|
|
// already been adjusted with a buffer to account for easy probing attacks.
|
|
func calcBlindedPathPolicies(relayInfo []*record.PaymentRelayInfo,
|
|
ourMinFinalCLTVDelta uint16) (lnwire.MilliSatoshi, uint32, uint16) {
|
|
|
|
var (
|
|
totalFeeBase lnwire.MilliSatoshi
|
|
totalFeeProp uint32
|
|
totalCLTV = ourMinFinalCLTVDelta
|
|
)
|
|
// Use the algorithms defined in BOLT 4 to calculate the accumulated
|
|
// relay fees for the route:
|
|
//nolint:lll
|
|
// https://github.com/lightning/bolts/blob/db278ab9b2baa0b30cfe79fb3de39280595938d3/04-onion-routing.md?plain=1#L255
|
|
for i := len(relayInfo) - 1; i >= 0; i-- {
|
|
info := relayInfo[i]
|
|
|
|
totalFeeBase = calcNextTotalBaseFee(
|
|
totalFeeBase, info.BaseFee, info.FeeRate,
|
|
)
|
|
|
|
totalFeeProp = calcNextTotalFeeRate(totalFeeProp, info.FeeRate)
|
|
|
|
totalCLTV += info.CltvExpiryDelta
|
|
}
|
|
|
|
return totalFeeBase, totalFeeProp, totalCLTV
|
|
}
|
|
|
|
// calcNextTotalBaseFee takes the current total accumulated base fee of a
|
|
// blinded path at hop `n` along with the fee rate and base fee of the hop at
|
|
// `n+1` and uses these to calculate the accumulated base fee at hop `n+1`.
|
|
func calcNextTotalBaseFee(currentTotal, hopBaseFee lnwire.MilliSatoshi,
|
|
hopFeeRate uint32) lnwire.MilliSatoshi {
|
|
|
|
numerator := (uint32(hopBaseFee) * oneMillion) +
|
|
(uint32(currentTotal) * (oneMillion + hopFeeRate)) +
|
|
oneMillion - 1
|
|
|
|
return lnwire.MilliSatoshi(numerator / oneMillion)
|
|
}
|
|
|
|
// calculateNextTotalFeeRate takes the current total accumulated fee rate of a
|
|
// blinded path at hop `n` along with the fee rate of the hop at `n+1` and uses
|
|
// these to calculate the accumulated fee rate at hop `n+1`.
|
|
func calcNextTotalFeeRate(currentTotal, hopFeeRate uint32) uint32 {
|
|
numerator := (currentTotal+hopFeeRate)*oneMillion +
|
|
currentTotal*hopFeeRate + oneMillion - 1
|
|
|
|
return numerator / oneMillion
|
|
}
|
|
|
|
// hopData packages the record.BlindedRouteData for a hop on a blinded path with
|
|
// the real node ID of that hop.
|
|
type hopData struct {
|
|
data *record.BlindedRouteData
|
|
nodeID *btcec.PublicKey
|
|
}
|
|
|
|
// padStats can be used to keep track of various pieces of data that we collect
|
|
// during a call to padHopInfo. This is useful for logging and for test
|
|
// assertions.
|
|
type padStats struct {
|
|
minPayloadSize int
|
|
maxPayloadSize int
|
|
finalPaddedSize int
|
|
numIterations int
|
|
}
|
|
|
|
// padHopInfo iterates over a set of record.BlindedRouteData and adds padding
|
|
// where needed until the resulting encrypted data blobs are all the same size.
|
|
// This may take a few iterations due to the fact that a TLV field is used to
|
|
// add this padding. For example, if we want to add a 1 byte padding to a
|
|
// record.BlindedRouteData when it does not yet have any padding, then adding
|
|
// a 1 byte padding will actually add 3 bytes due to the bytes required when
|
|
// adding the initial type and length bytes. However, on the next iteration if
|
|
// we again add just 1 byte, then only a single byte will be added. The same
|
|
// iteration is required for padding values on the BigSize encoding bucket
|
|
// edges. The number of iterations that this function takes is also returned for
|
|
// testing purposes. If prePad is true, then zero byte padding is added to each
|
|
// payload that does not yet have padding. This will save some iterations for
|
|
// the majority of cases. minSize can be used to specify a minimum size that all
|
|
// payloads should be.
|
|
func padHopInfo(hopInfo []*hopData, prePad bool, minSize int) (
|
|
[]*sphinx.HopInfo, *padStats, error) {
|
|
|
|
var (
|
|
paymentPath = make([]*sphinx.HopInfo, len(hopInfo))
|
|
stats = padStats{finalPaddedSize: minSize}
|
|
)
|
|
|
|
// Pre-pad each payload with zero byte padding (if it does not yet have
|
|
// padding) to save a couple of iterations in the majority of cases.
|
|
if prePad {
|
|
for _, info := range hopInfo {
|
|
if info.data.Padding.IsSome() {
|
|
continue
|
|
}
|
|
|
|
info.data.PadBy(0)
|
|
}
|
|
}
|
|
|
|
for {
|
|
stats.numIterations++
|
|
|
|
// On each iteration of the loop, we first determine the
|
|
// current largest encoded data blob size. This will be the
|
|
// size we aim to get the others to match.
|
|
var (
|
|
maxLen = minSize
|
|
minLen = math.MaxInt8
|
|
)
|
|
for i, hop := range hopInfo {
|
|
plainText, err := record.EncodeBlindedRouteData(
|
|
hop.data,
|
|
)
|
|
if err != nil {
|
|
return nil, nil, err
|
|
}
|
|
|
|
if len(plainText) > maxLen {
|
|
maxLen = len(plainText)
|
|
|
|
// Update the stats to take note of this new
|
|
// max since this may be the final max that all
|
|
// payloads will be padded to.
|
|
stats.finalPaddedSize = maxLen
|
|
}
|
|
if len(plainText) < minLen {
|
|
minLen = len(plainText)
|
|
}
|
|
|
|
paymentPath[i] = &sphinx.HopInfo{
|
|
NodePub: hop.nodeID,
|
|
PlainText: plainText,
|
|
}
|
|
}
|
|
|
|
// If this is our first iteration, then we take note of the min
|
|
// and max lengths of the payloads pre-padding for logging
|
|
// later.
|
|
if stats.numIterations == 1 {
|
|
stats.minPayloadSize = minLen
|
|
stats.maxPayloadSize = maxLen
|
|
}
|
|
|
|
// Now we iterate over them again and determine which ones we
|
|
// need to add padding to.
|
|
var numEqual int
|
|
for i, hop := range hopInfo {
|
|
plainText := paymentPath[i].PlainText
|
|
|
|
// If the plaintext length is equal to the desired
|
|
// length, then we can continue. We use numEqual to
|
|
// keep track of how many have the same length.
|
|
if len(plainText) == maxLen {
|
|
numEqual++
|
|
|
|
continue
|
|
}
|
|
|
|
// If we previously added padding to this hop, we keep
|
|
// the length of that initial padding too.
|
|
var existingPadding int
|
|
hop.data.Padding.WhenSome(
|
|
func(p tlv.RecordT[tlv.TlvType1, []byte]) {
|
|
existingPadding = len(p.Val)
|
|
},
|
|
)
|
|
|
|
// Add some padding bytes to the hop.
|
|
hop.data.PadBy(
|
|
existingPadding + maxLen - len(plainText),
|
|
)
|
|
}
|
|
|
|
// If all the payloads have the same length, we can exit the
|
|
// loop.
|
|
if numEqual == len(hopInfo) {
|
|
break
|
|
}
|
|
}
|
|
|
|
log.Debugf("Finished padding %d blinded path payloads to %d bytes "+
|
|
"each where the pre-padded min and max sizes were %d and %d "+
|
|
"bytes respectively", len(hopInfo), stats.finalPaddedSize,
|
|
stats.minPayloadSize, stats.maxPayloadSize)
|
|
|
|
return paymentPath, &stats, nil
|
|
}
|