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

lightningnetwork / lnd / 17777197509

16 Sep 2025 07:46PM UTC coverage: 57.202% (-9.5%) from 66.657%
17777197509

Pull #9489

github

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

329 of 564 new or added lines in 12 files covered. (58.33%)

28576 existing lines in 457 files now uncovered.

99724 of 174338 relevant lines covered (57.2%)

1.78 hits per line

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

58.22
/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

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

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

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

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

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

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

86
type Server struct {
87
        cfg *Config
88

89
        // Required by the grpc-gateway/v2 library for forward compatibility.
90
        // Must be after the atomically used variables to not break struct
91
        // alignment.
92
        UnimplementedSwitchServer
93
}
94

95
// New creates a new instance of the SwitchServer given a configuration struct
96
// that contains all external dependencies. If the target macaroon exists, and
97
// we're unable to create it, then an error will be returned. We also return
98
// the set of permissions that we require as a server. At the time of writing
99
// of this documentation, this is the same macaroon as the admin macaroon.
100
func New(cfg *Config) (*Server, lnrpc.MacaroonPerms, error) {
3✔
101
        // If the path of the router macaroon wasn't generated, then we'll
3✔
102
        // assume that it's found at the default network directory.
3✔
103
        if cfg.SwitchMacPath == "" {
6✔
104
                cfg.SwitchMacPath = filepath.Join(
3✔
105
                        cfg.NetworkDir, DefaultSwitchMacFilename,
3✔
106
                )
3✔
107
        }
3✔
108

109
        // Now that we know the full path of the switch macaroon, we can check
110
        // to see if we need to create it or not. If stateless_init is set
111
        // then we don't write the macaroons.
112
        macFilePath := cfg.SwitchMacPath
3✔
113
        if cfg.MacService != nil && !cfg.MacService.StatelessInit &&
3✔
114
                !lnrpc.FileExists(macFilePath) {
3✔
NEW
115

×
NEW
116
                log.Infof("Making macaroons for Switch RPC Server at: %v",
×
NEW
117
                        macFilePath)
×
NEW
118

×
NEW
119
                // At this point, we know that the switch macaroon doesn't yet,
×
NEW
120
                // exist, so we need to create it with the help of the main
×
NEW
121
                // macaroon service.
×
NEW
122
                switchMac, err := cfg.MacService.NewMacaroon(
×
NEW
123
                        context.Background(), macaroons.DefaultRootKeyID,
×
NEW
124
                        macaroonOps...,
×
NEW
125
                )
×
NEW
126
                if err != nil {
×
NEW
127
                        return nil, nil, err
×
NEW
128
                }
×
NEW
129
                switchMacBytes, err := switchMac.M().MarshalBinary()
×
NEW
130
                if err != nil {
×
NEW
131
                        return nil, nil, err
×
NEW
132
                }
×
NEW
133
                err = os.WriteFile(macFilePath, switchMacBytes, 0644)
×
NEW
134
                if err != nil {
×
NEW
135
                        _ = os.Remove(macFilePath)
×
NEW
136
                        return nil, nil, err
×
NEW
137
                }
×
138
        }
139

140
        switchServer := &Server{
3✔
141
                cfg: cfg,
3✔
142
                // quit: make(chan struct{}),
3✔
143
        }
3✔
144

3✔
145
        return switchServer, macPermissions, nil
3✔
146
}
147

148
// Start launches any helper goroutines required for the Server to function.
149
//
150
// NOTE: This is part of the lnrpc.SubServer interface.
151
func (s *Server) Start() error {
3✔
152
        return nil
3✔
153
}
3✔
154

155
// Stop signals any active goroutines for a graceful closure.
156
//
157
// NOTE: This is part of the lnrpc.SubServer interface.
158
func (s *Server) Stop() error {
3✔
159
        return nil
3✔
160
}
3✔
161

162
// Name returns a unique string representation of the sub-server. This can be
163
// used to identify the sub-server and also de-duplicate them.
164
//
165
// NOTE: This is part of the lnrpc.SubServer interface.
166
func (s *Server) Name() string {
3✔
167
        return subServerName
3✔
168
}
3✔
169

