mirror of
https://gitlab.com/pulsechaincom/prysm-pulse.git
synced 2024-12-25 21:07:18 +00:00
fa01ee5eba
* WIP * WIP * WIP * Add tests * WIP * Add more tests * Address DeepSource errors * Remove unused param * Add more tests * Address PR comments * Address PR comments * Fix formatting * Remove unused parameter * Fix TestLittleEndianBytesToBigInt Co-authored-by: Radosław Kapka <rkapka@wp.pl>
458 lines
16 KiB
Go
458 lines
16 KiB
Go
package validator
|
|
|
|
import (
|
|
"bytes"
|
|
"context"
|
|
"fmt"
|
|
"time"
|
|
|
|
"github.com/pkg/errors"
|
|
"github.com/prometheus/client_golang/prometheus"
|
|
"github.com/prometheus/client_golang/prometheus/promauto"
|
|
"github.com/prysmaticlabs/prysm/v3/beacon-chain/core/blocks"
|
|
"github.com/prysmaticlabs/prysm/v3/beacon-chain/core/signing"
|
|
"github.com/prysmaticlabs/prysm/v3/beacon-chain/core/transition/interop"
|
|
"github.com/prysmaticlabs/prysm/v3/beacon-chain/db/kv"
|
|
"github.com/prysmaticlabs/prysm/v3/config/params"
|
|
consensusblocks "github.com/prysmaticlabs/prysm/v3/consensus-types/blocks"
|
|
"github.com/prysmaticlabs/prysm/v3/consensus-types/interfaces"
|
|
types "github.com/prysmaticlabs/prysm/v3/consensus-types/primitives"
|
|
"github.com/prysmaticlabs/prysm/v3/encoding/bytesutil"
|
|
"github.com/prysmaticlabs/prysm/v3/encoding/ssz"
|
|
enginev1 "github.com/prysmaticlabs/prysm/v3/proto/engine/v1"
|
|
ethpb "github.com/prysmaticlabs/prysm/v3/proto/prysm/v1alpha1"
|
|
"github.com/prysmaticlabs/prysm/v3/runtime/version"
|
|
"github.com/prysmaticlabs/prysm/v3/time/slots"
|
|
"github.com/sirupsen/logrus"
|
|
)
|
|
|
|
// builderGetPayloadMissCount tracks the number of misses when validator tries to get a payload from builder
|
|
var builderGetPayloadMissCount = promauto.NewCounter(prometheus.CounterOpts{
|
|
Name: "builder_get_payload_miss_count",
|
|
Help: "The number of get payload misses for validator requests to builder",
|
|
})
|
|
|
|
// blockBuilderTimeout is the maximum amount of time allowed for a block builder to respond to a
|
|
// block request. This value is known as `BUILDER_PROPOSAL_DELAY_TOLERANCE` in builder spec.
|
|
const blockBuilderTimeout = 1 * time.Second
|
|
|
|
func (vs *Server) getBellatrixBeaconBlock(ctx context.Context, req *ethpb.BlockRequest) (*ethpb.GenericBeaconBlock, error) {
|
|
altairBlk, err := vs.BuildAltairBeaconBlock(ctx, req)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
if !req.SkipMevBoost {
|
|
registered, err := vs.validatorRegistered(ctx, altairBlk.ProposerIndex)
|
|
if registered && err == nil {
|
|
builderReady, b, err := vs.GetAndBuildBlindBlock(ctx, altairBlk)
|
|
if err != nil {
|
|
// In the event of an error, the node should fall back to default execution engine for building block.
|
|
log.WithError(err).Error("Failed to build a block from external builder, falling " +
|
|
"back to local execution client")
|
|
builderGetPayloadMissCount.Inc()
|
|
} else if builderReady {
|
|
return b, nil
|
|
}
|
|
} else if err != nil {
|
|
log.WithError(err).WithFields(logrus.Fields{
|
|
"slot": req.Slot,
|
|
"validatorIndex": altairBlk.ProposerIndex,
|
|
}).Error("Could not determine validator has registered. Defaulting to local execution client")
|
|
}
|
|
}
|
|
payload, err := vs.getExecutionPayload(ctx, req.Slot, altairBlk.ProposerIndex, bytesutil.ToBytes32(altairBlk.ParentRoot))
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
blk := ðpb.BeaconBlockBellatrix{
|
|
Slot: altairBlk.Slot,
|
|
ProposerIndex: altairBlk.ProposerIndex,
|
|
ParentRoot: altairBlk.ParentRoot,
|
|
StateRoot: params.BeaconConfig().ZeroHash[:],
|
|
Body: ðpb.BeaconBlockBodyBellatrix{
|
|
RandaoReveal: altairBlk.Body.RandaoReveal,
|
|
Eth1Data: altairBlk.Body.Eth1Data,
|
|
Graffiti: altairBlk.Body.Graffiti,
|
|
ProposerSlashings: altairBlk.Body.ProposerSlashings,
|
|
AttesterSlashings: altairBlk.Body.AttesterSlashings,
|
|
Attestations: altairBlk.Body.Attestations,
|
|
Deposits: altairBlk.Body.Deposits,
|
|
VoluntaryExits: altairBlk.Body.VoluntaryExits,
|
|
SyncAggregate: altairBlk.Body.SyncAggregate,
|
|
ExecutionPayload: payload,
|
|
},
|
|
}
|
|
// Compute state root with the newly constructed block.
|
|
wsb, err := consensusblocks.NewSignedBeaconBlock(
|
|
ðpb.SignedBeaconBlockBellatrix{Block: blk, Signature: make([]byte, 96)},
|
|
)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
stateRoot, err := vs.computeStateRoot(ctx, wsb)
|
|
if err != nil {
|
|
interop.WriteBlockToDisk(wsb, true /*failed*/)
|
|
return nil, fmt.Errorf("could not compute state root: %v", err)
|
|
}
|
|
blk.StateRoot = stateRoot
|
|
return ðpb.GenericBeaconBlock{Block: ðpb.GenericBeaconBlock_Bellatrix{Bellatrix: blk}}, nil
|
|
}
|
|
|
|
// This function retrieves the payload header given the slot number and the validator index.
|
|
// It's a no-op if the latest head block is not versioned bellatrix.
|
|
func (vs *Server) getPayloadHeaderFromBuilder(ctx context.Context, slot types.Slot, idx types.ValidatorIndex) (*enginev1.ExecutionPayloadHeader, error) {
|
|
b, err := vs.HeadFetcher.HeadBlock(ctx)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
if blocks.IsPreBellatrixVersion(b.Version()) {
|
|
return nil, nil
|
|
}
|
|
|
|
h, err := b.Block().Body().Execution()
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
pk, err := vs.HeadFetcher.HeadValidatorIndexToPublicKey(ctx, idx)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
bid, err := vs.BlockBuilder.GetHeader(ctx, slot, bytesutil.ToBytes32(h.BlockHash()), pk)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
if bid == nil || bid.Message == nil {
|
|
return nil, errors.New("builder returned nil bid")
|
|
}
|
|
|
|
v := bytesutil.LittleEndianBytesToBigInt(bid.Message.Value)
|
|
if v.String() == "0" {
|
|
return nil, errors.New("builder returned header with 0 bid amount")
|
|
}
|
|
|
|
emptyRoot, err := ssz.TransactionsRoot([][]byte{})
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
if bytesutil.ToBytes32(bid.Message.Header.TransactionsRoot) == emptyRoot {
|
|
return nil, errors.New("builder returned header with an empty tx root")
|
|
}
|
|
|
|
if !bytes.Equal(bid.Message.Header.ParentHash, h.BlockHash()) {
|
|
return nil, fmt.Errorf("incorrect parent hash %#x != %#x", bid.Message.Header.ParentHash, h.BlockHash())
|
|
}
|
|
|
|
t, err := slots.ToTime(uint64(vs.TimeFetcher.GenesisTime().Unix()), slot)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
if bid.Message.Header.Timestamp != uint64(t.Unix()) {
|
|
return nil, fmt.Errorf("incorrect timestamp %d != %d", bid.Message.Header.Timestamp, uint64(t.Unix()))
|
|
}
|
|
|
|
if err := validateBuilderSignature(bid); err != nil {
|
|
return nil, errors.Wrap(err, "could not validate builder signature")
|
|
}
|
|
|
|
log.WithFields(logrus.Fields{
|
|
"value": v.String(),
|
|
"builderPubKey": fmt.Sprintf("%#x", bid.Message.Pubkey),
|
|
"blockHash": fmt.Sprintf("%#x", bid.Message.Header.BlockHash),
|
|
}).Info("Received header with bid")
|
|
return bid.Message.Header, nil
|
|
}
|
|
|
|
// This function constructs the builder block given the input altair block and the header. It returns a generic beacon block for signing
|
|
func (vs *Server) buildBlindBlock(ctx context.Context, b *ethpb.BeaconBlockAltair, h *enginev1.ExecutionPayloadHeader) (*ethpb.GenericBeaconBlock, error) {
|
|
if b == nil || b.Body == nil {
|
|
return nil, errors.New("nil block")
|
|
}
|
|
if h == nil {
|
|
return nil, errors.New("nil header")
|
|
}
|
|
|
|
blk := ðpb.BlindedBeaconBlockBellatrix{
|
|
Slot: b.Slot,
|
|
ProposerIndex: b.ProposerIndex,
|
|
ParentRoot: b.ParentRoot,
|
|
StateRoot: params.BeaconConfig().ZeroHash[:],
|
|
Body: ðpb.BlindedBeaconBlockBodyBellatrix{
|
|
RandaoReveal: b.Body.RandaoReveal,
|
|
Eth1Data: b.Body.Eth1Data,
|
|
Graffiti: b.Body.Graffiti,
|
|
ProposerSlashings: b.Body.ProposerSlashings,
|
|
AttesterSlashings: b.Body.AttesterSlashings,
|
|
Attestations: b.Body.Attestations,
|
|
Deposits: b.Body.Deposits,
|
|
VoluntaryExits: b.Body.VoluntaryExits,
|
|
SyncAggregate: b.Body.SyncAggregate,
|
|
ExecutionPayloadHeader: h,
|
|
},
|
|
}
|
|
wsb, err := consensusblocks.NewSignedBeaconBlock(
|
|
ðpb.SignedBlindedBeaconBlockBellatrix{Block: blk, Signature: make([]byte, 96)},
|
|
)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
stateRoot, err := vs.computeStateRoot(ctx, wsb)
|
|
if err != nil {
|
|
return nil, errors.Wrap(err, "could not compute state root")
|
|
}
|
|
blk.StateRoot = stateRoot
|
|
return ðpb.GenericBeaconBlock{Block: ðpb.GenericBeaconBlock_BlindedBellatrix{BlindedBellatrix: blk}}, nil
|
|
}
|
|
|
|
// This function retrieves the full payload block using the input blind block. This input must be versioned as
|
|
// bellatrix blind block. The output block will contain the full payload. The original header block
|
|
// will be returned the block builder is not configured.
|
|
func (vs *Server) unblindBuilderBlock(ctx context.Context, b interfaces.SignedBeaconBlock) (interfaces.SignedBeaconBlock, error) {
|
|
if err := consensusblocks.BeaconBlockIsNil(b); err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
// No-op if the input block is not version blind and bellatrix.
|
|
if b.Version() != version.Bellatrix || !b.IsBlinded() {
|
|
return b, nil
|
|
}
|
|
// No-op nothing if the builder has not been configured.
|
|
if !vs.BlockBuilder.Configured() {
|
|
return b, nil
|
|
}
|
|
|
|
agg, err := b.Block().Body().SyncAggregate()
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
h, err := b.Block().Body().Execution()
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
header, ok := h.Proto().(*enginev1.ExecutionPayloadHeader)
|
|
if !ok {
|
|
return nil, errors.New("execution data must be execution payload header")
|
|
}
|
|
parentRoot := b.Block().ParentRoot()
|
|
stateRoot := b.Block().StateRoot()
|
|
randaoReveal := b.Block().Body().RandaoReveal()
|
|
graffiti := b.Block().Body().Graffiti()
|
|
sig := b.Signature()
|
|
sb := ðpb.SignedBlindedBeaconBlockBellatrix{
|
|
Block: ðpb.BlindedBeaconBlockBellatrix{
|
|
Slot: b.Block().Slot(),
|
|
ProposerIndex: b.Block().ProposerIndex(),
|
|
ParentRoot: parentRoot[:],
|
|
StateRoot: stateRoot[:],
|
|
Body: ðpb.BlindedBeaconBlockBodyBellatrix{
|
|
RandaoReveal: randaoReveal[:],
|
|
Eth1Data: b.Block().Body().Eth1Data(),
|
|
Graffiti: graffiti[:],
|
|
ProposerSlashings: b.Block().Body().ProposerSlashings(),
|
|
AttesterSlashings: b.Block().Body().AttesterSlashings(),
|
|
Attestations: b.Block().Body().Attestations(),
|
|
Deposits: b.Block().Body().Deposits(),
|
|
VoluntaryExits: b.Block().Body().VoluntaryExits(),
|
|
SyncAggregate: agg,
|
|
ExecutionPayloadHeader: header,
|
|
},
|
|
},
|
|
Signature: sig[:],
|
|
}
|
|
|
|
payload, err := vs.BlockBuilder.SubmitBlindedBlock(ctx, sb)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
headerRoot, err := header.HashTreeRoot()
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
payloadRoot, err := payload.HashTreeRoot()
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
if headerRoot != payloadRoot {
|
|
return nil, fmt.Errorf("header and payload root do not match, consider disconnect from relay to avoid further issues, "+
|
|
"%#x != %#x", headerRoot, payloadRoot)
|
|
}
|
|
|
|
bb := ðpb.SignedBeaconBlockBellatrix{
|
|
Block: ðpb.BeaconBlockBellatrix{
|
|
Slot: sb.Block.Slot,
|
|
ProposerIndex: sb.Block.ProposerIndex,
|
|
ParentRoot: sb.Block.ParentRoot,
|
|
StateRoot: sb.Block.StateRoot,
|
|
Body: ðpb.BeaconBlockBodyBellatrix{
|
|
RandaoReveal: sb.Block.Body.RandaoReveal,
|
|
Eth1Data: sb.Block.Body.Eth1Data,
|
|
Graffiti: sb.Block.Body.Graffiti,
|
|
ProposerSlashings: sb.Block.Body.ProposerSlashings,
|
|
AttesterSlashings: sb.Block.Body.AttesterSlashings,
|
|
Attestations: sb.Block.Body.Attestations,
|
|
Deposits: sb.Block.Body.Deposits,
|
|
VoluntaryExits: sb.Block.Body.VoluntaryExits,
|
|
SyncAggregate: agg,
|
|
ExecutionPayload: payload,
|
|
},
|
|
},
|
|
Signature: sb.Signature,
|
|
}
|
|
wb, err := consensusblocks.NewSignedBeaconBlock(bb)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
log.WithFields(logrus.Fields{
|
|
"blockHash": fmt.Sprintf("%#x", h.BlockHash()),
|
|
"feeRecipient": fmt.Sprintf("%#x", h.FeeRecipient()),
|
|
"gasUsed": h.GasUsed,
|
|
"slot": b.Block().Slot(),
|
|
"txs": len(payload.Transactions),
|
|
}).Info("Retrieved full payload from builder")
|
|
|
|
return wb, nil
|
|
}
|
|
|
|
// readyForBuilder returns true if builder is allowed to be used. Builder is only allowed to be use after the
|
|
// first finalized checkpt has been execution-enabled.
|
|
func (vs *Server) readyForBuilder(ctx context.Context) (bool, error) {
|
|
cp := vs.FinalizationFetcher.FinalizedCheckpt()
|
|
// Checkpoint root is zero means we are still at genesis epoch.
|
|
if bytesutil.ToBytes32(cp.Root) == params.BeaconConfig().ZeroHash {
|
|
return false, nil
|
|
}
|
|
b, err := vs.BeaconDB.Block(ctx, bytesutil.ToBytes32(cp.Root))
|
|
if err != nil {
|
|
return false, err
|
|
}
|
|
if err = consensusblocks.BeaconBlockIsNil(b); err != nil {
|
|
return false, err
|
|
}
|
|
return blocks.IsExecutionBlock(b.Block().Body())
|
|
}
|
|
|
|
// circuitBreakBuilder returns true if the builder is not allowed to be used due to circuit breaker conditions.
|
|
func (vs *Server) circuitBreakBuilder(s types.Slot) (bool, error) {
|
|
if vs.ForkFetcher == nil || vs.ForkFetcher.ForkChoicer() == nil {
|
|
return true, errors.New("no fork choicer configured")
|
|
}
|
|
|
|
// Circuit breaker is active if the missing consecutive slots greater than `MaxBuilderConsecutiveMissedSlots`.
|
|
highestReceivedSlot := vs.ForkFetcher.ForkChoicer().HighestReceivedBlockSlot()
|
|
maxConsecutiveSkipSlotsAllowed := params.BeaconConfig().MaxBuilderConsecutiveMissedSlots
|
|
diff, err := s.SafeSubSlot(highestReceivedSlot)
|
|
if err != nil {
|
|
return true, err
|
|
}
|
|
if diff > maxConsecutiveSkipSlotsAllowed {
|
|
log.WithFields(logrus.Fields{
|
|
"currentSlot": s,
|
|
"highestReceivedSlot": highestReceivedSlot,
|
|
"maxConsecutiveSkipSlotsAllowed": maxConsecutiveSkipSlotsAllowed,
|
|
}).Warn("Builder circuit breaker activated due to missing consecutive slot")
|
|
return true, nil
|
|
}
|
|
|
|
// Not much reason to check missed slots epoch rolling window if input slot is less than epoch.
|
|
if s < params.BeaconConfig().SlotsPerEpoch {
|
|
return false, nil
|
|
}
|
|
|
|
// Circuit breaker is active if the missing slots per epoch (rolling window) greater than `MaxBuilderEpochMissedSlots`.
|
|
receivedCount, err := vs.ForkFetcher.ForkChoicer().ReceivedBlocksLastEpoch()
|
|
if err != nil {
|
|
return true, err
|
|
}
|
|
maxEpochSkipSlotsAllowed := params.BeaconConfig().MaxBuilderEpochMissedSlots
|
|
diff, err = params.BeaconConfig().SlotsPerEpoch.SafeSub(receivedCount)
|
|
if err != nil {
|
|
return true, err
|
|
}
|
|
if diff > maxEpochSkipSlotsAllowed {
|
|
log.WithFields(logrus.Fields{
|
|
"totalMissed": diff,
|
|
"maxEpochSkipSlotsAllowed": maxEpochSkipSlotsAllowed,
|
|
}).Warn("Builder circuit breaker activated due to missing enough slots last epoch")
|
|
return true, nil
|
|
}
|
|
|
|
return false, nil
|
|
}
|
|
|
|
// GetAndBuildBlindBlock builds blind block from builder network. Returns a boolean status, built block and error.
|
|
// If the status is false that means builder the header block is disallowed.
|
|
// This routine is time limited by `blockBuilderTimeout`.
|
|
func (vs *Server) GetAndBuildBlindBlock(ctx context.Context, b *ethpb.BeaconBlockAltair) (bool, *ethpb.GenericBeaconBlock, error) {
|
|
// No op. Builder is not defined. User did not specify a user URL. We should use local EE.
|
|
if vs.BlockBuilder == nil || !vs.BlockBuilder.Configured() {
|
|
return false, nil, nil
|
|
}
|
|
ctx, cancel := context.WithTimeout(ctx, blockBuilderTimeout)
|
|
defer cancel()
|
|
// Does the protocol allow for builder at this current moment. Builder is only allowed post merge after finalization.
|
|
ready, err := vs.readyForBuilder(ctx)
|
|
if err != nil {
|
|
return false, nil, errors.Wrap(err, "could not determine if builder is ready")
|
|
}
|
|
if !ready {
|
|
return false, nil, nil
|
|
}
|
|
|
|
circuitBreak, err := vs.circuitBreakBuilder(b.Slot)
|
|
if err != nil {
|
|
return false, nil, errors.Wrap(err, "could not determine if builder circuit breaker condition")
|
|
}
|
|
if circuitBreak {
|
|
return false, nil, nil
|
|
}
|
|
|
|
h, err := vs.getPayloadHeaderFromBuilder(ctx, b.Slot, b.ProposerIndex)
|
|
if err != nil {
|
|
return false, nil, errors.Wrap(err, "could not get payload header")
|
|
}
|
|
log.WithFields(logrus.Fields{
|
|
"blockHash": fmt.Sprintf("%#x", h.BlockHash),
|
|
"feeRecipient": fmt.Sprintf("%#x", h.FeeRecipient),
|
|
"gasUsed": h.GasUsed,
|
|
"slot": b.Slot,
|
|
}).Info("Retrieved header from builder")
|
|
gb, err := vs.buildBlindBlock(ctx, b, h)
|
|
if err != nil {
|
|
return false, nil, errors.Wrap(err, "could not combine altair block with payload header")
|
|
}
|
|
return true, gb, nil
|
|
}
|
|
|
|
// validatorRegistered returns true if validator with index `id` was previously registered in the database.
|
|
func (vs *Server) validatorRegistered(ctx context.Context, id types.ValidatorIndex) (bool, error) {
|
|
if vs.BeaconDB == nil {
|
|
return false, errors.New("nil beacon db")
|
|
}
|
|
_, err := vs.BeaconDB.RegistrationByValidatorID(ctx, id)
|
|
switch {
|
|
case errors.Is(err, kv.ErrNotFoundFeeRecipient):
|
|
return false, nil
|
|
case err != nil:
|
|
return false, err
|
|
}
|
|
return true, nil
|
|
}
|
|
|
|
// Validates builder signature and returns an error if the signature is invalid.
|
|
func validateBuilderSignature(bid *ethpb.SignedBuilderBid) error {
|
|
d, err := signing.ComputeDomain(params.BeaconConfig().DomainApplicationBuilder,
|
|
nil, /* fork version */
|
|
nil /* genesis val root */)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
if bid == nil || bid.Message == nil {
|
|
return errors.New("nil builder bid")
|
|
}
|
|
return signing.VerifySigningRoot(bid.Message, bid.Message.Pubkey, bid.Signature, d)
|
|
}
|