Skip to content
Merged
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
2 changes: 1 addition & 1 deletion .github/workflows/codeql-analysis.yml
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,7 @@ on:
- cron: "18 19 * * 6"

env:
GOLANG_VERSION: "1.27"
GOLANG_VERSION: "1.27.3"

jobs:
analyze:
Expand Down
7 changes: 6 additions & 1 deletion Dockerfile
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
# Container for building Go binary.
FROM golang:1.27.2-trixie AS builder
FROM golang:1.27.3-trixie AS builder
# Install dependencies
RUN apt-get update && apt-get install -y --no-install-recommends build-essential git

Expand All @@ -24,6 +24,11 @@ RUN echo "Built charon version=$(./charon version)"
FROM debian:trixie-slim
RUN apt-get update && apt-get install -y --no-install-recommends ca-certificates fio wget \
&& rm -rf /var/lib/apt/lists/* /tmp/* /var/tmp/*
# Distrust the emSign ECC Root CA - G3 and AAA Certificate Services roots in the
# system store (used by wget and other non-Go tools in the image) until Debian's
# ca-certificates drops them. Remove once the base image ships the update.
RUN sed -i -E 's#^(mozilla/(emSign_ECC_Root_CA_-_G3|Comodo_AAA_Services_root)\.crt)$#!\1#' /etc/ca-certificates.conf \
&& update-ca-certificates --fresh
ARG GITHUB_SHA=local
ENV GITHUB_SHA=${GITHUB_SHA}

Expand Down
8 changes: 4 additions & 4 deletions app/app.go
Original file line number Diff line number Diff line change
Expand Up @@ -104,7 +104,7 @@ type Config struct {
ConsensusProtocol string
Nickname string
BeaconNodeHeaders []string
TargetGasLimit uint
ProposerGasLimit uint64
FallbackBeaconNodeAddrs []string
ExecutionEngineAddr string
Graffiti []string
Expand Down Expand Up @@ -511,7 +511,7 @@ func wireCoreWorkflow(ctx context.Context, life *lifecycle.Manager, conf Config,
life.RegisterStart(lifecycle.AsyncAppCtx, lifecycle.StartBuilderRegWatcher,
lifecycle.HookFuncCtx(builderRegSvc.Run))

wireProposerConfigFile(ctx, conf, lock, nodeIdx, builderRegSvc.FeeRecipient, builderRegSvc.GasLimit)
wireProposerConfigFile(ctx, conf, lock, nodeIdx, builderRegSvc.FeeRecipient)

peers, err := lock.Peers()
if err != nil {
Expand Down Expand Up @@ -556,7 +556,7 @@ func wireCoreWorkflow(ctx context.Context, life *lifecycle.Manager, conf Config,
// Setup duties cache, refreshing it every epoch.
var dutiesCache *eth2wrap.DutiesCache
if !featureset.Enabled(featureset.DisableDutiesCache) {
dutiesCache = eth2wrap.NewDutiesCache(eth2Cl, []eth2p0.ValidatorIndex{})
dutiesCache = eth2wrap.NewDutiesCache(eth2Cl, []eth2p0.ValidatorIndex{}, forkSchedule)
eth2Cl.SetDutiesCache(dutiesCache.ProposerDutiesCache, dutiesCache.ProposerDutiesV2Cache, dutiesCache.AttesterDutiesCache, dutiesCache.SyncCommDutiesCache)
sseListener.SubscribeChainReorgEvent(dutiesCache.InvalidateCache)
sseListener.SetDutiesDependentRootFunc(dutiesCache.AttesterDutiesDependentRoot)
Expand Down Expand Up @@ -655,7 +655,7 @@ func wireCoreWorkflow(ctx context.Context, life *lifecycle.Manager, conf Config,

dutyDB := dutydb.NewMemDB(retainerFunc("dutydb"))

vapi, err := validatorapi.NewComponent(eth2Cl, allPubSharesByKey, nodeIdx.ShareIdx, builderRegSvc.FeeRecipient, conf.BuilderAPI, lock.TargetGasLimit,
vapi, err := validatorapi.NewComponent(eth2Cl, allPubSharesByKey, nodeIdx.ShareIdx, builderRegSvc.FeeRecipient, conf.BuilderAPI, conf.ProposerGasLimit,
validatorapi.WithBuilderHosts(hosts))
if err != nil {
return err
Expand Down
38 changes: 1 addition & 37 deletions app/builderregistration.go
Original file line number Diff line number Diff line change
Expand Up @@ -45,9 +45,6 @@ type BuilderRegistrationService interface {
Registrations() []*eth2api.VersionedSignedValidatorRegistration
// FeeRecipient returns the current fee recipient address for the given pubkey.
FeeRecipient(pubkey core.PubKey) string
// GasLimit returns the current builder registration gas limit for the given pubkey,
// zero if unknown.
GasLimit(pubkey core.PubKey) uint64
// Run watches the overrides file for changes, periodically fetches from the
// Obol API, and reloads when either source is updated. It blocks until ctx is cancelled.
Run(ctx context.Context)
Expand Down Expand Up @@ -158,7 +155,6 @@ type builderRegistrationService struct {
baseFeeRecipients map[core.PubKey]string
registrations []*eth2api.VersionedSignedValidatorRegistration
feeRecipients map[core.PubKey]string
gasLimits map[core.PubKey]uint64

// Fields for background API fetching.
obolClient *obolapi.Client
Expand Down Expand Up @@ -216,14 +212,6 @@ func (s *builderRegistrationService) FeeRecipient(pubkey core.PubKey) string {
return s.feeRecipients[pubkey]
}

// GasLimit returns the current builder registration gas limit for the given pubkey.
func (s *builderRegistrationService) GasLimit(pubkey core.PubKey) uint64 {
s.mu.RLock()
defer s.mu.RUnlock()

return s.gasLimits[pubkey]
}

// Run watches the overrides file for changes, periodically fetches from the Obol API,
// and reloads when either source is updated. It blocks until ctx is cancelled.
func (s *builderRegistrationService) Run(ctx context.Context) {
Expand Down Expand Up @@ -376,13 +364,12 @@ func (s *builderRegistrationService) fetchFromAPI(ctx context.Context) (bool, er
// and updates the effective registrations and fee recipients.
func (s *builderRegistrationService) recompute(ctx context.Context) {
feeRecipients := maps.Clone(s.baseFeeRecipients)
gasLimits := gasLimitsByPubkey(s.baseRegistrations)

overrides := mergeOverrides(s.fileOverrides, s.apiOverrides)

var regs []*eth2api.VersionedSignedValidatorRegistration
if len(overrides) > 0 {
regs = applyBuilderRegistrationOverrides(ctx, s.baseRegistrations, overrides, feeRecipients, gasLimits)
regs = applyBuilderRegistrationOverrides(ctx, s.baseRegistrations, overrides, feeRecipients)
} else {
regs = s.baseRegistrations
}
Expand All @@ -392,27 +379,6 @@ func (s *builderRegistrationService) recompute(ctx context.Context) {

s.registrations = regs
s.feeRecipients = feeRecipients
s.gasLimits = gasLimits
}

// gasLimitsByPubkey maps validator pubkeys to their builder registration gas limit.
func gasLimitsByPubkey(registrations []*eth2api.VersionedSignedValidatorRegistration) map[core.PubKey]uint64 {
res := make(map[core.PubKey]uint64, len(registrations))

for _, reg := range registrations {
if reg == nil || reg.V1 == nil || reg.V1.Message == nil || reg.V1.Message.GasLimit == 0 {
continue
}

pubkey, err := core.PubKeyFromBytes(reg.V1.Message.Pubkey[:])
if err != nil {
continue
}

res[pubkey] = reg.V1.Message.GasLimit
}

return res
}

// mergeOverrides combines two override slices, keeping the entry with the highest
Expand Down Expand Up @@ -609,7 +575,6 @@ func applyBuilderRegistrationOverrides(
builderRegs []*eth2api.VersionedSignedValidatorRegistration,
overrides []*eth2api.VersionedSignedValidatorRegistration,
feeRecipientByPubkey map[core.PubKey]string,
gasLimitByPubkey map[core.PubKey]uint64,
) []*eth2api.VersionedSignedValidatorRegistration {
// Build lookup from overrides keyed by lowercase pubkey hex.
overrideByPubkey := make(map[string]*eth2api.VersionedSignedValidatorRegistration, len(overrides))
Expand Down Expand Up @@ -649,7 +614,6 @@ func applyBuilderRegistrationOverrides(
}

feeRecipientByPubkey[corePubkey] = "0x" + hex.EncodeToString(override.V1.Message.FeeRecipient[:])
gasLimitByPubkey[corePubkey] = override.V1.Message.GasLimit

log.Info(ctx, "Applied builder registration override for 0x"+key, z.Str("fee_recipient", feeRecipientByPubkey[corePubkey]))
}
Expand Down
8 changes: 4 additions & 4 deletions app/builderregistration_internal_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -193,7 +193,7 @@ func Test_applyBuilderRegistrationOverrides(t *testing.T) {

t.Run("no overrides", func(t *testing.T) {
feeMap := maps.Clone(feeRecipientByPubkey)
result := applyBuilderRegistrationOverrides(ctx, baseRegs, nil, feeMap, map[core.PubKey]uint64{})
result := applyBuilderRegistrationOverrides(ctx, baseRegs, nil, feeMap)
require.Equal(t, baseRegs, result)
})

Expand All @@ -213,7 +213,7 @@ func Test_applyBuilderRegistrationOverrides(t *testing.T) {
},
}

result := applyBuilderRegistrationOverrides(ctx, baseRegs, []*eth2api.VersionedSignedValidatorRegistration{override}, feeMap, map[core.PubKey]uint64{})
result := applyBuilderRegistrationOverrides(ctx, baseRegs, []*eth2api.VersionedSignedValidatorRegistration{override}, feeMap)

require.Equal(t, override, result[0])

Expand All @@ -237,7 +237,7 @@ func Test_applyBuilderRegistrationOverrides(t *testing.T) {
},
}

result := applyBuilderRegistrationOverrides(ctx, baseRegs, []*eth2api.VersionedSignedValidatorRegistration{override}, feeMap, map[core.PubKey]uint64{})
result := applyBuilderRegistrationOverrides(ctx, baseRegs, []*eth2api.VersionedSignedValidatorRegistration{override}, feeMap)

require.Equal(t, baseRegs[0], result[0])
})
Expand All @@ -257,7 +257,7 @@ func Test_applyBuilderRegistrationOverrides(t *testing.T) {
},
}

result := applyBuilderRegistrationOverrides(ctx, baseRegs, []*eth2api.VersionedSignedValidatorRegistration{override}, feeMap, map[core.PubKey]uint64{})
result := applyBuilderRegistrationOverrides(ctx, baseRegs, []*eth2api.VersionedSignedValidatorRegistration{override}, feeMap)

for i := range baseRegs {
require.Equal(t, baseRegs[i], result[i])
Expand Down
19 changes: 13 additions & 6 deletions app/eth2wrap/cache.go
Original file line number Diff line number Diff line change
Expand Up @@ -275,10 +275,11 @@ type CachedDutiesProvider interface {
SyncCommDutiesCache(context.Context, eth2p0.Epoch, []eth2p0.ValidatorIndex) (SyncDutyWithMeta, error)
}

// NewDutiesCache creates a new validator cache.
func NewDutiesCache(eth2Cl Client, valIdxs []eth2p0.ValidatorIndex) *DutiesCache {
// NewDutiesCache creates a new duties cache. The fork schedule function returns the schedule charon currently applies.
func NewDutiesCache(eth2Cl Client, valIdxs []eth2p0.ValidatorIndex, forkSchedule func() ForkForkSchedule) *DutiesCache {
return &DutiesCache{
eth2Cl: eth2Cl,
forkSchedule: forkSchedule,
activeValIdxs: ValIdxs{valIdxs: valIdxs},

proposerDuties: ProposerDuties{
Expand Down Expand Up @@ -307,6 +308,7 @@ func NewDutiesCache(eth2Cl Client, valIdxs []eth2p0.ValidatorIndex) *DutiesCache
// DutiesCache caches active duties.
type DutiesCache struct {
eth2Cl Client
forkSchedule func() ForkForkSchedule
activeValIdxs ValIdxs

// proposerDuties and proposerDutiesV2 are cached separately: the endpoints return the
Expand Down Expand Up @@ -387,8 +389,6 @@ func (c *DutiesCache) UpdateActiveValIndices(vidxs []eth2p0.ValidatorIndex) {
}

// ProposerDutiesCache returns the cached proposer duties, or fetches them if not available, populating the cache with the newly fetched ones.
//
//nolint:dupl // The logic is very similar between proposer, attester and sync duties, but the code is not easily reusable without adding complexity, hence the duplication.
func (c *DutiesCache) ProposerDutiesCache(ctx context.Context, epoch eth2p0.Epoch, vidxs []eth2p0.ValidatorIndex) (ProposerDutyWithMeta, error) {
cacheUsed := false

Expand Down Expand Up @@ -471,13 +471,20 @@ func (c *DutiesCache) ProposerDutiesCache(ctx context.Context, epoch eth2p0.Epoc
dutiesDeref = append(dutiesDeref, d)
}

dutiesResult = append(dutiesResult, eth2Resp.Data...)

// The v1 dependent root is the block root preceding the epoch, so duties fetched ahead of their epoch hold a
// provisional one, which VCs sign into the first gloas epoch's proposer preferences. Don't cache v1 duties from
// the epoch before the fork onwards, from which VCs resolve their duties via v2.
if c.forkSchedule().Active(Gloas, epoch+1) {
return ProposerDutyWithMeta{Duties: dutiesResult, Metadata: eth2Resp.Metadata}, nil
}

_, ok = c.storeOrAmendProposerDuties(epoch, ProposerDutiesForEpoch{duties: dutiesDeref, metadata: eth2Resp.Metadata, requestedIdxs: requestVidxs})
if !ok {
log.Debug(ctx, "Failed to cache proposer duties - another routine already cached duties for this epoch, skipping", z.U64("epoch", uint64(epoch)))
}

dutiesResult = append(dutiesResult, eth2Resp.Data...)

return ProposerDutyWithMeta{Duties: dutiesResult, Metadata: eth2Resp.Metadata}, nil
}

Expand Down
14 changes: 7 additions & 7 deletions app/eth2wrap/cache_internal_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -105,7 +105,7 @@ func epochCached(c *DutiesCache, epoch eth2p0.Epoch) bool {
// TestDutiesCacheTrimCleansOldEpochs verifies that Trim removes cached entries for epochs
// older than (current - dutiesCacheTrimThreshold), preventing unbounded memory growth.
func TestDutiesCacheTrimCleansOldEpochs(t *testing.T) {
cache := NewDutiesCache(nil, nil)
cache := NewDutiesCache(nil, nil, nil)

// Seed epochs 0 through 4.
populateDutiesCache(t, cache, []eth2p0.Epoch{0, 1, 2, 3, 4})
Expand Down Expand Up @@ -137,7 +137,7 @@ func TestDutiesCacheTrimCleansOldEpochs(t *testing.T) {
// TestDutiesCacheTrimBelowThresholdIsNoop verifies that calling Trim with an epoch smaller
// than dutiesCacheTrimThreshold is a no-op and does not clear any cached entries.
func TestDutiesCacheTrimBelowThresholdIsNoop(t *testing.T) {
cache := NewDutiesCache(nil, nil)
cache := NewDutiesCache(nil, nil, nil)

populateDutiesCache(t, cache, []eth2p0.Epoch{0, 1, 2})

Expand All @@ -153,7 +153,7 @@ func TestDutiesCacheTrimBelowThresholdIsNoop(t *testing.T) {
// TestDutiesCacheTrimSequential verifies that successive Trim calls at advancing epochs
// progressively clean older entries, leaving only the most recent epochs.
func TestDutiesCacheTrimSequential(t *testing.T) {
cache := NewDutiesCache(nil, nil)
cache := NewDutiesCache(nil, nil, nil)

populateDutiesCache(t, cache, []eth2p0.Epoch{0, 1, 2, 3, 4, 5})

Expand Down Expand Up @@ -182,7 +182,7 @@ func TestDutiesCacheRequestedIdxsNoDuplicates(t *testing.T) {
const epoch = eth2p0.Epoch(5)

t.Run("proposer", func(t *testing.T) {
cache := NewDutiesCache(nil, nil)
cache := NewDutiesCache(nil, nil, nil)

// Only validator 1 has a proposer duty; validators 2,3 have none.
cache.storeOrAmendProposerDuties(epoch, ProposerDutiesForEpoch{
Expand All @@ -206,7 +206,7 @@ func TestDutiesCacheRequestedIdxsNoDuplicates(t *testing.T) {
})

t.Run("attester", func(t *testing.T) {
cache := NewDutiesCache(nil, nil)
cache := NewDutiesCache(nil, nil, nil)

cache.storeOrAmendAttesterDuties(epoch, AttesterDutiesForEpoch{
duties: []eth2v1.AttesterDuty{{ValidatorIndex: 1}},
Expand All @@ -228,7 +228,7 @@ func TestDutiesCacheRequestedIdxsNoDuplicates(t *testing.T) {
})

t.Run("sync", func(t *testing.T) {
cache := NewDutiesCache(nil, nil)
cache := NewDutiesCache(nil, nil, nil)

cache.storeOrAmendSyncDuties(epoch, SyncDutiesForEpoch{
duties: []eth2v1.SyncCommitteeDuty{{ValidatorIndex: 1}},
Expand All @@ -253,7 +253,7 @@ func TestDutiesCacheRequestedIdxsNoDuplicates(t *testing.T) {
func TestDutiesCacheTrimThousandEpochs(t *testing.T) {
const total = 1000

cache := NewDutiesCache(nil, nil)
cache := NewDutiesCache(nil, nil, nil)

epochs := make([]eth2p0.Epoch, total)
for i := range total {
Expand Down
Loading
Loading