Merge pull request #124 from lightninglabs/createwallet

Add `createwallet` and `signpsbt` subcommands
pull/127/head
Oliver Gugger 1 month ago committed by GitHub
commit 997d86cd84
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194

@ -410,6 +410,7 @@ Available Commands:
chanbackup Create a channel.backup file from a channel database
closepoolaccount Tries to close a Pool account that has expired
compactdb Create a copy of a channel.db file in safe/read-only mode
createwallet Create a new lnd compatible wallet.db file from an existing seed or by generating a new one
deletepayments Remove all (failed) payments from a channel DB
derivekey Derive a key with a specific derivation path
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:
rescuetweakedkey Attempt to rescue funds locked in an address with a key that was affected by a specific bug in lnd
showrootkey Extract and show the BIP32 HD root key from the 24 word lnd aezeed
signmessage Sign a message with the nodes identity pubkey.
signpsbt Sign a Partially Signed Bitcoin Transaction (PSBT)
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
summary Compile a summary about the current state of channels
sweeptimelock Sweep the force-closed state after the time lock has expired
@ -470,6 +472,7 @@ Legend:
| [chanbackup](doc/chantools_chanbackup.md) | :pencil: Extract a `channel.backup` file from a `channel.db` file |
| [closepoolaccount](doc/chantools_closepoolaccount.md) | :pencil: Manually close an expired Lightning Pool account |
| [compactdb](doc/chantools_compactdb.md) | Run database compaction manually to reclaim space |
| [createwallet](doc/chantools_createwallet.md) | :pencil: Create a new lnd compatible wallet.db file from an existing seed or by generating a new one |
| [deletepayments](doc/chantools_deletepayments.md) | Remove ALL payments from a `channel.db` file to reduce size |
| [derivekey](doc/chantools_derivekey.md) | :pencil: Derive a single private/public key from `lnd`'s seed, use to test seed |
| [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:
| [forceclose](doc/chantools_forceclose.md) | :pencil: (:skull: :warning:) Publish an old channel state from a `channel.db` file |
| [genimportscript](doc/chantools_genimportscript.md) | :pencil: Create a script/text file that can be used to import `lnd` keys into other software |
| [migratedb](doc/chantools_migratedb.md) | Upgrade the `channel.db` file to the latest version |
| [pullanchor](doc/chantools_pullanchor.md) | :pencil: Attempt to CPFP an anchor output of a channel |
| [pullanchor](doc/chantools_pullanchor.md) | :pencil: Attempt to CPFP an anchor output of a channel |
| [recoverloopin](doc/chantools_recoverloopin.md) | :pencil: Recover funds from a failed Lightning Loop inbound swap |
| [removechannel](doc/chantools_removechannel.md) | (:skull: :warning:) Remove a single channel from a `channel.db` file |
| [rescueclosed](doc/chantools_rescueclosed.md) | :pencil: (:pushpin:) Rescue funds in a legacy (pre `STATIC_REMOTE_KEY`) channel output |
| [rescuefunding](doc/chantools_rescuefunding.md) | :pencil: (:pushpin:) Rescue funds from a funding transaction. Deprecated, use [zombierecovery](doc/chantools_zombierecovery.md) instead |
| [showrootkey](doc/chantools_showrootkey.md) | :pencil: Display the master root key (`xprv`) from your seed (DO NOT SHARE WITH ANYONE) |
| [signmessage](doc/chantools_signmessage.md) | :pencil: Sign a message with the nodes identity pubkey. |
| [signpsbt](doc/chantools_signpsbt.md) | :pencil: Sign a Partially Signed Bitcoin Transaction (PSBT) |
| [signrescuefunding](doc/chantools_signrescuefunding.md) | :pencil: (:pushpin:) Sign to funds from a funding transaction. Deprecated, use [zombierecovery](doc/chantools_zombierecovery.md) instead |
| [summary](doc/chantools_summary.md) | Create a summary of channel funds from a `channel.db` file |
| [sweepremoteclosed](doc/chantools_sweepremoteclosed.md) | :pencil: Find channel funds from remotely force closed channels and sweep them |

@ -0,0 +1,232 @@
package main
import (
"bytes"
"fmt"
"os"
"strings"
"time"
"github.com/btcsuite/btcd/btcutil/hdkeychain"
_ "github.com/btcsuite/btcwallet/walletdb/bdb"
"github.com/lightninglabs/chantools/lnd"
"github.com/lightningnetwork/lnd/aezeed"
"github.com/lightningnetwork/lnd/keychain"
"github.com/lightningnetwork/lnd/lnwallet"
"github.com/lightningnetwork/lnd/lnwallet/btcwallet"
"github.com/spf13/cobra"
)
type createWalletCommand struct {
WalletDBDir string
GenerateSeed bool
rootKey *rootKey
cmd *cobra.Command
}
func newCreateWalletCommand() *cobra.Command {
cc := &createWalletCommand{}
cc.cmd = &cobra.Command{
Use: "createwallet",
Short: "Create a new lnd compatible wallet.db file from an " +
"existing seed or by generating a new one",
Long: `Creates a new wallet that can be used with lnd or with
chantools. The wallet can be created from an existing seed or a new one can be
generated (use --generateseed).`,
Example: `chantools createwallet \
--walletdbdir ~/.lnd/data/chain/bitcoin/mainnet`,
RunE: cc.Execute,
}
cc.cmd.Flags().StringVar(
&cc.WalletDBDir, "walletdbdir", "", "the folder to create the "+
"new wallet.db file in",
)
cc.cmd.Flags().BoolVar(
&cc.GenerateSeed, "generateseed", false, "generate a new "+
"seed instead of using an existing one",
)
cc.rootKey = newRootKey(cc.cmd, "creating the new wallet")
return cc.cmd
}
func (c *createWalletCommand) Execute(_ *cobra.Command, _ []string) error {
var (
publicWalletPw = lnwallet.DefaultPublicPassphrase
privateWalletPw = lnwallet.DefaultPrivatePassphrase
masterRootKey *hdkeychain.ExtendedKey
birthday time.Time
err error
)
// Check that we have a wallet DB.
if c.WalletDBDir == "" {
return fmt.Errorf("wallet DB directory is required")
}
// Make sure the directory (and parents) exists.
if err := os.MkdirAll(c.WalletDBDir, 0700); err != nil {
return fmt.Errorf("error creating wallet DB directory '%s': %w",
c.WalletDBDir, err)
}
// Check if we should create a new seed or read if from the console or
// environment.
if c.GenerateSeed {
fmt.Printf("Generating new lnd compatible aezeed...\n")
seed, err := aezeed.New(
keychain.KeyDerivationVersionTaproot, nil, time.Now(),
)
if err != nil {
return fmt.Errorf("error creating new seed: %w", err)
}
birthday = seed.BirthdayTime()
// Derive the master extended key from the seed.
masterRootKey, err = hdkeychain.NewMaster(
seed.Entropy[:], chainParams,
)
if err != nil {
return fmt.Errorf("failed to derive master extended "+
"key: %w", err)
}
passphrase, err := lnd.ReadPassphrase("shouldn't use")
if err != nil {
return fmt.Errorf("error reading passphrase: %w", err)
}
mnemonic, err := seed.ToMnemonic(passphrase)
if err != nil {
return fmt.Errorf("error converting seed to "+
"mnemonic: %w", err)
}
fmt.Println("Generated new seed")
printCipherSeedWords(mnemonic[:])
} else {
masterRootKey, birthday, err = c.rootKey.readWithBirthday()
if err != nil {
return err
}
}
// To automate things with chantools, we also offer reading the wallet
// password from environment variables.
pw := []byte(strings.TrimSpace(os.Getenv(lnd.PasswordEnvName)))
// Because we cannot differentiate between an empty and a non-existent
// environment variable, we need a special character that indicates that
// no password should be used. We use a single dash (-) for that as that
// would be too short for an explicit password anyway.
switch {
// The user indicated in the environment variable that no passphrase
// should be used. We don't set any value.
case string(pw) == "-":
// The environment variable didn't contain anything, we'll read the
// passphrase from the terminal.
case len(pw) == 0:
fmt.Printf("\n\nThe wallet password is used to encrypt the " +
"wallet.db file itself and is unrelated to the seed.\n")
pw, err = lnd.PasswordFromConsole("Input new wallet password: ")
if err != nil {
return err
}
pw2, err := lnd.PasswordFromConsole(
"Confirm new wallet password: ",
)
if err != nil {
return err
}
if !bytes.Equal(pw, pw2) {
return fmt.Errorf("passwords don't match")
}
if len(pw) > 0 {
publicWalletPw = pw
privateWalletPw = pw
}
// There was a password in the environment, just use it directly.
default:
publicWalletPw = pw
privateWalletPw = pw
}
// Try to create the wallet.
loader, err := btcwallet.NewWalletLoader(
chainParams, 0, btcwallet.LoaderWithLocalWalletDB(
c.WalletDBDir, true, 0,
),
)
if err != nil {
return fmt.Errorf("error creating wallet loader: %w", err)
}
_, err = loader.CreateNewWalletExtendedKey(
publicWalletPw, privateWalletPw, masterRootKey, birthday,
)
if err != nil {
return fmt.Errorf("error creating new wallet: %w", err)
}
if err := loader.UnloadWallet(); err != nil {
return fmt.Errorf("error unloading wallet: %w", err)
}
fmt.Printf("Wallet created successfully at %v\n", c.WalletDBDir)
return nil
}
func printCipherSeedWords(mnemonicWords []string) {
fmt.Println("!!!YOU MUST WRITE DOWN THIS SEED TO BE ABLE TO " +
"RESTORE THE WALLET!!!")
fmt.Println()
fmt.Println("---------------BEGIN LND CIPHER SEED---------------")
numCols := 4
colWords := monoWidthColumns(mnemonicWords, numCols)
for i := 0; i < len(colWords); i += numCols {
fmt.Printf("%2d. %3s %2d. %3s %2d. %3s %2d. %3s\n",
i+1, colWords[i], i+2, colWords[i+1], i+3,
colWords[i+2], i+4, colWords[i+3])
}
fmt.Println("---------------END LND CIPHER SEED-----------------")
fmt.Println("\n!!!YOU MUST WRITE DOWN THIS SEED TO BE ABLE TO " +
"RESTORE THE WALLET!!!")
}
// monoWidthColumns takes a set of words, and the number of desired columns,
// and returns a new set of words that have had white space appended to the
// word in order to create a mono-width column.
func monoWidthColumns(words []string, ncols int) []string {
// Determine max size of words in each column.
colWidths := make([]int, ncols)
for i, word := range words {
col := i % ncols
curWidth := colWidths[col]
if len(word) > curWidth {
colWidths[col] = len(word)
}
}
// Append whitespace to each word to make columns mono-width.
finalWords := make([]string, len(words))
for i, word := range words {
col := i % ncols
width := colWidths[col]
diff := width - len(word)
finalWords[i] = word + strings.Repeat(" ", diff)
}
return finalWords
}

@ -209,7 +209,7 @@ func (c *rescueFundingCommand) Execute(_ *cobra.Command, _ []string) error {
Index: c.LocalKeyIndex,
},
}
privKey, err := signer.FetchPrivKey(localKeyDesc)
privKey, err := signer.FetchPrivateKey(localKeyDesc)
if err != nil {
return fmt.Errorf("error deriving local key: %w", err)
}

