mirror of
https://github.com/lightningnetwork/lnd.git
synced 2025-09-07 19:30:46 +02:00
walletunlocker+lnd: implement stateless wallet initialization
This commit is contained in:
@@ -3,8 +3,10 @@ package walletunlocker_test
|
||||
import (
|
||||
"bytes"
|
||||
"context"
|
||||
"fmt"
|
||||
"io/ioutil"
|
||||
"os"
|
||||
"path"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
@@ -13,8 +15,10 @@ import (
|
||||
"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"
|
||||
@@ -24,6 +28,7 @@ import (
|
||||
var (
|
||||
testPassword = []byte("test-password")
|
||||
testSeed = []byte("test-seed-123456789")
|
||||
testMac = []byte("fakemacaroon")
|
||||
|
||||
testEntropy = [aezeed.EntropySize]byte{
|
||||
0x81, 0xb6, 0x37, 0xd8,
|
||||
@@ -44,6 +49,12 @@ var (
|
||||
)
|
||||
|
||||
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
|
||||
@@ -60,13 +71,66 @@ func createTestWallet(t *testing.T, dir string, netParams *chaincfg.Params) {
|
||||
netDir := btcwallet.NetworkDir(dir, netParams)
|
||||
loader := wallet.NewLoader(netParams, netDir, true, 0)
|
||||
_, err := loader.CreateNewWallet(
|
||||
testPassword, testPassword, testSeed, time.Time{},
|
||||
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,
|
||||
)
|
||||
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) {
|
||||
@@ -183,17 +247,9 @@ func TestInitWallet(t *testing.T) {
|
||||
service := walletunlocker.New(testDir, testNetParams, true, nil)
|
||||
|
||||
// Once we have the unlocker service created, we'll now instantiate a
|
||||
// new cipher seed instance.
|
||||
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.
|
||||
// new cipher seed and its mnemonic.
|
||||
pass := []byte("test")
|
||||
mnemonic, err := cipherSeed.ToMnemonic(pass)
|
||||
require.NoError(t, err)
|
||||
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
|
||||
@@ -205,13 +261,29 @@ func TestInitWallet(t *testing.T) {
|
||||
CipherSeedMnemonic: mnemonic[:],
|
||||
AezeedPassphrase: pass,
|
||||
RecoveryWindow: int32(testRecoveryWindow),
|
||||
StatelessInit: true,
|
||||
}
|
||||
_, err = service.InitWallet(ctx, req)
|
||||
require.NoError(t, err)
|
||||
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)
|
||||
@@ -221,6 +293,11 @@ func TestInitWallet(t *testing.T) {
|
||||
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")
|
||||
@@ -288,6 +365,7 @@ func TestUnlockWallet(t *testing.T) {
|
||||
req := &lnrpc.UnlockWalletRequest{
|
||||
WalletPassword: testPassword,
|
||||
RecoveryWindow: int32(testRecoveryWindow),
|
||||
StatelessInit: true,
|
||||
}
|
||||
|
||||
// Should fail to unlock non-existing wallet.
|
||||
@@ -305,23 +383,39 @@ func TestUnlockWallet(t *testing.T) {
|
||||
require.Error(t, err)
|
||||
|
||||
// With the correct password, we should be able to unlock the wallet.
|
||||
_, err = service.UnlockWallet(ctx, req)
|
||||
require.NoError(t, err)
|
||||
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")
|
||||
}
|
||||
}
|
||||
|
||||
// TestChangeWalletPassword tests that we can successfully change the wallet's
|
||||
// password needed to unlock it.
|
||||
func TestChangeWalletPassword(t *testing.T) {
|
||||
// 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.
|
||||
@@ -331,8 +425,18 @@ func TestChangeWalletPassword(t *testing.T) {
|
||||
_ = 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.
|
||||
// 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, "")
|
||||
@@ -350,8 +454,9 @@ func TestChangeWalletPassword(t *testing.T) {
|
||||
newPassword := []byte("hunter2???")
|
||||
|
||||
req := &lnrpc.ChangePasswordRequest{
|
||||
CurrentPassword: testPassword,
|
||||
NewPassword: newPassword,
|
||||
CurrentPassword: testPassword,
|
||||
NewPassword: newPassword,
|
||||
NewMacaroonRootKey: true,
|
||||
}
|
||||
|
||||
// Changing the password to a non-existing wallet should fail.
|
||||
@@ -387,8 +492,24 @@ func TestChangeWalletPassword(t *testing.T) {
|
||||
// When providing the correct wallet's current password and a new
|
||||
// password that meets the length requirement, the password change
|
||||
// should succeed.
|
||||
_, err = service.ChangePassword(ctx, req)
|
||||
require.NoError(t, err)
|
||||
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 {
|
||||
@@ -396,13 +517,146 @@ func TestChangeWalletPassword(t *testing.T) {
|
||||
t.Fatal("file exists but it shouldn't")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// The new password should be sent over the channel.
|
||||
// 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,
|
||||
})
|
||||
|
||||
// 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, newPassword, unlockMsg.Passphrase)
|
||||
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
|
||||
}
|
||||
}
|
||||
|
Reference in New Issue
Block a user