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

lightningnetwork / lnd / 11357847688

16 Oct 2024 02:36AM UTC coverage: 57.864% (-0.9%) from 58.779%
11357847688

Pull #9148

github

ProofOfKeags
lnwire: change DynPropose/DynCommit TLV numbers to align with spec
Pull Request #9148: DynComms [2/n]: lnwire: add authenticated wire messages for Dyn*

350 of 644 new or added lines in 12 files covered. (54.35%)

19831 existing lines in 241 files now uncovered.

99337 of 171674 relevant lines covered (57.86%)

38595.9 hits per line

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

0.0
/cmd/commands/cmd_walletunlocker.go
1
package commands
2

3
import (
4
        "bufio"
5
        "bytes"
6
        "encoding/hex"
7
        "fmt"
8
        "os"
9
        "strconv"
10
        "strings"
11

12
        "github.com/lightningnetwork/lnd/lncfg"
13
        "github.com/lightningnetwork/lnd/lnrpc"
14
        "github.com/lightningnetwork/lnd/lnrpc/walletrpc"
15
        "github.com/lightningnetwork/lnd/walletunlocker"
16
        "github.com/urfave/cli"
17
)
18

19
var (
20
        statelessInitFlag = cli.BoolFlag{
21
                Name: "stateless_init",
22
                Usage: "do not create any macaroon files in the file " +
23
                        "system of the daemon",
24
        }
25
        saveToFlag = cli.StringFlag{
26
                Name:  "save_to",
27
                Usage: "save returned admin macaroon to this file",
28
        }
29
)
30

31
var createCommand = cli.Command{
32
        Name:     "create",
33
        Category: "Startup",
34
        Usage:    "Initialize a wallet when starting lnd for the first time.",
35
        Description: `
36
        The create command is used to initialize an lnd wallet from scratch for
37
        the very first time. This is an interactive command with one required
38
        input (the password), and one optional input (the mnemonic passphrase).
39

40
        The first input (the password) is required and MUST be greater than 8
41
        characters. This will be used to encrypt the wallet within lnd. This
42
        MUST be remembered as it will be required to fully start up the daemon.
43

44
        The second input is an optional 24-word mnemonic derived from BIP 39.
45
        If provided, then the internal wallet will use the seed derived from
46
        this mnemonic to generate all keys.
47

48
        This command returns a 24-word seed in the scenario that NO mnemonic
49
        was provided by the user. This should be written down as it can be used
50
        to potentially recover all on-chain funds, and most off-chain funds as
51
        well.
52

53
        If the --stateless_init flag is set, no macaroon files are created by
54
        the daemon. Instead, the binary serialized admin macaroon is returned
55
        in the answer. This answer MUST be stored somewhere, otherwise all
56
        access to the RPC server will be lost and the wallet must be recreated
57
        to re-gain access.
58
        If the --save_to parameter is set, the macaroon is saved to this file,
59
        otherwise it is printed to standard out.
60

61
        Finally, it's also possible to use this command and a set of static
62
        channel backups to trigger a recover attempt for the provided Static
63
        Channel Backups. Only one of the three parameters will be accepted. See
64
        the restorechanbackup command for further details w.r.t the format
65
        accepted.
66
        `,
67
        Flags: []cli.Flag{
68
                cli.StringFlag{
69
                        Name: "single_backup",
70
                        Usage: "a hex encoded single channel backup obtained " +
71
                                "from exportchanbackup",
72
                },
73
                cli.StringFlag{
74
                        Name: "multi_backup",
75
                        Usage: "a hex encoded multi-channel backup obtained " +
76
                                "from exportchanbackup",
77
                },
78
                cli.StringFlag{
79
                        Name:  "multi_file",
80
                        Usage: "the path to a multi-channel back up file",
81
                },
82
                statelessInitFlag,
83
                saveToFlag,
84
        },
85
        Action: actionDecorator(create),
86
}
87

88
// monowidthColumns takes a set of words, and the number of desired columns,
89
// and returns a new set of words that have had white space appended to the
90
// word in order to create a mono-width column.
91
func monowidthColumns(words []string, ncols int) []string {
92
        // Determine max size of words in each column.
93
        colWidths := make([]int, ncols)
94
        for i, word := range words {
95
                col := i % ncols
96
                curWidth := colWidths[col]
97
                if len(word) > curWidth {
98
                        colWidths[col] = len(word)
99
                }
UNCOV
100
        }
×
UNCOV
101

×
UNCOV
102
        // Append whitespace to each word to make columns mono-width.
×
103
        finalWords := make([]string, len(words))
×
104
        for i, word := range words {
×
105
                col := i % ncols
×
106
                width := colWidths[col]
×
107

×
108
                diff := width - len(word)
×
109
                finalWords[i] = word + strings.Repeat(" ", diff)
110
        }
111

112
        return finalWords
×
UNCOV
113
}
×
UNCOV
114

