• Home
  • Features
  • Pricing
  • Docs
  • Announcements
  • Sign In

lightningnetwork / lnd / 14721313522

29 Apr 2025 01:25AM UTC coverage: 58.598% (+0.006%) from 58.592%
14721313522

Pull #9489

github

web-flow
Merge 290053529 into b34afa33f
Pull Request #9489: multi: add BuildOnion, SendOnion, and TrackOnion RPCs

360 of 553 new or added lines in 10 files covered. (65.1%)

83 existing lines in 10 files now uncovered.

97747 of 166809 relevant lines covered (58.6%)

1.82 hits per line

Source File
Press 'n' to go to next uncovered line, 'b' for previous

62.88
/lnrpc/switchrpc/switch_server.go
1
//go:build switchrpc
2
// +build switchrpc
3

4
package switchrpc
5

6
import (
7
        "bytes"
8
        "context"
9
        "encoding/hex"
10
        "errors"
11
        "fmt"
12
        "math/big"
13
        "os"
14
        "path/filepath"
15
        "strconv"
16
        "strings"
17
        "sync"
18

19
        "github.com/RoaringBitmap/roaring/v2/roaring64"
20
        "github.com/btcsuite/btcd/btcec/v2"
21
        "github.com/grpc-ecosystem/grpc-gateway/v2/runtime"
22
        sphinx "github.com/lightningnetwork/lightning-onion"
23
        "github.com/lightningnetwork/lnd/channeldb"
24
        "github.com/lightningnetwork/lnd/htlcswitch"
25
        "github.com/lightningnetwork/lnd/lnrpc"
26
        "github.com/lightningnetwork/lnd/lntypes"
27
        "github.com/lightningnetwork/lnd/lnwire"
28
        "github.com/lightningnetwork/lnd/macaroons"
29
        "github.com/lightningnetwork/lnd/routing"
30
        "github.com/lightningnetwork/lnd/tlv"
31
        grpc "google.golang.org/grpc"
32
        codes "google.golang.org/grpc/codes"
33
        status "google.golang.org/grpc/status"
34
        "gopkg.in/macaroon-bakery.v2/bakery"
35
)
36

37
const (
38
        // subServerName is the name of the sub rpc server. We'll use this name
39
        // to register ourselves, and we also require that the main
40
        // SubServerConfigDispatcher instance recognize it as the name of our
41
        // RPC service.
42
        subServerName = "SwitchRPC"
43
)
44

45
var (
46
        // macaroonOps are the set of capabilities that our minted macaroon (if
47
        // it doesn't already exist) will have.
48
        macaroonOps = []bakery.Op{
49
                {
50
                        Entity: "offchain",
51
                        Action: "read",
52
                },
53
                {
54
                        Entity: "offchain",
55
                        Action: "write",
56
                },
57
        }
58

59
        // macPermissions maps RPC calls to the permissions they require.
60
        macPermissions = map[string][]bakery.Op{
61
                "/switchrpc.Switch/SendOnion": {{
62
                        Entity: "offchain",
63
                        Action: "write",
64
                }},
65
                "/switchrpc.Switch/TrackOnion": {{
66
                        Entity: "offchain",
67
                        Action: "read",
68
                }},
69
                "/switchrpc.Switch/BuildOnion": {{
70
                        Entity: "offchain",
71
                        Action: "read",
72
                }},
73
        }
74

75
        // DefaultSwitchMacFilename is the default name of the switch macaroon
76
        // that we expect to find via a file handle within the main
77
        // configuration file in this package.
78
        DefaultSwitchMacFilename = "switch.macaroon"
79
)
80

81
// ServerShell is a shell struct holding a reference to the actual sub-server.
82
// It is used to register the gRPC sub-server with the root server before we
83
// have the necessary dependencies to populate the actual sub-server.
84
type ServerShell struct {
85
        SwitchServer
86
}
87

88
type Server struct {
89
        cfg *Config
90

91
        // usedAttemptIDs maintains the set of attempt IDs that have been used
92
        // in either SendOnion or TrackOnion. This ensures that if TrackOnion
93
        // returns PAYMENT_ID_NOT_FOUND or SendOnion initiates HTLC creation for
94
        // a given attempt ID, SendOnion cannot subsequently succeed with the
95
        // same attempt ID. This mechanism safeguards against overpayment in
96
        // scenarios where network requests are reordered. If an attempt ID has
97
        // already been used by either SendOnion or TrackOnion, SendOnion will
98
        // return DUPLICATE_HTLC for that attempt ID.
99
        usedAttemptIDs *roaring64.Bitmap
100

101
        // usedAttemptIDsMu is a mutex which protects accesses to
102
        // usedAttemptIDs.
103
        usedAttemptIDsMu sync.Mutex
104

105
        // Required by the grpc-gateway/v2 library for forward compatibility.
106
        // Must be after the atomically used variables to not break struct
107
        // alignment.
108
        UnimplementedSwitchServer
109
}
110

