prysm-pulse/validator/client/runner.go
hyunchel 470ea6d717
Remove no-op cancel func (#13069)
This cancel function is currently a no-op due to the blank identifier.
One might argue that the cancel func should be restored from no-op by
replacing the blank identifier with the proper variable. When the parent
context is cancelled, however, all the functions down the call tree with
the context will be notified of the cancellation anyway. Removing the
cancel function would not change any outcome under the current
implementation.

Co-authored-by: Preston Van Loon <pvanloon@offchainlabs.com>
Co-authored-by: Radosław Kapka <rkapka@wp.pl>
2023-10-30 14:43:25 +00:00

281 lines
9.1 KiB
Go

package client
import (
"context"
"fmt"
"sync"
"time"
"github.com/pkg/errors"
"github.com/prysmaticlabs/prysm/v4/cmd/validator/flags"
fieldparams "github.com/prysmaticlabs/prysm/v4/config/fieldparams"
"github.com/prysmaticlabs/prysm/v4/config/params"
"github.com/prysmaticlabs/prysm/v4/consensus-types/primitives"
"github.com/prysmaticlabs/prysm/v4/encoding/bytesutil"
"github.com/prysmaticlabs/prysm/v4/time/slots"
"github.com/prysmaticlabs/prysm/v4/validator/client/iface"
"go.opencensus.io/trace"
"google.golang.org/grpc/codes"
"google.golang.org/grpc/status"
)
// time to wait before trying to reconnect with beacon node.
var backOffPeriod = 10 * time.Second
// Run the main validator routine. This routine exits if the context is
// canceled.
//
// Order of operations:
// 1 - Initialize validator data
// 2 - Wait for validator activation
// 3 - Wait for the next slot start
// 4 - Update assignments
// 5 - Determine role at current slot
// 6 - Perform assigned role, if any
func run(ctx context.Context, v iface.Validator) {
cleanup := v.Done
defer cleanup()
headSlot, err := initializeValidatorAndGetHeadSlot(ctx, v)
if err != nil {
return // Exit if context is canceled.
}
connectionErrorChannel := make(chan error, 1)
go v.ReceiveBlocks(ctx, connectionErrorChannel)
if err := v.UpdateDuties(ctx, headSlot); err != nil {
handleAssignmentError(err, headSlot)
}
accountsChangedChan := make(chan [][fieldparams.BLSPubkeyLength]byte, 1)
km, err := v.Keymanager()
if err != nil {
log.WithError(err).Fatal("Could not get keymanager")
}
sub := km.SubscribeAccountChanges(accountsChangedChan)
// check if proposer settings is still nil
// Set properties on the beacon node like the fee recipient for validators that are being used & active.
if v.ProposerSettings() != nil {
log.Infof("Validator client started with provided proposer settings that sets options such as fee recipient"+
" and will periodically update the beacon node and custom builder (if --%s)", flags.EnableBuilderFlag.Name)
deadline := time.Now().Add(5 * time.Minute)
if err := v.PushProposerSettings(ctx, km, headSlot, deadline); err != nil {
if errors.Is(err, ErrBuilderValidatorRegistration) {
log.WithError(err).Warn("Push proposer settings error")
} else {
log.WithError(err).Fatal("Failed to update proposer settings") // allow fatal. skipcq
}
}
} else {
log.Warn("Validator client started without proposer settings such as fee recipient" +
" and will continue to use settings provided in the beacon node.")
}
for {
ctx, span := trace.StartSpan(ctx, "validator.processSlot")
select {
case <-ctx.Done():
log.Info("Context canceled, stopping validator")
span.End()
sub.Unsubscribe()
close(accountsChangedChan)
return // Exit if context is canceled.
case blocksError := <-connectionErrorChannel:
if blocksError != nil {
log.WithError(blocksError).Warn("block stream interrupted")
go v.ReceiveBlocks(ctx, connectionErrorChannel)
continue
}
case currentKeys := <-accountsChangedChan:
onAccountsChanged(ctx, v, currentKeys, accountsChangedChan)
case slot := <-v.NextSlot():
span.AddAttributes(trace.Int64Attribute("slot", int64(slot))) // lint:ignore uintcast -- This conversion is OK for tracing.
deadline := v.SlotDeadline(slot)
slotCtx, cancel := context.WithDeadline(ctx, deadline)
log := log.WithField("slot", slot)
log.WithField("deadline", deadline).Debug("Set deadline for proposals and attestations")
// Keep trying to update assignments if they are nil or if we are past an
// epoch transition in the beacon node's state.
if err := v.UpdateDuties(ctx, slot); err != nil {
handleAssignmentError(err, slot)
cancel()
span.End()
continue
}
// call push proposer setting at the start of each epoch to account for the following edge case:
// proposer is activated at the start of epoch and tries to propose immediately
if slots.IsEpochStart(slot) && v.ProposerSettings() != nil {
go func() {
// deadline set for 1 epoch from call to not overlap.
epochDeadline := v.SlotDeadline(slot + params.BeaconConfig().SlotsPerEpoch - 1)
if err := v.PushProposerSettings(ctx, km, slot, epochDeadline); err != nil {
log.WithError(err).Warn("Failed to update proposer settings")
}
}()
}
// Start fetching domain data for the next epoch.
if slots.IsEpochEnd(slot) {
go v.UpdateDomainDataCaches(ctx, slot+1)
}
var wg sync.WaitGroup
allRoles, err := v.RolesAt(ctx, slot)
if err != nil {
log.WithError(err).Error("Could not get validator roles")
cancel()
span.End()
continue
}
performRoles(slotCtx, allRoles, v, slot, &wg, span)
}
}
}
func onAccountsChanged(ctx context.Context, v iface.Validator, current [][48]byte, ac chan [][fieldparams.BLSPubkeyLength]byte) {
anyActive, err := v.HandleKeyReload(ctx, current)
if err != nil {
log.WithError(err).Error("Could not properly handle reloaded keys")
}
if !anyActive {
log.Warn("No active keys found. Waiting for activation...")
err := v.WaitForActivation(ctx, ac)
if err != nil {
log.WithError(err).Warn("Could not wait for validator activation")
}
}
}
func initializeValidatorAndGetHeadSlot(ctx context.Context, v iface.Validator) (primitives.Slot, error) {
ticker := time.NewTicker(backOffPeriod)
defer ticker.Stop()
var headSlot primitives.Slot
firstTime := true
for {
if !firstTime {
if ctx.Err() != nil {
log.Info("Context canceled, stopping validator")
return headSlot, errors.New("context canceled")
}
<-ticker.C
} else {
firstTime = false
}
err := v.WaitForChainStart(ctx)
if isConnectionError(err) {
log.WithError(err).Warn("Could not determine if beacon chain started")
continue
}
if err != nil {
log.WithError(err).Fatal("Could not determine if beacon chain started")
}
err = v.WaitForKeymanagerInitialization(ctx)
if err != nil {
// log.Fatal will prevent defer from being called
v.Done()
log.WithError(err).Fatal("Wallet is not ready")
}
err = v.WaitForSync(ctx)
if isConnectionError(err) {
log.WithError(err).Warn("Could not determine if beacon chain started")
continue
}
if err != nil {
log.WithError(err).Fatal("Could not determine if beacon node synced")
}
err = v.WaitForActivation(ctx, nil /* accountsChangedChan */)
if err != nil {
log.WithError(err).Fatal("Could not wait for validator activation")
}
headSlot, err = v.CanonicalHeadSlot(ctx)
if isConnectionError(err) {
log.WithError(err).Warn("Could not get current canonical head slot")
continue
}
if err != nil {
log.WithError(err).Fatal("Could not get current canonical head slot")
}
err = v.CheckDoppelGanger(ctx)
if isConnectionError(err) {
log.WithError(err).Warn("Could not wait for checking doppelganger")
continue
}
if err != nil {
log.WithError(err).Fatal("Could not succeed with doppelganger check")
}
break
}
return headSlot, nil
}
func performRoles(slotCtx context.Context, allRoles map[[48]byte][]iface.ValidatorRole, v iface.Validator, slot primitives.Slot, wg *sync.WaitGroup, span *trace.Span) {
for pubKey, roles := range allRoles {
wg.Add(len(roles))
for _, role := range roles {
go func(role iface.ValidatorRole, pubKey [fieldparams.BLSPubkeyLength]byte) {
defer wg.Done()
switch role {
case iface.RoleAttester:
v.SubmitAttestation(slotCtx, slot, pubKey)
case iface.RoleProposer:
v.ProposeBlock(slotCtx, slot, pubKey)
case iface.RoleAggregator:
v.SubmitAggregateAndProof(slotCtx, slot, pubKey)
case iface.RoleSyncCommittee:
v.SubmitSyncCommitteeMessage(slotCtx, slot, pubKey)
case iface.RoleSyncCommitteeAggregator:
v.SubmitSignedContributionAndProof(slotCtx, slot, pubKey)
case iface.RoleUnknown:
log.WithField("pubKey", fmt.Sprintf("%#x", bytesutil.Trunc(pubKey[:]))).Trace("No active roles, doing nothing")
default:
log.Warnf("Unhandled role %v", role)
}
}(role, pubKey)
}
}
// Wait for all processes to complete, then report span complete.
go func() {
wg.Wait()
defer span.End()
defer func() {
if err := recover(); err != nil { // catch any panic in logging
log.WithField("err", err).
Error("Panic occurred when logging validator report. This" +
" should never happen! Please file a report at github.com/prysmaticlabs/prysm/issues/new")
}
}()
// Log this client performance in the previous epoch
v.LogAttestationsSubmitted()
v.LogSyncCommitteeMessagesSubmitted()
if err := v.LogValidatorGainsAndLosses(slotCtx, slot); err != nil {
log.WithError(err).Error("Could not report validator's rewards/penalties")
}
}()
}
func isConnectionError(err error) bool {
return err != nil && errors.Is(err, iface.ErrConnectionIssue)
}
func handleAssignmentError(err error, slot primitives.Slot) {
if errors.Is(err, ErrValidatorsAllExited) {
log.Warn(ErrValidatorsAllExited)
} else if errCode, ok := status.FromError(err); ok && errCode.Code() == codes.NotFound {
log.WithField(
"epoch", slot/params.BeaconConfig().SlotsPerEpoch,
).Warn("Validator not yet assigned to epoch")
} else {
log.WithField("error", err).Error("Failed to update assignments")
}
}