mirror of
https://github.com/lightningnetwork/lnd.git
synced 2025-01-19 05:45:21 +01:00
3cec72ae9c
Fixes a bug and makes the function more robust. Before we would always return the encrypted data size of last hop of the last path. Now we return the greatest last hop payload not always the one of the last path.
538 lines
17 KiB
Go
538 lines
17 KiB
Go
package routing
|
|
|
|
import (
|
|
"bytes"
|
|
"errors"
|
|
"fmt"
|
|
|
|
"github.com/btcsuite/btcd/btcec/v2"
|
|
"github.com/decred/dcrd/dcrec/secp256k1/v4"
|
|
sphinx "github.com/lightningnetwork/lightning-onion"
|
|
"github.com/lightningnetwork/lnd/graph/db/models"
|
|
"github.com/lightningnetwork/lnd/input"
|
|
"github.com/lightningnetwork/lnd/lnwire"
|
|
"github.com/lightningnetwork/lnd/routing/route"
|
|
)
|
|
|
|
// BlindedPathNUMSHex is the hex encoded version of the blinded path target
|
|
// NUMs key (in compressed format) which has no known private key.
|
|
// This was generated using the following script:
|
|
// https://github.com/lightninglabs/lightning-node-connect/tree/master/
|
|
// mailbox/numsgen, with the seed phrase "Lightning Blinded Path".
|
|
const BlindedPathNUMSHex = "02667a98ef82ecb522f803b17a74f14508a48b25258f9831" +
|
|
"dd6e95f5e299dfd54e"
|
|
|
|
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")
|
|
|
|
// BlindedPathNUMSKey is a NUMS key (nothing up my sleeves number) that
|
|
// has no known private key.
|
|
BlindedPathNUMSKey = input.MustParsePubKey(BlindedPathNUMSHex)
|
|
|
|
// CompressedBlindedPathNUMSKey is the compressed version of the
|
|
// BlindedPathNUMSKey.
|
|
CompressedBlindedPathNUMSKey = BlindedPathNUMSKey.SerializeCompressed()
|
|
)
|
|
|
|
// 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. For blinded paths which have more than one single hop a
|
|
// dummy hop via a NUMS key is appeneded to allow for MPP path finding via
|
|
// multiple blinded paths.
|
|
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")
|
|
}
|
|
}
|
|
|
|
// Deep copy the paths to avoid mutating the original paths.
|
|
pathSet := make([]*BlindedPayment, len(paths))
|
|
for i, path := range paths {
|
|
pathSet[i] = path.deepCopy()
|
|
}
|
|
|
|
// For blinded paths we use the NUMS key as a target if the blinded
|
|
// path has more hops than just the introduction node.
|
|
targetPub := &BlindedPathNUMSKey
|
|
|
|
var finalCLTVDelta uint16
|
|
|
|
// In case the paths do NOT include a single hop route we append a
|
|
// dummy hop via a NUMS key to allow for MPP path finding via multiple
|
|
// blinded paths. A unified target is needed to use all blinded paths
|
|
// during the payment lifecycle. A dummy hop is solely added for the
|
|
// path finding process and is removed after the path is found. This
|
|
// ensures that we still populate the mission control with the correct
|
|
// data and also respect these mc entries when looking for a path.
|
|
for _, path := range pathSet {
|
|
pathLength := len(path.BlindedPath.BlindedHops)
|
|
|
|
// 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.
|
|
if pathLength == 1 {
|
|
pathSet = []*BlindedPayment{path}
|
|
finalCLTVDelta = path.CltvExpiryDelta
|
|
targetPub = path.BlindedPath.IntroductionPoint
|
|
|
|
break
|
|
}
|
|
|
|
lastHop := path.BlindedPath.BlindedHops[pathLength-1]
|
|
path.BlindedPath.BlindedHops = append(
|
|
path.BlindedPath.BlindedHops,
|
|
&sphinx.BlindedHopInfo{
|
|
BlindedNodePub: &BlindedPathNUMSKey,
|
|
// We add the last hop's cipher text so that
|
|
// the payload size of the final hop is equal
|
|
// to the real last hop.
|
|
CipherText: lastHop.CipherText,
|
|
},
|
|
)
|
|
}
|
|
|
|
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,
|
|
error) {
|
|
|
|
var (
|
|
largestPath *BlindedPayment
|
|
currentMax int
|
|
)
|
|
|
|
if len(s.paths) == 0 {
|
|
return nil, fmt.Errorf("no blinded paths in the set")
|
|
}
|
|
|
|
// We set the largest path to make sure we always return a path even
|
|
// if the cipher text is empty.
|
|
largestPath = s.paths[0]
|
|
|
|
for _, path := range s.paths {
|
|
numHops := len(path.BlindedPath.BlindedHops)
|
|
lastHop := path.BlindedPath.BlindedHops[numHops-1]
|
|
|
|
if len(lastHop.CipherText) > currentMax {
|
|
largestPath = path
|
|
currentMax = len(lastHop.CipherText)
|
|
}
|
|
}
|
|
|
|
return largestPath, nil
|
|
}
|
|
|
|
// 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()
|
|
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
|
|
}
|
|
|
|
// IsBlindedRouteNUMSTargetKey returns true if the given public key is the
|
|
// NUMS key used as a target for blinded path final hops.
|
|
func IsBlindedRouteNUMSTargetKey(pk []byte) bool {
|
|
return bytes.Equal(pk, CompressedBlindedPathNUMSKey)
|
|
}
|
|
|
|
// 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)
|
|
}
|
|
|
|
for _, hop := range b.BlindedPath.BlindedHops {
|
|
// The first hop of the blinded path does not necessarily have
|
|
// blinded node pub key because it is the introduction point.
|
|
if hop.BlindedNodePub == nil {
|
|
continue
|
|
}
|
|
|
|
if IsBlindedRouteNUMSTargetKey(
|
|
hop.BlindedNodePub.SerializeCompressed(),
|
|
) {
|
|
|
|
return fmt.Errorf("blinded path cannot include NUMS "+
|
|
"key: %s", BlindedPathNUMSHex)
|
|
}
|
|
}
|
|
|
|
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).
|
|
func (b *BlindedPayment) toRouteHints() (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}
|
|
}
|
|
|
|
return hints, nil
|
|
}
|
|
|
|
// deepCopy returns a deep copy of the BlindedPayment.
|
|
func (b *BlindedPayment) deepCopy() *BlindedPayment {
|
|
if b == nil {
|
|
return nil
|
|
}
|
|
|
|
cpyPayment := &BlindedPayment{
|
|
BaseFee: b.BaseFee,
|
|
ProportionalFeeRate: b.ProportionalFeeRate,
|
|
CltvExpiryDelta: b.CltvExpiryDelta,
|
|
HtlcMinimum: b.HtlcMinimum,
|
|
HtlcMaximum: b.HtlcMaximum,
|
|
}
|
|
|
|
// Deep copy the BlindedPath if it exists
|
|
if b.BlindedPath != nil {
|
|
cpyPayment.BlindedPath = &sphinx.BlindedPath{
|
|
BlindedHops: make([]*sphinx.BlindedHopInfo,
|
|
len(b.BlindedPath.BlindedHops)),
|
|
}
|
|
|
|
if b.BlindedPath.IntroductionPoint != nil {
|
|
cpyPayment.BlindedPath.IntroductionPoint =
|
|
copyPublicKey(b.BlindedPath.IntroductionPoint)
|
|
}
|
|
|
|
if b.BlindedPath.BlindingPoint != nil {
|
|
cpyPayment.BlindedPath.BlindingPoint =
|
|
copyPublicKey(b.BlindedPath.BlindingPoint)
|
|
}
|
|
|
|
// Copy each blinded hop info.
|
|
for i, hop := range b.BlindedPath.BlindedHops {
|
|
if hop == nil {
|
|
continue
|
|
}
|
|
|
|
cpyHop := &sphinx.BlindedHopInfo{
|
|
CipherText: hop.CipherText,
|
|
}
|
|
|
|
if hop.BlindedNodePub != nil {
|
|
cpyHop.BlindedNodePub =
|
|
copyPublicKey(hop.BlindedNodePub)
|
|
}
|
|
|
|
cpyHop.CipherText = make([]byte, len(hop.CipherText))
|
|
copy(cpyHop.CipherText, hop.CipherText)
|
|
|
|
cpyPayment.BlindedPath.BlindedHops[i] = cpyHop
|
|
}
|
|
}
|
|
|
|
// Deep copy the Features if they exist
|
|
if b.Features != nil {
|
|
cpyPayment.Features = b.Features.Clone()
|
|
}
|
|
|
|
return cpyPayment
|
|
}
|
|
|
|
// copyPublicKey makes a deep copy of a public key.
|
|
//
|
|
// TODO(ziggie): Remove this function if this is available in the btcec library.
|
|
func copyPublicKey(pk *btcec.PublicKey) *btcec.PublicKey {
|
|
var result secp256k1.JacobianPoint
|
|
pk.AsJacobian(&result)
|
|
result.ToAffine()
|
|
|
|
return btcec.NewPublicKey(&result.X, &result.Y)
|
|
}
|