Skip to content

Commit

Permalink
Merge pull request #9316 from ziggie1984/fix-blindedpath-mc
Browse files Browse the repository at this point in the history
routing: fix mc blinded path behaviour.
  • Loading branch information
guggero authored Dec 10, 2024
2 parents 8c9de4d + 101311d commit 7a34015
Show file tree
Hide file tree
Showing 9 changed files with 574 additions and 115 deletions.
4 changes: 4 additions & 0 deletions docs/release-notes/release-notes-0.18.4.md
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,9 @@
* [Fixed a bug](https://github.com/lightningnetwork/lnd/pull/9324) to prevent
potential deadlocks when LND depends on external components (e.g. aux
components, hooks).

* [Make sure blinded payment failures are handled correctly in the mission
controller](https://github.com/lightningnetwork/lnd/pull/9316).

# New Features

Expand Down Expand Up @@ -121,4 +124,5 @@ types in a series of changes:
* George Tsagkarelis
* Olaoluwa Osuntokun
* Oliver Gugger
* Ziggie

4 changes: 2 additions & 2 deletions docs/release-notes/release-notes-0.19.0.md
Original file line number Diff line number Diff line change
Expand Up @@ -60,7 +60,7 @@
* [Make the contract resolutions for the channel arbitrator optional](
https://github.com/lightningnetwork/lnd/pull/9253)

* [Fixed a bug](https://github.com/lightningnetwork/lnd/pull/9322) that caused
* [Fixed a bug](https://github.com/lightningnetwork/lnd/pull/9322) that caused
estimateroutefee to ignore the default payment timeout.

# New Features
Expand All @@ -80,7 +80,7 @@
## RPC Additions

* [Add a new rpc endpoint](https://github.com/lightningnetwork/lnd/pull/8843)
`BumpForceCloseFee` which moves the functionality soley available in the
`BumpForceCloseFee` which moves the functionality solely available in the
`lncli` to LND hence making it more universal.

* [The `walletrpc.FundPsbt` RPC method now has an option to specify the fee as
Expand Down
6 changes: 3 additions & 3 deletions input/script_utils.go
Original file line number Diff line number Diff line change
Expand Up @@ -29,9 +29,9 @@ var (
SequenceLockTimeSeconds = uint32(1 << 22)
)

// mustParsePubKey parses a hex encoded public key string into a public key and
// MustParsePubKey parses a hex encoded public key string into a public key and
// panic if parsing fails.
func mustParsePubKey(pubStr string) btcec.PublicKey {
func MustParsePubKey(pubStr string) btcec.PublicKey {
pubBytes, err := hex.DecodeString(pubStr)
if err != nil {
panic(err)
Expand All @@ -55,7 +55,7 @@ var (
// https://github.com/lightninglabs/lightning-node-connect/tree/
// master/mailbox/numsgen, with the seed phrase "Lightning Simple
// Taproot".
TaprootNUMSKey = mustParsePubKey(TaprootNUMSHex)
TaprootNUMSKey = MustParsePubKey(TaprootNUMSHex)
)

// Signature is an interface for objects that can populate signatures during
Expand Down
219 changes: 174 additions & 45 deletions routing/blinding.go
Original file line number Diff line number Diff line change
@@ -1,17 +1,27 @@
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/fn/v2"
"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.
Expand All @@ -25,6 +35,14 @@ var (
// 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
Expand Down Expand Up @@ -70,7 +88,9 @@ type BlindedPaymentPathSet struct {
}

// NewBlindedPaymentPathSet constructs a new BlindedPaymentPathSet from a set of
// BlindedPayments.
// 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) {

Expand Down Expand Up @@ -103,36 +123,53 @@ func NewBlindedPaymentPathSet(paths []*BlindedPayment) (*BlindedPaymentPathSet,
}
}

// 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
// Deep copy the paths to avoid mutating the original paths.
pathSet := make([]*BlindedPayment, len(paths))
for i, path := range paths {
pathSet[i] = path.deepCopy()
}
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
// 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
}

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{
Expand Down Expand Up @@ -198,21 +235,33 @@ func (s *BlindedPaymentPathSet) FinalCLTVDelta() uint16 {
// 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 {
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
return largestPath, nil
}

// ToRouteHints converts the blinded path payment set into a RouteHints map so
Expand All @@ -222,7 +271,7 @@ func (s *BlindedPaymentPathSet) ToRouteHints() (RouteHints, error) {
hints := make(RouteHints)

for _, path := range s.paths {
pathHints, err := path.toRouteHints(fn.Some(s.targetPubKey))
pathHints, err := path.toRouteHints()
if err != nil {
return nil, err
}
Expand All @@ -239,6 +288,12 @@ func (s *BlindedPaymentPathSet) ToRouteHints() (RouteHints, error) {
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 {
Expand Down Expand Up @@ -291,6 +346,22 @@ func (b *BlindedPayment) Validate() error {
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
}

Expand All @@ -301,11 +372,8 @@ func (b *BlindedPayment) Validate() error {
// 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) {

// 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
Expand Down Expand Up @@ -393,16 +461,77 @@ func (b *BlindedPayment) toRouteHints(
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)
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)),
}

// Then override the final hint with this updated edge.
hints[fromNode] = []AdditionalEdge{lastEdge}
})
if b.BlindedPath.IntroductionPoint != nil {
cpyPayment.BlindedPath.IntroductionPoint =
copyPublicKey(b.BlindedPath.IntroductionPoint)
}

return hints, nil
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)
}
Loading

0 comments on commit 7a34015

Please sign in to comment.