170
// RegisterWithRootServer will be called by the root gRPC server to direct a
171
// sub RPC server to register itself with the main gRPC root server. Until this
172
// is called, each sub-server won't be able to have
173
// requests routed towards it.
174
//
175
// NOTE: This is part of the lnrpc.GrpcHandler interface.
176
func (r *ServerShell) RegisterWithRootServer(grpcServer *grpc.Server) error {
3✔
177
        // We make sure that we register it with the main gRPC server to ensure
3✔
178
        // all our methods are routed properly.
3✔
179
        RegisterSwitchServer(grpcServer, r)
3✔
180

3✔
181
        log.Debugf("Switch RPC server successfully register with root " +
3✔
182
                "gRPC server")
3✔
183

3✔
184
        return nil
3✔
185
}
3✔
186

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

3✔
195
        log.Debugf("Switch REST server successfully registered with " +
3✔
196
                "root REST server")
3✔
197

3✔
198
        return nil
3✔
199
}
3✔
200

201
// CreateSubServer populates the subserver's dependencies using the passed
202
// SubServerConfigDispatcher. This method should fully initialize the
203
// sub-server instance, making it ready for action. It returns the macaroon
204
// permissions that the sub-server wishes to pass on to the root server for all
205
// methods routed towards it.
206
//
207
// NOTE: This is part of the lnrpc.GrpcHandler interface.
208
func (r *ServerShell) CreateSubServer(
209
        configRegistry lnrpc.SubServerConfigDispatcher) (
210
        lnrpc.SubServer, lnrpc.MacaroonPerms, error) {
3✔
211

3✔
212
        subServer, macPermissions, err := createNewSubServer(configRegistry)
3✔
213
        if err != nil {
3✔
NEW
214
                return nil, nil, err
×
NEW
215
        }
×
216

217
        r.SwitchServer = subServer
3✔
218

3✔
219
        return subServer, macPermissions, nil
3✔
220
}
221