111
// tryAddAttemptID tries to add attemptID to usedAttemptIDs under the mutex and
112
// returns if the attemptID is new.
113
func (s *Server) tryAddAttemptID(attemptID uint64) bool {
3✔
114
        s.usedAttemptIDsMu.Lock()
3✔
115
        defer s.usedAttemptIDsMu.Unlock()
3✔
116

3✔
117
        return s.usedAttemptIDs.CheckedAdd(attemptID)
3✔
118
}
3✔
119

120
// tryAddAttemptID adds attemptID to usedAttemptIDs under the mutex.
121
func (s *Server) addAttemptID(attemptID uint64) {
3✔
122
        s.usedAttemptIDsMu.Lock()
3✔
123
        defer s.usedAttemptIDsMu.Unlock()
3✔
124

3✔
125
        s.usedAttemptIDs.Add(attemptID)
3✔
126
}
3✔
127

128
// New creates a new instance of the SwitchServer given a configuration struct
129
// that contains all external dependencies. If the target macaroon exists, and
130
// we're unable to create it, then an error will be returned. We also return
131
// the set of permissions that we require as a server. At the time of writing
132
// of this documentation, this is the same macaroon as the admin macaroon.
133
func New(cfg *Config) (*Server, lnrpc.MacaroonPerms, error) {
3✔
134
        // If the path of the router macaroon wasn't generated, then we'll
3✔
135
        // assume that it's found at the default network directory.
3✔
136
        if cfg.SwitchMacPath == "" {
6✔
137
                cfg.SwitchMacPath = filepath.Join(
3✔
138
                        cfg.NetworkDir, DefaultSwitchMacFilename,
3✔
139
                )
3✔
140
        }
3✔
141

142
        // Now that we know the full path of the switch macaroon, we can check
143
        // to see if we need to create it or not. If stateless_init is set
144
        // then we don't write the macaroons.
145
        macFilePath := cfg.SwitchMacPath
3✔
146
        if cfg.MacService != nil && !cfg.MacService.StatelessInit &&
3✔
147
                !lnrpc.FileExists(macFilePath) {
3✔
NEW
148

×
NEW
149
                log.Infof("Making macaroons for Switch RPC Server at: %v",
×
NEW
150
                        macFilePath)
×
NEW
151

×
NEW
152
                // At this point, we know that the switch macaroon doesn't yet,
×
NEW
153
                // exist, so we need to create it with the help of the main
×
NEW
154
                // macaroon service.
×
NEW
155
                switchMac, err := cfg.MacService.NewMacaroon(
×
NEW
156
                        context.Background(), macaroons.DefaultRootKeyID,
×
NEW
157
                        macaroonOps...,
×
NEW
158
                )
×
NEW
159
                if err != nil {
×
NEW
160
                        return nil, nil, err
×
NEW
161
                }
×
NEW
162
                switchMacBytes, err := switchMac.M().MarshalBinary()
×
NEW
163
                if err != nil {
×
NEW
164
                        return nil, nil, err
×
NEW
165
                }
×
NEW
166
                err = os.WriteFile(macFilePath, switchMacBytes, 0644)
×
NEW
167
                if err != nil {
×
NEW
168
                        _ = os.Remove(macFilePath)
×
NEW
169
                        return nil, nil, err
×
NEW
170
                }
×
171
        }
172

173
        switchServer := &Server{
3✔
174
                cfg:            cfg,
3✔
175
                usedAttemptIDs: roaring64.New(),
3✔
176
                // quit: make(chan struct{}),
3✔
177
        }
3✔
178

3✔
179
        return switchServer, macPermissions, nil
3✔
180
}
181

182
// Start launches any helper goroutines required for the Server to function.
183
//
184
// NOTE: This is part of the lnrpc.SubServer interface.
185
func (s *Server) Start() error {
3✔
186
        return nil
3✔
187
}
3✔
188

189
// Stop signals any active goroutines for a graceful closure.
190
//
191
// NOTE: This is part of the lnrpc.SubServer interface.
192
func (s *Server) Stop() error {
3✔
193
        return nil
3✔
194
}
3✔
195

196
// Name returns a unique string representation of the sub-server. This can be
197
// used to identify the sub-server and also de-duplicate them.
198
//
199
// NOTE: This is part of the lnrpc.SubServer interface.
200
func (s *Server) Name() string {
3✔
201
        return subServerName
3✔
202
}
3✔
203

