From 24adf475cea6a47ff690d3412d74ab25e4c15a48 Mon Sep 17 00:00:00 2001 From: Oliver Gugger Date: Tue, 6 Oct 2020 17:23:40 +0200 Subject: [PATCH] lnd_test: add integration tests for stateless init --- lntest/harness.go | 54 ++++++++----- lntest/itest/lnd_channel_backup_test.go | 4 +- lntest/itest/lnd_macaroons_test.go | 102 ++++++++++++++++++++++++ lntest/itest/lnd_test.go | 8 +- lntest/itest/lnd_test_list_on_test.go | 4 + lntest/node.go | 80 +++++++++++++++++-- 6 files changed, 223 insertions(+), 29 deletions(-) diff --git a/lntest/harness.go b/lntest/harness.go index 01c960db..bd459f4b 100644 --- a/lntest/harness.go +++ b/lntest/harness.go @@ -270,11 +270,12 @@ func (n *NetworkHarness) NewNode(name string, extraArgs []string) (*HarnessNode, // wallet password. The generated mnemonic is returned along with the // initialized harness node. func (n *NetworkHarness) NewNodeWithSeed(name string, extraArgs []string, - password []byte) (*HarnessNode, []string, error) { + password []byte, statelessInit bool) (*HarnessNode, []string, []byte, + error) { node, err := n.newNode(name, extraArgs, true, password) if err != nil { - return nil, nil, err + return nil, nil, nil, err } timeout := time.Duration(time.Second * 15) @@ -289,7 +290,7 @@ func (n *NetworkHarness) NewNodeWithSeed(name string, extraArgs []string, ctxt, _ := context.WithTimeout(ctxb, timeout) genSeedResp, err := node.GenSeed(ctxt, genSeedReq) if err != nil { - return nil, nil, err + return nil, nil, nil, err } // With the seed created, construct the init request to the node, @@ -298,20 +299,25 @@ func (n *NetworkHarness) NewNodeWithSeed(name string, extraArgs []string, WalletPassword: password, CipherSeedMnemonic: genSeedResp.CipherSeedMnemonic, AezeedPassphrase: password, + StatelessInit: statelessInit, } // Pass the init request via rpc to finish unlocking the node. This will // also initialize the macaroon-authenticated LightningClient. - err = node.Init(ctxb, initReq) + response, err := node.Init(ctxb, initReq) if err != nil { - return nil, nil, err + return nil, nil, nil, err } // With the node started, we can now record its public key within the // global mapping. n.RegisterNode(node) - return node, genSeedResp.CipherSeedMnemonic, nil + // In stateless initialization mode we get a macaroon back that we have + // to return to the test, otherwise gRPC calls won't be possible since + // there are no macaroon files created in that mode. + // In stateful init the admin macaroon will just be nil. + return node, genSeedResp.CipherSeedMnemonic, response.AdminMacaroon, nil } // RestoreNodeWithSeed fully initializes a HarnessNode using a chosen mnemonic, @@ -336,7 +342,7 @@ func (n *NetworkHarness) RestoreNodeWithSeed(name string, extraArgs []string, ChannelBackups: chanBackups, } - err = node.Init(context.Background(), initReq) + _, err = node.Init(context.Background(), initReq) if err != nil { return nil, err } @@ -616,17 +622,8 @@ func (n *NetworkHarness) DisconnectNodes(ctx context.Context, a, b *HarnessNode) func (n *NetworkHarness) RestartNode(node *HarnessNode, callback func() error, chanBackups ...*lnrpc.ChanBackupSnapshot) error { - if err := node.stop(); err != nil { - return err - } - - if callback != nil { - if err := callback(); err != nil { - return err - } - } - - if err := node.start(n.lndBinary, n.lndErrorChan); err != nil { + err := n.RestartNodeNoUnlock(node, callback) + if err != nil { return err } @@ -649,6 +646,27 @@ func (n *NetworkHarness) RestartNode(node *HarnessNode, callback func() error, return node.Unlock(context.Background(), unlockReq) } +// RestartNodeNoUnlock attempts to restart a lightning node by shutting it down +// cleanly, then restarting the process. In case the node was setup with a seed, +// it will be left in the unlocked state. This function is fully blocking. If +// the callback parameter is non-nil, then the function will be executed after +// the node shuts down, but *before* the process has been started up again. +func (n *NetworkHarness) RestartNodeNoUnlock(node *HarnessNode, + callback func() error) error { + + if err := node.stop(); err != nil { + return err + } + + if callback != nil { + if err := callback(); err != nil { + return err + } + } + + return node.start(n.lndBinary, n.lndErrorChan) +} + // SuspendNode stops the given node and returns a callback that can be used to // start it again. func (n *NetworkHarness) SuspendNode(node *HarnessNode) (func() error, error) { diff --git a/lntest/itest/lnd_channel_backup_test.go b/lntest/itest/lnd_channel_backup_test.go index 2a7e0eb6..e6b57a73 100644 --- a/lntest/itest/lnd_channel_backup_test.go +++ b/lntest/itest/lnd_channel_backup_test.go @@ -797,8 +797,8 @@ func testChanRestoreScenario(t *harnessTest, net *lntest.NetworkHarness, // First, we'll create a brand new node we'll use within the test. If // we have a custom backup file specified, then we'll also create that // for use. - dave, mnemonic, err := net.NewNodeWithSeed( - "dave", nodeArgs, password, + dave, mnemonic, _, err := net.NewNodeWithSeed( + "dave", nodeArgs, password, false, ) if err != nil { t.Fatalf("unable to create new node: %v", err) diff --git a/lntest/itest/lnd_macaroons_test.go b/lntest/itest/lnd_macaroons_test.go index 93ef10b0..d9136019 100644 --- a/lntest/itest/lnd_macaroons_test.go +++ b/lntest/itest/lnd_macaroons_test.go @@ -1,10 +1,13 @@ package itest import ( + "bytes" "context" "encoding/hex" + "os" "sort" "strconv" + "strings" "testing" "github.com/lightningnetwork/lnd/lnrpc" @@ -486,6 +489,105 @@ func testDeleteMacaroonID(net *lntest.NetworkHarness, t *harnessTest) { require.Contains(t.t, err.Error(), "cannot get macaroon") } +// testStatelessInit checks that the stateless initialization of the daemon +// does not write any macaroon files to the daemon's file system and returns +// the admin macaroon in the response. It then checks that the password +// change of the wallet can also happen stateless. +func testStatelessInit(net *lntest.NetworkHarness, t *harnessTest) { + var ( + initPw = []byte("stateless") + newPw = []byte("stateless-new") + newAddrReq = &lnrpc.NewAddressRequest{ + Type: AddrTypeWitnessPubkeyHash, + } + ) + + // First, create a new node and request it to initialize stateless. + // This should return us the binary serialized admin macaroon that we + // can then use for further calls. + carol, _, macBytes, err := net.NewNodeWithSeed( + "Carol", nil, initPw, true, + ) + require.NoError(t.t, err) + if len(macBytes) == 0 { + t.Fatalf("invalid macaroon returned in stateless init") + } + + // Now make sure no macaroon files have been created by the node Carol. + _, err = os.Stat(carol.AdminMacPath()) + require.Error(t.t, err) + _, err = os.Stat(carol.ReadMacPath()) + require.Error(t.t, err) + _, err = os.Stat(carol.InvoiceMacPath()) + require.Error(t.t, err) + + // Then check that we can unmarshal the binary serialized macaroon. + adminMac := &macaroon.Macaroon{} + err = adminMac.UnmarshalBinary(macBytes) + require.NoError(t.t, err) + + // Find out if we can actually use the macaroon that has been returned + // to us for a RPC call. + conn, err := carol.ConnectRPCWithMacaroon(adminMac) + require.NoError(t.t, err) + defer conn.Close() + adminMacClient := lnrpc.NewLightningClient(conn) + ctxt, _ := context.WithTimeout(context.Background(), defaultTimeout) + res, err := adminMacClient.NewAddress(ctxt, newAddrReq) + require.NoError(t.t, err) + if !strings.HasPrefix(res.Address, harnessNetParams.Bech32HRPSegwit) { + t.Fatalf("returned address was not a regtest address") + } + + // As a second part, shut down the node and then try to change the + // password when we start it up again. + if err := net.RestartNodeNoUnlock(carol, nil); err != nil { + t.Fatalf("Node restart failed: %v", err) + } + changePwReq := &lnrpc.ChangePasswordRequest{ + CurrentPassword: initPw, + NewPassword: newPw, + StatelessInit: true, + } + ctxb := context.Background() + response, err := carol.InitChangePassword(ctxb, changePwReq) + require.NoError(t.t, err) + + // Again, make sure no macaroon files have been created by the node + // Carol. + _, err = os.Stat(carol.AdminMacPath()) + require.Error(t.t, err) + _, err = os.Stat(carol.ReadMacPath()) + require.Error(t.t, err) + _, err = os.Stat(carol.InvoiceMacPath()) + require.Error(t.t, err) + + // Then check that we can unmarshal the new binary serialized macaroon + // and that it really is a new macaroon. + if err = adminMac.UnmarshalBinary(response.AdminMacaroon); err != nil { + t.Fatalf("unable to unmarshal macaroon: %v", err) + } + if bytes.Equal(response.AdminMacaroon, macBytes) { + t.Fatalf("expected new macaroon to be different") + } + + // Finally, find out if we can actually use the new macaroon that has + // been returned to us for a RPC call. + conn2, err := carol.ConnectRPCWithMacaroon(adminMac) + require.NoError(t.t, err) + defer conn2.Close() + adminMacClient = lnrpc.NewLightningClient(conn2) + + // Changing the password takes a while, so we use the default timeout + // of 30 seconds to wait for the connection to be ready. + ctxt, _ = context.WithTimeout(context.Background(), defaultTimeout) + res, err = adminMacClient.NewAddress(ctxt, newAddrReq) + require.NoError(t.t, err) + if !strings.HasPrefix(res.Address, harnessNetParams.Bech32HRPSegwit) { + t.Fatalf("returned address was not a regtest address") + } +} + // readMacaroonFromHex loads a macaroon from a hex string. func readMacaroonFromHex(macHex string) (*macaroon.Macaroon, error) { macBytes, err := hex.DecodeString(macHex) diff --git a/lntest/itest/lnd_test.go b/lntest/itest/lnd_test.go index e49c7090..03c6490b 100644 --- a/lntest/itest/lnd_test.go +++ b/lntest/itest/lnd_test.go @@ -774,7 +774,9 @@ func testGetRecoveryInfo(net *lntest.NetworkHarness, t *harnessTest) { // used for key derivation. This will bring up Carol with an empty // wallet, and such that she is synced up. password := []byte("The Magic Words are Squeamish Ossifrage") - carol, mnemonic, err := net.NewNodeWithSeed("Carol", nil, password) + carol, mnemonic, _, err := net.NewNodeWithSeed( + "Carol", nil, password, false, + ) if err != nil { t.Fatalf("unable to create node with seed; %v", err) } @@ -875,7 +877,9 @@ func testOnchainFundRecovery(net *lntest.NetworkHarness, t *harnessTest) { // used for key derivation. This will bring up Carol with an empty // wallet, and such that she is synced up. password := []byte("The Magic Words are Squeamish Ossifrage") - carol, mnemonic, err := net.NewNodeWithSeed("Carol", nil, password) + carol, mnemonic, _, err := net.NewNodeWithSeed( + "Carol", nil, password, false, + ) if err != nil { t.Fatalf("unable to create node with seed; %v", err) } diff --git a/lntest/itest/lnd_test_list_on_test.go b/lntest/itest/lnd_test_list_on_test.go index 3575213c..cbbc8b37 100644 --- a/lntest/itest/lnd_test_list_on_test.go +++ b/lntest/itest/lnd_test_list_on_test.go @@ -282,4 +282,8 @@ var allTestCases = []*testCase{ name: "connection timeout", test: testNetworkConnectionTimeout, }, + { + name: "stateless init", + test: testStatelessInit, + }, } diff --git a/lntest/node.go b/lntest/node.go index cb368e75..32d1cadb 100644 --- a/lntest/node.go +++ b/lntest/node.go @@ -613,22 +613,88 @@ func (hn *HarnessNode) initClientWhenReady() error { } // Init initializes a harness node by passing the init request via rpc. After -// the request is submitted, this method will block until an -// macaroon-authenticated rpc connection can be established to the harness node. +// the request is submitted, this method will block until a +// macaroon-authenticated RPC connection can be established to the harness node. // Once established, the new connection is used to initialize the // LightningClient and subscribes the HarnessNode to topology changes. func (hn *HarnessNode) Init(ctx context.Context, - initReq *lnrpc.InitWalletRequest) error { + initReq *lnrpc.InitWalletRequest) (*lnrpc.InitWalletResponse, error) { - ctxt, _ := context.WithTimeout(ctx, DefaultTimeout) - _, err := hn.InitWallet(ctxt, initReq) + ctxt, cancel := context.WithTimeout(ctx, DefaultTimeout) + defer cancel() + response, err := hn.InitWallet(ctxt, initReq) if err != nil { - return err + return nil, err } // Wait for the wallet to finish unlocking, such that we can connect to // it via a macaroon-authenticated rpc connection. - return hn.initClientWhenReady() + var conn *grpc.ClientConn + if err = wait.Predicate(func() bool { + // If the node has been initialized stateless, we need to pass + // the macaroon to the client. + if initReq.StatelessInit { + adminMac := &macaroon.Macaroon{} + err := adminMac.UnmarshalBinary(response.AdminMacaroon) + if err != nil { + return false + } + conn, err = hn.ConnectRPCWithMacaroon(adminMac) + return err == nil + } + + // Normal initialization, we expect a macaroon to be in the + // file system. + conn, err = hn.ConnectRPC(true) + return err == nil + }, DefaultTimeout); err != nil { + return nil, err + } + + return response, hn.initLightningClient(conn) +} + +// InitChangePassword initializes a harness node by passing the change password +// request via RPC. After the request is submitted, this method will block until +// a macaroon-authenticated RPC connection can be established to the harness +// node. Once established, the new connection is used to initialize the +// LightningClient and subscribes the HarnessNode to topology changes. +func (hn *HarnessNode) InitChangePassword(ctx context.Context, + chngPwReq *lnrpc.ChangePasswordRequest) (*lnrpc.ChangePasswordResponse, + error) { + + ctxt, cancel := context.WithTimeout(ctx, DefaultTimeout) + defer cancel() + response, err := hn.ChangePassword(ctxt, chngPwReq) + if err != nil { + return nil, err + } + + // Wait for the wallet to finish unlocking, such that we can connect to + // it via a macaroon-authenticated rpc connection. + var conn *grpc.ClientConn + if err = wait.Predicate(func() bool { + // If the node has been initialized stateless, we need to pass + // the macaroon to the client. + if chngPwReq.StatelessInit { + adminMac := &macaroon.Macaroon{} + err := adminMac.UnmarshalBinary(response.AdminMacaroon) + if err != nil { + return false + } + conn, err = hn.ConnectRPCWithMacaroon(adminMac) + return err == nil + } + + // Normal initialization, we expect a macaroon to be in the + // file system. + conn, err = hn.ConnectRPC(true) + return err == nil + }, DefaultTimeout); err != nil { + return nil, err + } + + return response, hn.initLightningClient(conn) } // Unlock attempts to unlock the wallet of the target HarnessNode. This method