Skip to content

Commit 997d86c

Browse files
authored
Merge pull request #124 from lightninglabs/createwallet
Add `createwallet` and `signpsbt` subcommands
2 parents 676ba60 + 71b824e commit 997d86c

38 files changed

+946
-223
lines changed

README.md

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -410,6 +410,7 @@ Available Commands:
410410
chanbackup Create a channel.backup file from a channel database
411411
closepoolaccount Tries to close a Pool account that has expired
412412
compactdb Create a copy of a channel.db file in safe/read-only mode
413+
createwallet Create a new lnd compatible wallet.db file from an existing seed or by generating a new one
413414
deletepayments Remove all (failed) payments from a channel DB
414415
derivekey Derive a key with a specific derivation path
415416
doublespendinputs Tries to double spend the given inputs by deriving the private for the address and sweeping the funds to the given address. This can only be used with inputs that belong to an lnd wallet.
@@ -430,6 +431,7 @@ Available Commands:
430431
rescuetweakedkey Attempt to rescue funds locked in an address with a key that was affected by a specific bug in lnd
431432
showrootkey Extract and show the BIP32 HD root key from the 24 word lnd aezeed
432433
signmessage Sign a message with the nodes identity pubkey.
434+
signpsbt Sign a Partially Signed Bitcoin Transaction (PSBT)
433435
signrescuefunding Rescue funds locked in a funding multisig output that never resulted in a proper channel; this is the command the remote node (the non-initiator) of the channel needs to run
434436
summary Compile a summary about the current state of channels
435437
sweeptimelock Sweep the force-closed state after the time lock has expired
@@ -470,6 +472,7 @@ Legend:
470472
| [chanbackup](doc/chantools_chanbackup.md) | :pencil: Extract a `channel.backup` file from a `channel.db` file |
471473
| [closepoolaccount](doc/chantools_closepoolaccount.md) | :pencil: Manually close an expired Lightning Pool account |
472474
| [compactdb](doc/chantools_compactdb.md) | Run database compaction manually to reclaim space |
475+
| [createwallet](doc/chantools_createwallet.md) | :pencil: Create a new lnd compatible wallet.db file from an existing seed or by generating a new one |
473476
| [deletepayments](doc/chantools_deletepayments.md) | Remove ALL payments from a `channel.db` file to reduce size |
474477
| [derivekey](doc/chantools_derivekey.md) | :pencil: Derive a single private/public key from `lnd`'s seed, use to test seed |
475478
| [doublespendinputs](doc/chantools_doublespendinputs.md) | :pencil: Tries to double spend the given inputs by deriving the private for the address and sweeping the funds to the given address |
@@ -483,13 +486,14 @@ Legend:
483486
| [forceclose](doc/chantools_forceclose.md) | :pencil: (:skull: :warning:) Publish an old channel state from a `channel.db` file |
484487
| [genimportscript](doc/chantools_genimportscript.md) | :pencil: Create a script/text file that can be used to import `lnd` keys into other software |
485488
| [migratedb](doc/chantools_migratedb.md) | Upgrade the `channel.db` file to the latest version |
486-
| [pullanchor](doc/chantools_pullanchor.md) | :pencil: Attempt to CPFP an anchor output of a channel |
489+
| [pullanchor](doc/chantools_pullanchor.md) | :pencil: Attempt to CPFP an anchor output of a channel |
487490
| [recoverloopin](doc/chantools_recoverloopin.md) | :pencil: Recover funds from a failed Lightning Loop inbound swap |
488491
| [removechannel](doc/chantools_removechannel.md) | (:skull: :warning:) Remove a single channel from a `channel.db` file |
489492
| [rescueclosed](doc/chantools_rescueclosed.md) | :pencil: (:pushpin:) Rescue funds in a legacy (pre `STATIC_REMOTE_KEY`) channel output |
490493
| [rescuefunding](doc/chantools_rescuefunding.md) | :pencil: (:pushpin:) Rescue funds from a funding transaction. Deprecated, use [zombierecovery](doc/chantools_zombierecovery.md) instead |
491494
| [showrootkey](doc/chantools_showrootkey.md) | :pencil: Display the master root key (`xprv`) from your seed (DO NOT SHARE WITH ANYONE) |
492495
| [signmessage](doc/chantools_signmessage.md) | :pencil: Sign a message with the nodes identity pubkey. |
496+
| [signpsbt](doc/chantools_signpsbt.md) | :pencil: Sign a Partially Signed Bitcoin Transaction (PSBT) |
493497
| [signrescuefunding](doc/chantools_signrescuefunding.md) | :pencil: (:pushpin:) Sign to funds from a funding transaction. Deprecated, use [zombierecovery](doc/chantools_zombierecovery.md) instead |
494498
| [summary](doc/chantools_summary.md) | Create a summary of channel funds from a `channel.db` file |
495499
| [sweepremoteclosed](doc/chantools_sweepremoteclosed.md) | :pencil: Find channel funds from remotely force closed channels and sweep them |