222
// SendOnion handles the incoming request to send a payment using a
223
// preconstructed onion blob provided by the caller.
224
func (s *Server) SendOnion(_ context.Context,
225
        req *SendOnionRequest) (*SendOnionResponse, error) {
3✔
226

3✔
227
        if len(req.OnionBlob) == 0 {
3✔
NEW
228
                return nil, status.Error(codes.InvalidArgument,
×
NEW
229
                        "onion blob is required")
×
NEW
230
        }
×
231
        if len(req.OnionBlob) != lnwire.OnionPacketSize {
3✔
NEW
232
                return nil, status.Errorf(codes.InvalidArgument,
×
NEW
233
                        "onion blob size=%d does not match expected %d bytes",
×
NEW
234
                        len(req.OnionBlob), lnwire.OnionPacketSize)
×
NEW
235
        }
×
236

237
        if len(req.PaymentHash) == 0 {
3✔
NEW
238
                return nil, status.Error(codes.InvalidArgument,
×
NEW
239
                        "payment hash is required")
×
NEW
240
        }
×
241

242
        amount := lnwire.MilliSatoshi(req.Amount)
3✔
243
        if amount <= 0 {
3✔
NEW
244
                return nil, status.Error(codes.InvalidArgument,
×
NEW
245
                        "amount must be greater than zero")
×
NEW
246
        }
×
247

248
        pubkeySet := len(req.FirstHopPubkey) != 0
3✔
249
        channelIDSet := req.FirstHopChanId != 0
3✔
250

3✔
251
        if pubkeySet == channelIDSet {
3✔
NEW
252
                return nil, status.Error(codes.InvalidArgument,
×
NEW
253
                        "must specify exactly one of first_hop_pubkey or "+
×
NEW
254
                                "first_hop_chan_id")
×
NEW
255
        }
×
256

257
        var chanID lnwire.ShortChannelID
3✔
258

3✔
259
        // Case 1: The caller provided the first hop channel ID directly.
3✔
260
        if channelIDSet {
6✔
261
                chanID = lnwire.NewShortChanIDFromInt(req.FirstHopChanId)
3✔
262
        } else {
6✔
263
                // Case 2: Convert the first hop pubkey into a format usable by
3✔
264
                // the forwarding subsystem.
3✔
265
                firstHop, err := btcec.ParsePubKey(req.FirstHopPubkey)
3✔
266
                if err != nil {
3✔
NEW
267
                        return nil, status.Errorf(codes.InvalidArgument,
×
NEW
268
                                "invalid first hop pubkey=%x: %v",
×
NEW
269
                                req.FirstHopPubkey, err)
×
NEW
270
                }
×
271

272
                // Find an eligible channel ID for the given first-hop pubkey.
273
                chanID, err = s.findEligibleChannelID(firstHop, amount)
3✔
274
                if err != nil {
3✔
NEW
275
                        return nil, status.Errorf(codes.Internal,
×
NEW
276
                                "unable to find eligible channel for pubkey=%x"+
×
NEW
277
                                        ": %v", firstHop.SerializeCompressed(),
×
NEW
278
                                err)
×
NEW
279
                }
×
280
        }
281

282
        hash, err := lntypes.MakeHash(req.PaymentHash)
3✔
283
        if err != nil {
3✔
NEW
284
                return nil, status.Errorf(codes.InvalidArgument,
×
NEW
285
                        "invalid payment_hash=%x: %v", req.PaymentHash, err)
×
NEW
286
        }
×
287

288
        blindingPoint := lnwire.BlindingPointRecord{}
3✔
289
        if len(req.BlindingPoint) > 0 {
3✔
NEW
290
                pubkey, err := btcec.ParsePubKey(req.BlindingPoint)
×
NEW
291
                if err != nil {
×
NEW
292
                        return nil, status.Errorf(codes.InvalidArgument,
×
NEW
293
                                "invalid blinding point: %v", err)
×
NEW
294
                }
×
295

NEW
296
                blindingPoint = tlv.SomeRecordT(
×
NEW
297
                        tlv.NewPrimitiveRecord[lnwire.BlindingPointTlvType](
×
NEW
298
                                pubkey,
×
NEW
299
                        ),
×
NEW
300
                )
×
301
        }
302

303
        // Craft an HTLC packet to send to the htlcswitch. The metadata within
304
        // this packet will be used to route the payment through the network,
305
        // starting with the first-hop.
306
        htlcAdd := &lnwire.UpdateAddHTLC{
3✔
307
                Amount:        amount,
3✔
308
                Expiry:        req.Timelock,
3✔
309
                PaymentHash:   hash,
3✔
310
                OnionBlob:     [lnwire.OnionPacketSize]byte(req.OnionBlob),
3✔
311
                BlindingPoint: blindingPoint,
3✔
312
                CustomRecords: req.CustomRecords,
3✔
313
                ExtraData:     lnwire.ExtraOpaqueData(req.ExtraData),
3✔
314
        }
3✔
315

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

3✔
320
        // Send the HTLC to the first hop directly by way of the HTLCSwitch.
3✔
321
        err = s.cfg.HtlcDispatcher.SendHTLC(chanID, req.AttemptId, htlcAdd)
3✔
322
        if err != nil {
6✔
323
                message, code := translateErrorForRPC(err)
3✔
324
                return &SendOnionResponse{
3✔
325
                        Success:      false,
3✔
326
                        ErrorMessage: message,
3✔
327
                        ErrorCode:    code,
3✔
328
                }, nil
3✔
329
        }
3✔
330

331
        return &SendOnionResponse{Success: true}, nil
3✔
332
}
333

334
// findEligibleChannelID attempts to find an eligible channel based on the
335
// provided public key and the amount to be sent. It returns a channel ID that
336
// can carry the given payment amount.
337
func (s *Server) findEligibleChannelID(pubKey *btcec.PublicKey,
338
        amount lnwire.MilliSatoshi) (lnwire.ShortChannelID, error) {
3✔
339

3✔
340
        var pubKeyArray [33]byte
3✔
341
        copy(pubKeyArray[:], pubKey.SerializeCompressed())
3✔
342

3✔
343
        links, err := s.cfg.ChannelInfoAccessor.GetLinksByPubkey(pubKeyArray)
3✔
344
        if err != nil {
3✔
NEW
345
                return lnwire.ShortChannelID{},
×
NEW
346
                        fmt.Errorf("failed to retrieve channels: %w", err)
×
NEW
347
        }
×
348

349
        for _, link := range links {
6✔
350
                log.Debugf("Considering channel link scid=%v",
3✔
351
                        link.ShortChanID())
3✔
352

3✔
353
                // Ensure the link is eligible to forward payments.
3✔
354
                if !link.EligibleToForward() {
3✔
NEW
355
                        continue
×
356
                }
357

358
                // Check if the channel has sufficient bandwidth.
359
                if link.Bandwidth() >= amount {
6✔
360
                        // Check if adding an HTLC of this amount is possible.
3✔
361
                        if err := link.MayAddOutgoingHtlc(amount); err == nil {
6✔
362
                                return link.ShortChanID(), nil
3✔
363
                        }
3✔
364
                }
365
        }
366

NEW
367
        return lnwire.ShortChannelID{},
×
NEW
368
                fmt.Errorf("no suitable channel found for amount: %d msat",
×
NEW
369
                        amount)
×
370
}
371

