blindedpath: smarter dummy hop policy selection
This commit introduces more sophisticated code for selecting dummy hop policy values for dummy hops in blinded paths. For the case where the path does contain real hops, the dummy hop policy values are derived by taking the average of those hop polices. For the case where there are no real hops (in other words, we are the introduction node), we use the default policy values used for normal ChannelUpdates but then for the MaxHTLC value, we take the average of all our open channel capacities.
This commit is contained in:
4 files changed
+196
-45
No files matched your search
@@ -109,10 +109,15 @@ type AddInvoiceConfig struct {
|
|||||||
// appropriate values (like maximum HTLC) by 10%.
|
// appropriate values (like maximum HTLC) by 10%.
|
||||||
BlindedRoutePolicyDecrMultiplier float64
|
BlindedRoutePolicyDecrMultiplier float64
|
||||||
|
|
||||||
// MinNumHops is the minimum number of hops that a blinded path should
|
// MinNumBlindedPathHops is the minimum number of hops that a blinded
|
||||||
// be. Dummy hops will be used to pad any route with a length less than
|
// path should be. Dummy hops will be used to pad any route with a
|
||||||
// this.
|
// length less than this.
|
||||||
MinNumHops uint8
|
MinNumBlindedPathHops uint8
|
||||||
|
|
||||||
|
// DefaultDummyHopPolicy holds the default policy values to use for
|
||||||
|
// dummy hops in a blinded path in the case where they cant be derived
|
||||||
|
// through other means.
|
||||||
|
DefaultDummyHopPolicy *blindedpath.BlindedHopPolicy
|
||||||
}
|
}
|
||||||
|
|
||||||
// AddInvoiceData contains the required data to create a new invoice.
|
// AddInvoiceData contains the required data to create a new invoice.
|
||||||
@@ -508,6 +513,7 @@ func AddInvoice(ctx context.Context, cfg *AddInvoiceConfig,
|
|||||||
&blindedpath.BuildBlindedPathCfg{
|
&blindedpath.BuildBlindedPathCfg{
|
||||||
FindRoutes: cfg.QueryBlindedRoutes,
|
FindRoutes: cfg.QueryBlindedRoutes,
|
||||||
FetchChannelEdgesByID: cfg.Graph.FetchChannelEdgesByID,
|
FetchChannelEdgesByID: cfg.Graph.FetchChannelEdgesByID,
|
||||||
|
FetchOurOpenChannels: cfg.ChanDB.FetchAllOpenChannels,
|
||||||
PathID: paymentAddr[:],
|
PathID: paymentAddr[:],
|
||||||
ValueMsat: invoice.Value,
|
ValueMsat: invoice.Value,
|
||||||
BestHeight: cfg.BestHeight,
|
BestHeight: cfg.BestHeight,
|
||||||
@@ -523,15 +529,8 @@ func AddInvoice(ctx context.Context, cfg *AddInvoiceConfig,
|
|||||||
cfg.BlindedRoutePolicyDecrMultiplier,
|
cfg.BlindedRoutePolicyDecrMultiplier,
|
||||||
)
|
)
|
||||||
},
|
},
|
||||||
MinNumHops: cfg.MinNumHops,
|
MinNumHops: cfg.MinNumBlindedPathHops,
|
||||||
// TODO: make configurable
|
DefaultDummyHopPolicy: cfg.DefaultDummyHopPolicy,
|
||||||
DummyHopPolicy: &blindedpath.BlindedHopPolicy{
|
|
||||||
CLTVExpiryDelta: 80,
|
|
||||||
FeeRate: 100,
|
|
||||||
BaseFee: 100,
|
|
||||||
MinHTLCMsat: 0,
|
|
||||||
MaxHTLCMsat: lnwire.MaxMilliSatoshi,
|
|
||||||
},
|
|
||||||
},
|
},
|
||||||
)
|
)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
|
|||||||
@@ -8,7 +8,9 @@ import (
|
|||||||
"sort"
|
"sort"
|
||||||
|
|
||||||
"github.com/btcsuite/btcd/btcec/v2"
|
"github.com/btcsuite/btcd/btcec/v2"
|
||||||
|
"github.com/btcsuite/btcd/btcutil"
|
||||||
sphinx "github.com/lightningnetwork/lightning-onion"
|
sphinx "github.com/lightningnetwork/lightning-onion"
|
||||||
|
"github.com/lightningnetwork/lnd/channeldb"
|
||||||
"github.com/lightningnetwork/lnd/channeldb/models"
|
"github.com/lightningnetwork/lnd/channeldb/models"
|
||||||
"github.com/lightningnetwork/lnd/lnwire"
|
"github.com/lightningnetwork/lnd/lnwire"
|
||||||
"github.com/lightningnetwork/lnd/record"
|
"github.com/lightningnetwork/lnd/record"
|
||||||
@@ -43,6 +45,9 @@ type BuildBlindedPathCfg struct {
|
|||||||
FetchChannelEdgesByID func(chanID uint64) (*models.ChannelEdgeInfo,
|
FetchChannelEdgesByID func(chanID uint64) (*models.ChannelEdgeInfo,
|
||||||
*models.ChannelEdgePolicy, *models.ChannelEdgePolicy, error)
|
*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
|
// BestHeight can be used to fetch the best block height that this node
|
||||||
// is aware of.
|
// is aware of.
|
||||||
BestHeight func() (uint32, error)
|
BestHeight func() (uint32, error)
|
||||||
@@ -53,7 +58,7 @@ type BuildBlindedPathCfg struct {
|
|||||||
// during the lifetime of the blinded path, then the path remains valid
|
// 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
|
// 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
|
// for the policies of real nodes and won't be applied to
|
||||||
// DummyHopPolicy.
|
// DefaultDummyHopPolicy.
|
||||||
AddPolicyBuffer func(policy *BlindedHopPolicy) (*BlindedHopPolicy,
|
AddPolicyBuffer func(policy *BlindedHopPolicy) (*BlindedHopPolicy,
|
||||||
error)
|
error)
|
||||||
|
|
||||||
@@ -86,9 +91,13 @@ type BuildBlindedPathCfg struct {
|
|||||||
// route.
|
// route.
|
||||||
MinNumHops uint8
|
MinNumHops uint8
|
||||||
|
|
||||||
// DummyHopPolicy holds the policy values that should be used for dummy
|
// DefaultDummyHopPolicy holds the policy values that should be used for
|
||||||
// hops. Note that these will _not_ be buffered via AddPolicyBuffer.
|
// dummy hops in the cases where it cannot be derived via other means
|
||||||
DummyHopPolicy *BlindedHopPolicy
|
// 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
|
// BuildBlindedPaymentPaths uses the passed config to construct a set of blinded
|
||||||
@@ -334,44 +343,102 @@ type hopRelayInfo struct {
|
|||||||
// Therefore, when we go through the route and its hops to collect policies, our
|
// 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
|
// index for collecting public keys will be trailing that of the channel IDs by
|
||||||
// 1.
|
// 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) (
|
func collectRelayInfo(cfg *BuildBlindedPathCfg, path *candidatePath) (
|
||||||
[]*hopRelayInfo, lnwire.MilliSatoshi, lnwire.MilliSatoshi, error) {
|
[]*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 (
|
var (
|
||||||
hops = make([]*hopRelayInfo, 0, len(path.hops))
|
hops = make([]*hopRelayInfo, 0, len(path.hops))
|
||||||
minHTLC lnwire.MilliSatoshi
|
minHTLC lnwire.MilliSatoshi
|
||||||
maxHTLC lnwire.MilliSatoshi
|
maxHTLC lnwire.MilliSatoshi
|
||||||
)
|
)
|
||||||
|
// The first pub key is that of the introduction node.
|
||||||
var (
|
hopSource = path.introNode
|
||||||
// The first pub key is that of the introduction node.
|
|
||||||
hopSource = path.introNode
|
|
||||||
)
|
|
||||||
for _, hop := range path.hops {
|
for _, hop := range path.hops {
|
||||||
var (
|
var (
|
||||||
// For dummy hops, we use pre-configured policy values.
|
policy = dummyHopPolicy
|
||||||
policy = cfg.DummyHopPolicy
|
ok bool
|
||||||
err error
|
err error
|
||||||
)
|
)
|
||||||
if !hop.isDummy {
|
|
||||||
// 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
|
|
||||||
}
|
|
||||||
|
|
||||||
// Apply any policy changes now before caching the
|
if !hop.isDummy {
|
||||||
// policy.
|
policy, ok = policies[hop.channelID]
|
||||||
policy, err = cfg.AddPolicyBuffer(policy)
|
if !ok {
|
||||||
if err != nil {
|
return nil, 0, 0, fmt.Errorf("no cached "+
|
||||||
return nil, 0, 0, err
|
"policy found for channel ID: %d",
|
||||||
|
hop.channelID)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
policy, err = cfg.AddPolicyBuffer(policy)
|
||||||
|
if err != nil {
|
||||||
|
return nil, 0, 0, err
|
||||||
|
}
|
||||||
|
|
||||||
// If this is the first policy we are collecting, then use this
|
// If this is the first policy we are collecting, then use this
|
||||||
// policy to set the base values for min/max htlc.
|
// policy to set the base values for min/max htlc.
|
||||||
if len(hops) == 0 {
|
if len(hops) == 0 {
|
||||||
@@ -435,6 +502,79 @@ func buildDummyRouteData(node route.Vertex, relayInfo *record.PaymentRelayInfo,
|
|||||||
}, nil
|
}, 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
|
// buildHopRouteData constructs the record.BlindedRouteData struct for the given
|
||||||
// non-final hop on a blinded path and packages it with the node's ID.
|
// non-final hop on a blinded path and packages it with the node's ID.
|
||||||
func buildHopRouteData(node route.Vertex, scid lnwire.ShortChannelID,
|
func buildHopRouteData(node route.Vertex, scid lnwire.ShortChannelID,
|
||||||
|
|||||||
@@ -802,7 +802,7 @@ func TestBuildBlindedPathWithDummyHops(t *testing.T) {
|
|||||||
// hops to be added to the real route.
|
// hops to be added to the real route.
|
||||||
MinNumHops: 4,
|
MinNumHops: 4,
|
||||||
|
|
||||||
DummyHopPolicy: &BlindedHopPolicy{
|
DefaultDummyHopPolicy: &BlindedHopPolicy{
|
||||||
CLTVExpiryDelta: 50,
|
CLTVExpiryDelta: 50,
|
||||||
FeeRate: 100,
|
FeeRate: 100,
|
||||||
BaseFee: 100,
|
BaseFee: 100,
|
||||||
@@ -817,8 +817,8 @@ func TestBuildBlindedPathWithDummyHops(t *testing.T) {
|
|||||||
|
|
||||||
// Check that all the accumulated policy values are correct.
|
// Check that all the accumulated policy values are correct.
|
||||||
require.EqualValues(t, 403, path.FeeBaseMsat)
|
require.EqualValues(t, 403, path.FeeBaseMsat)
|
||||||
require.EqualValues(t, 1203, path.FeeRate)
|
require.EqualValues(t, 2003, path.FeeRate)
|
||||||
require.EqualValues(t, 400, path.CltvExpiryDelta)
|
require.EqualValues(t, 588, path.CltvExpiryDelta)
|
||||||
require.EqualValues(t, 1000, path.HTLCMinMsat)
|
require.EqualValues(t, 1000, path.HTLCMinMsat)
|
||||||
require.EqualValues(t, lnwire.MaxMilliSatoshi, path.HTLCMaxMsat)
|
require.EqualValues(t, lnwire.MaxMilliSatoshi, path.HTLCMaxMsat)
|
||||||
|
|
||||||
@@ -861,7 +861,7 @@ func TestBuildBlindedPathWithDummyHops(t *testing.T) {
|
|||||||
}, data.RelayInfo.UnwrapOrFail(t).Val)
|
}, data.RelayInfo.UnwrapOrFail(t).Val)
|
||||||
|
|
||||||
require.Equal(t, record.PaymentConstraints{
|
require.Equal(t, record.PaymentConstraints{
|
||||||
MaxCltvExpiry: 1600,
|
MaxCltvExpiry: 1788,
|
||||||
HtlcMinimumMsat: 1000,
|
HtlcMinimumMsat: 1000,
|
||||||
}, data.Constraints.UnwrapOrFail(t).Val)
|
}, data.Constraints.UnwrapOrFail(t).Val)
|
||||||
|
|
||||||
@@ -883,7 +883,7 @@ func TestBuildBlindedPathWithDummyHops(t *testing.T) {
|
|||||||
}, data.RelayInfo.UnwrapOrFail(t).Val)
|
}, data.RelayInfo.UnwrapOrFail(t).Val)
|
||||||
|
|
||||||
require.Equal(t, record.PaymentConstraints{
|
require.Equal(t, record.PaymentConstraints{
|
||||||
MaxCltvExpiry: 1456,
|
MaxCltvExpiry: 1644,
|
||||||
HtlcMinimumMsat: 1000,
|
HtlcMinimumMsat: 1000,
|
||||||
}, data.Constraints.UnwrapOrFail(t).Val)
|
}, data.Constraints.UnwrapOrFail(t).Val)
|
||||||
|
|
||||||
|
|||||||
+13
-1
@@ -75,6 +75,7 @@ import (
|
|||||||
"github.com/lightningnetwork/lnd/peernotifier"
|
"github.com/lightningnetwork/lnd/peernotifier"
|
||||||
"github.com/lightningnetwork/lnd/record"
|
"github.com/lightningnetwork/lnd/record"
|
||||||
"github.com/lightningnetwork/lnd/routing"
|
"github.com/lightningnetwork/lnd/routing"
|
||||||
|
"github.com/lightningnetwork/lnd/routing/blindedpath"
|
||||||
"github.com/lightningnetwork/lnd/routing/route"
|
"github.com/lightningnetwork/lnd/routing/route"
|
||||||
"github.com/lightningnetwork/lnd/rpcperms"
|
"github.com/lightningnetwork/lnd/rpcperms"
|
||||||
"github.com/lightningnetwork/lnd/signal"
|
"github.com/lightningnetwork/lnd/signal"
|
||||||
@@ -5825,7 +5826,18 @@ func (r *rpcServer) AddInvoice(ctx context.Context,
|
|||||||
blindingRestrictions,
|
blindingRestrictions,
|
||||||
)
|
)
|
||||||
},
|
},
|
||||||
MinNumHops: r.server.cfg.Routing.BlindedPaths.NumHops,
|
MinNumBlindedPathHops: r.server.cfg.Routing.BlindedPaths.
|
||||||
|
NumHops,
|
||||||
|
DefaultDummyHopPolicy: &blindedpath.BlindedHopPolicy{
|
||||||
|
CLTVExpiryDelta: uint16(defaultDelta),
|
||||||
|
FeeRate: uint32(r.server.cfg.Bitcoin.FeeRate),
|
||||||
|
BaseFee: r.server.cfg.Bitcoin.BaseFee,
|
||||||
|
MinHTLCMsat: r.server.cfg.Bitcoin.MinHTLCIn,
|
||||||
|
|
||||||
|
// MaxHTLCMsat will be calculated on the fly by using
|
||||||
|
// the introduction node's channel's capacities.
|
||||||
|
MaxHTLCMsat: 0,
|
||||||
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
value, err := lnrpc.UnmarshallAmt(invoice.Value, invoice.ValueMsat)
|
value, err := lnrpc.UnmarshallAmt(invoice.Value, invoice.ValueMsat)
|
||||||
|
|||||||
Reference in new issue
Block a user