@ -1,14 +1,12 @@
package main
import (
"bufio"
"bytes"
"encoding/json"
"fmt"
"io/ioutil"
"os"
"strings"
"syscall"
"time"
"github.com/btcsuite/btcd/btcutil/hdkeychain"
@ -22,7 +20,6 @@ import (
"github.com/lightningnetwork/lnd/channeldb"
"github.com/lightningnetwork/lnd/peer"
"github.com/spf13/cobra"
"golang.org/x/crypto/ssh/terminal"
)
const (
@ -33,7 +30,7 @@ const (
// version is the current version of the tool. It is set during build.
// NOTE: When changing this, please also update the version in the
// download link shown in the README.
version = "0.12.2"
version = "0.13.0"
na = "n/a"
// lndVersion is the current version of lnd that we support. This is
@ -101,6 +98,7 @@ func main() {
rootCmd.AddCommand(
newChanBackupCommand(),
newClosePoolAccountCommand(),
newCreateWalletCommand(),
newCompactDBCommand(),
newDeletePaymentsCommand(),
newDeriveKeyCommand(),
@ -125,6 +123,7 @@ func main() {
newShowRootKeyCommand(),
newSignMessageCommand(),
newSignRescueFundingCommand(),
newSignPSBTCommand(),
newSummaryCommand(),
newSweepTimeLockCommand(),
newSweepTimeLockManualCommand(),
@ -142,8 +141,9 @@ func main() {
}
type rootKey struct {
RootKey string
BIP39 bool
RootKey string
BIP39 bool
WalletDB string
}
func newRootKey(cmd *cobra.Command, desc string) *rootKey {
@ -158,6 +158,12 @@ func newRootKey(cmd *cobra.Command, desc string) *rootKey {
"passphrase from the terminal instead of asking for "+
"lnd seed format or providing the --rootkey flag",
)
cmd.Flags().StringVar(
&r.WalletDB, "walletdb", "", "read the seed/master root key "+
"to use fro "+desc+" from an lnd wallet.db file "+
"instead of asking for a seed or providing the "+
"--rootkey flag",
)
return r
}
@ -180,6 +186,39 @@ func (r *rootKey) readWithBirthday() (*hdkeychain.ExtendedKey, time.Time,
extendedKey, err := btc.ReadMnemonicFromTerminal(chainParams)
return extendedKey, time.Unix(0, 0), err
case r.WalletDB != "":
wallet, pw, cleanup, err := lnd.OpenWallet(
r.WalletDB, chainParams,
)
if err != nil {
return nil, time.Unix(0, 0), fmt.Errorf("error "+
"opening wallet '%s': %w", r.WalletDB, err)
}
defer func() {
if err := cleanup(); err != nil {
log.Errorf("error closing wallet: %v", err)
}
}()
extendedKeyBytes, err := lnd.DecryptWalletRootKey(
wallet.Database(), pw,
)
if err != nil {
return nil, time.Unix(0, 0), fmt.Errorf("error "+
"decrypting wallet root key: %w", err)
}
extendedKey, err := hdkeychain.NewKeyFromString(
string(extendedKeyBytes),
)
if err != nil {
return nil, time.Unix(0, 0), fmt.Errorf("error "+
"parsing master key: %w", err)
}
return extendedKey, wallet.Manager.Birthday(), nil
default:
return lnd.ReadAezeed(chainParams)
}
@ -264,27 +303,6 @@ func readInput(input string) ([]byte, error) {
return ioutil.ReadFile(input)
}
func passwordFromConsole(userQuery string) ([]byte, error) {
// Read from terminal (if there is one).
if terminal.IsTerminal(int(syscall.Stdin)) { //nolint
fmt.Print(userQuery)
pw, err := terminal.ReadPassword(int(syscall.Stdin)) //nolint
if err != nil {
return nil, err
}
fmt.Println()
return pw, nil
}
// Read from stdin as a fallback.
reader := bufio.NewReader(os.Stdin)
pw, err := reader.ReadBytes('\n')
if err != nil {
return nil, err
}
return pw, nil
}
func setupLogging() {
setSubLogger("CHAN", log)
addSubLogger("CHDB", channeldb.UseLogger)
@ -327,10 +345,6 @@ func setSubLogger(subsystem string, logger btclog.Logger,
}
}
func noConsole() ([]byte, error) {
return nil, fmt.Errorf("wallet db requires console access")
}
func newExplorerAPI(apiURL string) *btc.ExplorerAPI {
// Override for testnet if default is used.
if apiURL == defaultAPIURL &&

@ -61,7 +61,7 @@ func (c *signMessageCommand) Execute(_ *cobra.Command, _ []string) error {
}
// Fetch the private key for node key.
privKey, err := signer.FetchPrivKey(&keychain.KeyDescriptor{
privKey, err := signer.FetchPrivateKey(&keychain.KeyDescriptor{
KeyLocator: keyLocator,
})
if err != nil {

@ -0,0 +1,217 @@
package main
import (
"bytes"
"encoding/base64"
"encoding/binary"
"fmt"
"os"
"github.com/btcsuite/btcd/btcutil"
"github.com/btcsuite/btcd/btcutil/hdkeychain"
"github.com/btcsuite/btcd/btcutil/psbt"
"github.com/lightninglabs/chantools/lnd"
"github.com/spf13/cobra"
)
type signPSBTCommand struct {
Psbt string
FromRawPsbtFile string
ToRawPsbtFile string
rootKey *rootKey
cmd *cobra.Command
}
func newSignPSBTCommand() *cobra.Command {
cc := &signPSBTCommand{}
cc.cmd = &cobra.Command{
Use: "signpsbt",
Short: "Sign a Partially Signed Bitcoin Transaction (PSBT)",
Long: `Sign a PSBT with a master root key. The PSBT must contain
an input that is owned by the master root key.`,
Example: `chantools signpsbt \
--psbt <the_base64_encoded_psbt>
chantools signpsbt --fromrawpsbtfile <file_with_psbt>`,
RunE: cc.Execute,
}
cc.cmd.Flags().StringVar(
&cc.Psbt, "psbt", "", "Partially Signed Bitcoin Transaction "+
"to sign",
)
cc.cmd.Flags().StringVar(
&cc.FromRawPsbtFile, "fromrawpsbtfile", "", "the file containing "+
"the raw, binary encoded PSBT packet to sign",
)
cc.cmd.Flags().StringVar(
&cc.ToRawPsbtFile, "torawpsbtfile", "", "the file to write "+
"the resulting signed raw, binary encoded PSBT packet "+
"to",
)
cc.rootKey = newRootKey(cc.cmd, "signing the PSBT")
return cc.cmd
}
func (c *signPSBTCommand) Execute(_ *cobra.Command, _ []string) error {
extendedKey, err := c.rootKey.read()
if err != nil {
return fmt.Errorf("error reading root key: %w", err)
}
signer := &lnd.Signer{
ExtendedKey: extendedKey,
ChainParams: chainParams,
}
var packet *psbt.Packet
// Decode the PSBT, either from the command line or the binary file.
switch {
case c.Psbt != "":
packet, err = psbt.NewFromRawBytes(
bytes.NewReader([]byte(c.Psbt)), true,
)
if err != nil {
return fmt.Errorf("error decoding PSBT: %w", err)
}
case c.FromRawPsbtFile != "":
f, err := os.Open(c.FromRawPsbtFile)
if err != nil {
return fmt.Errorf("error opening PSBT file '%s': %w",
c.FromRawPsbtFile, err)
}
packet, err = psbt.NewFromRawBytes(f, false)
if err != nil {
return fmt.Errorf("error decoding PSBT from file "+
"'%s': %w", c.FromRawPsbtFile, err)
}
default:
return fmt.Errorf("either the PSBT or the raw PSBT file " +
"must be set")
}
err = signPsbt(extendedKey, packet, signer)
if err != nil {
return fmt.Errorf("error signing PSBT: %w", err)
}
switch {
case c.ToRawPsbtFile != "":
f, err := os.Create(c.ToRawPsbtFile)
if err != nil {
return fmt.Errorf("error creating PSBT file '%s': %w",
c.ToRawPsbtFile, err)
}
if err := packet.Serialize(f); err != nil {
return fmt.Errorf("error serializing PSBT to file "+
"'%s': %w", c.ToRawPsbtFile, err)
}
fmt.Printf("Successfully signed PSBT and wrote it to file "+
"'%s'\n", c.ToRawPsbtFile)
default:
var buf bytes.Buffer
if err := packet.Serialize(&buf); err != nil {
return fmt.Errorf("error serializing PSBT: %w", err)
}
fmt.Printf("Successfully signed PSBT:\n\n%s\n",
base64.StdEncoding.EncodeToString(buf.Bytes()))
}
return nil
}
func signPsbt(rootKey *hdkeychain.ExtendedKey,
packet *psbt.Packet, signer *lnd.Signer) error {
// Check that we have an input with a derivation path that belongs to
// the root key.
derivationPath, inputIndex, err := findMatchingDerivationPath(
rootKey, packet,
)
if err != nil {
return fmt.Errorf("could not find matching derivation path: %w",
err)
}
if len(derivationPath) < 5 {
return fmt.Errorf("invalid derivation path, expected at least "+
"5 elements, got %d", len(derivationPath))
}
localKey, err := lnd.DeriveChildren(rootKey, derivationPath)
if err != nil {
return fmt.Errorf("could not derive local key: %w", err)
}
if len(packet.Inputs[inputIndex].WitnessScript) == 0 {
return fmt.Errorf("invalid PSBT, input %d is missing witness "+
"script", inputIndex)
}
witnessScript := packet.Inputs[inputIndex].WitnessScript
if packet.Inputs[inputIndex].WitnessUtxo == nil {
return fmt.Errorf("invalid PSBT, input %d is missing witness "+
"UTXO", inputIndex)
}
utxo := packet.Inputs[inputIndex].WitnessUtxo
localPrivateKey, err := localKey.ECPrivKey()
if err != nil {
return fmt.Errorf("error getting private key: %w", err)
}
err = signer.AddPartialSignatureForPrivateKey(
packet, localPrivateKey, utxo, witnessScript, inputIndex,
)
if err != nil {
return fmt.Errorf("error adding partial signature: %w", err)
}
return nil
}
func findMatchingDerivationPath(rootKey *hdkeychain.ExtendedKey,
packet *psbt.Packet) ([]uint32, int, error) {
pubKey, err := rootKey.ECPubKey()
if err != nil {
return nil, 0, fmt.Errorf("error getting public key: %w", err)
}
pubKeyHash := btcutil.Hash160(pubKey.SerializeCompressed())
fingerprint := binary.LittleEndian.Uint32(pubKeyHash[:4])
for idx, input := range packet.Inputs {
if len(input.Bip32Derivation) == 0 {
continue
}
for _, derivation := range input.Bip32Derivation {
// A special case where there is only a single
// derivation path and the master key fingerprint is not
// set, we assume we are the correct signer... This
// might not be correct, but we have no way of knowing.
if derivation.MasterKeyFingerprint == 0 &&
len(input.Bip32Derivation) == 1 {
return derivation.Bip32Path, idx, nil
}
// The normal case, where a derivation path has the
// master fingerprint set.
if derivation.MasterKeyFingerprint == fingerprint {
return derivation.Bip32Path, idx, nil
}
}
}
return nil, 0, fmt.Errorf("no matching derivation path found")
}

@ -1,28 +1,19 @@
package main
import (
"errors"
"fmt"
"os"
"strings"
"github.com/btcsuite/btcd/btcec/v2"
"github.com/btcsuite/btcwallet/snacl"
"github.com/btcsuite/btcwallet/waddrmgr"
"github.com/btcsuite/btcwallet/wallet"
"github.com/btcsuite/btcwallet/walletdb"
_ "github.com/btcsuite/btcwallet/walletdb/bdb"
"github.com/lightninglabs/chantools/lnd"
"github.com/lightningnetwork/lnd/keychain"
"github.com/lightningnetwork/lnd/lncfg"
"github.com/lightningnetwork/lnd/lnwallet"
"github.com/spf13/cobra"
"go.etcd.io/bbolt"
)
const (
passwordEnvName = "WALLET_PASSWORD"
walletInfoFormat = `
Identity Pubkey: %x
BIP32 HD extended root key: %s
@ -38,19 +29,7 @@ Scope: m/%d'/%d'
)
var (
// Namespace from github.com/btcsuite/btcwallet/wallet/wallet.go.
waddrmgrNamespaceKey = []byte("waddrmgr")
// Bucket names from github.com/btcsuite/btcwallet/waddrmgr/db.go.
mainBucketName = []byte("main")
masterPrivKeyName = []byte("mpriv")
cryptoPrivKeyName = []byte("cpriv")
masterHDPrivName = []byte("mhdpriv")
defaultAccount = uint32(waddrmgr.DefaultAccountNum)
openCallbacks = &waddrmgr.OpenCallbacks{
ObtainSeed: noConsole,
ObtainPrivatePass: noConsole,
}
defaultAccount = uint32(waddrmgr.DefaultAccountNum)
)
type walletInfoCommand struct {
@ -98,75 +77,24 @@ or simply press <enter> without entering a password when being prompted.`,
}
func (c *walletInfoCommand) Execute(_ *cobra.Command, _ []string) error {
var (
publicWalletPw = lnwallet.DefaultPublicPassphrase
privateWalletPw = lnwallet.DefaultPrivatePassphrase
err error
)
// Check that we have a wallet DB.
if c.WalletDB == "" {
return fmt.Errorf("wallet DB is required")
}
// To automate things with chantools, we also offer reading the wallet
// password from environment variables.
pw := []byte(strings.TrimSpace(os.Getenv(passwordEnvName)))
// Because we cannot differentiate between an empty and a non-existent
// environment variable, we need a special character that indicates that
// no password should be used. We use a single dash (-) for that as that
// would be too short for an explicit password anyway.
switch {
// The user indicated in the environment variable that no passphrase
// should be used. We don't set any value.
case string(pw) == "-":
// The environment variable didn't contain anything, we'll read the
// passphrase from the terminal.
case len(pw) == 0:
pw, err = passwordFromConsole("Input wallet password: ")
if err != nil {
return err
}
if len(pw) > 0 {
publicWalletPw = pw
privateWalletPw = pw
}
// There was a password in the environment, just use it directly.
default:
publicWalletPw = pw
privateWalletPw = pw
}
// Try to load and open the wallet.
db, err := walletdb.Open(
"bdb", lncfg.CleanAndExpandPath(c.WalletDB), false,
lnd.DefaultOpenTimeout,
w, privateWalletPw, cleanup, err := lnd.OpenWallet(
c.WalletDB, chainParams,
)
if errors.Is(err, bbolt.ErrTimeout) {
return fmt.Errorf("error opening wallet database, make sure " +
"lnd is not running and holding the exclusive lock " +
"on the wallet")
}
if err != nil {
return fmt.Errorf("error opening wallet database: %w", err)
return fmt.Errorf("error opening wallet file '%s': %w",
c.WalletDB, err)
}
defer func() { _ = db.Close() }()
w, err := wallet.Open(db, publicWalletPw, openCallbacks, chainParams, 0)
if err != nil {
return err
}
// Start and unlock the wallet.
w.Start()
defer w.Stop()
err = w.Unlock(privateWalletPw, nil)
if err != nil {
return err
}
defer func() {
if err := cleanup(); err != nil {
log.Errorf("error closing wallet: %v", err)
}
}()
// Print the wallet info and if requested the root key.
identityKey, scopeInfo, err := walletInfo(w, c.DumpAddrs)
@ -175,7 +103,9 @@ func (c *walletInfoCommand) Execute(_ *cobra.Command, _ []string) error {
}
rootKey := na
if c.WithRootKey {
masterHDPrivKey, err := decryptRootKey(db, privateWalletPw)
masterHDPrivKey, err := lnd.DecryptWalletRootKey(
w.Database(), privateWalletPw,
)
if err != nil {
return err
}
@ -259,7 +189,7 @@ func walletInfo(w *wallet.Wallet, dumpAddrs bool) (*btcec.PublicKey, string,
err = walletdb.View(
w.Database(), func(tx walletdb.ReadTx) error {
waddrmgrNs := tx.ReadBucket(
waddrmgrNamespaceKey,
lnd.WaddrmgrNamespaceKey,
)
return mgr.ForEachAccountAddress(
@ -304,64 +234,3 @@ func printScopeInfo(name string, w *wallet.Wallet,
return scopeInfo, nil
}
func decryptRootKey(db walletdb.DB, privPassphrase []byte) ([]byte, error) {
// Step 1: Load the encryption parameters and encrypted keys from the
// database.
var masterKeyPrivParams []byte
var cryptoKeyPrivEnc []byte
var masterHDPrivEnc []byte
err := walletdb.View(db, func(tx walletdb.ReadTx) error {
ns := tx.ReadBucket(waddrmgrNamespaceKey)
if ns == nil {
return fmt.Errorf("namespace '%s' does not exist",
waddrmgrNamespaceKey)
}
mainBucket := ns.NestedReadBucket(mainBucketName)
if mainBucket == nil {
return fmt.Errorf("bucket '%s' does not exist",
mainBucketName)
}
val := mainBucket.Get(masterPrivKeyName)
if val != nil {
masterKeyPrivParams = make([]byte, len(val))
copy(masterKeyPrivParams, val)
}
val = mainBucket.Get(cryptoPrivKeyName)
if val != nil {
cryptoKeyPrivEnc = make([]byte, len(val))
copy(cryptoKeyPrivEnc, val)
}
val = mainBucket.Get(masterHDPrivName)
if val != nil {
masterHDPrivEnc = make([]byte, len(val))
copy(masterHDPrivEnc, val)
}
return nil
})
if err != nil {
return nil, err
}
// Step 2: Unmarshal the master private key parameters and derive
// key from passphrase.
var masterKeyPriv snacl.SecretKey
if err := masterKeyPriv.Unmarshal(masterKeyPrivParams); err != nil {
return nil, err
}
if err := masterKeyPriv.DeriveKey(&privPassphrase); err != nil {
return nil, err
}
// Step 3: Decrypt the keys in the correct order.
cryptoKeyPriv := &snacl.CryptoKey{}
cryptoKeyPrivBytes, err := masterKeyPriv.Decrypt(cryptoKeyPrivEnc)
if err != nil {
return nil, err
}
copy(cryptoKeyPriv[:], cryptoKeyPrivBytes)
return cryptoKeyPriv.Decrypt(masterHDPrivEnc)
}

@ -3,6 +3,7 @@ package main
import (
"testing"
"github.com/lightninglabs/chantools/lnd"
"github.com/stretchr/testify/require"
)
@ -20,7 +21,7 @@ func TestWalletInfo(t *testing.T) {
WithRootKey: true,
}
t.Setenv(passwordEnvName, testPassPhrase)
t.Setenv(lnd.PasswordEnvName, testPassPhrase)
err := info.Execute(nil, nil)
require.NoError(t, err)

@ -23,6 +23,7 @@ https://github.com/lightninglabs/chantools/.
* [chantools chanbackup](chantools_chanbackup.md) - Create a channel.backup file from a channel database
* [chantools closepoolaccount](chantools_closepoolaccount.md) - Tries to close a Pool account that has expired
* [chantools compactdb](chantools_compactdb.md) - Create a copy of a channel.db file in safe/read-only mode
* [chantools createwallet](chantools_createwallet.md) - Create a new lnd compatible wallet.db file from an existing seed or by generating a new one
* [chantools deletepayments](chantools_deletepayments.md) - Remove all (failed) payments from a channel DB
* [chantools derivekey](chantools_derivekey.md) - Derive a key with a specific derivation path
* [chantools doublespendinputs](chantools_doublespendinputs.md) - Replace a transaction by double spending its input
@ -43,6 +44,8 @@ https://github.com/lightninglabs/chantools/.
* [chantools rescuefunding](chantools_rescuefunding.md) - Rescue funds locked in a funding multisig output that never resulted in a proper channel; this is the command the initiator of the channel needs to run
* [chantools rescuetweakedkey](chantools_rescuetweakedkey.md) - Attempt to rescue funds locked in an address with a key that was affected by a specific bug in lnd
* [chantools showrootkey](chantools_showrootkey.md) - Extract and show the BIP32 HD root key from the 24 word lnd aezeed
* [chantools signmessage](chantools_signmessage.md) - Sign a message with the node's private key.
* [chantools signpsbt](chantools_signpsbt.md) - Sign a Partially Signed Bitcoin Transaction (PSBT)
* [chantools signrescuefunding](chantools_signrescuefunding.md) - 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
* [chantools summary](chantools_summary.md) - Compile a summary about the current state of channels
* [chantools sweepremoteclosed](chantools_sweepremoteclosed.md) - Go through all the addresses that could have funds of channels that were force-closed by the remote party. A public block explorer is queried for each address and if any balance is found, all funds are swept to a given address

@ -27,6 +27,7 @@ chantools chanbackup \
-h, --help help for chanbackup
--multi_file string lnd channel.backup file to create
--rootkey string BIP32 HD root key of the wallet to use for creating the backup; leave empty to prompt for lnd 24 word aezeed
--walletdb string read the seed/master root key to use fro creating the backup from an lnd wallet.db file instead of asking for a seed or providing the --rootkey flag
```
### Options inherited from parent commands

@ -42,6 +42,7 @@ chantools closepoolaccount \
--publish publish sweep TX to the chain API instead of just printing the TX
--rootkey string BIP32 HD root key of the wallet to use for deriving keys; leave empty to prompt for lnd 24 word aezeed
--sweepaddr string address to recover the funds to; specify 'fromseed' to derive a new address from the seed automatically
--walletdb string read the seed/master root key to use fro deriving keys from an lnd wallet.db file instead of asking for a seed or providing the --rootkey flag
```
### Options inherited from parent commands

@ -0,0 +1,44 @@
## chantools createwallet
Create a new lnd compatible wallet.db file from an existing seed or by generating a new one
### Synopsis
Creates a new wallet that can be used with lnd or with
chantools. The wallet can be created from an existing seed or a new one can be
generated (use --generateseed).
```
chantools createwallet [flags]
```
### Examples
```
chantools createwallet \
--walletdbdir ~/.lnd/data/chain/bitcoin/mainnet
```
### Options
```
--bip39 read a classic BIP39 seed and passphrase from the terminal instead of asking for lnd seed format or providing the --rootkey flag
--generateseed generate a new seed instead of using an existing one
-h, --help help for createwallet
--rootkey string BIP32 HD root key of the wallet to use for creating the new wallet; leave empty to prompt for lnd 24 word aezeed
--walletdb string read the seed/master root key to use fro creating the new wallet from an lnd wallet.db file instead of asking for a seed or providing the --rootkey flag
--walletdbdir string the folder to create the new wallet.db file in
```
### Options inherited from parent commands
```
-r, --regtest Indicates if regtest parameters should be used
-s, --signet Indicates if the public signet parameters should be used
-t, --testnet Indicates if testnet parameters should be used
```
### SEE ALSO
* [chantools](chantools.md) - Chantools helps recover funds from lightning channels

@ -23,12 +23,13 @@ chantools derivekey --identity
### Options
```
--bip39 read a classic BIP39 seed and passphrase from the terminal instead of asking for lnd seed format or providing the --rootkey flag
-h, --help help for derivekey
--identity derive the lnd identity_pubkey
--neuter don't output private key(s), only public key(s)
--path string BIP32 derivation path to derive; must start with "m/"
--rootkey string BIP32 HD root key of the wallet to use for decrypting the backup; leave empty to prompt for lnd 24 word aezeed
--bip39 read a classic BIP39 seed and passphrase from the terminal instead of asking for lnd seed format or providing the --rootkey flag
-h, --help help for derivekey
--identity derive the lnd identity_pubkey
--neuter don't output private key(s), only public key(s)
--path string BIP32 derivation path to derive; must start with "m/"
--rootkey string BIP32 HD root key of the wallet to use for decrypting the backup; leave empty to prompt for lnd 24 word aezeed
--walletdb string read the seed/master root key to use fro decrypting the backup from an lnd wallet.db file instead of asking for a seed or providing the --rootkey flag
```
### Options inherited from parent commands

@ -34,6 +34,7 @@ chantools doublespendinputs \
--recoverywindow uint32 number of keys to scan per internal/external branch; output will consist of double this amount of keys (default 2500)
--rootkey string BIP32 HD root key of the wallet to use for deriving the input keys; leave empty to prompt for lnd 24 word aezeed
--sweepaddr string address to recover the funds to; specify 'fromseed' to derive a new address from the seed automatically
--walletdb string read the seed/master root key to use fro deriving the input keys from an lnd wallet.db file instead of asking for a seed or providing the --rootkey flag
```
### Options inherited from parent commands

@ -25,6 +25,7 @@ chantools dumpbackup \
-h, --help help for dumpbackup
--multi_file string lnd channel.backup file to dump
--rootkey string BIP32 HD root key of the wallet to use for decrypting the backup; leave empty to prompt for lnd 24 word aezeed
--walletdb string read the seed/master root key to use fro decrypting the backup from an lnd wallet.db file instead of asking for a seed or providing the --rootkey flag
```
### Options inherited from parent commands

@ -65,6 +65,7 @@ chantools fakechanbackup --from_channel_graph lncli_describegraph.json \
--remote_node_addr string the remote node connection information in the format pubkey@host:port
--rootkey string BIP32 HD root key of the wallet to use for encrypting the backup; leave empty to prompt for lnd 24 word aezeed
--short_channel_id string the short channel ID in the format <blockheight>x<transactionindex>x<outputindex>
--walletdb string read the seed/master root key to use fro encrypting the backup from an lnd wallet.db file instead of asking for a seed or providing the --rootkey flag
```
### Options inherited from parent commands

@ -27,6 +27,7 @@ chantools filterbackup \
-h, --help help for filterbackup
--multi_file string lnd channel.backup file to filter
--rootkey string BIP32 HD root key of the wallet to use for decrypting the backup; leave empty to prompt for lnd 24 word aezeed
--walletdb string read the seed/master root key to use fro decrypting the backup from an lnd wallet.db file instead of asking for a seed or providing the --rootkey flag
```
### Options inherited from parent commands

@ -28,6 +28,7 @@ chantools fixoldbackup \
-h, --help help for fixoldbackup
--multi_file string lnd channel.backup file to fix
--rootkey string BIP32 HD root key of the wallet to use for decrypting the backup; leave empty to prompt for lnd 24 word aezeed
--walletdb string read the seed/master root key to use fro decrypting the backup from an lnd wallet.db file instead of asking for a seed or providing the --rootkey flag
```
### Options inherited from parent commands

@ -43,6 +43,7 @@ chantools forceclose \
--pendingchannels string channel input is in the format of lncli's pendingchannels format; specify '-' to read from stdin
--publish publish force-closing TX to the chain API instead of just printing the TX
--rootkey string BIP32 HD root key of the wallet to use for decrypting the backup; leave empty to prompt for lnd 24 word aezeed
--walletdb string read the seed/master root key to use fro decrypting the backup from an lnd wallet.db file instead of asking for a seed or providing the --rootkey flag
```
### Options inherited from parent commands

@ -51,6 +51,7 @@ chantools genimportscript --format bitcoin-cli \
--rescanfrom uint32 block number to rescan from; will be set automatically from the wallet birthday if the lnd 24 word aezeed is entered (default 500000)
--rootkey string BIP32 HD root key of the wallet to use for decrypting the backup; leave empty to prompt for lnd 24 word aezeed
--stdout write generated import script to standard out instead of writing it to a file
--walletdb string read the seed/master root key to use fro decrypting the backup from an lnd wallet.db file instead of asking for a seed or providing the --rootkey flag
```
### Options inherited from parent commands

@ -33,6 +33,7 @@ chantools pullanchor \
-h, --help help for pullanchor
--rootkey string BIP32 HD root key of the wallet to use for deriving keys; leave empty to prompt for lnd 24 word aezeed
--sponsorinput string the input to use to sponsor the CPFP transaction; must be owned by the lnd node that owns the anchor output
--walletdb string read the seed/master root key to use fro deriving keys from an lnd wallet.db file instead of asking for a seed or providing the --rootkey flag
```
### Options inherited from parent commands

@ -35,6 +35,7 @@ chantools recoverloopin \
--sweepaddr string address to recover the funds to; specify 'fromseed' to derive a new address from the seed automatically
--txid string transaction id of the on-chain transaction that created the HTLC
--vout uint32 output index of the on-chain transaction that created the HTLC
--walletdb string read the seed/master root key to use fro deriving starting key from an lnd wallet.db file instead of asking for a seed or providing the --rootkey flag
```
### Options inherited from parent commands

@ -60,6 +60,7 @@ chantools rescueclosed --fromsummary results/summary-xxxxxx.json \
--lnd_log string the lnd log file to read to get the commit_point values when rescuing multiple channels at the same time
--pendingchannels string channel input is in the format of lncli's pendingchannels format; specify '-' to read from stdin
--rootkey string BIP32 HD root key of the wallet to use for decrypting the backup; leave empty to prompt for lnd 24 word aezeed
--walletdb string read the seed/master root key to use fro decrypting the backup from an lnd wallet.db file instead of asking for a seed or providing the --rootkey flag
```
### Options inherited from parent commands

@ -50,6 +50,7 @@ chantools rescuefunding \
--remotepubkey string in case a channel DB is not available (but perhaps a channel backup file), the remote multisig public key can be specified manually
--rootkey string BIP32 HD root key of the wallet to use for deriving keys; leave empty to prompt for lnd 24 word aezeed
--sweepaddr string address to recover the funds to; specify 'fromseed' to derive a new address from the seed automatically
--walletdb string read the seed/master root key to use fro deriving keys from an lnd wallet.db file instead of asking for a seed or providing the --rootkey flag
```
### Options inherited from parent commands

@ -29,6 +29,7 @@ chantools rescuetweakedkey \
--path string BIP32 derivation path to derive the starting key from; must start with "m/"
--rootkey string BIP32 HD root key of the wallet to use for deriving starting key; leave empty to prompt for lnd 24 word aezeed
--targetaddr string address the funds are locked in
--walletdb string read the seed/master root key to use fro deriving starting key from an lnd wallet.db file instead of asking for a seed or providing the --rootkey flag
```
### Options inherited from parent commands

@ -21,9 +21,10 @@ chantools showrootkey
### Options
```
--bip39 read a classic BIP39 seed and passphrase from the terminal instead of asking for lnd seed format or providing the --rootkey flag
-h, --help help for showrootkey
--rootkey string BIP32 HD root key of the wallet to use for decrypting the backup; leave empty to prompt for lnd 24 word aezeed
--bip39 read a classic BIP39 seed and passphrase from the terminal instead of asking for lnd seed format or providing the --rootkey flag
-h, --help help for showrootkey
--rootkey string BIP32 HD root key of the wallet to use for decrypting the backup; leave empty to prompt for lnd 24 word aezeed
--walletdb string read the seed/master root key to use fro decrypting the backup from an lnd wallet.db file instead of asking for a seed or providing the --rootkey flag
```
### Options inherited from parent commands

@ -1,10 +1,12 @@
## chantools signmessage
Signs a message with the nodes key, results in the same signature as
`lncli signmessage`
Sign a message with the node's private key.
### Synopsis
Sign msg with the resident node's private key.
Returns the signature as a zbase32 string.
```
chantools signmessage [flags]
```
@ -18,9 +20,22 @@ chantools signmessage --msg=foobar
### Options
```
--bip39 read a classic BIP39 seed and passphrase from the terminal instead of asking for lnd seed format or providing the --rootkey flag
-h, --help help for signmessage
--msg string the message to sign
--rootkey string BIP32 HD root key of the wallet to use for decrypting the backup; leave empty to prompt for lnd 24 word aezeed
--single_hash single hash the msg instead of double hash (lnd default is false)
```
--bip39 read a classic BIP39 seed and passphrase from the terminal instead of asking for lnd seed format or providing the --rootkey flag
-h, --help help for signmessage
--msg string the message to sign
--rootkey string BIP32 HD root key of the wallet to use for decrypting the backup; leave empty to prompt for lnd 24 word aezeed
--walletdb string read the seed/master root key to use fro decrypting the backup from an lnd wallet.db file instead of asking for a seed or providing the --rootkey flag
```
### Options inherited from parent commands
```
-r, --regtest Indicates if regtest parameters should be used
-s, --signet Indicates if the public signet parameters should be used
-t, --testnet Indicates if testnet parameters should be used
```
### SEE ALSO
* [chantools](chantools.md) - Chantools helps recover funds from lightning channels

@ -0,0 +1,46 @@
## chantools signpsbt
Sign a Partially Signed Bitcoin Transaction (PSBT)
### Synopsis
Sign a PSBT with a master root key. The PSBT must contain
an input that is owned by the master root key.
```
chantools signpsbt [flags]
```
### Examples
```
chantools signpsbt \
--psbt <the_base64_encoded_psbt>
chantools signpsbt --fromrawpsbtfile <file_with_psbt>
```
### Options
```
--bip39 read a classic BIP39 seed and passphrase from the terminal instead of asking for lnd seed format or providing the --rootkey flag
--fromrawpsbtfile string the file containing the raw, binary encoded PSBT packet to sign
-h, --help help for signpsbt
--psbt string Partially Signed Bitcoin Transaction to sign
--rootkey string BIP32 HD root key of the wallet to use for signing the PSBT; leave empty to prompt for lnd 24 word aezeed
--torawpsbtfile string the file to write the resulting signed raw, binary encoded PSBT packet to
--walletdb string read the seed/master root key to use fro signing the PSBT from an lnd wallet.db file instead of asking for a seed or providing the --rootkey flag
```
### Options inherited from parent commands
```
-r, --regtest Indicates if regtest parameters should be used
-s, --signet Indicates if the public signet parameters should be used
-t, --testnet Indicates if testnet parameters should be used
```
### SEE ALSO
* [chantools](chantools.md) - Chantools helps recover funds from lightning channels

@ -26,10 +26,11 @@ chantools signrescuefunding \
### Options
```
--bip39 read a classic BIP39 seed and passphrase from the terminal instead of asking for lnd seed format or providing the --rootkey flag
-h, --help help for signrescuefunding
--psbt string Partially Signed Bitcoin Transaction that was provided by the initiator of the channel to rescue
--rootkey string BIP32 HD root key of the wallet to use for deriving keys; leave empty to prompt for lnd 24 word aezeed
--bip39 read a classic BIP39 seed and passphrase from the terminal instead of asking for lnd seed format or providing the --rootkey flag
-h, --help help for signrescuefunding
--psbt string Partially Signed Bitcoin Transaction that was provided by the initiator of the channel to rescue
--rootkey string BIP32 HD root key of the wallet to use for deriving keys; leave empty to prompt for lnd 24 word aezeed
--walletdb string read the seed/master root key to use fro deriving keys from an lnd wallet.db file instead of asking for a seed or providing the --rootkey flag
```
### Options inherited from parent commands

@ -41,6 +41,7 @@ chantools sweepremoteclosed \
--recoverywindow uint32 number of keys to scan per derivation path (default 200)
--rootkey string BIP32 HD root key of the wallet to use for sweeping the wallet; leave empty to prompt for lnd 24 word aezeed
--sweepaddr string address to recover the funds to; specify 'fromseed' to derive a new address from the seed automatically
--walletdb string read the seed/master root key to use fro sweeping the wallet from an lnd wallet.db file instead of asking for a seed or providing the --rootkey flag
```
### Options inherited from parent commands

@ -41,6 +41,7 @@ chantools sweeptimelock \
--publish publish sweep TX to the chain API instead of just printing the TX
--rootkey string BIP32 HD root key of the wallet to use for deriving keys; leave empty to prompt for lnd 24 word aezeed
--sweepaddr string address to recover the funds to; specify 'fromseed' to derive a new address from the seed automatically
--walletdb string read the seed/master root key to use fro deriving keys from an lnd wallet.db file instead of asking for a seed or providing the --rootkey flag
```
### Options inherited from parent commands

@ -63,6 +63,7 @@ chantools sweeptimelockmanual \
--rootkey string BIP32 HD root key of the wallet to use for deriving keys; leave empty to prompt for lnd 24 word aezeed
--sweepaddr string address to recover the funds to; specify 'fromseed' to derive a new address from the seed automatically
--timelockaddr string address of the time locked commitment output where the funds are stuck in
--walletdb string read the seed/master root key to use fro deriving keys from an lnd wallet.db file instead of asking for a seed or providing the --rootkey flag
```
### Options inherited from parent commands

@ -31,6 +31,7 @@ chantools triggerforceclose \
-h, --help help for triggerforceclose
--peer string remote peer address (<pubkey>@<host>[:<port>])
--rootkey string BIP32 HD root key of the wallet to use for deriving the identity key; leave empty to prompt for lnd 24 word aezeed
--walletdb string read the seed/master root key to use fro deriving the identity key from an lnd wallet.db file instead of asking for a seed or providing the --rootkey flag
```
### Options inherited from parent commands

@ -35,6 +35,7 @@ chantools zombierecovery makeoffer \
--node1_keys string the JSON file generated in theprevious step ('preparekeys') command of node 1
--node2_keys string the JSON file generated in theprevious step ('preparekeys') command of node 2
--rootkey string BIP32 HD root key of the wallet to use for signing the offer; leave empty to prompt for lnd 24 word aezeed
--walletdb string read the seed/master root key to use fro signing the offer from an lnd wallet.db file instead of asking for a seed or providing the --rootkey flag
```
### Options inherited from parent commands

@ -31,6 +31,7 @@ chantools zombierecovery preparekeys \
--num_keys uint32 the number of multisig keys to derive (default 2500)
--payout_addr string the address where this node's rescued funds should be sent to, must be a P2WPKH (native SegWit) address
--rootkey string BIP32 HD root key of the wallet to use for deriving the multisig keys; leave empty to prompt for lnd 24 word aezeed
--walletdb string read the seed/master root key to use fro deriving the multisig keys from an lnd wallet.db file instead of asking for a seed or providing the --rootkey flag
```
### Options inherited from parent commands

@ -21,10 +21,11 @@ chantools zombierecovery signoffer \
### Options
```
--bip39 read a classic BIP39 seed and passphrase from the terminal instead of asking for lnd seed format or providing the --rootkey flag
-h, --help help for signoffer
--psbt string the base64 encoded PSBT that the other party sent as an offer to rescue funds
--rootkey string BIP32 HD root key of the wallet to use for signing the offer; leave empty to prompt for lnd 24 word aezeed
--bip39 read a classic BIP39 seed and passphrase from the terminal instead of asking for lnd seed format or providing the --rootkey flag
-h, --help help for signoffer
--psbt string the base64 encoded PSBT that the other party sent as an offer to rescue funds
--rootkey string BIP32 HD root key of the wallet to use for signing the offer; leave empty to prompt for lnd 24 word aezeed
--walletdb string read the seed/master root key to use fro signing the offer from an lnd wallet.db file instead of asking for a seed or providing the --rootkey flag
```
### Options inherited from parent commands

@ -2,6 +2,7 @@ package lnd
import (
"bufio"
"errors"
"fmt"
"os"
"regexp"
@ -11,20 +12,47 @@ import (
"github.com/btcsuite/btcd/btcutil/hdkeychain"
"github.com/btcsuite/btcd/chaincfg"
"github.com/btcsuite/btcwallet/snacl"
"github.com/btcsuite/btcwallet/waddrmgr"
"github.com/btcsuite/btcwallet/wallet"
"github.com/btcsuite/btcwallet/walletdb"
"github.com/lightningnetwork/lnd/aezeed"
"github.com/lightningnetwork/lnd/lncfg"
"github.com/lightningnetwork/lnd/lnwallet"
"go.etcd.io/bbolt"
"golang.org/x/crypto/ssh/terminal"
)
const (
MnemonicEnvName = "AEZEED_MNEMONIC"
PassphraseEnvName = "AEZEED_PASSPHRASE"
PasswordEnvName = "WALLET_PASSWORD"
)
var (
numberDotsRegex = regexp.MustCompile(`[\d.\-\n\r\t]*`)
multipleSpaces = regexp.MustCompile(" [ ]+")
openCallbacks = &waddrmgr.OpenCallbacks{
ObtainSeed: noConsole,
ObtainPrivatePass: noConsole,
}
// Namespace from github.com/btcsuite/btcwallet/wallet/wallet.go.
WaddrmgrNamespaceKey = []byte("waddrmgr")
// Bucket names from github.com/btcsuite/btcwallet/waddrmgr/db.go.
mainBucketName = []byte("main")
masterPrivKeyName = []byte("mpriv")
cryptoPrivKeyName = []byte("cpriv")
masterHDPrivName = []byte("mhdpriv")
)
func noConsole() ([]byte, error) {
return nil, fmt.Errorf("wallet db requires console access")
}
// ReadAezeed reads an aezeed from the console or the environment variable.
func ReadAezeed(params *chaincfg.Params) (*hdkeychain.ExtendedKey, time.Time,
error) {
@ -67,6 +95,32 @@ func ReadAezeed(params *chaincfg.Params) (*hdkeychain.ExtendedKey, time.Time,
len(cipherSeedMnemonic), 24)
}
passphraseBytes, err := ReadPassphrase("doesn't have")
if err != nil {
return nil, time.Unix(0, 0), err
}
var mnemonic aezeed.Mnemonic
copy(mnemonic[:], cipherSeedMnemonic)
// If we're unable to map it back into the ciphertext, then either the
// mnemonic is wrong, or the passphrase is wrong.
cipherSeed, err := mnemonic.ToCipherSeed(passphraseBytes)
if err != nil {
return nil, time.Unix(0, 0), fmt.Errorf("failed to decrypt "+
"seed with passphrase: %w", err)
}
rootKey, err := hdkeychain.NewMaster(cipherSeed.Entropy[:], params)
if err != nil {
return nil, time.Unix(0, 0), fmt.Errorf("failed to derive " +
"master extended key")
}
return rootKey, cipherSeed.BirthdayTime(), nil
}
// ReadPassphrase reads a cipher seed passphrase from the console or the
// environment variable.
func ReadPassphrase(verb string) ([]byte, error) {
// Additionally, the user may have a passphrase, that will also need to
// be provided so the daemon can properly decipher the cipher seed.
// Try the environment variable first.
@ -85,14 +139,14 @@ func ReadAezeed(params *chaincfg.Params) (*hdkeychain.ExtendedKey, time.Time,
// The environment variable didn't contain anything, we'll read the
// passphrase from the terminal.
case passphrase == "":
fmt.Printf("Input your cipher seed passphrase (press enter " +
"if your seed doesn't have a passphrase): ")
fmt.Printf("Input your cipher seed passphrase (press enter "+
"if your seed %s a passphrase): ", verb)
var err error
passphraseBytes, err = terminal.ReadPassword(
int(syscall.Stdin), //nolint
)
if err != nil {
return nil, time.Unix(0, 0), err
return nil, err
}
fmt.Println()
@ -101,20 +155,176 @@ func ReadAezeed(params *chaincfg.Params) (*hdkeychain.ExtendedKey, time.Time,
passphraseBytes = []byte(passphrase)
}
var mnemonic aezeed.Mnemonic
copy(mnemonic[:], cipherSeedMnemonic)
return passphraseBytes, nil
}
// If we're unable to map it back into the ciphertext, then either the
// mnemonic is wrong, or the passphrase is wrong.
cipherSeed, err := mnemonic.ToCipherSeed(passphraseBytes)
// PasswordFromConsole reads a password from the console or stdin.
func PasswordFromConsole(userQuery string) ([]byte, error) {
// Read from terminal (if there is one).
if terminal.IsTerminal(int(syscall.Stdin)) { //nolint
fmt.Print(userQuery)
pw, err := terminal.ReadPassword(int(syscall.Stdin)) //nolint
if err != nil {
return nil, err
}
fmt.Println()
return pw, nil
}
// Read from stdin as a fallback.
reader := bufio.NewReader(os.Stdin)
pw, err := reader.ReadBytes('\n')
if err != nil {
return nil, time.Unix(0, 0), fmt.Errorf("failed to decrypt "+
"seed with passphrase: %w", err)
return nil, err
}
return pw, nil
}
// OpenWallet opens a lnd compatible wallet and returns it, along with the
// private wallet password.
func OpenWallet(walletDbPath string,
chainParams *chaincfg.Params) (*wallet.Wallet, []byte, func() error,
error) {
var (
publicWalletPw = lnwallet.DefaultPublicPassphrase
privateWalletPw = lnwallet.DefaultPrivatePassphrase
err error
)
// To automate things with chantools, we also offer reading the wallet
// password from environment variables.
pw := []byte(strings.TrimSpace(os.Getenv(PasswordEnvName)))
// Because we cannot differentiate between an empty and a non-existent
// environment variable, we need a special character that indicates that
// no password should be used. We use a single dash (-) for that as that
// would be too short for an explicit password anyway.
switch {
// The user indicated in the environment variable that no passphrase
// should be used. We don't set any value.
case string(pw) == "-":
// The environment variable didn't contain anything, we'll read the
// passphrase from the terminal.
case len(pw) == 0:
pw, err = PasswordFromConsole("Input wallet password: ")
if err != nil {
return nil, nil, nil, err
}
if len(pw) > 0 {
publicWalletPw = pw
privateWalletPw = pw
}
// There was a password in the environment, just use it directly.
default:
publicWalletPw = pw
privateWalletPw = pw
}
// Try to load and open the wallet.
db, err := walletdb.Open(
"bdb", lncfg.CleanAndExpandPath(walletDbPath), false,
DefaultOpenTimeout,
)
if errors.Is(err, bbolt.ErrTimeout) {
return nil, nil, nil, fmt.Errorf("error opening wallet " +
"database, make sure lnd is not running and holding " +
"the exclusive lock on the wallet")
}
rootKey, err := hdkeychain.NewMaster(cipherSeed.Entropy[:], params)
if err != nil {
return nil, time.Unix(0, 0), fmt.Errorf("failed to derive " +
"master extended key")
return nil, nil, nil, fmt.Errorf("error opening wallet "+
"database: %w", err)
}
return rootKey, cipherSeed.BirthdayTime(), nil
w, err := wallet.Open(db, publicWalletPw, openCallbacks, chainParams, 0)
if err != nil {
_ = db.Close()
return nil, nil, nil, fmt.Errorf("error opening wallet %w", err)
}
// Start and unlock the wallet.
w.Start()
err = w.Unlock(privateWalletPw, nil)
if err != nil {
w.Stop()
_ = db.Close()
return nil, nil, nil, err
}
cleanup := func() error {
w.Stop()
if err := db.Close(); err != nil {
return err
}
return nil
}
return w, privateWalletPw, cleanup, nil
}
// DecryptWalletRootKey decrypts a lnd compatible wallet's root key.
func DecryptWalletRootKey(db walletdb.DB,
privatePassphrase []byte) ([]byte, error) {
// Step 1: Load the encryption parameters and encrypted keys from the
// database.
var masterKeyPrivParams []byte
var cryptoKeyPrivEnc []byte
var masterHDPrivEnc []byte
err := walletdb.View(db, func(tx walletdb.ReadTx) error {
ns := tx.ReadBucket(WaddrmgrNamespaceKey)
if ns == nil {
return fmt.Errorf("namespace '%s' does not exist",
WaddrmgrNamespaceKey)
}
mainBucket := ns.NestedReadBucket(mainBucketName)
if mainBucket == nil {
return fmt.Errorf("bucket '%s' does not exist",
mainBucketName)
}
val := mainBucket.Get(masterPrivKeyName)
if val != nil {
masterKeyPrivParams = make([]byte, len(val))
copy(masterKeyPrivParams, val)
}
val = mainBucket.Get(cryptoPrivKeyName)
if val != nil {
cryptoKeyPrivEnc = make([]byte, len(val))
copy(cryptoKeyPrivEnc, val)
}
val = mainBucket.Get(masterHDPrivName)
if val != nil {
masterHDPrivEnc = make([]byte, len(val))
copy(masterHDPrivEnc, val)
}
return nil
})
if err != nil {
return nil, err
}
// Step 2: Unmarshal the master private key parameters and derive
// key from passphrase.
var masterKeyPriv snacl.SecretKey
if err := masterKeyPriv.Unmarshal(masterKeyPrivParams); err != nil {
return nil, err
}
if err := masterKeyPriv.DeriveKey(&privatePassphrase); err != nil {
return nil, err
}
// Step 3: Decrypt the keys in the correct order.
cryptoKeyPriv := &snacl.CryptoKey{}
cryptoKeyPrivBytes, err := masterKeyPriv.Decrypt(cryptoKeyPrivEnc)
if err != nil {
return nil, err
}
copy(cryptoKeyPriv[:], cryptoKeyPrivBytes)
return cryptoKeyPriv.Decrypt(masterHDPrivEnc)
}

@ -30,15 +30,15 @@ func (s *Signer) SignOutputRaw(tx *wire.MsgTx,
// First attempt to fetch the private key which corresponds to the
// specified public key.
privKey, err := s.FetchPrivKey(&signDesc.KeyDesc)
privKey, err := s.FetchPrivateKey(&signDesc.KeyDesc)
if err != nil {
return nil, err
}
return s.SignOutputRawWithPrivkey(tx, signDesc, privKey)
return s.SignOutputRawWithPrivateKey(tx, signDesc, privKey)
}
func (s *Signer) SignOutputRawWithPrivkey(tx *wire.MsgTx,
func (s *Signer) SignOutputRawWithPrivateKey(tx *wire.MsgTx,
signDesc *input.SignDescriptor,
privKey *secp256k1.PrivateKey) (input.Signature, error) {
@ -122,7 +122,7 @@ func (s *Signer) ComputeInputScript(_ *wire.MsgTx, _ *input.SignDescriptor) (
return nil, fmt.Errorf("unimplemented")
}
func (s *Signer) FetchPrivKey(descriptor *keychain.KeyDescriptor) (
func (s *Signer) FetchPrivateKey(descriptor *keychain.KeyDescriptor) (
*btcec.PrivateKey, error) {
key, err := DeriveChildren(s.ExtendedKey, []uint32{
@ -181,6 +181,50 @@ func (s *Signer) AddPartialSignature(packet *psbt.Packet,
return nil
}
func (s *Signer) AddPartialSignatureForPrivateKey(packet *psbt.Packet,
privateKey *btcec.PrivateKey, utxo *wire.TxOut, witnessScript []byte,
inputIndex int) error {
// Now we add our partial signature.
prevOutFetcher := wallet.PsbtPrevOutputFetcher(packet)
signDesc := &input.SignDescriptor{
WitnessScript: witnessScript,
Output: utxo,
InputIndex: inputIndex,
HashType: txscript.SigHashAll,
PrevOutputFetcher: prevOutFetcher,
SigHashes: txscript.NewTxSigHashes(
packet.UnsignedTx, prevOutFetcher,
),
}
ourSigRaw, err := s.SignOutputRawWithPrivateKey(
packet.UnsignedTx, signDesc, privateKey,
)
if err != nil {
return fmt.Errorf("error signing with our key: %w", err)
}
ourSig := append(ourSigRaw.Serialize(), byte(txscript.SigHashAll))
// Great, we were able to create our sig, let's add it to the PSBT.
updater, err := psbt.NewUpdater(packet)
if err != nil {
return fmt.Errorf("error creating PSBT updater: %w", err)
}
status, err := updater.Sign(
inputIndex, ourSig, privateKey.PubKey().SerializeCompressed(),
nil, witnessScript,
)
if err != nil {
return fmt.Errorf("error adding signature to PSBT: %w", err)
}
if status != 0 {
return fmt.Errorf("unexpected status for signature update, "+
"got %d wanted 0", status)
}
return nil
}
// maybeTweakPrivKey examines the single tweak parameters on the passed sign
// descriptor and may perform a mapping on the passed private key in order to
// utilize the tweaks, if populated.

Loading…
Cancel
Save