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" "encoding/hex"
"errors" "errors"
"fmt" "fmt"
"os"
"strings"
"syscall"
"github.com/btcsuite/btcd/chaincfg" "github.com/btcsuite/btcd/chaincfg"
"github.com/btcsuite/btcutil/hdkeychain" "github.com/btcsuite/btcutil/hdkeychain"
"github.com/guggero/chantools/bip39" "github.com/guggero/chantools/bip39"
"golang.org/x/crypto/pbkdf2" "golang.org/x/crypto/pbkdf2"
"golang.org/x/crypto/ssh/terminal" "golang.org/x/crypto/ssh/terminal"
"os"
"strings"
"syscall"
)
const (
BIP39MnemonicEnvName = "SEED_MNEMONIC"
BIP39PassphraseEnvName = "SEED_PASSPHRASE"
) )
func ReadMnemonicFromTerminal(params *chaincfg.Params) (*hdkeychain.ExtendedKey, func ReadMnemonicFromTerminal(params *chaincfg.Params) (*hdkeychain.ExtendedKey,
error) { error) {
// We'll now prompt the user to enter in their 12 to 24 word mnemonic. var err error
fmt.Printf("Input your 12 to 24 word mnemonic separated by spaces: ")
reader := bufio.NewReader(os.Stdin) reader := bufio.NewReader(os.Stdin)
mnemonicStr, err := reader.ReadString('\n')
if err != nil { // To automate things with chantools, we also offer reading the seed
return nil, err // 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 // We'll trim off extra spaces, and ensure the mnemonic is all
// lower case. // lower case.
@ -40,62 +54,87 @@ func ReadMnemonicFromTerminal(params *chaincfg.Params) (*hdkeychain.ExtendedKey,
"must be between 12 and 24 words") "must be between 12 and 24 words")
} }
// Additionally, the user may have a passphrase, that will also // Additionally, the user may have a passphrase, that will also need to
// need to be provided so the daemon can properly decipher the // be provided so the daemon can properly decipher the cipher seed.
// cipher seed. // Try the environment variable first.
fmt.Printf("Input your cipher seed passphrase (press enter if " + passphrase := strings.TrimSpace(os.Getenv(BIP39PassphraseEnvName))
"your seed doesn't have a passphrase): ")
passphrase, err := terminal.ReadPassword(int(syscall.Stdin)) // nolint
if err != nil {
return nil, err
}
fmt.Println()
// Check that the mnemonic is valid. // Because we cannot differentiate between an empty and a non-existent
_, err = bip39.EntropyFromMnemonic(mnemonicStr) // environment variable, we need a special character that indicates that
if err != nil { // no passphrase should be used. We use a single dash (-) for that as
return nil, err // 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 // The environment variable didn't contain anything, we'll read the
fmt.Printf("Please choose passphrase mode:\n" + // passphrase from the terminal.
" 0 - Default BIP39\n" + case passphrase == "":
" 1 - Passphrase to hex\n" + // Additionally, the user may have a passphrase, that will also
" 2 - Digital Bitbox (extra round of PBKDF2)\n" + // need to be provided so the daemon can properly decipher the
"\n" + // cipher seed.
"Choice [default 0]: ") fmt.Printf("Input your cipher seed passphrase (press enter " +
choice, err := reader.ReadString('\n') "if your seed doesn't have a passphrase): ")
if err != nil { passphraseBytes, err = terminal.ReadPassword(
return nil, err 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) { switch strings.TrimSpace(choice) {
case "", "0": case "", "0":
seed = pbkdf2.Key( seed = pbkdf2.Key(
[]byte(mnemonicStr), append( []byte(mnemonicStr), append(
[]byte("mnemonic"), passphrase..., []byte("mnemonic"), passphraseBytes...,
), 2048, 64, sha512.New, ), 2048, 64, sha512.New,
) )
case "1": case "1":
passphrase = []byte(hex.EncodeToString(passphrase)) p := []byte(hex.EncodeToString(passphraseBytes))
seed = pbkdf2.Key( seed = pbkdf2.Key(
[]byte(mnemonicStr), append( []byte(mnemonicStr), append([]byte("mnemonic"), p...),
[]byte("mnemonic"), passphrase..., 2048, 64, sha512.New,
), 2048, 64, sha512.New,
) )
case "2": case "2":
passphrase = pbkdf2.Key( p := hex.EncodeToString(pbkdf2.Key(
passphrase, []byte("Digital Bitbox"), 20480, 64, passphraseBytes, []byte("Digital Bitbox"), 20480, 64,
sha512.New, sha512.New,
) ))
passphrase = []byte(hex.EncodeToString(passphrase))
seed = pbkdf2.Key( seed = pbkdf2.Key(
[]byte(mnemonicStr), append( []byte(mnemonicStr), append([]byte("mnemonic"), p...),
[]byte("mnemonic"), passphrase..., 2048, 64, sha512.New,
), 2048, 64, sha512.New,
) )
default: 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 { if err != nil {
return fmt.Errorf("error opening source DB: %v", err) return fmt.Errorf("error opening source DB: %v", err)
} }
defer func() { _ = src.Close() }()
dst, err := c.openDB(c.DestDB, false) dst, err := c.openDB(c.DestDB, false)
if err != nil { if err != nil {
return fmt.Errorf("error opening destination DB: %v", err) return fmt.Errorf("error opening destination DB: %v", err)
} }
defer func() { _ = dst.Close() }()
err = c.compact(dst, src) err = c.compact(dst, src)
if err != nil { if err != nil {
return fmt.Errorf("error compacting DB: %v", err) 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 ( import (
"fmt" "fmt"
"github.com/btcsuite/btcutil" "github.com/btcsuite/btcutil"
"github.com/btcsuite/btcutil/hdkeychain" "github.com/btcsuite/btcutil/hdkeychain"
"github.com/guggero/chantools/lnd" "github.com/guggero/chantools/lnd"
"github.com/spf13/cobra" "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 { type deriveKeyCommand struct {
BIP39 bool
Path string Path string
Neuter bool 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`, --path "m/1017'/0'/5'/0/0'" --neuter`,
RunE: cc.Execute, 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.cmd.Flags().StringVar(
&cc.Path, "path", "", "BIP32 derivation path to derive; must "+ &cc.Path, "path", "", "BIP32 derivation path to derive; must "+
"start with \"m/\"", "start with \"m/\"",
@ -60,7 +64,6 @@ func (c *deriveKeyCommand) Execute(_ *cobra.Command, _ []string) error {
func deriveKey(extendedKey *hdkeychain.ExtendedKey, path string, func deriveKey(extendedKey *hdkeychain.ExtendedKey, path string,
neuter bool) error { neuter bool) error {
fmt.Printf("Deriving path %s for network %s.\n", path, chainParams.Name)
child, pubKey, wif, err := lnd.DeriveKey(extendedKey, path, chainParams) child, pubKey, wif, err := lnd.DeriveKey(extendedKey, path, chainParams)
if err != nil { if err != nil {
return fmt.Errorf("could not derive keys: %v", err) return fmt.Errorf("could not derive keys: %v", err)
@ -69,8 +72,6 @@ func deriveKey(extendedKey *hdkeychain.ExtendedKey, path string,
if err != nil { if err != nil {
return fmt.Errorf("could not neuter child key: %v", err) 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. // Print the address too.
hash160 := btcutil.Hash160(pubKey.SerializeCompressed()) hash160 := btcutil.Hash160(pubKey.SerializeCompressed())
@ -84,13 +85,21 @@ func deriveKey(extendedKey *hdkeychain.ExtendedKey, path string,
if err != nil { if err != nil {
return fmt.Errorf("could not create address: %v", err) 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 { if !neuter {
fmt.Printf("\nPrivate key (WIF): %s\n", wif.String()) privKey, xPriv = wif.String(), child.String()
fmt.Printf("Extended private key (xprv): %s\n", 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 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 { func newDocCommand() *cobra.Command {
cmd := &cobra.Command{ cmd := &cobra.Command{
Use: "doc", Use: "doc",
Short: "Generate the markdown documentation of all commands", Short: "Generate the markdown documentation of all commands",
Hidden: true, Hidden: true,
RunE: func(cmd *cobra.Command, args []string) error { RunE: func(cmd *cobra.Command, args []string) error {
return doc.GenMarkdownTree(rootCmd, "./doc") return doc.GenMarkdownTree(rootCmd, "./doc")
@ -16,4 +16,4 @@ func newDocCommand() *cobra.Command {
} }
return cmd return cmd
} }

@ -64,9 +64,14 @@ func dumpChannelBackup(multiFile *chanbackup.MultiFile,
if err != nil { if err != nil {
return fmt.Errorf("could not extract multi file: %v", err) return fmt.Errorf("could not extract multi file: %v", err)
} }
spew.Dump(dump.BackupMulti{ content := dump.BackupMulti{
Version: multi.Version, Version: multi.Version,
StaticBackups: dump.BackupDump(multi, chainParams), 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 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 { if err != nil {
return fmt.Errorf("error opening rescue DB: %v", err) return fmt.Errorf("error opening rescue DB: %v", err)
} }
defer func() { _ = db.Close() }()
if c.Closed { if c.Closed {
return dumpClosedChannelInfo(db) return dumpClosedChannelInfo(db)
@ -69,6 +70,10 @@ func dumpOpenChannelInfo(chanDb *channeldb.DB) error {
} }
spew.Dump(dumpChannels) spew.Dump(dumpChannels)
// For the tests, also log as trace level which is disabled by default.
log.Tracef(spew.Sdump(dumpChannels))
return nil return nil
} }
@ -84,5 +89,9 @@ func dumpClosedChannelInfo(chanDb *channeldb.DB) error {
} }
spew.Dump(dumpChannels) spew.Dump(dumpChannels)
// For the tests, also log as trace level which is disabled by default.
log.Tracef(spew.Sdump(dumpChannels))
return nil 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) 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. // same.
if !birthday.IsZero() { if !birthday.IsZero() {
c.RescanFrom = btc.SeedBirthdayToBlock( c.RescanFrom = btc.SeedBirthdayToBlock(

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

@ -5,7 +5,7 @@ import (
"bytes" "bytes"
"encoding/json" "encoding/json"
"fmt" "fmt"
"github.com/btcsuite/btcutil/hdkeychain" "github.com/guggero/chantools/btc"
"io/ioutil" "io/ioutil"
"os" "os"
"strings" "strings"
@ -14,6 +14,7 @@ import (
"github.com/btcsuite/btcd/chaincfg" "github.com/btcsuite/btcd/chaincfg"
"github.com/btcsuite/btclog" "github.com/btcsuite/btclog"
"github.com/btcsuite/btcutil/hdkeychain"
"github.com/guggero/chantools/dataformat" "github.com/guggero/chantools/dataformat"
"github.com/guggero/chantools/lnd" "github.com/guggero/chantools/lnd"
"github.com/lightningnetwork/lnd/build" "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. funds locked in lnd channels in case lnd itself cannot run properly anymore.
Complete documentation is available at https://github.com/guggero/chantools/.`, Complete documentation is available at https://github.com/guggero/chantools/.`,
Version: fmt.Sprintf("v%s, commit %s", version, Commit), Version: fmt.Sprintf("v%s, commit %s", version, Commit),
PreRun: func(cmd *cobra.Command, args []string) { PersistentPreRun: func(cmd *cobra.Command, args []string) {
switch { switch {
case Testnet: case Testnet:
chainParams = &chaincfg.TestNet3Params chainParams = &chaincfg.TestNet3Params
@ -109,6 +110,7 @@ func main() {
type rootKey struct { type rootKey struct {
RootKey string RootKey string
BIP39 bool
} }
func newRootKey(cmd *cobra.Command, desc string) *rootKey { 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 "+ "to use for "+desc+"; leave empty to prompt for "+
"lnd 24 word aezeed", "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 return r
} }
@ -136,6 +143,10 @@ func (r *rootKey) readWithBirthday() (*hdkeychain.ExtendedKey, time.Time,
extendedKey, err := hdkeychain.NewKeyFromString(r.RootKey) extendedKey, err := hdkeychain.NewKeyFromString(r.RootKey)
return extendedKey, time.Unix(0, 0), err return extendedKey, time.Unix(0, 0), err
case r.BIP39:
extendedKey, err := btc.ReadMnemonicFromTerminal(chainParams)
return extendedKey, time.Unix(0, 0), err
default: default:
return lnd.ReadAezeed(chainParams) return lnd.ReadAezeed(chainParams)
} }
@ -249,7 +260,7 @@ func setupLogging() {
if err != nil { if err != nil {
panic(err) panic(err)
} }
err = build.ParseAndSetDebugLevels("trace", logWriter) err = build.ParseAndSetDebugLevels("debug", logWriter)
if err != nil { if err != nil {
panic(err) 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 ( import (
"fmt" "fmt"
"github.com/btcsuite/btcutil/hdkeychain"
"github.com/guggero/chantools/btc"
"github.com/spf13/cobra" "github.com/spf13/cobra"
) )
type showRootKeyCommand struct { const showRootKeyFormat = `
BIP39 bool Your BIP32 HD root key is: %v
`
type showRootKeyCommand struct {
rootKey *rootKey rootKey *rootKey
cmd *cobra.Command cmd *cobra.Command
} }
@ -27,11 +27,6 @@ commands of this tool.`,
Example: `chantools showrootkey`, Example: `chantools showrootkey`,
RunE: cc.Execute, 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") cc.rootKey = newRootKey(cc.cmd, "decrypting the backup")
@ -39,23 +34,16 @@ commands of this tool.`,
} }
func (c *showRootKeyCommand) Execute(_ *cobra.Command, _ []string) error { func (c *showRootKeyCommand) Execute(_ *cobra.Command, _ []string) error {
var ( extendedKey, err := c.rootKey.read()
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()
}
if err != nil { if err != nil {
return fmt.Errorf("error reading root key: %v", err) 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 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> </pre>
`, `,
Example: `chantools vanitygen --prefix 022222 --threads 8`, Example: `chantools vanitygen --prefix 022222 --threads 8`,
RunE: cc.Execute, RunE: cc.Execute,
} }
cc.cmd.Flags().StringVar( cc.cmd.Flags().StringVar(
&cc.Prefix, "prefix", "", "hex encoded prefix to find in node "+ &cc.Prefix, "prefix", "", "hex encoded prefix to find in node "+

@ -1,11 +1,8 @@
package main package main
import ( import (
"encoding/hex"
"fmt" "fmt"
"os" "os"
"os/user"
"path/filepath"
"strings" "strings"
"github.com/btcsuite/btcd/btcec" "github.com/btcsuite/btcd/btcec"
@ -15,6 +12,7 @@ import (
"github.com/btcsuite/btcwallet/walletdb" "github.com/btcsuite/btcwallet/walletdb"
"github.com/guggero/chantools/lnd" "github.com/guggero/chantools/lnd"
"github.com/lightningnetwork/lnd/keychain" "github.com/lightningnetwork/lnd/keychain"
"github.com/lightningnetwork/lnd/lncfg"
"github.com/lightningnetwork/lnd/lnwallet" "github.com/lightningnetwork/lnd/lnwallet"
"github.com/spf13/cobra" "github.com/spf13/cobra"
@ -27,6 +25,19 @@ import (
const ( const (
passwordEnvName = "WALLET_PASSWORD" 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 ( var (
@ -126,13 +137,14 @@ func (c *walletInfoCommand) Execute(_ *cobra.Command, _ []string) error {
// Try to load and open the wallet. // Try to load and open the wallet.
db, err := walletdb.Open( db, err := walletdb.Open(
"bdb", cleanAndExpandPath(c.WalletDB), false, "bdb", lncfg.CleanAndExpandPath(c.WalletDB), false,
lnd.DefaultOpenTimeout, lnd.DefaultOpenTimeout,
) )
if err != nil { if err != nil {
return fmt.Errorf("error opening wallet database: %v", err) 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) w, err := wallet.Open(db, publicWalletPw, openCallbacks, chainParams, 0)
if err != nil { if err != nil {
return err return err
@ -147,21 +159,33 @@ func (c *walletInfoCommand) Execute(_ *cobra.Command, _ []string) error {
} }
// Print the wallet info and if requested the root key. // Print the wallet info and if requested the root key.
err = walletInfo(w) identityKey, scopeInfo, err := walletInfo(w)
if err != nil { if err != nil {
return err return err
} }
rootKey := "n/a"
if c.WithRootKey { if c.WithRootKey {
masterHDPrivKey, err := decryptRootKey(db, privateWalletPw) masterHDPrivKey, err := decryptRootKey(db, privateWalletPw)
if err != nil { if err != nil {
return err 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 return nil
} }
func walletInfo(w *wallet.Wallet) error { func walletInfo(w *wallet.Wallet) (*btcec.PublicKey, string, error) {
keyRing := keychain.NewBtcWalletKeyRing(w, chainParams.HDCoinType) keyRing := keychain.NewBtcWalletKeyRing(w, chainParams.HDCoinType)
idPrivKey, err := keyRing.DerivePrivKey(keychain.KeyDescriptor{ idPrivKey, err := keyRing.DerivePrivKey(keychain.KeyDescriptor{
KeyLocator: keychain.KeyLocator{ KeyLocator: keychain.KeyLocator{
@ -170,47 +194,48 @@ func walletInfo(w *wallet.Wallet) error {
}, },
}) })
if err != nil { if err != nil {
return fmt.Errorf("unable to open key ring for coin type %d: "+ return nil, "", fmt.Errorf("unable to open key ring for coin "+
"%v", chainParams.HDCoinType, err) "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. // Collect information about the different addresses in use.
printScopeInfo( scopeNp2wkh, err := printScopeInfo(
"np2wkh", w, "np2wkh", w, w.Manager.ScopesForExternalAddrType(
w.Manager.ScopesForExternalAddrType(
waddrmgr.NestedWitnessPubKey, waddrmgr.NestedWitnessPubKey,
), ),
) )
printScopeInfo( if err != nil {
"p2wkh", w, return nil, "", err
w.Manager.ScopesForExternalAddrType( }
scopeP2wkh, err := printScopeInfo(
"p2wkh", w, w.Manager.ScopesForExternalAddrType(
waddrmgr.WitnessPubKey, 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 { for _, scope := range scopes {
props, err := w.AccountProperties(scope, defaultAccount) props, err := w.AccountProperties(scope, defaultAccount)
if err != nil { 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()) scopeInfo += fmt.Sprintf(
fmt.Printf( keyScopeformat, scope.Purpose, scope.Coin, name,
" Number of internal (change) %s addresses: %d\n", props.InternalKeyCount, name, props.ExternalKeyCount,
name, props.InternalKeyCount,
)
fmt.Printf(
" Number of external %s addresses: %d\n", name,
props.ExternalKeyCount,
) )
} }
return scopeInfo, nil
} }
func decryptRootKey(db walletdb.DB, privPassphrase []byte) ([]byte, error) { 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 { err := walletdb.View(db, func(tx walletdb.ReadTx) error {
ns := tx.ReadBucket(waddrmgrNamespaceKey) ns := tx.ReadBucket(waddrmgrNamespaceKey)
if ns == nil { if ns == nil {
return fmt.Errorf( return fmt.Errorf("namespace '%s' does not exist",
"namespace '%s' does not exist", waddrmgrNamespaceKey)
waddrmgrNamespaceKey,
)
} }
mainBucket := ns.NestedReadBucket(mainBucketName) mainBucket := ns.NestedReadBucket(mainBucketName)
if mainBucket == nil { if mainBucket == nil {
return fmt.Errorf( return fmt.Errorf("bucket '%s' does not exist",
"bucket '%s' does not exist", mainBucketName)
mainBucketName,
)
} }
val := mainBucket.Get(masterPrivKeyName) val := mainBucket.Get(masterPrivKeyName)
@ -251,6 +272,7 @@ func decryptRootKey(db walletdb.DB, privPassphrase []byte) ([]byte, error) {
masterHDPrivEnc = make([]byte, len(val)) masterHDPrivEnc = make([]byte, len(val))
copy(masterHDPrivEnc, val) copy(masterHDPrivEnc, val)
} }
return nil return nil
}) })
if err != nil { if err != nil {
@ -276,36 +298,3 @@ func decryptRootKey(db walletdb.DB, privPassphrase []byte) ([]byte, error) {
copy(cryptoKeyPriv[:], cryptoKeyPrivBytes) copy(cryptoKeyPriv[:], cryptoKeyPrivBytes)
return cryptoKeyPriv.Decrypt(masterHDPrivEnc) 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/ltcsuite/ltcd v0.0.0-20191228044241-92166e412499 // indirect
github.com/miekg/dns v1.1.26 // indirect github.com/miekg/dns v1.1.26 // indirect
github.com/spf13/cobra v1.1.1 github.com/spf13/cobra v1.1.1
github.com/stretchr/testify v1.6.1
go.etcd.io/bbolt v1.3.5-0.20200615073812-232d8fc87f50 go.etcd.io/bbolt v1.3.5-0.20200615073812-232d8fc87f50
golang.org/x/crypto v0.0.0-20200709230013-948cd5f35899 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.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4=
github.com/stretchr/testify v1.5.1 h1:nOGnQDM7FYENwehXlg/kFVnos3rEvtKTjRvOWSzb6H4= 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.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/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 h1:Slr1R9HxAlEKefgq5jn9U+DnETlIUa6HfgEzj0g5d7s=
github.com/subosito/gotenv v1.2.0/go.mod h1:N0PQaV/YGNqwC0u51sEeR/aUtSLEXKX9iv69rRypqCw= 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.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 h1:D8xgwECY7CYvx+Y2n4sBz93Jn9JRvxdiyyo8CTfuKaY=
gopkg.in/yaml.v2 v2.4.0/go.mod h1:RDklbk79AGWmwhnvt/jBztapEOGDOx6ZbXqjP6csGnQ= 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= 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-20180728063816-88497007e858/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4=
honnef.co/go/tools v0.0.0-20190102054323-c2f93a96b099/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 ( const (
memonicEnvName = "AEZEED_MNEMONIC" MnemonicEnvName = "AEZEED_MNEMONIC"
passphraseEnvName = "AEZEED_PASSPHRASE" PassphraseEnvName = "AEZEED_PASSPHRASE"
) )
var ( 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 // To automate things with chantools, we also offer reading the seed
// from environment variables. // 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 // If nothing is set in the environment, read the seed from the
// terminal. // 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 // Additionally, the user may have a passphrase, that will also need to
// be provided so the daemon can properly decipher the cipher seed. // be provided so the daemon can properly decipher the cipher seed.
// Try the environment variable first. // 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 // Because we cannot differentiate between an empty and a non-existent
// environment variable, we need a special character that indicates that // environment variable, we need a special character that indicates that

Loading…
Cancel
Save