372
// TrackOnion provides callers the means to query whether or not a payment
373
// dispatched via SendOnion succeeded or failed.
374
func (s *Server) TrackOnion(ctx context.Context,
375
        req *TrackOnionRequest) (*TrackOnionResponse, error) {
3✔
376

3✔
377
        hash, err := lntypes.MakeHash(req.PaymentHash)
3✔
378
        if err != nil {
3✔
NEW
379
                return nil, status.Errorf(codes.InvalidArgument,
×
NEW
380
                        "invalid payment_hash=%x: %v", req.PaymentHash, err)
×
NEW
381
        }
×
382

383
        // NOTE: In order to decrypt errors server side we require either the
384
        // combination of session key and hop public keys from which we can
385
        // construct the shared secrets used to build the onion or,
386
        // alternatively, the caller can provide the list of shared secrets used
387
        // during onion construction directly if they wish to maintain route
388
        // privacy from the server.
389
        //
390
        // TODO(calvin): If we want to support "oblivious sends", then we'll
391
        // need to pass the shared secrets to the sphinx library.
392
        if req.SharedSecrets != nil {
3✔
NEW
393
                return nil, status.Errorf(codes.Unimplemented,
×
NEW
394
                        "unable to process shared secrets")
×
NEW
395
        }
×
396

397
        log.Debugf("Looking up status of onion attempt_id=%d for payment=%v",
3✔
398
                req.AttemptId, hash)
3✔
399

3✔
400
        // Attempt to build the error decryptor with the provided session key
3✔
401
        // and hop public keys.
3✔
402
        errorDecryptor, err := buildErrorDecryptor(
3✔
403
                req.SessionKey, req.HopPubkeys,
3✔
404
        )
3✔
405
        if err != nil {
3✔
NEW
406
                return nil, status.Errorf(codes.InvalidArgument,
×
NEW
407
                        "error building decryptor: %v", err)
×
NEW
408
        }
×
409

410
        if errorDecryptor == nil {
6✔
411
                log.Debug("Unable to build error decrypter with information " +
3✔
412
                        "provided. Will defer error handling to caller")
3✔
413
        }
3✔
414

415
        // Query the switch for the result of the payment attempt via onion.
416
        resultChan, err := s.cfg.HtlcDispatcher.GetAttemptResult(
3✔
417
                req.AttemptId, hash, errorDecryptor,
3✔
418
        )
3✔
419
        if err != nil {
3✔
NEW
420
                message, code := translateErrorForRPC(err)
×
NEW
421

×
NEW
422
                log.Errorf("GetAttemptResult failed for attempt_id=%d of "+
×
NEW
423
                        " payment=%x: %v", req.AttemptId, hash, message)
×
NEW
424

×
NEW
425
                // If this is a htlcswitch.ErrPaymentIDNotFound error, we need
×
NEW
426
                // to transmit that fact to the client so they can know with
×
NEW
427
                // certainty that no HTLC by that attempt ID is in-flight and
×
NEW
428
                // that it is safe to retry.
×
NEW
429
                return &TrackOnionResponse{
×
NEW
430
                        ErrorCode:    code,
×
NEW
431
                        ErrorMessage: message,
×
NEW
432
                }, nil
×
NEW
433
        }
×
434

435
        // The switch knows about this payment, we'll wait for a result to be
436
        // available.
437
        var (
3✔
438
                result *htlcswitch.PaymentResult
3✔
439
                ok     bool
3✔
440
        )
3✔
441

3✔
442
        select {
3✔
443
        case result, ok = <-resultChan:
3✔
444
                if !ok {
3✔
NEW
445
                        // This channel is closed when the Switch shuts down.
×
NEW
446
                        return &TrackOnionResponse{
×
NEW
447
                                ErrorCode: ErrorCode_ERROR_CODE_SWITCH_EXITING,
×
NEW
448
                                ErrorMessage: htlcswitch.ErrSwitchExiting.
×
NEW
449
                                        Error(),
×
NEW
450
                        }, nil
×
NEW
451
                }
×
452

NEW
453
        case <-ctx.Done():
×
NEW
454
                return nil, status.Error(codes.Canceled,
×
NEW
455
                        "client context canceled")
×
456
        }
457

458
        // The attempt result arrived so the HTLC is no longer in-flight.
459
        if result.Error != nil {
6✔
460
                message, code := translateErrorForRPC(result.Error)
3✔
461

3✔
462
                log.Errorf("Payment via onion failed for payment=%v: %v",
3✔
463
                        hash, message)
3✔
464

3✔
465
                return &TrackOnionResponse{
3✔
466
                        ErrorCode:    code,
3✔
467
                        ErrorMessage: message,
3✔
468
                }, nil
3✔
469
        }
3✔
470

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

3✔
476
                return &TrackOnionResponse{
3✔
477
                        EncryptedError: result.EncryptedError,
3✔
478
                }, nil
3✔
479
        }
