Skip to content

Commit fa62a57

Browse files
committed
multi: add unit tests
1 parent e6fcb58 commit fa62a57

25 files changed

+629
-174
lines changed

btc/bip39.go

Lines changed: 87 additions & 48 deletions
Original file line numberDiff line numberDiff line change
@@ -6,28 +6,42 @@ import (
66
"encoding/hex"
77
"errors"
88
"fmt"
9-
"os"
10-
"strings"
11-
"syscall"
12-
139
"github.com/btcsuite/btcd/chaincfg"
1410
"github.com/btcsuite/btcutil/hdkeychain"
1511
"github.com/guggero/chantools/bip39"
1612
"golang.org/x/crypto/pbkdf2"
1713
"golang.org/x/crypto/ssh/terminal"
14+
"os"
15+
"strings"
16+
"syscall"
17+
)
18+
19+
const (
20+
BIP39MnemonicEnvName = "SEED_MNEMONIC"
21+
BIP39PassphraseEnvName = "SEED_PASSPHRASE"
1822
)
1923

2024
func ReadMnemonicFromTerminal(params *chaincfg.Params) (*hdkeychain.ExtendedKey,
2125
error) {
2226

23-
// We'll now prompt the user to enter in their 12 to 24 word mnemonic.
24-
fmt.Printf("Input your 12 to 24 word mnemonic separated by spaces: ")
27+
var err error
2528
reader := bufio.NewReader(os.Stdin)
26-
mnemonicStr, err := reader.ReadString('\n')
27-
if err != nil {
28-
return nil, err
29+
30+
// To automate things with chantools, we also offer reading the seed
31+
// from environment variables.
32+
mnemonicStr := strings.TrimSpace(os.Getenv(BIP39MnemonicEnvName))
33+
34+
if mnemonicStr == "" {
35+
// If there's no value in the environment, we'll now prompt the
36+
//user to enter in their 12 to 24 word mnemonic.
37+
fmt.Printf("Input your 12 to 24 word mnemonic separated by " +
38+
"spaces: ")
39+
mnemonicStr, err = reader.ReadString('\n')
40+
if err != nil {
41+
return nil, err
42+
}
43+
fmt.Println()
2944
}
30-
fmt.Println()
3145

3246
// We'll trim off extra spaces, and ensure the mnemonic is all
3347
// lower case.
@@ -40,62 +54,87 @@ func ReadMnemonicFromTerminal(params *chaincfg.Params) (*hdkeychain.ExtendedKey,
4054
"must be between 12 and 24 words")
4155
}
4256

43-
// Additionally, the user may have a passphrase, that will also
44-
// need to be provided so the daemon can properly decipher the
45-
// cipher seed.
46-
fmt.Printf("Input your cipher seed passphrase (press enter if " +
47-
"your seed doesn't have a passphrase): ")
48-
passphrase, err := terminal.ReadPassword(int(syscall.Stdin)) // nolint
49-
if err != nil {
50-
return nil, err
51-
}
52-
fmt.Println()
57+
// Additionally, the user may have a passphrase, that will also need to
58+
// be provided so the daemon can properly decipher the cipher seed.
59+
// Try the environment variable first.
60+
passphrase := strings.TrimSpace(os.Getenv(BIP39PassphraseEnvName))
5361

54-
// Check that the mnemonic is valid.
55-
_, err = bip39.EntropyFromMnemonic(mnemonicStr)
56-
if err != nil {
57-
return nil, err
58-
}
62+
// Because we cannot differentiate between an empty and a non-existent
63+
// environment variable, we need a special character that indicates that
64+
// no passphrase should be used. We use a single dash (-) for that as
65+
// that would be too short for a passphrase anyway.
66+
var (
67+
passphraseBytes []byte
68+
seed []byte
69+
choice string
70+
)
71+
switch {
72+
// The user indicated in the environment variable that no passphrase
73+
// should be used. We don't set any value.
74+
case passphrase == "-":
5975

60-
var seed []byte
61-
fmt.Printf("Please choose passphrase mode:\n" +
62-
" 0 - Default BIP39\n" +
63-
" 1 - Passphrase to hex\n" +
64-
" 2 - Digital Bitbox (extra round of PBKDF2)\n" +
65-
"\n" +
66-
"Choice [default 0]: ")
67-
choice, err := reader.ReadString('\n')
68-
if err != nil {
69-
return nil, err
76+
// The environment variable didn't contain anything, we'll read the
77+
// passphrase from the terminal.
78+
case passphrase == "":
79+
// Additionally, the user may have a passphrase, that will also
80+
// need to be provided so the daemon can properly decipher the
81+
// cipher seed.
82+
fmt.Printf("Input your cipher seed passphrase (press enter " +
83+
"if your seed doesn't have a passphrase): ")
84+
passphraseBytes, err = terminal.ReadPassword(
85+
int(syscall.Stdin), // nolint
86+
)
87+
if err != nil {
88+
return nil, err
89+
}
90+
fmt.Println()
91+
92+
// Check that the mnemonic is valid.
93+
_, err = bip39.EntropyFromMnemonic(mnemonicStr)
94+
if err != nil {
95+
return nil, err
96+
}
97+
98+
fmt.Printf("Please choose passphrase mode:\n" +
99+
" 0 - Default BIP39\n" +
100+
" 1 - Passphrase to hex\n" +
101+
" 2 - Digital Bitbox (extra round of PBKDF2)\n" +
102+
"\n" +
103+
"Choice [default 0]: ")
104+
choice, err = reader.ReadString('\n')
105+
if err != nil {
106+
return nil, err
107+
}
108+
fmt.Println()
109+
110+
// There was a password in the environment, just convert it to bytes.
111+
default:
112+
passphraseBytes = []byte(passphrase)
70113
}
71-
fmt.Println()
72114

73115
switch strings.TrimSpace(choice) {
74116
case "", "0":
75117
seed = pbkdf2.Key(
76118
[]byte(mnemonicStr), append(
77-
[]byte("mnemonic"), passphrase...,
119+
[]byte("mnemonic"), passphraseBytes...,
78120
), 2048, 64, sha512.New,
79121
)
80122

81123
case "1":
82-
passphrase = []byte(hex.EncodeToString(passphrase))
124+
p := []byte(hex.EncodeToString(passphraseBytes))
83125
seed = pbkdf2.Key(
84-
[]byte(mnemonicStr), append(
85-
[]byte("mnemonic"), passphrase...,
86-
), 2048, 64, sha512.New,
126+
[]byte(mnemonicStr), append([]byte("mnemonic"), p...),
127+
2048, 64, sha512.New,
87128
)
88129

89130
case "2":
90-
passphrase = pbkdf2.Key(
91-
passphrase, []byte("Digital Bitbox"), 20480, 64,
131+
p := hex.EncodeToString(pbkdf2.Key(
132+
passphraseBytes, []byte("Digital Bitbox"), 20480, 64,
92133
sha512.New,
93-
)
94-
passphrase = []byte(hex.EncodeToString(passphrase))
134+
))
95135
seed = pbkdf2.Key(
96-
[]byte(mnemonicStr), append(
97-
[]byte("mnemonic"), passphrase...,
98-
), 2048, 64, sha512.New,
136+
[]byte(mnemonicStr), append([]byte("mnemonic"), p...),
137+
2048, 64, sha512.New,
99138
)
100139

101140
default:

cmd/chantools/chanbackup_test.go

Lines changed: 37 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,37 @@
1+
package main
2+
3+
import (
4+
"testing"
5+
6+
"github.com/stretchr/testify/require"
7+
)
8+
9+
const (
10+
backupContent = "FundingOutpoint: (string) (len=66) \"10279f626196340" +
11+
"58b6133cb7ac6c1693a8e6df7caa91c6263ca3d0bf704ad4d:0\""
12+
)
13+
14+
func TestChanBackupAndDumpBackup(t *testing.T) {
15+
h := newHarness(t)
16+
17+
// Create a channel backup from a channel DB file.
18+
makeBackup := &chanBackupCommand{
19+
ChannelDB: h.testdataFile("channel.db"),
20+
MultiFile: h.tempFile("extracted.backup"),
21+
rootKey: &rootKey{RootKey: rootKeyAezeed},
22+
}
23+
24+
err := makeBackup.Execute(nil, nil)
25+
require.NoError(t, err)
26+
27+
// Decrypt and dump the channel backup file.
28+
dumpBackup := &dumpBackupCommand{
29+
MultiFile: makeBackup.MultiFile,
30+
rootKey: &rootKey{RootKey: rootKeyAezeed},
31+
}
32+
33+
err = dumpBackup.Execute(nil, nil)
34+
require.NoError(t, err)
35+
36+
h.assertLogContains(backupContent)
37+
}

cmd/chantools/compactdb.go

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -64,10 +64,14 @@ func (c *compactDBCommand) Execute(_ *cobra.Command, _ []string) error {
6464
if err != nil {
6565
return fmt.Errorf("error opening source DB: %v", err)
6666
}
67+
defer func() { _ = src.Close() }()
68+
6769
dst, err := c.openDB(c.DestDB, false)
6870
if err != nil {
6971
return fmt.Errorf("error opening destination DB: %v", err)
7072
}
73+
defer func() { _ = dst.Close() }()
74+
7175
err = c.compact(dst, src)
7276
if err != nil {
7377
return fmt.Errorf("error compacting DB: %v", err)

cmd/chantools/compactdb_test.go

Lines changed: 46 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,46 @@
1+
package main
2+
3+
import (
4+
"testing"
5+
6+
"github.com/stretchr/testify/require"
7+
)
8+
9+
func TestCompactDBAndDumpChannels(t *testing.T) {
10+
h := newHarness(t)
11+
12+
// Compact the test DB.
13+
compact := &compactDBCommand{
14+
SourceDB: h.testdataFile("channel.db"),
15+
DestDB: h.tempFile("compacted.db"),
16+
}
17+
18+
err := compact.Execute(nil, nil)
19+
require.NoError(t, err)
20+
21+
require.FileExists(t, compact.DestDB)
22+
23+
// Compacting small DBs actually increases the size slightly. But we
24+
// just want to make sure the contents match.
25+
require.GreaterOrEqual(
26+
t, h.fileSize(compact.DestDB), h.fileSize(compact.SourceDB),
27+
)
28+
29+
// Compare the content of the source and destination DB by looking at
30+
// the logged dump.
31+
dump := &dumpChannelsCommand{
32+
ChannelDB: compact.SourceDB,
33+
}
34+
h.clearLog()
35+
err = dump.Execute(nil, nil)
36+
require.NoError(t, err)
37+
sourceDump := h.getLog()
38+
39+
h.clearLog()
40+
dump.ChannelDB = compact.DestDB
41+
err = dump.Execute(nil, nil)
42+
require.NoError(t, err)
43+
destDump := h.getLog()
44+
45+
h.assertLogEqual(sourceDump, destDump)
46+
}

cmd/chantools/derivekey.go

Lines changed: 23 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -2,15 +2,24 @@ package main
22

33
import (
44
"fmt"
5-
65
"github.com/btcsuite/btcutil"
76
"github.com/btcsuite/btcutil/hdkeychain"
87
"github.com/guggero/chantools/lnd"
98
"github.com/spf13/cobra"
109
)
1110

11+
const deriveKeyFormat = `
12+
Path: %s
13+
Network: %s
14+
Public key: %x
15+
Extended public key (xpub): %v
16+
Address: %v
17+
Legacy address: %v
18+
Private key (WIF): %s
19+
Extended private key (xprv): %s
20+
`
21+
1222
type deriveKeyCommand struct {
13-
BIP39 bool
1423
Path string
1524
Neuter bool
1625

@@ -29,11 +38,6 @@ derivation path from the root key and prints it to the console.`,
2938
--path "m/1017'/0'/5'/0/0'" --neuter`,
3039
RunE: cc.Execute,
3140
}
32-
cc.cmd.Flags().BoolVar(
33-
&cc.BIP39, "bip39", false, "read a classic BIP39 seed and "+
34-
"passphrase from the terminal instead of asking for "+
35-
"lnd seed format or providing the --rootkey flag",
36-
)
3741
cc.cmd.Flags().StringVar(
3842
&cc.Path, "path", "", "BIP32 derivation path to derive; must "+
3943
"start with \"m/\"",
@@ -60,7 +64,6 @@ func (c *deriveKeyCommand) Execute(_ *cobra.Command, _ []string) error {
6064
func deriveKey(extendedKey *hdkeychain.ExtendedKey, path string,
6165
neuter bool) error {
6266

63-
fmt.Printf("Deriving path %s for network %s.\n", path, chainParams.Name)
6467
child, pubKey, wif, err := lnd.DeriveKey(extendedKey, path, chainParams)
6568
if err != nil {
6669
return fmt.Errorf("could not derive keys: %v", err)
@@ -69,8 +72,6 @@ func deriveKey(extendedKey *hdkeychain.ExtendedKey, path string,
6972
if err != nil {
7073
return fmt.Errorf("could not neuter child key: %v", err)
7174
}
72-
fmt.Printf("\nPublic key: %x\n", pubKey.SerializeCompressed())
73-
fmt.Printf("Extended public key (xpub): %s\n", neutered.String())
7475

7576
// Print the address too.
7677
hash160 := btcutil.Hash160(pubKey.SerializeCompressed())
@@ -84,13 +85,21 @@ func deriveKey(extendedKey *hdkeychain.ExtendedKey, path string,
8485
if err != nil {
8586
return fmt.Errorf("could not create address: %v", err)
8687
}
87-
fmt.Printf("Address: %s\n", addrP2WKH)
88-
fmt.Printf("Legacy address: %s\n", addrP2PKH)
8988

89+
privKey, xPriv := "n/a", "n/a"
9090
if !neuter {
91-
fmt.Printf("\nPrivate key (WIF): %s\n", wif.String())
92-
fmt.Printf("Extended private key (xprv): %s\n", child.String())
91+
privKey, xPriv = wif.String(), child.String()
9392
}
9493

94+
result := fmt.Sprintf(
95+
deriveKeyFormat, path, chainParams.Name,
96+
pubKey.SerializeCompressed(), neutered, addrP2WKH, addrP2PKH,
97+
privKey, xPriv,
98+
)
99+
fmt.Printf(result)
100+
101+
// For the tests, also log as trace level which is disabled by default.
102+
log.Tracef(result)
103+
95104
return nil
96105
}

0 commit comments

Comments
 (0)