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

lightningnetwork / lnd / 13211764208

08 Feb 2025 03:08AM UTC coverage: 49.288% (-9.5%) from 58.815%
13211764208

Pull #9489

github

calvinrzachman
itest: verify switchrpc server enforces send then track

We prevent the rpc server from allowing onion dispatches for
attempt IDs which have already been tracked by rpc clients.

This helps protect the client from leaking a duplicate onion
attempt. NOTE: This is not the only method for solving this
issue! The issue could be addressed via careful client side
programming which accounts for the uncertainty and async
nature of dispatching onions to a remote process via RPC.
This would require some lnd ChannelRouter changes for how
we intend to use these RPCs though.
Pull Request #9489: multi: add BuildOnion, SendOnion, and TrackOnion RPCs

474 of 990 new or added lines in 11 files covered. (47.88%)

27321 existing lines in 435 files now uncovered.

101192 of 205306 relevant lines covered (49.29%)

1.54 hits per line

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

61.16
/walletunlocker/service.go
1
package walletunlocker
2

3
import (
4
        "context"
5
        "crypto/rand"
6
        "errors"
7
        "fmt"
8
        "os"
9
        "time"
10

11
        "github.com/btcsuite/btcd/btcutil/hdkeychain"
12
        "github.com/btcsuite/btcd/chaincfg"
13
        "github.com/btcsuite/btcwallet/waddrmgr"
14
        "github.com/btcsuite/btcwallet/wallet"
15
        "github.com/lightningnetwork/lnd/aezeed"
16
        "github.com/lightningnetwork/lnd/chanbackup"
17
        "github.com/lightningnetwork/lnd/keychain"
18
        "github.com/lightningnetwork/lnd/kvdb"
19
        "github.com/lightningnetwork/lnd/lnrpc"
20
        "github.com/lightningnetwork/lnd/lnwallet"
21
        "github.com/lightningnetwork/lnd/lnwallet/btcwallet"
22
        "github.com/lightningnetwork/lnd/macaroons"
23
)
24

25
var (
26
        // ErrUnlockTimeout signals that we did not get the expected unlock
27
        // message before the timeout occurred.
28
        ErrUnlockTimeout = errors.New("got no unlock message before timeout")
29
)
30

31
// WalletUnlockParams holds the variables used to parameterize the unlocking of
32
// lnd's wallet after it has already been created.
33
type WalletUnlockParams struct {
34
        // Password is the public and private wallet passphrase.
35
        Password []byte
36

37
        // Birthday specifies the approximate time that this wallet was created.
38
        // This is used to bound any rescans on startup.
39
        Birthday time.Time
40

41
        // RecoveryWindow specifies the address lookahead when entering recovery
42
        // mode. A recovery will be attempted if this value is non-zero.
43
        RecoveryWindow uint32
44

45
        // Wallet is the loaded and unlocked Wallet. This is returned
46
        // from the unlocker service to avoid it being unlocked twice (once in
47
        // the unlocker service to check if the password is correct and again
48
        // later when lnd actually uses it). Because unlocking involves scrypt
49
        // which is resource intensive, we want to avoid doing it twice.
50
        Wallet *wallet.Wallet
51

52
        // ChansToRestore a set of static channel backups that should be
53
        // restored before the main server instance starts up.
54
        ChansToRestore ChannelsToRecover
55

56
        // UnloadWallet is a function for unloading the wallet, which should
57
        // be called on shutdown.
58
        UnloadWallet func() error
59

60
        // StatelessInit signals that the user requested the daemon to be
61
        // initialized stateless, which means no unencrypted macaroons should be
62
        // written to disk.
63
        StatelessInit bool
64

65
        // MacResponseChan is the channel for sending back the admin macaroon to
66
        // the WalletUnlocker service.
67
        MacResponseChan chan []byte
68

69
        // MacRootKey is the 32 byte macaroon root key specified by the user
70
        // during wallet initialization.
71
        MacRootKey []byte
72
}
73

74
// ChannelsToRecover wraps any set of packed (serialized+encrypted) channel
75
// back ups together. These can be passed in when unlocking the wallet, or
76
// creating a new wallet for the first time with an existing seed.
77
type ChannelsToRecover struct {
78
        // PackedMultiChanBackup is an encrypted and serialized multi-channel
79
        // backup.
80
        PackedMultiChanBackup chanbackup.PackedMulti
81

82
        // PackedSingleChanBackups is a series of encrypted and serialized
83
        // single-channel backup for one or more channels.
84
        PackedSingleChanBackups chanbackup.PackedSingles
85
}
86

