prysm-pulse/validator/client/wait_for_activation_test.go
kasey ba9a744552
Keymanager mock cleanup (#12341)
* add callback for deterministic key fetch ordering

* provide a nicer way to control key ordering

* more concise test setup = less confusion

---------

Co-authored-by: Kasey Kirkham <kasey@users.noreply.github.com>
2023-04-27 05:25:17 +00:00

351 lines
13 KiB
Go

package client
import (
"bytes"
"context"
"fmt"
"testing"
"time"
"github.com/golang/mock/gomock"
"github.com/pkg/errors"
fieldparams "github.com/prysmaticlabs/prysm/v4/config/fieldparams"
ethpb "github.com/prysmaticlabs/prysm/v4/proto/prysm/v1alpha1"
"github.com/prysmaticlabs/prysm/v4/testing/assert"
"github.com/prysmaticlabs/prysm/v4/testing/mock"
"github.com/prysmaticlabs/prysm/v4/testing/require"
validatormock "github.com/prysmaticlabs/prysm/v4/testing/validator-mock"
walletMock "github.com/prysmaticlabs/prysm/v4/validator/accounts/testing"
"github.com/prysmaticlabs/prysm/v4/validator/keymanager/derived"
constant "github.com/prysmaticlabs/prysm/v4/validator/testing"
logTest "github.com/sirupsen/logrus/hooks/test"
mock2 "github.com/stretchr/testify/mock"
"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()
validatorClient := validatormock.NewMockValidatorClient(ctrl)
beaconClient := validatormock.NewMockBeaconChainClient(ctrl)
kp := randKeypair(t)
v := validator{
validatorClient: validatorClient,
keyManager: newMockKeymanager(t, kp),
beaconClient: beaconClient,
}
clientStream := mock.NewMockBeaconNodeValidator_WaitForActivationClient(ctrl)
validatorClient.EXPECT().WaitForActivation(
gomock.Any(),
&ethpb.ValidatorActivationRequest{
PublicKeys: [][]byte{kp.pub[:]},
},
).Return(clientStream, nil)
clientStream.EXPECT().Recv().Return(
&ethpb.ValidatorActivationResponse{},
nil,
)
ctx, cancel := context.WithCancel(context.Background())
cancel()
assert.ErrorContains(t, cancelledCtx, v.WaitForActivation(ctx, nil))
}
func TestWaitActivation_StreamSetupFails_AttemptsToReconnect(t *testing.T) {
ctrl := gomock.NewController(t)
defer ctrl.Finish()
validatorClient := validatormock.NewMockValidatorClient(ctrl)
beaconClient := validatormock.NewMockBeaconChainClient(ctrl)
kp := randKeypair(t)
v := validator{
validatorClient: validatorClient,
keyManager: newMockKeymanager(t, kp),
beaconClient: beaconClient,
}
clientStream := mock.NewMockBeaconNodeValidator_WaitForActivationClient(ctrl)
validatorClient.EXPECT().WaitForActivation(
gomock.Any(),
&ethpb.ValidatorActivationRequest{
PublicKeys: [][]byte{kp.pub[:]},
},
).Return(clientStream, errors.New("failed stream")).Return(clientStream, nil)
beaconClient.EXPECT().ListValidators(gomock.Any(), gomock.Any()).Return(&ethpb.Validators{}, nil)
resp := generateMockStatusResponse([][]byte{kp.pub[:]})
resp.Statuses[0].Status.Status = ethpb.ValidatorStatus_ACTIVE
clientStream.EXPECT().Recv().Return(resp, nil)
assert.NoError(t, v.WaitForActivation(context.Background(), nil))
}
func TestWaitForActivation_ReceiveErrorFromStream_AttemptsReconnection(t *testing.T) {
ctrl := gomock.NewController(t)
defer ctrl.Finish()
validatorClient := validatormock.NewMockValidatorClient(ctrl)
beaconClient := validatormock.NewMockBeaconChainClient(ctrl)
kp := randKeypair(t)
v := validator{
validatorClient: validatorClient,
keyManager: newMockKeymanager(t, kp),
beaconClient: beaconClient,
}
clientStream := mock.NewMockBeaconNodeValidator_WaitForActivationClient(ctrl)
validatorClient.EXPECT().WaitForActivation(
gomock.Any(),
&ethpb.ValidatorActivationRequest{
PublicKeys: [][]byte{kp.pub[:]},
},
).Return(clientStream, nil)
beaconClient.EXPECT().ListValidators(gomock.Any(), gomock.Any()).Return(&ethpb.Validators{}, nil)
// A stream fails the first time, but succeeds the second time.
resp := generateMockStatusResponse([][]byte{kp.pub[:]})
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(), nil))
}
func TestWaitActivation_LogsActivationEpochOK(t *testing.T) {
hook := logTest.NewGlobal()
ctrl := gomock.NewController(t)
defer ctrl.Finish()
validatorClient := validatormock.NewMockValidatorClient(ctrl)
beaconClient := validatormock.NewMockBeaconChainClient(ctrl)
kp := randKeypair(t)
v := validator{
validatorClient: validatorClient,
keyManager: newMockKeymanager(t, kp),
genesisTime: 1,
beaconClient: beaconClient,
}
resp := generateMockStatusResponse([][]byte{kp.pub[:]})
resp.Statuses[0].Status.Status = ethpb.ValidatorStatus_ACTIVE
clientStream := mock.NewMockBeaconNodeValidator_WaitForActivationClient(ctrl)
validatorClient.EXPECT().WaitForActivation(
gomock.Any(),
&ethpb.ValidatorActivationRequest{
PublicKeys: [][]byte{kp.pub[:]},
},
).Return(clientStream, nil)
beaconClient.EXPECT().ListValidators(gomock.Any(), gomock.Any()).Return(&ethpb.Validators{}, nil)
clientStream.EXPECT().Recv().Return(
resp,
nil,
)
assert.NoError(t, v.WaitForActivation(context.Background(), nil), "Could not wait for activation")
assert.LogsContain(t, hook, "Validator activated")
}
func TestWaitForActivation_Exiting(t *testing.T) {
ctrl := gomock.NewController(t)
defer ctrl.Finish()
validatorClient := validatormock.NewMockValidatorClient(ctrl)
beaconClient := validatormock.NewMockBeaconChainClient(ctrl)
kp := randKeypair(t)
v := validator{
validatorClient: validatorClient,
keyManager: newMockKeymanager(t, kp),
beaconClient: beaconClient,
}
resp := generateMockStatusResponse([][]byte{kp.pub[:]})
resp.Statuses[0].Status.Status = ethpb.ValidatorStatus_EXITING
clientStream := mock.NewMockBeaconNodeValidator_WaitForActivationClient(ctrl)
validatorClient.EXPECT().WaitForActivation(
gomock.Any(),
&ethpb.ValidatorActivationRequest{
PublicKeys: [][]byte{kp.pub[:]},
},
).Return(clientStream, nil)
beaconClient.EXPECT().ListValidators(gomock.Any(), gomock.Any()).Return(&ethpb.Validators{}, nil)
clientStream.EXPECT().Recv().Return(
resp,
nil,
)
assert.NoError(t, v.WaitForActivation(context.Background(), nil))
}
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()
validatorClient := validatormock.NewMockValidatorClient(ctrl)
beaconClient := validatormock.NewMockBeaconChainClient(ctrl)
kp := randKeypair(t)
km := newMockKeymanager(t, kp)
km.fetchNoKeys = true
v := validator{
validatorClient: validatorClient,
keyManager: km,
beaconClient: beaconClient,
}
resp := generateMockStatusResponse([][]byte{kp.pub[:]})
resp.Statuses[0].Status.Status = ethpb.ValidatorStatus_ACTIVE
clientStream := mock.NewMockBeaconNodeValidator_WaitForActivationClient(ctrl)
validatorClient.EXPECT().WaitForActivation(
gomock.Any(),
&ethpb.ValidatorActivationRequest{
PublicKeys: [][]byte{kp.pub[:]},
},
).Return(clientStream, nil)
beaconClient.EXPECT().ListValidators(gomock.Any(), gomock.Any()).Return(&ethpb.Validators{}, nil)
clientStream.EXPECT().Recv().Return(
resp,
nil)
assert.NoError(t, v.internalWaitForActivation(context.Background(), make(chan [][fieldparams.BLSPubkeyLength]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) {
inactive := randKeypair(t)
active := randKeypair(t)
km := newMockKeymanager(t, inactive)
validatorClient := validatormock.NewMockValidatorClient(ctrl)
beaconClient := validatormock.NewMockBeaconChainClient(ctrl)
v := validator{
validatorClient: validatorClient,
keyManager: km,
beaconClient: beaconClient,
}
inactiveResp := generateMockStatusResponse([][]byte{inactive.pub[:]})
inactiveResp.Statuses[0].Status.Status = ethpb.ValidatorStatus_UNKNOWN_STATUS
inactiveClientStream := mock.NewMockBeaconNodeValidator_WaitForActivationClient(ctrl)
validatorClient.EXPECT().WaitForActivation(
gomock.Any(),
&ethpb.ValidatorActivationRequest{
PublicKeys: [][]byte{inactive.pub[:]},
},
).Return(inactiveClientStream, nil)
beaconClient.EXPECT().ListValidators(gomock.Any(), gomock.Any()).Return(&ethpb.Validators{}, nil).AnyTimes()
inactiveClientStream.EXPECT().Recv().Return(
inactiveResp,
nil,
).AnyTimes()
activeResp := generateMockStatusResponse([][]byte{inactive.pub[:], active.pub[:]})
activeResp.Statuses[0].Status.Status = ethpb.ValidatorStatus_UNKNOWN_STATUS
activeResp.Statuses[1].Status.Status = ethpb.ValidatorStatus_ACTIVE
activeClientStream := mock.NewMockBeaconNodeValidator_WaitForActivationClient(ctrl)
validatorClient.EXPECT().WaitForActivation(
gomock.Any(),
mock2.MatchedBy(func(req *ethpb.ValidatorActivationRequest) bool {
found := 0
for _, pk := range req.PublicKeys {
if bytes.Equal(pk, active.pub[:]) || bytes.Equal(pk, inactive.pub[:]) {
found++
}
}
return found == 2
}),
).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)
require.NoError(t, km.add(active))
km.SimulateAccountChanges(make([][fieldparams.BLSPubkeyLength]byte, 0))
}()
assert.NoError(t, v.WaitForActivation(context.Background(), nil))
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)
var inactivePubKey [fieldparams.BLSPubkeyLength]byte
copy(inactivePubKey[:], inactivePrivKey.PublicKey().Marshal())
activePrivKey, err :=
util.PrivateKeyFromSeedAndPath(seed, fmt.Sprintf(derived.ValidatingKeyDerivationPathTemplate, 1))
require.NoError(t, err)
var activePubKey [fieldparams.BLSPubkeyLength]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, derived.DefaultMnemonicLanguage, "", 1)
require.NoError(t, err)
validatorClient := validatormock.NewMockValidatorClient(ctrl)
beaconClient := validatormock.NewMockBeaconChainClient(ctrl)
v := validator{
validatorClient: validatorClient,
keyManager: km,
genesisTime: 1,
beaconClient: beaconClient,
}
inactiveResp := generateMockStatusResponse([][]byte{inactivePubKey[:]})
inactiveResp.Statuses[0].Status.Status = ethpb.ValidatorStatus_UNKNOWN_STATUS
inactiveClientStream := mock.NewMockBeaconNodeValidator_WaitForActivationClient(ctrl)
validatorClient.EXPECT().WaitForActivation(
gomock.Any(),
&ethpb.ValidatorActivationRequest{
PublicKeys: [][]byte{inactivePubKey[:]},
},
).Return(inactiveClientStream, nil)
beaconClient.EXPECT().ListValidators(gomock.Any(), gomock.Any()).Return(&ethpb.Validators{}, nil).AnyTimes()
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)
validatorClient.EXPECT().WaitForActivation(
gomock.Any(),
&ethpb.ValidatorActivationRequest{
PublicKeys: [][]byte{inactivePubKey[:], activePubKey[:]},
},
).Return(activeClientStream, nil)
activeClientStream.EXPECT().Recv().Return(
activeResp,
nil,
)
channel := make(chan [][fieldparams.BLSPubkeyLength]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, derived.DefaultMnemonicLanguage, "", 2)
require.NoError(t, err)
channel <- [][fieldparams.BLSPubkeyLength]byte{}
}()
assert.NoError(t, v.internalWaitForActivation(context.Background(), channel))
assert.LogsContain(t, hook, "Waiting for deposit to be observed by beacon node")
assert.LogsContain(t, hook, "Validator activated")
})
}