mirror of
https://github.com/OffchainLabs/prysm.git
synced 2026-04-19 03:01:06 -04:00
<!-- Thanks for sending a PR! Before submitting: 1. If this is your first PR, check out our contribution guide here https://docs.prylabs.network/docs/contribute/contribution-guidelines You will then need to sign our Contributor License Agreement (CLA), which will show up as a comment from a bot in this pull request after you open it. We cannot review code without a signed CLA. 2. Please file an associated tracking issue if this pull request is non-trivial and requires context for our team to understand. All features and most bug fixes should have an associated issue with a design discussed and decided upon. Small bug fixes and documentation improvements don't need issues. 3. New features and bug fixes must have tests. Documentation may need to be updated. If you're unsure what to update, send the PR, and we'll discuss in review. 4. Note that PRs updating dependencies and new Go versions are not accepted. Please file an issue instead. 5. A changelog entry is required for user facing issues. --> **What type of PR is this?** ## Summary This PR implements gRPC fallback support for the validator client, allowing it to automatically switch between multiple beacon node endpoints when the primary node becomes unavailable or unhealthy. ## Changes - Added `grpcConnectionProvider` to manage multiple gRPC connections with circular failover - Validator automatically detects unhealthy beacon nodes and switches to the next available endpoint - Health checks verify both node responsiveness AND sync status before accepting a node - Improved logging to only show "Found fully synced beacon node" when an actual switch occurs (reduces log noise) I removed the old middleware that uses gRPC's built in load balancer because: - gRPC's pick_first load balancer doesn't provide sync-status-aware failover - The validator needs to ensure it connects to a fully synced node, not just a reachable one ## Test Scenario ### Setup Deployed a 4-node Kurtosis testnet with local validator connecting to 2 beacon nodes: ```yaml # kurtosis-grpc-fallback-test.yaml participants: - el_type: nethermind cl_type: prysm validator_count: 128 # Keeps chain advancing - el_type: nethermind cl_type: prysm validator_count: 64 - el_type: nethermind cl_type: prysm validator_count: 64 # Keeps chain advancing - el_type: nethermind cl_type: prysm validator_count: 64 # Keeps chain advancing network_params: fulu_fork_epoch: 0 seconds_per_slot: 6 ``` Local validator started with: ```bash ./validator --beacon-rpc-provider=127.0.0.1:33005,127.0.0.1:33012 ... ``` ### Test 1: Primary Failover (cl-1 → cl-2) 1. Stopped cl-1 beacon node 2. Validator detected failure and switched to cl-2 **Logs:** ``` WARN Beacon node is not responding, switching host currentHost=127.0.0.1:33005 nextHost=127.0.0.1:33012 DEBUG Trying gRPC endpoint newHost=127.0.0.1:33012 previousHost=127.0.0.1:33005 INFO Failover succeeded: connected to healthy beacon node failedAttempts=[127.0.0.1:33005] newHost=127.0.0.1:33012 previousHost=127.0.0.1:33005 ``` **Result:** ✅ PASSED - Validator continued submitting attestations on cl-2 ### Test 2: Circular Failover (cl-2 → cl-1) 1. Restarted cl-1, stopped cl-2 2. Validator detected failure and switched back to cl-1 **Logs:** ``` WARN Beacon node is not responding, switching host currentHost=127.0.0.1:33012 nextHost=127.0.0.1:33005 DEBUG Trying gRPC endpoint newHost=127.0.0.1:33005 previousHost=127.0.0.1:33012 INFO Failover succeeded: connected to healthy beacon node failedAttempts=[127.0.0.1:33012] newHost=127.0.0.1:33005 previousHost=127.0.0.1:33012 ``` **Result:** ✅ PASSED - Circular fallback works correctly ## Key Log Messages | Log Level | Message | Source | |-----------|---------|--------| | WARN | "Beacon node is not responding, switching host" | `changeHost()` in validator.go | | INFO | "Switched gRPC endpoint" | `SetHost()` in grpc_connection_provider.go | | INFO | "Found fully synced beacon node" | `FindHealthyHost()` in validator.go (only on actual switch) | ## Test Plan - [x] Verify primary failover (cl-1 → cl-2) - [x] Verify circular failover (cl-2 → cl-1) - [x] Verify validator continues producing attestations after switch - [x] Verify "Found fully synced beacon node" only logs on actual switch (not every health check) **What does this PR do? Why is it needed?** **Which issues(s) does this PR fix?** Fixes # https://github.com/OffchainLabs/prysm/pull/7133 **Other notes for review** **Acknowledgements** - [x] I have read [CONTRIBUTING.md](https://github.com/prysmaticlabs/prysm/blob/develop/CONTRIBUTING.md). - [x] I have included a uniquely named [changelog fragment file](https://github.com/prysmaticlabs/prysm/blob/develop/CONTRIBUTING.md#maintaining-changelogmd). - [x] I have added a description with sufficient context for reviewers to understand this PR. - [x] I have tested that my changes work as expected and I added a testing plan to the PR description (if applicable). --------- Co-authored-by: factory-droid[bot] <138933559+factory-droid[bot]@users.noreply.github.com> Co-authored-by: Radosław Kapka <rkapka@wp.pl> Co-authored-by: Manu NALEPA <enalepa@offchainlabs.com>
3200 lines
107 KiB
Go
3200 lines
107 KiB
Go
package client
|
|
|
|
import (
|
|
"bytes"
|
|
"context"
|
|
"errors"
|
|
"flag"
|
|
"fmt"
|
|
"io"
|
|
"math"
|
|
"os"
|
|
"path/filepath"
|
|
"sort"
|
|
"strings"
|
|
"sync"
|
|
"testing"
|
|
"time"
|
|
|
|
grpcutil "github.com/OffchainLabs/prysm/v7/api/grpc"
|
|
"github.com/OffchainLabs/prysm/v7/api/rest"
|
|
"github.com/OffchainLabs/prysm/v7/api/server/structs"
|
|
"github.com/OffchainLabs/prysm/v7/async/event"
|
|
"github.com/OffchainLabs/prysm/v7/cmd/validator/flags"
|
|
"github.com/OffchainLabs/prysm/v7/config/features"
|
|
fieldparams "github.com/OffchainLabs/prysm/v7/config/fieldparams"
|
|
"github.com/OffchainLabs/prysm/v7/config/params"
|
|
"github.com/OffchainLabs/prysm/v7/config/proposer"
|
|
"github.com/OffchainLabs/prysm/v7/consensus-types/primitives"
|
|
validatorType "github.com/OffchainLabs/prysm/v7/consensus-types/validator"
|
|
"github.com/OffchainLabs/prysm/v7/crypto/bls"
|
|
blsmock "github.com/OffchainLabs/prysm/v7/crypto/bls/common/mock"
|
|
"github.com/OffchainLabs/prysm/v7/encoding/bytesutil"
|
|
ethpb "github.com/OffchainLabs/prysm/v7/proto/prysm/v1alpha1"
|
|
validatorpb "github.com/OffchainLabs/prysm/v7/proto/prysm/v1alpha1/validator-client"
|
|
"github.com/OffchainLabs/prysm/v7/testing/assert"
|
|
"github.com/OffchainLabs/prysm/v7/testing/require"
|
|
"github.com/OffchainLabs/prysm/v7/testing/util"
|
|
validatormock "github.com/OffchainLabs/prysm/v7/testing/validator-mock"
|
|
"github.com/OffchainLabs/prysm/v7/validator/accounts/wallet"
|
|
"github.com/OffchainLabs/prysm/v7/validator/client/iface"
|
|
dbTest "github.com/OffchainLabs/prysm/v7/validator/db/testing"
|
|
validatorHelpers "github.com/OffchainLabs/prysm/v7/validator/helpers"
|
|
"github.com/OffchainLabs/prysm/v7/validator/keymanager"
|
|
"github.com/OffchainLabs/prysm/v7/validator/keymanager/local"
|
|
remoteweb3signer "github.com/OffchainLabs/prysm/v7/validator/keymanager/remote-web3signer"
|
|
"github.com/ethereum/go-ethereum/common"
|
|
"github.com/ethereum/go-ethereum/common/hexutil"
|
|
"github.com/golang/protobuf/ptypes/empty"
|
|
"github.com/sirupsen/logrus"
|
|
logTest "github.com/sirupsen/logrus/hooks/test"
|
|
"github.com/urfave/cli/v2"
|
|
"go.uber.org/mock/gomock"
|
|
"google.golang.org/protobuf/types/known/emptypb"
|
|
)
|
|
|
|
func init() {
|
|
logrus.SetLevel(logrus.DebugLevel)
|
|
logrus.SetOutput(io.Discard)
|
|
}
|
|
|
|
var _ iface.Validator = (*validator)(nil)
|
|
|
|
const cancelledCtx = "context has been canceled"
|
|
|
|
var unknownIndex = primitives.ValidatorIndex(^uint64(0))
|
|
|
|
func genMockKeymanager(t *testing.T, numKeys int) *mockKeymanager {
|
|
pairs := make([]keypair, numKeys)
|
|
for i := range numKeys {
|
|
pairs[i] = randKeypair(t)
|
|
}
|
|
|
|
return newMockKeymanager(t, pairs...)
|
|
}
|
|
|
|
type keypair struct {
|
|
pub [fieldparams.BLSPubkeyLength]byte
|
|
pri bls.SecretKey
|
|
}
|
|
|
|
func randKeypair(t *testing.T) keypair {
|
|
pri, err := bls.RandKey()
|
|
require.NoError(t, err)
|
|
var pub [fieldparams.BLSPubkeyLength]byte
|
|
copy(pub[:], pri.PublicKey().Marshal())
|
|
return keypair{pub: pub, pri: pri}
|
|
}
|
|
|
|
func newMockKeymanager(t *testing.T, pairs ...keypair) *mockKeymanager {
|
|
m := &mockKeymanager{keysMap: make(map[[fieldparams.BLSPubkeyLength]byte]bls.SecretKey)}
|
|
require.NoError(t, m.add(pairs...))
|
|
return m
|
|
}
|
|
|
|
type mockKeymanager struct {
|
|
lock sync.RWMutex
|
|
keysMap map[[fieldparams.BLSPubkeyLength]byte]bls.SecretKey
|
|
keys [][fieldparams.BLSPubkeyLength]byte
|
|
fetchNoKeys bool
|
|
accountsChangedFeed *event.Feed
|
|
}
|
|
|
|
var errMockKeyExists = errors.New("key already in mockKeymanager map")
|
|
|
|
func (m *mockKeymanager) add(pairs ...keypair) error {
|
|
for _, kp := range pairs {
|
|
if _, exists := m.keysMap[kp.pub]; exists {
|
|
return errMockKeyExists
|
|
}
|
|
m.keys = append(m.keys, kp.pub)
|
|
m.keysMap[kp.pub] = kp.pri
|
|
}
|
|
return nil
|
|
}
|
|
|
|
func (m *mockKeymanager) FetchValidatingPublicKeys(_ context.Context) ([][fieldparams.BLSPubkeyLength]byte, error) {
|
|
m.lock.RLock()
|
|
defer m.lock.RUnlock()
|
|
if m.fetchNoKeys {
|
|
m.fetchNoKeys = false
|
|
return [][fieldparams.BLSPubkeyLength]byte{}, nil
|
|
}
|
|
return m.keys, nil
|
|
}
|
|
|
|
func (m *mockKeymanager) Sign(_ context.Context, req *validatorpb.SignRequest) (bls.Signature, error) {
|
|
var pubKey [fieldparams.BLSPubkeyLength]byte
|
|
copy(pubKey[:], req.PublicKey)
|
|
privKey, ok := m.keysMap[pubKey]
|
|
if !ok {
|
|
return nil, errors.New("not found")
|
|
}
|
|
sig := privKey.Sign(req.SigningRoot)
|
|
return sig, nil
|
|
}
|
|
|
|
func (m *mockKeymanager) SubscribeAccountChanges(pubKeysChan chan [][fieldparams.BLSPubkeyLength]byte) event.Subscription {
|
|
if m.accountsChangedFeed == nil {
|
|
m.accountsChangedFeed = &event.Feed{}
|
|
}
|
|
return m.accountsChangedFeed.Subscribe(pubKeysChan)
|
|
}
|
|
|
|
func (m *mockKeymanager) SimulateAccountChanges(newKeys [][fieldparams.BLSPubkeyLength]byte) {
|
|
m.accountsChangedFeed.Send(newKeys)
|
|
}
|
|
|
|
func (*mockKeymanager) ExtractKeystores(
|
|
_ context.Context, _ []bls.PublicKey, _ string,
|
|
) ([]*keymanager.Keystore, error) {
|
|
return nil, errors.New("extracting keys not supported on mock keymanager")
|
|
}
|
|
|
|
func (*mockKeymanager) ListKeymanagerAccounts(
|
|
context.Context, keymanager.ListKeymanagerAccountConfig) error {
|
|
return nil
|
|
}
|
|
|
|
func (*mockKeymanager) DeleteKeystores(context.Context, [][]byte,
|
|
) ([]*keymanager.KeyStatus, error) {
|
|
return nil, nil
|
|
}
|
|
|
|
func TestWaitForChainStart_SetsGenesisInfo(t *testing.T) {
|
|
for _, isSlashingProtectionMinimal := range [...]bool{false, true} {
|
|
t.Run(fmt.Sprintf("SlashingProtectionMinimal:%v", isSlashingProtectionMinimal), func(t *testing.T) {
|
|
ctrl := gomock.NewController(t)
|
|
defer ctrl.Finish()
|
|
client := validatormock.NewMockValidatorClient(ctrl)
|
|
|
|
db := dbTest.SetupDB(t, t.TempDir(), [][fieldparams.BLSPubkeyLength]byte{}, isSlashingProtectionMinimal)
|
|
v := validator{
|
|
validatorClient: client,
|
|
db: db,
|
|
}
|
|
|
|
// Make sure its clean at the start.
|
|
savedGenValRoot, err := db.GenesisValidatorsRoot(t.Context())
|
|
require.NoError(t, err)
|
|
assert.DeepEqual(t, []byte(nil), savedGenValRoot, "Unexpected saved genesis validators root")
|
|
|
|
genesis := time.Unix(1, 0)
|
|
genesisValidatorsRoot := bytesutil.ToBytes32([]byte("validators"))
|
|
client.EXPECT().WaitForChainStart(
|
|
gomock.Any(),
|
|
&emptypb.Empty{},
|
|
).Return(ðpb.ChainStartResponse{
|
|
Started: true,
|
|
GenesisTime: uint64(genesis.Unix()),
|
|
GenesisValidatorsRoot: genesisValidatorsRoot[:],
|
|
}, nil)
|
|
require.NoError(t, v.WaitForChainStart(t.Context()))
|
|
savedGenValRoot, err = db.GenesisValidatorsRoot(t.Context())
|
|
require.NoError(t, err)
|
|
|
|
assert.DeepEqual(t, genesisValidatorsRoot[:], savedGenValRoot, "Unexpected saved genesis validators root")
|
|
assert.Equal(t, genesis, v.genesisTime, "Unexpected chain start time")
|
|
|
|
// Make sure there are no errors running if it is the same data.
|
|
client.EXPECT().WaitForChainStart(
|
|
gomock.Any(),
|
|
&emptypb.Empty{},
|
|
).Return(ðpb.ChainStartResponse{
|
|
Started: true,
|
|
GenesisTime: uint64(genesis.Unix()),
|
|
GenesisValidatorsRoot: genesisValidatorsRoot[:],
|
|
}, nil)
|
|
require.NoError(t, v.WaitForChainStart(t.Context()))
|
|
})
|
|
}
|
|
}
|
|
|
|
func TestWaitForChainStart_SetsGenesisInfo_IncorrectSecondTry(t *testing.T) {
|
|
for _, isSlashingProtectionMinimal := range [...]bool{false, true} {
|
|
t.Run(fmt.Sprintf("SlashingProtectionMinimal:%v", isSlashingProtectionMinimal), func(t *testing.T) {
|
|
ctrl := gomock.NewController(t)
|
|
defer ctrl.Finish()
|
|
client := validatormock.NewMockValidatorClient(ctrl)
|
|
|
|
db := dbTest.SetupDB(t, t.TempDir(), [][fieldparams.BLSPubkeyLength]byte{}, isSlashingProtectionMinimal)
|
|
v := validator{
|
|
validatorClient: client,
|
|
db: db,
|
|
}
|
|
genesis := time.Unix(1, 0)
|
|
genesisValidatorsRoot := bytesutil.ToBytes32([]byte("validators"))
|
|
client.EXPECT().WaitForChainStart(
|
|
gomock.Any(),
|
|
&emptypb.Empty{},
|
|
).Return(ðpb.ChainStartResponse{
|
|
Started: true,
|
|
GenesisTime: uint64(genesis.Unix()),
|
|
GenesisValidatorsRoot: genesisValidatorsRoot[:],
|
|
}, nil)
|
|
require.NoError(t, v.WaitForChainStart(t.Context()))
|
|
savedGenValRoot, err := db.GenesisValidatorsRoot(t.Context())
|
|
require.NoError(t, err)
|
|
|
|
assert.DeepEqual(t, genesisValidatorsRoot[:], savedGenValRoot, "Unexpected saved genesis validators root")
|
|
assert.Equal(t, genesis, v.genesisTime, "Unexpected chain start time")
|
|
|
|
genesisValidatorsRoot = bytesutil.ToBytes32([]byte("badvalidators"))
|
|
|
|
// Make sure there are no errors running if it is the same data.
|
|
client.EXPECT().WaitForChainStart(
|
|
gomock.Any(),
|
|
&emptypb.Empty{},
|
|
).Return(ðpb.ChainStartResponse{
|
|
Started: true,
|
|
GenesisTime: uint64(genesis.Unix()),
|
|
GenesisValidatorsRoot: genesisValidatorsRoot[:],
|
|
}, nil)
|
|
err = v.WaitForChainStart(t.Context())
|
|
require.ErrorContains(t, "does not match root saved", err)
|
|
})
|
|
}
|
|
}
|
|
|
|
func TestWaitForChainStart_ContextCanceled(t *testing.T) {
|
|
ctrl := gomock.NewController(t)
|
|
defer ctrl.Finish()
|
|
client := validatormock.NewMockValidatorClient(ctrl)
|
|
|
|
v := validator{
|
|
//keyManager: testKeyManager,
|
|
validatorClient: client,
|
|
}
|
|
genesis := uint64(time.Unix(0, 0).Unix())
|
|
genesisValidatorsRoot := bytesutil.PadTo([]byte("validators"), 32)
|
|
client.EXPECT().WaitForChainStart(
|
|
gomock.Any(),
|
|
&emptypb.Empty{},
|
|
).Return(ðpb.ChainStartResponse{
|
|
Started: true,
|
|
GenesisTime: genesis,
|
|
GenesisValidatorsRoot: genesisValidatorsRoot,
|
|
}, nil)
|
|
ctx, cancel := context.WithCancel(t.Context())
|
|
cancel()
|
|
assert.ErrorContains(t, cancelledCtx, v.WaitForChainStart(ctx))
|
|
}
|
|
|
|
func TestWaitForChainStart_ReceiveErrorFromStream(t *testing.T) {
|
|
ctrl := gomock.NewController(t)
|
|
defer ctrl.Finish()
|
|
client := validatormock.NewMockValidatorClient(ctrl)
|
|
|
|
v := validator{
|
|
validatorClient: client,
|
|
}
|
|
client.EXPECT().WaitForChainStart(
|
|
gomock.Any(),
|
|
&emptypb.Empty{},
|
|
).Return(nil, errors.New("fails"))
|
|
err := v.WaitForChainStart(t.Context())
|
|
want := "could not receive ChainStart from stream"
|
|
assert.ErrorContains(t, want, err)
|
|
}
|
|
|
|
func TestWaitSync_ContextCanceled(t *testing.T) {
|
|
ctrl := gomock.NewController(t)
|
|
defer ctrl.Finish()
|
|
n := validatormock.NewMockNodeClient(ctrl)
|
|
|
|
v := validator{
|
|
nodeClient: n,
|
|
}
|
|
|
|
ctx, cancel := context.WithCancel(t.Context())
|
|
cancel()
|
|
|
|
n.EXPECT().SyncStatus(
|
|
gomock.Any(),
|
|
gomock.Any(),
|
|
).Return(ðpb.SyncStatus{Syncing: true}, nil)
|
|
|
|
assert.ErrorContains(t, cancelledCtx, v.WaitForSync(ctx))
|
|
}
|
|
|
|
func TestWaitSync_NotSyncing(t *testing.T) {
|
|
ctrl := gomock.NewController(t)
|
|
defer ctrl.Finish()
|
|
n := validatormock.NewMockNodeClient(ctrl)
|
|
|
|
v := validator{
|
|
nodeClient: n,
|
|
}
|
|
|
|
n.EXPECT().SyncStatus(
|
|
gomock.Any(),
|
|
gomock.Any(),
|
|
).Return(ðpb.SyncStatus{Syncing: false}, nil)
|
|
|
|
require.NoError(t, v.WaitForSync(t.Context()))
|
|
}
|
|
|
|
func TestWaitSync_Syncing(t *testing.T) {
|
|
ctrl := gomock.NewController(t)
|
|
defer ctrl.Finish()
|
|
n := validatormock.NewMockNodeClient(ctrl)
|
|
|
|
v := validator{
|
|
nodeClient: n,
|
|
}
|
|
|
|
n.EXPECT().SyncStatus(
|
|
gomock.Any(),
|
|
gomock.Any(),
|
|
).Return(ðpb.SyncStatus{Syncing: true}, nil)
|
|
|
|
n.EXPECT().SyncStatus(
|
|
gomock.Any(),
|
|
gomock.Any(),
|
|
).Return(ðpb.SyncStatus{Syncing: false}, nil)
|
|
|
|
require.NoError(t, v.WaitForSync(t.Context()))
|
|
}
|
|
|
|
func TestUpdateDuties_DoesNothingWhenNotEpochStart_AlreadyExistingAssignments(t *testing.T) {
|
|
ctrl := gomock.NewController(t)
|
|
defer ctrl.Finish()
|
|
client := validatormock.NewMockValidatorClient(ctrl)
|
|
|
|
v := validator{
|
|
km: newMockKeymanager(t, randKeypair(t)),
|
|
validatorClient: client,
|
|
duties: ðpb.ValidatorDutiesContainer{
|
|
CurrentEpochDuties: []*ethpb.ValidatorDuty{
|
|
{
|
|
AttesterSlot: 10,
|
|
CommitteeIndex: 20,
|
|
},
|
|
},
|
|
NextEpochDuties: []*ethpb.ValidatorDuty{
|
|
{
|
|
AttesterSlot: 10,
|
|
CommitteeIndex: 20,
|
|
},
|
|
},
|
|
},
|
|
}
|
|
client.EXPECT().Duties(
|
|
gomock.Any(),
|
|
gomock.Any(),
|
|
).Times(1)
|
|
|
|
assert.NoError(t, v.UpdateDuties(t.Context()), "Could not update assignments")
|
|
}
|
|
|
|
func TestUpdateDuties_ReturnsError(t *testing.T) {
|
|
ctrl := gomock.NewController(t)
|
|
defer ctrl.Finish()
|
|
client := validatormock.NewMockValidatorClient(ctrl)
|
|
|
|
v := validator{
|
|
validatorClient: client,
|
|
km: newMockKeymanager(t, randKeypair(t)),
|
|
duties: ðpb.ValidatorDutiesContainer{
|
|
CurrentEpochDuties: []*ethpb.ValidatorDuty{
|
|
{
|
|
CommitteeIndex: 1,
|
|
},
|
|
},
|
|
},
|
|
}
|
|
|
|
expected := errors.New("bad")
|
|
|
|
client.EXPECT().Duties(
|
|
gomock.Any(),
|
|
gomock.Any(),
|
|
).Return(nil, expected)
|
|
|
|
assert.ErrorContains(t, expected.Error(), v.UpdateDuties(t.Context()))
|
|
assert.Equal(t, (*ethpb.ValidatorDutiesContainer)(nil), v.duties, "Assignments should have been cleared on failure")
|
|
}
|
|
|
|
func TestUpdateDuties_OK(t *testing.T) {
|
|
ctrl := gomock.NewController(t)
|
|
defer ctrl.Finish()
|
|
client := validatormock.NewMockValidatorClient(ctrl)
|
|
|
|
resp := ðpb.ValidatorDutiesContainer{
|
|
CurrentEpochDuties: []*ethpb.ValidatorDuty{
|
|
{
|
|
AttesterSlot: params.BeaconConfig().SlotsPerEpoch,
|
|
ValidatorIndex: 200,
|
|
CommitteeIndex: 100,
|
|
CommitteeLength: 4,
|
|
PublicKey: []byte("testPubKey_1"),
|
|
ProposerSlots: []primitives.Slot{params.BeaconConfig().SlotsPerEpoch + 1},
|
|
},
|
|
},
|
|
}
|
|
v := validator{
|
|
km: newMockKeymanager(t, randKeypair(t)),
|
|
validatorClient: client,
|
|
}
|
|
client.EXPECT().Duties(
|
|
gomock.Any(),
|
|
gomock.Any(),
|
|
).Return(resp, nil)
|
|
|
|
var wg sync.WaitGroup
|
|
wg.Add(1)
|
|
|
|
client.EXPECT().SubscribeCommitteeSubnets(
|
|
gomock.Any(),
|
|
gomock.Any(),
|
|
gomock.Any(),
|
|
).DoAndReturn(func(_ context.Context, _ *ethpb.CommitteeSubnetsSubscribeRequest, _ []*ethpb.ValidatorDuty) (*emptypb.Empty, error) {
|
|
wg.Done()
|
|
return nil, nil
|
|
})
|
|
|
|
require.NoError(t, v.UpdateDuties(t.Context()), "Could not update assignments")
|
|
|
|
util.WaitTimeout(&wg, 2*time.Second)
|
|
|
|
assert.Equal(t, params.BeaconConfig().SlotsPerEpoch+1, v.duties.CurrentEpochDuties[0].ProposerSlots[0], "Unexpected validator assignments")
|
|
assert.Equal(t, params.BeaconConfig().SlotsPerEpoch, v.duties.CurrentEpochDuties[0].AttesterSlot, "Unexpected validator assignments")
|
|
assert.Equal(t, resp.CurrentEpochDuties[0].CommitteeIndex, v.duties.CurrentEpochDuties[0].CommitteeIndex, "Unexpected validator assignments")
|
|
assert.Equal(t, resp.CurrentEpochDuties[0].ValidatorIndex, v.duties.CurrentEpochDuties[0].ValidatorIndex, "Unexpected validator assignments")
|
|
}
|
|
|
|
func TestUpdateDuties_OK_FilterBlacklistedPublicKeys(t *testing.T) {
|
|
hook := logTest.NewGlobal()
|
|
ctrl := gomock.NewController(t)
|
|
defer ctrl.Finish()
|
|
client := validatormock.NewMockValidatorClient(ctrl)
|
|
|
|
numValidators := 10
|
|
km := genMockKeymanager(t, numValidators)
|
|
blacklistedPublicKeys := make(map[[fieldparams.BLSPubkeyLength]byte]bool)
|
|
for _, k := range km.keys {
|
|
blacklistedPublicKeys[k] = true
|
|
}
|
|
v := validator{
|
|
km: km,
|
|
validatorClient: client,
|
|
blacklistedPubkeys: blacklistedPublicKeys,
|
|
}
|
|
|
|
resp := ðpb.ValidatorDutiesContainer{
|
|
CurrentEpochDuties: []*ethpb.ValidatorDuty{},
|
|
}
|
|
client.EXPECT().Duties(
|
|
gomock.Any(),
|
|
gomock.Any(),
|
|
).Return(resp, nil)
|
|
|
|
var wg sync.WaitGroup
|
|
wg.Add(1)
|
|
client.EXPECT().SubscribeCommitteeSubnets(
|
|
gomock.Any(),
|
|
gomock.Any(),
|
|
gomock.Any(),
|
|
).DoAndReturn(func(_ context.Context, _ *ethpb.CommitteeSubnetsSubscribeRequest, _ []*ethpb.ValidatorDuty) (*emptypb.Empty, error) {
|
|
wg.Done()
|
|
return nil, nil
|
|
})
|
|
|
|
require.NoError(t, v.UpdateDuties(t.Context()), "Could not update assignments")
|
|
|
|
util.WaitTimeout(&wg, 2*time.Second)
|
|
|
|
for range blacklistedPublicKeys {
|
|
assert.LogsContain(t, hook, "Not including slashable public key")
|
|
}
|
|
}
|
|
|
|
func TestUpdateDuties_AllValidatorsExited(t *testing.T) {
|
|
ctrl := gomock.NewController(t)
|
|
defer ctrl.Finish()
|
|
client := validatormock.NewMockValidatorClient(ctrl)
|
|
|
|
resp := ðpb.ValidatorDutiesContainer{
|
|
CurrentEpochDuties: []*ethpb.ValidatorDuty{
|
|
{
|
|
AttesterSlot: params.BeaconConfig().SlotsPerEpoch,
|
|
ValidatorIndex: 200,
|
|
CommitteeIndex: 100,
|
|
CommitteeLength: 4,
|
|
PublicKey: []byte("testPubKey_1"),
|
|
ProposerSlots: []primitives.Slot{params.BeaconConfig().SlotsPerEpoch + 1},
|
|
Status: ethpb.ValidatorStatus_EXITED,
|
|
},
|
|
{
|
|
AttesterSlot: params.BeaconConfig().SlotsPerEpoch,
|
|
ValidatorIndex: 201,
|
|
CommitteeIndex: 101,
|
|
CommitteeLength: 4,
|
|
PublicKey: []byte("testPubKey_2"),
|
|
ProposerSlots: []primitives.Slot{params.BeaconConfig().SlotsPerEpoch + 1},
|
|
Status: ethpb.ValidatorStatus_EXITED,
|
|
},
|
|
},
|
|
}
|
|
v := validator{
|
|
km: newMockKeymanager(t, randKeypair(t)),
|
|
validatorClient: client,
|
|
}
|
|
client.EXPECT().Duties(
|
|
gomock.Any(),
|
|
gomock.Any(),
|
|
).Return(resp, nil)
|
|
|
|
err := v.UpdateDuties(t.Context())
|
|
require.ErrorContains(t, ErrValidatorsAllExited.Error(), err)
|
|
|
|
}
|
|
|
|
func TestUpdateDuties_Distributed(t *testing.T) {
|
|
ctrl := gomock.NewController(t)
|
|
defer ctrl.Finish()
|
|
client := validatormock.NewMockValidatorClient(ctrl)
|
|
|
|
// Start of third epoch.
|
|
slot := 2 * params.BeaconConfig().SlotsPerEpoch
|
|
keys := randKeypair(t)
|
|
resp := ðpb.ValidatorDutiesContainer{
|
|
CurrentEpochDuties: []*ethpb.ValidatorDuty{
|
|
{
|
|
AttesterSlot: slot, // First slot in epoch.
|
|
ValidatorIndex: 200,
|
|
CommitteeIndex: 100,
|
|
PublicKey: keys.pub[:],
|
|
Status: ethpb.ValidatorStatus_ACTIVE,
|
|
},
|
|
},
|
|
NextEpochDuties: []*ethpb.ValidatorDuty{
|
|
{
|
|
AttesterSlot: slot + params.BeaconConfig().SlotsPerEpoch, // First slot in next epoch.
|
|
ValidatorIndex: 200,
|
|
CommitteeIndex: 100,
|
|
PublicKey: keys.pub[:],
|
|
Status: ethpb.ValidatorStatus_ACTIVE,
|
|
},
|
|
},
|
|
}
|
|
|
|
v := validator{
|
|
km: newMockKeymanager(t, keys),
|
|
validatorClient: client,
|
|
distributed: true,
|
|
}
|
|
|
|
sigDomain := make([]byte, 32)
|
|
|
|
client.EXPECT().Duties(
|
|
gomock.Any(),
|
|
gomock.Any(),
|
|
).Return(resp, nil)
|
|
|
|
client.EXPECT().DomainData(
|
|
gomock.Any(), // ctx
|
|
gomock.Any(), // epoch
|
|
).Return(
|
|
ðpb.DomainResponse{SignatureDomain: sigDomain},
|
|
nil, /*err*/
|
|
)
|
|
|
|
client.EXPECT().AggregatedSelections(
|
|
gomock.Any(),
|
|
gomock.Any(), // fill this properly
|
|
).Return(
|
|
[]iface.BeaconCommitteeSelection{
|
|
{
|
|
SelectionProof: make([]byte, 32),
|
|
Slot: slot,
|
|
ValidatorIndex: 200,
|
|
},
|
|
{
|
|
SelectionProof: make([]byte, 32),
|
|
Slot: slot + params.BeaconConfig().SlotsPerEpoch,
|
|
ValidatorIndex: 200,
|
|
},
|
|
},
|
|
nil,
|
|
)
|
|
|
|
var wg sync.WaitGroup
|
|
wg.Add(1)
|
|
|
|
client.EXPECT().SubscribeCommitteeSubnets(
|
|
gomock.Any(),
|
|
gomock.Any(),
|
|
gomock.Any(),
|
|
).DoAndReturn(func(_ context.Context, _ *ethpb.CommitteeSubnetsSubscribeRequest, _ []*ethpb.ValidatorDuty) (*emptypb.Empty, error) {
|
|
wg.Done()
|
|
return nil, nil
|
|
})
|
|
|
|
require.NoError(t, v.UpdateDuties(t.Context()), "Could not update assignments")
|
|
util.WaitTimeout(&wg, 2*time.Second)
|
|
require.Equal(t, 2, len(v.attSelections))
|
|
}
|
|
|
|
func TestRolesAt_OK(t *testing.T) {
|
|
for _, isSlashingProtectionMinimal := range [...]bool{false, true} {
|
|
t.Run(fmt.Sprintf("SlashingProtectionMinimal:%v", isSlashingProtectionMinimal), func(t *testing.T) {
|
|
v, m, validatorKey, finish := setup(t, isSlashingProtectionMinimal)
|
|
defer finish()
|
|
|
|
v.duties = ðpb.ValidatorDutiesContainer{
|
|
CurrentEpochDuties: []*ethpb.ValidatorDuty{
|
|
{
|
|
CommitteeIndex: 1,
|
|
AttesterSlot: 1,
|
|
PublicKey: validatorKey.PublicKey().Marshal(),
|
|
IsSyncCommittee: true,
|
|
},
|
|
},
|
|
NextEpochDuties: []*ethpb.ValidatorDuty{
|
|
{
|
|
CommitteeIndex: 1,
|
|
AttesterSlot: 1,
|
|
PublicKey: validatorKey.PublicKey().Marshal(),
|
|
IsSyncCommittee: true,
|
|
},
|
|
},
|
|
}
|
|
|
|
m.validatorClient.EXPECT().DomainData(
|
|
gomock.Any(), // ctx
|
|
gomock.Any(), // epoch
|
|
).Return(ðpb.DomainResponse{SignatureDomain: make([]byte, 32)}, nil /*err*/)
|
|
|
|
m.validatorClient.EXPECT().SyncSubcommitteeIndex(
|
|
gomock.Any(), // ctx
|
|
ðpb.SyncSubcommitteeIndexRequest{
|
|
PublicKey: validatorKey.PublicKey().Marshal(),
|
|
Slot: 1,
|
|
},
|
|
).Return(ðpb.SyncSubcommitteeIndexResponse{}, nil /*err*/)
|
|
|
|
roleMap, err := v.RolesAt(t.Context(), 1)
|
|
require.NoError(t, err)
|
|
|
|
assert.Equal(t, iface.RoleAttester, roleMap[bytesutil.ToBytes48(validatorKey.PublicKey().Marshal())][0])
|
|
assert.Equal(t, iface.RoleAggregator, roleMap[bytesutil.ToBytes48(validatorKey.PublicKey().Marshal())][1])
|
|
assert.Equal(t, iface.RoleSyncCommittee, roleMap[bytesutil.ToBytes48(validatorKey.PublicKey().Marshal())][2])
|
|
|
|
// Test sync committee role at epoch boundary.
|
|
v.duties = ðpb.ValidatorDutiesContainer{
|
|
CurrentEpochDuties: []*ethpb.ValidatorDuty{
|
|
{
|
|
CommitteeIndex: 1,
|
|
AttesterSlot: 1,
|
|
PublicKey: validatorKey.PublicKey().Marshal(),
|
|
IsSyncCommittee: false,
|
|
},
|
|
},
|
|
NextEpochDuties: []*ethpb.ValidatorDuty{
|
|
{
|
|
CommitteeIndex: 1,
|
|
AttesterSlot: 1,
|
|
PublicKey: validatorKey.PublicKey().Marshal(),
|
|
IsSyncCommittee: true,
|
|
},
|
|
},
|
|
}
|
|
|
|
m.validatorClient.EXPECT().SyncSubcommitteeIndex(
|
|
gomock.Any(), // ctx
|
|
ðpb.SyncSubcommitteeIndexRequest{
|
|
PublicKey: validatorKey.PublicKey().Marshal(),
|
|
Slot: 31,
|
|
},
|
|
).Return(ðpb.SyncSubcommitteeIndexResponse{}, nil /*err*/)
|
|
|
|
roleMap, err = v.RolesAt(t.Context(), params.BeaconConfig().SlotsPerEpoch-1)
|
|
require.NoError(t, err)
|
|
assert.Equal(t, iface.RoleSyncCommittee, roleMap[bytesutil.ToBytes48(validatorKey.PublicKey().Marshal())][0])
|
|
})
|
|
}
|
|
}
|
|
|
|
func TestRolesAt_DoesNotAssignProposer_Slot0(t *testing.T) {
|
|
for _, isSlashingProtectionMinimal := range [...]bool{false, true} {
|
|
t.Run(fmt.Sprintf("SlashingProtectionMinimal:%v", isSlashingProtectionMinimal), func(t *testing.T) {
|
|
v, m, validatorKey, finish := setup(t, isSlashingProtectionMinimal)
|
|
defer finish()
|
|
|
|
v.duties = ðpb.ValidatorDutiesContainer{
|
|
CurrentEpochDuties: []*ethpb.ValidatorDuty{
|
|
{
|
|
CommitteeIndex: 1,
|
|
AttesterSlot: 0,
|
|
ProposerSlots: []primitives.Slot{0},
|
|
PublicKey: validatorKey.PublicKey().Marshal(),
|
|
},
|
|
},
|
|
}
|
|
|
|
m.validatorClient.EXPECT().DomainData(
|
|
gomock.Any(), // ctx
|
|
gomock.Any(), // epoch
|
|
).Return(ðpb.DomainResponse{SignatureDomain: make([]byte, 32)}, nil /*err*/)
|
|
|
|
roleMap, err := v.RolesAt(t.Context(), 0)
|
|
require.NoError(t, err)
|
|
|
|
assert.Equal(t, iface.RoleAttester, roleMap[bytesutil.ToBytes48(validatorKey.PublicKey().Marshal())][0])
|
|
})
|
|
}
|
|
}
|
|
|
|
func TestCheckAndLogValidatorStatus_OK(t *testing.T) {
|
|
nonexistentIndex := primitives.ValidatorIndex(^uint64(0))
|
|
type statusTest struct {
|
|
name string
|
|
status *validatorStatus
|
|
log string
|
|
active bool
|
|
}
|
|
pubKeys := [][]byte{bytesutil.Uint64ToBytesLittleEndian(0)}
|
|
tests := []statusTest{
|
|
{
|
|
name: "UNKNOWN_STATUS, no deposit found yet",
|
|
status: &validatorStatus{
|
|
publicKey: pubKeys[0],
|
|
index: nonexistentIndex,
|
|
status: ðpb.ValidatorStatusResponse{
|
|
Status: ethpb.ValidatorStatus_UNKNOWN_STATUS,
|
|
},
|
|
},
|
|
log: "Waiting for deposit to be observed by beacon node",
|
|
active: false,
|
|
},
|
|
{
|
|
name: "DEPOSITED into state",
|
|
status: &validatorStatus{
|
|
publicKey: pubKeys[0],
|
|
index: 30,
|
|
status: ðpb.ValidatorStatusResponse{
|
|
Status: ethpb.ValidatorStatus_DEPOSITED,
|
|
PositionInActivationQueue: 30,
|
|
},
|
|
},
|
|
log: "Validator deposited, entering activation queue after finalization\" package=validator/client pubkey=0x000000000000 status=DEPOSITED validatorIndex=30",
|
|
active: false,
|
|
},
|
|
{
|
|
name: "PENDING",
|
|
status: &validatorStatus{
|
|
publicKey: pubKeys[0],
|
|
index: 50,
|
|
status: ðpb.ValidatorStatusResponse{
|
|
Status: ethpb.ValidatorStatus_PENDING,
|
|
ActivationEpoch: params.BeaconConfig().FarFutureEpoch,
|
|
PositionInActivationQueue: 6,
|
|
},
|
|
},
|
|
log: "Waiting for activation... Check validator queue status in a block explorer\" package=validator/client pubkey=0x000000000000 status=PENDING validatorIndex=50",
|
|
active: false,
|
|
},
|
|
{
|
|
name: "ACTIVE",
|
|
status: &validatorStatus{
|
|
publicKey: pubKeys[0],
|
|
index: 89,
|
|
status: ðpb.ValidatorStatusResponse{
|
|
Status: ethpb.ValidatorStatus_ACTIVE,
|
|
},
|
|
},
|
|
active: true,
|
|
},
|
|
{
|
|
name: "EXITING",
|
|
status: &validatorStatus{
|
|
publicKey: pubKeys[0],
|
|
index: 89,
|
|
status: ðpb.ValidatorStatusResponse{
|
|
Status: ethpb.ValidatorStatus_EXITING,
|
|
},
|
|
},
|
|
active: true,
|
|
},
|
|
{
|
|
name: "EXITED",
|
|
status: &validatorStatus{
|
|
publicKey: pubKeys[0],
|
|
status: ðpb.ValidatorStatusResponse{
|
|
Status: ethpb.ValidatorStatus_EXITED,
|
|
},
|
|
},
|
|
log: "Validator exited",
|
|
active: false,
|
|
},
|
|
}
|
|
for _, test := range tests {
|
|
t.Run(test.name, func(t *testing.T) {
|
|
hook := logTest.NewGlobal()
|
|
ctrl := gomock.NewController(t)
|
|
defer ctrl.Finish()
|
|
client := validatormock.NewMockValidatorClient(ctrl)
|
|
v := validator{
|
|
validatorClient: client,
|
|
duties: ðpb.ValidatorDutiesContainer{
|
|
CurrentEpochDuties: []*ethpb.ValidatorDuty{
|
|
{
|
|
CommitteeIndex: 1,
|
|
},
|
|
},
|
|
},
|
|
pubkeyToStatus: make(map[[48]byte]*validatorStatus),
|
|
}
|
|
v.pubkeyToStatus[bytesutil.ToBytes48(test.status.publicKey)] = test.status
|
|
active := v.checkAndLogValidatorStatus()
|
|
require.Equal(t, test.active, active)
|
|
if test.log != "" {
|
|
require.LogsContain(t, hook, test.log)
|
|
}
|
|
})
|
|
}
|
|
}
|
|
|
|
type doppelGangerRequestMatcher struct {
|
|
req *ethpb.DoppelGangerRequest
|
|
}
|
|
|
|
var _ gomock.Matcher = (*doppelGangerRequestMatcher)(nil)
|
|
|
|
func (m *doppelGangerRequestMatcher) Matches(x any) bool {
|
|
r, ok := x.(*ethpb.DoppelGangerRequest)
|
|
if !ok {
|
|
panic("Invalid match type")
|
|
}
|
|
return gomock.InAnyOrder(m.req.ValidatorRequests).Matches(r.ValidatorRequests)
|
|
}
|
|
|
|
func (m *doppelGangerRequestMatcher) String() string {
|
|
return fmt.Sprintf("%#v", m.req.ValidatorRequests)
|
|
}
|
|
|
|
func TestValidator_CheckDoppelGanger(t *testing.T) {
|
|
for _, isSlashingProtectionMinimal := range [...]bool{false, true} {
|
|
ctrl := gomock.NewController(t)
|
|
defer ctrl.Finish()
|
|
flgs := features.Get()
|
|
flgs.EnableDoppelGanger = true
|
|
reset := features.InitWithReset(flgs)
|
|
defer reset()
|
|
tests := []struct {
|
|
name string
|
|
validatorSetter func(t *testing.T) *validator
|
|
err string
|
|
}{
|
|
{
|
|
name: "no doppelganger",
|
|
validatorSetter: func(t *testing.T) *validator {
|
|
client := validatormock.NewMockValidatorClient(ctrl)
|
|
km := genMockKeymanager(t, 10)
|
|
keys, err := km.FetchValidatingPublicKeys(t.Context())
|
|
assert.NoError(t, err)
|
|
db := dbTest.SetupDB(t, t.TempDir(), keys, isSlashingProtectionMinimal)
|
|
req := ðpb.DoppelGangerRequest{ValidatorRequests: []*ethpb.DoppelGangerRequest_ValidatorRequest{}}
|
|
for _, k := range keys {
|
|
pkey := k
|
|
att := createAttestation(10, 12)
|
|
rt, err := att.Data.HashTreeRoot()
|
|
assert.NoError(t, err)
|
|
assert.NoError(t, db.SaveAttestationForPubKey(t.Context(), pkey, rt, att))
|
|
signedRoot := rt[:]
|
|
if isSlashingProtectionMinimal {
|
|
signedRoot = nil
|
|
}
|
|
req.ValidatorRequests = append(req.ValidatorRequests, ðpb.DoppelGangerRequest_ValidatorRequest{PublicKey: pkey[:], Epoch: att.Data.Target.Epoch, SignedRoot: signedRoot})
|
|
}
|
|
v := &validator{
|
|
validatorClient: client,
|
|
km: km,
|
|
db: db,
|
|
}
|
|
client.EXPECT().CheckDoppelGanger(
|
|
gomock.Any(), // ctx
|
|
&doppelGangerRequestMatcher{req}, // request
|
|
).Return(nil, nil /*err*/)
|
|
|
|
return v
|
|
},
|
|
},
|
|
{
|
|
name: "multiple doppelganger exists",
|
|
validatorSetter: func(t *testing.T) *validator {
|
|
client := validatormock.NewMockValidatorClient(ctrl)
|
|
km := genMockKeymanager(t, 10)
|
|
keys, err := km.FetchValidatingPublicKeys(t.Context())
|
|
assert.NoError(t, err)
|
|
db := dbTest.SetupDB(t, t.TempDir(), keys, isSlashingProtectionMinimal)
|
|
req := ðpb.DoppelGangerRequest{ValidatorRequests: []*ethpb.DoppelGangerRequest_ValidatorRequest{}}
|
|
resp := ðpb.DoppelGangerResponse{Responses: []*ethpb.DoppelGangerResponse_ValidatorResponse{}}
|
|
for i, k := range keys {
|
|
pkey := k
|
|
att := createAttestation(10, 12)
|
|
rt, err := att.Data.HashTreeRoot()
|
|
assert.NoError(t, err)
|
|
assert.NoError(t, db.SaveAttestationForPubKey(t.Context(), pkey, rt, att))
|
|
if i%3 == 0 {
|
|
resp.Responses = append(resp.Responses, ðpb.DoppelGangerResponse_ValidatorResponse{PublicKey: pkey[:], DuplicateExists: true})
|
|
}
|
|
|
|
signedRoot := rt[:]
|
|
if isSlashingProtectionMinimal {
|
|
signedRoot = nil
|
|
}
|
|
|
|
req.ValidatorRequests = append(req.ValidatorRequests, ðpb.DoppelGangerRequest_ValidatorRequest{PublicKey: pkey[:], Epoch: att.Data.Target.Epoch, SignedRoot: signedRoot})
|
|
|
|
}
|
|
v := &validator{
|
|
validatorClient: client,
|
|
km: km,
|
|
db: db,
|
|
}
|
|
client.EXPECT().CheckDoppelGanger(
|
|
gomock.Any(), // ctx
|
|
&doppelGangerRequestMatcher{req}, // request
|
|
).Return(resp, nil /*err*/)
|
|
return v
|
|
},
|
|
err: "Duplicate instances exists in the network for validator keys",
|
|
},
|
|
{
|
|
name: "single doppelganger exists",
|
|
validatorSetter: func(t *testing.T) *validator {
|
|
client := validatormock.NewMockValidatorClient(ctrl)
|
|
km := genMockKeymanager(t, 10)
|
|
keys, err := km.FetchValidatingPublicKeys(t.Context())
|
|
assert.NoError(t, err)
|
|
db := dbTest.SetupDB(t, t.TempDir(), keys, isSlashingProtectionMinimal)
|
|
req := ðpb.DoppelGangerRequest{ValidatorRequests: []*ethpb.DoppelGangerRequest_ValidatorRequest{}}
|
|
resp := ðpb.DoppelGangerResponse{Responses: []*ethpb.DoppelGangerResponse_ValidatorResponse{}}
|
|
for i, k := range keys {
|
|
pkey := k
|
|
att := createAttestation(10, 12)
|
|
rt, err := att.Data.HashTreeRoot()
|
|
assert.NoError(t, err)
|
|
assert.NoError(t, db.SaveAttestationForPubKey(t.Context(), pkey, rt, att))
|
|
if i%9 == 0 {
|
|
resp.Responses = append(resp.Responses, ðpb.DoppelGangerResponse_ValidatorResponse{PublicKey: pkey[:], DuplicateExists: true})
|
|
}
|
|
signedRoot := rt[:]
|
|
if isSlashingProtectionMinimal {
|
|
signedRoot = nil
|
|
}
|
|
|
|
req.ValidatorRequests = append(req.ValidatorRequests, ðpb.DoppelGangerRequest_ValidatorRequest{PublicKey: pkey[:], Epoch: att.Data.Target.Epoch, SignedRoot: signedRoot})
|
|
}
|
|
v := &validator{
|
|
validatorClient: client,
|
|
km: km,
|
|
db: db,
|
|
}
|
|
client.EXPECT().CheckDoppelGanger(
|
|
gomock.Any(), // ctx
|
|
&doppelGangerRequestMatcher{req}, // request
|
|
).Return(resp, nil /*err*/)
|
|
return v
|
|
},
|
|
err: "Duplicate instances exists in the network for validator keys",
|
|
},
|
|
{
|
|
name: "multiple attestations saved",
|
|
validatorSetter: func(t *testing.T) *validator {
|
|
client := validatormock.NewMockValidatorClient(ctrl)
|
|
km := genMockKeymanager(t, 10)
|
|
keys, err := km.FetchValidatingPublicKeys(t.Context())
|
|
assert.NoError(t, err)
|
|
db := dbTest.SetupDB(t, t.TempDir(), keys, isSlashingProtectionMinimal)
|
|
req := ðpb.DoppelGangerRequest{ValidatorRequests: []*ethpb.DoppelGangerRequest_ValidatorRequest{}}
|
|
resp := ðpb.DoppelGangerResponse{Responses: []*ethpb.DoppelGangerResponse_ValidatorResponse{}}
|
|
attLimit := 5
|
|
for i, k := range keys {
|
|
pkey := k
|
|
for j := range attLimit {
|
|
att := createAttestation(10+primitives.Epoch(j), 12+primitives.Epoch(j))
|
|
rt, err := att.Data.HashTreeRoot()
|
|
assert.NoError(t, err)
|
|
assert.NoError(t, db.SaveAttestationForPubKey(t.Context(), pkey, rt, att))
|
|
|
|
signedRoot := rt[:]
|
|
if isSlashingProtectionMinimal {
|
|
signedRoot = nil
|
|
}
|
|
|
|
if j == attLimit-1 {
|
|
req.ValidatorRequests = append(req.ValidatorRequests, ðpb.DoppelGangerRequest_ValidatorRequest{PublicKey: pkey[:], Epoch: att.Data.Target.Epoch, SignedRoot: signedRoot})
|
|
}
|
|
}
|
|
if i%3 == 0 {
|
|
resp.Responses = append(resp.Responses, ðpb.DoppelGangerResponse_ValidatorResponse{PublicKey: pkey[:], DuplicateExists: true})
|
|
}
|
|
}
|
|
v := &validator{
|
|
validatorClient: client,
|
|
km: km,
|
|
db: db,
|
|
}
|
|
client.EXPECT().CheckDoppelGanger(
|
|
gomock.Any(), // ctx
|
|
&doppelGangerRequestMatcher{req}, // request
|
|
).Return(resp, nil /*err*/)
|
|
return v
|
|
},
|
|
err: "Duplicate instances exists in the network for validator keys",
|
|
},
|
|
{
|
|
name: "no history exists",
|
|
validatorSetter: func(t *testing.T) *validator {
|
|
client := validatormock.NewMockValidatorClient(ctrl)
|
|
// Use only 1 key for deterministic order.
|
|
km := genMockKeymanager(t, 1)
|
|
keys, err := km.FetchValidatingPublicKeys(t.Context())
|
|
assert.NoError(t, err)
|
|
db := dbTest.SetupDB(t, t.TempDir(), keys, isSlashingProtectionMinimal)
|
|
resp := ðpb.DoppelGangerResponse{Responses: []*ethpb.DoppelGangerResponse_ValidatorResponse{}}
|
|
req := ðpb.DoppelGangerRequest{ValidatorRequests: []*ethpb.DoppelGangerRequest_ValidatorRequest{}}
|
|
for _, k := range keys {
|
|
resp.Responses = append(resp.Responses, ðpb.DoppelGangerResponse_ValidatorResponse{PublicKey: k[:], DuplicateExists: false})
|
|
req.ValidatorRequests = append(req.ValidatorRequests, ðpb.DoppelGangerRequest_ValidatorRequest{PublicKey: k[:], SignedRoot: make([]byte, 32), Epoch: 0})
|
|
}
|
|
v := &validator{
|
|
validatorClient: client,
|
|
km: km,
|
|
db: db,
|
|
}
|
|
client.EXPECT().CheckDoppelGanger(
|
|
gomock.Any(), // ctx
|
|
req, // request
|
|
).Return(resp, nil /*err*/)
|
|
return v
|
|
},
|
|
err: "",
|
|
},
|
|
}
|
|
for _, tt := range tests {
|
|
t.Run(fmt.Sprintf("%s/isSlashingProtectionMinimal:%v", tt.name, isSlashingProtectionMinimal), func(t *testing.T) {
|
|
v := tt.validatorSetter(t)
|
|
if err := v.CheckDoppelGanger(t.Context()); tt.err != "" {
|
|
assert.ErrorContains(t, tt.err, err)
|
|
}
|
|
})
|
|
}
|
|
}
|
|
}
|
|
|
|
func TestValidatorAttestationsAreOrdered(t *testing.T) {
|
|
for _, isSlashingProtectionMinimal := range [...]bool{false, true} {
|
|
t.Run(fmt.Sprintf("SlashingProtectionMinimal:%v", isSlashingProtectionMinimal), func(t *testing.T) {
|
|
km := genMockKeymanager(t, 10)
|
|
keys, err := km.FetchValidatingPublicKeys(t.Context())
|
|
assert.NoError(t, err)
|
|
db := dbTest.SetupDB(t, t.TempDir(), keys, isSlashingProtectionMinimal)
|
|
|
|
k := keys[0]
|
|
att := createAttestation(10, 14)
|
|
rt, err := att.Data.HashTreeRoot()
|
|
assert.NoError(t, err)
|
|
assert.NoError(t, db.SaveAttestationForPubKey(t.Context(), k, rt, att))
|
|
|
|
att = createAttestation(6, 8)
|
|
rt, err = att.Data.HashTreeRoot()
|
|
assert.NoError(t, err)
|
|
|
|
err = db.SaveAttestationForPubKey(t.Context(), k, rt, att)
|
|
if isSlashingProtectionMinimal {
|
|
assert.ErrorContains(t, "could not sign attestation with source lower than recorded source epoch", err)
|
|
} else {
|
|
assert.NoError(t, err)
|
|
}
|
|
|
|
att = createAttestation(10, 12)
|
|
rt, err = att.Data.HashTreeRoot()
|
|
assert.NoError(t, err)
|
|
|
|
err = db.SaveAttestationForPubKey(t.Context(), k, rt, att)
|
|
if isSlashingProtectionMinimal {
|
|
assert.ErrorContains(t, "could not sign attestation with target lower than or equal to recorded target epoch", err)
|
|
} else {
|
|
assert.NoError(t, err)
|
|
}
|
|
|
|
att = createAttestation(2, 3)
|
|
rt, err = att.Data.HashTreeRoot()
|
|
assert.NoError(t, err)
|
|
|
|
err = db.SaveAttestationForPubKey(t.Context(), k, rt, att)
|
|
if isSlashingProtectionMinimal {
|
|
assert.ErrorContains(t, "could not sign attestation with source lower than recorded source epoch", err)
|
|
} else {
|
|
assert.NoError(t, err)
|
|
}
|
|
})
|
|
}
|
|
}
|
|
|
|
func createAttestation(source, target primitives.Epoch) *ethpb.IndexedAttestation {
|
|
return ðpb.IndexedAttestation{
|
|
Data: ðpb.AttestationData{
|
|
Source: ðpb.Checkpoint{
|
|
Epoch: source,
|
|
Root: make([]byte, 32),
|
|
},
|
|
Target: ðpb.Checkpoint{
|
|
Epoch: target,
|
|
Root: make([]byte, 32),
|
|
},
|
|
BeaconBlockRoot: make([]byte, 32),
|
|
},
|
|
Signature: make([]byte, fieldparams.BLSSignatureLength),
|
|
}
|
|
}
|
|
|
|
func TestIsSyncCommitteeAggregator_OK(t *testing.T) {
|
|
for _, isSlashingProtectionMinimal := range [...]bool{false, true} {
|
|
t.Run(fmt.Sprintf("SlashingProtectionMinimal:%v", isSlashingProtectionMinimal), func(t *testing.T) {
|
|
params.SetupTestConfigCleanup(t)
|
|
v, m, validatorKey, finish := setup(t, isSlashingProtectionMinimal)
|
|
defer finish()
|
|
|
|
slot := primitives.Slot(1)
|
|
pubKey := validatorKey.PublicKey().Marshal()
|
|
|
|
m.validatorClient.EXPECT().SyncSubcommitteeIndex(
|
|
gomock.Any(), // ctx
|
|
ðpb.SyncSubcommitteeIndexRequest{
|
|
PublicKey: validatorKey.PublicKey().Marshal(),
|
|
Slot: 1,
|
|
},
|
|
).Return(ðpb.SyncSubcommitteeIndexResponse{}, nil /*err*/)
|
|
|
|
aggregator, err := v.isSyncCommitteeAggregator(t.Context(), slot, map[primitives.ValidatorIndex][fieldparams.BLSPubkeyLength]byte{
|
|
0: bytesutil.ToBytes48(pubKey),
|
|
})
|
|
require.NoError(t, err)
|
|
require.Equal(t, false, aggregator[0])
|
|
|
|
c := params.BeaconConfig().Copy()
|
|
c.TargetAggregatorsPerSyncSubcommittee = math.MaxUint64
|
|
params.OverrideBeaconConfig(c)
|
|
|
|
m.validatorClient.EXPECT().DomainData(
|
|
gomock.Any(), // ctx
|
|
gomock.Any(), // epoch
|
|
).Return(ðpb.DomainResponse{SignatureDomain: make([]byte, 32)}, nil /*err*/)
|
|
|
|
m.validatorClient.EXPECT().SyncSubcommitteeIndex(
|
|
gomock.Any(), // ctx
|
|
ðpb.SyncSubcommitteeIndexRequest{
|
|
PublicKey: validatorKey.PublicKey().Marshal(),
|
|
Slot: 1,
|
|
},
|
|
).Return(ðpb.SyncSubcommitteeIndexResponse{Indices: []primitives.CommitteeIndex{0}}, nil /*err*/)
|
|
|
|
aggregator, err = v.isSyncCommitteeAggregator(t.Context(), slot, map[primitives.ValidatorIndex][fieldparams.BLSPubkeyLength]byte{
|
|
0: bytesutil.ToBytes48(pubKey),
|
|
})
|
|
require.NoError(t, err)
|
|
require.Equal(t, true, aggregator[0])
|
|
})
|
|
}
|
|
}
|
|
|
|
func TestIsSyncCommitteeAggregator_Distributed_OK(t *testing.T) {
|
|
for _, isSlashingProtectionMinimal := range [...]bool{false, true} {
|
|
t.Run(fmt.Sprintf("SlashingProtectionMinimal:%v", isSlashingProtectionMinimal), func(t *testing.T) {
|
|
params.SetupTestConfigCleanup(t)
|
|
v, m, validatorKey, finish := setup(t, isSlashingProtectionMinimal)
|
|
defer finish()
|
|
|
|
v.distributed = true
|
|
slot := primitives.Slot(1)
|
|
pubKey := validatorKey.PublicKey().Marshal()
|
|
|
|
m.validatorClient.EXPECT().SyncSubcommitteeIndex(
|
|
gomock.Any(), // ctx
|
|
ðpb.SyncSubcommitteeIndexRequest{
|
|
PublicKey: validatorKey.PublicKey().Marshal(),
|
|
Slot: 1,
|
|
},
|
|
).Return(ðpb.SyncSubcommitteeIndexResponse{}, nil /*err*/)
|
|
|
|
aggregator, err := v.isSyncCommitteeAggregator(t.Context(), slot, map[primitives.ValidatorIndex][fieldparams.BLSPubkeyLength]byte{
|
|
0: bytesutil.ToBytes48(pubKey),
|
|
})
|
|
require.NoError(t, err)
|
|
require.Equal(t, false, aggregator[0])
|
|
|
|
c := params.BeaconConfig().Copy()
|
|
c.TargetAggregatorsPerSyncSubcommittee = math.MaxUint64
|
|
params.OverrideBeaconConfig(c)
|
|
|
|
m.validatorClient.EXPECT().DomainData(
|
|
gomock.Any(), // ctx
|
|
gomock.Any(), // epoch
|
|
).Return(ðpb.DomainResponse{SignatureDomain: make([]byte, 32)}, nil /*err*/).Times(2)
|
|
|
|
m.validatorClient.EXPECT().SyncSubcommitteeIndex(
|
|
gomock.Any(), // ctx
|
|
ðpb.SyncSubcommitteeIndexRequest{
|
|
PublicKey: validatorKey.PublicKey().Marshal(),
|
|
Slot: 1,
|
|
},
|
|
).Return(ðpb.SyncSubcommitteeIndexResponse{Indices: []primitives.CommitteeIndex{0}}, nil /*err*/)
|
|
|
|
sig, err := v.signSyncSelectionData(t.Context(), bytesutil.ToBytes48(pubKey), 0, slot)
|
|
require.NoError(t, err)
|
|
|
|
selection := iface.SyncCommitteeSelection{
|
|
SelectionProof: sig,
|
|
Slot: 1,
|
|
ValidatorIndex: 123,
|
|
SubcommitteeIndex: 0,
|
|
}
|
|
m.validatorClient.EXPECT().AggregatedSyncSelections(
|
|
gomock.Any(), // ctx
|
|
[]iface.SyncCommitteeSelection{selection},
|
|
).Return([]iface.SyncCommitteeSelection{selection}, nil)
|
|
|
|
aggregator, err = v.isSyncCommitteeAggregator(t.Context(), slot, map[primitives.ValidatorIndex][fieldparams.BLSPubkeyLength]byte{
|
|
123: bytesutil.ToBytes48(pubKey),
|
|
})
|
|
require.NoError(t, err)
|
|
require.Equal(t, true, aggregator[123])
|
|
})
|
|
}
|
|
}
|
|
|
|
func TestValidator_WaitForKeymanagerInitialization_web3Signer(t *testing.T) {
|
|
for _, isSlashingProtectionMinimal := range [...]bool{false, true} {
|
|
t.Run(fmt.Sprintf("SlashingProtectionMinimal:%v", isSlashingProtectionMinimal), func(t *testing.T) {
|
|
ctx := t.Context()
|
|
db := dbTest.SetupDB(t, t.TempDir(), [][fieldparams.BLSPubkeyLength]byte{}, isSlashingProtectionMinimal)
|
|
root := make([]byte, 32)
|
|
copy(root[2:], "a")
|
|
err := db.SaveGenesisValidatorsRoot(ctx, root)
|
|
require.NoError(t, err)
|
|
app := cli.App{}
|
|
set := flag.NewFlagSet("test", 0)
|
|
newDir := filepath.Join(t.TempDir(), "new")
|
|
require.NoError(t, os.MkdirAll(newDir, 0700))
|
|
set.String(flags.WalletDirFlag.Name, newDir, "")
|
|
w := wallet.NewWalletForWeb3Signer(cli.NewContext(&app, set, nil))
|
|
v := validator{
|
|
db: db,
|
|
enableAPI: false,
|
|
wallet: w,
|
|
web3SignerConfig: &remoteweb3signer.SetupConfig{
|
|
BaseEndpoint: "http://localhost:8545",
|
|
ProvidedPublicKeys: []string{"0xa2b5aaad9c6efefe7bb9b1243a043404f3362937cfb6b31833929833173f476630ea2cfeb0d9ddf15f97ca8685948820"},
|
|
},
|
|
}
|
|
err = v.WaitForKeymanagerInitialization(t.Context())
|
|
require.NoError(t, err)
|
|
km, err := v.Keymanager()
|
|
require.NoError(t, err)
|
|
require.NotNil(t, km)
|
|
})
|
|
}
|
|
}
|
|
|
|
func TestValidator_WaitForKeymanagerInitialization_Web(t *testing.T) {
|
|
for _, isSlashingProtectionMinimal := range [...]bool{false, true} {
|
|
t.Run(fmt.Sprintf("SlashingProtectionMinimal:%v", isSlashingProtectionMinimal), func(t *testing.T) {
|
|
ctx := t.Context()
|
|
db := dbTest.SetupDB(t, t.TempDir(), [][fieldparams.BLSPubkeyLength]byte{}, isSlashingProtectionMinimal)
|
|
root := make([]byte, 32)
|
|
copy(root[2:], "a")
|
|
err := db.SaveGenesisValidatorsRoot(ctx, root)
|
|
require.NoError(t, err)
|
|
walletChan := make(chan *wallet.Wallet, 1)
|
|
v := validator{
|
|
db: db,
|
|
enableAPI: true,
|
|
walletInitializedFeed: &event.Feed{},
|
|
walletInitializedChan: walletChan,
|
|
}
|
|
wait := make(chan struct{})
|
|
go func() {
|
|
defer close(wait)
|
|
err = v.WaitForKeymanagerInitialization(ctx)
|
|
require.NoError(t, err)
|
|
km, err := v.Keymanager()
|
|
require.NoError(t, err)
|
|
require.NotNil(t, km)
|
|
}()
|
|
|
|
walletChan <- wallet.New(&wallet.Config{
|
|
KeymanagerKind: keymanager.Local,
|
|
})
|
|
<-wait
|
|
})
|
|
}
|
|
}
|
|
|
|
func TestValidator_WaitForKeymanagerInitialization_Interop(t *testing.T) {
|
|
for _, isSlashingProtectionMinimal := range [...]bool{false, true} {
|
|
t.Run(fmt.Sprintf("SlashingProtectionMinimal:%v", isSlashingProtectionMinimal), func(t *testing.T) {
|
|
ctx := t.Context()
|
|
db := dbTest.SetupDB(t, t.TempDir(), [][fieldparams.BLSPubkeyLength]byte{}, isSlashingProtectionMinimal)
|
|
root := make([]byte, 32)
|
|
copy(root[2:], "a")
|
|
err := db.SaveGenesisValidatorsRoot(ctx, root)
|
|
require.NoError(t, err)
|
|
v := validator{
|
|
db: db,
|
|
enableAPI: false,
|
|
interopKeysConfig: &local.InteropKeymanagerConfig{
|
|
NumValidatorKeys: 2,
|
|
Offset: 1,
|
|
},
|
|
}
|
|
err = v.WaitForKeymanagerInitialization(ctx)
|
|
require.NoError(t, err)
|
|
km, err := v.Keymanager()
|
|
require.NoError(t, err)
|
|
require.NotNil(t, km)
|
|
})
|
|
}
|
|
}
|
|
|
|
type PrepareBeaconProposerRequestMatcher struct {
|
|
expectedRecipients []*ethpb.PrepareBeaconProposerRequest_FeeRecipientContainer
|
|
}
|
|
|
|
func (m *PrepareBeaconProposerRequestMatcher) Matches(x any) bool {
|
|
req, ok := x.(*ethpb.PrepareBeaconProposerRequest)
|
|
if !ok {
|
|
return false
|
|
}
|
|
|
|
if len(req.Recipients) != len(m.expectedRecipients) {
|
|
return false
|
|
}
|
|
|
|
// Build maps for efficient comparison
|
|
expectedMap := make(map[primitives.ValidatorIndex][]byte)
|
|
for _, recipient := range m.expectedRecipients {
|
|
expectedMap[recipient.ValidatorIndex] = recipient.FeeRecipient
|
|
}
|
|
|
|
// Compare the maps
|
|
for _, fc := range req.Recipients {
|
|
expectedFeeRecipient, exists := expectedMap[fc.ValidatorIndex]
|
|
if !exists || !bytes.Equal(expectedFeeRecipient, fc.FeeRecipient) {
|
|
return false
|
|
}
|
|
}
|
|
return true
|
|
}
|
|
|
|
func (m *PrepareBeaconProposerRequestMatcher) String() string {
|
|
return fmt.Sprintf("matches PrepareBeaconProposerRequest with Recipients: %v", m.expectedRecipients)
|
|
}
|
|
|
|
func TestValidator_PushSettings(t *testing.T) {
|
|
for _, isSlashingProtectionMinimal := range [...]bool{false, true} {
|
|
ctrl := gomock.NewController(t)
|
|
ctx := t.Context()
|
|
db := dbTest.SetupDB(t, t.TempDir(), [][fieldparams.BLSPubkeyLength]byte{}, isSlashingProtectionMinimal)
|
|
client := validatormock.NewMockValidatorClient(ctrl)
|
|
nodeClient := validatormock.NewMockNodeClient(ctrl)
|
|
defaultFeeHex := "0x046Fb65722E7b2455043BFEBf6177F1D2e9738D9"
|
|
byteValueAddress, err := hexutil.Decode("0x046Fb65722E7b2455043BFEBf6177F1D2e9738D9")
|
|
require.NoError(t, err)
|
|
|
|
type ExpectedValidatorRegistration struct {
|
|
FeeRecipient []byte
|
|
GasLimit uint64
|
|
Timestamp uint64
|
|
Pubkey []byte
|
|
}
|
|
|
|
tests := []struct {
|
|
name string
|
|
validatorSetter func(t *testing.T) *validator
|
|
feeRecipientMap map[primitives.ValidatorIndex]string
|
|
mockExpectedRequests []ExpectedValidatorRegistration
|
|
err string
|
|
logDelay time.Duration
|
|
logMessages []string
|
|
doesntContainLogs bool
|
|
}{
|
|
{
|
|
name: "Happy Path proposer config not nil",
|
|
validatorSetter: func(t *testing.T) *validator {
|
|
|
|
v := validator{
|
|
validatorClient: client,
|
|
nodeClient: nodeClient,
|
|
db: db,
|
|
pubkeyToStatus: make(map[[fieldparams.BLSPubkeyLength]byte]*validatorStatus),
|
|
signedValidatorRegistrations: make(map[[fieldparams.BLSPubkeyLength]byte]*ethpb.SignedValidatorRegistrationV1),
|
|
enableAPI: false,
|
|
interopKeysConfig: &local.InteropKeymanagerConfig{
|
|
NumValidatorKeys: 2,
|
|
Offset: 1,
|
|
},
|
|
}
|
|
err := v.WaitForKeymanagerInitialization(ctx)
|
|
require.NoError(t, err)
|
|
config := make(map[[fieldparams.BLSPubkeyLength]byte]*proposer.Option)
|
|
km, err := v.Keymanager()
|
|
require.NoError(t, err)
|
|
keys, err := km.FetchValidatingPublicKeys(ctx)
|
|
require.NoError(t, err)
|
|
v.pubkeyToStatus[keys[0]] = &validatorStatus{
|
|
publicKey: keys[0][:],
|
|
status: ðpb.ValidatorStatusResponse{Status: ethpb.ValidatorStatus_ACTIVE},
|
|
index: primitives.ValidatorIndex(1),
|
|
}
|
|
v.pubkeyToStatus[keys[1]] = &validatorStatus{
|
|
publicKey: keys[1][:],
|
|
status: ðpb.ValidatorStatusResponse{Status: ethpb.ValidatorStatus_ACTIVE},
|
|
index: primitives.ValidatorIndex(2),
|
|
}
|
|
client.EXPECT().MultipleValidatorStatus(
|
|
gomock.Any(),
|
|
gomock.Any()).Return(
|
|
ðpb.MultipleValidatorStatusResponse{
|
|
Statuses: []*ethpb.ValidatorStatusResponse{{Status: ethpb.ValidatorStatus_ACTIVE}, {Status: ethpb.ValidatorStatus_ACTIVE}},
|
|
PublicKeys: [][]byte{keys[0][:], keys[1][:]},
|
|
Indices: []primitives.ValidatorIndex{1, 2},
|
|
}, nil)
|
|
client.EXPECT().PrepareBeaconProposer(gomock.Any(), &PrepareBeaconProposerRequestMatcher{
|
|
expectedRecipients: []*ethpb.PrepareBeaconProposerRequest_FeeRecipientContainer{
|
|
{FeeRecipient: common.HexToAddress("0x055Fb65722E7b2455043BFEBf6177F1D2e9738D9").Bytes(), ValidatorIndex: 1},
|
|
{FeeRecipient: common.HexToAddress(defaultFeeHex).Bytes(), ValidatorIndex: 2},
|
|
},
|
|
}).Return(nil, nil)
|
|
config[keys[0]] = &proposer.Option{
|
|
FeeRecipientConfig: &proposer.FeeRecipientConfig{
|
|
FeeRecipient: common.HexToAddress("0x055Fb65722E7b2455043BFEBf6177F1D2e9738D9"),
|
|
},
|
|
BuilderConfig: &proposer.BuilderConfig{
|
|
Enabled: true,
|
|
GasLimit: 40000000,
|
|
},
|
|
}
|
|
err = v.SetProposerSettings(t.Context(), &proposer.Settings{
|
|
ProposeConfig: config,
|
|
DefaultConfig: &proposer.Option{
|
|
FeeRecipientConfig: &proposer.FeeRecipientConfig{
|
|
FeeRecipient: common.HexToAddress(defaultFeeHex),
|
|
},
|
|
BuilderConfig: &proposer.BuilderConfig{
|
|
Enabled: true,
|
|
GasLimit: 35000000,
|
|
},
|
|
},
|
|
})
|
|
require.NoError(t, err)
|
|
client.EXPECT().SubmitValidatorRegistrations(
|
|
gomock.Any(),
|
|
gomock.Any(),
|
|
).Return(&empty.Empty{}, nil)
|
|
return &v
|
|
},
|
|
feeRecipientMap: map[primitives.ValidatorIndex]string{
|
|
1: "0x055Fb65722E7b2455043BFEBf6177F1D2e9738D9",
|
|
2: defaultFeeHex,
|
|
},
|
|
mockExpectedRequests: []ExpectedValidatorRegistration{
|
|
|
|
{
|
|
FeeRecipient: common.HexToAddress("0x055Fb65722E7b2455043BFEBf6177F1D2e9738D9").Bytes(),
|
|
GasLimit: 40000000,
|
|
},
|
|
{
|
|
FeeRecipient: byteValueAddress,
|
|
GasLimit: 35000000,
|
|
},
|
|
},
|
|
},
|
|
{
|
|
name: " Happy Path default doesn't send validator registration",
|
|
validatorSetter: func(t *testing.T) *validator {
|
|
|
|
v := validator{
|
|
validatorClient: client,
|
|
nodeClient: nodeClient,
|
|
db: db,
|
|
pubkeyToStatus: make(map[[fieldparams.BLSPubkeyLength]byte]*validatorStatus),
|
|
signedValidatorRegistrations: make(map[[fieldparams.BLSPubkeyLength]byte]*ethpb.SignedValidatorRegistrationV1),
|
|
enableAPI: false,
|
|
interopKeysConfig: &local.InteropKeymanagerConfig{
|
|
NumValidatorKeys: 2,
|
|
Offset: 1,
|
|
},
|
|
}
|
|
err := v.WaitForKeymanagerInitialization(ctx)
|
|
require.NoError(t, err)
|
|
config := make(map[[fieldparams.BLSPubkeyLength]byte]*proposer.Option)
|
|
km, err := v.Keymanager()
|
|
require.NoError(t, err)
|
|
keys, err := km.FetchValidatingPublicKeys(ctx)
|
|
require.NoError(t, err)
|
|
v.pubkeyToStatus[keys[0]] = &validatorStatus{
|
|
publicKey: keys[0][:],
|
|
status: ðpb.ValidatorStatusResponse{Status: ethpb.ValidatorStatus_ACTIVE},
|
|
index: primitives.ValidatorIndex(1),
|
|
}
|
|
v.pubkeyToStatus[keys[1]] = &validatorStatus{
|
|
publicKey: keys[1][:],
|
|
status: ðpb.ValidatorStatusResponse{Status: ethpb.ValidatorStatus_ACTIVE},
|
|
index: primitives.ValidatorIndex(2),
|
|
}
|
|
client.EXPECT().MultipleValidatorStatus(
|
|
gomock.Any(),
|
|
gomock.Any()).Return(
|
|
ðpb.MultipleValidatorStatusResponse{
|
|
Statuses: []*ethpb.ValidatorStatusResponse{{Status: ethpb.ValidatorStatus_ACTIVE}, {Status: ethpb.ValidatorStatus_ACTIVE}},
|
|
PublicKeys: [][]byte{keys[0][:], keys[1][:]},
|
|
Indices: []primitives.ValidatorIndex{1, 2},
|
|
}, nil)
|
|
client.EXPECT().PrepareBeaconProposer(gomock.Any(), &PrepareBeaconProposerRequestMatcher{
|
|
expectedRecipients: []*ethpb.PrepareBeaconProposerRequest_FeeRecipientContainer{
|
|
{FeeRecipient: common.HexToAddress("0x055Fb65722E7b2455043BFEBf6177F1D2e9738D9").Bytes(), ValidatorIndex: 1},
|
|
{FeeRecipient: common.HexToAddress(defaultFeeHex).Bytes(), ValidatorIndex: 2},
|
|
},
|
|
}).Return(nil, nil)
|
|
config[keys[0]] = &proposer.Option{
|
|
FeeRecipientConfig: &proposer.FeeRecipientConfig{
|
|
FeeRecipient: common.HexToAddress("0x055Fb65722E7b2455043BFEBf6177F1D2e9738D9"),
|
|
},
|
|
BuilderConfig: &proposer.BuilderConfig{
|
|
Enabled: true,
|
|
GasLimit: 40000000,
|
|
},
|
|
}
|
|
err = v.SetProposerSettings(t.Context(), &proposer.Settings{
|
|
ProposeConfig: config,
|
|
DefaultConfig: &proposer.Option{
|
|
FeeRecipientConfig: &proposer.FeeRecipientConfig{
|
|
FeeRecipient: common.HexToAddress(defaultFeeHex),
|
|
},
|
|
BuilderConfig: &proposer.BuilderConfig{
|
|
Enabled: false,
|
|
GasLimit: 35000000,
|
|
},
|
|
},
|
|
})
|
|
require.NoError(t, err)
|
|
client.EXPECT().SubmitValidatorRegistrations(
|
|
gomock.Any(),
|
|
gomock.Any(),
|
|
).Return(&empty.Empty{}, nil)
|
|
return &v
|
|
},
|
|
feeRecipientMap: map[primitives.ValidatorIndex]string{
|
|
1: "0x055Fb65722E7b2455043BFEBf6177F1D2e9738D9",
|
|
2: defaultFeeHex,
|
|
},
|
|
mockExpectedRequests: []ExpectedValidatorRegistration{
|
|
|
|
{
|
|
FeeRecipient: common.HexToAddress("0x055Fb65722E7b2455043BFEBf6177F1D2e9738D9").Bytes(),
|
|
GasLimit: uint64(40000000),
|
|
},
|
|
},
|
|
},
|
|
{
|
|
name: " Happy Path default doesn't send any validator registrations",
|
|
validatorSetter: func(t *testing.T) *validator {
|
|
v := validator{
|
|
validatorClient: client,
|
|
nodeClient: nodeClient,
|
|
db: db,
|
|
pubkeyToStatus: make(map[[fieldparams.BLSPubkeyLength]byte]*validatorStatus),
|
|
signedValidatorRegistrations: make(map[[fieldparams.BLSPubkeyLength]byte]*ethpb.SignedValidatorRegistrationV1),
|
|
enableAPI: false,
|
|
interopKeysConfig: &local.InteropKeymanagerConfig{
|
|
NumValidatorKeys: 2,
|
|
Offset: 1,
|
|
},
|
|
}
|
|
err := v.WaitForKeymanagerInitialization(ctx)
|
|
require.NoError(t, err)
|
|
config := make(map[[fieldparams.BLSPubkeyLength]byte]*proposer.Option)
|
|
km, err := v.Keymanager()
|
|
require.NoError(t, err)
|
|
keys, err := km.FetchValidatingPublicKeys(ctx)
|
|
require.NoError(t, err)
|
|
v.pubkeyToStatus[keys[0]] = &validatorStatus{
|
|
publicKey: keys[0][:],
|
|
status: ðpb.ValidatorStatusResponse{Status: ethpb.ValidatorStatus_ACTIVE},
|
|
index: primitives.ValidatorIndex(1),
|
|
}
|
|
v.pubkeyToStatus[keys[1]] = &validatorStatus{
|
|
publicKey: keys[1][:],
|
|
status: ðpb.ValidatorStatusResponse{Status: ethpb.ValidatorStatus_ACTIVE},
|
|
index: primitives.ValidatorIndex(2),
|
|
}
|
|
client.EXPECT().MultipleValidatorStatus(
|
|
gomock.Any(),
|
|
gomock.Any()).Return(
|
|
ðpb.MultipleValidatorStatusResponse{
|
|
Statuses: []*ethpb.ValidatorStatusResponse{{Status: ethpb.ValidatorStatus_ACTIVE}, {Status: ethpb.ValidatorStatus_ACTIVE}},
|
|
PublicKeys: [][]byte{keys[0][:], keys[1][:]},
|
|
Indices: []primitives.ValidatorIndex{1, 2},
|
|
}, nil)
|
|
client.EXPECT().PrepareBeaconProposer(gomock.Any(), &PrepareBeaconProposerRequestMatcher{
|
|
expectedRecipients: []*ethpb.PrepareBeaconProposerRequest_FeeRecipientContainer{
|
|
{FeeRecipient: common.HexToAddress("0x055Fb65722E7b2455043BFEBf6177F1D2e9738D9").Bytes(), ValidatorIndex: 1},
|
|
{FeeRecipient: common.HexToAddress(defaultFeeHex).Bytes(), ValidatorIndex: 2},
|
|
},
|
|
}).Return(nil, nil)
|
|
config[keys[0]] = &proposer.Option{
|
|
FeeRecipientConfig: &proposer.FeeRecipientConfig{
|
|
FeeRecipient: common.HexToAddress("0x055Fb65722E7b2455043BFEBf6177F1D2e9738D9"),
|
|
},
|
|
}
|
|
err = v.SetProposerSettings(t.Context(), &proposer.Settings{
|
|
ProposeConfig: config,
|
|
DefaultConfig: &proposer.Option{
|
|
FeeRecipientConfig: &proposer.FeeRecipientConfig{
|
|
FeeRecipient: common.HexToAddress(defaultFeeHex),
|
|
},
|
|
},
|
|
})
|
|
require.NoError(t, err)
|
|
return &v
|
|
},
|
|
feeRecipientMap: map[primitives.ValidatorIndex]string{
|
|
1: "0x055Fb65722E7b2455043BFEBf6177F1D2e9738D9",
|
|
2: defaultFeeHex,
|
|
},
|
|
logMessages: []string{"will not be included in builder validator registration"},
|
|
doesntContainLogs: true,
|
|
},
|
|
{
|
|
name: " Happy Path",
|
|
validatorSetter: func(t *testing.T) *validator {
|
|
|
|
v := validator{
|
|
validatorClient: client,
|
|
nodeClient: nodeClient,
|
|
db: db,
|
|
pubkeyToStatus: make(map[[fieldparams.BLSPubkeyLength]byte]*validatorStatus),
|
|
signedValidatorRegistrations: make(map[[fieldparams.BLSPubkeyLength]byte]*ethpb.SignedValidatorRegistrationV1),
|
|
enableAPI: false,
|
|
interopKeysConfig: &local.InteropKeymanagerConfig{
|
|
NumValidatorKeys: 1,
|
|
Offset: 1,
|
|
},
|
|
genesisTime: time.Unix(0, 0),
|
|
}
|
|
// set bellatrix as current epoch
|
|
params.BeaconConfig().BellatrixForkEpoch = 0
|
|
err := v.WaitForKeymanagerInitialization(ctx)
|
|
require.NoError(t, err)
|
|
km, err := v.Keymanager()
|
|
require.NoError(t, err)
|
|
keys, err := km.FetchValidatingPublicKeys(ctx)
|
|
require.NoError(t, err)
|
|
err = v.SetProposerSettings(t.Context(), &proposer.Settings{
|
|
ProposeConfig: nil,
|
|
DefaultConfig: &proposer.Option{
|
|
FeeRecipientConfig: &proposer.FeeRecipientConfig{
|
|
FeeRecipient: common.HexToAddress(defaultFeeHex),
|
|
},
|
|
BuilderConfig: &proposer.BuilderConfig{
|
|
Enabled: true,
|
|
GasLimit: validatorType.Uint64(params.BeaconConfig().DefaultBuilderGasLimit),
|
|
},
|
|
},
|
|
})
|
|
require.NoError(t, err)
|
|
v.pubkeyToStatus[keys[0]] = &validatorStatus{
|
|
publicKey: keys[0][:],
|
|
status: ðpb.ValidatorStatusResponse{Status: ethpb.ValidatorStatus_ACTIVE},
|
|
index: primitives.ValidatorIndex(1),
|
|
}
|
|
client.EXPECT().MultipleValidatorStatus(
|
|
gomock.Any(),
|
|
gomock.Any()).Return(
|
|
ðpb.MultipleValidatorStatusResponse{
|
|
Statuses: []*ethpb.ValidatorStatusResponse{{Status: ethpb.ValidatorStatus_ACTIVE}},
|
|
PublicKeys: [][]byte{keys[0][:]},
|
|
Indices: []primitives.ValidatorIndex{1},
|
|
}, nil)
|
|
|
|
client.EXPECT().SubmitValidatorRegistrations(
|
|
gomock.Any(),
|
|
gomock.Any(),
|
|
).Return(&empty.Empty{}, nil)
|
|
client.EXPECT().PrepareBeaconProposer(gomock.Any(), ðpb.PrepareBeaconProposerRequest{
|
|
Recipients: []*ethpb.PrepareBeaconProposerRequest_FeeRecipientContainer{
|
|
{FeeRecipient: common.HexToAddress(defaultFeeHex).Bytes(), ValidatorIndex: 1},
|
|
},
|
|
}).Return(nil, nil)
|
|
return &v
|
|
},
|
|
feeRecipientMap: map[primitives.ValidatorIndex]string{
|
|
1: defaultFeeHex,
|
|
},
|
|
mockExpectedRequests: []ExpectedValidatorRegistration{
|
|
{
|
|
FeeRecipient: byteValueAddress,
|
|
GasLimit: params.BeaconConfig().DefaultBuilderGasLimit,
|
|
},
|
|
},
|
|
},
|
|
{
|
|
name: " Happy Path validator index not found in cache",
|
|
validatorSetter: func(t *testing.T) *validator {
|
|
|
|
v := validator{
|
|
validatorClient: client,
|
|
nodeClient: nodeClient,
|
|
db: db,
|
|
pubkeyToStatus: make(map[[fieldparams.BLSPubkeyLength]byte]*validatorStatus),
|
|
signedValidatorRegistrations: make(map[[fieldparams.BLSPubkeyLength]byte]*ethpb.SignedValidatorRegistrationV1),
|
|
enableAPI: false,
|
|
interopKeysConfig: &local.InteropKeymanagerConfig{
|
|
NumValidatorKeys: 1,
|
|
Offset: 1,
|
|
},
|
|
}
|
|
err := v.WaitForKeymanagerInitialization(ctx)
|
|
require.NoError(t, err)
|
|
err = v.SetProposerSettings(t.Context(), &proposer.Settings{
|
|
ProposeConfig: nil,
|
|
DefaultConfig: &proposer.Option{
|
|
FeeRecipientConfig: &proposer.FeeRecipientConfig{
|
|
FeeRecipient: common.HexToAddress(defaultFeeHex),
|
|
},
|
|
BuilderConfig: &proposer.BuilderConfig{
|
|
Enabled: true,
|
|
GasLimit: 40000000,
|
|
},
|
|
},
|
|
})
|
|
require.NoError(t, err)
|
|
km, err := v.Keymanager()
|
|
require.NoError(t, err)
|
|
keys, err := km.FetchValidatingPublicKeys(ctx)
|
|
require.NoError(t, err)
|
|
v.pubkeyToStatus[keys[0]] = &validatorStatus{
|
|
publicKey: keys[0][:],
|
|
status: ðpb.ValidatorStatusResponse{Status: ethpb.ValidatorStatus_ACTIVE},
|
|
index: primitives.ValidatorIndex(1),
|
|
}
|
|
client.EXPECT().MultipleValidatorStatus(
|
|
gomock.Any(),
|
|
gomock.Any()).Return(
|
|
ðpb.MultipleValidatorStatusResponse{
|
|
Statuses: []*ethpb.ValidatorStatusResponse{{Status: ethpb.ValidatorStatus_ACTIVE}},
|
|
PublicKeys: [][]byte{keys[0][:]},
|
|
Indices: []primitives.ValidatorIndex{1},
|
|
}, nil)
|
|
client.EXPECT().SubmitValidatorRegistrations(
|
|
gomock.Any(),
|
|
gomock.Any(),
|
|
).Return(&empty.Empty{}, nil)
|
|
client.EXPECT().PrepareBeaconProposer(gomock.Any(), ðpb.PrepareBeaconProposerRequest{
|
|
Recipients: []*ethpb.PrepareBeaconProposerRequest_FeeRecipientContainer{
|
|
{FeeRecipient: common.HexToAddress(defaultFeeHex).Bytes(), ValidatorIndex: 1},
|
|
},
|
|
}).Return(nil, nil)
|
|
return &v
|
|
},
|
|
feeRecipientMap: map[primitives.ValidatorIndex]string{
|
|
1: defaultFeeHex,
|
|
},
|
|
mockExpectedRequests: []ExpectedValidatorRegistration{
|
|
{
|
|
FeeRecipient: byteValueAddress,
|
|
GasLimit: params.BeaconConfig().DefaultBuilderGasLimit,
|
|
},
|
|
},
|
|
},
|
|
{
|
|
name: " proposer config not nil but fee recipient empty",
|
|
validatorSetter: func(t *testing.T) *validator {
|
|
|
|
v := validator{
|
|
validatorClient: client,
|
|
nodeClient: nodeClient,
|
|
db: db,
|
|
pubkeyToStatus: make(map[[fieldparams.BLSPubkeyLength]byte]*validatorStatus),
|
|
signedValidatorRegistrations: make(map[[fieldparams.BLSPubkeyLength]byte]*ethpb.SignedValidatorRegistrationV1),
|
|
enableAPI: false,
|
|
interopKeysConfig: &local.InteropKeymanagerConfig{
|
|
NumValidatorKeys: 1,
|
|
Offset: 1,
|
|
},
|
|
}
|
|
err := v.WaitForKeymanagerInitialization(ctx)
|
|
require.NoError(t, err)
|
|
config := make(map[[fieldparams.BLSPubkeyLength]byte]*proposer.Option)
|
|
km, err := v.Keymanager()
|
|
require.NoError(t, err)
|
|
keys, err := km.FetchValidatingPublicKeys(ctx)
|
|
require.NoError(t, err)
|
|
v.pubkeyToStatus[keys[0]] = &validatorStatus{
|
|
publicKey: keys[0][:],
|
|
status: ðpb.ValidatorStatusResponse{Status: ethpb.ValidatorStatus_ACTIVE},
|
|
index: primitives.ValidatorIndex(1),
|
|
}
|
|
client.EXPECT().MultipleValidatorStatus(
|
|
gomock.Any(),
|
|
gomock.Any()).Return(
|
|
ðpb.MultipleValidatorStatusResponse{
|
|
Statuses: []*ethpb.ValidatorStatusResponse{{Status: ethpb.ValidatorStatus_ACTIVE}},
|
|
PublicKeys: [][]byte{keys[0][:]},
|
|
Indices: []primitives.ValidatorIndex{1},
|
|
}, nil)
|
|
client.EXPECT().PrepareBeaconProposer(gomock.Any(), ðpb.PrepareBeaconProposerRequest{
|
|
Recipients: []*ethpb.PrepareBeaconProposerRequest_FeeRecipientContainer{
|
|
{FeeRecipient: common.HexToAddress("0x0").Bytes(), ValidatorIndex: 1},
|
|
},
|
|
}).Return(nil, nil)
|
|
config[keys[0]] = &proposer.Option{
|
|
FeeRecipientConfig: &proposer.FeeRecipientConfig{
|
|
FeeRecipient: common.Address{},
|
|
},
|
|
}
|
|
err = v.SetProposerSettings(t.Context(), &proposer.Settings{
|
|
ProposeConfig: config,
|
|
DefaultConfig: &proposer.Option{
|
|
FeeRecipientConfig: &proposer.FeeRecipientConfig{
|
|
FeeRecipient: common.HexToAddress(defaultFeeHex),
|
|
},
|
|
},
|
|
})
|
|
require.NoError(t, err)
|
|
return &v
|
|
},
|
|
},
|
|
{
|
|
name: "Validator index not found with proposeconfig",
|
|
validatorSetter: func(t *testing.T) *validator {
|
|
|
|
v := validator{
|
|
validatorClient: client,
|
|
db: db,
|
|
pubkeyToStatus: make(map[[fieldparams.BLSPubkeyLength]byte]*validatorStatus),
|
|
signedValidatorRegistrations: make(map[[fieldparams.BLSPubkeyLength]byte]*ethpb.SignedValidatorRegistrationV1),
|
|
enableAPI: false,
|
|
interopKeysConfig: &local.InteropKeymanagerConfig{
|
|
NumValidatorKeys: 1,
|
|
Offset: 1,
|
|
},
|
|
}
|
|
err := v.WaitForKeymanagerInitialization(ctx)
|
|
require.NoError(t, err)
|
|
config := make(map[[fieldparams.BLSPubkeyLength]byte]*proposer.Option)
|
|
km, err := v.Keymanager()
|
|
require.NoError(t, err)
|
|
keys, err := km.FetchValidatingPublicKeys(ctx)
|
|
require.NoError(t, err)
|
|
config[keys[0]] = &proposer.Option{
|
|
FeeRecipientConfig: &proposer.FeeRecipientConfig{
|
|
FeeRecipient: common.HexToAddress("0x046Fb65722E7b2455043BFEBf6177F1D2e9738D9"),
|
|
},
|
|
}
|
|
client.EXPECT().MultipleValidatorStatus(
|
|
gomock.Any(),
|
|
gomock.Any()).Return(
|
|
ðpb.MultipleValidatorStatusResponse{
|
|
Statuses: []*ethpb.ValidatorStatusResponse{{Status: ethpb.ValidatorStatus_UNKNOWN_STATUS}},
|
|
PublicKeys: [][]byte{keys[0][:]},
|
|
Indices: []primitives.ValidatorIndex{unknownIndex},
|
|
}, nil)
|
|
err = v.SetProposerSettings(t.Context(), &proposer.Settings{
|
|
ProposeConfig: config,
|
|
DefaultConfig: &proposer.Option{
|
|
FeeRecipientConfig: &proposer.FeeRecipientConfig{
|
|
FeeRecipient: common.HexToAddress(defaultFeeHex),
|
|
},
|
|
},
|
|
})
|
|
require.NoError(t, err)
|
|
return &v
|
|
},
|
|
},
|
|
{
|
|
name: "register validator batch failed",
|
|
validatorSetter: func(t *testing.T) *validator {
|
|
v := validator{
|
|
validatorClient: client,
|
|
nodeClient: nodeClient,
|
|
db: db,
|
|
pubkeyToStatus: make(map[[fieldparams.BLSPubkeyLength]byte]*validatorStatus),
|
|
signedValidatorRegistrations: make(map[[fieldparams.BLSPubkeyLength]byte]*ethpb.SignedValidatorRegistrationV1),
|
|
enableAPI: false,
|
|
interopKeysConfig: &local.InteropKeymanagerConfig{
|
|
NumValidatorKeys: 1,
|
|
Offset: 1,
|
|
},
|
|
}
|
|
err := v.WaitForKeymanagerInitialization(ctx)
|
|
require.NoError(t, err)
|
|
config := make(map[[fieldparams.BLSPubkeyLength]byte]*proposer.Option)
|
|
km, err := v.Keymanager()
|
|
require.NoError(t, err)
|
|
keys, err := km.FetchValidatingPublicKeys(ctx)
|
|
require.NoError(t, err)
|
|
v.pubkeyToStatus[keys[0]] = &validatorStatus{
|
|
publicKey: keys[0][:],
|
|
status: ðpb.ValidatorStatusResponse{Status: ethpb.ValidatorStatus_ACTIVE},
|
|
index: primitives.ValidatorIndex(1),
|
|
}
|
|
client.EXPECT().MultipleValidatorStatus(
|
|
gomock.Any(),
|
|
gomock.Any()).Return(
|
|
ðpb.MultipleValidatorStatusResponse{
|
|
Statuses: []*ethpb.ValidatorStatusResponse{{Status: ethpb.ValidatorStatus_ACTIVE}},
|
|
PublicKeys: [][]byte{keys[0][:]},
|
|
Indices: []primitives.ValidatorIndex{1},
|
|
}, nil)
|
|
|
|
config[keys[0]] = &proposer.Option{
|
|
FeeRecipientConfig: &proposer.FeeRecipientConfig{
|
|
FeeRecipient: common.Address{},
|
|
},
|
|
BuilderConfig: &proposer.BuilderConfig{
|
|
Enabled: true,
|
|
GasLimit: 40000000,
|
|
},
|
|
}
|
|
err = v.SetProposerSettings(t.Context(), &proposer.Settings{
|
|
ProposeConfig: config,
|
|
DefaultConfig: &proposer.Option{
|
|
FeeRecipientConfig: &proposer.FeeRecipientConfig{
|
|
FeeRecipient: common.HexToAddress(defaultFeeHex),
|
|
},
|
|
BuilderConfig: &proposer.BuilderConfig{
|
|
Enabled: true,
|
|
GasLimit: 40000000,
|
|
},
|
|
},
|
|
})
|
|
require.NoError(t, err)
|
|
client.EXPECT().PrepareBeaconProposer(gomock.Any(), ðpb.PrepareBeaconProposerRequest{
|
|
Recipients: []*ethpb.PrepareBeaconProposerRequest_FeeRecipientContainer{
|
|
{FeeRecipient: common.HexToAddress("0x0").Bytes(), ValidatorIndex: 1},
|
|
},
|
|
}).Return(nil, nil)
|
|
client.EXPECT().SubmitValidatorRegistrations(
|
|
gomock.Any(),
|
|
gomock.Any(),
|
|
).Return(&empty.Empty{}, errors.New("request failed"))
|
|
return &v
|
|
},
|
|
logMessages: []string{"request failed"},
|
|
logDelay: 1 * time.Second,
|
|
},
|
|
}
|
|
for _, tt := range tests {
|
|
t.Run(tt.name, func(t *testing.T) {
|
|
hook := logTest.NewGlobal()
|
|
v := tt.validatorSetter(t)
|
|
km, err := v.Keymanager()
|
|
require.NoError(t, err)
|
|
pubkeys, err := km.FetchValidatingPublicKeys(ctx)
|
|
require.NoError(t, err)
|
|
if tt.feeRecipientMap != nil {
|
|
feeRecipients, err := v.buildPrepProposerReqs(pubkeys)
|
|
require.NoError(t, err)
|
|
signedRegisterValidatorRequests := v.buildSignedRegReqs(ctx, pubkeys, km.Sign, 0, false)
|
|
for _, recipient := range feeRecipients {
|
|
require.Equal(t, strings.ToLower(tt.feeRecipientMap[recipient.ValidatorIndex]), strings.ToLower(hexutil.Encode(recipient.FeeRecipient)))
|
|
}
|
|
require.Equal(t, len(tt.feeRecipientMap), len(feeRecipients))
|
|
for i, request := range tt.mockExpectedRequests {
|
|
require.Equal(t, tt.mockExpectedRequests[i].GasLimit, request.GasLimit)
|
|
require.Equal(t, hexutil.Encode(tt.mockExpectedRequests[i].FeeRecipient), hexutil.Encode(request.FeeRecipient))
|
|
}
|
|
// check if Pubkeys are always unique
|
|
var unique = make(map[string]bool)
|
|
for _, request := range signedRegisterValidatorRequests {
|
|
require.Equal(t, unique[common.BytesToAddress(request.Message.Pubkey).Hex()], false)
|
|
unique[common.BytesToAddress(request.Message.Pubkey).Hex()] = true
|
|
}
|
|
require.Equal(t, len(tt.mockExpectedRequests), len(signedRegisterValidatorRequests))
|
|
require.Equal(t, len(signedRegisterValidatorRequests), len(v.signedValidatorRegistrations))
|
|
}
|
|
if err := v.PushProposerSettings(ctx, 0, false); tt.err != "" {
|
|
assert.ErrorContains(t, tt.err, err)
|
|
}
|
|
if len(tt.logMessages) > 0 {
|
|
if tt.logDelay > 0 {
|
|
time.Sleep(tt.logDelay)
|
|
}
|
|
for _, message := range tt.logMessages {
|
|
if tt.doesntContainLogs {
|
|
assert.LogsDoNotContain(t, hook, message)
|
|
} else {
|
|
assert.LogsContain(t, hook, message)
|
|
}
|
|
}
|
|
|
|
}
|
|
})
|
|
}
|
|
}
|
|
}
|
|
|
|
func pubkeyFromString(t *testing.T, stringPubkey string) [fieldparams.BLSPubkeyLength]byte {
|
|
pubkeyTemp, err := hexutil.Decode(stringPubkey)
|
|
require.NoError(t, err)
|
|
|
|
var pubkey [fieldparams.BLSPubkeyLength]byte
|
|
copy(pubkey[:], pubkeyTemp)
|
|
|
|
return pubkey
|
|
}
|
|
|
|
func feeRecipientFromString(t *testing.T, stringFeeRecipient string) common.Address {
|
|
feeRecipientTemp, err := hexutil.Decode(stringFeeRecipient)
|
|
require.NoError(t, err)
|
|
|
|
var feeRecipient common.Address
|
|
copy(feeRecipient[:], feeRecipientTemp)
|
|
|
|
return feeRecipient
|
|
}
|
|
|
|
func TestValidator_buildPrepProposerReqs_WithoutDefaultConfig(t *testing.T) {
|
|
// pubkey1 => feeRecipient1 (already in `v.validatorIndex`)
|
|
// pubkey2 => feeRecipient2 (NOT in `v.validatorIndex`, index found by beacon node)
|
|
// pubkey3 => feeRecipient3 (NOT in `v.validatorIndex`, index NOT found by beacon node)
|
|
// pubkey4 => Nothing (already in `v.validatorIndex`)
|
|
|
|
// Public keys
|
|
pubkey1 := pubkeyFromString(t, "0x111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111")
|
|
pubkey2 := pubkeyFromString(t, "0x222222222222222222222222222222222222222222222222222222222222222222222222222222222222222222222222")
|
|
pubkey3 := pubkeyFromString(t, "0x333333333333333333333333333333333333333333333333333333333333333333333333333333333333333333333333")
|
|
pubkey4 := pubkeyFromString(t, "0x444444444444444444444444444444444444444444444444444444444444444444444444444444444444444444444444")
|
|
|
|
// Fee recipients
|
|
feeRecipient1 := feeRecipientFromString(t, "0x1111111111111111111111111111111111111111")
|
|
feeRecipient2 := feeRecipientFromString(t, "0x0000000000000000000000000000000000000000")
|
|
feeRecipient3 := feeRecipientFromString(t, "0x3333333333333333333333333333333333333333")
|
|
feeRecipient4 := common.Address{}
|
|
|
|
ctrl := gomock.NewController(t)
|
|
defer ctrl.Finish()
|
|
|
|
ctx := t.Context()
|
|
client := validatormock.NewMockValidatorClient(ctrl)
|
|
|
|
client.EXPECT().MultipleValidatorStatus(
|
|
gomock.Any(),
|
|
gomock.Any()).Return(
|
|
ðpb.MultipleValidatorStatusResponse{
|
|
Statuses: []*ethpb.ValidatorStatusResponse{{Status: ethpb.ValidatorStatus_ACTIVE}, {Status: ethpb.ValidatorStatus_ACTIVE}, {Status: ethpb.ValidatorStatus_UNKNOWN_STATUS}, {Status: ethpb.ValidatorStatus_ACTIVE}},
|
|
PublicKeys: [][]byte{pubkey1[:], pubkey2[:], pubkey3[:], pubkey4[:]},
|
|
Indices: []primitives.ValidatorIndex{1, 2, unknownIndex, 4},
|
|
}, nil)
|
|
v := validator{
|
|
validatorClient: client,
|
|
proposerSettings: &proposer.Settings{
|
|
DefaultConfig: nil,
|
|
ProposeConfig: map[[48]byte]*proposer.Option{
|
|
pubkey1: {
|
|
FeeRecipientConfig: &proposer.FeeRecipientConfig{
|
|
FeeRecipient: feeRecipient1,
|
|
},
|
|
},
|
|
pubkey2: {
|
|
FeeRecipientConfig: &proposer.FeeRecipientConfig{
|
|
FeeRecipient: feeRecipient2,
|
|
},
|
|
},
|
|
pubkey3: {
|
|
FeeRecipientConfig: &proposer.FeeRecipientConfig{
|
|
FeeRecipient: feeRecipient3,
|
|
},
|
|
},
|
|
pubkey4: {
|
|
FeeRecipientConfig: &proposer.FeeRecipientConfig{
|
|
FeeRecipient: feeRecipient4,
|
|
},
|
|
},
|
|
},
|
|
},
|
|
pubkeyToStatus: map[[48]byte]*validatorStatus{
|
|
pubkey1: {
|
|
publicKey: pubkey1[:],
|
|
status: ðpb.ValidatorStatusResponse{Status: ethpb.ValidatorStatus_ACTIVE},
|
|
index: 1,
|
|
},
|
|
pubkey4: {
|
|
publicKey: pubkey4[:],
|
|
status: ðpb.ValidatorStatusResponse{Status: ethpb.ValidatorStatus_ACTIVE},
|
|
index: 4,
|
|
},
|
|
},
|
|
}
|
|
|
|
pubkeys := [][fieldparams.BLSPubkeyLength]byte{pubkey1, pubkey2, pubkey3, pubkey4}
|
|
|
|
expected := []*ethpb.PrepareBeaconProposerRequest_FeeRecipientContainer{
|
|
{
|
|
ValidatorIndex: 1,
|
|
FeeRecipient: feeRecipient1[:],
|
|
},
|
|
{
|
|
ValidatorIndex: 2,
|
|
FeeRecipient: feeRecipient2[:],
|
|
},
|
|
{
|
|
ValidatorIndex: 4,
|
|
FeeRecipient: feeRecipient4[:],
|
|
},
|
|
}
|
|
filteredKeys, err := v.filterAndCacheActiveKeys(ctx, pubkeys, 0)
|
|
require.NoError(t, err)
|
|
actual, err := v.buildPrepProposerReqs(filteredKeys)
|
|
require.NoError(t, err)
|
|
sort.Slice(actual, func(i, j int) bool {
|
|
return actual[i].ValidatorIndex < actual[j].ValidatorIndex
|
|
})
|
|
assert.DeepEqual(t, expected, actual)
|
|
}
|
|
|
|
func TestValidator_filterAndCacheActiveKeys(t *testing.T) {
|
|
// Public keys
|
|
pubkey1 := pubkeyFromString(t, "0x111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111")
|
|
pubkey2 := pubkeyFromString(t, "0x222222222222222222222222222222222222222222222222222222222222222222222222222222222222222222222222")
|
|
pubkey3 := pubkeyFromString(t, "0x333333333333333333333333333333333333333333333333333333333333333333333333333333333333333333333333")
|
|
pubkey4 := pubkeyFromString(t, "0x444444444444444444444444444444444444444444444444444444444444444444444444444444444444444444444444")
|
|
|
|
t.Run("refetch all keys at start of epoch", func(t *testing.T) {
|
|
ctrl := gomock.NewController(t)
|
|
defer ctrl.Finish()
|
|
|
|
ctx := t.Context()
|
|
client := validatormock.NewMockValidatorClient(ctrl)
|
|
|
|
client.EXPECT().MultipleValidatorStatus(
|
|
gomock.Any(),
|
|
gomock.Any()).Return(
|
|
ðpb.MultipleValidatorStatusResponse{
|
|
Statuses: []*ethpb.ValidatorStatusResponse{{Status: ethpb.ValidatorStatus_ACTIVE}, {Status: ethpb.ValidatorStatus_ACTIVE}, {Status: ethpb.ValidatorStatus_UNKNOWN_STATUS}, {Status: ethpb.ValidatorStatus_ACTIVE}},
|
|
PublicKeys: [][]byte{pubkey1[:], pubkey2[:], pubkey3[:], pubkey4[:]},
|
|
Indices: []primitives.ValidatorIndex{1, 2, unknownIndex, 4},
|
|
}, nil)
|
|
v := validator{
|
|
validatorClient: client,
|
|
pubkeyToStatus: make(map[[48]byte]*validatorStatus),
|
|
}
|
|
keys, err := v.filterAndCacheActiveKeys(ctx, [][48]byte{pubkey1, pubkey2, pubkey3, pubkey4}, 0)
|
|
require.NoError(t, err)
|
|
// one key is unknown status
|
|
require.Equal(t, 3, len(keys))
|
|
})
|
|
t.Run("refetch all keys at start of epoch, even with cache", func(t *testing.T) {
|
|
ctrl := gomock.NewController(t)
|
|
defer ctrl.Finish()
|
|
|
|
ctx := t.Context()
|
|
client := validatormock.NewMockValidatorClient(ctrl)
|
|
|
|
client.EXPECT().MultipleValidatorStatus(
|
|
gomock.Any(),
|
|
gomock.Any()).Return(
|
|
ðpb.MultipleValidatorStatusResponse{
|
|
Statuses: []*ethpb.ValidatorStatusResponse{{Status: ethpb.ValidatorStatus_ACTIVE}, {Status: ethpb.ValidatorStatus_ACTIVE}, {Status: ethpb.ValidatorStatus_UNKNOWN_STATUS}, {Status: ethpb.ValidatorStatus_ACTIVE}},
|
|
PublicKeys: [][]byte{pubkey1[:], pubkey2[:], pubkey3[:], pubkey4[:]},
|
|
Indices: []primitives.ValidatorIndex{1, 2, unknownIndex, 4},
|
|
}, nil)
|
|
v := validator{
|
|
validatorClient: client,
|
|
pubkeyToStatus: map[[48]byte]*validatorStatus{
|
|
pubkey1: {
|
|
publicKey: pubkey1[:],
|
|
status: ðpb.ValidatorStatusResponse{Status: ethpb.ValidatorStatus_ACTIVE},
|
|
index: 1,
|
|
},
|
|
pubkey2: {
|
|
publicKey: pubkey2[:],
|
|
status: ðpb.ValidatorStatusResponse{Status: ethpb.ValidatorStatus_ACTIVE},
|
|
index: 2,
|
|
},
|
|
pubkey3: {
|
|
publicKey: pubkey3[:],
|
|
status: ðpb.ValidatorStatusResponse{Status: ethpb.ValidatorStatus_ACTIVE}, // gets overridden
|
|
index: 3,
|
|
},
|
|
pubkey4: {
|
|
publicKey: pubkey4[:],
|
|
status: ðpb.ValidatorStatusResponse{Status: ethpb.ValidatorStatus_ACTIVE},
|
|
index: 4,
|
|
},
|
|
},
|
|
}
|
|
keys, err := v.filterAndCacheActiveKeys(ctx, [][48]byte{pubkey1, pubkey2, pubkey3, pubkey4}, 0)
|
|
require.NoError(t, err)
|
|
// one key is unknown status
|
|
require.Equal(t, 3, len(keys))
|
|
})
|
|
t.Run("cache used mid epoch, no new keys added", func(t *testing.T) {
|
|
ctx := t.Context()
|
|
v := validator{
|
|
pubkeyToStatus: map[[48]byte]*validatorStatus{
|
|
pubkey1: {
|
|
publicKey: pubkey1[:],
|
|
status: ðpb.ValidatorStatusResponse{Status: ethpb.ValidatorStatus_ACTIVE},
|
|
index: 1,
|
|
},
|
|
pubkey4: {
|
|
publicKey: pubkey4[:],
|
|
status: ðpb.ValidatorStatusResponse{Status: ethpb.ValidatorStatus_ACTIVE},
|
|
index: 4,
|
|
},
|
|
},
|
|
}
|
|
keys, err := v.filterAndCacheActiveKeys(ctx, [][48]byte{pubkey1, pubkey4}, 5)
|
|
require.NoError(t, err)
|
|
// one key is unknown status
|
|
require.Equal(t, 2, len(keys))
|
|
})
|
|
|
|
}
|
|
|
|
func TestValidator_buildPrepProposerReqs_WithDefaultConfig(t *testing.T) {
|
|
// pubkey1 => feeRecipient1 - Status: active
|
|
// pubkey2 => feeRecipient2 - Status: active
|
|
// pubkey3 => feeRecipient3 - Status: unknown
|
|
// pubkey4 => Nothing - Status: active
|
|
// pubkey5 => Nothing - Status: exited
|
|
// pubkey6 => Nothing - Status: pending - ActivationEpoch: 35 (current slot: 641 - current epoch: 20)
|
|
// pubkey7 => Nothing - Status: pending - ActivationEpoch: 20 (current slot: 641 - current epoch: 20)
|
|
// pubkey8 => feeRecipient8 - Status: exiting
|
|
|
|
// Public keys
|
|
pubkey1 := pubkeyFromString(t, "0x111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111")
|
|
pubkey2 := pubkeyFromString(t, "0x222222222222222222222222222222222222222222222222222222222222222222222222222222222222222222222222")
|
|
pubkey3 := pubkeyFromString(t, "0x333333333333333333333333333333333333333333333333333333333333333333333333333333333333333333333333")
|
|
pubkey4 := pubkeyFromString(t, "0x444444444444444444444444444444444444444444444444444444444444444444444444444444444444444444444444")
|
|
pubkey5 := pubkeyFromString(t, "0x555555555555555555555555555555555555555555555555555555555555555555555555555555555555555555555555")
|
|
pubkey6 := pubkeyFromString(t, "0x666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666")
|
|
pubkey7 := pubkeyFromString(t, "0x777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777")
|
|
pubkey8 := pubkeyFromString(t, "0x888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888")
|
|
|
|
// Fee recipients
|
|
feeRecipient1 := feeRecipientFromString(t, "0x1111111111111111111111111111111111111111")
|
|
feeRecipient2 := feeRecipientFromString(t, "0x0000000000000000000000000000000000000000")
|
|
feeRecipient3 := feeRecipientFromString(t, "0x3333333333333333333333333333333333333333")
|
|
feeRecipient8 := feeRecipientFromString(t, "0x8888888888888888888888888888888888888888")
|
|
|
|
defaultFeeRecipient := feeRecipientFromString(t, "0xdddddddddddddddddddddddddddddddddddddddd")
|
|
|
|
pubkeyToStatus := map[[fieldparams.BLSPubkeyLength]byte]ethpb.ValidatorStatus{
|
|
pubkey1: ethpb.ValidatorStatus_ACTIVE,
|
|
pubkey2: ethpb.ValidatorStatus_ACTIVE,
|
|
pubkey3: ethpb.ValidatorStatus_UNKNOWN_STATUS,
|
|
pubkey4: ethpb.ValidatorStatus_ACTIVE,
|
|
pubkey5: ethpb.ValidatorStatus_EXITED,
|
|
pubkey6: ethpb.ValidatorStatus_PENDING,
|
|
pubkey7: ethpb.ValidatorStatus_PENDING,
|
|
pubkey8: ethpb.ValidatorStatus_EXITING,
|
|
}
|
|
|
|
pubkeyToActivationEpoch := map[[fieldparams.BLSPubkeyLength]byte]primitives.Epoch{
|
|
pubkey1: 0,
|
|
pubkey2: 0,
|
|
pubkey3: 0,
|
|
pubkey4: 0,
|
|
pubkey5: 0,
|
|
pubkey6: 35,
|
|
pubkey7: 20,
|
|
pubkey8: 0,
|
|
}
|
|
|
|
pubkeyToIndex := map[[fieldparams.BLSPubkeyLength]byte]primitives.ValidatorIndex{
|
|
pubkey1: 1,
|
|
pubkey2: 2,
|
|
pubkey3: unknownIndex,
|
|
pubkey4: 4,
|
|
pubkey5: 5,
|
|
pubkey6: 6,
|
|
pubkey7: 7,
|
|
pubkey8: 8,
|
|
}
|
|
|
|
ctrl := gomock.NewController(t)
|
|
defer ctrl.Finish()
|
|
|
|
ctx := t.Context()
|
|
client := validatormock.NewMockValidatorClient(ctrl)
|
|
|
|
client.EXPECT().MultipleValidatorStatus(
|
|
gomock.Any(),
|
|
gomock.Any()).DoAndReturn(func(ctx context.Context, val *ethpb.MultipleValidatorStatusRequest) (*ethpb.MultipleValidatorStatusResponse, error) {
|
|
resp := ðpb.MultipleValidatorStatusResponse{}
|
|
|
|
for _, k := range val.PublicKeys {
|
|
resp.PublicKeys = append(resp.PublicKeys, bytesutil.SafeCopyBytes(k))
|
|
resp.Statuses = append(resp.Statuses, ðpb.ValidatorStatusResponse{
|
|
Status: pubkeyToStatus[bytesutil.ToBytes48(k)],
|
|
ActivationEpoch: pubkeyToActivationEpoch[bytesutil.ToBytes48(k)],
|
|
})
|
|
index := pubkeyToIndex[bytesutil.ToBytes48(k)]
|
|
resp.Indices = append(resp.Indices, index)
|
|
}
|
|
return resp, nil
|
|
})
|
|
|
|
v := validator{
|
|
validatorClient: client,
|
|
proposerSettings: &proposer.Settings{
|
|
DefaultConfig: &proposer.Option{
|
|
FeeRecipientConfig: &proposer.FeeRecipientConfig{
|
|
FeeRecipient: defaultFeeRecipient,
|
|
},
|
|
},
|
|
ProposeConfig: map[[fieldparams.BLSPubkeyLength]byte]*proposer.Option{
|
|
pubkey1: {
|
|
FeeRecipientConfig: &proposer.FeeRecipientConfig{
|
|
FeeRecipient: feeRecipient1,
|
|
},
|
|
},
|
|
pubkey2: {
|
|
FeeRecipientConfig: &proposer.FeeRecipientConfig{
|
|
FeeRecipient: feeRecipient2,
|
|
},
|
|
},
|
|
pubkey3: {
|
|
FeeRecipientConfig: &proposer.FeeRecipientConfig{
|
|
FeeRecipient: feeRecipient3,
|
|
},
|
|
},
|
|
pubkey8: {
|
|
FeeRecipientConfig: &proposer.FeeRecipientConfig{
|
|
FeeRecipient: feeRecipient8,
|
|
},
|
|
},
|
|
},
|
|
},
|
|
pubkeyToStatus: map[[fieldparams.BLSPubkeyLength]byte]*validatorStatus{
|
|
pubkey1: {
|
|
publicKey: pubkey1[:],
|
|
status: ðpb.ValidatorStatusResponse{Status: ethpb.ValidatorStatus_ACTIVE},
|
|
index: 1,
|
|
},
|
|
pubkey2: {
|
|
publicKey: pubkey2[:],
|
|
status: ðpb.ValidatorStatusResponse{Status: ethpb.ValidatorStatus_ACTIVE},
|
|
index: 2,
|
|
},
|
|
pubkey3: {
|
|
publicKey: pubkey3[:],
|
|
status: ðpb.ValidatorStatusResponse{Status: ethpb.ValidatorStatus_UNKNOWN_STATUS},
|
|
index: unknownIndex,
|
|
},
|
|
pubkey4: {
|
|
publicKey: pubkey4[:],
|
|
status: ðpb.ValidatorStatusResponse{Status: ethpb.ValidatorStatus_ACTIVE},
|
|
index: 4,
|
|
},
|
|
pubkey5: {
|
|
publicKey: pubkey5[:],
|
|
status: ðpb.ValidatorStatusResponse{Status: ethpb.ValidatorStatus_ACTIVE},
|
|
index: 5,
|
|
},
|
|
pubkey6: {
|
|
publicKey: pubkey6[:],
|
|
status: ðpb.ValidatorStatusResponse{Status: ethpb.ValidatorStatus_ACTIVE},
|
|
index: 6,
|
|
},
|
|
pubkey7: {
|
|
publicKey: pubkey7[:],
|
|
status: ðpb.ValidatorStatusResponse{Status: ethpb.ValidatorStatus_ACTIVE},
|
|
index: 7,
|
|
},
|
|
pubkey8: {
|
|
publicKey: pubkey8[:],
|
|
status: ðpb.ValidatorStatusResponse{Status: ethpb.ValidatorStatus_ACTIVE},
|
|
index: 8,
|
|
},
|
|
},
|
|
}
|
|
|
|
pubkeys := [][fieldparams.BLSPubkeyLength]byte{
|
|
pubkey1,
|
|
pubkey2,
|
|
pubkey3,
|
|
pubkey4,
|
|
pubkey5,
|
|
pubkey6,
|
|
pubkey7,
|
|
pubkey8,
|
|
}
|
|
|
|
expected := []*ethpb.PrepareBeaconProposerRequest_FeeRecipientContainer{
|
|
{
|
|
ValidatorIndex: 1,
|
|
FeeRecipient: feeRecipient1[:],
|
|
},
|
|
{
|
|
ValidatorIndex: 2,
|
|
FeeRecipient: feeRecipient2[:],
|
|
},
|
|
{
|
|
ValidatorIndex: 4,
|
|
FeeRecipient: defaultFeeRecipient[:],
|
|
},
|
|
{
|
|
ValidatorIndex: 7,
|
|
FeeRecipient: defaultFeeRecipient[:],
|
|
},
|
|
{
|
|
ValidatorIndex: 8,
|
|
FeeRecipient: feeRecipient8[:],
|
|
},
|
|
}
|
|
filteredKeys, err := v.filterAndCacheActiveKeys(ctx, pubkeys, 640)
|
|
require.NoError(t, err)
|
|
actual, err := v.buildPrepProposerReqs(filteredKeys)
|
|
require.NoError(t, err)
|
|
sort.Slice(actual, func(i, j int) bool {
|
|
return actual[i].ValidatorIndex < actual[j].ValidatorIndex
|
|
})
|
|
assert.DeepEqual(t, expected, actual)
|
|
}
|
|
|
|
func TestValidator_buildSignedRegReqs_DefaultConfigDisabled(t *testing.T) {
|
|
// pubkey1 => feeRecipient1, builder enabled
|
|
// pubkey2 => feeRecipient2, builder disabled
|
|
// pubkey3 => Nothing, builder enabled
|
|
|
|
// Public keys
|
|
pubkey1 := pubkeyFromString(t, "0x111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111")
|
|
pubkey2 := pubkeyFromString(t, "0x222222222222222222222222222222222222222222222222222222222222222222222222222222222222222222222222")
|
|
pubkey3 := pubkeyFromString(t, "0x333333333333333333333333333333333333333333333333333333333333333333333333333333333333333333333333")
|
|
|
|
// Fee recipients
|
|
feeRecipient1 := feeRecipientFromString(t, "0x0000000000000000000000000000000000000000")
|
|
feeRecipient2 := feeRecipientFromString(t, "0x2222222222222222222222222222222222222222")
|
|
|
|
defaultFeeRecipient := feeRecipientFromString(t, "0xdddddddddddddddddddddddddddddddddddddddd")
|
|
|
|
ctrl := gomock.NewController(t)
|
|
defer ctrl.Finish()
|
|
|
|
ctx := t.Context()
|
|
client := validatormock.NewMockValidatorClient(ctrl)
|
|
|
|
signature := blsmock.NewMockSignature(ctrl)
|
|
signature.EXPECT().Marshal().Return([]byte{})
|
|
|
|
v := validator{
|
|
signedValidatorRegistrations: map[[48]byte]*ethpb.SignedValidatorRegistrationV1{},
|
|
validatorClient: client,
|
|
proposerSettings: &proposer.Settings{
|
|
DefaultConfig: &proposer.Option{
|
|
FeeRecipientConfig: &proposer.FeeRecipientConfig{
|
|
FeeRecipient: defaultFeeRecipient,
|
|
},
|
|
BuilderConfig: &proposer.BuilderConfig{
|
|
Enabled: false,
|
|
GasLimit: 9999,
|
|
},
|
|
},
|
|
ProposeConfig: map[[48]byte]*proposer.Option{
|
|
pubkey1: {
|
|
FeeRecipientConfig: &proposer.FeeRecipientConfig{
|
|
FeeRecipient: feeRecipient1,
|
|
},
|
|
BuilderConfig: &proposer.BuilderConfig{
|
|
Enabled: true,
|
|
GasLimit: 1111,
|
|
},
|
|
},
|
|
pubkey2: {
|
|
FeeRecipientConfig: &proposer.FeeRecipientConfig{
|
|
FeeRecipient: feeRecipient2,
|
|
},
|
|
BuilderConfig: &proposer.BuilderConfig{
|
|
Enabled: false,
|
|
GasLimit: 2222,
|
|
},
|
|
},
|
|
pubkey3: {
|
|
FeeRecipientConfig: nil,
|
|
BuilderConfig: &proposer.BuilderConfig{
|
|
Enabled: true,
|
|
GasLimit: 3333,
|
|
},
|
|
},
|
|
},
|
|
},
|
|
pubkeyToStatus: make(map[[48]byte]*validatorStatus),
|
|
}
|
|
|
|
pubkeys := [][fieldparams.BLSPubkeyLength]byte{pubkey1, pubkey2, pubkey3}
|
|
|
|
var signer = func(_ context.Context, _ *validatorpb.SignRequest) (bls.Signature, error) {
|
|
return signature, nil
|
|
}
|
|
v.pubkeyToStatus[pubkey1] = &validatorStatus{
|
|
publicKey: pubkey1[:],
|
|
status: ðpb.ValidatorStatusResponse{Status: ethpb.ValidatorStatus_ACTIVE},
|
|
index: 1,
|
|
}
|
|
v.pubkeyToStatus[pubkey2] = &validatorStatus{
|
|
publicKey: pubkey2[:],
|
|
status: ðpb.ValidatorStatusResponse{Status: ethpb.ValidatorStatus_ACTIVE},
|
|
index: 2,
|
|
}
|
|
v.pubkeyToStatus[pubkey3] = &validatorStatus{
|
|
publicKey: pubkey3[:],
|
|
status: ðpb.ValidatorStatusResponse{Status: ethpb.ValidatorStatus_ACTIVE},
|
|
index: 3,
|
|
}
|
|
actual := v.buildSignedRegReqs(ctx, pubkeys, signer, 0, false)
|
|
|
|
assert.Equal(t, 1, len(actual))
|
|
assert.DeepEqual(t, feeRecipient1[:], actual[0].Message.FeeRecipient)
|
|
assert.Equal(t, uint64(1111), actual[0].Message.GasLimit)
|
|
assert.DeepEqual(t, pubkey1[:], actual[0].Message.Pubkey)
|
|
|
|
}
|
|
|
|
func TestValidator_buildSignedRegReqs_DefaultConfigEnabled(t *testing.T) {
|
|
// pubkey1 => feeRecipient1, builder enabled
|
|
// pubkey2 => feeRecipient2, builder disabled
|
|
// pubkey3 => Nothing, builder enabled
|
|
// pubkey4 => added after builder requests built once, used in mid epoch test
|
|
|
|
// Public keys
|
|
pubkey1 := pubkeyFromString(t, "0x111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111")
|
|
pubkey2 := pubkeyFromString(t, "0x222222222222222222222222222222222222222222222222222222222222222222222222222222222222222222222222")
|
|
pubkey3 := pubkeyFromString(t, "0x333333333333333333333333333333333333333333333333333333333333333333333333333333333333333333333333")
|
|
pubkey4 := pubkeyFromString(t, "0x444444444444444444444444444444444444444444444444444444444444444444444444444444444444444444444444")
|
|
|
|
// Fee recipients
|
|
feeRecipient1 := feeRecipientFromString(t, "0x0000000000000000000000000000000000000000")
|
|
feeRecipient2 := feeRecipientFromString(t, "0x2222222222222222222222222222222222222222")
|
|
|
|
defaultFeeRecipient := feeRecipientFromString(t, "0xdddddddddddddddddddddddddddddddddddddddd")
|
|
|
|
ctrl := gomock.NewController(t)
|
|
defer ctrl.Finish()
|
|
|
|
ctx := t.Context()
|
|
client := validatormock.NewMockValidatorClient(ctrl)
|
|
|
|
signature := blsmock.NewMockSignature(ctrl)
|
|
signature.EXPECT().Marshal().Return([]byte{}).AnyTimes()
|
|
v := validator{
|
|
signedValidatorRegistrations: map[[48]byte]*ethpb.SignedValidatorRegistrationV1{},
|
|
validatorClient: client,
|
|
proposerSettings: &proposer.Settings{
|
|
DefaultConfig: &proposer.Option{
|
|
FeeRecipientConfig: &proposer.FeeRecipientConfig{
|
|
FeeRecipient: defaultFeeRecipient,
|
|
},
|
|
BuilderConfig: &proposer.BuilderConfig{
|
|
Enabled: true,
|
|
GasLimit: 9999,
|
|
},
|
|
},
|
|
ProposeConfig: map[[48]byte]*proposer.Option{
|
|
pubkey1: {
|
|
FeeRecipientConfig: &proposer.FeeRecipientConfig{
|
|
FeeRecipient: feeRecipient1,
|
|
},
|
|
BuilderConfig: &proposer.BuilderConfig{
|
|
Enabled: true,
|
|
GasLimit: 1111,
|
|
},
|
|
},
|
|
pubkey2: {
|
|
FeeRecipientConfig: &proposer.FeeRecipientConfig{
|
|
FeeRecipient: feeRecipient2,
|
|
},
|
|
BuilderConfig: &proposer.BuilderConfig{
|
|
Enabled: false,
|
|
GasLimit: 2222,
|
|
},
|
|
},
|
|
pubkey3: {
|
|
FeeRecipientConfig: nil,
|
|
BuilderConfig: &proposer.BuilderConfig{
|
|
Enabled: true,
|
|
GasLimit: 3333,
|
|
},
|
|
},
|
|
},
|
|
},
|
|
pubkeyToStatus: make(map[[48]byte]*validatorStatus),
|
|
}
|
|
|
|
pubkeys := [][fieldparams.BLSPubkeyLength]byte{pubkey1, pubkey2, pubkey3}
|
|
|
|
var signer = func(_ context.Context, _ *validatorpb.SignRequest) (bls.Signature, error) {
|
|
return signature, nil
|
|
}
|
|
v.pubkeyToStatus[pubkey1] = &validatorStatus{
|
|
publicKey: pubkey1[:],
|
|
status: ðpb.ValidatorStatusResponse{Status: ethpb.ValidatorStatus_ACTIVE},
|
|
index: 1,
|
|
}
|
|
v.pubkeyToStatus[pubkey2] = &validatorStatus{
|
|
publicKey: pubkey2[:],
|
|
status: ðpb.ValidatorStatusResponse{Status: ethpb.ValidatorStatus_ACTIVE},
|
|
index: 2,
|
|
}
|
|
v.pubkeyToStatus[pubkey3] = &validatorStatus{
|
|
publicKey: pubkey3[:],
|
|
status: ðpb.ValidatorStatusResponse{Status: ethpb.ValidatorStatus_ACTIVE},
|
|
index: 3,
|
|
}
|
|
actual := v.buildSignedRegReqs(ctx, pubkeys, signer, 0, false)
|
|
|
|
assert.Equal(t, 2, len(actual))
|
|
|
|
assert.DeepEqual(t, feeRecipient1[:], actual[0].Message.FeeRecipient)
|
|
assert.Equal(t, uint64(1111), actual[0].Message.GasLimit)
|
|
assert.DeepEqual(t, pubkey1[:], actual[0].Message.Pubkey)
|
|
|
|
assert.DeepEqual(t, defaultFeeRecipient[:], actual[1].Message.FeeRecipient)
|
|
assert.Equal(t, uint64(9999), actual[1].Message.GasLimit)
|
|
assert.DeepEqual(t, pubkey3[:], actual[1].Message.Pubkey)
|
|
|
|
t.Run("mid epoch only pushes newly added key", func(t *testing.T) {
|
|
v.pubkeyToStatus[pubkey4] = &validatorStatus{
|
|
publicKey: pubkey4[:],
|
|
status: ðpb.ValidatorStatusResponse{Status: ethpb.ValidatorStatus_ACTIVE},
|
|
index: 4,
|
|
}
|
|
pubkeys = append(pubkeys, pubkey4)
|
|
actual = v.buildSignedRegReqs(ctx, pubkeys, signer, 5, false)
|
|
assert.Equal(t, 1, len(actual))
|
|
|
|
assert.DeepEqual(t, defaultFeeRecipient[:], actual[0].Message.FeeRecipient)
|
|
assert.Equal(t, uint64(9999), actual[0].Message.GasLimit)
|
|
assert.DeepEqual(t, pubkey4[:], actual[0].Message.Pubkey)
|
|
})
|
|
|
|
t.Run("force push all keys mid epoch", func(t *testing.T) {
|
|
actual = v.buildSignedRegReqs(ctx, pubkeys, signer, 5, true)
|
|
assert.Equal(t, 3, len(actual))
|
|
})
|
|
}
|
|
|
|
func TestValidator_buildSignedRegReqs_SignerOnError(t *testing.T) {
|
|
// Public keys
|
|
pubkey1 := pubkeyFromString(t, "0x111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111")
|
|
|
|
// Fee recipients
|
|
defaultFeeRecipient := feeRecipientFromString(t, "0xdddddddddddddddddddddddddddddddddddddddd")
|
|
|
|
ctrl := gomock.NewController(t)
|
|
defer ctrl.Finish()
|
|
|
|
ctx := t.Context()
|
|
client := validatormock.NewMockValidatorClient(ctrl)
|
|
|
|
v := validator{
|
|
signedValidatorRegistrations: map[[48]byte]*ethpb.SignedValidatorRegistrationV1{},
|
|
validatorClient: client,
|
|
proposerSettings: &proposer.Settings{
|
|
DefaultConfig: &proposer.Option{
|
|
FeeRecipientConfig: &proposer.FeeRecipientConfig{
|
|
FeeRecipient: defaultFeeRecipient,
|
|
},
|
|
BuilderConfig: &proposer.BuilderConfig{
|
|
Enabled: true,
|
|
GasLimit: 9999,
|
|
},
|
|
},
|
|
},
|
|
}
|
|
|
|
pubkeys := [][fieldparams.BLSPubkeyLength]byte{pubkey1}
|
|
|
|
var signer = func(_ context.Context, _ *validatorpb.SignRequest) (bls.Signature, error) {
|
|
return nil, errors.New("custom error")
|
|
}
|
|
|
|
actual := v.buildSignedRegReqs(ctx, pubkeys, signer, 0, false)
|
|
assert.Equal(t, 0, len(actual))
|
|
}
|
|
|
|
func TestValidator_buildSignedRegReqs_TimestampBeforeGenesis(t *testing.T) {
|
|
// Public keys
|
|
pubkey1 := pubkeyFromString(t, "0x111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111")
|
|
|
|
// Fee recipients
|
|
feeRecipient1 := feeRecipientFromString(t, "0x0000000000000000000000000000000000000000")
|
|
|
|
defaultFeeRecipient := feeRecipientFromString(t, "0xdddddddddddddddddddddddddddddddddddddddd")
|
|
|
|
ctrl := gomock.NewController(t)
|
|
defer ctrl.Finish()
|
|
|
|
ctx := t.Context()
|
|
client := validatormock.NewMockValidatorClient(ctrl)
|
|
|
|
signature := blsmock.NewMockSignature(ctrl)
|
|
|
|
v := validator{
|
|
signedValidatorRegistrations: map[[48]byte]*ethpb.SignedValidatorRegistrationV1{},
|
|
validatorClient: client,
|
|
genesisTime: time.Now().Add(1000 * time.Second),
|
|
proposerSettings: &proposer.Settings{
|
|
DefaultConfig: &proposer.Option{
|
|
FeeRecipientConfig: &proposer.FeeRecipientConfig{
|
|
FeeRecipient: defaultFeeRecipient,
|
|
},
|
|
BuilderConfig: &proposer.BuilderConfig{
|
|
Enabled: true,
|
|
GasLimit: 9999,
|
|
},
|
|
},
|
|
ProposeConfig: map[[48]byte]*proposer.Option{
|
|
pubkey1: {
|
|
FeeRecipientConfig: &proposer.FeeRecipientConfig{
|
|
FeeRecipient: feeRecipient1,
|
|
},
|
|
BuilderConfig: &proposer.BuilderConfig{
|
|
Enabled: true,
|
|
GasLimit: 1111,
|
|
},
|
|
},
|
|
},
|
|
},
|
|
pubkeyToStatus: make(map[[48]byte]*validatorStatus),
|
|
}
|
|
|
|
pubkeys := [][fieldparams.BLSPubkeyLength]byte{pubkey1}
|
|
|
|
var signer = func(_ context.Context, _ *validatorpb.SignRequest) (bls.Signature, error) {
|
|
return signature, nil
|
|
}
|
|
v.pubkeyToStatus[pubkey1] = &validatorStatus{
|
|
publicKey: pubkey1[:],
|
|
status: ðpb.ValidatorStatusResponse{Status: ethpb.ValidatorStatus_ACTIVE},
|
|
index: 1,
|
|
}
|
|
actual := v.buildSignedRegReqs(ctx, pubkeys, signer, 0, false)
|
|
assert.Equal(t, 0, len(actual))
|
|
}
|
|
|
|
func TestValidator_Host(t *testing.T) {
|
|
ctrl := gomock.NewController(t)
|
|
defer ctrl.Finish()
|
|
|
|
client := validatormock.NewMockValidatorClient(ctrl)
|
|
v := validator{
|
|
validatorClient: client,
|
|
}
|
|
|
|
client.EXPECT().Host().Return("host").Times(1)
|
|
require.Equal(t, "host", v.Host())
|
|
}
|
|
|
|
func TestValidator_ChangeHost(t *testing.T) {
|
|
// Enable REST API mode for this test since changeHost only calls SwitchHost in REST API mode
|
|
resetCfg := features.InitWithReset(&features.Flags{EnableBeaconRESTApi: true})
|
|
defer resetCfg()
|
|
|
|
ctrl := gomock.NewController(t)
|
|
defer ctrl.Finish()
|
|
|
|
hosts := []string{"http://localhost:8080", "http://localhost:8081"}
|
|
restProvider := &rest.MockRestProvider{MockHosts: hosts}
|
|
conn, err := validatorHelpers.NewNodeConnection(validatorHelpers.WithRestProvider(restProvider))
|
|
require.NoError(t, err)
|
|
|
|
client := validatormock.NewMockValidatorClient(ctrl)
|
|
v := validator{
|
|
validatorClient: client,
|
|
conn: conn,
|
|
currentHostIndex: 0,
|
|
}
|
|
|
|
client.EXPECT().SwitchHost(hosts[1])
|
|
client.EXPECT().SwitchHost(hosts[0])
|
|
v.changeHost()
|
|
assert.Equal(t, uint64(1), v.currentHostIndex)
|
|
v.changeHost()
|
|
assert.Equal(t, uint64(0), v.currentHostIndex)
|
|
}
|
|
|
|
func TestUpdateValidatorStatusCache(t *testing.T) {
|
|
ctx := t.Context()
|
|
ctrl := gomock.NewController(t)
|
|
defer ctrl.Finish()
|
|
pubkeys := [][fieldparams.BLSPubkeyLength]byte{
|
|
{0x01},
|
|
{0x02},
|
|
}
|
|
statusRequestKeys := [][]byte{
|
|
pubkeys[0][:],
|
|
pubkeys[1][:],
|
|
}
|
|
|
|
client := validatormock.NewMockValidatorClient(ctrl)
|
|
mockResponse := ðpb.MultipleValidatorStatusResponse{
|
|
PublicKeys: statusRequestKeys,
|
|
Statuses: []*ethpb.ValidatorStatusResponse{
|
|
{
|
|
Status: ethpb.ValidatorStatus_ACTIVE,
|
|
}, {
|
|
Status: ethpb.ValidatorStatus_EXITING,
|
|
}},
|
|
Indices: []primitives.ValidatorIndex{1, 2},
|
|
}
|
|
client.EXPECT().MultipleValidatorStatus(
|
|
gomock.Any(),
|
|
gomock.Any()).Return(mockResponse, nil)
|
|
|
|
mockProvider := &grpcutil.MockGrpcProvider{MockHosts: []string{"localhost:4000", "localhost:4001"}}
|
|
conn, err := validatorHelpers.NewNodeConnection(validatorHelpers.WithGRPCProvider(mockProvider))
|
|
require.NoError(t, err)
|
|
|
|
v := &validator{
|
|
validatorClient: client,
|
|
conn: conn,
|
|
currentHostIndex: 0,
|
|
pubkeyToStatus: map[[fieldparams.BLSPubkeyLength]byte]*validatorStatus{
|
|
[fieldparams.BLSPubkeyLength]byte{0x03}: { // add non existent key and status to cache, should be fully removed on update
|
|
publicKey: []byte{0x03},
|
|
status: ðpb.ValidatorStatusResponse{
|
|
Status: ethpb.ValidatorStatus_ACTIVE,
|
|
},
|
|
index: 3,
|
|
},
|
|
},
|
|
}
|
|
|
|
err = v.updateValidatorStatusCache(ctx, pubkeys)
|
|
assert.NoError(t, err)
|
|
|
|
// make sure the nonexistent key is fully removed
|
|
_, ok := v.pubkeyToStatus[[fieldparams.BLSPubkeyLength]byte{0x03}]
|
|
require.Equal(t, false, ok)
|
|
// make sure we only have the added values
|
|
assert.Equal(t, 2, len(v.pubkeyToStatus))
|
|
for i, pk := range pubkeys {
|
|
status, exists := v.pubkeyToStatus[pk]
|
|
require.Equal(t, true, exists)
|
|
require.DeepEqual(t, pk[:], status.publicKey)
|
|
require.Equal(t, mockResponse.Statuses[i], status.status)
|
|
require.Equal(t, mockResponse.Indices[i], status.index)
|
|
}
|
|
|
|
err = v.updateValidatorStatusCache(ctx, nil)
|
|
assert.NoError(t, err)
|
|
// make sure the value is 0
|
|
assert.Equal(t, 0, len(v.pubkeyToStatus))
|
|
}
|
|
|
|
func TestValidator_CheckDependentRoots(t *testing.T) {
|
|
ctrl := gomock.NewController(t)
|
|
defer ctrl.Finish()
|
|
|
|
ctx := t.Context()
|
|
client := validatormock.NewMockValidatorClient(ctrl)
|
|
|
|
v := &validator{
|
|
km: newMockKeymanager(t, randKeypair(t)),
|
|
validatorClient: client,
|
|
duties: ðpb.ValidatorDutiesContainer{
|
|
PrevDependentRoot: bytesutil.PadTo([]byte{0x01, 0x02, 0x03}, fieldparams.RootLength),
|
|
CurrDependentRoot: bytesutil.PadTo([]byte{0x04, 0x05, 0x06}, fieldparams.RootLength),
|
|
CurrentEpochDuties: []*ethpb.ValidatorDuty{
|
|
{
|
|
AttesterSlot: params.BeaconConfig().SlotsPerEpoch,
|
|
ValidatorIndex: 200,
|
|
CommitteeIndex: 100,
|
|
CommitteeLength: 4,
|
|
PublicKey: []byte("testPubKey_1"),
|
|
ProposerSlots: []primitives.Slot{params.BeaconConfig().SlotsPerEpoch + 1},
|
|
},
|
|
},
|
|
},
|
|
}
|
|
|
|
t.Run("nil head event", func(t *testing.T) {
|
|
err := v.checkDependentRoots(ctx, nil)
|
|
require.ErrorContains(t, "received empty head event", err)
|
|
})
|
|
|
|
t.Run("invalid previous duty dependent root", func(t *testing.T) {
|
|
head := &structs.HeadEvent{
|
|
Slot: "0",
|
|
PreviousDutyDependentRoot: "invalid_hex",
|
|
}
|
|
err := v.checkDependentRoots(ctx, head)
|
|
require.ErrorContains(t, "failed to decode previous duty dependent root", err)
|
|
})
|
|
|
|
t.Run("invalid current duty dependent root", func(t *testing.T) {
|
|
head := &structs.HeadEvent{
|
|
Slot: "0",
|
|
PreviousDutyDependentRoot: "0x0102030000000000000000000000000000000000000000000000000000000000",
|
|
CurrentDutyDependentRoot: "invalid_hex",
|
|
}
|
|
err := v.checkDependentRoots(ctx, head)
|
|
require.ErrorContains(t, "failed to decode current duty dependent root", err)
|
|
})
|
|
|
|
t.Run("update duties for previous root mismatch", func(t *testing.T) {
|
|
head := &structs.HeadEvent{
|
|
Slot: "1",
|
|
PreviousDutyDependentRoot: "0xe3f7a1b2c489d56f03a6b8d9c7e1fa2456bb09f3de42a67c8910fc3e7a5d4b12",
|
|
CurrentDutyDependentRoot: "0xe3f7a1b2c489d56f03a6b8d9c7e1fa2456bb09f3de42a67c8910fc3e7a5d4b12",
|
|
}
|
|
client.EXPECT().SubscribeCommitteeSubnets(
|
|
gomock.Any(),
|
|
gomock.Any(),
|
|
gomock.Any(),
|
|
).DoAndReturn(func(_ context.Context, _ *ethpb.CommitteeSubnetsSubscribeRequest, _ []*ethpb.ValidatorDuty) (*emptypb.Empty, error) {
|
|
return nil, nil
|
|
}).AnyTimes()
|
|
client.EXPECT().Duties(gomock.Any(), gomock.Any()).Return(v.duties, nil)
|
|
err := v.checkDependentRoots(ctx, head)
|
|
require.NoError(t, err)
|
|
})
|
|
|
|
t.Run("update duties for current root mismatch", func(t *testing.T) {
|
|
head := &structs.HeadEvent{
|
|
Slot: "1",
|
|
PreviousDutyDependentRoot: "0x0102030000000000000000000000000000000000000000000000000000000000",
|
|
CurrentDutyDependentRoot: "0xe3f7a1b2c489d56f03a6b8d9c7e1fa2456bb09f3de42a67c8910fc3e7a5d4b12",
|
|
}
|
|
client.EXPECT().Duties(gomock.Any(), gomock.Any()).Return(v.duties, nil)
|
|
var wg sync.WaitGroup
|
|
wg.Add(1)
|
|
|
|
client.EXPECT().SubscribeCommitteeSubnets(
|
|
gomock.Any(),
|
|
gomock.Any(),
|
|
gomock.Any(),
|
|
).DoAndReturn(func(_ context.Context, _ *ethpb.CommitteeSubnetsSubscribeRequest, _ []*ethpb.ValidatorDuty) (*emptypb.Empty, error) {
|
|
wg.Done()
|
|
return nil, nil
|
|
}).AnyTimes()
|
|
err := v.checkDependentRoots(ctx, head)
|
|
require.NoError(t, err)
|
|
util.WaitTimeout(&wg, 2*time.Second)
|
|
})
|
|
t.Run("no updates needed", func(t *testing.T) {
|
|
head := &structs.HeadEvent{
|
|
Slot: "0",
|
|
PreviousDutyDependentRoot: "0x0102030000000000000000000000000000000000000000000000000000000000",
|
|
CurrentDutyDependentRoot: "0x0405060000000000000000000000000000000000000000000000000000000000",
|
|
}
|
|
curr, err := bytesutil.DecodeHexWithLength(head.CurrentDutyDependentRoot, fieldparams.RootLength)
|
|
require.NoError(t, err)
|
|
require.DeepEqual(t, curr, v.duties.CurrDependentRoot)
|
|
require.NoError(t, v.checkDependentRoots(ctx, head))
|
|
})
|
|
}
|
|
|
|
func TestGetAttestationData_PreElectraNoCaching(t *testing.T) {
|
|
ctrl := gomock.NewController(t)
|
|
defer ctrl.Finish()
|
|
|
|
client := validatormock.NewMockValidatorClient(ctrl)
|
|
v := &validator{validatorClient: client}
|
|
|
|
// Pre-Electra slot (Electra fork epoch is far in the future by default)
|
|
preElectraSlot := primitives.Slot(10)
|
|
|
|
expectedData := ðpb.AttestationData{
|
|
Slot: preElectraSlot,
|
|
CommitteeIndex: 5,
|
|
BeaconBlockRoot: bytesutil.PadTo([]byte("root"), 32),
|
|
Source: ðpb.Checkpoint{Epoch: 1, Root: bytesutil.PadTo([]byte("source"), 32)},
|
|
Target: ðpb.Checkpoint{Epoch: 2, Root: bytesutil.PadTo([]byte("target"), 32)},
|
|
}
|
|
|
|
// Each call should go to the beacon node (no caching pre-Electra)
|
|
client.EXPECT().AttestationData(gomock.Any(), ðpb.AttestationDataRequest{
|
|
Slot: preElectraSlot,
|
|
CommitteeIndex: 5,
|
|
}).Return(expectedData, nil)
|
|
client.EXPECT().AttestationData(gomock.Any(), ðpb.AttestationDataRequest{
|
|
Slot: preElectraSlot,
|
|
CommitteeIndex: 7,
|
|
}).Return(expectedData, nil)
|
|
|
|
// First call with committee index 5
|
|
data1, err := v.getAttestationData(context.Background(), preElectraSlot, 5)
|
|
require.NoError(t, err)
|
|
require.DeepEqual(t, expectedData, data1)
|
|
|
|
// Second call with different committee index 7 - should still call beacon node
|
|
data2, err := v.getAttestationData(context.Background(), preElectraSlot, 7)
|
|
require.NoError(t, err)
|
|
require.DeepEqual(t, expectedData, data2)
|
|
}
|
|
|
|
func TestGetAttestationData_PostElectraCaching(t *testing.T) {
|
|
ctrl := gomock.NewController(t)
|
|
defer ctrl.Finish()
|
|
|
|
// Set up Electra fork epoch for this test
|
|
cfg := params.BeaconConfig().Copy()
|
|
originalElectraForkEpoch := cfg.ElectraForkEpoch
|
|
cfg.ElectraForkEpoch = 1
|
|
params.OverrideBeaconConfig(cfg)
|
|
defer func() {
|
|
cfg.ElectraForkEpoch = originalElectraForkEpoch
|
|
params.OverrideBeaconConfig(cfg)
|
|
}()
|
|
|
|
client := validatormock.NewMockValidatorClient(ctrl)
|
|
v := &validator{validatorClient: client}
|
|
|
|
// Post-Electra slot
|
|
postElectraSlot := primitives.Slot(params.BeaconConfig().SlotsPerEpoch + 5)
|
|
|
|
expectedData := ðpb.AttestationData{
|
|
Slot: postElectraSlot,
|
|
CommitteeIndex: 0,
|
|
BeaconBlockRoot: bytesutil.PadTo([]byte("root"), 32),
|
|
Source: ðpb.Checkpoint{Epoch: 1, Root: bytesutil.PadTo([]byte("source"), 32)},
|
|
Target: ðpb.Checkpoint{Epoch: 2, Root: bytesutil.PadTo([]byte("target"), 32)},
|
|
}
|
|
|
|
// Only ONE call should go to the beacon node (caching post-Electra)
|
|
client.EXPECT().AttestationData(gomock.Any(), ðpb.AttestationDataRequest{
|
|
Slot: postElectraSlot,
|
|
CommitteeIndex: 0,
|
|
}).Return(expectedData, nil).Times(1)
|
|
|
|
// First call - should hit beacon node
|
|
data1, err := v.getAttestationData(context.Background(), postElectraSlot, 5)
|
|
require.NoError(t, err)
|
|
require.DeepEqual(t, expectedData, data1)
|
|
|
|
// Second call with different committee index - should use cache
|
|
data2, err := v.getAttestationData(context.Background(), postElectraSlot, 7)
|
|
require.NoError(t, err)
|
|
require.DeepEqual(t, expectedData, data2)
|
|
|
|
// Third call - should still use cache
|
|
data3, err := v.getAttestationData(context.Background(), postElectraSlot, 10)
|
|
require.NoError(t, err)
|
|
require.DeepEqual(t, expectedData, data3)
|
|
}
|
|
|
|
func TestGetAttestationData_PostElectraCacheInvalidatesOnNewSlot(t *testing.T) {
|
|
ctrl := gomock.NewController(t)
|
|
defer ctrl.Finish()
|
|
|
|
// Set up Electra fork epoch for this test
|
|
cfg := params.BeaconConfig().Copy()
|
|
originalElectraForkEpoch := cfg.ElectraForkEpoch
|
|
cfg.ElectraForkEpoch = 1
|
|
params.OverrideBeaconConfig(cfg)
|
|
defer func() {
|
|
cfg.ElectraForkEpoch = originalElectraForkEpoch
|
|
params.OverrideBeaconConfig(cfg)
|
|
}()
|
|
|
|
client := validatormock.NewMockValidatorClient(ctrl)
|
|
v := &validator{validatorClient: client}
|
|
|
|
slot1 := primitives.Slot(params.BeaconConfig().SlotsPerEpoch + 5)
|
|
slot2 := primitives.Slot(params.BeaconConfig().SlotsPerEpoch + 6)
|
|
|
|
dataSlot1 := ðpb.AttestationData{
|
|
Slot: slot1,
|
|
CommitteeIndex: 0,
|
|
BeaconBlockRoot: bytesutil.PadTo([]byte("root1"), 32),
|
|
Source: ðpb.Checkpoint{Epoch: 1, Root: bytesutil.PadTo([]byte("source"), 32)},
|
|
Target: ðpb.Checkpoint{Epoch: 2, Root: bytesutil.PadTo([]byte("target"), 32)},
|
|
}
|
|
dataSlot2 := ðpb.AttestationData{
|
|
Slot: slot2,
|
|
CommitteeIndex: 0,
|
|
BeaconBlockRoot: bytesutil.PadTo([]byte("root2"), 32),
|
|
Source: ðpb.Checkpoint{Epoch: 1, Root: bytesutil.PadTo([]byte("source"), 32)},
|
|
Target: ðpb.Checkpoint{Epoch: 2, Root: bytesutil.PadTo([]byte("target"), 32)},
|
|
}
|
|
|
|
// Expect one call per slot
|
|
client.EXPECT().AttestationData(gomock.Any(), ðpb.AttestationDataRequest{
|
|
Slot: slot1,
|
|
CommitteeIndex: 0,
|
|
}).Return(dataSlot1, nil).Times(1)
|
|
client.EXPECT().AttestationData(gomock.Any(), ðpb.AttestationDataRequest{
|
|
Slot: slot2,
|
|
CommitteeIndex: 0,
|
|
}).Return(dataSlot2, nil).Times(1)
|
|
|
|
// First slot - should hit beacon node
|
|
data1, err := v.getAttestationData(context.Background(), slot1, 5)
|
|
require.NoError(t, err)
|
|
require.DeepEqual(t, dataSlot1, data1)
|
|
|
|
// Same slot - should use cache
|
|
data1Again, err := v.getAttestationData(context.Background(), slot1, 7)
|
|
require.NoError(t, err)
|
|
require.DeepEqual(t, dataSlot1, data1Again)
|
|
|
|
// New slot - should invalidate cache and hit beacon node
|
|
data2, err := v.getAttestationData(context.Background(), slot2, 5)
|
|
require.NoError(t, err)
|
|
require.DeepEqual(t, dataSlot2, data2)
|
|
}
|
|
|
|
func TestGetAttestationData_PostElectraConcurrentAccess(t *testing.T) {
|
|
ctrl := gomock.NewController(t)
|
|
defer ctrl.Finish()
|
|
|
|
// Set up Electra fork epoch for this test
|
|
cfg := params.BeaconConfig().Copy()
|
|
originalElectraForkEpoch := cfg.ElectraForkEpoch
|
|
cfg.ElectraForkEpoch = 1
|
|
params.OverrideBeaconConfig(cfg)
|
|
defer func() {
|
|
cfg.ElectraForkEpoch = originalElectraForkEpoch
|
|
params.OverrideBeaconConfig(cfg)
|
|
}()
|
|
|
|
client := validatormock.NewMockValidatorClient(ctrl)
|
|
v := &validator{validatorClient: client}
|
|
|
|
postElectraSlot := primitives.Slot(params.BeaconConfig().SlotsPerEpoch + 5)
|
|
|
|
expectedData := ðpb.AttestationData{
|
|
Slot: postElectraSlot,
|
|
CommitteeIndex: 0,
|
|
BeaconBlockRoot: bytesutil.PadTo([]byte("root"), 32),
|
|
Source: ðpb.Checkpoint{Epoch: 1, Root: bytesutil.PadTo([]byte("source"), 32)},
|
|
Target: ðpb.Checkpoint{Epoch: 2, Root: bytesutil.PadTo([]byte("target"), 32)},
|
|
}
|
|
|
|
// Should only call beacon node once despite concurrent requests
|
|
client.EXPECT().AttestationData(gomock.Any(), ðpb.AttestationDataRequest{
|
|
Slot: postElectraSlot,
|
|
CommitteeIndex: 0,
|
|
}).Return(expectedData, nil).Times(1)
|
|
|
|
var wg sync.WaitGroup
|
|
numGoroutines := 10
|
|
results := make([]*ethpb.AttestationData, numGoroutines)
|
|
errs := make([]error, numGoroutines)
|
|
|
|
for i := range numGoroutines {
|
|
wg.Add(1)
|
|
go func(idx int) {
|
|
defer wg.Done()
|
|
results[idx], errs[idx] = v.getAttestationData(context.Background(), postElectraSlot, primitives.CommitteeIndex(idx))
|
|
}(i)
|
|
}
|
|
|
|
wg.Wait()
|
|
|
|
for i := range numGoroutines {
|
|
require.NoError(t, errs[i])
|
|
require.DeepEqual(t, expectedData, results[i])
|
|
}
|
|
}
|