87
// WalletInitMsg is a message sent by the UnlockerService when a user wishes to
88
// set up the internal wallet for the first time. The user MUST provide a
89
// passphrase, but is also able to provide their own source of entropy. If
90
// provided, then this source of entropy will be used to generate the wallet's
91
// HD seed. Otherwise, the wallet will generate one itself.
92
type WalletInitMsg struct {
93
        // Passphrase is the passphrase that will be used to encrypt the wallet
94
        // itself. This MUST be at least 8 characters.
95
        Passphrase []byte
96

97
        // WalletSeed is the deciphered cipher seed that the wallet should use
98
        // to initialize itself. The seed might be nil if the wallet should be
99
        // created from an extended master root key instead.
100
        WalletSeed *aezeed.CipherSeed
101

102
        // WalletExtendedKey is the wallet's extended master root key that
103
        // should be used instead of the seed, if non-nil. The extended key is
104
        // mutually exclusive to the wallet seed, but one of both is always set.
105
        WalletExtendedKey *hdkeychain.ExtendedKey
106

107
        // ExtendedKeyBirthday is the birthday of a wallet that's being restored
108
        // through an extended key instead of an aezeed.
109
        ExtendedKeyBirthday time.Time
110

111
        // WatchOnlyAccounts is a map of scoped account extended public keys
112
        // that should be imported to create a watch-only wallet.
113
        WatchOnlyAccounts map[waddrmgr.ScopedIndex]*hdkeychain.ExtendedKey
114

115
        // WatchOnlyBirthday is the birthday of the master root key the above
116
        // watch-only account xpubs were derived from.
117
        WatchOnlyBirthday time.Time
118

119
        // WatchOnlyMasterFingerprint is the fingerprint of the master root key
120
        // the above watch-only account xpubs were derived from.
121
        WatchOnlyMasterFingerprint uint32
122

123
        // RecoveryWindow is the address look-ahead used when restoring a seed
124
        // with existing funds. A recovery window zero indicates that no
125
        // recovery should be attempted, such as after the wallet's initial
126
        // creation.
127
        RecoveryWindow uint32
128

129
        // ChanBackups a set of static channel backups that should be received
130
        // after the wallet has been initialized.
131
        ChanBackups ChannelsToRecover
132

133
        // StatelessInit signals that the user requested the daemon to be
134
        // initialized stateless, which means no unencrypted macaroons should be
135
        // written to disk.
136
        StatelessInit bool
137

138
        // MacRootKey is the 32 byte macaroon root key specified by the user
139
        // during wallet initialization.
140
        MacRootKey []byte
141
}
142

143
// WalletUnlockMsg is a message sent by the UnlockerService when a user wishes
144
// to unlock the internal wallet after initial setup. The user can optionally
145
// specify a recovery window, which will resume an interrupted rescan for used
146
// addresses.
147
type WalletUnlockMsg struct {
148
        // Passphrase is the passphrase that will be used to encrypt the wallet
149
        // itself. This MUST be at least 8 characters.
150
        Passphrase []byte
151

152
        // RecoveryWindow is the address look-ahead used when restoring a seed
153
        // with existing funds. A recovery window zero indicates that no
154
        // recovery should be attempted, such as after the wallet's initial
155
        // creation, but before any addresses have been created.
156
        RecoveryWindow uint32
157

158
        // Wallet is the loaded and unlocked Wallet. This is returned through
159
        // the channel to avoid it being unlocked twice (once to check if the
160
        // password is correct, here in the WalletUnlocker and again later when
161
        // lnd actually uses it). Because unlocking involves scrypt which is
162
        // resource intensive, we want to avoid doing it twice.
163
        Wallet *wallet.Wallet
164

165
        // ChanBackups a set of static channel backups that should be received
166
        // after the wallet has been unlocked.
167
        ChanBackups ChannelsToRecover
168

169
        // UnloadWallet is a function for unloading the wallet, which should
170
        // be called on shutdown.
171
        UnloadWallet func() error
172

173
        // StatelessInit signals that the user requested the daemon to be
174
        // initialized stateless, which means no unencrypted macaroons should be
175
        // written to disk.
176
        StatelessInit bool
177
}
178

179
// UnlockerService implements the WalletUnlocker service used to provide lnd
180
// with a password for wallet encryption at startup. Additionally, during
181
// initial setup, users can provide their own source of entropy which will be
182
// used to generate the seed that's ultimately used within the wallet.
183
type UnlockerService struct {
184
        // Required by the grpc-gateway/v2 library for forward compatibility.
185
        lnrpc.UnimplementedWalletUnlockerServer
186

187
        // InitMsgs is a channel that carries all wallet init messages.
188
        InitMsgs chan *WalletInitMsg
189

190
        // UnlockMsgs is a channel where unlock parameters provided by the rpc
191
        // client to be used to unlock and decrypt an existing wallet will be
192
        // sent.
193
        UnlockMsgs chan *WalletUnlockMsg
194

195
        // MacResponseChan is the channel for sending back the admin macaroon to
196
        // the WalletUnlocker service.
197
        MacResponseChan chan []byte
198

199
        netParams *chaincfg.Params
200

201
        // macaroonFiles is the path to the three generated macaroons with
202
        // different access permissions. These might not exist in a stateless
203
        // initialization of lnd.
204
        macaroonFiles []string
205

206
        // resetWalletTransactions indicates that the wallet state should be
207
        // reset on unlock to force a full chain rescan.
208
        resetWalletTransactions bool
209

210
        // LoaderOpts holds the functional options for the wallet loader.
211
        loaderOpts []btcwallet.LoaderOption
212

213
        // macaroonDB is an instance of a database backend that stores all
214
        // macaroon root keys. This will be nil on initialization and must be
215
        // set using the SetMacaroonDB method as soon as it's available.
216
        macaroonDB kvdb.Backend
217
}
218