3✔
480

481
        return &TrackOnionResponse{
3✔
482
                Preimage: result.Preimage[:],
3✔
483
        }, nil
3✔
484
}
485

486
// buildErrorDecryptor constructs an error decrypter given a sphinx session
487
// key and hop public keys for a payment route.
488
func buildErrorDecryptor(sessionKeyBytes []byte,
489
        hopPubkeys [][]byte) (htlcswitch.ErrorDecrypter, error) {
3✔
490

3✔
491
        if len(sessionKeyBytes) == 0 || len(hopPubkeys) == 0 {
6✔
492
                return nil, nil
3✔
493
        }
3✔
494

495
        if err := validateSessionKey(sessionKeyBytes); err != nil {
3✔
NEW
496
                return nil, fmt.Errorf("invalid session key: %w", err)
×
NEW
497
        }
×
498

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

3✔
503
        pubKeys := make([]*btcec.PublicKey, 0, len(hopPubkeys))
3✔
504
        for _, keyBytes := range hopPubkeys {
6✔
505
                pubKey, err := btcec.ParsePubKey(keyBytes)
3✔
506
                if err != nil {
3✔
NEW
507
                        return nil, fmt.Errorf("invalid public key: %w", err)
×
NEW
508
                }
×
509
                pubKeys = append(pubKeys, pubKey)
3✔
510
        }
511

512
        // Construct the sphinx circuit needed for error decryption using the
513
        // provided session key and hop public keys.
514
        circuit := reconstructCircuit(sessionKey, pubKeys)
3✔
515

3✔
516
        // Using the created circuit, initialize the error decrypter so we can
3✔
517
        // parse+decode any failures incurred by this payment within the
3✔
518
        // switch.
3✔
519
        return &htlcswitch.SphinxErrorDecrypter{
3✔
520
                OnionErrorDecrypter: sphinx.NewOnionErrorDecrypter(circuit),
3✔
521
        }, nil
3✔
522
}
523

524
// validateSessionKey validates the session key to ensure it has the correct
525
// length and is within the expected range [1, N-1] for the secp256k1 curve. If
526
// the session key is invalid, an error is returned.
527
func validateSessionKey(sessionKeyBytes []byte) error {
3✔
528
        const expectedKeyLength = 32
3✔
529

3✔
530
        // Check length of session key.
3✔
531
        if len(sessionKeyBytes) != expectedKeyLength {
3✔
NEW
532
                return fmt.Errorf("invalid session key length: got %d, "+
×
NEW
533
                        "expected %d", len(sessionKeyBytes), expectedKeyLength)
×
NEW
534
        }
×
535

536
        // Interpret the key as a big-endian unsigned integer.
537
        keyValue := new(big.Int).SetBytes(sessionKeyBytes)
3✔
538

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

544
        return nil
3✔
545
}
546

