prysm-pulse/validator/client/wait_for_activation_test.go
Preston Van Loon f074c5ee89
Fix validator activation monitoring with inactive keys (#8558)
* refactor / move waiting for activation updates

* Commentary

* Update test to follow the full code path

* gofmt and goimports

* manual imports fixes

* Apply suggestions from code review

typo fixes

* Remove redundant handleAccountsChanged and chan. Thanks @nisdas

* var sub = to sub :=

Co-authored-by: Radosław Kapka <rkapka@wp.pl>
2021-03-05 18:33:39 +00:00

381 lines
12 KiB
Go

package client
import (
"context"
"fmt"
"testing"
"time"
"github.com/golang/mock/gomock"
"github.com/pkg/errors"
ethpb "github.com/prysmaticlabs/ethereumapis/eth/v1alpha1"
"github.com/prysmaticlabs/prysm/shared/bls"
"github.com/prysmaticlabs/prysm/shared/mock"
"github.com/prysmaticlabs/prysm/shared/testutil/assert"
"github.com/prysmaticlabs/prysm/shared/testutil/require"
walletMock "github.com/prysmaticlabs/prysm/validator/accounts/testing"
"github.com/prysmaticlabs/prysm/validator/keymanager/derived"
constant "github.com/prysmaticlabs/prysm/validator/testing"
logTest "github.com/sirupsen/logrus/hooks/test"
"github.com/tyler-smith/go-bip39"
util "github.com/wealdtech/go-eth2-util"
)
func TestWaitActivation_ContextCanceled(t *testing.T) {
ctrl := gomock.NewController(t)
defer ctrl.Finish()
client := mock.NewMockBeaconNodeValidatorClient(ctrl)
privKey, err := bls.RandKey()
require.NoError(t, err)
pubKey := [48]byte{}
copy(pubKey[:], privKey.PublicKey().Marshal())
km := &mockKeymanager{
keysMap: map[[48]byte]bls.SecretKey{
pubKey: privKey,
},
}
v := validator{
validatorClient: client,
keyManager: km,
}
clientStream := mock.NewMockBeaconNodeValidator_WaitForActivationClient(ctrl)
client.EXPECT().WaitForActivation(
gomock.Any(),
&ethpb.ValidatorActivationRequest{
PublicKeys: [][]byte{pubKey[:]},
},
).Return(clientStream, nil)
clientStream.EXPECT().Recv().Return(
&ethpb.ValidatorActivationResponse{},
nil,
)
ctx, cancel := context.WithCancel(context.Background())
cancel()
assert.ErrorContains(t, cancelledCtx, v.WaitForActivation(ctx))
}
func TestWaitActivation_StreamSetupFails_AttemptsToReconnect(t *testing.T) {
ctrl := gomock.NewController(t)
defer ctrl.Finish()
client := mock.NewMockBeaconNodeValidatorClient(ctrl)
privKey, err := bls.RandKey()
require.NoError(t, err)
pubKey := [48]byte{}
copy(pubKey[:], privKey.PublicKey().Marshal())
km := &mockKeymanager{
keysMap: map[[48]byte]bls.SecretKey{
pubKey: privKey,
},
}
v := validator{
validatorClient: client,
keyManager: km,
}
clientStream := mock.NewMockBeaconNodeValidator_WaitForActivationClient(ctrl)
client.EXPECT().WaitForActivation(
gomock.Any(),
&ethpb.ValidatorActivationRequest{
PublicKeys: [][]byte{pubKey[:]},
},
).Return(clientStream, errors.New("failed stream")).Return(clientStream, nil)
resp := generateMockStatusResponse([][]byte{pubKey[:]})
resp.Statuses[0].Status.Status = ethpb.ValidatorStatus_ACTIVE
clientStream.EXPECT().Recv().Return(resp, nil)
assert.NoError(t, v.WaitForActivation(context.Background()))
}
func TestWaitForActivation_ReceiveErrorFromStream_AttemptsReconnection(t *testing.T) {
ctrl := gomock.NewController(t)
defer ctrl.Finish()
client := mock.NewMockBeaconNodeValidatorClient(ctrl)
privKey, err := bls.RandKey()
require.NoError(t, err)
pubKey := [48]byte{}
copy(pubKey[:], privKey.PublicKey().Marshal())
km := &mockKeymanager{
keysMap: map[[48]byte]bls.SecretKey{
pubKey: privKey,
},
}
v := validator{
validatorClient: client,
keyManager: km,
}
clientStream := mock.NewMockBeaconNodeValidator_WaitForActivationClient(ctrl)
client.EXPECT().WaitForActivation(
gomock.Any(),
&ethpb.ValidatorActivationRequest{
PublicKeys: [][]byte{pubKey[:]},
},
).Return(clientStream, nil)
// A stream fails the first time, but succeeds the second time.
resp := generateMockStatusResponse([][]byte{pubKey[:]})
resp.Statuses[0].Status.Status = ethpb.ValidatorStatus_ACTIVE
clientStream.EXPECT().Recv().Return(
nil,
errors.New("fails"),
).Return(resp, nil)
assert.NoError(t, v.WaitForActivation(context.Background()))
}
func TestWaitActivation_LogsActivationEpochOK(t *testing.T) {
hook := logTest.NewGlobal()
ctrl := gomock.NewController(t)
defer ctrl.Finish()
client := mock.NewMockBeaconNodeValidatorClient(ctrl)
privKey, err := bls.RandKey()
require.NoError(t, err)
pubKey := [48]byte{}
copy(pubKey[:], privKey.PublicKey().Marshal())
km := &mockKeymanager{
keysMap: map[[48]byte]bls.SecretKey{
pubKey: privKey,
},
}
v := validator{
validatorClient: client,
keyManager: km,
genesisTime: 1,
}
resp := generateMockStatusResponse([][]byte{pubKey[:]})
resp.Statuses[0].Status.Status = ethpb.ValidatorStatus_ACTIVE
clientStream := mock.NewMockBeaconNodeValidator_WaitForActivationClient(ctrl)
client.EXPECT().WaitForActivation(
gomock.Any(),
&ethpb.ValidatorActivationRequest{
PublicKeys: [][]byte{pubKey[:]},
},
).Return(clientStream, nil)
clientStream.EXPECT().Recv().Return(
resp,
nil,
)
assert.NoError(t, v.WaitForActivation(context.Background()), "Could not wait for activation")
assert.LogsContain(t, hook, "Validator activated")
}
func TestWaitForActivation_Exiting(t *testing.T) {
ctrl := gomock.NewController(t)
defer ctrl.Finish()
client := mock.NewMockBeaconNodeValidatorClient(ctrl)
privKey, err := bls.RandKey()
require.NoError(t, err)
pubKey := [48]byte{}
copy(pubKey[:], privKey.PublicKey().Marshal())
km := &mockKeymanager{
keysMap: map[[48]byte]bls.SecretKey{
pubKey: privKey,
},
}
v := validator{
validatorClient: client,
keyManager: km,
genesisTime: 1,
}
resp := generateMockStatusResponse([][]byte{pubKey[:]})
resp.Statuses[0].Status.Status = ethpb.ValidatorStatus_EXITING
clientStream := mock.NewMockBeaconNodeValidator_WaitForActivationClient(ctrl)
client.EXPECT().WaitForActivation(
gomock.Any(),
&ethpb.ValidatorActivationRequest{
PublicKeys: [][]byte{pubKey[:]},
},
).Return(clientStream, nil)
clientStream.EXPECT().Recv().Return(
resp,
nil,
)
assert.NoError(t, v.WaitForActivation(context.Background()))
}
func TestWaitForActivation_RefetchKeys(t *testing.T) {
originalPeriod := keyRefetchPeriod
defer func() {
keyRefetchPeriod = originalPeriod
}()
keyRefetchPeriod = 1 * time.Second
hook := logTest.NewGlobal()
ctrl := gomock.NewController(t)
defer ctrl.Finish()
client := mock.NewMockBeaconNodeValidatorClient(ctrl)
privKey, err := bls.RandKey()
require.NoError(t, err)
pubKey := [48]byte{}
copy(pubKey[:], privKey.PublicKey().Marshal())
km := &mockKeymanager{
keysMap: map[[48]byte]bls.SecretKey{
pubKey: privKey,
},
fetchNoKeys: true,
}
v := validator{
validatorClient: client,
keyManager: km,
genesisTime: 1,
}
resp := generateMockStatusResponse([][]byte{pubKey[:]})
resp.Statuses[0].Status.Status = ethpb.ValidatorStatus_ACTIVE
clientStream := mock.NewMockBeaconNodeValidator_WaitForActivationClient(ctrl)
client.EXPECT().WaitForActivation(
gomock.Any(),
&ethpb.ValidatorActivationRequest{
PublicKeys: [][]byte{pubKey[:]},
},
).Return(clientStream, nil)
clientStream.EXPECT().Recv().Return(
resp,
nil,
)
assert.NoError(t, v.waitForActivation(context.Background(), make(chan [][48]byte)), "Could not wait for activation")
assert.LogsContain(t, hook, msgNoKeysFetched)
assert.LogsContain(t, hook, "Validator activated")
}
// Regression test for a scenario where you start with an inactive key and then import an active key.
func TestWaitForActivation_AccountsChanged(t *testing.T) {
hook := logTest.NewGlobal()
ctrl := gomock.NewController(t)
defer ctrl.Finish()
t.Run("Imported keymanager", func(t *testing.T) {
inactivePrivKey, err := bls.RandKey()
require.NoError(t, err)
inactivePubKey := [48]byte{}
copy(inactivePubKey[:], inactivePrivKey.PublicKey().Marshal())
activePrivKey, err := bls.RandKey()
require.NoError(t, err)
activePubKey := [48]byte{}
copy(activePubKey[:], activePrivKey.PublicKey().Marshal())
km := &mockKeymanager{
keysMap: map[[48]byte]bls.SecretKey{
inactivePubKey: inactivePrivKey,
},
}
client := mock.NewMockBeaconNodeValidatorClient(ctrl)
v := validator{
validatorClient: client,
keyManager: km,
genesisTime: 1,
}
inactiveResp := generateMockStatusResponse([][]byte{inactivePubKey[:]})
inactiveResp.Statuses[0].Status.Status = ethpb.ValidatorStatus_UNKNOWN_STATUS
inactiveClientStream := mock.NewMockBeaconNodeValidator_WaitForActivationClient(ctrl)
client.EXPECT().WaitForActivation(
gomock.Any(),
&ethpb.ValidatorActivationRequest{
PublicKeys: [][]byte{inactivePubKey[:]},
},
).Return(inactiveClientStream, nil)
inactiveClientStream.EXPECT().Recv().Return(
inactiveResp,
nil,
).AnyTimes()
activeResp := generateMockStatusResponse([][]byte{inactivePubKey[:], activePubKey[:]})
activeResp.Statuses[0].Status.Status = ethpb.ValidatorStatus_UNKNOWN_STATUS
activeResp.Statuses[1].Status.Status = ethpb.ValidatorStatus_ACTIVE
activeClientStream := mock.NewMockBeaconNodeValidator_WaitForActivationClient(ctrl)
client.EXPECT().WaitForActivation(
gomock.Any(),
&ethpb.ValidatorActivationRequest{
PublicKeys: [][]byte{inactivePubKey[:], activePubKey[:]},
},
).Return(activeClientStream, nil)
activeClientStream.EXPECT().Recv().Return(
activeResp,
nil,
)
go func() {
// We add the active key into the keymanager and simulate a key refresh.
time.Sleep(time.Second * 1)
km.keysMap[activePubKey] = activePrivKey
km.SimulateAccountChanges()
}()
assert.NoError(t, v.WaitForActivation(context.Background()))
assert.LogsContain(t, hook, "Waiting for deposit to be observed by beacon node")
assert.LogsContain(t, hook, "Validator activated")
})
t.Run("Derived keymanager", func(t *testing.T) {
seed := bip39.NewSeed(constant.TestMnemonic, "")
inactivePrivKey, err :=
util.PrivateKeyFromSeedAndPath(seed, fmt.Sprintf(derived.ValidatingKeyDerivationPathTemplate, 0))
require.NoError(t, err)
inactivePubKey := [48]byte{}
copy(inactivePubKey[:], inactivePrivKey.PublicKey().Marshal())
activePrivKey, err :=
util.PrivateKeyFromSeedAndPath(seed, fmt.Sprintf(derived.ValidatingKeyDerivationPathTemplate, 1))
require.NoError(t, err)
activePubKey := [48]byte{}
copy(activePubKey[:], activePrivKey.PublicKey().Marshal())
wallet := &walletMock.Wallet{
Files: make(map[string]map[string][]byte),
AccountPasswords: make(map[string]string),
WalletPassword: "secretPassw0rd$1999",
}
ctx := context.Background()
km, err := derived.NewKeymanager(ctx, &derived.SetupConfig{
Wallet: wallet,
ListenForChanges: true,
})
require.NoError(t, err)
err = km.RecoverAccountsFromMnemonic(ctx, constant.TestMnemonic, "", 1)
require.NoError(t, err)
client := mock.NewMockBeaconNodeValidatorClient(ctrl)
v := validator{
validatorClient: client,
keyManager: km,
genesisTime: 1,
}
inactiveResp := generateMockStatusResponse([][]byte{inactivePubKey[:]})
inactiveResp.Statuses[0].Status.Status = ethpb.ValidatorStatus_UNKNOWN_STATUS
inactiveClientStream := mock.NewMockBeaconNodeValidator_WaitForActivationClient(ctrl)
client.EXPECT().WaitForActivation(
gomock.Any(),
&ethpb.ValidatorActivationRequest{
PublicKeys: [][]byte{inactivePubKey[:]},
},
).Return(inactiveClientStream, nil)
inactiveClientStream.EXPECT().Recv().Return(
inactiveResp,
nil,
).AnyTimes()
activeResp := generateMockStatusResponse([][]byte{inactivePubKey[:], activePubKey[:]})
activeResp.Statuses[0].Status.Status = ethpb.ValidatorStatus_UNKNOWN_STATUS
activeResp.Statuses[1].Status.Status = ethpb.ValidatorStatus_ACTIVE
activeClientStream := mock.NewMockBeaconNodeValidator_WaitForActivationClient(ctrl)
client.EXPECT().WaitForActivation(
gomock.Any(),
&ethpb.ValidatorActivationRequest{
PublicKeys: [][]byte{inactivePubKey[:], activePubKey[:]},
},
).Return(activeClientStream, nil)
activeClientStream.EXPECT().Recv().Return(
activeResp,
nil,
)
channel := make(chan [][48]byte)
go func() {
// We add the active key into the keymanager and simulate a key refresh.
time.Sleep(time.Second * 1)
err = km.RecoverAccountsFromMnemonic(ctx, constant.TestMnemonic, "", 2)
require.NoError(t, err)
channel <- [][48]byte{}
}()
assert.NoError(t, v.waitForActivation(context.Background(), channel))
assert.LogsContain(t, hook, "Waiting for deposit to be observed by beacon node")
assert.LogsContain(t, hook, "Validator activated")
})
}