219
// New creates and returns a new UnlockerService.
220
func New(params *chaincfg.Params, macaroonFiles []string,
221
        resetWalletTransactions bool,
222
        loaderOpts []btcwallet.LoaderOption) *UnlockerService {
3✔
223

3✔
224
        return &UnlockerService{
3✔
225
                InitMsgs:   make(chan *WalletInitMsg, 1),
3✔
226
                UnlockMsgs: make(chan *WalletUnlockMsg, 1),
3✔
227

3✔
228
                // Make sure we buffer the channel is buffered so the main lnd
3✔
229
                // goroutine isn't blocking on writing to it.
3✔
230
                MacResponseChan:         make(chan []byte, 1),
3✔
231
                netParams:               params,
3✔
232
                macaroonFiles:           macaroonFiles,
3✔
233
                resetWalletTransactions: resetWalletTransactions,
3✔
234
                loaderOpts:              loaderOpts,
3✔
235
        }
3✔
236
}
3✔
237

238
// SetLoaderOpts can be used to inject wallet loader options after the unlocker
239
// service has been hooked to the main RPC server.
240
func (u *UnlockerService) SetLoaderOpts(loaderOpts []btcwallet.LoaderOption) {
3✔
241
        u.loaderOpts = loaderOpts
3✔
242
}
3✔
243

244
// SetMacaroonDB can be used to inject the macaroon database after the unlocker
245
// service has been hooked to the main RPC server.
246
func (u *UnlockerService) SetMacaroonDB(macaroonDB kvdb.Backend) {
3✔
247
        u.macaroonDB = macaroonDB
3✔
248
}
3✔
249

250
func (u *UnlockerService) newLoader(recoveryWindow uint32) (*wallet.Loader,
251
        error) {
3✔
252

3✔
253
        return btcwallet.NewWalletLoader(
3✔
254
                u.netParams, recoveryWindow, u.loaderOpts...,
3✔
255
        )
3✔
256
}
3✔
257

258
// WalletExists returns whether a wallet exists on the file path the
259
// UnlockerService is using.
260
func (u *UnlockerService) WalletExists() (bool, error) {
3✔
261
        loader, err := u.newLoader(0)
3✔
262
        if err != nil {
3✔
263
                return false, err
×
264
        }
×
265
        return loader.WalletExists()
3✔
266
}
267

268
// GenSeed is the first method that should be used to instantiate a new lnd
269
// instance. This method allows a caller to generate a new aezeed cipher seed
270
// given an optional passphrase. If provided, the passphrase will be necessary
271
// to decrypt the cipherseed to expose the internal wallet seed.
272
//
273
// Once the cipherseed is obtained and verified by the user, the InitWallet
274
// method should be used to commit the newly generated seed, and create the
275
// wallet.
276
func (u *UnlockerService) GenSeed(_ context.Context,
277
        in *lnrpc.GenSeedRequest) (*lnrpc.GenSeedResponse, error) {
3✔
278

3✔
279
        // Before we start, we'll ensure that the wallet hasn't already created
3✔
280
        // so we don't show a *new* seed to the user if one already exists.
3✔
281
        loader, err := u.newLoader(0)
3✔
282
        if err != nil {
3✔
283
                return nil, err
×
284
        }
×
285

286
        walletExists, err := loader.WalletExists()
3✔
287
        if err != nil {
3✔
288
                return nil, err
×
289
        }
×
290
        if walletExists {
3✔
UNCOV
291
                return nil, fmt.Errorf("wallet already exists")
×
UNCOV
292
        }
×
293

294
        var entropy [aezeed.EntropySize]byte
3✔
295

3✔
296
        switch {
3✔
297
        // If the user provided any entropy, then we'll make sure it's sized
298
        // properly.
UNCOV
299
        case len(in.SeedEntropy) != 0 && len(in.SeedEntropy) != aezeed.EntropySize:
×
UNCOV
300
                return nil, fmt.Errorf("incorrect entropy length: expected "+
×
UNCOV
301
                        "16 bytes, instead got %v bytes", len(in.SeedEntropy))
×
302

303
        // If the user provided the correct number of bytes, then we'll copy it
304
        // over into our buffer for usage.
UNCOV
305
        case len(in.SeedEntropy) == aezeed.EntropySize:
×
UNCOV
306
                copy(entropy[:], in.SeedEntropy[:])
×
307

308
        // Otherwise, we'll generate a fresh new set of bytes to use as entropy
309
        // to generate the seed.
310
        default:
3✔
311
                if _, err := rand.Read(entropy[:]); err != nil {
3✔
312
                        return nil, err
×
313
                }
×
314
        }
315

316
        // Now that we have our set of entropy, we'll create a new cipher seed
317
        // instance.
318
        //
319
        cipherSeed, err := aezeed.New(
3✔
320
                keychain.CurrentKeyDerivationVersion, &entropy, time.Now(),
3✔
321
        )
3✔
322
        if err != nil {
3✔
323
                return nil, err
×
324
        }
×
325

326
        // With our raw cipher seed obtained, we'll convert it into an encoded
327
        // mnemonic using the user specified pass phrase.
328
        mnemonic, err := cipherSeed.ToMnemonic(in.AezeedPassphrase)
3✔
329
        if err != nil {
3✔
330
                return nil, err
×
331
        }
×
332

333
        // Additionally, we'll also obtain the raw enciphered cipher seed as
334
        // well to return to the user.
335
        encipheredSeed, err := cipherSeed.Encipher(in.AezeedPassphrase)
3✔
336
        if err != nil {
3✔
337
                return nil, err
×
338
        }
×
339

340
        return &lnrpc.GenSeedResponse{
3✔
341
                CipherSeedMnemonic: mnemonic[:],
3✔
342
                EncipheredSeed:     encipheredSeed[:],
3✔
343
        }, nil
3✔
344
}
345