×
115
func create(ctx *cli.Context) error {
×
116
        ctxc := getContext()
×
117
        client, cleanUp := getWalletUnlockerClient(ctx)
×
118
        defer cleanUp()
×
119

×
120
        var (
121
                chanBackups *lnrpc.ChanBackupSnapshot
×
122

123
                // We use var restoreSCB to track if we will be including an SCB
124
                // recovery in the init wallet request.
×
125
                restoreSCB = false
×
126
        )
×
127

×
128
        backups, err := parseChanBackups(ctx)
×
129

×
130
        // We'll check to see if the user provided any static channel backups (SCB),
×
131
        // if so, we will warn the user that SCB recovery closes all open channels
×
132
        // and ask them to confirm their intention.
×
133
        // If the user agrees, we'll add the SCB recovery onto the final init wallet
×
134
        // request.
×
135
        switch {
×
UNCOV
136
        // parseChanBackups returns an errMissingBackup error (which we ignore) if
×
UNCOV
137
        // the user did not request a SCB recovery.
×
138
        case err == errMissingChanBackup:
×
UNCOV
139

×
UNCOV
140
        // Passed an invalid channel backup file.
×
141
        case err != nil:
×
142
                return fmt.Errorf("unable to parse chan backups: %w", err)
×
UNCOV
143

×
UNCOV
144
        // We have an SCB recovery option with a valid backup file.
×
145
        default:
146

147
        warningLoop:
×
148
                for {
149
                        fmt.Println()
150
                        fmt.Printf("WARNING: You are attempting to restore from a " +
×
151
                                "static channel backup (SCB) file.\nThis action will CLOSE " +
×
152
                                "all currently open channels, and you will pay on-chain fees." +
153
                                "\n\nAre you sure you want to recover funds from a" +
154
                                " static channel backup? (Enter y/n): ")
×
155

×
156
                        reader := bufio.NewReader(os.Stdin)
×
157
                        answer, err := reader.ReadString('\n')
×
158
                        if err != nil {
×
159
                                return err
×
160
                        }
×
UNCOV
161

×
162
                        answer = strings.TrimSpace(answer)
×
163
                        answer = strings.ToLower(answer)
×
164

×
165
                        switch answer {
×
166
                        case "y":
×
167
                                restoreSCB = true
×
168
                                break warningLoop
×
169
                        case "n":
×
170
                                fmt.Println("Aborting SCB recovery")
171
                                return nil
×
UNCOV
172
                        }
×
UNCOV
173
                }
×
UNCOV
174
        }
×
UNCOV
175

×
UNCOV
176
        // Proceed with SCB recovery.
×
177
        if restoreSCB {
×
178
                fmt.Println("Static Channel Backup (SCB) recovery selected!")
×
179
                if backups != nil {
×
180
                        switch {
×
181
                        case backups.GetChanBackups() != nil:
182
                                singleBackup := backups.GetChanBackups()
183
                                chanBackups = &lnrpc.ChanBackupSnapshot{
184
                                        SingleChanBackups: singleBackup,
185
                                }
UNCOV
186

×
187
                        case backups.GetMultiChanBackup() != nil:
×
188
                                multiBackup := backups.GetMultiChanBackup()
×
189
                                chanBackups = &lnrpc.ChanBackupSnapshot{
×
190
                                        MultiChanBackup: &lnrpc.MultiChanBackup{
×
191
                                                MultiChanBackup: multiBackup,
×
192
                                        },
×
193
                                }
×
UNCOV
194
                        }
×
195
                }
UNCOV
196
        }
×
UNCOV
197

×
UNCOV
198
        // Should the daemon be initialized stateless? Then we expect an answer
×
UNCOV
199
        // with the admin macaroon later. Because the --save_to is related to
×
UNCOV
200
        // stateless init, it doesn't make sense to be set on its own.
×
201
        statelessInit := ctx.Bool(statelessInitFlag.Name)
×
202
        if !statelessInit && ctx.IsSet(saveToFlag.Name) {
×
203
                return fmt.Errorf("cannot set save_to parameter without " +
204
                        "stateless_init")
205
        }
206

207
        walletPassword, err := capturePassword(
208
                "Input wallet password: ", false, walletunlocker.ValidatePassword,
209
        )
210
        if err != nil {
×
211
                return err
×
212
        }
×
UNCOV
213

×
UNCOV
214
        // Next, we'll see if the user has 24-word mnemonic they want to use to
×
215
        // derive a seed within the wallet or if they want to specify an
UNCOV
216
        // extended master root key (xprv) directly.
×
217
        var (
×
218
                hasMnemonic bool
×
219
                hasXprv     bool
×
220
        )
×
221

×
222
mnemonicCheck:
223
        for {
224
                fmt.Println()
225
                fmt.Printf("Do you have an existing cipher seed " +
226
                        "mnemonic or extended master root key you want to " +
×
227
                        "use?\nEnter 'y' to use an existing cipher seed " +
×
228
                        "mnemonic, 'x' to use an extended master root key " +
×
229
                        "\nor 'n' to create a new seed (Enter y/x/n): ")
×
230

×
231
                reader := bufio.NewReader(os.Stdin)
×
232
                answer, err := reader.ReadString('\n')
×
233
                if err != nil {
×
234
                        return err
×
235
                }
×
UNCOV
236

×
237
                fmt.Println()
×
238

×
239
                answer = strings.TrimSpace(answer)
×
240
                answer = strings.ToLower(answer)
×
241

×
242
                switch answer {
×
243
                case "y":
×
244
                        hasMnemonic = true
×
245
                        break mnemonicCheck
UNCOV
246

×
247
                case "x":
×
248
                        hasXprv = true
×
249
                        break mnemonicCheck
×
UNCOV
250

×
251
                case "n":
×
252
                        break mnemonicCheck
×
UNCOV
253
                }
×
UNCOV
254
        }
×
255

UNCOV
256
        // If the user *does* have an existing seed or root key they want to
×
UNCOV
257
        // use, then we'll read that in directly from the terminal.
×
258
        var (
×
259
                cipherSeedMnemonic      []string
260
                aezeedPass              []byte
×
261
                extendedRootKey         string
×
262
                extendedRootKeyBirthday uint64
263
                recoveryWindow          int32
264
        )
265
        switch {
266
        // Use an existing cipher seed mnemonic in the aezeed format.
267
        case hasMnemonic:
×
268
                // We'll now prompt the user to enter in their 24-word
×
269
                // mnemonic.
×
270
                fmt.Printf("Input your 24-word mnemonic separated by spaces: ")
×
271
                reader := bufio.NewReader(os.Stdin)
×
272
                mnemonic, err := reader.ReadString('\n')
×
273
                if err != nil {
×
274
                        return err
×
275
                }
×
276

UNCOV
277
                // We'll trim off extra spaces, and ensure the mnemonic is all
×
UNCOV
278
                // lower case, then populate our request.
×
279
                mnemonic = strings.TrimSpace(mnemonic)
×
280
                mnemonic = strings.ToLower(mnemonic)
×
281

×
282
                cipherSeedMnemonic = strings.Split(mnemonic, " ")
×
283

×
284
                fmt.Println()
×
285

×
286
                if len(cipherSeedMnemonic) != 24 {
287
                        return fmt.Errorf("wrong cipher seed mnemonic "+
288
                                "length: got %v words, expecting %v words",
289
                                len(cipherSeedMnemonic), 24)
×
290
                }
×
UNCOV
291

×
UNCOV
292
                // Additionally, the user may have a passphrase, that will also
×
UNCOV
293
                // need to be provided so the daemon can properly decipher the
×
UNCOV
294
                // cipher seed.
×
295
                aezeedPass, err = readPassword("Input your cipher seed " +
×
296
                        "passphrase (press enter if your seed doesn't have a " +
×
297
                        "passphrase): ")
×
298
                if err != nil {
×
299
                        return err
×
300
                }
×
301

302
                recoveryWindow, err = askRecoveryWindow()
303
                if err != nil {
304
                        return err
305
                }
×
UNCOV
306

×
UNCOV
307
        // Use an existing extended master root key to create the wallet.
×
308
        case hasXprv:
×
309
                // We'll now prompt the user to enter in their extended master
×
310
                // root key.
×
311
                fmt.Printf("Input your extended master root key (usually " +
312
                        "starting with xprv... on mainnet): ")
×
313
                reader := bufio.NewReader(os.Stdin)
×
314
                extendedRootKey, err = reader.ReadString('\n')
×
315
                if err != nil {
×
316
                        return err
317
                }
318
                extendedRootKey = strings.TrimSpace(extendedRootKey)
×
319

×
320
                extendedRootKeyBirthday, err = askBirthdayTimestamp()
×
321
                if err != nil {
×
322
                        return err
×
323
                }
×
UNCOV
324

×
325
                recoveryWindow, err = askRecoveryWindow()
×
326
                if err != nil {
×
327
                        return err
×
328
                }
×
UNCOV
329

×
UNCOV
330
        // Neither a seed nor a master root key was specified, the user wants
×
UNCOV
331
        // to create a new seed.
×
332
        default:
×
333
                // Otherwise, if the user doesn't have a mnemonic that they
×
334
                // want to use, we'll generate a fresh one with the GenSeed
335
                // command.
×
336
                fmt.Println("Your cipher seed can optionally be encrypted.")
×
337

×
338
                instruction := "Input your passphrase if you wish to encrypt it " +
×
339
                        "(or press enter to proceed without a cipher seed " +
340
                        "passphrase): "
341
                aezeedPass, err = capturePassword(
342
                        instruction, true, func(_ []byte) error { return nil },
×
UNCOV
343
                )
×
344
                if err != nil {
×
345
                        return err
×
346
                }
×
UNCOV
347

×
348
                fmt.Println()
×
349
                fmt.Println("Generating fresh cipher seed...")
×
350
                fmt.Println()
×
351

×
352
                genSeedReq := &lnrpc.GenSeedRequest{
×
353
                        AezeedPassphrase: aezeedPass,
354
                }
×
355
                seedResp, err := client.GenSeed(ctxc, genSeedReq)
×
356
                if err != nil {
×
357
                        return fmt.Errorf("unable to generate seed: %w", err)
358
                }
×
UNCOV
359

×
360
                cipherSeedMnemonic = seedResp.CipherSeedMnemonic
×
UNCOV
361
        }
×
UNCOV
362

×
UNCOV
363
        // Before we initialize the wallet, we'll display the cipher seed to
×
UNCOV
364
        // the user so they can write it down.
×
365
        if len(cipherSeedMnemonic) > 0 {
×
366
                printCipherSeedWords(cipherSeedMnemonic)
×
367
        }
×
UNCOV
368

×
369
        // With either the user's prior cipher seed, or a newly generated one,
UNCOV
370
        // we'll go ahead and initialize the wallet.
×
371
        req := &lnrpc.InitWalletRequest{
372
                WalletPassword:                     walletPassword,
373
                CipherSeedMnemonic:                 cipherSeedMnemonic,
374
                AezeedPassphrase:                   aezeedPass,
375
                ExtendedMasterKey:                  extendedRootKey,
×
376
                ExtendedMasterKeyBirthdayTimestamp: extendedRootKeyBirthday,
×
377
                RecoveryWindow:                     recoveryWindow,
×
378
                ChannelBackups:                     chanBackups,
379
                StatelessInit:                      statelessInit,
380
        }
×
381
        response, err := client.InitWallet(ctxc, req)
×
382
        if err != nil {
×
383
                return err
×
384
        }
×
UNCOV
385

×
386
        fmt.Println("\nlnd successfully initialized!")
×
387

×
388
        if statelessInit {
389
                return storeOrPrintAdminMac(ctx, response.AdminMacaroon)
×
390
        }
×
UNCOV
391

×
392
        return nil
×
UNCOV
393
}
×
394