204
// RegisterWithRootServer will be called by the root gRPC server to direct a
205
// sub RPC server to register itself with the main gRPC root server. Until this
206
// is called, each sub-server won't be able to have
207
// requests routed towards it.
208
//
209
// NOTE: This is part of the lnrpc.GrpcHandler interface.
210
func (r *ServerShell) RegisterWithRootServer(grpcServer *grpc.Server) error {
3✔
211
        // We make sure that we register it with the main gRPC server to ensure
3✔
212
        // all our methods are routed properly.
3✔
213
        RegisterSwitchServer(grpcServer, r)
3✔
214

3✔
215
        log.Debugf("Switch RPC server successfully register with root " +
3✔
216
                "gRPC server")
3✔
217

3✔
218
        return nil
3✔
219
}
3✔
220

221
// RegisterWithRestServer will be called by the root REST mux to direct a sub
222
// RPC server to register itself with the main REST mux server. Until this is
223
// called, each sub-server won't be able to have requests routed towards it.
224
//
225
// NOTE: This is part of the lnrpc.GrpcHandler interface.
226
func (r *ServerShell) RegisterWithRestServer(ctx context.Context,
227
        mux *runtime.ServeMux, dest string, opts []grpc.DialOption) error {
3✔
228

3✔
229
        log.Debugf("Switch REST server successfully registered with " +
3✔
230
                "root REST server")
3✔
231

3✔
232
        return nil
3✔
233
}
3✔
234

235
// CreateSubServer populates the subserver's dependencies using the passed
236
// SubServerConfigDispatcher. This method should fully initialize the
237
// sub-server instance, making it ready for action. It returns the macaroon
238
// permissions that the sub-server wishes to pass on to the root server for all
239
// methods routed towards it.
240
//
241
// NOTE: This is part of the lnrpc.GrpcHandler interface.
242
func (r *ServerShell) CreateSubServer(configRegistry lnrpc.SubServerConfigDispatcher) (
243
        lnrpc.SubServer, lnrpc.MacaroonPerms, error) {
3✔
244

3✔
245
        subServer, macPermissions, err := createNewSubServer(configRegistry)
3✔
246
        if err != nil {
3✔
NEW
247
                return nil, nil, err
×
NEW
248
        }
×
249

250
        r.SwitchServer = subServer
3✔
251

3✔
252
        return subServer, macPermissions, nil
3✔
253
}
254

