Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
7 changes: 7 additions & 0 deletions cmd/api.go
Original file line number Diff line number Diff line change
Expand Up @@ -42,6 +42,8 @@ var (
apiInternalAPI bool
apiProposerAPI bool
apiLogTag string

apiKnownValidators []string
)

func init() {
Expand Down Expand Up @@ -69,6 +71,9 @@ func init() {
apiCmd.Flags().BoolVar(&apiDataAPI, "data-api", apiDefaultDataAPIEnabled, "enable data API (/data/...)")
apiCmd.Flags().BoolVar(&apiInternalAPI, "internal-api", apiDefaultInternalAPIEnabled, "enable internal API (/internal/...)")
apiCmd.Flags().BoolVar(&apiProposerAPI, "proposer-api", apiDefaultProposerAPIEnabled, "enable proposer API (/proposer/...)")

apiCmd.Flags().StringSliceVar(&apiKnownValidators, "known-validators", nil,
"comma-separated validator pubkeys to seed the known-validators cache at startup")
}

var apiCmd = &cobra.Command{
Expand Down Expand Up @@ -172,6 +177,8 @@ var apiCmd = &cobra.Command{
DataAPI: apiDataAPI,
InternalAPI: apiInternalAPI,
ProposerAPI: apiProposerAPI,

InitialKnownValidators: apiKnownValidators,
}

// Decode the private key
Expand Down
27 changes: 27 additions & 0 deletions datastore/datastore.go
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ package datastore

import (
"database/sql"
"fmt"
"strconv"
"strings"
"sync"
Expand Down Expand Up @@ -156,6 +157,32 @@ func (ds *Datastore) RefreshKnownValidatorsWithoutChecks(log *logrus.Entry, beac
log.Infof("known validators updated")
}

// SetInitialKnownValidators seeds the known-validators cache directly from a
// caller-provided list of pubkeys. Intended for devnets to skip the multi-minute
// cold start while the api waits for the first natural RefreshKnownValidators
// trigger. Assigns synthetic incrementing indices since the real validator
// indices are only known after the first beacon query.
func (ds *Datastore) SetInitialKnownValidators(pubkeys []string, slot uint64) error {
Comment on lines +160 to +165
byPubkey := make(map[common.PubkeyHex]uint64, len(pubkeys))
byIndex := make(map[uint64]common.PubkeyHex, len(pubkeys))
for i, p := range pubkeys {
pk := common.NewPubkeyHex(strings.TrimSpace(p))
if len(pk.String()) != 98 {
return fmt.Errorf("invalid pubkey at index %d: %q", i, p)
}
idx := uint64(i)
byPubkey[pk] = idx
byIndex[idx] = pk
}
ds.knownValidatorsLock.Lock()
ds.knownValidatorsByPubkey = byPubkey
ds.knownValidatorsByIndex = byIndex
ds.knownValidatorsLock.Unlock()
ds.knownValidatorsLastSlot.Store(slot)
ds.KnownValidatorsWasUpdated.Store(true)
return nil
}
Comment on lines +165 to +184

func (ds *Datastore) IsKnownValidator(pubkeyHex common.PubkeyHex) bool {
ds.knownValidatorsLock.RLock()
defer ds.knownValidatorsLock.RUnlock()
Expand Down
56 changes: 50 additions & 6 deletions services/api/service.go
Original file line number Diff line number Diff line change
Expand Up @@ -152,6 +152,10 @@ type RelayAPIOpts struct {
BlockBuilderAPI bool
DataAPI bool
InternalAPI bool

// InitialKnownValidators seeds the known-validators cache at startup so the
// relay accepts registrations before the first beacon-driven refresh.
InitialKnownValidators []string
}

type payloadAttributesHelper struct {
Expand Down Expand Up @@ -233,6 +237,8 @@ type RelayAPI struct {
ffEnableCancellations bool // whether to enable block builder cancellations
ffRegValContinueOnInvalidSig bool // whether to continue processing further validators if one fails
ffIgnorableValidationErrors bool // whether to enable ignorable validation errors
ffOptimisticAllSlots bool // accept optimistically regardless of the slot==optimisticSlot gate
ffDisableDemotion bool // never demote a builder (skip the demotion and its DB write)

payloadAttributes map[string]payloadAttributesHelper // key:parentBlockHash
payloadAttributesLock sync.RWMutex
Expand Down Expand Up @@ -352,6 +358,16 @@ func NewRelayAPI(opts RelayAPIOpts) (api *RelayAPI, err error) {
api.ffIgnorableValidationErrors = true
}

if os.Getenv("ENABLE_OPTIMISTIC_ALL_SLOTS") == "1" {
api.log.Warn("env: ENABLE_OPTIMISTIC_ALL_SLOTS - optimistic processing bypasses the slot==optimisticSlot gate")
api.ffOptimisticAllSlots = true
}

if os.Getenv("DISABLE_BUILDER_DEMOTION") == "1" {
api.log.Warn("env: DISABLE_BUILDER_DEMOTION - builders are never demoted")
api.ffDisableDemotion = true
}

return api, nil
}

Expand Down Expand Up @@ -481,6 +497,17 @@ func (api *RelayAPI) StartServer() (err error) {

// start proposer API specific things
if api.opts.ProposerAPI {
// Optionally seed the known-validators cache so registrations are accepted
// before the first beacon-driven refresh (which can be many minutes away
// on a fresh devnet because the natural triggers gate on slot positions
// 4 and 20 of each epoch).
if len(api.opts.InitialKnownValidators) > 0 {
if err := api.datastore.SetInitialKnownValidators(api.opts.InitialKnownValidators, currentSlot); err != nil {
return fmt.Errorf("seed known validators: %w", err)
}
api.log.Infof("seeded %d known validators from --known-validators", len(api.opts.InitialKnownValidators))
}

// Update known validators (which can take 10-30 sec). This is a requirement for service readiness, because without them,
// getPayload() doesn't have the information it needs (known validators), which could lead to missed slots.
go api.datastore.RefreshKnownValidators(api.log, api.beaconClient, currentSlot)
Expand Down Expand Up @@ -667,6 +694,9 @@ func (api *RelayAPI) simulateBlock(ctx context.Context, opts blockSimOptions) (b
}

func (api *RelayAPI) demoteBuilder(pubkey string, req *common.VersionedSubmitBlockRequest, simError error) {
if api.ffDisableDemotion {
return // never demote: skips the status flip and its postgres write
}
metrics.BuilderDemotionCount.Add(
context.Background(),
1,
Expand Down Expand Up @@ -870,7 +900,7 @@ func (api *RelayAPI) updateProposerDuties(headSlot uint64) {
defer api.isUpdatingProposerDuties.Store(false)

// Update once every 8 slots (or more, if a slot was missed)
if headSlot%8 != 0 && headSlot-api.proposerDutiesSlot < 8 {
if api.proposerDutiesSlot > 0 && headSlot%8 != 0 && headSlot-api.proposerDutiesSlot < 8 {
return
}

Expand Down Expand Up @@ -903,7 +933,11 @@ func (api *RelayAPI) UpdateProposerDutiesWithoutChecks(headSlot uint64) {
api.proposerDutiesResponse = &respBytes
}
api.proposerDutiesMap = dutiesMap
api.proposerDutiesSlot = headSlot
// Only advance the gate slot when we actually got duties; otherwise let
// the next head event retry (Redis may not be populated yet by the housekeeper).
if len(duties) > 0 {
api.proposerDutiesSlot = headSlot
}
api.proposerDutiesLock.Unlock()

// pretty-print
Expand Down Expand Up @@ -2207,6 +2241,11 @@ func (api *RelayAPI) getForkFromSlot(slot uint64) spec.DataVersion {

func (api *RelayAPI) handleSubmitNewBlock(w http.ResponseWriter, req *http.Request) {
var pf common.Profile
// Under ENABLE_OPTIMISTIC_ALL_SLOTS every submission is processed optimistically,
// so label it optimistic from the very start β€” otherwise submissions that exit
// early (rejects, before the optimistic decision below) keep the default false
// and show up as optimistic="false" in the metrics.
pf.Optimistic = api.ffOptimisticAllSlots
var prevTime, nextTime time.Time

headSlot := api.headSlot.Load()
Expand Down Expand Up @@ -2523,10 +2562,15 @@ func (api *RelayAPI) handleSubmitNewBlock(w http.ResponseWriter, req *http.Reque
ParentBeaconBlockRoot: attrs.parentBeaconRoot,
},
}
// With sufficient collateral, process the block optimistically.
optimistic := builderEntry.status.IsOptimistic &&
builderEntry.collateral.Cmp(submission.BidTrace.Value.ToBig()) >= 0 &&
submission.BidTrace.Slot == api.optimisticSlot.Load()
// With sufficient collateral, process the block optimistically. When
// ENABLE_OPTIMISTIC_ALL_SLOTS is set, force the optimistic path unconditionally β€”
// bypassing the builder-status, collateral, and slot-gate checks β€” so every
// submission is optimistic from the very first one, with no pessimistic warmup
// while a builder's optimistic status and collateral propagate into the cache.
optimistic := api.ffOptimisticAllSlots ||
(builderEntry.status.IsOptimistic &&
builderEntry.collateral.Cmp(submission.BidTrace.Value.ToBig()) >= 0 &&
submission.BidTrace.Slot == api.optimisticSlot.Load())
pf.Optimistic = optimistic
if optimistic {
go api.processOptimisticBlock(opts, simResultC)
Expand Down
2 changes: 1 addition & 1 deletion services/housekeeper/housekeeper.go
Original file line number Diff line number Diff line change
Expand Up @@ -179,7 +179,7 @@ func (hk *Housekeeper) updateProposerDuties(headSlot uint64) {
defer hk.isUpdatingProposerDuties.Store(false)

slotsForHalfAnEpoch := common.SlotsPerEpoch / 2
if headSlot%slotsForHalfAnEpoch != 0 && headSlot-hk.proposerDutiesSlot < slotsForHalfAnEpoch {
if hk.proposerDutiesSlot > 0 && headSlot%slotsForHalfAnEpoch != 0 && headSlot-hk.proposerDutiesSlot < slotsForHalfAnEpoch {
return
}

Expand Down
Loading