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/graph/db/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:ll // 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 }