255
// SendOnion handles the incoming request to send a payment using a
256
// preconstructed onion blob provided by the caller.
257
func (s *Server) SendOnion(_ context.Context,
258
        req *SendOnionRequest) (*SendOnionResponse, error) {
3✔
259

3✔
260
        if len(req.OnionBlob) == 0 {
3✔
NEW
261
                return nil, status.Error(codes.InvalidArgument,
×
NEW
262
                        "onion blob is required")
×
NEW
263
        }
×
264
        if len(req.OnionBlob) != lnwire.OnionPacketSize {
3✔
NEW
265
                return nil, status.Errorf(codes.InvalidArgument,
×
NEW
266
                        "onion blob size=%d does not match expected %d bytes",
×
NEW
267
                        len(req.OnionBlob), lnwire.OnionPacketSize)
×
NEW
268
        }
×
269

270
        if len(req.PaymentHash) == 0 {
3✔
NEW
271
                return nil, status.Error(codes.InvalidArgument,
×
NEW
272
                        "payment hash is required")
×
NEW
273
        }
×
274

275
        amount := lnwire.MilliSatoshi(req.Amount)
3✔
276
        if amount <= 0 {
3✔
NEW
277
                return nil, status.Error(codes.InvalidArgument,
×
NEW
278
                        "amount must be greater than zero")
×
NEW
279
        }
×
280

281
        pubkeySet := len(req.FirstHopPubkey) != 0
3✔
282
        channelIDSet := req.FirstHopChanId != 0
3✔
283

3✔
284
        if pubkeySet == channelIDSet {
3✔
NEW
285
                return nil, status.Error(codes.InvalidArgument,
×
NEW
286
                        "must specify exactly one of first_hop_pubkey or "+
×
NEW
287
                                "first_hop_chan_id")
×
NEW
288
        }
×
289

290
        var chanID lnwire.ShortChannelID
3✔
291

3✔
292
        // Case 1: The caller provided the first hop channel ID directly.
3✔
293
        if channelIDSet {
6✔
294
                chanID = lnwire.NewShortChanIDFromInt(req.FirstHopChanId)
3✔
295
        } else {
6✔
296
                // Case 2: Convert the first hop pubkey into a format usable by
3✔
297
                // the forwarding subsystem.
3✔
298
                firstHop, err := btcec.ParsePubKey(req.FirstHopPubkey)
3✔
299
                if err != nil {
3✔
NEW
300
                        return nil, status.Errorf(codes.InvalidArgument,
×
NEW
301
                                "invalid first hop pubkey=%x: %v",
×
NEW
302
                                req.FirstHopPubkey, err)
×
NEW
303
                }
×
304

305
                // Find an eligible channel ID for the given first-hop pubkey.
306
                chanID, err = s.findEligibleChannelID(firstHop, amount)
3✔
307
                if err != nil {
3✔
NEW
308
                        return nil, status.Errorf(codes.Internal,
×
NEW
309
                                "unable to find eligible channel ID for pubkey=%x: %v",
×
NEW
310
                                firstHop.SerializeCompressed(), err)
×
NEW
311
                }
×
312
        }
313

314
        hash, err := lntypes.MakeHash(req.PaymentHash)
3✔
315
        if err != nil {
3✔
NEW
316
                return nil, status.Errorf(codes.InvalidArgument,
×
NEW
317
                        "invalid payment_hash=%x: %v", req.PaymentHash, err)
×
NEW
318
        }
×
319

320
        blindingPoint := lnwire.BlindingPointRecord{}
3✔
321
        if len(req.BlindingPoint) > 0 {
3✔
NEW
322
                pubkey, err := btcec.ParsePubKey(req.BlindingPoint)
×
NEW
323
                if err != nil {
×
NEW
324
                        return nil, status.Errorf(codes.InvalidArgument,
×
NEW
325
                                "invalid blinding point: %v", err)
×
NEW
326
                }
×
327

NEW
328
                blindingPoint = tlv.SomeRecordT(
×
NEW
329
                        tlv.NewPrimitiveRecord[lnwire.BlindingPointTlvType](
×
NEW
330
                                pubkey,
×
NEW
331
                        ),
×
NEW
332
                )
×
333
        }
334

335
        // Craft an HTLC packet to send to the htlcswitch. The metadata within
336
        // this packet will be used to route the payment through the network,
337
        // starting with the first-hop.
338
        htlcAdd := &lnwire.UpdateAddHTLC{
3✔
339
                Amount:      amount,
3✔
340
                Expiry:      req.Timelock,
3✔
341
                PaymentHash: hash,
3✔
342
                OnionBlob:   [lnwire.OnionPacketSize]byte(req.OnionBlob),
3✔
343
                // TODO: Add these from SendOnionRequest.
3✔
344
                BlindingPoint: blindingPoint,
3✔
345
                CustomRecords: req.CustomRecords,
3✔
346
                ExtraData:     lnwire.ExtraOpaqueData(req.ExtraData),
3✔
347
        }
3✔
348

3✔
349
        // Make sure that SendOnion and TrackOnion have not been called with
3✔
350
        // this AttemptID.
3✔
351
        if !s.tryAddAttemptID(req.AttemptId) {
6✔
352
                return &SendOnionResponse{
3✔
353
                        Success:      false,
3✔
354
                        ErrorMessage: htlcswitch.ErrDuplicateAdd.Error(),
3✔
355
                        ErrorCode:    ErrorCode_ERROR_CODE_DUPLICATE_HTLC,
3✔
356
                }, nil
3✔
357
        }
3✔
358

359
        log.Debugf("Dispatching HTLC attempt(id=%v, amt=%v) for payment=%v via "+
3✔
360
                "first_hop=%x over channel=%s", req.AttemptId, req.Amount,
3✔
361
                hash, req.FirstHopPubkey, chanID)
3✔
362

3✔
363
        // Send the HTLC to the first hop directly by way of the HTLCSwitch.
3✔
364
        err = s.cfg.HtlcDispatcher.SendHTLC(chanID, req.AttemptId, htlcAdd)
3✔
365
        if err != nil {
3✔
NEW
366
                message, code := TranslateErrorForRPC(err)
×
NEW
367
                return &SendOnionResponse{
×
NEW
368
                        Success:      false,
×
NEW
369
                        ErrorMessage: message,
×
NEW
370
                        ErrorCode:    code,
×
NEW
371
                }, nil
×
NEW
372
        }
×
373

374
        return &SendOnionResponse{Success: true}, nil
3✔
375
}
376

377
// ChannelInfoAccessor defines an interface for accessing channel information
378
// necessary for routing payments, specifically methods for fetching links by
379
// public key.
380
type ChannelInfoAccessor interface {
381
        GetLinksByPubkey(pubKey [33]byte) ([]htlcswitch.ChannelInfoProvider,
382
                error)
383
}
384

