diff --git a/lnrpc/invoicesrpc/addinvoice.go b/lnrpc/invoicesrpc/addinvoice.go index 69bc0a16e2..bef42a1f8a 100644 --- a/lnrpc/invoicesrpc/addinvoice.go +++ b/lnrpc/invoicesrpc/addinvoice.go @@ -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. @@ -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, @@ -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 { diff --git a/routing/blindedpath/blinded_path.go b/routing/blindedpath/blinded_path.go index 03c24747b0..542882e9ff 100644 --- a/routing/blindedpath/blinded_path.go +++ b/routing/blindedpath/blinded_path.go @@ -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" @@ -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) @@ -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) @@ -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 @@ -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 @@ -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, diff --git a/routing/blindedpath/blinded_path_test.go b/routing/blindedpath/blinded_path_test.go index 1f5d685d13..b63509de7d 100644 --- a/routing/blindedpath/blinded_path_test.go +++ b/routing/blindedpath/blinded_path_test.go @@ -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, @@ -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) @@ -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) @@ -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) diff --git a/rpcserver.go b/rpcserver.go index fb3c46b822..ea774a6d5c 100644 --- a/rpcserver.go +++ b/rpcserver.go @@ -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" @@ -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)