395
// capturePassword returns a password value that has been entered twice by the
396
// user, to ensure that the user knows what password they have entered. The user
397
// will be prompted to retry until the passwords match. If the optional param is
UNCOV
398
// true, the function may return an empty byte array if the user opts against
×
UNCOV
399
// using a password.
×
UNCOV
400
func capturePassword(instruction string, optional bool,
×
401
        validate func([]byte) error) ([]byte, error) {
×
402

×
403
        for {
×
404
                password, err := readPassword(instruction)
×
405
                if err != nil {
×
406
                        return nil, err
×
407
                }
×
UNCOV
408

×
UNCOV
409
                // Do not require users to repeat password if
×
UNCOV
410
                // it is optional and they are not using one.
×
411
                if len(password) == 0 && optional {
×
412
                        return nil, nil
×
413
                }
UNCOV
414

×
UNCOV
415
                // If the password provided is not valid, restart
×
UNCOV
416
                // password capture process from the beginning.
×
417
                if err := validate(password); err != nil {
×
418
                        fmt.Println(err.Error())
×
419
                        fmt.Println()
420
                        continue
×
421
                }
422

423
                passwordConfirmed, err := readPassword("Confirm password: ")
424
                if err != nil {
425
                        return nil, err
426
                }
427

428
                if bytes.Equal(password, passwordConfirmed) {
429
                        return password, nil
×
430
                }
×
UNCOV
431

×
432
                fmt.Println("Passwords don't match, please try again")
×
433
                fmt.Println()
×
UNCOV
434
        }
×
UNCOV
435
}
×
436