385
// findEligibleChannelID attempts to find an eligible channel based on the
386
// provided public key and the amount to be sent. It returns a channel ID that
387
// can carry the given payment amount.
388
func (s *Server) findEligibleChannelID(pubKey *btcec.PublicKey,
389
        amount lnwire.MilliSatoshi) (lnwire.ShortChannelID, error) {
3✔
390

3✔
391
        var pubKeyArray [33]byte
3✔
392
        copy(pubKeyArray[:], pubKey.SerializeCompressed())
3✔
393

3✔
394
        links, err := s.cfg.ChannelInfoAccessor.GetLinksByPubkey(pubKeyArray)
3✔
395
        if err != nil {
3✔
NEW
396
                return lnwire.ShortChannelID{},
×
NEW
397
                        fmt.Errorf("failed to retrieve channels: %w", err)
×
NEW
398
        }
×
399

400
        // NOTE(calvin): This is NOT duplicating the checks that the Switch
401
        // itself will perform as those are only performed in ForwardPackets().
402
        for _, link := range links {
6✔
403
                log.Debugf("Considering channel link scid=%v",
3✔
404
                        link.ShortChanID())
3✔
405

3✔
406
                // Ensure the link is eligible to forward payments.
3✔
407
                if !link.EligibleToForward() {
3✔
NEW
408
                        continue
×
409
                }
410

411
                // Check if the channel has sufficient bandwidth.
412
                if link.Bandwidth() >= amount {
6✔
413
                        // Check if adding an HTLC of this amount is possible.
3✔
414
                        if err := link.MayAddOutgoingHtlc(amount); err == nil {
6✔
415
                                return link.ShortChanID(), nil
3✔
416
                        }
3✔
417
                }
418
        }
419

NEW
420
        return lnwire.ShortChannelID{},
×
NEW
421
                fmt.Errorf("no suitable channel found for amount: %d msat",
×
NEW
422
                        amount)
×
423
}
424

425
// TrackOnion provides callers the means to query whether or not a payment
426
// dispatched via SendOnion succeeded or failed.
427
func (s *Server) TrackOnion(ctx context.Context,
428
        req *TrackOnionRequest) (*TrackOnionResponse, error) {
3✔
429

3✔
430
        hash, err := lntypes.MakeHash(req.PaymentHash)
3✔
431
        if err != nil {
3✔
NEW
432
                return nil, status.Errorf(codes.InvalidArgument,
×
NEW
433
                        "invalid payment_hash=%x: %v", req.PaymentHash, err)
×
NEW
434
        }
×
435

436
        if req.SharedSecrets != nil {
3✔
NEW
437
                return nil, status.Errorf(codes.Unimplemented,
×
NEW
438
                        "unable to process shared secrets")
×
NEW
439
        }
×
440

441
        // Mark this AttemptID as used so SendOnion can't reuse it later.
442
        // this AttemptId.
443
        s.addAttemptID(req.AttemptId)
3✔
444

3✔
445
        // NOTE(calvin): In order to decrypt errors server side we require
3✔
446
        // either the combination of session key and hop public keys from which
3✔
447
        // we can construct the shared secrets used to build the onion or,
3✔
448
        // alternatively, the caller can provide the list of shared secrets used
3✔
449
        // during onion construction directly if they wish to maintain route
3✔
450
        // privacy from the server.
3✔
451
        //
3✔
452
        // TODO: If we want to support "oblivious sends", then we'll need to
3✔
453
        // pass the shared secrets to the sphinx library.
3✔
454
        log.Debugf("Looking up status of onion attempt_id=%d for payment=%v",
3✔
455
                req.AttemptId, hash)
3✔
456

3✔
457
        // Attempt to build the error decryptor with the provided session key
3✔
458
        // and hop public keys.
3✔
459
        errorDecryptor, err := buildErrorDecryptor(
3✔
460
                req.SessionKey, req.HopPubkeys,
3✔
461
        )
3✔
462
        if err != nil {
3✔
NEW
463
                return nil, status.Errorf(codes.InvalidArgument,
×
NEW
464
                        "error building decryptor: %v", err)
×
NEW
465
        }
×
466

467
        if errorDecryptor == nil {
6✔
468
                log.Debug("Unable to build error decrypter with information " +
3✔
469
                        "provided. Will defer error handling to caller")
3✔
470
        }
3✔
471

472
        // Query the switch for the result of the payment attempt via onion.
473
        resultChan, err := s.cfg.HtlcDispatcher.GetAttemptResult(
3✔
474
                req.AttemptId, hash, errorDecryptor,
3✔
475
        )
3✔
476
        if err != nil {
6✔
477
                message, code := TranslateErrorForRPC(err)
3✔
478

3✔
479
                log.Errorf("GetAttemptResult failed for attempt_id=%d of "+
3✔
480
                        " payment=%v: %v", hash, message)
3✔
481

3✔
482
                // If this is a htlcswitch.ErrPaymentIDNotFound error, we need
3✔
483
                // to transmit that fact to the client so they can know with
3✔
484
                // certainty that no HTLC by that attempt ID is in-flight and
3✔
485
                // that it is safe to retry.
3✔
486
                return &TrackOnionResponse{
3✔
487
                        ErrorCode:    code,
3✔
488
                        ErrorMessage: message,
3✔
489
                }, nil
3✔
490
        }
3✔
491

492
        // The switch knows about this payment, we'll wait for a result to be
493
        // available.
494
        var (
3✔
495
                result *htlcswitch.PaymentResult
3✔
496
                ok     bool
3✔
497
        )
3✔
498

3✔
499
        select {
3✔
500
        case result, ok = <-resultChan:
3✔
501
                if !ok {
3✔
NEW
502
                        // NOTE(calvin): Transport via protobuf message if
×
NEW
503
                        // string matching is not preferred in client.
×
NEW
504
                        return nil, status.Errorf(codes.Internal,
×
NEW
505
                                "failed locate payment attempt: %v",
×
NEW
506
                                htlcswitch.ErrSwitchExiting)
×
NEW
507
                }
×
508

NEW
509
        case <-ctx.Done():
×
NEW
510
                return nil, nil
×
511
        }
512

513
        // The attempt result arrived so the HTLC is no longer in-flight.
514
        if result.Error != nil {
6✔
515
                message, code := TranslateErrorForRPC(result.Error)
3✔
516

3✔
517
                log.Errorf("Payment via onion failed for payment=%v: %v",
3✔
518
                        hash, message)
3✔
519

3✔
520
                return &TrackOnionResponse{
3✔
521
                        ErrorCode:    code,
3✔
522
                        ErrorMessage: message,
3✔
523
                }, nil
3✔
524
        }
3✔
525

526
        // In case we don't process the error, we'll return the encrypted
527
        // error blob for handling by the caller.
528
        if result.EncryptedError != nil {
6✔
529
                log.Errorf("Payment via onion failed for payment=%v", hash)
3✔
530

3✔
531
                return &TrackOnionResponse{
3✔
532
                        EncryptedError: result.EncryptedError,
3✔
533
                }, nil
3✔
534
        }
3✔
535

536
        return &TrackOnionResponse{
3✔
537
                Preimage: result.Preimage[:],
3✔
538
        }, nil
3✔
539
}
540

