multi: add unit tests

pull/17/head
Oliver Gugger 3 years ago
parent e6fcb580a3
commit fa62a57e95
No known key found for this signature in database
GPG Key ID: 8E4256593F177720

@ -6,28 +6,42 @@ import (
"encoding/hex"
"errors"
"fmt"
"os"
"strings"
"syscall"
"github.com/btcsuite/btcd/chaincfg"
"github.com/btcsuite/btcutil/hdkeychain"
"github.com/guggero/chantools/bip39"
"golang.org/x/crypto/pbkdf2"
"golang.org/x/crypto/ssh/terminal"
"os"
"strings"
"syscall"
)
const (
BIP39MnemonicEnvName = "SEED_MNEMONIC"
BIP39PassphraseEnvName = "SEED_PASSPHRASE"
)
func ReadMnemonicFromTerminal(params *chaincfg.Params) (*hdkeychain.ExtendedKey,
error) {
// We'll now prompt the user to enter in their 12 to 24 word mnemonic.
fmt.Printf("Input your 12 to 24 word mnemonic separated by spaces: ")
var err error
reader := bufio.NewReader(os.Stdin)
mnemonicStr, err := reader.ReadString('\n')
if err != nil {
return nil, err
// To automate things with chantools, we also offer reading the seed
// from environment variables.
mnemonicStr := strings.TrimSpace(os.Getenv(BIP39MnemonicEnvName))
if mnemonicStr == "" {
// If there's no value in the environment, we'll now prompt the
//user to enter in their 12 to 24 word mnemonic.
fmt.Printf("Input your 12 to 24 word mnemonic separated by " +
"spaces: ")
mnemonicStr, err = reader.ReadString('\n')
if err != nil {
return nil, err
}
fmt.Println()
}
fmt.Println()
// We'll trim off extra spaces, and ensure the mnemonic is all
// lower case.
@ -40,62 +54,87 @@ func ReadMnemonicFromTerminal(params *chaincfg.Params) (*hdkeychain.ExtendedKey,
"must be between 12 and 24 words")
}
// Additionally, the user may have a passphrase, that will also
// need to be provided so the daemon can properly decipher the
// cipher seed.
fmt.Printf("Input your cipher seed passphrase (press enter if " +
"your seed doesn't have a passphrase): ")
passphrase, err := terminal.ReadPassword(int(syscall.Stdin)) // nolint
if err != nil {
return nil, err
}
fmt.Println()
// 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.
passphrase := strings.TrimSpace(os.Getenv(BIP39PassphraseEnvName))
// Check that the mnemonic is valid.
_, err = bip39.EntropyFromMnemonic(mnemonicStr)
if err != nil {
return nil, err
}
// Because we cannot differentiate between an empty and a non-existent
// environment variable, we need a special character that indicates that
// no passphrase should be used. We use a single dash (-) for that as
// that would be too short for a passphrase anyway.
var (
passphraseBytes []byte
seed []byte
choice string
)
switch {
// The user indicated in the environment variable that no passphrase
// should be used. We don't set any value.
case passphrase == "-":
var seed []byte
fmt.Printf("Please choose passphrase mode:\n" +
" 0 - Default BIP39\n" +
" 1 - Passphrase to hex\n" +
" 2 - Digital Bitbox (extra round of PBKDF2)\n" +
"\n" +
"Choice [default 0]: ")
choice, err := reader.ReadString('\n')
if err != nil {
return nil, err
// The environment variable didn't contain anything, we'll read the
// passphrase from the terminal.
case passphrase == "":
// Additionally, the user may have a passphrase, that will also
// need to be provided so the daemon can properly decipher the
// cipher seed.
fmt.Printf("Input your cipher seed passphrase (press enter " +
"if your seed doesn't have a passphrase): ")
passphraseBytes, err = terminal.ReadPassword(
int(syscall.Stdin), // nolint
)
if err != nil {
return nil, err
}
fmt.Println()
// Check that the mnemonic is valid.
_, err = bip39.EntropyFromMnemonic(mnemonicStr)
if err != nil {
return nil, err
}
fmt.Printf("Please choose passphrase mode:\n" +
" 0 - Default BIP39\n" +
" 1 - Passphrase to hex\n" +
" 2 - Digital Bitbox (extra round of PBKDF2)\n" +
"\n" +
"Choice [default 0]: ")
choice, err = reader.ReadString('\n')
if err != nil {
return nil, err
}
fmt.Println()
// There was a password in the environment, just convert it to bytes.
default:
passphraseBytes = []byte(passphrase)
}
fmt.Println()
switch strings.TrimSpace(choice) {
case "", "0":
seed = pbkdf2.Key(
[]byte(mnemonicStr), append(
[]byte("mnemonic"), passphrase...,
[]byte("mnemonic"), passphraseBytes...,
), 2048, 64, sha512.New,
)
case "1":
passphrase = []byte(hex.EncodeToString(passphrase))
p := []byte(hex.EncodeToString(passphraseBytes))
seed = pbkdf2.Key(
[]byte(mnemonicStr), append(
[]byte("mnemonic"), passphrase...,
), 2048, 64, sha512.New,
[]byte(mnemonicStr), append([]byte("mnemonic"), p...),
2048, 64, sha512.New,
)
case "2":
passphrase = pbkdf2.Key(
passphrase, []byte("Digital Bitbox"), 20480, 64,
p := hex.EncodeToString(pbkdf2.Key(
passphraseBytes, []byte("Digital Bitbox"), 20480, 64,
sha512.New,
)
passphrase = []byte(hex.EncodeToString(passphrase))
))
seed = pbkdf2.Key(
[]byte(mnemonicStr), append(
[]byte("mnemonic"), passphrase...,
), 2048, 64, sha512.New,
[]byte(mnemonicStr), append([]byte("mnemonic"), p...),
2048, 64, sha512.New,
)
default:

@ -0,0 +1,37 @@
package main
import (
"testing"
"github.com/stretchr/testify/require"
)
const (
backupContent = "FundingOutpoint: (string) (len=66) \"10279f626196340" +
"58b6133cb7ac6c1693a8e6df7caa91c6263ca3d0bf704ad4d:0\""
)
func TestChanBackupAndDumpBackup(t *testing.T) {
h := newHarness(t)
// Create a channel backup from a channel DB file.
makeBackup := &chanBackupCommand{
ChannelDB: h.testdataFile("channel.db"),
MultiFile: h.tempFile("extracted.backup"),
rootKey: &rootKey{RootKey: rootKeyAezeed},
}
err := makeBackup.Execute(nil, nil)
require.NoError(t, err)
// Decrypt and dump the channel backup file.
dumpBackup := &dumpBackupCommand{
MultiFile: makeBackup.MultiFile,
rootKey: &rootKey{RootKey: rootKeyAezeed},
}
err = dumpBackup.Execute(nil, nil)
require.NoError(t, err)
h.assertLogContains(backupContent)
}

@ -64,10 +64,14 @@ func (c *compactDBCommand) Execute(_ *cobra.Command, _ []string) error {
if err != nil {
return fmt.Errorf("error opening source DB: %v", err)
}
defer func() { _ = src.Close() }()
dst, err := c.openDB(c.DestDB, false)
if err != nil {
return fmt.Errorf("error opening destination DB: %v", err)
}
defer func() { _ = dst.Close() }()
err = c.compact(dst, src)
if err != nil {
return fmt.Errorf("error compacting DB: %v", err)

@ -0,0 +1,46 @@
package main
import (
"testing"
"github.com/stretchr/testify/require"
)
func TestCompactDBAndDumpChannels(t *testing.T) {
h := newHarness(t)
// Compact the test DB.
compact := &compactDBCommand{
SourceDB: h.testdataFile("channel.db"),
DestDB: h.tempFile("compacted.db"),
}
err := compact.Execute(nil, nil)
require.NoError(t, err)
require.FileExists(t, compact.DestDB)
// Compacting small DBs actually increases the size slightly. But we
// just want to make sure the contents match.
require.GreaterOrEqual(
t, h.fileSize(compact.DestDB), h.fileSize(compact.SourceDB),
)
// Compare the content of the source and destination DB by looking at
// the logged dump.
dump := &dumpChannelsCommand{
ChannelDB: compact.SourceDB,
}
h.clearLog()
err = dump.Execute(nil, nil)
require.NoError(t, err)
sourceDump := h.getLog()
h.clearLog()
dump.ChannelDB = compact.DestDB
err = dump.Execute(nil, nil)
require.NoError(t, err)
destDump := h.getLog()
h.assertLogEqual(sourceDump, destDump)
}

@ -2,15 +2,24 @@ package main
import (
"fmt"
"github.com/btcsuite/btcutil"
"github.com/btcsuite/btcutil/hdkeychain"
"github.com/guggero/chantools/lnd"
"github.com/spf13/cobra"
)
const deriveKeyFormat = `
Path: %s
Network: %s
Public key: %x
Extended public key (xpub): %v
Address: %v
Legacy address: %v
Private key (WIF): %s
Extended private key (xprv): %s
`
type deriveKeyCommand struct {
BIP39 bool
Path string
Neuter bool
@ -29,11 +38,6 @@ derivation path from the root key and prints it to the console.`,
--path "m/1017'/0'/5'/0/0'" --neuter`,
RunE: cc.Execute,
}
cc.cmd.Flags().BoolVar(
&cc.BIP39, "bip39", false, "read a classic BIP39 seed and "+
"passphrase from the terminal instead of asking for "+
"lnd seed format or providing the --rootkey flag",
)
cc.cmd.Flags().StringVar(
&cc.Path, "path", "", "BIP32 derivation path to derive; must "+
"start with \"m/\"",
@ -60,7 +64,6 @@ func (c *deriveKeyCommand) Execute(_ *cobra.Command, _ []string) error {
func deriveKey(extendedKey *hdkeychain.ExtendedKey, path string,
neuter bool) error {
fmt.Printf("Deriving path %s for network %s.\n", path, chainParams.Name)
child, pubKey, wif, err := lnd.DeriveKey(extendedKey, path, chainParams)
if err != nil {
return fmt.Errorf("could not derive keys: %v", err)
@ -69,8 +72,6 @@ func deriveKey(extendedKey *hdkeychain.ExtendedKey, path string,
if err != nil {
return fmt.Errorf("could not neuter child key: %v", err)
}
fmt.Printf("\nPublic key: %x\n", pubKey.SerializeCompressed())
fmt.Printf("Extended public key (xpub): %s\n", neutered.String())
// Print the address too.
hash160 := btcutil.Hash160(pubKey.SerializeCompressed())
@ -84,13 +85,21 @@ func deriveKey(extendedKey *hdkeychain.ExtendedKey, path string,
if err != nil {
return fmt.Errorf("could not create address: %v", err)
}
fmt.Printf("Address: %s\n", addrP2WKH)
fmt.Printf("Legacy address: %s\n", addrP2PKH)
privKey, xPriv := "n/a", "n/a"
if !neuter {
fmt.Printf("\nPrivate key (WIF): %s\n", wif.String())
fmt.Printf("Extended private key (xprv): %s\n", child.String())
privKey, xPriv = wif.String(), child.String()
}
result := fmt.Sprintf(
deriveKeyFormat, path, chainParams.Name,
pubKey.SerializeCompressed(), neutered, addrP2WKH, addrP2PKH,
privKey, xPriv,
)
fmt.Printf(result)
// For the tests, also log as trace level which is disabled by default.
log.Tracef(result)
return nil
}

@ -0,0 +1,91 @@
package main
import (
"github.com/guggero/chantools/btc"
"github.com/guggero/chantools/lnd"
"os"
"testing"
"github.com/stretchr/testify/require"
)
const (
testPath = "m/123'/45'/67'/8/9"
keyContent = "bcrt1qnl5qfvpfcmj7y56nugpermluu46x79sfz0ku70"
keyContentBIP39 = "bcrt1q3pae32m7jdqm5ulf80yc3n59xy4s4xm5a28ekr"
)
func TestDeriveKey(t *testing.T) {
h := newHarness(t)
// Derive a specific key from the serialized root key.
derive := &deriveKeyCommand{
Path: testPath,
rootKey: &rootKey{RootKey: rootKeyAezeed},
}
err := derive.Execute(nil, nil)
require.NoError(t, err)
h.assertLogContains(keyContent)
}
func TestDeriveKeyAezeedNoPassphrase(t *testing.T) {
h := newHarness(t)
// Derive a specific key from the serialized root key.
derive := &deriveKeyCommand{
Path: testPath,
rootKey: &rootKey{},
}
err := os.Setenv(lnd.MnemonicEnvName, seedAezeedNoPassphrase)
require.NoError(t, err)
err = os.Setenv(lnd.PassphraseEnvName, "-")
require.NoError(t, err)
err = derive.Execute(nil, nil)
require.NoError(t, err)
h.assertLogContains(keyContent)
}
func TestDeriveKeyAezeedWithPassphrase(t *testing.T) {
h := newHarness(t)
// Derive a specific key from the serialized root key.
derive := &deriveKeyCommand{
Path: testPath,
rootKey: &rootKey{},
}
err := os.Setenv(lnd.MnemonicEnvName, seedAezeedWithPassphrase)
require.NoError(t, err)
err = os.Setenv(lnd.PassphraseEnvName, testPassPhrase)
require.NoError(t, err)
err = derive.Execute(nil, nil)
require.NoError(t, err)
h.assertLogContains(keyContent)
}
func TestDeriveKeySeedBip39(t *testing.T) {
h := newHarness(t)
// Derive a specific key from the serialized root key.
derive := &deriveKeyCommand{
Path: testPath,
rootKey: &rootKey{BIP39: true},
}
err := os.Setenv(btc.BIP39MnemonicEnvName, seedBip39)
require.NoError(t, err)
err = os.Setenv(btc.BIP39PassphraseEnvName, "-")
require.NoError(t, err)
err = derive.Execute(nil, nil)
require.NoError(t, err)
h.assertLogContains(keyContentBIP39)
}

@ -7,8 +7,8 @@ import (
func newDocCommand() *cobra.Command {
cmd := &cobra.Command{
Use: "doc",
Short: "Generate the markdown documentation of all commands",
Use: "doc",
Short: "Generate the markdown documentation of all commands",
Hidden: true,
RunE: func(cmd *cobra.Command, args []string) error {
return doc.GenMarkdownTree(rootCmd, "./doc")
@ -16,4 +16,4 @@ func newDocCommand() *cobra.Command {
}
return cmd
}
}

@ -64,9 +64,14 @@ func dumpChannelBackup(multiFile *chanbackup.MultiFile,
if err != nil {
return fmt.Errorf("could not extract multi file: %v", err)
}
spew.Dump(dump.BackupMulti{
content := dump.BackupMulti{
Version: multi.Version,
StaticBackups: dump.BackupDump(multi, chainParams),
})
}
spew.Dump(content)
// For the tests, also log as trace level which is disabled by default.
log.Tracef(spew.Sdump(content))
return nil
}

@ -0,0 +1,4 @@
package main
// This file is empty for now, the dumpbackup command is covered by the test in
// chanbackup_test.go.

@ -50,6 +50,7 @@ func (c *dumpChannelsCommand) Execute(_ *cobra.Command, _ []string) error {
if err != nil {
return fmt.Errorf("error opening rescue DB: %v", err)
}
defer func() { _ = db.Close() }()
if c.Closed {
return dumpClosedChannelInfo(db)
@ -69,6 +70,10 @@ func dumpOpenChannelInfo(chanDb *channeldb.DB) error {
}
spew.Dump(dumpChannels)
// For the tests, also log as trace level which is disabled by default.
log.Tracef(spew.Sdump(dumpChannels))
return nil
}
@ -84,5 +89,9 @@ func dumpClosedChannelInfo(chanDb *channeldb.DB) error {
}
spew.Dump(dumpChannels)
// For the tests, also log as trace level which is disabled by default.
log.Tracef(spew.Sdump(dumpChannels))
return nil
}

@ -0,0 +1,4 @@
package main
// This file is empty for now, the dumpchannels command is covered by the test
// in compactdb_test.go.

@ -96,7 +96,7 @@ func (c *genImportScriptCommand) Execute(_ *cobra.Command, _ []string) error {
return fmt.Errorf("error reading root key: %v", err)
}
// The btcwallet gives the birthday a slack of 48 hours, let's do the
// The btcwallet gives the birthday a slack of 48 hours, let's do the
// same.
if !birthday.IsZero() {
c.RescanFrom = btc.SeedBirthdayToBlock(

@ -26,7 +26,7 @@ func newRemoveChannelCommand() *cobra.Command {
Short: "Remove a single channel from the given channel DB",
Example: `chantools --channeldb ~/.lnd/data/graph/mainnet/channel.db \
--channel 3149764effbe82718b280de425277e5e7b245a4573aa4a0203ac12cee1c37816:0`,
RunE: cc.Execute,
RunE: cc.Execute,
}
cc.cmd.Flags().StringVar(
&cc.ChannelDB, "channeldb", "", "lnd channel.backup file to "+

@ -5,7 +5,7 @@ import (
"bytes"
"encoding/json"
"fmt"
"github.com/btcsuite/btcutil/hdkeychain"
"github.com/guggero/chantools/btc"
"io/ioutil"
"os"
"strings"
@ -14,6 +14,7 @@ import (
"github.com/btcsuite/btcd/chaincfg"
"github.com/btcsuite/btclog"
"github.com/btcsuite/btcutil/hdkeychain"
"github.com/guggero/chantools/dataformat"
"github.com/guggero/chantools/lnd"
"github.com/lightningnetwork/lnd/build"
@ -46,7 +47,7 @@ var rootCmd = &cobra.Command{
funds locked in lnd channels in case lnd itself cannot run properly anymore.
Complete documentation is available at https://github.com/guggero/chantools/.`,
Version: fmt.Sprintf("v%s, commit %s", version, Commit),
PreRun: func(cmd *cobra.Command, args []string) {
PersistentPreRun: func(cmd *cobra.Command, args []string) {
switch {
case Testnet:
chainParams = &chaincfg.TestNet3Params
@ -109,6 +110,7 @@ func main() {
type rootKey struct {
RootKey string
BIP39 bool
}
func newRootKey(cmd *cobra.Command, desc string) *rootKey {
@ -118,6 +120,11 @@ func newRootKey(cmd *cobra.Command, desc string) *rootKey {
"to use for "+desc+"; leave empty to prompt for "+
"lnd 24 word aezeed",
)
cmd.Flags().BoolVar(
&r.BIP39, "bip39", false, "read a classic BIP39 seed and "+
"passphrase from the terminal instead of asking for "+
"lnd seed format or providing the --rootkey flag",
)
return r
}
@ -136,6 +143,10 @@ func (r *rootKey) readWithBirthday() (*hdkeychain.ExtendedKey, time.Time,
extendedKey, err := hdkeychain.NewKeyFromString(r.RootKey)
return extendedKey, time.Unix(0, 0), err
case r.BIP39:
extendedKey, err := btc.ReadMnemonicFromTerminal(chainParams)
return extendedKey, time.Unix(0, 0), err
default:
return lnd.ReadAezeed(chainParams)
}
@ -249,7 +260,7 @@ func setupLogging() {
if err != nil {
panic(err)
}
err = build.ParseAndSetDebugLevels("trace", logWriter)
err = build.ParseAndSetDebugLevels("debug", logWriter)
if err != nil {
panic(err)
}

@ -0,0 +1,117 @@
package main
import (
"bytes"
"fmt"
"io/ioutil"
"os"
"path"
"regexp"
"testing"
"github.com/btcsuite/btcd/chaincfg"
"github.com/btcsuite/btclog"
"github.com/lightningnetwork/lnd/chanbackup"
"github.com/lightningnetwork/lnd/channeldb"
"github.com/stretchr/testify/require"
)
const (
seedAezeedNoPassphrase = "abandon kangaroo tribe spell brass entry " +
"argue buzz muffin total rug title autumn wish use bubble " +
"alarm rent machine hockey fork slam gaze tobacco"
seedAezeedWithPassphrase = "able pause keen exhibit duck olympic " +
"foot donor hire omit earth ribbon rotate cruise door orbit " +
"nephew mixture machine hockey fork scorpion shell door"
testPassPhrase = "testnet3"
seedBip39 = "uncover bargain diesel boss local host over divide " +
"orient cradle good crumble"
rootKeyAezeed = "tprv8ZgxMBicQKsPejNXQLJKe3dBBs9Zrt53EZrsBzVLQ8rZji3" +
"hVb3wcoRvgrjvTmjPG2ixoGUUkCyC6yBEy9T5gbLdvD2a5VmJbcFd5Q9pkAs"
rootKeyBip39 = "tprv8ZgxMBicQKsPdoVEZRN2MyzEgxGTqJepzhMc66b26zL1siLi" +
"WRQAGh9rAgPPJuQeHWWpgcDcS45yi6KBTFeGkQMEb2RNTrP11evJcB4UVSh"
rootKeyBip39Passphrase = ""
)
var (
datePattern = regexp.MustCompile(
"\\d{4}-\\d{2}-\\d{2} \\d{2}:\\d{2}:\\d{2}\\.\\d{3} ",
)
addressPattern = regexp.MustCompile("\\(0x[0-9a-f]{10}\\)")
)
type harness struct {
t *testing.T
logBuffer *bytes.Buffer
logger btclog.Logger
tempDir string
}
func newHarness(t *testing.T) *harness {
buf := &bytes.Buffer{}
logBackend := btclog.NewBackend(buf)
tempDir, err := ioutil.TempDir("", "chantools")
require.NoError(t, err)
h := &harness{
t: t,
logBuffer: buf,
logger: logBackend.Logger("CHAN"),
tempDir: tempDir,
}
h.logger.SetLevel(btclog.LevelTrace)
log = h.logger
channeldb.UseLogger(h.logger)
chanbackup.UseLogger(h.logger)
os.Clearenv()
chainParams = &chaincfg.RegressionNetParams
return h
}
func (h *harness) getLog() string {
return h.logBuffer.String()
}
func (h *harness) clearLog() {
h.logBuffer.Reset()
}
func (h *harness) assertLogContains(format string, args ...interface{}) {
h.t.Helper()
require.Contains(h.t, h.logBuffer.String(), fmt.Sprintf(format, args...))
}
func (h *harness) assertLogEqual(a, b string) {
// Remove all timestamps and all memory addresses from dumps as those
// are always different.
a = datePattern.ReplaceAllString(a, "")
a = addressPattern.ReplaceAllString(a, "")
b = datePattern.ReplaceAllString(b, "")
b = addressPattern.ReplaceAllString(b, "")
require.Equal(h.t, a, b)
}
func (h *harness) testdataFile(name string) string {
workingDir, err := os.Getwd()
require.NoError(h.t, err)
return path.Join(workingDir, "testdata", name)
}
func (h *harness) tempFile(name string) string {
return path.Join(h.tempDir, name)
}
func (h *harness) fileSize(name string) int64 {
stat, err := os.Stat(name)
require.NoError(h.t, err)
return stat.Size()
}

@ -3,14 +3,14 @@ package main
import (
"fmt"
"github.com/btcsuite/btcutil/hdkeychain"
"github.com/guggero/chantools/btc"
"github.com/spf13/cobra"
)
type showRootKeyCommand struct {
BIP39 bool
const showRootKeyFormat = `
Your BIP32 HD root key is: %v
`
type showRootKeyCommand struct {
rootKey *rootKey
cmd *cobra.Command
}
@ -27,11 +27,6 @@ commands of this tool.`,
Example: `chantools showrootkey`,
RunE: cc.Execute,
}
cc.cmd.Flags().BoolVar(
&cc.BIP39, "bip39", false, "read a classic BIP39 seed and "+
"passphrase from the terminal instead of asking for "+
"lnd seed format or providing the --rootkey flag",
)
cc.rootKey = newRootKey(cc.cmd, "decrypting the backup")
@ -39,23 +34,16 @@ commands of this tool.`,
}
func (c *showRootKeyCommand) Execute(_ *cobra.Command, _ []string) error {
var (
extendedKey *hdkeychain.ExtendedKey
err error
)
// Check that root key is valid or fall back to terminal input.
switch {
case c.BIP39:
extendedKey, err = btc.ReadMnemonicFromTerminal(chainParams)
default:
extendedKey, err = c.rootKey.read()
}
extendedKey, err := c.rootKey.read()
if err != nil {
return fmt.Errorf("error reading root key: %v", err)
}
fmt.Printf("\nYour BIP32 HD root key is: %s\n", extendedKey.String())
result := fmt.Sprintf(showRootKeyFormat, extendedKey)
fmt.Printf(result)
// For the tests, also log as trace level which is disabled by default.
log.Tracef(result)
return nil
}

@ -0,0 +1,67 @@
package main
import (
"github.com/guggero/chantools/btc"
"os"
"testing"
"github.com/guggero/chantools/lnd"
"github.com/stretchr/testify/require"
)
func TestShowRootKey(t *testing.T) {
h := newHarness(t)
// Derive the root key from the aezeed.
show := &showRootKeyCommand{
rootKey: &rootKey{},
}
err := os.Setenv(lnd.MnemonicEnvName, seedAezeedNoPassphrase)
require.NoError(t, err)
err = os.Setenv(lnd.PassphraseEnvName, "-")
require.NoError(t, err)
err = show.Execute(nil, nil)
require.NoError(t, err)
h.assertLogContains(rootKeyAezeed)
}
func TestShowRootKeyBIP39(t *testing.T) {
h := newHarness(t)
// Derive the root key from the BIP39 seed.
show := &showRootKeyCommand{
rootKey: &rootKey{BIP39: true},
}
err := os.Setenv(btc.BIP39MnemonicEnvName, seedBip39)
require.NoError(t, err)
err = os.Setenv(btc.BIP39PassphraseEnvName, "-")
require.NoError(t, err)
err = show.Execute(nil, nil)
require.NoError(t, err)
h.assertLogContains(rootKeyBip39)
}
func TestShowRootKeyBIP39WithPassphre(t *testing.T) {
h := newHarness(t)
// Derive the root key from the BIP39 seed.
show := &showRootKeyCommand{
rootKey: &rootKey{BIP39: true},
}
err := os.Setenv(btc.BIP39MnemonicEnvName, seedBip39)
require.NoError(t, err)
err = os.Setenv(btc.BIP39PassphraseEnvName, testPassPhrase)
require.NoError(t, err)
err = show.Execute(nil, nil)
require.NoError(t, err)
h.assertLogContains(rootKeyBip39Passphrase)
}

Binary file not shown.

Binary file not shown.

@ -51,7 +51,7 @@ phone]
</pre>
`,
Example: `chantools vanitygen --prefix 022222 --threads 8`,
RunE: cc.Execute,
RunE: cc.Execute,
}
cc.cmd.Flags().StringVar(
&cc.Prefix, "prefix", "", "hex encoded prefix to find in node "+

@ -1,11 +1,8 @@
package main
import (
"encoding/hex"
"fmt"
"os"
"os/user"
"path/filepath"
"strings"
"github.com/btcsuite/btcd/btcec"
@ -15,6 +12,7 @@ import (
"github.com/btcsuite/btcwallet/walletdb"
"github.com/guggero/chantools/lnd"
"github.com/lightningnetwork/lnd/keychain"
"github.com/lightningnetwork/lnd/lncfg"
"github.com/lightningnetwork/lnd/lnwallet"
"github.com/spf13/cobra"
@ -27,6 +25,19 @@ import (
const (
passwordEnvName = "WALLET_PASSWORD"
walletInfoFormat = `
Identity Pubkey: %x
BIP32 HD extended root key: %s
Wallet scopes:
%s
`
keyScopeformat = `
Scope: m/%d'/%d'
Number of internal %s addresses: %d
Number of external %s addresses: %d
`
)
var (
@ -126,13 +137,14 @@ func (c *walletInfoCommand) Execute(_ *cobra.Command, _ []string) error {
// Try to load and open the wallet.
db, err := walletdb.Open(
"bdb", cleanAndExpandPath(c.WalletDB), false,
"bdb", lncfg.CleanAndExpandPath(c.WalletDB), false,
lnd.DefaultOpenTimeout,
)
if err != nil {
return fmt.Errorf("error opening wallet database: %v", err)
}
defer closeWalletDb(db)
defer func() { _ = db.Close() }()
w, err := wallet.Open(db, publicWalletPw, openCallbacks, chainParams, 0)
if err != nil {
return err
@ -147,21 +159,33 @@ func (c *walletInfoCommand) Execute(_ *cobra.Command, _ []string) error {
}
// Print the wallet info and if requested the root key.
err = walletInfo(w)
identityKey, scopeInfo, err := walletInfo(w)
if err != nil {
return err
}
rootKey := "n/a"
if c.WithRootKey {
masterHDPrivKey, err := decryptRootKey(db, privateWalletPw)
if err != nil {
return err
}
fmt.Printf("BIP32 HD extended root key: %s\n", masterHDPrivKey)
rootKey = string(masterHDPrivKey)
}
result := fmt.Sprintf(
walletInfoFormat, identityKey.SerializeCompressed(), rootKey,
scopeInfo,
)
fmt.Printf(result)
// For the tests, also log as trace level which is disabled by default.
log.Tracef(result)
return nil
}
func walletInfo(w *wallet.Wallet) error {
func walletInfo(w *wallet.Wallet) (*btcec.PublicKey, string, error) {
keyRing := keychain.NewBtcWalletKeyRing(w, chainParams.HDCoinType)
idPrivKey, err := keyRing.DerivePrivKey(keychain.KeyDescriptor{
KeyLocator: keychain.KeyLocator{
@ -170,47 +194,48 @@ func walletInfo(w *wallet.Wallet) error {
},
})
if err != nil {
return fmt.Errorf("unable to open key ring for coin type %d: "+
"%v", chainParams.HDCoinType, err)
return nil, "", fmt.Errorf("unable to open key ring for coin "+
"type %d: %v", chainParams.HDCoinType, err)
}
idPrivKey.Curve = btcec.S256()
fmt.Printf(
"Identity Pubkey: %s\n",
hex.EncodeToString(idPrivKey.PubKey().SerializeCompressed()),
)
// Print information about the different addresses in use.
printScopeInfo(
"np2wkh", w,
w.Manager.ScopesForExternalAddrType(
// Collect information about the different addresses in use.
scopeNp2wkh, err := printScopeInfo(
"np2wkh", w, w.Manager.ScopesForExternalAddrType(
waddrmgr.NestedWitnessPubKey,
),
)
printScopeInfo(
"p2wkh", w,
w.Manager.ScopesForExternalAddrType(
if err != nil {
return nil, "", err
}
scopeP2wkh, err := printScopeInfo(
"p2wkh", w, w.Manager.ScopesForExternalAddrType(
waddrmgr.WitnessPubKey,
),
)
return nil
if err != nil {
return nil, "", err
}
return idPrivKey.PubKey(), scopeNp2wkh + scopeP2wkh, nil
}
func printScopeInfo(name string, w *wallet.Wallet, scopes []waddrmgr.KeyScope) {
func printScopeInfo(name string, w *wallet.Wallet,
scopes []waddrmgr.KeyScope) (string, error) {
scopeInfo := ""
for _, scope := range scopes {
props, err := w.AccountProperties(scope, defaultAccount)
if err != nil {
fmt.Printf("Error fetching account properties: %v", err)
return "", fmt.Errorf("error fetching account "+
"properties: %v", err)
}
fmt.Printf("Scope: %s\n", scope.String())
fmt.Printf(
" Number of internal (change) %s addresses: %d\n",
name, props.InternalKeyCount,
)
fmt.Printf(
" Number of external %s addresses: %d\n", name,
props.ExternalKeyCount,
scopeInfo += fmt.Sprintf(
keyScopeformat, scope.Purpose, scope.Coin, name,
props.InternalKeyCount, name, props.ExternalKeyCount,
)
}
return scopeInfo, nil
}
func decryptRootKey(db walletdb.DB, privPassphrase []byte) ([]byte, error) {
@ -222,18 +247,14 @@ func decryptRootKey(db walletdb.DB, privPassphrase []byte) ([]byte, error) {
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,
)
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,
)
return fmt.Errorf("bucket '%s' does not exist",
mainBucketName)
}
val := mainBucket.Get(masterPrivKeyName)
@ -251,6 +272,7 @@ func decryptRootKey(db walletdb.DB, privPassphrase []byte) ([]byte, error) {
masterHDPrivEnc = make([]byte, len(val))
copy(masterHDPrivEnc, val)
}
return nil
})
if err != nil {
@ -276,36 +298,3 @@ func decryptRootKey(db walletdb.DB, privPassphrase []byte) ([]byte, error) {
copy(cryptoKeyPriv[:], cryptoKeyPrivBytes)
return cryptoKeyPriv.Decrypt(masterHDPrivEnc)
}
func closeWalletDb(db walletdb.DB) {
err := db.Close()
if err != nil {
fmt.Printf("Error closing database: %v", err)
}
}
// cleanAndExpandPath expands environment variables and leading ~ in the
// passed path, cleans the result, and returns it.
// This function is taken from https://github.com/btcsuite/btcd
func cleanAndExpandPath(path string) string {
if path == "" {
return ""
}
// Expand initial ~ to OS specific home directory.
if strings.HasPrefix(path, "~") {
var homeDir string
u, err := user.Current()
if err == nil {
homeDir = u.HomeDir
} else {
homeDir = os.Getenv("HOME")
}
path = strings.Replace(path, "~", homeDir, 1)
}
// NOTE: The os.ExpandEnv doesn't work with Windows-style %VARIABLE%,
// but the variables can still be expanded via POSIX-style $VARIABLE.
return filepath.Clean(os.ExpandEnv(path))
}

@ -0,0 +1,32 @@
package main
import (
"os"
"testing"
"github.com/stretchr/testify/require"
)
const (
walletContent = "03b99ab108e39e9e4cf565c1b706480180a70a4fdc4828e44c50" +
"4530c056be5b5f"
)
func TestWalletInfo(t *testing.T) {
h := newHarness(t)
// Dump the wallet information.
info := &walletInfoCommand{
WalletDB: h.testdataFile("wallet.db"),
WithRootKey: true,
}
err := os.Setenv(passwordEnvName, testPassPhrase)
require.NoError(t, err)
err = info.Execute(nil, nil)
require.NoError(t, err)
h.assertLogContains(walletContent)
h.assertLogContains(rootKeyAezeed)
}

@ -17,6 +17,7 @@ require (
github.com/ltcsuite/ltcd v0.0.0-20191228044241-92166e412499 // indirect
github.com/miekg/dns v1.1.26 // indirect
github.com/spf13/cobra v1.1.1
github.com/stretchr/testify v1.6.1
go.etcd.io/bbolt v1.3.5-0.20200615073812-232d8fc87f50
golang.org/x/crypto v0.0.0-20200709230013-948cd5f35899
)

@ -568,6 +568,7 @@ github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UV
github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4=
github.com/stretchr/testify v1.5.1 h1:nOGnQDM7FYENwehXlg/kFVnos3rEvtKTjRvOWSzb6H4=
github.com/stretchr/testify v1.5.1/go.mod h1:5W2xD1RspED5o8YsWQXVCued0rvSQ+mT+I5cxcmMvtA=
github.com/stretchr/testify v1.6.1 h1:hDPOHmpOpP40lSULcqw7IrRb/u7w6RpDC9399XyoNd0=
github.com/stretchr/testify v1.6.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
github.com/subosito/gotenv v1.2.0 h1:Slr1R9HxAlEKefgq5jn9U+DnETlIUa6HfgEzj0g5d7s=
github.com/subosito/gotenv v1.2.0/go.mod h1:N0PQaV/YGNqwC0u51sEeR/aUtSLEXKX9iv69rRypqCw=
@ -858,6 +859,7 @@ gopkg.in/yaml.v2 v2.2.8/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
gopkg.in/yaml.v2 v2.3.0/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
gopkg.in/yaml.v2 v2.4.0 h1:D8xgwECY7CYvx+Y2n4sBz93Jn9JRvxdiyyo8CTfuKaY=
gopkg.in/yaml.v2 v2.4.0/go.mod h1:RDklbk79AGWmwhnvt/jBztapEOGDOx6ZbXqjP6csGnQ=
gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c h1:dUUwHk2QECo/6vqA44rthZ8ie2QXMNeKRTHCNY2nXvo=
gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
honnef.co/go/tools v0.0.0-20180728063816-88497007e858/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4=
honnef.co/go/tools v0.0.0-20190102054323-c2f93a96b099/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4=

@ -16,8 +16,8 @@ import (
)
const (
memonicEnvName = "AEZEED_MNEMONIC"
passphraseEnvName = "AEZEED_PASSPHRASE"
MnemonicEnvName = "AEZEED_MNEMONIC"
PassphraseEnvName = "AEZEED_PASSPHRASE"
)
var (
@ -30,7 +30,7 @@ func ReadAezeed(params *chaincfg.Params) (*hdkeychain.ExtendedKey, time.Time,
// To automate things with chantools, we also offer reading the seed
// from environment variables.
mnemonicStr := strings.TrimSpace(os.Getenv(memonicEnvName))
mnemonicStr := strings.TrimSpace(os.Getenv(MnemonicEnvName))
// If nothing is set in the environment, read the seed from the
// terminal.
@ -70,7 +70,7 @@ func ReadAezeed(params *chaincfg.Params) (*hdkeychain.ExtendedKey, time.Time,
// 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.
passphrase := strings.TrimSpace(os.Getenv(passphraseEnvName))
passphrase := strings.TrimSpace(os.Getenv(PassphraseEnvName))
// Because we cannot differentiate between an empty and a non-existent
// environment variable, we need a special character that indicates that

Loading…
Cancel
Save