437
var unlockCommand = cli.Command{
438
        Name:     "unlock",
UNCOV
439
        Category: "Startup",
×
UNCOV
440
        Usage:    "Unlock an encrypted wallet at startup.",
×
UNCOV
441
        Description: `
×
442
        The unlock command is used to decrypt lnd's wallet state in order to
443
        start up. This command MUST be run after booting up lnd before it's
444
        able to carry out its duties. An exception is if a user is running with
UNCOV
445
        --noseedbackup, then a default passphrase will be used.
×
UNCOV
446

×
UNCOV
447
        If the --stateless_init flag is set, no macaroon files are created by
×
UNCOV
448
        the daemon. This should be set for every unlock if the daemon was
×
449
        initially initialized stateless. Otherwise the daemon will create
450
        unencrypted macaroon files which could leak information to the system
UNCOV
451
        that the daemon runs on.
×
UNCOV
452
        `,
×
UNCOV
453
        Flags: []cli.Flag{
×
UNCOV
454
                cli.IntFlag{
×
455
                        Name: "recovery_window",
UNCOV
456
                        Usage: "address lookahead to resume recovery rescan, " +
×
UNCOV
457
                                "value should be non-zero --  To recover all " +
×
UNCOV
458
                                "funds, this should be greater than the " +
×
459
                                "maximum number of consecutive, unused " +
UNCOV
460
                                "addresses ever generated by the wallet.",
×
UNCOV
461
                },
×
462
                cli.BoolFlag{
463
                        Name: "stdin",
464
                        Usage: "read password from standard input instead of " +
465
                                "prompting for it. THIS IS CONSIDERED TO " +
466
                                "BE DANGEROUS if the password is located in " +
467
                                "a file that can be read by another user. " +
468
                                "This flag should only be used in " +
469
                                "combination with some sort of password " +
470
                                "manager or secrets vault.",
471
                },
472
                statelessInitFlag,
473
        },
474
        Action: actionDecorator(unlock),
475
}
476

