Skip to content

Commit c490279

Browse files
committed
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.
1 parent 60a856a commit c490279

File tree

4 files changed

+195
-44
lines changed

4 files changed

+195
-44
lines changed

lnrpc/invoicesrpc/addinvoice.go

Lines changed: 12 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -109,10 +109,15 @@ type AddInvoiceConfig struct {
109109
// appropriate values (like maximum HTLC) by 10%.
110110
BlindedRoutePolicyDecrMultiplier float64
111111

112-
// MinNumHops is the minimum number of hops that a blinded path should
113-
// be. Dummy hops will be used to pad any route with a length less than
114-
// this.
115-
MinNumHops uint8
112+
// MinNumBlindedPathHops is the minimum number of hops that a blinded
113+
// path should be. Dummy hops will be used to pad any route with a
114+
// length less than this.
115+
MinNumBlindedPathHops uint8
116+
117+
// DefaultDummyHopPolicy holds the default policy values to use for
118+
// dummy hops in a blinded path in the case where they cant be derived
119+
// through other means.
120+
DefaultDummyHopPolicy *blindedpath.BlindedHopPolicy
116121
}
117122

118123
// AddInvoiceData contains the required data to create a new invoice.
@@ -508,6 +513,7 @@ func AddInvoice(ctx context.Context, cfg *AddInvoiceConfig,
508513
&blindedpath.BuildBlindedPathCfg{
509514
FindRoutes: cfg.QueryBlindedRoutes,
510515
FetchChannelEdgesByID: cfg.Graph.FetchChannelEdgesByID,
516+
FetchOurOpenChannels: cfg.ChanDB.FetchAllOpenChannels,
511517
PathID: paymentAddr[:],
512518
ValueMsat: invoice.Value,
513519
BestHeight: cfg.BestHeight,
@@ -523,15 +529,8 @@ func AddInvoice(ctx context.Context, cfg *AddInvoiceConfig,
523529
cfg.BlindedRoutePolicyDecrMultiplier,
524530
)
525531
},
526-
MinNumHops: cfg.MinNumHops,
527-
// TODO: make configurable
528-
DummyHopPolicy: &blindedpath.BlindedHopPolicy{
529-
CLTVExpiryDelta: 80,
530-
FeeRate: 100,
531-
BaseFee: 100,
532-
MinHTLCMsat: 0,
533-
MaxHTLCMsat: lnwire.MaxMilliSatoshi,
534-
},
532+
MinNumHops: cfg.MinNumBlindedPathHops,
533+
DefaultDummyHopPolicy: cfg.DefaultDummyHopPolicy,
535534
},
536535
)
537536
if err != nil {

routing/blindedpath/blinded_path.go

Lines changed: 165 additions & 25 deletions
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,9 @@ import (
88
"sort"
99

1010
"github.com/btcsuite/btcd/btcec/v2"
11+
"github.com/btcsuite/btcd/btcutil"
1112
sphinx "github.com/lightningnetwork/lightning-onion"
13+
"github.com/lightningnetwork/lnd/channeldb"
1214
"github.com/lightningnetwork/lnd/channeldb/models"
1315
"github.com/lightningnetwork/lnd/lnwire"
1416
"github.com/lightningnetwork/lnd/record"
@@ -43,6 +45,9 @@ type BuildBlindedPathCfg struct {
4345
FetchChannelEdgesByID func(chanID uint64) (*models.ChannelEdgeInfo,
4446
*models.ChannelEdgePolicy, *models.ChannelEdgePolicy, error)
4547

48+
// FetchOurOpenChannels fetches this node's set of open channels.
49+
FetchOurOpenChannels func() ([]*channeldb.OpenChannel, error)
50+
4651
// BestHeight can be used to fetch the best block height that this node
4752
// is aware of.
4853
BestHeight func() (uint32, error)
@@ -53,7 +58,7 @@ type BuildBlindedPathCfg struct {
5358
// during the lifetime of the blinded path, then the path remains valid
5459
// and so probing is more difficult. Note that this will only be called
5560
// for the policies of real nodes and won't be applied to
56-
// DummyHopPolicy.
61+
// DefaultDummyHopPolicy.
5762
AddPolicyBuffer func(policy *BlindedHopPolicy) (*BlindedHopPolicy,
5863
error)
5964

@@ -86,9 +91,13 @@ type BuildBlindedPathCfg struct {
8691
// route.
8792
MinNumHops uint8
8893

89-
// DummyHopPolicy holds the policy values that should be used for dummy
90-
// hops. Note that these will _not_ be buffered via AddPolicyBuffer.
91-
DummyHopPolicy *BlindedHopPolicy
94+
// DefaultDummyHopPolicy holds the policy values that should be used for
95+
// dummy hops in the cases where it cannot be derived via other means
96+
// such as averaging the policy values of other hops on the path. This
97+
// would happen in the case where the introduction node is also the
98+
// introduction node. If these default policy values are used, then
99+
// the MaxHTLCMsat value must be carefully chosen.
100+
DefaultDummyHopPolicy *BlindedHopPolicy
92101
}
93102

94103
// BuildBlindedPaymentPaths uses the passed config to construct a set of blinded
@@ -334,42 +343,100 @@ type hopRelayInfo struct {
334343
// Therefore, when we go through the route and its hops to collect policies, our
335344
// index for collecting public keys will be trailing that of the channel IDs by
336345
// 1.
346+
//
347+
// For any dummy hops on the route, this function also decides what to use as
348+
// policy values for the dummy hops. If there are other real hops, then the
349+
// dummy hop policy values are derived by taking the average of the real
350+
// policy values. If there are no real hops (in other words we are the
351+
// introduction node), then we use some default routing values and we use the
352+
// average of our channel capacities for the MaxHTLC value.
337353
func collectRelayInfo(cfg *BuildBlindedPathCfg, path *candidatePath) (
338354
[]*hopRelayInfo, lnwire.MilliSatoshi, lnwire.MilliSatoshi, error) {
339355

340356
var (
341-
hops = make([]*hopRelayInfo, 0, len(path.hops))
342-
minHTLC lnwire.MilliSatoshi
343-
maxHTLC lnwire.MilliSatoshi
357+
// The first pub key is that of the introduction node.
358+
hopSource = path.introNode
359+
360+
// A collection of the policy values of real hops on the path.
361+
policies = make(map[uint64]*BlindedHopPolicy)
362+
363+
hasDummyHops bool
344364
)
345365

366+
// On this first iteration, we just collect policy values of the real
367+
// hops on the path.
368+
for _, hop := range path.hops {
369+
// Once we have hit a dummy hop, all hops after will be dummy
370+
// hops too.
371+
if hop.isDummy {
372+
hasDummyHops = true
373+
374+
break
375+
}
376+
377+
// For real hops, retrieve the channel policy for this hop's
378+
// channel ID in the direction pointing away from the hopSource
379+
// node.
380+
policy, err := getNodeChannelPolicy(
381+
cfg, hop.channelID, hopSource,
382+
)
383+
if err != nil {
384+
return nil, 0, 0, err
385+
}
386+
387+
policies[hop.channelID] = policy
388+
389+
// This hop's pub key will be the policy creator for the next
390+
// hop.
391+
hopSource = hop.pubKey
392+
}
393+
346394
var (
347-
// The first pub key is that of the introduction node.
348-
hopSource = path.introNode
395+
dummyHopPolicy *BlindedHopPolicy
396+
err error
397+
)
398+
399+
// If the path does have dummy hops, we need to decide which policy
400+
// values to use for these hops.
401+
if hasDummyHops {
402+
dummyHopPolicy, err = computeDummyHopPolicy(
403+
cfg.DefaultDummyHopPolicy, cfg.FetchOurOpenChannels,
404+
policies,
405+
)
406+
if err != nil {
407+
return nil, 0, 0, err
408+
}
409+
}
410+
411+
// We iterate through the hops one more time. This time it is to
412+
// buffer the policy values, collect the payment relay info to send to
413+
// each hop, and to compute the min and max HTLC values for the path.
414+
var (
415+
hops = make([]*hopRelayInfo, 0, len(path.hops))
416+
minHTLC lnwire.MilliSatoshi
417+
maxHTLC lnwire.MilliSatoshi
349418
)
419+
// The first pub key is that of the introduction node.
420+
hopSource = path.introNode
350421
for _, hop := range path.hops {
351422
var (
352-
// For dummy hops, we use pre-configured policy values.
353-
policy = cfg.DummyHopPolicy
423+
policy = dummyHopPolicy
424+
ok bool
354425
err error
355426
)
427+
356428
if !hop.isDummy {
357-
// For real hops, retrieve the channel policy for this
358-
// hop's channel ID in the direction pointing away from
359-
// the hopSource node.
360-
policy, err = getNodeChannelPolicy(
361-
cfg, hop.channelID, hopSource,
362-
)
363-
if err != nil {
364-
return nil, 0, 0, err
429+
policy, ok = policies[hop.channelID]
430+
if !ok {
431+
return nil, 0, 0, fmt.Errorf("no cached "+
432+
"policy found for channel ID: %d",
433+
hop.channelID)
365434
}
435+
}
366436

367-
// Apply any policy changes now before caching the
368-
// policy.
369-
policy, err = cfg.AddPolicyBuffer(policy)
370-
if err != nil {
371-
return nil, 0, 0, err
372-
}
437+
policy, err = cfg.AddPolicyBuffer(policy)
438+
if err != nil {
439+
return nil, 0, 0, err
373440
}
374441

375442
// If this is the first policy we are collecting, then use this
@@ -435,6 +502,79 @@ func buildDummyRouteData(node route.Vertex, relayInfo *record.PaymentRelayInfo,
435502
}, nil
436503
}
437504

505+
// computeDummyHopPolicy determines policy values to use for a dummy hop on a
506+
// blinded path. If other real policy values exist, then we use the average of
507+
// those values for the dummy hop policy values. Otherwise, in the case were
508+
// there are no real policy values due to this node being the introduction node,
509+
// we use the provided default policy values, and we get the average capacity of
510+
// this node's channels to compute a MaxHTLC value.
511+
func computeDummyHopPolicy(defaultPolicy *BlindedHopPolicy,
512+
fetchOurChannels func() ([]*channeldb.OpenChannel, error),
513+
policies map[uint64]*BlindedHopPolicy) (*BlindedHopPolicy, error) {
514+
515+
numPolicies := len(policies)
516+
517+
// If there are no real policies to calculate an average policy from,
518+
// then we use the default. The only thing we need to calculate here
519+
// though is the MaxHTLC value.
520+
if numPolicies == 0 {
521+
chans, err := fetchOurChannels()
522+
if err != nil {
523+
return nil, err
524+
}
525+
526+
if len(chans) == 0 {
527+
return nil, fmt.Errorf("node has no channels to " +
528+
"receive on")
529+
}
530+
531+
// Calculate the average channel capacity and use this as the
532+
// MaxHTLC value.
533+
var maxHTLC btcutil.Amount
534+
for _, c := range chans {
535+
maxHTLC += c.Capacity
536+
}
537+
538+
maxHTLC = btcutil.Amount(float64(maxHTLC) / float64(len(chans)))
539+
540+
return &BlindedHopPolicy{
541+
CLTVExpiryDelta: defaultPolicy.CLTVExpiryDelta,
542+
FeeRate: defaultPolicy.FeeRate,
543+
BaseFee: defaultPolicy.BaseFee,
544+
MinHTLCMsat: defaultPolicy.MinHTLCMsat,
545+
MaxHTLCMsat: lnwire.NewMSatFromSatoshis(maxHTLC),
546+
}, nil
547+
}
548+
549+
var avgPolicy BlindedHopPolicy
550+
551+
for _, policy := range policies {
552+
avgPolicy.MinHTLCMsat += policy.MinHTLCMsat
553+
avgPolicy.MaxHTLCMsat += policy.MaxHTLCMsat
554+
avgPolicy.BaseFee += policy.BaseFee
555+
avgPolicy.FeeRate += policy.FeeRate
556+
avgPolicy.CLTVExpiryDelta += policy.CLTVExpiryDelta
557+
}
558+
559+
avgPolicy.MinHTLCMsat = lnwire.MilliSatoshi(
560+
float64(avgPolicy.MinHTLCMsat) / float64(numPolicies),
561+
)
562+
avgPolicy.MaxHTLCMsat = lnwire.MilliSatoshi(
563+
float64(avgPolicy.MaxHTLCMsat) / float64(numPolicies),
564+
)
565+
avgPolicy.BaseFee = lnwire.MilliSatoshi(
566+
float64(avgPolicy.BaseFee) / float64(numPolicies),
567+
)
568+
avgPolicy.FeeRate = uint32(
569+
float64(avgPolicy.FeeRate) / float64(numPolicies),
570+
)
571+
avgPolicy.CLTVExpiryDelta = uint16(
572+
float64(avgPolicy.CLTVExpiryDelta) / float64(numPolicies),
573+
)
574+
575+
return &avgPolicy, nil
576+
}
577+
438578
// buildHopRouteData constructs the record.BlindedRouteData struct for the given
439579
// non-final hop on a blinded path and packages it with the node's ID.
440580
func buildHopRouteData(node route.Vertex, scid lnwire.ShortChannelID,

routing/blindedpath/blinded_path_test.go

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -802,7 +802,7 @@ func TestBuildBlindedPathWithDummyHops(t *testing.T) {
802802
// hops to be added to the real route.
803803
MinNumHops: 4,
804804

805-
DummyHopPolicy: &BlindedHopPolicy{
805+
DefaultDummyHopPolicy: &BlindedHopPolicy{
806806
CLTVExpiryDelta: 50,
807807
FeeRate: 100,
808808
BaseFee: 100,
@@ -817,8 +817,8 @@ func TestBuildBlindedPathWithDummyHops(t *testing.T) {
817817

818818
// Check that all the accumulated policy values are correct.
819819
require.EqualValues(t, 403, path.FeeBaseMsat)
820-
require.EqualValues(t, 1203, path.FeeRate)
821-
require.EqualValues(t, 400, path.CltvExpiryDelta)
820+
require.EqualValues(t, 2003, path.FeeRate)
821+
require.EqualValues(t, 588, path.CltvExpiryDelta)
822822
require.EqualValues(t, 1000, path.HTLCMinMsat)
823823
require.EqualValues(t, lnwire.MaxMilliSatoshi, path.HTLCMaxMsat)
824824

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

863863
require.Equal(t, record.PaymentConstraints{
864-
MaxCltvExpiry: 1600,
864+
MaxCltvExpiry: 1788,
865865
HtlcMinimumMsat: 1000,
866866
}, data.Constraints.UnwrapOrFail(t).Val)
867867

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

885885
require.Equal(t, record.PaymentConstraints{
886-
MaxCltvExpiry: 1456,
886+
MaxCltvExpiry: 1644,
887887
HtlcMinimumMsat: 1000,
888888
}, data.Constraints.UnwrapOrFail(t).Val)
889889

rpcserver.go

Lines changed: 13 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -75,6 +75,7 @@ import (
7575
"github.com/lightningnetwork/lnd/peernotifier"
7676
"github.com/lightningnetwork/lnd/record"
7777
"github.com/lightningnetwork/lnd/routing"
78+
"github.com/lightningnetwork/lnd/routing/blindedpath"
7879
"github.com/lightningnetwork/lnd/routing/route"
7980
"github.com/lightningnetwork/lnd/rpcperms"
8081
"github.com/lightningnetwork/lnd/signal"
@@ -5825,7 +5826,18 @@ func (r *rpcServer) AddInvoice(ctx context.Context,
58255826
blindingRestrictions,
58265827
)
58275828
},
5828-
MinNumHops: r.server.cfg.Routing.BlindedPaths.NumHops,
5829+
MinNumBlindedPathHops: r.server.cfg.Routing.BlindedPaths.
5830+
NumHops,
5831+
DefaultDummyHopPolicy: &blindedpath.BlindedHopPolicy{
5832+
CLTVExpiryDelta: uint16(defaultDelta),
5833+
FeeRate: uint32(r.server.cfg.Bitcoin.FeeRate),
5834+
BaseFee: r.server.cfg.Bitcoin.BaseFee,
5835+
MinHTLCMsat: r.server.cfg.Bitcoin.MinHTLCIn,
5836+
5837+
// MaxHTLCMsat will be calculated on the fly by using
5838+
// the introduction node's channel's capacities.
5839+
MaxHTLCMsat: 0,
5840+
},
58295841
}
58305842

58315843
value, err := lnrpc.UnmarshallAmt(invoice.Value, invoice.ValueMsat)

0 commit comments

Comments
 (0)