Skip to content

Commit 3447484

Browse files
committed
Merge branch '0-18-4-branch-rc2-9316' into 0-18-4-branch-rc2
2 parents f312064 + 71eb1ae commit 3447484

8 files changed

+611
-122
lines changed

docs/release-notes/release-notes-0.18.4.md

+4
Original file line numberDiff line numberDiff line change
@@ -29,6 +29,9 @@
2929
* [Fixed a bug](https://github.com/lightningnetwork/lnd/pull/9324) to prevent
3030
potential deadlocks when LND depends on external components (e.g. aux
3131
components, hooks).
32+
33+
* [Make sure blinded payment failures are handled correctly in the mission
34+
controller](https://github.com/lightningnetwork/lnd/pull/9316).
3235

3336
# New Features
3437

@@ -125,4 +128,5 @@ types in a series of changes:
125128
* George Tsagkarelis
126129
* Olaoluwa Osuntokun
127130
* Oliver Gugger
131+
* Ziggie
128132

input/script_utils.go

+3-3
Original file line numberDiff line numberDiff line change
@@ -29,9 +29,9 @@ var (
2929
SequenceLockTimeSeconds = uint32(1 << 22)
3030
)
3131

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

6161
// Signature is an interface for objects that can populate signatures during

routing/blinding.go

+174-45
Original file line numberDiff line numberDiff line change
@@ -1,17 +1,27 @@
11
package routing
22

33
import (
4+
"bytes"
45
"errors"
56
"fmt"
67

78
"github.com/btcsuite/btcd/btcec/v2"
9+
"github.com/decred/dcrd/dcrec/secp256k1/v4"
810
sphinx "github.com/lightningnetwork/lightning-onion"
911
"github.com/lightningnetwork/lnd/channeldb/models"
10-
"github.com/lightningnetwork/lnd/fn"
12+
"github.com/lightningnetwork/lnd/input"
1113
"github.com/lightningnetwork/lnd/lnwire"
1214
"github.com/lightningnetwork/lnd/routing/route"
1315
)
1416

17+
// BlindedPathNUMSHex is the hex encoded version of the blinded path target
18+
// NUMs key (in compressed format) which has no known private key.
19+
// This was generated using the following script:
20+
// https://github.com/lightninglabs/lightning-node-connect/tree/master/
21+
// mailbox/numsgen, with the seed phrase "Lightning Blinded Path".
22+
const BlindedPathNUMSHex = "02667a98ef82ecb522f803b17a74f14508a48b25258f9831" +
23+
"dd6e95f5e299dfd54e"
24+
1525
var (
1626
// ErrNoBlindedPath is returned when the blinded path in a blinded
1727
// payment is missing.
@@ -25,6 +35,14 @@ var (
2535
// ErrHTLCRestrictions is returned when a blinded path has invalid
2636
// HTLC maximum and minimum values.
2737
ErrHTLCRestrictions = errors.New("invalid htlc minimum and maximum")
38+
39+
// BlindedPathNUMSKey is a NUMS key (nothing up my sleeves number) that
40+
// has no known private key.
41+
BlindedPathNUMSKey = input.MustParsePubKey(BlindedPathNUMSHex)
42+
43+
// CompressedBlindedPathNUMSKey is the compressed version of the
44+
// BlindedPathNUMSKey.
45+
CompressedBlindedPathNUMSKey = BlindedPathNUMSKey.SerializeCompressed()
2846
)
2947

3048
// BlindedPaymentPathSet groups the data we need to handle sending to a set of
@@ -70,7 +88,9 @@ type BlindedPaymentPathSet struct {
7088
}
7189

7290
// NewBlindedPaymentPathSet constructs a new BlindedPaymentPathSet from a set of
73-
// BlindedPayments.
91+
// BlindedPayments. For blinded paths which have more than one single hop a
92+
// dummy hop via a NUMS key is appeneded to allow for MPP path finding via
93+
// multiple blinded paths.
7494
func NewBlindedPaymentPathSet(paths []*BlindedPayment) (*BlindedPaymentPathSet,
7595
error) {
7696

@@ -103,36 +123,53 @@ func NewBlindedPaymentPathSet(paths []*BlindedPayment) (*BlindedPaymentPathSet,
103123
}
104124
}
105125

106-
// Derive an ephemeral target priv key that will be injected into each
107-
// blinded path final hop.
108-
targetPriv, err := btcec.NewPrivateKey()
109-
if err != nil {
110-
return nil, err
126+
// Deep copy the paths to avoid mutating the original paths.
127+
pathSet := make([]*BlindedPayment, len(paths))
128+
for i, path := range paths {
129+
pathSet[i] = path.deepCopy()
111130
}
112-
targetPub := targetPriv.PubKey()
113131

114-
var (
115-
pathSet = paths
116-
finalCLTVDelta uint16
117-
)
118-
// If any provided blinded path only has a single hop (ie, the
119-
// destination node is also the introduction node), then we discard all
120-
// other paths since we know the real pub key of the destination node.
121-
// We also then set the final CLTV delta to the path's delta since
122-
// there are no other edge hints that will account for it. For a single
123-
// hop path, there is also no need for the pseudo target pub key
124-
// replacement, so our target pub key in this case just remains the
125-
// real introduction node ID.
126-
for _, path := range paths {
127-
if len(path.BlindedPath.BlindedHops) != 1 {
128-
continue
132+
// For blinded paths we use the NUMS key as a target if the blinded
133+
// path has more hops than just the introduction node.
134+
targetPub := &BlindedPathNUMSKey
135+
136+
var finalCLTVDelta uint16
137+
138+
// In case the paths do NOT include a single hop route we append a
139+
// dummy hop via a NUMS key to allow for MPP path finding via multiple
140+
// blinded paths. A unified target is needed to use all blinded paths
141+
// during the payment lifecycle. A dummy hop is solely added for the
142+
// path finding process and is removed after the path is found. This
143+
// ensures that we still populate the mission control with the correct
144+
// data and also respect these mc entries when looking for a path.
145+
for _, path := range pathSet {
146+
pathLength := len(path.BlindedPath.BlindedHops)
147+
148+
// If any provided blinded path only has a single hop (ie, the
149+
// destination node is also the introduction node), then we
150+
// discard all other paths since we know the real pub key of the
151+
// destination node. We also then set the final CLTV delta to
152+
// the path's delta since there are no other edge hints that
153+
// will account for it.
154+
if pathLength == 1 {
155+
pathSet = []*BlindedPayment{path}
156+
finalCLTVDelta = path.CltvExpiryDelta
157+
targetPub = path.BlindedPath.IntroductionPoint
158+
159+
break
129160
}
130161

131-
pathSet = []*BlindedPayment{path}
132-
finalCLTVDelta = path.CltvExpiryDelta
133-
targetPub = path.BlindedPath.IntroductionPoint
134-
135-
break
162+
lastHop := path.BlindedPath.BlindedHops[pathLength-1]
163+
path.BlindedPath.BlindedHops = append(
164+
path.BlindedPath.BlindedHops,
165+
&sphinx.BlindedHopInfo{
166+
BlindedNodePub: &BlindedPathNUMSKey,
167+
// We add the last hop's cipher text so that
168+
// the payload size of the final hop is equal
169+
// to the real last hop.
170+
CipherText: lastHop.CipherText,
171+
},
172+
)
136173
}
137174

138175
return &BlindedPaymentPathSet{
@@ -198,21 +235,33 @@ func (s *BlindedPaymentPathSet) FinalCLTVDelta() uint16 {
198235
// LargestLastHopPayloadPath returns the BlindedPayment in the set that has the
199236
// largest last-hop payload. This is to be used for onion size estimation in
200237
// path finding.
201-
func (s *BlindedPaymentPathSet) LargestLastHopPayloadPath() *BlindedPayment {
238+
func (s *BlindedPaymentPathSet) LargestLastHopPayloadPath() (*BlindedPayment,
239+
error) {
240+
202241
var (
203242
largestPath *BlindedPayment
204243
currentMax int
205244
)
245+
246+
if len(s.paths) == 0 {
247+
return nil, fmt.Errorf("no blinded paths in the set")
248+
}
249+
250+
// We set the largest path to make sure we always return a path even
251+
// if the cipher text is empty.
252+
largestPath = s.paths[0]
253+
206254
for _, path := range s.paths {
207255
numHops := len(path.BlindedPath.BlindedHops)
208256
lastHop := path.BlindedPath.BlindedHops[numHops-1]
209257

210258
if len(lastHop.CipherText) > currentMax {
211259
largestPath = path
260+
currentMax = len(lastHop.CipherText)
212261
}
213262
}
214263

215-
return largestPath
264+
return largestPath, nil
216265
}
217266

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

224273
for _, path := range s.paths {
225-
pathHints, err := path.toRouteHints(fn.Some(s.targetPubKey))
274+
pathHints, err := path.toRouteHints()
226275
if err != nil {
227276
return nil, err
228277
}
@@ -239,6 +288,12 @@ func (s *BlindedPaymentPathSet) ToRouteHints() (RouteHints, error) {
239288
return hints, nil
240289
}
241290

291+
// IsBlindedRouteNUMSTargetKey returns true if the given public key is the
292+
// NUMS key used as a target for blinded path final hops.
293+
func IsBlindedRouteNUMSTargetKey(pk []byte) bool {
294+
return bytes.Equal(pk, CompressedBlindedPathNUMSKey)
295+
}
296+
242297
// BlindedPayment provides the path and payment parameters required to send a
243298
// payment along a blinded path.
244299
type BlindedPayment struct {
@@ -291,6 +346,22 @@ func (b *BlindedPayment) Validate() error {
291346
b.HtlcMaximum, b.HtlcMinimum)
292347
}
293348

349+
for _, hop := range b.BlindedPath.BlindedHops {
350+
// The first hop of the blinded path does not necessarily have
351+
// blinded node pub key because it is the introduction point.
352+
if hop.BlindedNodePub == nil {
353+
continue
354+
}
355+
356+
if IsBlindedRouteNUMSTargetKey(
357+
hop.BlindedNodePub.SerializeCompressed(),
358+
) {
359+
360+
return fmt.Errorf("blinded path cannot include NUMS "+
361+
"key: %s", BlindedPathNUMSHex)
362+
}
363+
}
364+
294365
return nil
295366
}
296367

@@ -301,11 +372,8 @@ func (b *BlindedPayment) Validate() error {
301372
// effectively the final_cltv_delta for the receiving introduction node). In
302373
// the case of multiple blinded hops, CLTV delta is fully accounted for in the
303374
// hints (both for intermediate hops and the final_cltv_delta for the receiving
304-
// node). The pseudoTarget, if provided, will be used to override the pub key
305-
// of the destination node in the path.
306-
func (b *BlindedPayment) toRouteHints(
307-
pseudoTarget fn.Option[*btcec.PublicKey]) (RouteHints, error) {
308-
375+
// node).
376+
func (b *BlindedPayment) toRouteHints() (RouteHints, error) {
309377
// If we just have a single hop in our blinded route, it just contains
310378
// an introduction node (this is a valid path according to the spec).
311379
// Since we have the un-blinded node ID for the introduction node, we
@@ -393,16 +461,77 @@ func (b *BlindedPayment) toRouteHints(
393461
hints[fromNode] = []AdditionalEdge{lastEdge}
394462
}
395463

396-
pseudoTarget.WhenSome(func(key *btcec.PublicKey) {
397-
// For the very last hop on the path, switch out the ToNodePub
398-
// for the pseudo target pub key.
399-
lastEdge.policy.ToNodePubKey = func() route.Vertex {
400-
return route.NewVertex(key)
464+
return hints, nil
465+
}
466+
467+
// deepCopy returns a deep copy of the BlindedPayment.
468+
func (b *BlindedPayment) deepCopy() *BlindedPayment {
469+
if b == nil {
470+
return nil
471+
}
472+
473+
cpyPayment := &BlindedPayment{
474+
BaseFee: b.BaseFee,
475+
ProportionalFeeRate: b.ProportionalFeeRate,
476+
CltvExpiryDelta: b.CltvExpiryDelta,
477+
HtlcMinimum: b.HtlcMinimum,
478+
HtlcMaximum: b.HtlcMaximum,
479+
}
480+
481+
// Deep copy the BlindedPath if it exists
482+
if b.BlindedPath != nil {
483+
cpyPayment.BlindedPath = &sphinx.BlindedPath{
484+
BlindedHops: make([]*sphinx.BlindedHopInfo,
485+
len(b.BlindedPath.BlindedHops)),
401486
}
402487

403-
// Then override the final hint with this updated edge.
404-
hints[fromNode] = []AdditionalEdge{lastEdge}
405-
})
488+
if b.BlindedPath.IntroductionPoint != nil {
489+
cpyPayment.BlindedPath.IntroductionPoint =
490+
copyPublicKey(b.BlindedPath.IntroductionPoint)
491+
}
406492

407-
return hints, nil
493+
if b.BlindedPath.BlindingPoint != nil {
494+
cpyPayment.BlindedPath.BlindingPoint =
495+
copyPublicKey(b.BlindedPath.BlindingPoint)
496+
}
497+
498+
// Copy each blinded hop info.
499+
for i, hop := range b.BlindedPath.BlindedHops {
500+
if hop == nil {
501+
continue
502+
}
503+
504+
cpyHop := &sphinx.BlindedHopInfo{
505+
CipherText: hop.CipherText,
506+
}
507+
508+
if hop.BlindedNodePub != nil {
509+
cpyHop.BlindedNodePub =
510+
copyPublicKey(hop.BlindedNodePub)
511+
}
512+
513+
cpyHop.CipherText = make([]byte, len(hop.CipherText))
514+
copy(cpyHop.CipherText, hop.CipherText)
515+
516+
cpyPayment.BlindedPath.BlindedHops[i] = cpyHop
517+
}
518+
}
519+
520+
// Deep copy the Features if they exist
521+
if b.Features != nil {
522+
cpyPayment.Features = b.Features.Clone()
523+
}
524+
525+
return cpyPayment
526+
}
527+
528+
// copyPublicKey makes a deep copy of a public key.
529+
//
530+
// TODO(ziggie): Remove this function if this is available in the btcec library.
531+
func copyPublicKey(pk *btcec.PublicKey) *btcec.PublicKey {
532+
var result secp256k1.JacobianPoint
533+
pk.AsJacobian(&result)
534+
result.ToAffine()
535+
536+
return btcec.NewPublicKey(&result.X, &result.Y)
408537
}

0 commit comments

Comments
 (0)