prysm-pulse/validator/client/propose.go
james-prysm 7f931bf65b
Keymanager APIs - get,post,delete graffiti (#13474)
* wip

* adding set and delete graffiti

* fixing mock

* fixing mock linting and putting in scaffolds for unit tests

* adding some tests

* gaz

* adding tests

* updating missing unit test

* fixing unit test

* Update validator/rpc/handlers_keymanager.go

Co-authored-by: Sammy Rosso <15244892+saolyn@users.noreply.github.com>

* Update validator/client/propose.go

Co-authored-by: Radosław Kapka <rkapka@wp.pl>

* Update validator/rpc/handlers_keymanager.go

Co-authored-by: Radosław Kapka <rkapka@wp.pl>

* Update validator/client/propose.go

Co-authored-by: Radosław Kapka <rkapka@wp.pl>

* radek's feedback

* fixing tests

* using wrapper for graffiti

* fixing linting

* wip

* fixing setting proposer settings

* more partial fixes to tests

* gaz

* fixing tests and setting logic

* changing keymanager

* fixing tests and making graffiti optional in the proposer file

* remove unneeded lines

* reverting unintended changes

* Update validator/client/propose.go

Co-authored-by: Manu NALEPA <enalepa@offchainlabs.com>

* addressing feedback

* removing uneeded line

* fixing bad merge resolution

* gofmt

* gaz

---------

Co-authored-by: Sammy Rosso <15244892+saolyn@users.noreply.github.com>
Co-authored-by: Radosław Kapka <rkapka@wp.pl>
Co-authored-by: Manu NALEPA <enalepa@offchainlabs.com>
2024-03-18 15:03:08 +00:00

503 lines
17 KiB
Go

package client
// Validator client proposer functions.
import (
"context"
"fmt"
"time"
"github.com/ethereum/go-ethereum/common/hexutil"
"github.com/golang/protobuf/ptypes/timestamp"
"github.com/pkg/errors"
"github.com/prysmaticlabs/prysm/v5/async"
"github.com/prysmaticlabs/prysm/v5/beacon-chain/core/signing"
fieldparams "github.com/prysmaticlabs/prysm/v5/config/fieldparams"
"github.com/prysmaticlabs/prysm/v5/config/params"
"github.com/prysmaticlabs/prysm/v5/config/proposer"
"github.com/prysmaticlabs/prysm/v5/consensus-types/blocks"
"github.com/prysmaticlabs/prysm/v5/consensus-types/interfaces"
"github.com/prysmaticlabs/prysm/v5/consensus-types/primitives"
"github.com/prysmaticlabs/prysm/v5/crypto/bls"
"github.com/prysmaticlabs/prysm/v5/crypto/rand"
"github.com/prysmaticlabs/prysm/v5/encoding/bytesutil"
ethpb "github.com/prysmaticlabs/prysm/v5/proto/prysm/v1alpha1"
validatorpb "github.com/prysmaticlabs/prysm/v5/proto/prysm/v1alpha1/validator-client"
"github.com/prysmaticlabs/prysm/v5/runtime/version"
prysmTime "github.com/prysmaticlabs/prysm/v5/time"
"github.com/prysmaticlabs/prysm/v5/time/slots"
"github.com/prysmaticlabs/prysm/v5/validator/client/iface"
"github.com/sirupsen/logrus"
"go.opencensus.io/trace"
)
const (
domainDataErr = "could not get domain data"
signingRootErr = "could not get signing root"
signExitErr = "could not sign voluntary exit proposal"
failedBlockSignLocalErr = "block rejected by local protection"
)
// ProposeBlock proposes a new beacon block for a given slot. This method collects the
// previous beacon block, any pending deposits, and ETH1 data from the beacon
// chain node to construct the new block. The new block is then processed with
// 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 primitives.Slot, pubKey [fieldparams.BLSPubkeyLength]byte) {
if slot == 0 {
log.Debug("Assigned to genesis slot, skipping proposal")
return
}
ctx, span := trace.StartSpan(ctx, "validator.ProposeBlock")
defer span.End()
lock := async.NewMultilock(fmt.Sprint(iface.RoleProposer), string(pubKey[:]))
lock.Lock()
defer lock.Unlock()
fmtKey := fmt.Sprintf("%#x", pubKey[:])
span.AddAttributes(trace.StringAttribute("validator", fmtKey))
log := log.WithField("pubkey", fmt.Sprintf("%#x", bytesutil.Trunc(pubKey[:])))
// Sign randao reveal, it's used to request block from beacon node
epoch := primitives.Epoch(slot / params.BeaconConfig().SlotsPerEpoch)
randaoReveal, err := v.signRandaoReveal(ctx, pubKey, epoch, slot)
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.GetBeaconBlock(ctx, &ethpb.BlockRequest{
Slot: slot,
RandaoReveal: randaoReveal,
Graffiti: g,
})
if err != nil {
log.WithField("slot", 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 := blocks.NewBeaconBlock(b.Block)
if err != nil {
log.WithError(err).Error("Failed to wrap block")
if v.emitAccountMetrics {
ValidatorProposeFailVec.WithLabelValues(fmtKey).Inc()
}
return
}
sig, signingRoot, err := v.signBlock(ctx, pubKey, epoch, slot, wb)
if err != nil {
log.WithError(err).Error("Failed to sign block")
if v.emitAccountMetrics {
ValidatorProposeFailVec.WithLabelValues(fmtKey).Inc()
}
return
}
blk, err := blocks.BuildSignedBeaconBlock(wb, sig)
if err != nil {
log.WithError(err).Error("Failed to build signed beacon block")
return
}
if err := v.db.SlashableProposalCheck(ctx, pubKey, blk, signingRoot, v.emitAccountMetrics, ValidatorProposeFailVec); err != nil {
log.WithFields(
blockLogFields(pubKey, wb, nil),
).WithError(err).Error("Failed block slashing protection check")
if v.emitAccountMetrics {
ValidatorProposeFailVec.WithLabelValues(fmtKey).Inc()
}
return
}
var genericSignedBlock *ethpb.GenericSignedBeaconBlock
if blk.Version() >= version.Deneb && !blk.IsBlinded() {
denebBlock, err := blk.PbDenebBlock()
if err != nil {
log.WithError(err).Error("Failed to get deneb block")
return
}
genericSignedBlock = &ethpb.GenericSignedBeaconBlock{
Block: &ethpb.GenericSignedBeaconBlock_Deneb{
Deneb: &ethpb.SignedBeaconBlockContentsDeneb{
Block: denebBlock,
KzgProofs: b.GetDeneb().KzgProofs,
Blobs: b.GetDeneb().Blobs,
},
},
}
} else {
genericSignedBlock, err = blk.PbGenericBlock()
if err != nil {
log.WithError(err).Error("Failed to create proposal request")
if v.emitAccountMetrics {
ValidatorProposeFailVec.WithLabelValues(fmtKey).Inc()
}
return
}
}
blkResp, err := v.validatorClient.ProposeBeaconBlock(ctx, genericSignedBlock)
if err != nil {
log.WithField("slot", slot).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(blk.Block().Body().Deposits()))),
trace.Int64Attribute("numAttestations", int64(len(blk.Block().Body().Attestations()))),
)
if blk.Version() >= version.Bellatrix {
p, err := blk.Block().Body().Execution()
if err != nil {
log.WithError(err).Error("Failed to get execution payload")
return
}
log = log.WithFields(logrus.Fields{
"payloadHash": fmt.Sprintf("%#x", bytesutil.Trunc(p.BlockHash())),
"parentHash": fmt.Sprintf("%#x", bytesutil.Trunc(p.ParentHash())),
"blockNumber": p.BlockNumber,
})
if !blk.IsBlinded() {
txs, err := p.Transactions()
if err != nil {
log.WithError(err).Error("Failed to get execution payload transactions")
return
}
log = log.WithField("txCount", len(txs))
}
if p.GasLimit() != 0 {
log = log.WithField("gasUtilized", float64(p.GasUsed())/float64(p.GasLimit()))
}
if blk.Version() >= version.Capella && !blk.IsBlinded() {
withdrawals, err := p.Withdrawals()
if err != nil {
log.WithError(err).Error("Failed to get execution payload withdrawals")
return
}
log = log.WithField("withdrawalCount", len(withdrawals))
}
if blk.Version() >= version.Deneb {
kzgs, err := blk.Block().Body().BlobKzgCommitments()
if err != nil {
log.WithError(err).Error("Failed to get blob KZG commitments")
return
} else if len(kzgs) != 0 {
log = log.WithField("kzgCommitmentCount", len(kzgs))
}
}
}
blkRoot := fmt.Sprintf("%#x", bytesutil.Trunc(blkResp.BlockRoot))
graffiti := blk.Block().Body().Graffiti()
log.WithFields(logrus.Fields{
"slot": blk.Block().Slot(),
"blockRoot": blkRoot,
"attestationCount": len(blk.Block().Body().Attestations()),
"depositCount": len(blk.Block().Body().Deposits()),
"graffiti": string(graffiti[:]),
"fork": version.String(blk.Block().Version()),
}).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(
ctx context.Context,
validatorClient iface.ValidatorClient,
signer iface.SigningFunc,
pubKey []byte,
epoch primitives.Epoch,
) error {
ctx, span := trace.StartSpan(ctx, "validator.ProposeExit")
defer span.End()
signedExit, err := CreateSignedVoluntaryExit(ctx, validatorClient, signer, pubKey, epoch)
if err != nil {
return errors.Wrap(err, "failed to create signed voluntary exit")
}
exitResp, err := validatorClient.ProposeExit(ctx, signedExit)
if err != nil {
return errors.Wrap(err, "failed to propose voluntary exit")
}
span.AddAttributes(
trace.StringAttribute("exitRoot", fmt.Sprintf("%#x", exitResp.ExitRoot)),
)
return nil
}
func CurrentEpoch(genesisTime *timestamp.Timestamp) (primitives.Epoch, error) {
totalSecondsPassed := prysmTime.Now().Unix() - genesisTime.Seconds
currentSlot := primitives.Slot((uint64(totalSecondsPassed)) / params.BeaconConfig().SecondsPerSlot)
currentEpoch := slots.ToEpoch(currentSlot)
return currentEpoch, nil
}
func CreateSignedVoluntaryExit(
ctx context.Context,
validatorClient iface.ValidatorClient,
signer iface.SigningFunc,
pubKey []byte,
epoch primitives.Epoch,
) (*ethpb.SignedVoluntaryExit, error) {
ctx, span := trace.StartSpan(ctx, "validator.CreateSignedVoluntaryExit")
defer span.End()
indexResponse, err := validatorClient.ValidatorIndex(ctx, &ethpb.ValidatorIndexRequest{PublicKey: pubKey})
if err != nil {
return nil, errors.Wrap(err, "gRPC call to get validator index failed")
}
exit := &ethpb.VoluntaryExit{Epoch: epoch, ValidatorIndex: indexResponse.Index}
slot, err := slots.EpochStart(epoch)
if err != nil {
return nil, errors.Wrap(err, "failed to retrieve slot")
}
sig, err := signVoluntaryExit(ctx, validatorClient, signer, pubKey, exit, slot)
if err != nil {
return nil, errors.Wrap(err, "failed to sign voluntary exit")
}
return &ethpb.SignedVoluntaryExit{Exit: exit, Signature: sig}, nil
}
// Sign randao reveal with randao domain and private key.
func (v *validator) signRandaoReveal(ctx context.Context, pubKey [fieldparams.BLSPubkeyLength]byte, epoch primitives.Epoch, slot primitives.Slot) ([]byte, error) {
domain, err := v.domainData(ctx, epoch, params.BeaconConfig().DomainRandao[:])
if err != nil {
return nil, errors.Wrap(err, domainDataErr)
}
if domain == nil {
return nil, errors.New(domainDataErr)
}
var randaoReveal bls.Signature
sszUint := primitives.SSZUint64(epoch)
root, err := signing.ComputeSigningRoot(&sszUint, domain.SignatureDomain)
if err != nil {
return nil, err
}
randaoReveal, err = v.keyManager.Sign(ctx, &validatorpb.SignRequest{
PublicKey: pubKey[:],
SigningRoot: root[:],
SignatureDomain: domain.SignatureDomain,
Object: &validatorpb.SignRequest_Epoch{Epoch: epoch},
SigningSlot: slot,
})
if err != nil {
return nil, err
}
return randaoReveal.Marshal(), nil
}
// Sign block with proposer domain and private key.
// Returns the signature, block signing root, and any error.
func (v *validator) signBlock(ctx context.Context, pubKey [fieldparams.BLSPubkeyLength]byte, epoch primitives.Epoch, slot primitives.Slot, b interfaces.ReadOnlyBeaconBlock) ([]byte, [32]byte, error) {
domain, err := v.domainData(ctx, epoch, params.BeaconConfig().DomainBeaconProposer[:])
if err != nil {
return nil, [32]byte{}, errors.Wrap(err, domainDataErr)
}
if domain == nil {
return nil, [32]byte{}, errors.New(domainDataErr)
}
blockRoot, err := signing.ComputeSigningRoot(b, domain.SignatureDomain)
if err != nil {
return nil, [32]byte{}, errors.Wrap(err, signingRootErr)
}
sro, err := b.AsSignRequestObject()
if err != nil {
return nil, [32]byte{}, err
}
sig, err := v.keyManager.Sign(ctx, &validatorpb.SignRequest{
PublicKey: pubKey[:],
SigningRoot: blockRoot[:],
SignatureDomain: domain.SignatureDomain,
Object: sro,
SigningSlot: slot,
})
if err != nil {
return nil, [32]byte{}, errors.Wrap(err, "could not sign block proposal")
}
return sig.Marshal(), blockRoot, nil
}
// Sign voluntary exit with proposer domain and private key.
func signVoluntaryExit(
ctx context.Context,
validatorClient iface.ValidatorClient,
signer iface.SigningFunc,
pubKey []byte,
exit *ethpb.VoluntaryExit,
slot primitives.Slot,
) ([]byte, error) {
req := &ethpb.DomainRequest{
Epoch: exit.Epoch,
Domain: params.BeaconConfig().DomainVoluntaryExit[:],
}
domain, err := validatorClient.DomainData(ctx, req)
if err != nil {
return nil, errors.Wrap(err, domainDataErr)
}
if domain == nil {
return nil, errors.New(domainDataErr)
}
exitRoot, err := signing.ComputeSigningRoot(exit, domain.SignatureDomain)
if err != nil {
return nil, errors.Wrap(err, signingRootErr)
}
sig, err := signer(ctx, &validatorpb.SignRequest{
PublicKey: pubKey,
SigningRoot: exitRoot[:],
SignatureDomain: domain.SignatureDomain,
Object: &validatorpb.SignRequest_Exit{Exit: exit},
SigningSlot: slot,
})
if err != nil {
return nil, errors.Wrap(err, signExitErr)
}
return sig.Marshal(), nil
}
// GetGraffiti gets the graffiti from cli or file for the validator public key.
func (v *validator) GetGraffiti(ctx context.Context, pubKey [fieldparams.BLSPubkeyLength]byte) ([]byte, error) {
if v.proposerSettings != nil {
// Check proposer settings for specific key first
if v.proposerSettings.ProposeConfig != nil {
option, ok := v.proposerSettings.ProposeConfig[pubKey]
if ok && option.GraffitiConfig != nil {
return []byte(option.GraffitiConfig.Graffiti), nil
}
}
// Check proposer settings for default settings second
if v.proposerSettings.DefaultConfig != nil {
if v.proposerSettings.DefaultConfig.GraffitiConfig != nil {
return []byte(v.proposerSettings.DefaultConfig.GraffitiConfig.Graffiti), nil
}
}
}
// When specified, use default graffiti from the command line.
if len(v.graffiti) != 0 {
return bytesutil.PadTo(v.graffiti, 32), nil
}
if v.graffitiStruct == nil {
return nil, errors.New("graffitiStruct can't be nil")
}
// When specified, individual validator specified graffiti takes the third priority.
idx, err := v.validatorClient.ValidatorIndex(ctx, &ethpb.ValidatorIndexRequest{PublicKey: pubKey[:]})
if err != nil {
return nil, err
}
g, ok := v.graffitiStruct.Specific[idx.Index]
if ok {
return bytesutil.PadTo([]byte(g), 32), nil
}
// When specified, a graffiti from the ordered list in the file take fourth priority.
if v.graffitiOrderedIndex < uint64(len(v.graffitiStruct.Ordered)) {
graffiti := v.graffitiStruct.Ordered[v.graffitiOrderedIndex]
v.graffitiOrderedIndex = v.graffitiOrderedIndex + 1
err := v.db.SaveGraffitiOrderedIndex(ctx, v.graffitiOrderedIndex)
if err != nil {
return nil, errors.Wrap(err, "failed to update graffiti ordered index")
}
return bytesutil.PadTo([]byte(graffiti), 32), nil
}
// When specified, a graffiti from the random list in the file take Fifth priority.
if len(v.graffitiStruct.Random) != 0 {
r := rand.NewGenerator()
r.Seed(time.Now().Unix())
i := r.Uint64() % uint64(len(v.graffitiStruct.Random))
return bytesutil.PadTo([]byte(v.graffitiStruct.Random[i]), 32), nil
}
// Finally, default graffiti if specified in the file will be used.
if v.graffitiStruct.Default != "" {
return bytesutil.PadTo([]byte(v.graffitiStruct.Default), 32), nil
}
return []byte{}, nil
}
func (v *validator) SetGraffiti(ctx context.Context, pubkey [fieldparams.BLSPubkeyLength]byte, graffiti []byte) error {
if graffiti == nil {
return nil
}
settings := &proposer.Settings{}
if v.proposerSettings != nil {
settings = v.proposerSettings.Clone()
}
if settings.ProposeConfig == nil {
settings.ProposeConfig = map[[48]byte]*proposer.Option{pubkey: {GraffitiConfig: &proposer.GraffitiConfig{Graffiti: string(graffiti)}}}
return v.SetProposerSettings(ctx, settings)
}
option, ok := settings.ProposeConfig[pubkey]
if !ok || option == nil {
settings.ProposeConfig[pubkey] = &proposer.Option{GraffitiConfig: &proposer.GraffitiConfig{
Graffiti: string(graffiti),
}}
} else {
option.GraffitiConfig = &proposer.GraffitiConfig{
Graffiti: string(graffiti),
}
}
return v.SetProposerSettings(ctx, settings) // save the proposer settings
}
func (v *validator) DeleteGraffiti(ctx context.Context, pubKey [fieldparams.BLSPubkeyLength]byte) error {
if v.proposerSettings == nil || v.proposerSettings.ProposeConfig == nil {
return errors.New("attempted to delete graffiti without proposer settings, graffiti will default to flag options")
}
ps := v.proposerSettings.Clone()
option, ok := ps.ProposeConfig[pubKey]
if !ok || option == nil {
return fmt.Errorf("graffiti not found in proposer settings for pubkey:%s", hexutil.Encode(pubKey[:]))
}
option.GraffitiConfig = nil
return v.SetProposerSettings(ctx, ps) // save the proposer settings
}
func blockLogFields(pubKey [fieldparams.BLSPubkeyLength]byte, blk interfaces.ReadOnlyBeaconBlock, sig []byte) logrus.Fields {
fields := logrus.Fields{
"proposerPublicKey": fmt.Sprintf("%#x", pubKey),
"proposerIndex": blk.ProposerIndex(),
"blockSlot": blk.Slot(),
}
if sig != nil {
fields["signature"] = fmt.Sprintf("%#x", sig)
}
return fields
}