547
// reconstructCircuit is a simple helper to assemble a sphinx Circuit from its
548
// consituent parts, namely ephemeral session key and hop public keys.
549
func reconstructCircuit(sessionKey *btcec.PrivateKey,
550
        pubKeys []*btcec.PublicKey) *sphinx.Circuit {
3✔
551

3✔
552
        return &sphinx.Circuit{
3✔
553
                SessionKey:  sessionKey,
3✔
554
                PaymentPath: pubKeys,
3✔
555
        }
3✔
556
}
3✔
557

558
// BuildOnion constructs a sphinx onion packet for the given route.
559
func (s *Server) BuildOnion(_ context.Context,
560
        req *BuildOnionRequest) (*BuildOnionResponse, error) {
3✔
561

3✔
562
        if req.Route == nil {
3✔
NEW
563
                return nil, status.Error(codes.InvalidArgument,
×
NEW
564
                        "route information is required")
×
NEW
565
        }
×
566
        if len(req.PaymentHash) == 0 {
3✔
NEW
567
                return nil, status.Error(codes.InvalidArgument,
×
NEW
568
                        "payment hash is required")
×
NEW
569
        }
×
570

571
        var sessionKey *btcec.PrivateKey
3✔
572
        var err error
3✔
573
        if len(req.SessionKey) == 0 {
6✔
574
                sessionKey, err = routing.GenerateNewSessionKey()
3✔
575
                if err != nil {
3✔
NEW
576
                        return nil, status.Errorf(codes.Internal,
×
NEW
577
                                "failed to generate session key: %v", err)
×
NEW
578
                }
×
NEW
579
        } else {
×
NEW
580
                if err := validateSessionKey(req.SessionKey); err != nil {
×
NEW
581
                        return nil, status.Errorf(codes.InvalidArgument,
×
NEW
582
                                "invalid session key: %v", err)
×
NEW
583
                }
×
584

NEW
585
                sessionKey, _ = btcec.PrivKeyFromBytes(req.SessionKey)
×
586
        }
587

588
        // Convert the route to a Sphinx path.
589
        route, err := s.cfg.RouteProcessor.UnmarshallRoute(req.Route)
3✔
590
        if err != nil {
3✔
NEW
591
                return nil, status.Errorf(codes.InvalidArgument,
×
NEW
592
                        "invalid route: %v", err)
×
NEW
593
        }
×
594

595
        // Generate the onion packet.
596
        onionBlob, circuit, err := paymentsdb.GenerateSphinxPacket(
3✔
597
                route, req.PaymentHash, sessionKey,
3✔
598
        )
3✔
599
        if err != nil {
3✔
NEW
600
                return nil, status.Errorf(codes.Internal,
×
NEW
601
                        "failed to create onion blob: %v", err)
×
NEW
602
        }
×
603

604
        // We'll provide the list of hop public keys for caller convenience.
605
        // They may wish to use them + the session key in a future call to
606
        // SendOnion so that the server can decrypt and handle errors.
607
        hopPubKeys := make([][]byte, len(circuit.PaymentPath))
3✔
608
        for i, pubKey := range circuit.PaymentPath {
6✔
609
                hopPubKeys[i] = pubKey.SerializeCompressed()
3✔
610
        }
3✔
611

612
        return &BuildOnionResponse{
3✔
613
                OnionBlob:  onionBlob,
3✔
614
                SessionKey: sessionKey.Serialize(),
3✔
615
                HopPubkeys: hopPubKeys,
3✔
616
        }, nil
3✔
617
}
618

