Validator RPC: add optimistic status check (#10291)

* Add optimistic status check

* Simplify a bit more

* Update status_test.go

* Add non opt tests

* Update aggregator_test.go

* More tests

* Preston's feedback
This commit is contained in:
terence tsao
2022-03-02 16:16:34 -08:00
committed by GitHub
parent 958dd9d783
commit 7303985232
11 changed files with 173 additions and 3 deletions

View File

@@ -154,6 +154,8 @@ go_test(
"@com_github_prysmaticlabs_go_bitfield//:go_default_library",
"@com_github_sirupsen_logrus//:go_default_library",
"@com_github_sirupsen_logrus//hooks/test:go_default_library",
"@org_golang_google_grpc//codes:go_default_library",
"@org_golang_google_grpc//status:go_default_library",
"@org_golang_google_protobuf//proto:go_default_library",
"@org_golang_google_protobuf//types/known/emptypb:go_default_library",
],

View File

@@ -28,6 +28,11 @@ func (vs *Server) SubmitAggregateSelectionProof(ctx context.Context, req *ethpb.
return nil, status.Errorf(codes.Unavailable, "Syncing to latest head, not ready to respond")
}
// An optimistic validator MUST NOT participate in attestation. (i.e., sign across the DOMAIN_BEACON_ATTESTER, DOMAIN_SELECTION_PROOF or DOMAIN_AGGREGATE_AND_PROOF domains).
if err := vs.optimisticStatus(ctx); err != nil {
return nil, err
}
st, err := vs.HeadFetcher.HeadState(ctx)
if err != nil {
return nil, status.Errorf(codes.Internal, "Could not determine head state: %v", err)

View File

@@ -55,6 +55,7 @@ func TestSubmitAggregateAndProof_CantFindValidatorIndex(t *testing.T) {
server := &Server{
HeadFetcher: &mock.ChainService{State: s},
SyncChecker: &mockSync.Sync{IsSyncing: false},
TimeFetcher: &mock.ChainService{Genesis: time.Now()},
}
priv, err := bls.RandKey()
@@ -82,6 +83,7 @@ func TestSubmitAggregateAndProof_IsAggregatorAndNoAtts(t *testing.T) {
HeadFetcher: &mock.ChainService{State: s},
SyncChecker: &mockSync.Sync{IsSyncing: false},
AttPool: attestations.NewPool(),
TimeFetcher: &mock.ChainService{Genesis: time.Now()},
}
priv, err := bls.RandKey()
@@ -115,6 +117,7 @@ func TestSubmitAggregateAndProof_UnaggregateOk(t *testing.T) {
SyncChecker: &mockSync.Sync{IsSyncing: false},
AttPool: attestations.NewPool(),
P2P: &mockp2p.MockBroadcaster{},
TimeFetcher: &mock.ChainService{Genesis: time.Now()},
}
priv, err := bls.RandKey()
@@ -152,6 +155,7 @@ func TestSubmitAggregateAndProof_AggregateOk(t *testing.T) {
SyncChecker: &mockSync.Sync{IsSyncing: false},
AttPool: attestations.NewPool(),
P2P: &mockp2p.MockBroadcaster{},
TimeFetcher: &mock.ChainService{Genesis: time.Now()},
}
priv, err := bls.RandKey()
@@ -191,6 +195,7 @@ func TestSubmitAggregateAndProof_AggregateNotOk(t *testing.T) {
SyncChecker: &mockSync.Sync{IsSyncing: false},
AttPool: attestations.NewPool(),
P2P: &mockp2p.MockBroadcaster{},
TimeFetcher: &mock.ChainService{Genesis: time.Now()},
}
priv, err := bls.RandKey()
@@ -319,6 +324,7 @@ func TestSubmitAggregateAndProof_PreferOwnAttestation(t *testing.T) {
SyncChecker: &mockSync.Sync{IsSyncing: false},
AttPool: attestations.NewPool(),
P2P: &mockp2p.MockBroadcaster{},
TimeFetcher: &mock.ChainService{Genesis: time.Now()},
}
priv, err := bls.RandKey()
@@ -369,6 +375,7 @@ func TestSubmitAggregateAndProof_SelectsMostBitsWhenOwnAttestationNotPresent(t *
SyncChecker: &mockSync.Sync{IsSyncing: false},
AttPool: attestations.NewPool(),
P2P: &mockp2p.MockBroadcaster{},
TimeFetcher: &mock.ChainService{Genesis: time.Now()},
}
priv, err := bls.RandKey()

View File

@@ -37,6 +37,11 @@ func (vs *Server) GetAttestationData(ctx context.Context, req *ethpb.Attestation
return nil, status.Errorf(codes.Unavailable, "Syncing to latest head, not ready to respond")
}
// An optimistic validator MUST NOT participate in attestation. (i.e., sign across the DOMAIN_BEACON_ATTESTER, DOMAIN_SELECTION_PROOF or DOMAIN_AGGREGATE_AND_PROOF domains).
if err := vs.optimisticStatus(ctx); err != nil {
return nil, err
}
if err := helpers.ValidateAttestationTime(req.Slot, vs.TimeFetcher.GenesisTime(),
params.BeaconNetworkConfig().MaximumGossipClockDisparity); err != nil {
return nil, status.Error(codes.InvalidArgument, fmt.Sprintf("invalid request: %v", err))

View File

@@ -26,6 +26,8 @@ import (
"github.com/prysmaticlabs/prysm/testing/util"
prysmTime "github.com/prysmaticlabs/prysm/time"
"github.com/prysmaticlabs/prysm/time/slots"
"google.golang.org/grpc/codes"
"google.golang.org/grpc/status"
"google.golang.org/protobuf/proto"
)
@@ -169,6 +171,35 @@ func TestGetAttestationData_SyncNotReady(t *testing.T) {
assert.ErrorContains(t, "Syncing to latest head", err)
}
func TestGetAttestationData_Optimistic(t *testing.T) {
params.SetupTestConfigCleanup(t)
cfg := params.BeaconConfig()
cfg.BellatrixForkEpoch = 0
params.OverrideBeaconConfig(cfg)
as := &Server{
SyncChecker: &mockSync.Sync{},
TimeFetcher: &mock.ChainService{Genesis: time.Now()},
HeadFetcher: &mock.ChainService{Optimistic: true},
}
_, err := as.GetAttestationData(context.Background(), &ethpb.AttestationDataRequest{})
s, ok := status.FromError(err)
require.Equal(t, true, ok)
require.DeepEqual(t, codes.Unavailable, s.Code())
require.ErrorContains(t, errOptimisticMode.Error(), err)
beaconState, err := util.NewBeaconState()
require.NoError(t, err)
as = &Server{
SyncChecker: &mockSync.Sync{},
TimeFetcher: &mock.ChainService{Genesis: time.Now()},
HeadFetcher: &mock.ChainService{Optimistic: false, State: beaconState},
AttestationCache: cache.NewAttestationCache(),
}
_, err = as.GetAttestationData(context.Background(), &ethpb.AttestationDataRequest{})
require.NoError(t, err)
}
func TestAttestationDataAtSlot_HandlesFarAwayJustifiedEpoch(t *testing.T) {
// Scenario:
//
@@ -317,6 +348,7 @@ func TestServer_GetAttestationData_InvalidRequestSlot(t *testing.T) {
offset := int64(slot.Mul(params.BeaconConfig().SecondsPerSlot))
attesterServer := &Server{
SyncChecker: &mockSync.Sync{IsSyncing: false},
HeadFetcher: &mock.ChainService{},
TimeFetcher: &mock.ChainService{Genesis: time.Now().Add(time.Duration(-1*offset) * time.Second)},
}

View File

@@ -48,6 +48,11 @@ func (vs *Server) GetBeaconBlock(ctx context.Context, req *ethpb.BlockRequest) (
return &ethpb.GenericBeaconBlock{Block: &ethpb.GenericBeaconBlock_Altair{Altair: blk}}, nil
}
// An optimistic validator MUST NOT produce a block (i.e., sign across the DOMAIN_BEACON_PROPOSER domain).
if err := vs.optimisticStatus(ctx); err != nil {
return nil, err
}
blk, err := vs.getBellatrixBeaconBlock(ctx, req)
if err != nil {
return nil, status.Errorf(codes.Internal, "Could not fetch Bellatrix beacon block: %v", err)

View File

@@ -4,6 +4,7 @@ import (
"context"
"math/big"
"testing"
"time"
types "github.com/prysmaticlabs/eth2-types"
"github.com/prysmaticlabs/go-bitfield"
@@ -12,7 +13,7 @@ import (
b "github.com/prysmaticlabs/prysm/beacon-chain/core/blocks"
"github.com/prysmaticlabs/prysm/beacon-chain/core/helpers"
"github.com/prysmaticlabs/prysm/beacon-chain/core/signing"
"github.com/prysmaticlabs/prysm/beacon-chain/core/time"
coretime "github.com/prysmaticlabs/prysm/beacon-chain/core/time"
dbutil "github.com/prysmaticlabs/prysm/beacon-chain/db/testing"
"github.com/prysmaticlabs/prysm/beacon-chain/operations/attestations"
"github.com/prysmaticlabs/prysm/beacon-chain/operations/slashings"
@@ -39,6 +40,8 @@ import (
"github.com/prysmaticlabs/prysm/testing/require"
"github.com/prysmaticlabs/prysm/testing/util"
"github.com/prysmaticlabs/prysm/time/slots"
"google.golang.org/grpc/codes"
"google.golang.org/grpc/status"
"google.golang.org/protobuf/proto"
)
@@ -320,7 +323,7 @@ func TestProposer_ComputeStateRoot_OK(t *testing.T) {
require.NoError(t, err)
require.NoError(t, beaconState.SetSlot(beaconState.Slot()-1))
req.Block.Body.RandaoReveal = randaoReveal
currentEpoch := time.CurrentEpoch(beaconState)
currentEpoch := coretime.CurrentEpoch(beaconState)
req.Signature, err = signing.ComputeDomainAndSign(beaconState, currentEpoch, req.Block, params.BeaconConfig().DomainBeaconProposer, privKeys[proposerIdx])
require.NoError(t, err)
@@ -2284,7 +2287,8 @@ func TestProposer_GetBeaconBlock_BellatrixEpoch(t *testing.T) {
require.NoError(t, db.SaveHeadBlockRoot(ctx, blkRoot), "Could not save genesis state")
proposerServer := &Server{
HeadFetcher: &mock.ChainService{State: beaconState, Root: parentRoot[:]},
HeadFetcher: &mock.ChainService{State: beaconState, Root: parentRoot[:], Optimistic: false},
TimeFetcher: &mock.ChainService{Genesis: time.Now()},
SyncChecker: &mockSync.Sync{IsSyncing: false},
BlockReceiver: &mock.ChainService{},
ChainStartFetcher: &mockPOW.POWChain{},
@@ -2320,6 +2324,27 @@ func TestProposer_GetBeaconBlock_BellatrixEpoch(t *testing.T) {
assert.DeepEqual(t, req.Graffiti, bellatrixBlk.Bellatrix.Body.Graffiti, "Expected block to have correct Graffiti")
}
func TestProposer_GetBeaconBlock_Optimistic(t *testing.T) {
params.SetupTestConfigCleanup(t)
cfg := params.MainnetConfig().Copy()
cfg.BellatrixForkEpoch = 2
cfg.AltairForkEpoch = 1
params.OverrideBeaconConfig(cfg)
bellatrixSlot, err := slots.EpochStart(params.BeaconConfig().BellatrixForkEpoch)
require.NoError(t, err)
proposerServer := &Server{HeadFetcher: &mock.ChainService{Optimistic: true}, TimeFetcher: &mock.ChainService{}}
req := &ethpb.BlockRequest{
Slot: bellatrixSlot + 1,
}
_, err = proposerServer.GetBeaconBlock(context.Background(), req)
s, ok := status.FromError(err)
require.Equal(t, true, ok)
require.DeepEqual(t, codes.Unavailable, s.Code())
require.ErrorContains(t, errOptimisticMode.Error(), err)
}
func TestProposer_GetSyncAggregate_OK(t *testing.T) {
proposerServer := &Server{
SyncChecker: &mockSync.Sync{IsSyncing: false},

View File

@@ -23,6 +23,7 @@ import (
)
var errPubkeyDoesNotExist = errors.New("pubkey does not exist")
var errOptimisticMode = errors.New("the node is currently optimistic and cannot serve validators")
var nonExistentIndex = types.ValidatorIndex(^uint64(0))
const numStatesToCheck = 2
@@ -237,6 +238,29 @@ func (vs *Server) activationStatus(
return activeValidatorExists, statusResponses, nil
}
// optimisticStatus returns an error if the node is currently optimistic with respect to head.
// by definition, an optimistic node is not a full node. It is unable to produce blocks,
// since an execution engine cannot produce a payload upon an unknown parent.
// It cannot faithfully attest to the head block of the chain, since it has not fully verified that block.
//
// Spec:
// https://github.com/ethereum/consensus-specs/blob/dev/sync/optimistic.md
func (vs *Server) optimisticStatus(ctx context.Context) error {
if slots.ToEpoch(vs.TimeFetcher.CurrentSlot()) < params.BeaconConfig().BellatrixForkEpoch {
return nil
}
optimistic, err := vs.HeadFetcher.IsOptimistic(ctx)
if err != nil {
return status.Errorf(codes.Internal, "Could not determine if the node is a optimistic node: %v", err)
}
if !optimistic {
return nil
}
return status.Errorf(codes.Unavailable, errOptimisticMode.Error())
}
// validatorStatus searches for the requested validator's state and deposit to retrieve its inclusion estimate. Also returns the validators index.
func (vs *Server) validatorStatus(
ctx context.Context,

View File

@@ -27,6 +27,8 @@ import (
"github.com/prysmaticlabs/prysm/testing/require"
"github.com/prysmaticlabs/prysm/testing/util"
"github.com/prysmaticlabs/prysm/time/slots"
"google.golang.org/grpc/codes"
"google.golang.org/grpc/status"
"google.golang.org/protobuf/proto"
)
@@ -582,6 +584,28 @@ func TestActivationStatus_OK(t *testing.T) {
}
}
func TestOptimisticStatus(t *testing.T) {
server := &Server{HeadFetcher: &mockChain.ChainService{}, TimeFetcher: &mockChain.ChainService{}}
err := server.optimisticStatus(context.Background())
require.NoError(t, err)
params.SetupTestConfigCleanup(t)
cfg := params.BeaconConfig()
cfg.BellatrixForkEpoch = 2
params.OverrideBeaconConfig(cfg)
server = &Server{HeadFetcher: &mockChain.ChainService{Optimistic: true}, TimeFetcher: &mockChain.ChainService{}}
err = server.optimisticStatus(context.Background())
s, ok := status.FromError(err)
require.Equal(t, true, ok)
require.DeepEqual(t, codes.Unavailable, s.Code())
require.ErrorContains(t, errOptimisticMode.Error(), err)
server = &Server{HeadFetcher: &mockChain.ChainService{Optimistic: false}, TimeFetcher: &mockChain.ChainService{}}
err = server.optimisticStatus(context.Background())
require.NoError(t, err)
}
func TestValidatorStatus_CorrectActivationQueue(t *testing.T) {
ctx := context.Background()

View File

@@ -22,6 +22,12 @@ import (
func (vs *Server) GetSyncMessageBlockRoot(
ctx context.Context, _ *emptypb.Empty,
) (*ethpb.SyncMessageBlockRootResponse, error) {
// An optimistic validator MUST NOT participate in sync committees
// (i.e., sign across the DOMAIN_SYNC_COMMITTEE, DOMAIN_SYNC_COMMITTEE_SELECTION_PROOF or DOMAIN_CONTRIBUTION_AND_PROOF domains).
if err := vs.optimisticStatus(ctx); err != nil {
return nil, err
}
r, err := vs.HeadFetcher.HeadRoot(ctx)
if err != nil {
return nil, status.Errorf(codes.Internal, "Could not retrieve head root: %v", err)
@@ -81,6 +87,12 @@ func (vs *Server) GetSyncSubcommitteeIndex(
func (vs *Server) GetSyncCommitteeContribution(
ctx context.Context, req *ethpb.SyncCommitteeContributionRequest,
) (*ethpb.SyncCommitteeContribution, error) {
// An optimistic validator MUST NOT participate in sync committees
// (i.e., sign across the DOMAIN_SYNC_COMMITTEE, DOMAIN_SYNC_COMMITTEE_SELECTION_PROOF or DOMAIN_CONTRIBUTION_AND_PROOF domains).
if err := vs.optimisticStatus(ctx); err != nil {
return nil, err
}
msgs, err := vs.SyncCommitteePool.SyncCommitteeMessages(req.Slot)
if err != nil {
return nil, status.Errorf(codes.Internal, "Could not get sync subcommittee messages: %v", err)

View File

@@ -3,6 +3,7 @@ package validator
import (
"context"
"testing"
"time"
types "github.com/prysmaticlabs/eth2-types"
mock "github.com/prysmaticlabs/prysm/beacon-chain/blockchain/testing"
@@ -18,6 +19,8 @@ import (
"github.com/prysmaticlabs/prysm/testing/assert"
"github.com/prysmaticlabs/prysm/testing/require"
"github.com/prysmaticlabs/prysm/testing/util"
"google.golang.org/grpc/codes"
"google.golang.org/grpc/status"
"google.golang.org/protobuf/types/known/emptypb"
)
@@ -25,12 +28,37 @@ func TestGetSyncMessageBlockRoot_OK(t *testing.T) {
r := []byte{'a'}
server := &Server{
HeadFetcher: &mock.ChainService{Root: r},
TimeFetcher: &mock.ChainService{Genesis: time.Now()},
}
res, err := server.GetSyncMessageBlockRoot(context.Background(), &emptypb.Empty{})
require.NoError(t, err)
require.DeepEqual(t, r, res.Root)
}
func TestGetSyncMessageBlockRoot_Optimistic(t *testing.T) {
params.SetupTestConfigCleanup(t)
cfg := params.BeaconConfig()
cfg.BellatrixForkEpoch = 0
params.OverrideBeaconConfig(cfg)
server := &Server{
HeadFetcher: &mock.ChainService{Optimistic: true},
TimeFetcher: &mock.ChainService{Genesis: time.Now()},
}
_, err := server.GetSyncMessageBlockRoot(context.Background(), &emptypb.Empty{})
s, ok := status.FromError(err)
require.Equal(t, true, ok)
require.DeepEqual(t, codes.Unavailable, s.Code())
require.ErrorContains(t, errOptimisticMode.Error(), err)
server = &Server{
HeadFetcher: &mock.ChainService{Optimistic: false},
TimeFetcher: &mock.ChainService{Genesis: time.Now()},
}
_, err = server.GetSyncMessageBlockRoot(context.Background(), &emptypb.Empty{})
require.NoError(t, err)
}
func TestSubmitSyncMessage_OK(t *testing.T) {
st, _ := util.DeterministicGenesisStateAltair(t, 10)
server := &Server{
@@ -80,6 +108,7 @@ func TestGetSyncCommitteeContribution_FiltersDuplicates(t *testing.T) {
State: st,
SyncCommitteeIndices: []types.CommitteeIndex{10},
},
TimeFetcher: &mock.ChainService{Genesis: time.Now()},
}
secKey, err := bls.RandKey()
require.NoError(t, err)