cmd/chantools/createwallet.go

Lines changed: 232 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,232 @@
1+
package main
2+
3+
import (
4+
"bytes"
5+
"fmt"
6+
"os"
7+
"strings"
8+
"time"
9+
10+
"github.com/btcsuite/btcd/btcutil/hdkeychain"
11+
_ "github.com/btcsuite/btcwallet/walletdb/bdb"
12+
"github.com/lightninglabs/chantools/lnd"
13+
"github.com/lightningnetwork/lnd/aezeed"
14+
"github.com/lightningnetwork/lnd/keychain"
15+
"github.com/lightningnetwork/lnd/lnwallet"
16+
"github.com/lightningnetwork/lnd/lnwallet/btcwallet"
17+
"github.com/spf13/cobra"
18+
)
19+
20+
type createWalletCommand struct {
21+
WalletDBDir string
22+
GenerateSeed bool
23+
24+
rootKey *rootKey
25+
cmd *cobra.Command
26+
}
27+
28+
func newCreateWalletCommand() *cobra.Command {
29+
cc := &createWalletCommand{}
30+
cc.cmd = &cobra.Command{
31+
Use: "createwallet",
32+
Short: "Create a new lnd compatible wallet.db file from an " +
33+
"existing seed or by generating a new one",
34+
Long: `Creates a new wallet that can be used with lnd or with
35+
chantools. The wallet can be created from an existing seed or a new one can be
36+
generated (use --generateseed).`,
37+
Example: `chantools createwallet \
38+
--walletdbdir ~/.lnd/data/chain/bitcoin/mainnet`,
39+
RunE: cc.Execute,
40+
}
41+
cc.cmd.Flags().StringVar(
42+
&cc.WalletDBDir, "walletdbdir", "", "the folder to create the "+
43+
"new wallet.db file in",
44+
)
45+
cc.cmd.Flags().BoolVar(
46+
&cc.GenerateSeed, "generateseed", false, "generate a new "+
47+
"seed instead of using an existing one",
48+
)
49+
50+
cc.rootKey = newRootKey(cc.cmd, "creating the new wallet")
51+
52+
return cc.cmd
53+
}
54+
55+
func (c *createWalletCommand) Execute(_ *cobra.Command, _ []string) error {
56+
var (
57+
publicWalletPw = lnwallet.DefaultPublicPassphrase
58+
privateWalletPw = lnwallet.DefaultPrivatePassphrase
59+
masterRootKey *hdkeychain.ExtendedKey
60+
birthday time.Time
61+
err error
62+
)
63+
64+
// Check that we have a wallet DB.
65+
if c.WalletDBDir == "" {
66+
return fmt.Errorf("wallet DB directory is required")
67+
}
68+
69+
// Make sure the directory (and parents) exists.
70+
if err := os.MkdirAll(c.WalletDBDir, 0700); err != nil {
71+
return fmt.Errorf("error creating wallet DB directory '%s': %w",
72+
c.WalletDBDir, err)
73+
}
74+
75+
// Check if we should create a new seed or read if from the console or
76+
// environment.
77+
if c.GenerateSeed {
78+
fmt.Printf("Generating new lnd compatible aezeed...\n")
79+
seed, err := aezeed.New(
80+
keychain.KeyDerivationVersionTaproot, nil, time.Now(),
81+
)
82+
if err != nil {
83+
return fmt.Errorf("error creating new seed: %w", err)
84+
}
85+
birthday = seed.BirthdayTime()
86+
87+
// Derive the master extended key from the seed.
88+
masterRootKey, err = hdkeychain.NewMaster(
89+
seed.Entropy[:], chainParams,
90+
)
91+
if err != nil {
92+
return fmt.Errorf("failed to derive master extended "+
93+
"key: %w", err)
94+
}
95+
96+
passphrase, err := lnd.ReadPassphrase("shouldn't use")
97+
if err != nil {
98+
return fmt.Errorf("error reading passphrase: %w", err)
99+
}
100+
101+
mnemonic, err := seed.ToMnemonic(passphrase)
102+
if err != nil {
103+
return fmt.Errorf("error converting seed to "+
104+
"mnemonic: %w", err)
105+
}
106+
107+
fmt.Println("Generated new seed")
108+
printCipherSeedWords(mnemonic[:])
109+
} else {
110+
masterRootKey, birthday, err = c.rootKey.readWithBirthday()
111+
if err != nil {
112+
return err
113+
}
114+
}
115+
116+
// To automate things with chantools, we also offer reading the wallet
117+
// password from environment variables.
118+
pw := []byte(strings.TrimSpace(os.Getenv(lnd.PasswordEnvName)))
119+
120+
// Because we cannot differentiate between an empty and a non-existent
121+
// environment variable, we need a special character that indicates that
122+
// no password should be used. We use a single dash (-) for that as that
123+
// would be too short for an explicit password anyway.
124+
switch {
125+
// The user indicated in the environment variable that no passphrase
126+
// should be used. We don't set any value.
127+
case string(pw) == "-":
128+
129+
// The environment variable didn't contain anything, we'll read the
130+
// passphrase from the terminal.
131+
case len(pw) == 0:
132+
fmt.Printf("\n\nThe wallet password is used to encrypt the " +
133+
"wallet.db file itself and is unrelated to the seed.\n")
134+
pw, err = lnd.PasswordFromConsole("Input new wallet password: ")
135+
if err != nil {
136+
return err
137+
}
138+
pw2, err := lnd.PasswordFromConsole(
139+
"Confirm new wallet password: ",
140+
)
141+
if err != nil {
142+
return err
143+
}
144+
145+
if !bytes.Equal(pw, pw2) {
146+
return fmt.Errorf("passwords don't match")
147+
}
148+
149+
if len(pw) > 0 {
150+
publicWalletPw = pw
151+
privateWalletPw = pw
152+
}
153+
154+
// There was a password in the environment, just use it directly.
155+
default:
156+
publicWalletPw = pw
157+
privateWalletPw = pw
158+
}
159+
160+
// Try to create the wallet.
161+
loader, err := btcwallet.NewWalletLoader(
162+
chainParams, 0, btcwallet.LoaderWithLocalWalletDB(
163+
c.WalletDBDir, true, 0,
164+
),
165+
)
166+
if err != nil {
167+
return fmt.Errorf("error creating wallet loader: %w", err)
168+
}
169+
170+
_, err = loader.CreateNewWalletExtendedKey(
171+
publicWalletPw, privateWalletPw, masterRootKey, birthday,
172+
)
173+
if err != nil {
174+
return fmt.Errorf("error creating new wallet: %w", err)
175+
}
176+
177+
if err := loader.UnloadWallet(); err != nil {
178+
return fmt.Errorf("error unloading wallet: %w", err)
179+
}
180+
181+
fmt.Printf("Wallet created successfully at %v\n", c.WalletDBDir)
182+
183+
return nil
184+
}
185+
186+
func printCipherSeedWords(mnemonicWords []string) {
187+
fmt.Println("!!!YOU MUST WRITE DOWN THIS SEED TO BE ABLE TO " +
188+
"RESTORE THE WALLET!!!")
189+
fmt.Println()
190+
191+
fmt.Println("---------------BEGIN LND CIPHER SEED---------------")
192+
193+
numCols := 4
194+
colWords := monoWidthColumns(mnemonicWords, numCols)
195+
for i := 0; i < len(colWords); i += numCols {
196+
fmt.Printf("%2d. %3s %2d. %3s %2d. %3s %2d. %3s\n",
197+
i+1, colWords[i], i+2, colWords[i+1], i+3,
198+
colWords[i+2], i+4, colWords[i+3])
199+
}
200+
201+
fmt.Println("---------------END LND CIPHER SEED-----------------")
202+
203+
fmt.Println("\n!!!YOU MUST WRITE DOWN THIS SEED TO BE ABLE TO " +
204+
"RESTORE THE WALLET!!!")
205+
}
206+
207+
// monoWidthColumns takes a set of words, and the number of desired columns,
208+
// and returns a new set of words that have had white space appended to the
209+
// word in order to create a mono-width column.
210+
func monoWidthColumns(words []string, ncols int) []string {
211+
// Determine max size of words in each column.
212+
colWidths := make([]int, ncols)
213+
for i, word := range words {
214+
col := i % ncols
215+
curWidth := colWidths[col]
216+
if len(word) > curWidth {
217+
colWidths[col] = len(word)
218+
}
219+
}
220+
221+
// Append whitespace to each word to make columns mono-width.
222+
finalWords := make([]string, len(words))
223+
for i, word := range words {
224+
col := i % ncols
225+
width := colWidths[col]
226+
227+
diff := width - len(word)
228+
finalWords[i] = word + strings.Repeat(" ", diff)
229+
}
230+
231+
return finalWords
232+
}

cmd/chantools/rescuefunding.go

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -209,7 +209,7 @@ func (c *rescueFundingCommand) Execute(_ *cobra.Command, _ []string) error {
209209
Index: c.LocalKeyIndex,
210210
},
211211
}
212-
privKey, err := signer.FetchPrivKey(localKeyDesc)
212+
privKey, err := signer.FetchPrivateKey(localKeyDesc)
213213
if err != nil {
214214
return fmt.Errorf("error deriving local key: %w", err)
215215
}

0 commit comments

Comments
 (0)