541
// buildErrorDecryptor constructs an error decrypter given a sphinx session
542
// key and hop public keys for a payment route.
543
func buildErrorDecryptor(sessionKeyBytes []byte,
544
        hopPubkeys [][]byte) (htlcswitch.ErrorDecrypter, error) {
3✔
545

3✔
546
        if len(sessionKeyBytes) == 0 || len(hopPubkeys) == 0 {
6✔
547
                return nil, nil
3✔
548
        }
3✔
549

550
        if err := validateSessionKey(sessionKeyBytes); err != nil {
3✔
NEW
551
                return nil, fmt.Errorf("invalid session key: %w", err)
×
NEW
552
        }
×
553

554
        // TODO(calvin): Validate that the session key makes a valid private
555
        // key? This is untrusted input received via RPC.
556
        sessionKey, _ := btcec.PrivKeyFromBytes(sessionKeyBytes)
3✔
557

3✔
558
        pubKeys := make([]*btcec.PublicKey, 0, len(hopPubkeys))
3✔
559
        for _, keyBytes := range hopPubkeys {
6✔
560
                pubKey, err := btcec.ParsePubKey(keyBytes)
3✔
561
                if err != nil {
3✔
NEW
562
                        return nil, fmt.Errorf("invalid public key: %w", err)
×
NEW
563
                }
×
564
                pubKeys = append(pubKeys, pubKey)
3✔
565
        }
566

567
        // Construct the sphinx circuit needed for error decryption using the
568
        // provided session key and hop public keys.
569
        circuit := reconstructCircuit(sessionKey, pubKeys)
3✔
570

3✔
571
        // Using the created circuit, initialize the error decrypter so we can
3✔
572
        // parse+decode any failures incurred by this payment within the
3✔
573
        // switch.
3✔
574
        return &htlcswitch.SphinxErrorDecrypter{
3✔
575
                OnionErrorDecrypter: sphinx.NewOnionErrorDecrypter(circuit),
3✔
576
        }, nil
3✔
577
}
578