346
// extractChanBackups is a helper function that extracts the set of channel
347
// backups from the proto into a format that we'll pass to higher level
348
// sub-systems.
349
func extractChanBackups(chanBackups *lnrpc.ChanBackupSnapshot) *ChannelsToRecover {
3✔
350
        // If there aren't any populated channel backups, then we can exit
3✔
351
        // early as there's nothing to extract.
3✔
352
        if chanBackups == nil || (chanBackups.SingleChanBackups == nil &&
3✔
353
                chanBackups.MultiChanBackup == nil) {
6✔
354
                return nil
3✔
355
        }
3✔
356

357
        // Now that we know there's at least a single back up populated, we'll
358
        // extract the multi-chan backup (if it's there).
359
        var backups ChannelsToRecover
3✔
360
        if chanBackups.MultiChanBackup != nil {
6✔
361
                multiBackup := chanBackups.MultiChanBackup
3✔
362
                backups.PackedMultiChanBackup = multiBackup.MultiChanBackup
3✔
363
        }
3✔
364

365
        if chanBackups.SingleChanBackups == nil {
6✔
366
                return &backups
3✔
367
        }
3✔
368

369
        // Finally, we can extract all the single chan backups as well.
370
        for _, backup := range chanBackups.SingleChanBackups.ChanBackups {
×
371
                singleChanBackup := backup.ChanBackup
×
372

×
373
                backups.PackedSingleChanBackups = append(
×
374
                        backups.PackedSingleChanBackups, singleChanBackup,
×
375
                )
×
376
        }
×
377

378
        return &backups
×
379
}
380