619
// translateErrorForRPC converts an error from the underlying HTLC switch to
620
// a form that we can package for delivery to SendOnion rpc clients.
621
func translateErrorForRPC(err error) (string, ErrorCode) {
3✔
622
        var (
3✔
623
                clearTextErr htlcswitch.ClearTextError
3✔
624
                fwdErr       *htlcswitch.ForwardingError
3✔
625
        )
3✔
626

3✔
627
        switch {
3✔
NEW
628
        case errors.Is(err, htlcswitch.ErrPaymentIDNotFound):
×
NEW
629
                return err.Error(), ErrorCode_ERROR_CODE_PAYMENT_ID_NOT_FOUND
×
630

631
        case errors.Is(err, htlcswitch.ErrDuplicateAdd):
3✔
632
                return err.Error(), ErrorCode_ERROR_CODE_DUPLICATE_HTLC
3✔
633

NEW
634
        case errors.Is(err, htlcswitch.ErrUnreadableFailureMessage):
×
NEW
635
                return err.Error(),
×
NEW
636
                        ErrorCode_ERROR_CODE_UNREADABLE_FAILURE_MESSAGE
×
637

NEW
638
        case errors.Is(err, htlcswitch.ErrSwitchExiting):
×
NEW
639
                return err.Error(), ErrorCode_ERROR_CODE_SWITCH_EXITING
×
640

641
        case errors.As(err, &clearTextErr):
3✔
642
                // If this is a forwarding error, we'll handle it specially.
3✔
643
                if errors.As(err, &fwdErr) {
6✔
644
                        encodedError, encodeErr := encodeForwardingError(fwdErr)
3✔
645
                        if encodeErr != nil {
3✔
NEW
646
                                return fmt.Sprintf("failed to encode wire "+
×
NEW
647
                                                "message: %v", encodeErr),
×
NEW
648
                                        ErrorCode_ERROR_CODE_INTERNAL
×
NEW
649
                        }
×
650

651
                        return encodedError,
3✔
652
                                ErrorCode_ERROR_CODE_FORWARDING_ERROR
3✔
653
                }
654

655
                // Otherwise, we'll just encode the clear text error.
NEW
656
                var buf bytes.Buffer
×
NEW
657
                encodeErr := lnwire.EncodeFailure(
×
NEW
658
                        &buf, clearTextErr.WireMessage(), 0,
×
NEW
659
                )
×
NEW
660
                if encodeErr != nil {
×
NEW
661
                        return fmt.Sprintf("failed to encode wire "+
×
NEW
662
                                        "message: %v", encodeErr),
×
NEW
663
                                ErrorCode_ERROR_CODE_INTERNAL
×
NEW
664
                }
×
665

NEW
666
                return hex.EncodeToString(buf.Bytes()),
×
NEW
667
                        ErrorCode_ERROR_CODE_CLEAR_TEXT_ERROR
×
668

NEW
669
        default:
×
NEW
670
                return err.Error(), ErrorCode_ERROR_CODE_INTERNAL
×
671
        }
672
}
673

674
func encodeForwardingError(e *htlcswitch.ForwardingError) (string, error) {
3✔
675
        var buf bytes.Buffer
3✔
676
        err := lnwire.EncodeFailure(&buf, e.WireMessage(), 0)
3✔
677
        if err != nil {
3✔
NEW
678
                return "", fmt.Errorf("failed to encode wire message: %w", err)
×
NEW
679
        }
×
680

681
        return fmt.Sprintf("%d@%s", e.FailureSourceIdx,
3✔
682
                hex.EncodeToString(buf.Bytes())), nil
3✔
683
}
684

685
// ParseForwardingError converts an error from the format in SendOnion rpc
686
// protos to a forwarding error type.
NEW
687
func ParseForwardingError(errStr string) (*htlcswitch.ForwardingError, error) {
×
NEW
688
        parts := strings.SplitN(errStr, "@", 2)
×
NEW
689
        if len(parts) != 2 {
×
NEW
690
                return nil, fmt.Errorf("invalid forwarding error format: %s",
×
NEW
691
                        errStr)
×
NEW
692
        }
×
693

NEW
694
        idx, err := strconv.Atoi(parts[0])
×
NEW
695
        if err != nil {
×
NEW
696
                return nil, fmt.Errorf("invalid forwarding error index: %s",
×
NEW
697
                        errStr)
×
NEW
698
        }
×
699

NEW
700
        wireMsgBytes, err := hex.DecodeString(parts[1])
×
NEW
701
        if err != nil {
×
NEW
702
                return nil, fmt.Errorf("invalid forwarding error wire "+
×
NEW
703
                        "message: %s", errStr)
×
NEW
704
        }
×
705

NEW
706
        r := bytes.NewReader(wireMsgBytes)
×
NEW
707
        wireMsg, err := lnwire.DecodeFailure(r, 0)
×
NEW
708
        if err != nil {
×
NEW
709
                return nil, fmt.Errorf("failed to decode wire message: %w",
×
NEW
710
                        err)
×
NEW
711
        }
×
712

NEW
713
        return htlcswitch.NewForwardingError(wireMsg, idx), nil
×
714
}
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