579
// validateSessionKey validates the session key to ensure it has the correct
580
// length and is within the expected range [1, N-1] for the secp256k1 curve. If
581
// the session key is invalid, an error is returned.
582
func validateSessionKey(sessionKeyBytes []byte) error {
3✔
583
        const expectedKeyLength = 32
3✔
584

3✔
585
        // Check length of session key.
3✔
586
        if len(sessionKeyBytes) != expectedKeyLength {
3✔
NEW
587
                return fmt.Errorf("invalid session key length: got %d, "+
×
NEW
588
                        "expected %d", len(sessionKeyBytes), expectedKeyLength)
×
NEW
589
        }
×
590

591
        // Interpret the key as a big-endian unsigned integer.
592
        keyValue := new(big.Int).SetBytes(sessionKeyBytes)
3✔
593

3✔
594
        // Check if the key is in the valid range [1, N-1].
3✔
595
        if keyValue.Sign() <= 0 || keyValue.Cmp(btcec.S256().N) >= 0 {
3✔
NEW
596
                return fmt.Errorf("session key is out of range")
×
NEW
597
        }
×
598

599
        return nil
3✔
600
}
601

602
func reconstructCircuit(sessionKey *btcec.PrivateKey,
603
        pubKeys []*btcec.PublicKey) *sphinx.Circuit {
3✔
604

3✔
605
        return &sphinx.Circuit{
3✔
606
                SessionKey:  sessionKey,
3✔
607
                PaymentPath: pubKeys,
3✔
608
        }
3✔
609
}
3✔
610

611
// BuildOnion constructs a sphinx onion packet for the given route.
612
func (s *Server) BuildOnion(_ context.Context,
613
        req *BuildOnionRequest) (*BuildOnionResponse, error) {
3✔
614

3✔
615
        if req.Route == nil {
3✔
NEW
616
                return nil, status.Error(codes.InvalidArgument,
×
NEW
617
                        "route information is required")
×
NEW
618
        }
×
619
        if len(req.PaymentHash) == 0 {
3✔
NEW
620
                return nil, status.Error(codes.InvalidArgument,
×
NEW
621
                        "payment hash is required")
×
NEW
622
        }
×
623

624
        var sessionKey *btcec.PrivateKey
3✔
625
        var err error
3✔
626
        if len(req.SessionKey) == 0 {
6✔
627
                sessionKey, err = routing.GenerateNewSessionKey()
3✔
628
                if err != nil {
3✔
NEW
629
                        return nil, status.Errorf(codes.Internal,
×
NEW
630
                                "failed to generate session key: %v", err)
×
NEW
631
                }
×
NEW
632
        } else {
×
NEW
633
                if err := validateSessionKey(req.SessionKey); err != nil {
×
NEW
634
                        return nil, status.Errorf(codes.InvalidArgument,
×
NEW
635
                                "invalid session key: %v", err)
×
NEW
636
                }
×
637

NEW
638
                sessionKey, _ = btcec.PrivKeyFromBytes(req.SessionKey)
×
639
        }
640

641
        // Convert the route to a Sphinx path.
642
        route, err := s.cfg.RouteProcessor.UnmarshallRoute(req.Route)
3✔
643
        if err != nil {
3✔
NEW
644
                return nil, status.Errorf(codes.InvalidArgument,
×
NEW
645
                        "invalid route: %v", err)
×
NEW
646
        }
×
647

648
        // Generate the onion packet.
649
        onionBlob, circuit, err := channeldb.GenerateSphinxPacket(
3✔
650
                route, req.PaymentHash, sessionKey,
3✔
651
        )
3✔
652
        if err != nil {
3✔
NEW
653
                return nil, status.Errorf(codes.Internal,
×
NEW
654
                        "failed to create onion blob: %v", err)
×
NEW
655
        }
×
656

657
        // We'll provide the list of hop public keys for caller convenience.
658
        // They may wish to use them + the session key in a future call to
659
        // SendOnion so that the server can decrypt and handle errors.
660
        hopPubKeys := make([][]byte, len(circuit.PaymentPath))
3✔
661
        for i, pubKey := range circuit.PaymentPath {
6✔
662
                hopPubKeys[i] = pubKey.SerializeCompressed()
3✔
663
        }
3✔
664

665
        return &BuildOnionResponse{
3✔
666
                OnionBlob:  onionBlob,
3✔
667
                SessionKey: sessionKey.Serialize(),
3✔
668
                HopPubkeys: hopPubKeys,
3✔
669
        }, nil
3✔
670
}
671