381
// InitWallet is used when lnd is starting up for the first time to fully
382
// initialize the daemon and its internal wallet. At the very least a wallet
383
// password must be provided. This will be used to encrypt sensitive material
384
// on disk.
385
//
386
// In the case of a recovery scenario, the user can also specify their aezeed
387
// mnemonic and passphrase. If set, then the daemon will use this prior state
388
// to initialize its internal wallet.
389
//
390
// Alternatively, this can be used along with the GenSeed RPC to obtain a
391
// seed, then present it to the user. Once it has been verified by the user,
392
// the seed can be fed into this RPC in order to commit the new wallet.
393
func (u *UnlockerService) InitWallet(ctx context.Context,
394
        in *lnrpc.InitWalletRequest) (*lnrpc.InitWalletResponse, error) {
3✔
395

3✔
396
        // Make sure the password meets our constraints.
3✔
397
        password := in.WalletPassword
3✔
398
        if err := ValidatePassword(password); err != nil {
3✔
399
                return nil, err
×
400
        }
×
401

402
        // Require that the recovery window be non-negative.
403
        recoveryWindow := in.RecoveryWindow
3✔
404
        if recoveryWindow < 0 {
3✔
405
                return nil, fmt.Errorf("recovery window %d must be "+
×
406
                        "non-negative", recoveryWindow)
×
407
        }
×
408

409
        // Ensure that the macaroon root key is *exactly* 32-bytes.
410
        macaroonRootKey := in.MacaroonRootKey
3✔
411
        if len(macaroonRootKey) > 0 &&
3✔
412
                len(macaroonRootKey) != macaroons.RootKeyLen {
3✔
413

×
414
                return nil, fmt.Errorf("macaroon root key must be exactly "+
×
415
                        "%v bytes, is instead %v",
×
416
                        macaroons.RootKeyLen, len(macaroonRootKey),
×
417
                )
×
418
        }
×
419

420
        // We'll then open up the directory that will be used to store the
421
        // wallet's files so we can check if the wallet already exists.
422
        loader, err := u.newLoader(uint32(recoveryWindow))
3✔
423
        if err != nil {
3✔
424
                return nil, err
×
425
        }
×
426

427
        walletExists, err := loader.WalletExists()
3✔
428
        if err != nil {
3✔
429
                return nil, err
×
430
        }
×
431

432
        // If the wallet already exists, then we'll exit early as we can't
433
        // create the wallet if it already exists!
434
        if walletExists {
3✔
UNCOV
435
                return nil, fmt.Errorf("wallet already exists")
×
UNCOV
436
        }
×
437

438
        // At this point, we know the wallet doesn't already exist so we can
439
        // prepare the message that we'll send over the channel later.
440
        initMsg := &WalletInitMsg{
3✔
441
                Passphrase:     password,
3✔
442
                RecoveryWindow: uint32(recoveryWindow),
3✔
443
                StatelessInit:  in.StatelessInit,
3✔
444
                MacRootKey:     macaroonRootKey,
3✔
445
        }
3✔
446

3✔
447
        // There are two supported ways to initialize the wallet. Either from
3✔
448
        // the aezeed or the final extended master key directly.
3✔
449
        switch {
3✔
450
        // Don't allow the user to specify both as that would be ambiguous.
451
        case len(in.CipherSeedMnemonic) > 0 && len(in.ExtendedMasterKey) > 0:
×
452
                return nil, fmt.Errorf("cannot specify both the cipher " +
×
453
                        "seed mnemonic and the extended master key")
×
454

455
        // The aezeed is the preferred and default way of initializing a wallet.
456
        case len(in.CipherSeedMnemonic) > 0:
3✔
457
                // We'll map the user provided aezeed and passphrase into a
3✔
458
                // decoded cipher seed instance.
3✔
459
                var mnemonic aezeed.Mnemonic
3✔
460
                copy(mnemonic[:], in.CipherSeedMnemonic)
3✔
461

3✔
462
                // If we're unable to map it back into the ciphertext, then
3✔
463
                // either the mnemonic is wrong, or the passphrase is wrong.
3✔
464
                cipherSeed, err := mnemonic.ToCipherSeed(in.AezeedPassphrase)
3✔
465
                if err != nil {
3✔
UNCOV
466
                        return nil, err
×
UNCOV
467
                }
×
468

469
                initMsg.WalletSeed = cipherSeed
3✔
470

471
        // To support restoring a wallet where the seed isn't known or a wallet
472
        // created externally to lnd, we also allow the extended master key
473
        // (xprv) to be imported directly. This is what'll be stored in the
474
        // btcwallet database anyway.
475
        case len(in.ExtendedMasterKey) > 0:
3✔
476
                extendedKey, err := hdkeychain.NewKeyFromString(
3✔
477
                        in.ExtendedMasterKey,
3✔
478
                )
3✔
479
                if err != nil {
3✔
480
                        return nil, err
×
481
                }
×
482

483
                // The on-chain wallet of lnd is going to derive keys based on
484
                // the BIP49/84 key derivation paths from this root key. To make
485
                // sure we use default derivation paths, we want to avoid
486
                // deriving keys from something other than the master key (at
487
                // depth 0, denoted with "m/" in BIP32 notation).
488
                if extendedKey.Depth() != 0 {
3✔
489
                        return nil, fmt.Errorf("extended master key must " +
×
490
                                "be at depth 0 not a child key")
×
491
                }
×
492

493
                // Because we need the master key (at depth 0), it must be an
494
                // extended private key as the first levels of BIP49/84
495
                // derivation paths are hardened, which isn't possible with
496
                // extended public keys.
497
                if !extendedKey.IsPrivate() {
3✔
498
                        return nil, fmt.Errorf("extended master key must " +
×
499
                                "contain private keys")
×
500
                }
×
501

502
                // To avoid using the wrong master key, we check that it was
503
                // issued for the correct network. This will cause problems if
504
                // someone tries to import a "new" BIP84 zprv key because with
505
                // this we only support the "legacy" zprv prefix. But it is
506
                // trivial to convert between those formats, as long as the user
507
                // knows what they're doing.
508
                if !extendedKey.IsForNet(u.netParams) {
3✔
509
                        return nil, fmt.Errorf("extended master key must be "+
×
510
                                "for network %s", u.netParams.Name)
×
511
                }
×
512

513
                // When importing a wallet from its extended private key we
514
                // don't know the birthday as that information is not encoded in
515
                // that format. We therefore must set an arbitrary date to start
516
                // rescanning at if the user doesn't provide an explicit value
517
                // for it. Since lnd only uses SegWit addresses, we pick the
518
                // date of the first block that contained SegWit transactions
519
                // (481824).
520
                initMsg.ExtendedKeyBirthday = time.Date(
3✔
521
                        2017, time.August, 24, 1, 57, 37, 0, time.UTC,
3✔
522
                )
3✔
523
                if in.ExtendedMasterKeyBirthdayTimestamp != 0 {
3✔
524
                        initMsg.ExtendedKeyBirthday = time.Unix(
×
525
                                int64(in.ExtendedMasterKeyBirthdayTimestamp), 0,
×
526
                        )
×
527
                }
×
528

529
                initMsg.WalletExtendedKey = extendedKey
3✔
530

531
        // The third option for creating a wallet is the watch-only mode:
532
        // Instead of providing the master root key directly, each individual
533
        // account is passed as an extended public key only. Because of the
534
        // hardened derivation path up to the account (depth 3), it is not
535
        // possible to create a master root extended _public_ key. Therefore, an
536
        // xpub must be derived and passed into the unlocker for _every_ account
537
        // lnd expects.
538
        case in.WatchOnly != nil && len(in.WatchOnly.Accounts) > 0:
3✔
539
                initMsg.WatchOnlyAccounts = make(
3✔
540
                        map[waddrmgr.ScopedIndex]*hdkeychain.ExtendedKey,
3✔
541
                        len(in.WatchOnly.Accounts),
3✔
542
                )
3✔
543

3✔
544
                for _, acct := range in.WatchOnly.Accounts {
6✔
545
                        scopedIndex := waddrmgr.ScopedIndex{
3✔
546
                                Scope: waddrmgr.KeyScope{
3✔
547
                                        Purpose: acct.Purpose,
3✔
548
                                        Coin:    acct.CoinType,
3✔
549
                                },
3✔
550
                                Index: acct.Account,
3✔
551
                        }
3✔
552
                        acctKey, err := hdkeychain.NewKeyFromString(acct.Xpub)
3✔
553
                        if err != nil {
3✔
554
                                return nil, fmt.Errorf("error parsing xpub "+
×
555
                                        "%v: %v", acct.Xpub, err)
×
556
                        }
×
557

558
                        // Just to make sure the user is doing the right thing,
559
                        // we expect the public key to be at derivation depth
560
                        // three (which is the account level) and the key not to
561
                        // contain any private key material.
562
                        if acctKey.Depth() != 3 {
3✔
563
                                return nil, fmt.Errorf("xpub must be at " +
×
564
                                        "depth 3")
×
565
                        }
×
566
                        if acctKey.IsPrivate() {
3✔
567
                                return nil, fmt.Errorf("xpub is not really " +
×
568
                                        "an xpub, contains private key")
×
569
                        }
×
570

571
                        initMsg.WatchOnlyAccounts[scopedIndex] = acctKey
3✔
572
                }
573

574
                // When importing a wallet from its extended public keys we
575
                // don't know the birthday as that information is not encoded in
576
                // that format. We therefore must set an arbitrary date to start
577
                // rescanning at if the user doesn't provide an explicit value
578
                // for it. Since lnd only uses SegWit addresses, we pick the
579
                // date of the first block that contained SegWit transactions
580
                // (481824).
581
                initMsg.WatchOnlyBirthday = time.Date(
3✔
582
                        2017, time.August, 24, 1, 57, 37, 0, time.UTC,
3✔
583
                )
3✔
584
                if in.WatchOnly.MasterKeyBirthdayTimestamp != 0 {
3✔
585
                        initMsg.WatchOnlyBirthday = time.Unix(
×
586
                                int64(in.WatchOnly.MasterKeyBirthdayTimestamp),
×
587
                                0,
×
588
                        )
×
589
                }
×
590

591
        // No key material was set, no wallet can be created.
592
        default:
×
593
                return nil, fmt.Errorf("must either specify cipher seed " +
×
594
                        "mnemonic or the extended master key")
×
595
        }
596

597
        // Before we return the unlock payload, we'll check if we can extract
598
        // any channel backups to pass up to the higher level sub-system.
599
        chansToRestore := extractChanBackups(in.ChannelBackups)
3✔
600
        if chansToRestore != nil {
6✔
601
                initMsg.ChanBackups = *chansToRestore
3✔
602
        }
3✔
603

604
        // Deliver the initialization message back to the main daemon.
605
        select {
3✔
606
        case u.InitMsgs <- initMsg:
3✔
607
                // We need to read from the channel to let the daemon continue
3✔
608
                // its work and to get the admin macaroon. Once the response
3✔
609
                // arrives, we directly forward it to the client.
3✔
610
                select {
3✔
611
                case adminMac := <-u.MacResponseChan:
3✔
612
                        return &lnrpc.InitWalletResponse{
3✔
613
                                AdminMacaroon: adminMac,
3✔
614
                        }, nil
3✔
615

616
                case <-ctx.Done():
×
617
                        return nil, ErrUnlockTimeout
×
618
                }
619

620
        case <-ctx.Done():
×
621
                return nil, ErrUnlockTimeout
×
622
        }
623
}
624