477
func unlock(ctx *cli.Context) error {
478
        ctxc := getContext()
479
        client, cleanUp := getWalletUnlockerClient(ctx)
480
        defer cleanUp()
481

482
        var (
483
                pw  []byte
484
                err error
485
        )
486
        switch {
487
        // Read the password from standard in as if it were a file. This should
488
        // only be used if the password is piped into lncli from some sort of
489
        // password manager. If the user types the password instead, it will be
490
        // echoed in the console.
491
        case ctx.IsSet("stdin"):
492
                reader := bufio.NewReader(os.Stdin)
493
                pw, err = reader.ReadBytes('\n')
494

495
                // Remove carriage return and newline characters.
496
                pw = bytes.Trim(pw, "\r\n")
497

498
        // Read the password from a terminal by default. This requires the
499
        // terminal to be a real tty and will fail if a string is piped into
500
        // lncli.
501
        default:
502
                pw, err = readPassword("Input wallet password: ")
503
        }
504
        if err != nil {
505
                return err
×
506
        }
×
UNCOV
507

×
508
        args := ctx.Args()
×
509

×
510
        // Parse the optional recovery window if it is specified. By default,
×
511
        // the recovery window will be 0, indicating no lookahead should be
×
512
        // used.
×
513
        var recoveryWindow int32
×
514
        switch {
×
515
        case ctx.IsSet("recovery_window"):
516
                recoveryWindow = int32(ctx.Int64("recovery_window"))
517
        case args.Present():
518
                window, err := strconv.ParseInt(args.First(), 10, 64)
519
                if err != nil {
×
520
                        return err
×
521
                }
×
522
                recoveryWindow = int32(window)
×
UNCOV
523
        }
×
UNCOV
524

×
525
        req := &lnrpc.UnlockWalletRequest{
526
                WalletPassword: pw,
527
                RecoveryWindow: recoveryWindow,
528
                StatelessInit:  ctx.Bool(statelessInitFlag.Name),
529
        }
×
530
        _, err = client.UnlockWallet(ctxc, req)
×
531
        if err != nil {
532
                return err
×
533
        }
×
UNCOV
534

×
535
        fmt.Println("\nlnd successfully unlocked!")
536

×
537
        // TODO(roasbeef): add ability to accept hex single and multi backups
×
538

×
539
        return nil
×
UNCOV
540
}
×
UNCOV
541

