Skip to content

Commit

Permalink
blindedpath: smarter dummy hop policy selection
Browse files Browse the repository at this point in the history
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.
  • Loading branch information
ellemouton committed Jul 26, 2024
1 parent 60a856a commit c490279
Show file tree
Hide file tree
Showing 4 changed files with 195 additions and 44 deletions.
25 changes: 12 additions & 13 deletions lnrpc/invoicesrpc/addinvoice.go
Original file line number Diff line number Diff line change
Expand Up @@ -109,10 +109,15 @@ type AddInvoiceConfig struct {
// appropriate values (like maximum HTLC) by 10%.
BlindedRoutePolicyDecrMultiplier float64

// MinNumHops is the minimum number of hops that a blinded path should
// be. Dummy hops will be used to pad any route with a length less than
// this.
MinNumHops uint8
// MinNumBlindedPathHops is the minimum number of hops that a blinded
// path should be. Dummy hops will be used to pad any route with a
// length less than this.
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.
Expand Down Expand Up @@ -508,6 +513,7 @@ func AddInvoice(ctx context.Context, cfg *AddInvoiceConfig,
&blindedpath.BuildBlindedPathCfg{
FindRoutes: cfg.QueryBlindedRoutes,
FetchChannelEdgesByID: cfg.Graph.FetchChannelEdgesByID,
FetchOurOpenChannels: cfg.ChanDB.FetchAllOpenChannels,
PathID: paymentAddr[:],
ValueMsat: invoice.Value,
BestHeight: cfg.BestHeight,
Expand All @@ -523,15 +529,8 @@ func AddInvoice(ctx context.Context, cfg *AddInvoiceConfig,
cfg.BlindedRoutePolicyDecrMultiplier,
)
},
MinNumHops: cfg.MinNumHops,
// TODO: make configurable
DummyHopPolicy: &blindedpath.BlindedHopPolicy{
CLTVExpiryDelta: 80,
FeeRate: 100,
BaseFee: 100,
MinHTLCMsat: 0,
MaxHTLCMsat: lnwire.MaxMilliSatoshi,
},
MinNumHops: cfg.MinNumBlindedPathHops,
DefaultDummyHopPolicy: cfg.DefaultDummyHopPolicy,
},
)
if err != nil {
Expand Down
190 changes: 165 additions & 25 deletions routing/blindedpath/blinded_path.go
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,9 @@ import (
"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"
Expand Down Expand Up @@ -43,6 +45,9 @@ type BuildBlindedPathCfg struct {
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)
Expand All @@ -53,7 +58,7 @@ type BuildBlindedPathCfg struct {
// 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
// DummyHopPolicy.
// DefaultDummyHopPolicy.
AddPolicyBuffer func(policy *BlindedHopPolicy) (*BlindedHopPolicy,
error)

Expand Down Expand Up @@ -86,9 +91,13 @@ type BuildBlindedPathCfg struct {
// route.
MinNumHops uint8

// DummyHopPolicy holds the policy values that should be used for dummy
// hops. Note that these will _not_ be buffered via AddPolicyBuffer.
DummyHopPolicy *BlindedHopPolicy
// 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
Expand Down Expand Up @@ -334,42 +343,100 @@ type hopRelayInfo struct {
// 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 (
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

// 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 (
// The first pub key is that of the introduction node.
hopSource = path.introNode
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 (
// For dummy hops, we use pre-configured policy values.
policy = cfg.DummyHopPolicy
policy = dummyHopPolicy
ok bool
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
policy, ok = policies[hop.channelID]
if !ok {
return nil, 0, 0, fmt.Errorf("no cached "+
"policy found for channel ID: %d",
hop.channelID)
}
}

// Apply any policy changes now before caching the
// policy.
policy, err = cfg.AddPolicyBuffer(policy)
if err != nil {
return nil, 0, 0, err
}
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
Expand Down Expand Up @@ -435,6 +502,79 @@ func buildDummyRouteData(node route.Vertex, relayInfo *record.PaymentRelayInfo,
}, 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,
Expand Down
10 changes: 5 additions & 5 deletions routing/blindedpath/blinded_path_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -802,7 +802,7 @@ func TestBuildBlindedPathWithDummyHops(t *testing.T) {
// hops to be added to the real route.
MinNumHops: 4,

DummyHopPolicy: &BlindedHopPolicy{
DefaultDummyHopPolicy: &BlindedHopPolicy{
CLTVExpiryDelta: 50,
FeeRate: 100,
BaseFee: 100,
Expand All @@ -817,8 +817,8 @@ func TestBuildBlindedPathWithDummyHops(t *testing.T) {

// Check that all the accumulated policy values are correct.
require.EqualValues(t, 403, path.FeeBaseMsat)
require.EqualValues(t, 1203, path.FeeRate)
require.EqualValues(t, 400, path.CltvExpiryDelta)
require.EqualValues(t, 2003, path.FeeRate)
require.EqualValues(t, 588, path.CltvExpiryDelta)
require.EqualValues(t, 1000, path.HTLCMinMsat)
require.EqualValues(t, lnwire.MaxMilliSatoshi, path.HTLCMaxMsat)

Expand Down Expand Up @@ -861,7 +861,7 @@ func TestBuildBlindedPathWithDummyHops(t *testing.T) {
}, data.RelayInfo.UnwrapOrFail(t).Val)

require.Equal(t, record.PaymentConstraints{
MaxCltvExpiry: 1600,
MaxCltvExpiry: 1788,
HtlcMinimumMsat: 1000,
}, data.Constraints.UnwrapOrFail(t).Val)

Expand All @@ -883,7 +883,7 @@ func TestBuildBlindedPathWithDummyHops(t *testing.T) {
}, data.RelayInfo.UnwrapOrFail(t).Val)

require.Equal(t, record.PaymentConstraints{
MaxCltvExpiry: 1456,
MaxCltvExpiry: 1644,
HtlcMinimumMsat: 1000,
}, data.Constraints.UnwrapOrFail(t).Val)

Expand Down
14 changes: 13 additions & 1 deletion rpcserver.go
Original file line number Diff line number Diff line change
Expand Up @@ -75,6 +75,7 @@ import (
"github.com/lightningnetwork/lnd/peernotifier"
"github.com/lightningnetwork/lnd/record"
"github.com/lightningnetwork/lnd/routing"
"github.com/lightningnetwork/lnd/routing/blindedpath"
"github.com/lightningnetwork/lnd/routing/route"
"github.com/lightningnetwork/lnd/rpcperms"
"github.com/lightningnetwork/lnd/signal"
Expand Down Expand Up @@ -5825,7 +5826,18 @@ func (r *rpcServer) AddInvoice(ctx context.Context,
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)
Expand Down

0 comments on commit c490279

Please sign in to comment.