625
// LoadAndUnlock creates a loader for the wallet and tries to unlock the wallet
626
// with the given password and recovery window. If the drop wallet transactions
627
// flag is set, the history state drop is performed before unlocking the wallet
628
// yet again.
629
func (u *UnlockerService) LoadAndUnlock(password []byte,
630
        recoveryWindow uint32) (*wallet.Wallet, func() error, error) {
3✔
631

3✔
632
        loader, err := u.newLoader(recoveryWindow)
3✔
633
        if err != nil {
3✔
634
                return nil, nil, err
×
635
        }
×
636

637
        // Check if wallet already exists.
638
        walletExists, err := loader.WalletExists()
3✔
639
        if err != nil {
3✔
640
                return nil, nil, err
×
641
        }
×
642

643
        if !walletExists {
3✔
UNCOV
644
                // Cannot unlock a wallet that does not exist!
×
UNCOV
645
                return nil, nil, fmt.Errorf("wallet not found")
×
UNCOV
646
        }
×
647

648
        // Try opening the existing wallet with the provided password.
649
        unlockedWallet, err := loader.OpenExistingWallet(password, false)
3✔
650
        if err != nil {
3✔
UNCOV
651
                // Could not open wallet, most likely this means that provided
×
UNCOV
652
                // password was incorrect.
×
UNCOV
653
                return nil, nil, err
×
UNCOV
654
        }
×
655

656
        // The user requested to drop their whole wallet transaction state to
657
        // force a full chain rescan for wallet addresses. Dropping the state
658
        // only properly takes effect after opening the wallet. That's why we
659
        // start, drop, stop and start again.
660
        if u.resetWalletTransactions {
6✔
661
                dropErr := wallet.DropTransactionHistory(
3✔
662
                        unlockedWallet.Database(), true,
3✔
663
                )
3✔
664

3✔
665
                // Even if dropping the history fails, we'll want to unload the
3✔
666
                // wallet. If unloading fails, that error is probably more
3✔
667
                // important to be returned to the user anyway.
3✔
668
                if err := loader.UnloadWallet(); err != nil {
3✔
669
                        return nil, nil, fmt.Errorf("could not unload "+
×
670
                                "wallet (tx history drop err: %v): %v", dropErr,
×
671
                                err)
×
672
                }
×
673

674
                // If dropping failed but unloading didn't, we'll still abort
675
                // and inform the user.
676
                if dropErr != nil {
3✔
677
                        return nil, nil, dropErr
×
678
                }
×
679

680
                // All looks good, let's now open the wallet again. The loader
681
                // was unloaded and might have removed its remote DB connection,
682
                // so let's re-create it as well.
683
                loader, err = u.newLoader(recoveryWindow)
3✔
684
                if err != nil {
3✔
685
                        return nil, nil, err
×
686
                }
×
687
                unlockedWallet, err = loader.OpenExistingWallet(password, false)
3✔
688
                if err != nil {
3✔
689
                        return nil, nil, err
×
690
                }
×
691
        }
692

693
        return unlockedWallet, loader.UnloadWallet, nil
3✔
694
}
695