×
UNCOV
542
var changePasswordCommand = cli.Command{
×
UNCOV
543
        Name:     "changepassword",
×
UNCOV
544
        Category: "Startup",
×
UNCOV
545
        Usage:    "Change an encrypted wallet's password at startup.",
×
UNCOV
546
        Description: `
×
UNCOV
547
        The changepassword command is used to Change lnd's encrypted wallet's
×
UNCOV
548
        password. It will automatically unlock the daemon if the password change
×
UNCOV
549
        is successful.
×
UNCOV
550

×
551
        If one did not specify a password for their wallet (running lnd with
552
        --noseedbackup), one must restart their daemon without
UNCOV
553
        --noseedbackup and use this command. The "current password" field
×
UNCOV
554
        should be left empty.
×
UNCOV
555

×
UNCOV
556
        If the daemon was originally initialized stateless, then the
×
UNCOV
557
        --stateless_init flag needs to be set for the change password request
×
UNCOV
558
        as well! Otherwise the daemon will generate unencrypted macaroon files
×
UNCOV
559
        in its file system again and possibly leak sensitive information.
×
UNCOV
560
        Changing the password will by default not change the macaroon root key
×
UNCOV
561
        (just re-encrypt the macaroon database with the new password). So all
×
562
        macaroons will still be valid.
UNCOV
563
        If one wants to make sure that all previously created macaroons are
×
UNCOV
564
        invalidated, a new macaroon root key can be generated by using the
×
UNCOV
565
        --new_mac_root_key flag.
×
UNCOV
566

×
UNCOV
567
        After a successful password change with the --stateless_init flag set,
×
568
        the current or new admin macaroon is returned binary serialized in the
569
        answer. This answer MUST then be stored somewhere, otherwise
570
        all access to the RPC server will be lost and the wallet must be re-
571
        created to re-gain access. If the --save_to parameter is set, the
572
        macaroon is saved to this file, otherwise it is printed to standard out.
573
        `,
574
        Flags: []cli.Flag{
575
                statelessInitFlag,
576
                saveToFlag,
577
                cli.BoolFlag{
578
                        Name: "new_mac_root_key",
579
                        Usage: "rotate the macaroon root key resulting in " +
580
                                "all previously created macaroons to be " +
581
                                "invalidated",
582
                },
583
        },
584
        Action: actionDecorator(changePassword),
585
}
586

587
func changePassword(ctx *cli.Context) error {
588
        ctxc := getContext()
589
        client, cleanUp := getWalletUnlockerClient(ctx)
590
        defer cleanUp()
591

592
        currentPw, err := readPassword("Input current wallet password: ")
593
        if err != nil {
594
                return err
595
        }
596

597
        newPw, err := readPassword("Input new wallet password: ")
598
        if err != nil {
599
                return err
600
        }
601

602
        confirmPw, err := readPassword("Confirm new wallet password: ")
603
        if err != nil {
604
                return err
605
        }
606

607
        if !bytes.Equal(newPw, confirmPw) {
608
                return fmt.Errorf("passwords don't match")
609
        }
610

611
        // Should the daemon be initialized stateless? Then we expect an answer
612
        // with the admin macaroon later. Because the --save_to is related to
613
        // stateless init, it doesn't make sense to be set on its own.
614
        statelessInit := ctx.Bool(statelessInitFlag.Name)
615
        if !statelessInit && ctx.IsSet(saveToFlag.Name) {
×
616
                return fmt.Errorf("cannot set save_to parameter without " +
×
617
                        "stateless_init")
×
618
        }
×
UNCOV
619

×
620
        req := &lnrpc.ChangePasswordRequest{
×
621
                CurrentPassword:    currentPw,
×
622
                NewPassword:        newPw,
×
623
                StatelessInit:      statelessInit,
×
624
                NewMacaroonRootKey: ctx.Bool("new_mac_root_key"),
625
        }
×
626

×
627
        response, err := client.ChangePassword(ctxc, req)
×
628
        if err != nil {
×
629
                return err
630
        }
×
UNCOV
631

×
632
        if statelessInit {
×
633
                return storeOrPrintAdminMac(ctx, response.AdminMacaroon)
×
634
        }
UNCOV
635

×
636
        return nil
×
UNCOV
637
}
×
638

639
var createWatchOnlyCommand = cli.Command{
640
        Name:      "createwatchonly",
641
        Category:  "Startup",
UNCOV
642
        ArgsUsage: "accounts-json-file",
×
UNCOV
643
        Usage: "Initialize a watch-only wallet after starting lnd for the " +
×
UNCOV
644
                "first time.",
×
UNCOV
645
        Description: `
×
UNCOV
646
        The create command is used to initialize an lnd wallet from scratch for
×
647
        the very first time, in watch-only mode. Watch-only means, there will be
UNCOV
648
        no private keys in lnd's wallet. This is only useful in combination with
×
UNCOV
649
        a remote signer or when lnd should be used as an on-chain wallet with
×
UNCOV
650
        PSBT interaction only.
×
UNCOV
651

×
UNCOV
652
        This is an interactive command that takes a JSON file as its first and
×
UNCOV
653
        only argument. The JSON is in the same format as the output of the
×
UNCOV
654
        'lncli wallet accounts list' command. This makes it easy to initialize
×
UNCOV
655
        the remote signer with the seed, then export the extended public account
×
UNCOV
656
        keys (xpubs) to import the watch-only wallet.
×
UNCOV
657

×
UNCOV
658
        Example JSON (non-mandatory or ignored fields are omitted):
×
659
        {
UNCOV
660
            "accounts": [
×
UNCOV
661
                {
×
UNCOV
662
                    "extended_public_key": "upub5Eep7....",
×
663
                    "derivation_path": "m/49'/0'/0'"
UNCOV
664
                },
×
665
                {
666
                    "extended_public_key": "vpub5ZU1PH...",
667
                    "derivation_path": "m/84'/0'/0'"
668
                },
669
                {
670
                    "extended_public_key": "tpubDDXFH...",
671
                    "derivation_path": "m/1017'/1'/0'"
672
                },
673
                ...
674
                {
675
                    "extended_public_key": "tpubDDXFH...",
676
                    "derivation_path": "m/1017'/1'/9'"
677
                }
678
           ]
679
        }
680

681
        There must be an account for each of the existing key families that lnd
682
        uses internally (currently 0-9, see keychain/derivation.go).
683

684
        Read the documentation under docs/remote-signing.md for more information
685
        on how to set up a remote signing node over RPC.
686
        `,
687
        Flags: []cli.Flag{
688
                statelessInitFlag,
689
                saveToFlag,
690
        },
691
        Action: actionDecorator(createWatchOnly),
692
}
693