672
// TranslateErrorForRPC converts an error from the underlying HTLC switch to
673
// a form that we can package for delivery to SendOnion rpc clients.
674
func TranslateErrorForRPC(err error) (string, ErrorCode) {
3✔
675
        var clearTextErr htlcswitch.ClearTextError
3✔
676

3✔
677
        switch {
3✔
678
        case errors.Is(err, htlcswitch.ErrPaymentIDNotFound):
3✔
679
                return err.Error(), ErrorCode_ERROR_CODE_PAYMENT_ID_NOT_FOUND
3✔
680

NEW
681
        case errors.Is(err, htlcswitch.ErrDuplicateAdd):
×
NEW
682
                return err.Error(), ErrorCode_ERROR_CODE_DUPLICATE_HTLC
×
683

NEW
684
        case errors.Is(err, htlcswitch.ErrUnreadableFailureMessage):
×
NEW
685
                return err.Error(), ErrorCode_ERROR_CODE_UNREADABLE_FAILURE_MESSAGE
×
686

NEW
687
        case errors.Is(err, htlcswitch.ErrSwitchExiting):
×
NEW
688
                return err.Error(), ErrorCode_ERROR_CODE_SWITCH_EXITING
×
689

690
        case errors.As(err, &clearTextErr):
3✔
691
                switch e := err.(type) {
3✔
692
                case *htlcswitch.ForwardingError:
3✔
693
                        encodedError, encodeErr := encodeForwardingError(e)
3✔
694
                        if encodeErr != nil {
3✔
NEW
695
                                return fmt.Sprintf("failed to encode wire "+
×
NEW
696
                                                "message: %v", encodeErr),
×
NEW
697
                                        ErrorCode_ERROR_CODE_INTERNAL
×
NEW
698
                        }
×
699

700
                        return encodedError,
3✔
701
                                ErrorCode_ERROR_CODE_FORWARDING_ERROR
3✔
NEW
702
                default:
×
NEW
703
                        var buf bytes.Buffer
×
NEW
704
                        encodeErr := lnwire.EncodeFailure(
×
NEW
705
                                &buf, clearTextErr.WireMessage(), 0,
×
NEW
706
                        )
×
NEW
707
                        if encodeErr != nil {
×
NEW
708
                                return fmt.Sprintf("failed to encode wire "+
×
NEW
709
                                                "message: %v", encodeErr),
×
NEW
710
                                        ErrorCode_ERROR_CODE_INTERNAL
×
NEW
711
                        }
×
712

NEW
713
                        return hex.EncodeToString(buf.Bytes()),
×
NEW
714
                                ErrorCode_ERROR_CODE_CLEAR_TEXT_ERROR
×
715
                }
716

NEW
717
        default:
×
NEW
718
                return err.Error(), ErrorCode_ERROR_CODE_INTERNAL
×
719
        }
720
}
721

722
func encodeForwardingError(e *htlcswitch.ForwardingError) (string, error) {
3✔
723
        var buf bytes.Buffer
3✔
724
        err := lnwire.EncodeFailure(&buf, e.WireMessage(), 0)
3✔
725
        if err != nil {
3✔
NEW
726
                return "", fmt.Errorf("failed to encode wire message: %w", err)
×
NEW
727
        }
×
728

729
        return fmt.Sprintf("%d@%s", e.FailureSourceIdx,
3✔
730
                hex.EncodeToString(buf.Bytes())), nil
3✔
731
}
732

733
// ParseForwardingError converts an error from the format in SendOnion rpc
734
// protos to a forwarding error type.
NEW
735
func ParseForwardingError(errStr string) (*htlcswitch.ForwardingError, error) {
×
NEW
736
        parts := strings.SplitN(errStr, "@", 2)
×
NEW
737
        if len(parts) != 2 {
×
NEW
738
                return nil, fmt.Errorf("invalid forwarding error format: %s",
×
NEW
739
                        errStr)
×
NEW
740
        }
×
741

NEW
742
        idx, err := strconv.Atoi(parts[0])
×
NEW
743
        if err != nil {
×
NEW
744
                return nil, fmt.Errorf("invalid forwarding error index: %s",
×
NEW
745
                        errStr)
×
NEW
746
        }
×
747

NEW
748
        wireMsgBytes, err := hex.DecodeString(parts[1])
×
NEW
749
        if err != nil {
×
NEW
750
                return nil, fmt.Errorf("invalid forwarding error wire message: %s",
×
NEW
751
                        errStr)
×
NEW
752
        }
×
753

NEW
754
        r := bytes.NewReader(wireMsgBytes)
×
NEW
755
        wireMsg, err := lnwire.DecodeFailure(r, 0)
×
NEW
756
        if err != nil {
×
NEW
757
                return nil, fmt.Errorf("failed to decode wire message: %v",
×
NEW
758
                        err)
×
NEW
759
        }
×
760

NEW
761
        return htlcswitch.NewForwardingError(wireMsg, idx), nil
×
762
}
STATUS · Troubleshooting · Open an Issue · Sales · Support · CAREERS · ENTERPRISE · START FREE · SCHEDULE DEMO
ANNOUNCEMENTS · TWITTER · TOS & SLA · Supported CI Services · What's a CI service? · Automated Testing

© 2025 Coveralls, Inc