696
// UnlockWallet sends the password provided by the incoming UnlockWalletRequest
697
// over the UnlockMsgs channel in case it successfully decrypts an existing
698
// wallet found in the chain's wallet database directory.
699
func (u *UnlockerService) UnlockWallet(ctx context.Context,
700
        in *lnrpc.UnlockWalletRequest) (*lnrpc.UnlockWalletResponse, error) {
3✔
701

3✔
702
        password := in.WalletPassword
3✔
703
        recoveryWindow := uint32(in.RecoveryWindow)
3✔
704

3✔
705
        unlockedWallet, unloadFn, err := u.LoadAndUnlock(
3✔
706
                password, recoveryWindow,
3✔
707
        )
3✔
708
        if err != nil {
3✔
UNCOV
709
                return nil, err
×
UNCOV
710
        }
×
711

712
        // We successfully opened the wallet and pass the instance back to
713
        // avoid it needing to be unlocked again.
714
        walletUnlockMsg := &WalletUnlockMsg{
3✔
715
                Passphrase:     password,
3✔
716
                RecoveryWindow: recoveryWindow,
3✔
717
                Wallet:         unlockedWallet,
3✔
718
                UnloadWallet:   unloadFn,
3✔
719
                StatelessInit:  in.StatelessInit,
3✔
720
        }
3✔
721

3✔
722
        // Before we return the unlock payload, we'll check if we can extract
3✔
723
        // any channel backups to pass up to the higher level sub-system.
3✔
724
        chansToRestore := extractChanBackups(in.ChannelBackups)
3✔
725
        if chansToRestore != nil {
6✔
726
                walletUnlockMsg.ChanBackups = *chansToRestore
3✔
727
        }
3✔
728

729
        // At this point we were able to open the existing wallet with the
730
        // provided password. We send the password over the UnlockMsgs
731
        // channel, such that it can be used by lnd to open the wallet.
732
        select {
3✔
733
        case u.UnlockMsgs <- walletUnlockMsg:
3✔
734
                // We need to read from the channel to let the daemon continue
3✔
735
                // its work. But we don't need the returned macaroon for this
3✔
736
                // operation, so we read it but then discard it.
3✔
737
                select {
3✔
738
                case <-u.MacResponseChan:
3✔
739
                        return &lnrpc.UnlockWalletResponse{}, nil
3✔
740

741
                case <-ctx.Done():
×
742
                        return nil, ErrUnlockTimeout
×
743
                }
744

745
        case <-ctx.Done():
×
746
                return nil, ErrUnlockTimeout
×
747
        }
748
}
749