694
func createWatchOnly(ctx *cli.Context) error {
695
        ctxc := getContext()
696
        client, cleanUp := getWalletUnlockerClient(ctx)
697
        defer cleanUp()
698

699
        if ctx.NArg() != 1 {
700
                return cli.ShowCommandHelp(ctx, "createwatchonly")
701
        }
702

703
        // Should the daemon be initialized stateless? Then we expect an answer
704
        // with the admin macaroon later. Because the --save_to is related to
705
        // stateless init, it doesn't make sense to be set on its own.
706
        statelessInit := ctx.Bool(statelessInitFlag.Name)
707
        if !statelessInit && ctx.IsSet(saveToFlag.Name) {
708
                return fmt.Errorf("cannot set save_to parameter without " +
709
                        "stateless_init")
710
        }
711

712
        jsonFile := lncfg.CleanAndExpandPath(ctx.Args().First())
713
        jsonBytes, err := os.ReadFile(jsonFile)
714
        if err != nil {
715
                return fmt.Errorf("error reading JSON from file %v: %v",
716
                        jsonFile, err)
717
        }
718

719
        jsonAccts := &walletrpc.ListAccountsResponse{}
720
        err = lnrpc.ProtoJSONUnmarshalOpts.Unmarshal(jsonBytes, jsonAccts)
721
        if err != nil {
722
                return fmt.Errorf("error parsing JSON: %w", err)
723
        }
×
724
        if len(jsonAccts.Accounts) == 0 {
×
725
                return fmt.Errorf("cannot import empty account list")
×
726
        }
×
UNCOV
727

×
728
        walletPassword, err := capturePassword(
×
729
                "Input wallet password: ", false,
×
730
                walletunlocker.ValidatePassword,
×
731
        )
732
        if err != nil {
733
                return err
734
        }
UNCOV
735

×
736
        extendedRootKeyBirthday, err := askBirthdayTimestamp()
×
737
        if err != nil {
×
738
                return err
×
739
        }
×
740

741
        recoveryWindow, err := askRecoveryWindow()
×
742
        if err != nil {
×
743
                return err
×
744
        }
×
UNCOV
745

×
746
        rpcAccounts, err := walletrpc.AccountsToWatchOnly(jsonAccts.Accounts)
×
747
        if err != nil {
748
                return err
×
749
        }
×
UNCOV
750

×
751
        rpcResp := &lnrpc.WatchOnly{
×
752
                MasterKeyBirthdayTimestamp: extendedRootKeyBirthday,
×
753
                Accounts:                   rpcAccounts,
×
754
        }
×
755

×
756
        // We assume that all accounts were exported from the same master root
757
        // key. So if one is set, we just forward that. If other accounts should
×
758
        // be watched later on, they should be imported into the watch-only
×
759
        // node, that then also forwards the import request to the remote
×
760
        // signer.
×
761
        for _, acct := range jsonAccts.Accounts {
×
762
                if len(acct.MasterKeyFingerprint) > 0 {
×
763
                        rpcResp.MasterKeyFingerprint = acct.MasterKeyFingerprint
×
764
                }
UNCOV
765
        }
×
UNCOV
766

×
767
        initResp, err := client.InitWallet(ctxc, &lnrpc.InitWalletRequest{
×
768
                WalletPassword: walletPassword,
×
769
                WatchOnly:      rpcResp,
770
                RecoveryWindow: recoveryWindow,
×
771
                StatelessInit:  statelessInit,
×
772
        })
×
773
        if err != nil {
×
774
                return err
775
        }
×
UNCOV
776

×
777
        if statelessInit {
×
778
                return storeOrPrintAdminMac(ctx, initResp.AdminMacaroon)
×
779
        }
UNCOV
780

×
781
        return nil
×
UNCOV
782
}
×
UNCOV
783

