Validator: propose Altair block (#9414)

* Validator can propose block

* Gazelle

* Preston's feedback

* Rename

* Fix deep source

* Fix build

Co-authored-by: Raul Jordan <raul@prysmaticlabs.com>
Co-authored-by: Preston Van Loon <preston@prysmaticlabs.com>
This commit is contained in:
terence tsao
2021-09-03 11:26:10 -07:00
committed by GitHub
parent cc790ceb2e
commit ebf3897017
3 changed files with 393 additions and 22 deletions

View File

@@ -43,6 +43,7 @@ go_library(
"//shared/slotutil:go_default_library",
"//shared/timeutils:go_default_library",
"//shared/traceutil:go_default_library",
"//shared/version:go_default_library",
"//validator/accounts/iface:go_default_library",
"//validator/accounts/wallet:go_default_library",
"//validator/client/iface:go_default_library",

View File

@@ -10,14 +10,16 @@ import (
types "github.com/prysmaticlabs/eth2-types"
"github.com/prysmaticlabs/prysm/beacon-chain/core/helpers"
ethpb "github.com/prysmaticlabs/prysm/proto/prysm/v1alpha1"
"github.com/prysmaticlabs/prysm/proto/prysm/v1alpha1/block"
validatorpb "github.com/prysmaticlabs/prysm/proto/prysm/v1alpha1/validator-client"
wrapperv1 "github.com/prysmaticlabs/prysm/proto/prysm/v1alpha1/wrapper"
wrapper "github.com/prysmaticlabs/prysm/proto/prysm/v1alpha1/wrapper"
"github.com/prysmaticlabs/prysm/shared/bls"
"github.com/prysmaticlabs/prysm/shared/bytesutil"
"github.com/prysmaticlabs/prysm/shared/mputil"
"github.com/prysmaticlabs/prysm/shared/params"
"github.com/prysmaticlabs/prysm/shared/rand"
"github.com/prysmaticlabs/prysm/shared/timeutils"
"github.com/prysmaticlabs/prysm/shared/version"
"github.com/prysmaticlabs/prysm/validator/client/iface"
"github.com/sirupsen/logrus"
"go.opencensus.io/trace"
@@ -36,6 +38,16 @@ const signExitErr = "could not sign voluntary exit proposal"
// the state root computation, and finally signed by the validator before being
// sent back to the beacon node for broadcasting.
func (v *validator) ProposeBlock(ctx context.Context, slot types.Slot, pubKey [48]byte) {
currEpoch := helpers.SlotToEpoch(slot)
switch {
case currEpoch >= params.BeaconConfig().AltairForkEpoch:
v.proposeBlockAltair(ctx, slot, pubKey)
default:
v.proposeBlockPhase0(ctx, slot, pubKey)
}
}
func (v *validator) proposeBlockPhase0(ctx context.Context, slot types.Slot, pubKey [48]byte) {
if slot == 0 {
log.Debug("Assigned to genesis slot, skipping proposal")
return
@@ -43,7 +55,7 @@ func (v *validator) ProposeBlock(ctx context.Context, slot types.Slot, pubKey [4
lock := mputil.NewMultilock(fmt.Sprint(iface.RoleProposer), string(pubKey[:]))
lock.Lock()
defer lock.Unlock()
ctx, span := trace.StartSpan(ctx, "validator.ProposeBlock")
ctx, span := trace.StartSpan(ctx, "validator.proposeBlockPhase0")
defer span.End()
fmtKey := fmt.Sprintf("%#x", pubKey[:])
@@ -84,7 +96,7 @@ func (v *validator) ProposeBlock(ctx context.Context, slot types.Slot, pubKey [4
}
// Sign returned block from beacon node
sig, domain, err := v.signBlock(ctx, pubKey, epoch, b)
sig, domain, err := v.signBlock(ctx, pubKey, epoch, wrapper.WrappedPhase0BeaconBlock(b))
if err != nil {
log.WithError(err).Error("Failed to sign block")
if v.emitAccountMetrics {
@@ -106,16 +118,16 @@ func (v *validator) ProposeBlock(ctx context.Context, slot types.Slot, pubKey [4
return
}
if err := v.preBlockSignValidations(ctx, pubKey, wrapperv1.WrappedPhase0BeaconBlock(b), signingRoot); err != nil {
if err := v.preBlockSignValidations(ctx, pubKey, wrapper.WrappedPhase0BeaconBlock(b), signingRoot); err != nil {
log.WithFields(
blockLogFields(pubKey, wrapperv1.WrappedPhase0BeaconBlock(b), nil),
blockLogFields(pubKey, wrapper.WrappedPhase0BeaconBlock(b), nil),
).WithError(err).Error("Failed block slashing protection check")
return
}
if err := v.postBlockSignUpdate(ctx, pubKey, wrapperv1.WrappedPhase0SignedBeaconBlock(blk), signingRoot); err != nil {
if err := v.postBlockSignUpdate(ctx, pubKey, wrapper.WrappedPhase0SignedBeaconBlock(blk), signingRoot); err != nil {
log.WithFields(
blockLogFields(pubKey, wrapperv1.WrappedPhase0BeaconBlock(b), sig),
blockLogFields(pubKey, wrapper.WrappedPhase0BeaconBlock(b), sig),
).WithError(err).Error("Failed block slashing protection check")
return
}
@@ -150,6 +162,146 @@ func (v *validator) ProposeBlock(ctx context.Context, slot types.Slot, pubKey [4
}
}
// This is a routine to propose altair compatible beacon blocks.
func (v *validator) proposeBlockAltair(ctx context.Context, slot types.Slot, pubKey [48]byte) {
if slot == 0 {
log.Debug("Assigned to genesis slot, skipping proposal")
return
}
ctx, span := trace.StartSpan(ctx, "validator.proposeBlockAltair")
defer span.End()
lock := mputil.NewMultilock(fmt.Sprint(iface.RoleProposer), string(pubKey[:]))
lock.Lock()
defer lock.Unlock()
fmtKey := fmt.Sprintf("%#x", pubKey[:])
span.AddAttributes(trace.StringAttribute("validator", fmt.Sprintf("%#x", pubKey)))
log := log.WithField("pubKey", fmt.Sprintf("%#x", bytesutil.Trunc(pubKey[:])))
// Sign randao reveal, it's used to request block from beacon node
epoch := types.Epoch(slot / params.BeaconConfig().SlotsPerEpoch)
randaoReveal, err := v.signRandaoReveal(ctx, pubKey, epoch)
if err != nil {
log.WithError(err).Error("Failed to sign randao reveal")
if v.emitAccountMetrics {
ValidatorProposeFailVec.WithLabelValues(fmtKey).Inc()
}
return
}
g, err := v.getGraffiti(ctx, pubKey)
if err != nil {
// Graffiti is not a critical enough to fail block production and cause
// validator to miss block reward. When failed, validator should continue
// to produce the block.
log.WithError(err).Warn("Could not get graffiti")
}
// Request block from beacon node
b, err := v.validatorClient.GetBlockAltair(ctx, &ethpb.BlockRequest{
Slot: slot,
RandaoReveal: randaoReveal,
Graffiti: g,
})
if err != nil {
log.WithField("blockSlot", slot).WithError(err).Error("Failed to request block from beacon node")
if v.emitAccountMetrics {
ValidatorProposeFailVec.WithLabelValues(fmtKey).Inc()
}
return
}
// Sign returned block from beacon node
wb, err := wrapper.WrappedAltairBeaconBlock(b)
if err != nil {
log.WithError(err).Error("Failed to wrap block")
if v.emitAccountMetrics {
ValidatorProposeFailVec.WithLabelValues(fmtKey).Inc()
}
return
}
sig, domain, err := v.signBlock(ctx, pubKey, epoch, wb)
if err != nil {
log.WithError(err).Error("Failed to sign block")
if v.emitAccountMetrics {
ValidatorProposeFailVec.WithLabelValues(fmtKey).Inc()
}
return
}
blk := &ethpb.SignedBeaconBlockAltair{
Block: b,
Signature: sig,
}
signingRoot, err := helpers.ComputeSigningRoot(b, domain.SignatureDomain)
if err != nil {
if v.emitAccountMetrics {
ValidatorProposeFailVec.WithLabelValues(fmtKey).Inc()
}
log.WithError(err).Error("Failed to compute signing root for block")
return
}
if err := v.preBlockSignValidations(ctx, pubKey, wb, signingRoot); err != nil {
log.WithFields(
blockLogFields(pubKey, wb, nil),
).WithError(err).Error("Failed block slashing protection check")
if v.emitAccountMetrics {
ValidatorProposeFailVec.WithLabelValues(fmtKey).Inc()
}
return
}
wsb, err := wrapper.WrappedAltairSignedBeaconBlock(blk)
if err != nil {
log.WithError(err).Error("Failed to wrap signed block")
if v.emitAccountMetrics {
ValidatorProposeFailVec.WithLabelValues(fmtKey).Inc()
}
return
}
if err := v.postBlockSignUpdate(ctx, pubKey, wsb, signingRoot); err != nil {
log.WithFields(
blockLogFields(pubKey, wb, sig),
).WithError(err).Error("Failed block slashing protection check")
if v.emitAccountMetrics {
ValidatorProposeFailVec.WithLabelValues(fmtKey).Inc()
}
return
}
// Propose and broadcast block via beacon node
blkResp, err := v.validatorClient.ProposeBlockAltair(ctx, blk)
if err != nil {
log.WithError(err).Error("Failed to propose block")
if v.emitAccountMetrics {
ValidatorProposeFailVec.WithLabelValues(fmtKey).Inc()
}
return
}
span.AddAttributes(
trace.StringAttribute("blockRoot", fmt.Sprintf("%#x", blkResp.BlockRoot)),
trace.Int64Attribute("numDeposits", int64(len(b.Body.Deposits))),
trace.Int64Attribute("numAttestations", int64(len(b.Body.Attestations))),
)
blkRoot := fmt.Sprintf("%#x", bytesutil.Trunc(blkResp.BlockRoot))
log.WithFields(logrus.Fields{
"slot": b.Slot,
"blockRoot": blkRoot,
"numAttestations": len(b.Body.Attestations),
"numDeposits": len(b.Body.Deposits),
"graffiti": string(b.Body.Graffiti),
"fork": "altair",
}).Info("Submitted new block")
if v.emitAccountMetrics {
ValidatorProposeSuccessVec.WithLabelValues(fmtKey).Inc()
}
}
// ProposeExit performs a voluntary exit on a validator.
// The exit is signed by the validator before being sent to the beacon node for broadcasting.
func ProposeExit(
@@ -221,7 +373,7 @@ func (v *validator) signRandaoReveal(ctx context.Context, pubKey [48]byte, epoch
}
// Sign block with proposer domain and private key.
func (v *validator) signBlock(ctx context.Context, pubKey [48]byte, epoch types.Epoch, b *ethpb.BeaconBlock) ([]byte, *ethpb.DomainResponse, error) {
func (v *validator) signBlock(ctx context.Context, pubKey [48]byte, epoch types.Epoch, b block.BeaconBlock) ([]byte, *ethpb.DomainResponse, error) {
domain, err := v.domainData(ctx, epoch, params.BeaconConfig().DomainBeaconProposer[:])
if err != nil {
return nil, nil, errors.Wrap(err, domainDataErr)
@@ -231,20 +383,48 @@ func (v *validator) signBlock(ctx context.Context, pubKey [48]byte, epoch types.
}
var sig bls.Signature
blockRoot, err := helpers.ComputeSigningRoot(b, domain.SignatureDomain)
if err != nil {
return nil, nil, errors.Wrap(err, signingRootErr)
switch b.Version() {
case version.Altair:
block, ok := b.Proto().(*ethpb.BeaconBlockAltair)
if !ok {
return nil, nil, errors.New("could not convert obj to beacon block altair")
}
blockRoot, err := helpers.ComputeSigningRoot(block, domain.SignatureDomain)
if err != nil {
return nil, nil, errors.Wrap(err, signingRootErr)
}
sig, err = v.keyManager.Sign(ctx, &validatorpb.SignRequest{
PublicKey: pubKey[:],
SigningRoot: blockRoot[:],
SignatureDomain: domain.SignatureDomain,
Object: &validatorpb.SignRequest_BlockV2{BlockV2: block},
})
if err != nil {
return nil, nil, errors.Wrap(err, "could not sign block proposal")
}
return sig.Marshal(), domain, nil
case version.Phase0:
block, ok := b.Proto().(*ethpb.BeaconBlock)
if !ok {
return nil, nil, errors.New("could not convert obj to beacon block phase 0")
}
blockRoot, err := helpers.ComputeSigningRoot(block, domain.SignatureDomain)
if err != nil {
return nil, nil, errors.Wrap(err, signingRootErr)
}
sig, err = v.keyManager.Sign(ctx, &validatorpb.SignRequest{
PublicKey: pubKey[:],
SigningRoot: blockRoot[:],
SignatureDomain: domain.SignatureDomain,
Object: &validatorpb.SignRequest_Block{Block: block},
})
if err != nil {
return nil, nil, errors.Wrap(err, "could not sign block proposal")
}
return sig.Marshal(), domain, nil
default:
return nil, nil, errors.New("unknown block type")
}
sig, err = v.keyManager.Sign(ctx, &validatorpb.SignRequest{
PublicKey: pubKey[:],
SigningRoot: blockRoot[:],
SignatureDomain: domain.SignatureDomain,
Object: &validatorpb.SignRequest_Block{Block: b},
})
if err != nil {
return nil, nil, errors.Wrap(err, "could not sign block proposal")
}
return sig.Marshal(), domain, nil
}
// Sign voluntary exit with proposer domain and private key.

View File

@@ -12,6 +12,7 @@ import (
types "github.com/prysmaticlabs/eth2-types"
ethpb "github.com/prysmaticlabs/prysm/proto/prysm/v1alpha1"
validatorpb "github.com/prysmaticlabs/prysm/proto/prysm/v1alpha1/validator-client"
"github.com/prysmaticlabs/prysm/proto/prysm/v1alpha1/wrapper"
"github.com/prysmaticlabs/prysm/shared/bls"
"github.com/prysmaticlabs/prysm/shared/bytesutil"
lruwrpr "github.com/prysmaticlabs/prysm/shared/lru"
@@ -156,6 +157,31 @@ func TestProposeBlock_RequestBlockFailed(t *testing.T) {
require.LogsContain(t, hook, "Failed to request block from beacon node")
}
func TestProposeBlockAltair_RequestBlockFailed(t *testing.T) {
hook := logTest.NewGlobal()
params.SetupTestConfigCleanup(t)
cfg := params.BeaconConfig()
cfg.AltairForkEpoch = 2
params.OverrideBeaconConfig(cfg)
validator, m, validatorKey, finish := setup(t)
defer finish()
pubKey := [48]byte{}
copy(pubKey[:], validatorKey.PublicKey().Marshal())
m.validatorClient.EXPECT().DomainData(
gomock.Any(), // ctx
gomock.Any(), // epoch
).Return(&ethpb.DomainResponse{SignatureDomain: make([]byte, 32)}, nil /*err*/)
m.validatorClient.EXPECT().GetBlockAltair(
gomock.Any(), // ctx
gomock.Any(), // block request
).Return(nil /*response*/, errors.New("uh oh"))
validator.ProposeBlock(context.Background(), 2*params.BeaconConfig().SlotsPerEpoch, pubKey)
require.LogsContain(t, hook, "Failed to request block from beacon node")
}
func TestProposeBlock_ProposeBlockFailed(t *testing.T) {
hook := logTest.NewGlobal()
validator, m, validatorKey, finish := setup(t)
@@ -187,6 +213,41 @@ func TestProposeBlock_ProposeBlockFailed(t *testing.T) {
require.LogsContain(t, hook, "Failed to propose block")
}
func TestProposeBlockAltair_ProposeBlockFailed(t *testing.T) {
hook := logTest.NewGlobal()
params.SetupTestConfigCleanup(t)
cfg := params.BeaconConfig()
cfg.AltairForkEpoch = 2
params.OverrideBeaconConfig(cfg)
validator, m, validatorKey, finish := setup(t)
defer finish()
pubKey := [48]byte{}
copy(pubKey[:], validatorKey.PublicKey().Marshal())
m.validatorClient.EXPECT().DomainData(
gomock.Any(), // ctx
gomock.Any(), // epoch
).Return(&ethpb.DomainResponse{SignatureDomain: make([]byte, 32)}, nil /*err*/)
m.validatorClient.EXPECT().GetBlockAltair(
gomock.Any(), // ctx
gomock.Any(),
).Return(testutil.NewBeaconBlockAltair().Block, nil /*err*/)
m.validatorClient.EXPECT().DomainData(
gomock.Any(), // ctx
gomock.Any(), // epoch
).Return(&ethpb.DomainResponse{SignatureDomain: make([]byte, 32)}, nil /*err*/)
m.validatorClient.EXPECT().ProposeBlockAltair(
gomock.Any(), // ctx
gomock.AssignableToTypeOf(&ethpb.SignedBeaconBlockAltair{}),
).Return(nil /*response*/, errors.New("uh oh"))
validator.ProposeBlock(context.Background(), 2*params.BeaconConfig().SlotsPerEpoch, pubKey)
require.LogsContain(t, hook, "Failed to propose block")
}
func TestProposeBlock_BlocksDoubleProposal(t *testing.T) {
hook := logTest.NewGlobal()
validator, m, validatorKey, finish := setup(t)
@@ -239,6 +300,62 @@ func TestProposeBlock_BlocksDoubleProposal(t *testing.T) {
require.LogsContain(t, hook, failedPreBlockSignLocalErr)
}
func TestProposeBlockAltair_BlocksDoubleProposal(t *testing.T) {
hook := logTest.NewGlobal()
params.SetupTestConfigCleanup(t)
cfg := params.BeaconConfig()
cfg.AltairForkEpoch = 2
params.OverrideBeaconConfig(cfg)
validator, m, validatorKey, finish := setup(t)
defer finish()
pubKey := [48]byte{}
copy(pubKey[:], validatorKey.PublicKey().Marshal())
dummyRoot := [32]byte{}
// Save a dummy proposal history at slot 0.
err := validator.db.SaveProposalHistoryForSlot(context.Background(), pubKey, 0, dummyRoot[:])
require.NoError(t, err)
m.validatorClient.EXPECT().DomainData(
gomock.Any(), // ctx
gomock.Any(), // epoch
).Times(1).Return(&ethpb.DomainResponse{SignatureDomain: make([]byte, 32)}, nil /*err*/)
testBlock := testutil.NewBeaconBlockAltair()
slot := params.BeaconConfig().SlotsPerEpoch*5 + 2
testBlock.Block.Slot = slot
m.validatorClient.EXPECT().GetBlockAltair(
gomock.Any(), // ctx
gomock.Any(),
).Return(testBlock.Block, nil /*err*/)
secondTestBlock := testutil.NewBeaconBlockAltair()
secondTestBlock.Block.Slot = slot
graffiti := [32]byte{}
copy(graffiti[:], "someothergraffiti")
secondTestBlock.Block.Body.Graffiti = graffiti[:]
m.validatorClient.EXPECT().GetBlockAltair(
gomock.Any(), // ctx
gomock.Any(),
).Return(secondTestBlock.Block, nil /*err*/)
m.validatorClient.EXPECT().DomainData(
gomock.Any(), // ctx
gomock.Any(), // epoch
).Times(3).Return(&ethpb.DomainResponse{SignatureDomain: make([]byte, 32)}, nil /*err*/)
m.validatorClient.EXPECT().ProposeBlockAltair(
gomock.Any(), // ctx
gomock.AssignableToTypeOf(&ethpb.SignedBeaconBlockAltair{}),
).Return(&ethpb.ProposeResponse{BlockRoot: make([]byte, 32)}, nil /*error*/)
validator.ProposeBlock(context.Background(), slot, pubKey)
require.LogsDoNotContain(t, hook, failedPreBlockSignLocalErr)
validator.ProposeBlock(context.Background(), slot, pubKey)
require.LogsContain(t, hook, failedPreBlockSignLocalErr)
}
func TestProposeBlock_BlocksDoubleProposal_After54KEpochs(t *testing.T) {
hook := logTest.NewGlobal()
validator, m, validatorKey, finish := setup(t)
@@ -455,6 +572,49 @@ func TestProposeBlock_BroadcastsBlock_WithGraffiti(t *testing.T) {
assert.Equal(t, string(validator.graffiti), string(sentBlock.Block.Body.Graffiti))
}
func TestProposeBlockAltair_BroadcastsBlock_WithGraffiti(t *testing.T) {
params.SetupTestConfigCleanup(t)
cfg := params.BeaconConfig()
cfg.AltairForkEpoch = 2
params.OverrideBeaconConfig(cfg)
validator, m, validatorKey, finish := setup(t)
defer finish()
pubKey := [48]byte{}
copy(pubKey[:], validatorKey.PublicKey().Marshal())
validator.graffiti = []byte("12345678901234567890123456789012")
m.validatorClient.EXPECT().DomainData(
gomock.Any(), // ctx
gomock.Any(), // epoch
).Return(&ethpb.DomainResponse{SignatureDomain: make([]byte, 32)}, nil /*err*/)
blk := testutil.NewBeaconBlockAltair()
blk.Block.Body.Graffiti = validator.graffiti
m.validatorClient.EXPECT().GetBlockAltair(
gomock.Any(), // ctx
gomock.Any(),
).Return(blk.Block, nil /*err*/)
m.validatorClient.EXPECT().DomainData(
gomock.Any(), // ctx
gomock.Any(), // epoch
).Return(&ethpb.DomainResponse{SignatureDomain: make([]byte, 32)}, nil /*err*/)
var sentBlock *ethpb.SignedBeaconBlockAltair
m.validatorClient.EXPECT().ProposeBlockAltair(
gomock.Any(), // ctx
gomock.AssignableToTypeOf(&ethpb.SignedBeaconBlockAltair{}),
).DoAndReturn(func(ctx context.Context, block *ethpb.SignedBeaconBlockAltair, opts ...grpc.CallOption) (*ethpb.ProposeResponse, error) {
sentBlock = block
return &ethpb.ProposeResponse{BlockRoot: make([]byte, 32)}, nil
})
validator.ProposeBlock(context.Background(), 2*params.BeaconConfig().SlotsPerEpoch, pubKey)
assert.Equal(t, string(validator.graffiti), string(sentBlock.Block.Body.Graffiti))
}
func TestProposeExit_ValidatorIndexFailed(t *testing.T) {
_, m, validatorKey, finish := setup(t)
defer finish()
@@ -661,7 +821,7 @@ func TestSignBlock(t *testing.T) {
},
}
validator.keyManager = km
sig, domain, err := validator.signBlock(ctx, pubKey, 0, blk.Block)
sig, domain, err := validator.signBlock(ctx, pubKey, 0, wrapper.WrappedPhase0BeaconBlock(blk.Block))
require.NoError(t, err, "%x,%x,%v", sig, domain.SignatureDomain, err)
require.Equal(t, "a049e1dc723e5a8b5bd14f292973572dffd53785ddb337"+
"82f20bf762cbe10ee7b9b4f5ae1ad6ff2089d352403750bed402b94b58469c072536"+
@@ -671,6 +831,36 @@ func TestSignBlock(t *testing.T) {
require.DeepEqual(t, proposerDomain, domain.SignatureDomain)
}
func TestSignAltairBlock(t *testing.T) {
validator, m, _, finish := setup(t)
defer finish()
secretKey, err := bls.SecretKeyFromBytes(bytesutil.PadTo([]byte{1}, 32))
require.NoError(t, err, "Failed to generate key from bytes")
publicKey := secretKey.PublicKey()
proposerDomain := make([]byte, 32)
m.validatorClient.EXPECT().
DomainData(gomock.Any(), gomock.Any()).
Return(&ethpb.DomainResponse{SignatureDomain: proposerDomain}, nil)
ctx := context.Background()
blk := testutil.NewBeaconBlockAltair()
blk.Block.Slot = 1
blk.Block.ProposerIndex = 100
var pubKey [48]byte
copy(pubKey[:], publicKey.Marshal())
km := &mockKeymanager{
keysMap: map[[48]byte]bls.SecretKey{
pubKey: secretKey,
},
}
validator.keyManager = km
wb, err := wrapper.WrappedAltairBeaconBlock(blk.Block)
require.NoError(t, err)
sig, domain, err := validator.signBlock(ctx, pubKey, 0, wb)
require.NoError(t, err, "%x,%x,%v", sig, domain.SignatureDomain, err)
require.DeepEqual(t, proposerDomain, domain.SignatureDomain)
}
func TestGetGraffiti_Ok(t *testing.T) {
ctrl := gomock.NewController(t)
m := &mocks{