750
// ChangePassword changes the password of the wallet and sends the new password
751
// across the UnlockPasswords channel to automatically unlock the wallet if
752
// successful.
753
func (u *UnlockerService) ChangePassword(ctx context.Context,
754
        in *lnrpc.ChangePasswordRequest) (*lnrpc.ChangePasswordResponse, error) {
3✔
755

3✔
756
        loader, err := u.newLoader(0)
3✔
757
        if err != nil {
3✔
758
                return nil, err
×
759
        }
×
760

761
        // First, we'll make sure the wallet exists for the specific chain and
762
        // network.
763
        walletExists, err := loader.WalletExists()
3✔
764
        if err != nil {
3✔
765
                return nil, err
×
766
        }
×
767

768
        if !walletExists {
3✔
UNCOV
769
                return nil, errors.New("wallet not found")
×
UNCOV
770
        }
×
771

772
        publicPw := in.CurrentPassword
3✔
773
        privatePw := in.CurrentPassword
3✔
774

3✔
775
        // If the current password is blank, we'll assume the user is coming
3✔
776
        // from a --noseedbackup state, so we'll use the default passwords.
3✔
777
        if len(in.CurrentPassword) == 0 {
3✔
UNCOV
778
                publicPw = lnwallet.DefaultPublicPassphrase
×
UNCOV
779
                privatePw = lnwallet.DefaultPrivatePassphrase
×
UNCOV
780
        }
×
781

782
        // Make sure the new password meets our constraints.
783
        if err := ValidatePassword(in.NewPassword); err != nil {
3✔
UNCOV
784
                return nil, err
×
UNCOV
785
        }
×
786

787
        // Load the existing wallet in order to proceed with the password change.
788
        w, err := loader.OpenExistingWallet(publicPw, false)
3✔
789
        if err != nil {
3✔
UNCOV
790
                return nil, err
×
UNCOV
791
        }
×
792

793
        // Now that we've opened the wallet, we need to close it in case of an
794
        // error. But not if we succeed, then the caller must close it.
795
        orderlyReturn := false
3✔
796
        defer func() {
6✔
797
                if !orderlyReturn {
3✔
UNCOV
798
                        _ = loader.UnloadWallet()
×
UNCOV
799
                }
×
800
        }()
801

802
        // Before we actually change the password, we need to check if all flags
803
        // were set correctly. The content of the previously generated macaroon
804
        // files will become invalid after we generate a new root key. So we try
805
        // to delete them here and they will be recreated during normal startup
806
        // later. If they are missing, this is only an error if the
807
        // stateless_init flag was not set.
808
        if in.NewMacaroonRootKey || in.StatelessInit {
6✔
809
                for _, file := range u.macaroonFiles {
6✔
810
                        err := os.Remove(file)
3✔
811
                        if err != nil && !in.StatelessInit {
3✔
UNCOV
812
                                return nil, fmt.Errorf("could not remove "+
×
UNCOV
813
                                        "macaroon file: %v. if the wallet "+
×
UNCOV
814
                                        "was initialized stateless please "+
×
UNCOV
815
                                        "add the --stateless_init "+
×
UNCOV
816
                                        "flag", err)
×
UNCOV
817
                        }
×
818
                }
819
        }
820

821
        // Attempt to change both the public and private passphrases for the
822
        // wallet. This will be done atomically in order to prevent one
823
        // passphrase change from being successful and not the other.
824
        err = w.ChangePassphrases(
3✔
825
                publicPw, in.NewPassword, privatePw, in.NewPassword,
3✔
826
        )
3✔
827
        if err != nil {
3✔
828
                return nil, fmt.Errorf("unable to change wallet passphrase: "+
×
829
                        "%v", err)
×
830
        }
×
831

832
        // The next step is to load the macaroon database, change the password
833
        // then close it again.
834
        // Attempt to open the macaroon DB, unlock it and then change
835
        // the passphrase.
836
        rootKeyStore, err := macaroons.NewRootKeyStorage(u.macaroonDB)
3✔
837
        if err != nil {
3✔
838
                return nil, err
×
839
        }
×
840
        macaroonService, err := macaroons.NewService(
3✔
841
                rootKeyStore, "lnd", in.StatelessInit,
3✔
842
        )
3✔
843
        if err != nil {
3✔
844
                return nil, err
×
845
        }
×
846

847
        err = macaroonService.CreateUnlock(&privatePw)
3✔
848
        if err != nil {
3✔
849
                closeErr := macaroonService.Close()
×
850
                if closeErr != nil {
×
851
                        return nil, fmt.Errorf("could not create unlock: %v "+
×
852
                                "--> follow-up error when closing: %v", err,
×
853
                                closeErr)
×
854
                }
×
855
                return nil, err
×
856
        }
857
        err = macaroonService.ChangePassword(privatePw, in.NewPassword)
3✔
858
        if err != nil {
3✔
859
                closeErr := macaroonService.Close()
×
860
                if closeErr != nil {
×
861
                        return nil, fmt.Errorf("could not change password: %v "+
×
862
                                "--> follow-up error when closing: %v", err,
×
863
                                closeErr)
×
864
                }
×
865
                return nil, err
×
866
        }
867

868
        // If requested by the user, attempt to replace the existing
869
        // macaroon root key with a new one.
870
        if in.NewMacaroonRootKey {
3✔
UNCOV
871
                err = macaroonService.GenerateNewRootKey()
×
UNCOV
872
                if err != nil {
×
873
                        closeErr := macaroonService.Close()
×
874
                        if closeErr != nil {
×
875
                                return nil, fmt.Errorf("could not generate "+
×
876
                                        "new root key: %v --> follow-up error "+
×
877
                                        "when closing: %v", err, closeErr)
×
878
                        }
×
879
                        return nil, err
×
880
                }
881
        }
882

883
        err = macaroonService.Close()
3✔
884
        if err != nil {
3✔
885
                return nil, fmt.Errorf("could not close macaroon service: %w",
×
886
                        err)
×
887
        }
×
888

889
        // Finally, send the new password across the UnlockPasswords channel to
890
        // automatically unlock the wallet.
891
        walletUnlockMsg := &WalletUnlockMsg{
3✔
892
                Passphrase:    in.NewPassword,
3✔
893
                Wallet:        w,
3✔
894
                StatelessInit: in.StatelessInit,
3✔
895
                UnloadWallet:  loader.UnloadWallet,
3✔
896
        }
3✔
897
        select {
3✔
898
        case u.UnlockMsgs <- walletUnlockMsg:
3✔
899
                // We need to read from the channel to let the daemon continue
3✔
900
                // its work and to get the admin macaroon. Once the response
3✔
901
                // arrives, we directly forward it to the client.
3✔
902
                orderlyReturn = true
3✔
903
                select {
3✔
904
                case adminMac := <-u.MacResponseChan:
3✔
905
                        return &lnrpc.ChangePasswordResponse{
3✔
906
                                AdminMacaroon: adminMac,
3✔
907
                        }, nil
3✔
908

909
                case <-ctx.Done():
×
910
                        return nil, ErrUnlockTimeout
×
911
                }
912

913
        case <-ctx.Done():
×
914
                return nil, ErrUnlockTimeout
×
915
        }
916
}
917

918
// ValidatePassword assures the password meets all of our constraints.
919
func ValidatePassword(password []byte) error {
3✔
920
        // Passwords should have a length of at least 8 characters.
3✔
921
        if len(password) < 8 {
3✔
UNCOV
922
                return errors.New("password must have at least 8 characters")
×
UNCOV
923
        }
×
924

925
        return nil
3✔
926
}
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