lnd.xprv/walletunlocker/service_test.go
Yong 582b164c46
kvdb: add timeout options for bbolt (#4787)
* mod: bump btcwallet version to accept db timeout

* btcwallet: add DBTimeOut in config

* kvdb: add database timeout option for bbolt

This commit adds a DBTimeout option in bbolt config. The relevant
functions walletdb.Open/Create are updated to use this config. In
addition, the bolt compacter also applies the new timeout option.

* channeldb: add DBTimeout in db options

This commit adds the DBTimeout option for channeldb. A new unit
test file is created to test the default options. In addition,
the params used in kvdb.Create inside channeldb_test is updated
with a DefaultDBTimeout value.

* contractcourt+routing: use DBTimeout in kvdb

This commit touches multiple test files in contractcourt and routing.
The call of function kvdb.Create and kvdb.Open are now updated with
the new param DBTimeout, using the default value kvdb.DefaultDBTimeout.

* lncfg: add DBTimeout option in db config

The DBTimeout option is added to db config. A new unit test is
added to check the default DB config is created as expected.

* migration: add DBTimeout param in kvdb.Create/kvdb.Open

* keychain: update tests to use DBTimeout param

* htlcswitch+chainreg: add DBTimeout option

* macaroons: support DBTimeout config in creation

This commit adds the DBTimeout during the creation of macaroons.db.
The usage of kvdb.Create and kvdb.Open in its tests are updated with
a timeout value using kvdb.DefaultDBTimeout.

* walletunlocker: add dbTimeout option in UnlockerService

This commit adds a new param, dbTimeout, during the creation of
UnlockerService. This param is then passed to wallet.NewLoader
inside various service calls, specifying a timeout value to be
used when opening the bbolt. In addition, the macaroonService
is also called with this dbTimeout param.

* watchtower/wtdb: add dbTimeout param during creation

This commit adds the dbTimeout param for the creation of both
watchtower.db and wtclient.db.

* multi: add db timeout param for walletdb.Create

This commit adds the db timeout param for the function call
walletdb.Create. It touches only the test files found in chainntnfs,
lnwallet, and routing.

* lnd: pass DBTimeout config to relevant services

This commit enables lnd to pass the DBTimeout config to the following
services/config/functions,
  - chainControlConfig
  - walletunlocker
  - wallet.NewLoader
  - macaroons
  - watchtower
In addition, the usage of wallet.Create is updated too.

* sample-config: add dbtimeout option
2020-12-07 15:31:49 -08:00

679 lines
20 KiB
Go

package walletunlocker_test
import (
"bytes"
"context"
"fmt"
"io/ioutil"
"os"
"path"
"testing"
"time"
"github.com/btcsuite/btcd/chaincfg"
"github.com/btcsuite/btcwallet/snacl"
"github.com/btcsuite/btcwallet/waddrmgr"
"github.com/btcsuite/btcwallet/wallet"
"github.com/lightningnetwork/lnd/aezeed"
"github.com/lightningnetwork/lnd/channeldb/kvdb"
"github.com/lightningnetwork/lnd/keychain"
"github.com/lightningnetwork/lnd/lnrpc"
"github.com/lightningnetwork/lnd/lnwallet"
"github.com/lightningnetwork/lnd/lnwallet/btcwallet"
"github.com/lightningnetwork/lnd/macaroons"
"github.com/lightningnetwork/lnd/walletunlocker"
"github.com/stretchr/testify/require"
)
var (
testPassword = []byte("test-password")
testSeed = []byte("test-seed-123456789")
testMac = []byte("fakemacaroon")
testEntropy = [aezeed.EntropySize]byte{
0x81, 0xb6, 0x37, 0xd8,
0x63, 0x59, 0xe6, 0x96,
0x0d, 0xe7, 0x95, 0xe4,
0x1e, 0x0b, 0x4c, 0xfd,
}
testNetParams = &chaincfg.MainNetParams
testRecoveryWindow uint32 = 150
defaultTestTimeout = 3 * time.Second
defaultRootKeyIDContext = macaroons.ContextWithRootKeyID(
context.Background(), macaroons.DefaultRootKeyID,
)
)
func createTestWallet(t *testing.T, dir string, netParams *chaincfg.Params) {
createTestWalletWithPw(t, testPassword, testPassword, dir, netParams)
}
func createTestWalletWithPw(t *testing.T, pubPw, privPw []byte, dir string,
netParams *chaincfg.Params) {
// Instruct waddrmgr to use the cranked down scrypt parameters when
// creating new wallet encryption keys.
fastScrypt := waddrmgr.FastScryptOptions
keyGen := func(passphrase *[]byte, config *waddrmgr.ScryptOptions) (
*snacl.SecretKey, error) {
return snacl.NewSecretKey(
passphrase, fastScrypt.N, fastScrypt.R, fastScrypt.P,
)
}
waddrmgr.SetSecretKeyGen(keyGen)
// Create a new test wallet that uses fast scrypt as KDF.
netDir := btcwallet.NetworkDir(dir, netParams)
loader := wallet.NewLoader(
netParams, netDir, true, kvdb.DefaultDBTimeout, 0,
)
_, err := loader.CreateNewWallet(
pubPw, privPw, testSeed, time.Time{},
)
require.NoError(t, err)
err = loader.UnloadWallet()
require.NoError(t, err)
}
func createSeedAndMnemonic(t *testing.T,
pass []byte) (*aezeed.CipherSeed, aezeed.Mnemonic) {
cipherSeed, err := aezeed.New(
keychain.KeyDerivationVersion, &testEntropy, time.Now(),
)
require.NoError(t, err)
// With the new seed created, we'll convert it into a mnemonic phrase
// that we'll send over to initialize the wallet.
mnemonic, err := cipherSeed.ToMnemonic(pass)
require.NoError(t, err)
return cipherSeed, mnemonic
}
// openOrCreateTestMacStore opens or creates a bbolt DB and then initializes a
// root key storage for that DB and then unlocks it, creating a root key in the
// process.
func openOrCreateTestMacStore(tempDir string, pw *[]byte,
netParams *chaincfg.Params) (*macaroons.RootKeyStorage, error) {
netDir := btcwallet.NetworkDir(tempDir, netParams)
err := os.MkdirAll(netDir, 0700)
if err != nil {
return nil, err
}
db, err := kvdb.Create(
kvdb.BoltBackendName, path.Join(netDir, macaroons.DBFilename),
true, kvdb.DefaultDBTimeout,
)
if err != nil {
return nil, err
}
store, err := macaroons.NewRootKeyStorage(db)
if err != nil {
_ = db.Close()
return nil, err
}
err = store.CreateUnlock(pw)
if err != nil {
_ = store.Close()
return nil, err
}
_, _, err = store.RootKey(defaultRootKeyIDContext)
if err != nil {
_ = store.Close()
return nil, err
}
return store, nil
}
// TestGenSeedUserEntropy tests that the gen seed method generates a valid
// cipher seed mnemonic phrase and user provided source of entropy.
func TestGenSeed(t *testing.T) {
t.Parallel()
// First, we'll create a new test directory and unlocker service for
// that directory.
testDir, err := ioutil.TempDir("", "testcreate")
require.NoError(t, err)
defer func() {
_ = os.RemoveAll(testDir)
}()
service := walletunlocker.New(
testDir, testNetParams, true, nil, kvdb.DefaultDBTimeout,
)
// Now that the service has been created, we'll ask it to generate a
// new seed for us given a test passphrase.
aezeedPass := []byte("kek")
genSeedReq := &lnrpc.GenSeedRequest{
AezeedPassphrase: aezeedPass,
SeedEntropy: testEntropy[:],
}
ctx := context.Background()
seedResp, err := service.GenSeed(ctx, genSeedReq)
require.NoError(t, err)
// We should then be able to take the generated mnemonic, and properly
// decipher both it.
var mnemonic aezeed.Mnemonic
copy(mnemonic[:], seedResp.CipherSeedMnemonic[:])
_, err = mnemonic.ToCipherSeed(aezeedPass)
require.NoError(t, err)
}
// TestGenSeedInvalidEntropy tests that the gen seed method generates a valid
// cipher seed mnemonic pass phrase even when the user doesn't provide its own
// source of entropy.
func TestGenSeedGenerateEntropy(t *testing.T) {
t.Parallel()
// First, we'll create a new test directory and unlocker service for
// that directory.
testDir, err := ioutil.TempDir("", "testcreate")
require.NoError(t, err)
defer func() {
_ = os.RemoveAll(testDir)
}()
service := walletunlocker.New(
testDir, testNetParams, true, nil, kvdb.DefaultDBTimeout,
)
// Now that the service has been created, we'll ask it to generate a
// new seed for us given a test passphrase. Note that we don't actually
aezeedPass := []byte("kek")
genSeedReq := &lnrpc.GenSeedRequest{
AezeedPassphrase: aezeedPass,
}
ctx := context.Background()
seedResp, err := service.GenSeed(ctx, genSeedReq)
require.NoError(t, err)
// We should then be able to take the generated mnemonic, and properly
// decipher both it.
var mnemonic aezeed.Mnemonic
copy(mnemonic[:], seedResp.CipherSeedMnemonic[:])
_, err = mnemonic.ToCipherSeed(aezeedPass)
require.NoError(t, err)
}
// TestGenSeedInvalidEntropy tests that if a user attempt to create a seed with
// the wrong number of bytes for the initial entropy, then the proper error is
// returned.
func TestGenSeedInvalidEntropy(t *testing.T) {
t.Parallel()
// First, we'll create a new test directory and unlocker service for
// that directory.
testDir, err := ioutil.TempDir("", "testcreate")
require.NoError(t, err)
defer func() {
_ = os.RemoveAll(testDir)
}()
service := walletunlocker.New(
testDir, testNetParams, true, nil, kvdb.DefaultDBTimeout,
)
// Now that the service has been created, we'll ask it to generate a
// new seed for us given a test passphrase. However, we'll be using an
// invalid set of entropy that's 55 bytes, instead of 15 bytes.
aezeedPass := []byte("kek")
genSeedReq := &lnrpc.GenSeedRequest{
AezeedPassphrase: aezeedPass,
SeedEntropy: bytes.Repeat([]byte("a"), 55),
}
// We should get an error now since the entropy source was invalid.
ctx := context.Background()
_, err = service.GenSeed(ctx, genSeedReq)
require.Error(t, err)
require.Contains(t, err.Error(), "incorrect entropy length")
}
// TestInitWallet tests that the user is able to properly initialize the wallet
// given an existing cipher seed passphrase.
func TestInitWallet(t *testing.T) {
t.Parallel()
// testDir is empty, meaning wallet was not created from before.
testDir, err := ioutil.TempDir("", "testcreate")
require.NoError(t, err)
defer func() {
_ = os.RemoveAll(testDir)
}()
// Create new UnlockerService.
service := walletunlocker.New(
testDir, testNetParams, true, nil, kvdb.DefaultDBTimeout,
)
// Once we have the unlocker service created, we'll now instantiate a
// new cipher seed and its mnemonic.
pass := []byte("test")
cipherSeed, mnemonic := createSeedAndMnemonic(t, pass)
// Now that we have all the necessary items, we'll now issue the Init
// command to the wallet. This should check the validity of the cipher
// seed, then send over the initialization information over the init
// channel.
ctx := context.Background()
req := &lnrpc.InitWalletRequest{
WalletPassword: testPassword,
CipherSeedMnemonic: mnemonic[:],
AezeedPassphrase: pass,
RecoveryWindow: int32(testRecoveryWindow),
StatelessInit: true,
}
errChan := make(chan error, 1)
go func() {
response, err := service.InitWallet(ctx, req)
if err != nil {
errChan <- err
return
}
if !bytes.Equal(response.AdminMacaroon, testMac) {
errChan <- fmt.Errorf("mismatched macaroon: "+
"expected %x, got %x", testMac,
response.AdminMacaroon)
}
}()
// The same user passphrase, and also the plaintext cipher seed
// should be sent over and match exactly.
select {
case err := <-errChan:
t.Fatalf("InitWallet call failed: %v", err)
case msg := <-service.InitMsgs:
msgSeed := msg.WalletSeed
require.Equal(t, testPassword, msg.Passphrase)
require.Equal(
t, cipherSeed.InternalVersion, msgSeed.InternalVersion,
)
require.Equal(t, cipherSeed.Birthday, msgSeed.Birthday)
require.Equal(t, cipherSeed.Entropy, msgSeed.Entropy)
require.Equal(t, testRecoveryWindow, msg.RecoveryWindow)
require.Equal(t, true, msg.StatelessInit)
// Send a fake macaroon that should be returned in the response
// in the async code above.
service.MacResponseChan <- testMac
case <-time.After(defaultTestTimeout):
t.Fatalf("password not received")
}
// Create a wallet in testDir.
createTestWallet(t, testDir, testNetParams)
// Now calling InitWallet should fail, since a wallet already exists in
// the directory.
_, err = service.InitWallet(ctx, req)
require.Error(t, err)
// Similarly, if we try to do GenSeed again, we should get an error as
// the wallet already exists.
_, err = service.GenSeed(ctx, &lnrpc.GenSeedRequest{})
require.Error(t, err)
}
// TestInitWalletInvalidCipherSeed tests that if we attempt to create a wallet
// with an invalid cipher seed, then we'll receive an error.
func TestCreateWalletInvalidEntropy(t *testing.T) {
t.Parallel()
// testDir is empty, meaning wallet was not created from before.
testDir, err := ioutil.TempDir("", "testcreate")
require.NoError(t, err)
defer func() {
_ = os.RemoveAll(testDir)
}()
// Create new UnlockerService.
service := walletunlocker.New(
testDir, testNetParams, true, nil, kvdb.DefaultDBTimeout,
)
// We'll attempt to init the wallet with an invalid cipher seed and
// passphrase.
req := &lnrpc.InitWalletRequest{
WalletPassword: testPassword,
CipherSeedMnemonic: []string{"invalid", "seed"},
AezeedPassphrase: []byte("fake pass"),
}
ctx := context.Background()
_, err = service.InitWallet(ctx, req)
require.Error(t, err)
}
// TestUnlockWallet checks that trying to unlock non-existing wallet fail, that
// unlocking existing wallet with wrong passphrase fails, and that unlocking
// existing wallet with correct passphrase succeeds.
func TestUnlockWallet(t *testing.T) {
t.Parallel()
// testDir is empty, meaning wallet was not created from before.
testDir, err := ioutil.TempDir("", "testunlock")
require.NoError(t, err)
defer func() {
_ = os.RemoveAll(testDir)
}()
// Create new UnlockerService.
service := walletunlocker.New(
testDir, testNetParams, true, nil, kvdb.DefaultDBTimeout,
)
ctx := context.Background()
req := &lnrpc.UnlockWalletRequest{
WalletPassword: testPassword,
RecoveryWindow: int32(testRecoveryWindow),
StatelessInit: true,
}
// Should fail to unlock non-existing wallet.
_, err = service.UnlockWallet(ctx, req)
require.Error(t, err)
// Create a wallet we can try to unlock.
createTestWallet(t, testDir, testNetParams)
// Try unlocking this wallet with the wrong passphrase.
wrongReq := &lnrpc.UnlockWalletRequest{
WalletPassword: []byte("wrong-ofc"),
}
_, err = service.UnlockWallet(ctx, wrongReq)
require.Error(t, err)
// With the correct password, we should be able to unlock the wallet.
errChan := make(chan error, 1)
go func() {
// With the correct password, we should be able to unlock the
// wallet.
_, err := service.UnlockWallet(ctx, req)
if err != nil {
errChan <- err
}
}()
// Password and recovery window should be sent over the channel.
select {
case err := <-errChan:
t.Fatalf("UnlockWallet call failed: %v", err)
case unlockMsg := <-service.UnlockMsgs:
require.Equal(t, testPassword, unlockMsg.Passphrase)
require.Equal(t, testRecoveryWindow, unlockMsg.RecoveryWindow)
require.Equal(t, true, unlockMsg.StatelessInit)
// Send a fake macaroon that should be returned in the response
// in the async code above.
service.MacResponseChan <- testMac
case <-time.After(defaultTestTimeout):
t.Fatalf("password not received")
}
}
// TestChangeWalletPasswordNewRootkey tests that we can successfully change the
// wallet's password needed to unlock it and rotate the root key for the
// macaroons in the same process.
func TestChangeWalletPasswordNewRootkey(t *testing.T) {
t.Parallel()
// testDir is empty, meaning wallet was not created from before.
testDir, err := ioutil.TempDir("", "testchangepassword")
require.NoError(t, err)
defer func() {
_ = os.RemoveAll(testDir)
}()
// Changing the password of the wallet will also try to change the
// password of the macaroon DB. We create a default DB here but close it
// immediately so the service does not fail when trying to open it.
store, err := openOrCreateTestMacStore(
testDir, &testPassword, testNetParams,
)
require.NoError(t, err)
require.NoError(t, store.Close())
// Create some files that will act as macaroon files that should be
// deleted after a password change is successful with a new root key
// requested.
var tempFiles []string
for i := 0; i < 3; i++ {
file, err := ioutil.TempFile(testDir, "")
if err != nil {
t.Fatalf("unable to create temp file: %v", err)
}
tempFiles = append(tempFiles, file.Name())
require.NoError(t, file.Close())
}
// Create a new UnlockerService with our temp files.
service := walletunlocker.New(
testDir, testNetParams, true, tempFiles, kvdb.DefaultDBTimeout,
)
ctx := context.Background()
newPassword := []byte("hunter2???")
req := &lnrpc.ChangePasswordRequest{
CurrentPassword: testPassword,
NewPassword: newPassword,
NewMacaroonRootKey: true,
}
// Changing the password to a non-existing wallet should fail.
_, err = service.ChangePassword(ctx, req)
require.Error(t, err)
// Create a wallet to test changing the password.
createTestWallet(t, testDir, testNetParams)
// Attempting to change the wallet's password using an incorrect
// current password should fail.
wrongReq := &lnrpc.ChangePasswordRequest{
CurrentPassword: []byte("wrong-ofc"),
NewPassword: newPassword,
}
_, err = service.ChangePassword(ctx, wrongReq)
require.Error(t, err)
// The files should still exist after an unsuccessful attempt to change
// the wallet's password.
for _, tempFile := range tempFiles {
if _, err := os.Stat(tempFile); os.IsNotExist(err) {
t.Fatal("file does not exist but it should")
}
}
// Attempting to change the wallet's password using an invalid
// new password should fail.
wrongReq.NewPassword = []byte("8")
_, err = service.ChangePassword(ctx, wrongReq)
require.Error(t, err)
// When providing the correct wallet's current password and a new
// password that meets the length requirement, the password change
// should succeed.
errChan := make(chan error, 1)
go doChangePassword(service, testDir, req, errChan)
// The new password should be sent over the channel.
select {
case err := <-errChan:
t.Fatalf("ChangePassword call failed: %v", err)
case unlockMsg := <-service.UnlockMsgs:
require.Equal(t, newPassword, unlockMsg.Passphrase)
// Send a fake macaroon that should be returned in the response
// in the async code above.
service.MacResponseChan <- testMac
case <-time.After(defaultTestTimeout):
t.Fatalf("password not received")
}
// The files should no longer exist.
for _, tempFile := range tempFiles {
if _, err := os.Open(tempFile); err == nil {
t.Fatal("file exists but it shouldn't")
}
}
}
// TestChangeWalletPasswordStateless checks that trying to change the password
// of an existing wallet that was initialized stateless works when when the
// --stateless_init flat is set. Also checks that if no password is given,
// the default password is used.
func TestChangeWalletPasswordStateless(t *testing.T) {
t.Parallel()
// testDir is empty, meaning wallet was not created from before.
testDir, err := ioutil.TempDir("", "testchangepasswordstateless")
require.NoError(t, err)
defer func() {
_ = os.RemoveAll(testDir)
}()
// Changing the password of the wallet will also try to change the
// password of the macaroon DB. We create a default DB here but close it
// immediately so the service does not fail when trying to open it.
store, err := openOrCreateTestMacStore(
testDir, &lnwallet.DefaultPrivatePassphrase, testNetParams,
)
require.NoError(t, err)
require.NoError(t, store.Close())
// Create a temp file that will act as the macaroon DB file that will
// be deleted by changing the password.
tmpFile, err := ioutil.TempFile(testDir, "")
require.NoError(t, err)
tempMacFile := tmpFile.Name()
err = tmpFile.Close()
require.NoError(t, err)
// Create a file name that does not exist that will be used as a
// macaroon file reference. The fact that the file does not exist should
// not throw an error when --stateless_init is used.
nonExistingFile := path.Join(testDir, "does-not-exist")
// Create a new UnlockerService with our temp files.
service := walletunlocker.New(testDir, testNetParams, true, []string{
tempMacFile, nonExistingFile,
}, kvdb.DefaultDBTimeout)
// Create a wallet we can try to unlock. We use the default password
// so we can check that the unlocker service defaults to this when
// we give it an empty CurrentPassword to indicate we come from a
// --noencryptwallet state.
createTestWalletWithPw(
t, lnwallet.DefaultPublicPassphrase,
lnwallet.DefaultPrivatePassphrase, testDir, testNetParams,
)
// We make sure that we get a proper error message if we forget to
// add the --stateless_init flag but the macaroon files don't exist.
badReq := &lnrpc.ChangePasswordRequest{
NewPassword: testPassword,
NewMacaroonRootKey: true,
}
ctx := context.Background()
_, err = service.ChangePassword(ctx, badReq)
require.Error(t, err)
// Prepare the correct request we are going to send to the unlocker
// service. We don't provide a current password to indicate there
// was none set before.
req := &lnrpc.ChangePasswordRequest{
NewPassword: testPassword,
StatelessInit: true,
NewMacaroonRootKey: true,
}
// Since we indicated the wallet was initialized stateless, the service
// will block until it receives the macaroon through the channel
// provided in the message in UnlockMsgs. So we need to call the service
// async and then wait for the unlock message to arrive so we can send
// back a fake macaroon.
errChan := make(chan error, 1)
go doChangePassword(service, testDir, req, errChan)
// Password and recovery window should be sent over the channel.
select {
case err := <-errChan:
t.Fatalf("ChangePassword call failed: %v", err)
case unlockMsg := <-service.UnlockMsgs:
require.Equal(t, testPassword, unlockMsg.Passphrase)
// Send a fake macaroon that should be returned in the response
// in the async code above.
service.MacResponseChan <- testMac
case <-time.After(defaultTestTimeout):
t.Fatalf("password not received")
}
}
func doChangePassword(service *walletunlocker.UnlockerService, testDir string,
req *lnrpc.ChangePasswordRequest, errChan chan error) {
// When providing the correct wallet's current password and a
// new password that meets the length requirement, the password
// change should succeed.
ctx := context.Background()
response, err := service.ChangePassword(ctx, req)
if err != nil {
errChan <- fmt.Errorf("could not change password: %v", err)
return
}
if !bytes.Equal(response.AdminMacaroon, testMac) {
errChan <- fmt.Errorf("mismatched macaroon: expected "+
"%x, got %x", testMac, response.AdminMacaroon)
}
// Close the macaroon DB and try to open it and read the root
// key with the new password.
store, err := openOrCreateTestMacStore(
testDir, &testPassword, testNetParams,
)
if err != nil {
errChan <- fmt.Errorf("could not create test store: %v", err)
return
}
_, _, err = store.RootKey(defaultRootKeyIDContext)
if err != nil {
errChan <- fmt.Errorf("could not get root key: %v", err)
return
}
// Do cleanup now. Since we are in a go func, the defer at the
// top of the outer would not work, because it would delete
// the directory before we could check the content in here.
err = store.Close()
if err != nil {
errChan <- fmt.Errorf("could not close store: %v", err)
return
}
err = os.RemoveAll(testDir)
if err != nil {
errChan <- err
return
}
}