×
UNCOV
784
// storeOrPrintAdminMac either stores the admin macaroon to a file specified or
×
UNCOV
785
// prints it to standard out, depending on the user flags set.
×
786
func storeOrPrintAdminMac(ctx *cli.Context, adminMac []byte) error {
×
787
        // The user specified the optional --save_to parameter. We'll save the
×
788
        // macaroon to that file.
×
789
        if ctx.IsSet(saveToFlag.Name) {
×
790
                macSavePath := lncfg.CleanAndExpandPath(ctx.String(
×
791
                        saveToFlag.Name,
×
792
                ))
×
793
                err := os.WriteFile(macSavePath, adminMac, 0644)
×
794
                if err != nil {
795
                        _ = os.Remove(macSavePath)
796
                        return err
797
                }
×
798
                fmt.Printf("Admin macaroon saved to %s\n", macSavePath)
×
799
                return nil
×
UNCOV
800
        }
×
UNCOV
801

×
UNCOV
802
        // Otherwise we just print it. The user MUST store this macaroon
×
UNCOV
803
        // somewhere so we either save it to a provided file path or just print
×
UNCOV
804
        // it to standard output.
×
805
        fmt.Printf("Admin macaroon: %s\n", hex.EncodeToString(adminMac))
×
806
        return nil
UNCOV
807
}
×
UNCOV
808

×
809
func askRecoveryWindow() (int32, error) {
×
810
        for {
×
811
                fmt.Println()
×
812
                fmt.Printf("Input an optional address look-ahead used to scan "+
813
                        "for used keys (default %d): ", defaultRecoveryWindow)
814

×
815
                reader := bufio.NewReader(os.Stdin)
×
816
                answer, err := reader.ReadString('\n')
×
817
                if err != nil {
×
818
                        return 0, err
×
819
                }
×
UNCOV
820

×
821
                fmt.Println()
×
822

×
823
                answer = strings.TrimSpace(answer)
×
824

825
                if len(answer) == 0 {
×
826
                        return defaultRecoveryWindow, nil
×
827
                }
×
828

829
                lookAhead, err := strconv.ParseInt(answer, 10, 32)
×
830
                if err != nil {
831
                        fmt.Printf("Unable to parse recovery window: %v\n", err)
832
                        continue
833
                }
UNCOV
834

×
835
                return int32(lookAhead), nil
×
UNCOV
836
        }
×
UNCOV
837
}
×
UNCOV
838

×
839
func askBirthdayTimestamp() (uint64, error) {
×
840
        for {
×
841
                fmt.Println()
×
842
                fmt.Printf("Input an optional wallet birthday unix timestamp " +
×
843
                        "of first block to start scanning from (default 0): ")
×
844

×
845
                reader := bufio.NewReader(os.Stdin)
×
846
                answer, err := reader.ReadString('\n')
×
847
                if err != nil {
×
848
                        return 0, err
849
                }
850

851
                fmt.Println()
852

853
                answer = strings.TrimSpace(answer)
×
854

×
855
                if len(answer) == 0 {
856
                        return 0, nil
857
                }
×
UNCOV
858

×
859
                birthdayTimestamp, err := strconv.ParseUint(answer, 10, 64)
×
860
                if err != nil {
×
861
                        fmt.Printf("Unable to parse birthday timestamp: %v\n",
×
862
                                err)
×
863

×
864
                        continue
×
UNCOV
865
                }
×
UNCOV
866

×
867
                return birthdayTimestamp, nil
×
868
        }
UNCOV
869
}
×
UNCOV
870

×
871
func printCipherSeedWords(mnemonicWords []string) {
×
872
        fmt.Println("!!!YOU MUST WRITE DOWN THIS SEED TO BE ABLE TO " +
×
873
                "RESTORE THE WALLET!!!")
×
874
        fmt.Println()
×
875

×
876
        fmt.Println("---------------BEGIN LND CIPHER SEED---------------")
877

×
878
        numCols := 4
×
879
        colWords := monowidthColumns(mnemonicWords, numCols)
×
880
        for i := 0; i < len(colWords); i += numCols {
×
881
                fmt.Printf("%2d. %3s  %2d. %3s  %2d. %3s  %2d. %3s\n",
882
                        i+1, colWords[i], i+2, colWords[i+1], i+3,
883
                        colWords[i+2], i+4, colWords[i+3])
×
884
        }
885

886
        fmt.Println("---------------END LND CIPHER SEED-----------------")
887

×
888
        fmt.Println("\n!!!YOU MUST WRITE DOWN THIS SEED TO BE ABLE TO " +
×
889
                "RESTORE THE WALLET!!!")
×
UNCOV
890
}
×
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