lnd/routing/blinding.go
Elle Mouton e87110317b
routing: final changes to BlindedPaymentPathSet
Continue adding some complexity behind the BlindedPaymentPathSet. What
we do here is add a new IntroNodeOnlyPath method. The assumption we
make here is: If multiple blinded paths are provided to us in an invoice
but one of those paths only includes an intro node, then there is no
point in looking at any other path since we know that the intro node is
the destination node. So in such a case, we would have discarded any
other path in the `NewBlindedPaymentPathSet` constructor. So then we
would only have a single blinded path made up of an introduction node
only. In this specific case, in the `newRoute` function, no edge passed
to the function would have a blindedPayment associated with it (since
there are no blinded hops in this case). So we will have a case where
`blindedPathSet` passed to `newRoute` is not nil but `blindedPayment` is
nil since nonce was extacted from any edge. If this happens then we can
assume that this is the Intro-Node-Only situation described above. And
so we grabe the associated payment from the path set.
2024-07-31 09:15:43 +02:00

409 lines
13 KiB
Go

package routing
import (
"errors"
"fmt"
"github.com/btcsuite/btcd/btcec/v2"
sphinx "github.com/lightningnetwork/lightning-onion"
"github.com/lightningnetwork/lnd/channeldb/models"
"github.com/lightningnetwork/lnd/fn"
"github.com/lightningnetwork/lnd/lnwire"
"github.com/lightningnetwork/lnd/routing/route"
)
var (
// ErrNoBlindedPath is returned when the blinded path in a blinded
// payment is missing.
ErrNoBlindedPath = errors.New("blinded path required")
// ErrInsufficientBlindedHops is returned when a blinded path does
// not have enough blinded hops.
ErrInsufficientBlindedHops = errors.New("blinded path requires " +
"at least one hop")
// ErrHTLCRestrictions is returned when a blinded path has invalid
// HTLC maximum and minimum values.
ErrHTLCRestrictions = errors.New("invalid htlc minimum and maximum")
)
// BlindedPaymentPathSet groups the data we need to handle sending to a set of
// blinded paths provided by the recipient of a payment.
//
// NOTE: for now this only holds a single BlindedPayment. By the end of the PR
// series, it will handle multiple paths.
type BlindedPaymentPathSet struct {
// paths is the set of blinded payment paths for a single payment.
// NOTE: For now this will always only have a single entry. By the end
// of this PR, it can hold multiple.
paths []*BlindedPayment
// targetPubKey is the ephemeral node pub key that we will inject into
// each path as the last hop. This is only for the sake of path finding.
// Once the path has been found, the original destination pub key is
// used again. In the edge case where there is only a single hop in the
// path (the introduction node is the destination node), then this will
// just be the introduction node's real public key.
targetPubKey *btcec.PublicKey
// features is the set of relay features available for the payment.
// This is extracted from the set of blinded payment paths. At the
// moment we require that all paths for the same payment have the
// same feature set.
features *lnwire.FeatureVector
// finalCLTV is the final hop's expiry delta of _any_ path in the set.
// For any multi-hop path, the final CLTV delta should be seen as zero
// since the final hop's final CLTV delta is accounted for in the
// accumulated path policy values. The only edge case is for when the
// final hop in the path is also the introduction node in which case
// that path's FinalCLTV must be the non-zero min CLTV of the final hop
// so that it is accounted for in path finding. For this reason, if
// we have any single path in the set with only one hop, then we throw
// away all the other paths. This should be fine to do since if there is
// a path where the intro node is also the destination node, then there
// isn't any need to try any other longer blinded path. In other words,
// if this value is non-zero, then there is only one path in this
// blinded path set and that path only has a single hop: the
// introduction node.
finalCLTV uint16
}
// NewBlindedPaymentPathSet constructs a new BlindedPaymentPathSet from a set of
// BlindedPayments.
func NewBlindedPaymentPathSet(paths []*BlindedPayment) (*BlindedPaymentPathSet,
error) {
if len(paths) == 0 {
return nil, ErrNoBlindedPath
}
// For now, we assert that all the paths have the same set of features.
features := paths[0].Features
noFeatures := features == nil || features.IsEmpty()
for i := 1; i < len(paths); i++ {
noFeats := paths[i].Features == nil ||
paths[i].Features.IsEmpty()
if noFeatures && !noFeats {
return nil, fmt.Errorf("all blinded paths must have " +
"the same set of features")
}
if noFeatures {
continue
}
if !features.RawFeatureVector.Equals(
paths[i].Features.RawFeatureVector,
) {
return nil, fmt.Errorf("all blinded paths must have " +
"the same set of features")
}
}
// Derive an ephemeral target priv key that will be injected into each
// blinded path final hop.
targetPriv, err := btcec.NewPrivateKey()
if err != nil {
return nil, err
}
targetPub := targetPriv.PubKey()
var (
pathSet = paths
finalCLTVDelta uint16
)
// If any provided blinded path only has a single hop (ie, the
// destination node is also the introduction node), then we discard all
// other paths since we know the real pub key of the destination node.
// We also then set the final CLTV delta to the path's delta since
// there are no other edge hints that will account for it. For a single
// hop path, there is also no need for the pseudo target pub key
// replacement, so our target pub key in this case just remains the
// real introduction node ID.
for _, path := range paths {
if len(path.BlindedPath.BlindedHops) != 1 {
continue
}
pathSet = []*BlindedPayment{path}
finalCLTVDelta = path.CltvExpiryDelta
targetPub = path.BlindedPath.IntroductionPoint
break
}
return &BlindedPaymentPathSet{
paths: pathSet,
targetPubKey: targetPub,
features: features,
finalCLTV: finalCLTVDelta,
}, nil
}
// TargetPubKey returns the public key to be used as the destination node's
// public key during pathfinding.
func (s *BlindedPaymentPathSet) TargetPubKey() *btcec.PublicKey {
return s.targetPubKey
}
// Features returns the set of relay features available for the payment.
func (s *BlindedPaymentPathSet) Features() *lnwire.FeatureVector {
return s.features
}
// IntroNodeOnlyPath can be called if it is expected that the path set only
// contains a single payment path which itself only has one hop. It errors if
// this is not the case.
func (s *BlindedPaymentPathSet) IntroNodeOnlyPath() (*BlindedPayment, error) {
if len(s.paths) != 1 {
return nil, fmt.Errorf("expected only a single path in the "+
"blinded payment set, got %d", len(s.paths))
}
if len(s.paths[0].BlindedPath.BlindedHops) > 1 {
return nil, fmt.Errorf("an intro node only path cannot have " +
"more than one hop")
}
return s.paths[0], nil
}
// IsIntroNode returns true if the given vertex is an introduction node for one
// of the paths in the blinded payment path set.
func (s *BlindedPaymentPathSet) IsIntroNode(source route.Vertex) bool {
for _, path := range s.paths {
introVertex := route.NewVertex(
path.BlindedPath.IntroductionPoint,
)
if source == introVertex {
return true
}
}
return false
}
// FinalCLTVDelta is the minimum CLTV delta to use for the final hop on the
// route. In most cases this will return zero since the value is accounted for
// in the path's accumulated CLTVExpiryDelta. Only in the edge case of the path
// set only including a single path which only includes an introduction node
// will this return a non-zero value.
func (s *BlindedPaymentPathSet) FinalCLTVDelta() uint16 {
return s.finalCLTV
}
// LargestLastHopPayloadPath returns the BlindedPayment in the set that has the
// largest last-hop payload. This is to be used for onion size estimation in
// path finding.
func (s *BlindedPaymentPathSet) LargestLastHopPayloadPath() *BlindedPayment {
var (
largestPath *BlindedPayment
currentMax int
)
for _, path := range s.paths {
numHops := len(path.BlindedPath.BlindedHops)
lastHop := path.BlindedPath.BlindedHops[numHops-1]
if len(lastHop.CipherText) > currentMax {
largestPath = path
}
}
return largestPath
}
// ToRouteHints converts the blinded path payment set into a RouteHints map so
// that the blinded payment paths can be treated like route hints throughout the
// code base.
func (s *BlindedPaymentPathSet) ToRouteHints() (RouteHints, error) {
hints := make(RouteHints)
for _, path := range s.paths {
pathHints, err := path.toRouteHints(fn.Some(s.targetPubKey))
if err != nil {
return nil, err
}
for from, edges := range pathHints {
hints[from] = append(hints[from], edges...)
}
}
if len(hints) == 0 {
return nil, nil
}
return hints, nil
}
// BlindedPayment provides the path and payment parameters required to send a
// payment along a blinded path.
type BlindedPayment struct {
// BlindedPath contains the unblinded introduction point and blinded
// hops for the blinded section of the payment.
BlindedPath *sphinx.BlindedPath
// BaseFee is the total base fee to be paid for payments made over the
// blinded path.
BaseFee uint32
// ProportionalFeeRate is the aggregated proportional fee rate for
// payments made over the blinded path.
ProportionalFeeRate uint32
// CltvExpiryDelta is the total expiry delta for the blinded path. This
// field includes the CLTV for the blinded hops *and* the final cltv
// delta for the receiver.
CltvExpiryDelta uint16
// HtlcMinimum is the highest HLTC minimum supported along the blinded
// path (while some hops may have lower values, we're effectively
// bounded by the highest minimum).
HtlcMinimum uint64
// HtlcMaximum is the lowest HTLC maximum supported along the blinded
// path (while some hops may have higher values, we're effectively
// bounded by the lowest maximum).
HtlcMaximum uint64
// Features is the set of relay features available for the payment.
Features *lnwire.FeatureVector
}
// Validate performs validation on a blinded payment.
func (b *BlindedPayment) Validate() error {
if b.BlindedPath == nil {
return ErrNoBlindedPath
}
// The sphinx library inserts the introduction node as the first hop,
// so we expect at least one hop.
if len(b.BlindedPath.BlindedHops) < 1 {
return fmt.Errorf("%w got: %v", ErrInsufficientBlindedHops,
len(b.BlindedPath.BlindedHops))
}
if b.HtlcMaximum < b.HtlcMinimum {
return fmt.Errorf("%w: %v < %v", ErrHTLCRestrictions,
b.HtlcMaximum, b.HtlcMinimum)
}
return nil
}
// toRouteHints produces a set of chained route hints that represent a blinded
// path. In the case of a single hop blinded route (which is paying directly
// to the introduction point), no hints will be returned. In this case callers
// *must* account for the blinded route's CLTV delta elsewhere (as this is
// effectively the final_cltv_delta for the receiving introduction node). In
// the case of multiple blinded hops, CLTV delta is fully accounted for in the
// hints (both for intermediate hops and the final_cltv_delta for the receiving
// node). The pseudoTarget, if provided, will be used to override the pub key
// of the destination node in the path.
func (b *BlindedPayment) toRouteHints(
pseudoTarget fn.Option[*btcec.PublicKey]) (RouteHints, error) {
// If we just have a single hop in our blinded route, it just contains
// an introduction node (this is a valid path according to the spec).
// Since we have the un-blinded node ID for the introduction node, we
// don't need to add any route hints.
if len(b.BlindedPath.BlindedHops) == 1 {
return nil, nil
}
hintCount := len(b.BlindedPath.BlindedHops) - 1
hints := make(
RouteHints, hintCount,
)
// Start at the unblinded introduction node, because our pathfinding
// will be able to locate this point in the graph.
fromNode := route.NewVertex(b.BlindedPath.IntroductionPoint)
features := lnwire.EmptyFeatureVector()
if b.Features != nil {
features = b.Features.Clone()
}
// Use the total aggregate relay parameters for the entire blinded
// route as the policy for the hint from our introduction node. This
// will ensure that pathfinding provides sufficient fees/delay for the
// blinded portion to the introduction node.
firstBlindedHop := b.BlindedPath.BlindedHops[1].BlindedNodePub
edgePolicy := &models.CachedEdgePolicy{
TimeLockDelta: b.CltvExpiryDelta,
MinHTLC: lnwire.MilliSatoshi(b.HtlcMinimum),
MaxHTLC: lnwire.MilliSatoshi(b.HtlcMaximum),
FeeBaseMSat: lnwire.MilliSatoshi(b.BaseFee),
FeeProportionalMillionths: lnwire.MilliSatoshi(
b.ProportionalFeeRate,
),
ToNodePubKey: func() route.Vertex {
return route.NewVertex(
// The first node in this slice is
// the introduction node, so we start
// at index 1 to get the first blinded
// relaying node.
firstBlindedHop,
)
},
ToNodeFeatures: features,
}
lastEdge, err := NewBlindedEdge(edgePolicy, b, 0)
if err != nil {
return nil, err
}
hints[fromNode] = []AdditionalEdge{lastEdge}
// Start at an offset of 1 because the first node in our blinded hops
// is the introduction node and terminate at the second-last node
// because we're dealing with hops as pairs.
for i := 1; i < hintCount; i++ {
// Set our origin node to the current
fromNode = route.NewVertex(
b.BlindedPath.BlindedHops[i].BlindedNodePub,
)
// Create a hint which has no fee or cltv delta. We
// specifically want zero values here because our relay
// parameters are expressed in encrypted blobs rather than the
// route itself for blinded routes.
nextHopIdx := i + 1
nextNode := route.NewVertex(
b.BlindedPath.BlindedHops[nextHopIdx].BlindedNodePub,
)
edgePolicy := &models.CachedEdgePolicy{
ToNodePubKey: func() route.Vertex {
return nextNode
},
ToNodeFeatures: features,
}
lastEdge, err = NewBlindedEdge(edgePolicy, b, i)
if err != nil {
return nil, err
}
hints[fromNode] = []AdditionalEdge{lastEdge}
}
pseudoTarget.WhenSome(func(key *btcec.PublicKey) {
// For the very last hop on the path, switch out the ToNodePub
// for the pseudo target pub key.
lastEdge.policy.ToNodePubKey = func() route.Vertex {
return route.NewVertex(key)
}
// Then override the final hint with this updated edge.
hints[fromNode] = []AdditionalEdge{lastEdge}
})
return hints, nil
}