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
6 changes: 3 additions & 3 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 @@ -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
95 changes: 43 additions & 52 deletions app/proposerconfig.go
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,6 @@ import (
"github.com/obolnetwork/charon/app/z"
"github.com/obolnetwork/charon/cluster"
"github.com/obolnetwork/charon/core"
"github.com/obolnetwork/charon/eth2util/registration"
)

// proposerConfigDir and proposerConfigFilename locate the generated validator client
Expand Down Expand Up @@ -94,11 +93,12 @@ type builderEntryJSON struct {
}

// writeProposerConfigFile generates the validator client proposer configuration file
// if it doesn't already exist, mirroring what charon registers with builders: the
// effective builder registrations including operator overrides, falling back to the
// cluster lock. An existing file is never modified.
// if it doesn't already exist. Fee recipients mirror what charon registers with builders:
// the effective builder registrations including operator overrides, falling back to the
// cluster lock. The gas limit is the configured proposer gas limit. An existing file is
// never modified.
func writeProposerConfigFile(conf Config, lock *cluster.Lock, nodeIdx cluster.NodeIdx,
feeRecipientFunc func(core.PubKey) string, gasLimitFunc func(core.PubKey) uint64,
feeRecipientFunc func(core.PubKey) string,
) (bool, error) {
dir := filepath.Join(filepath.Dir(conf.LockFile), proposerConfigDir)
if err := os.MkdirAll(dir, 0o755); err != nil {
Expand All @@ -124,13 +124,11 @@ func writeProposerConfigFile(conf Config, lock *cluster.Lock, nodeIdx cluster.No
type valSettings struct {
pubshareHex string
feeRecipient string
gasLimit uint64
}

var (
all []valSettings
feeCounts = make(map[string]int)
gasCounts = make(map[uint64]int)
)

for vi, val := range lock.Validators {
Expand All @@ -149,66 +147,42 @@ func writeProposerConfigFile(conf Config, lock *cluster.Lock, nodeIdx cluster.No
feeRecipient = feeRecipients[vi]
}

gasLimit := gasLimitFunc(corePubkey)
if gasLimit == 0 {
gasLimit = registration.DefaultGasLimit
}

vs := valSettings{
pubshareHex: fmt.Sprintf("%#x", pubshare),
feeRecipient: feeRecipient,
gasLimit: gasLimit,
}

all = append(all, vs)
feeCounts[vs.feeRecipient]++
gasCounts[vs.gasLimit]++
}

// The default config holds the per-field majority values, ties broken by the
// first validator holding them, so the common uniform cluster emits no entries.
var (
defaultFee string
defaultGas uint64
)
// The default config holds the majority fee recipient, ties broken by the first
// validator holding it, so the common uniform cluster emits no entries.
var defaultFee string

for _, vs := range all {
if feeCounts[vs.feeRecipient] > feeCounts[defaultFee] {
defaultFee = vs.feeRecipient
}

if gasCounts[vs.gasLimit] > gasCounts[defaultGas] {
defaultGas = vs.gasLimit
}
}

config := proposerConfigJSON{
Version: proposerConfigVersion,
ProposerConfig: make(map[string]proposerSettingsJSON),
DefaultConfig: proposerSettingsJSON{
FeeRecipient: defaultFee,
GasLimit: strconv.FormatUint(defaultGas, 10),
GasLimit: strconv.FormatUint(conf.ProposerGasLimit, 10),
Builder: builderSettings(conf),
},
}

// Entries only carry the fields diverging from the default config.
for _, vs := range all {
var entry proposerSettingsJSON

if vs.feeRecipient != defaultFee {
entry.FeeRecipient = vs.feeRecipient
}

if vs.gasLimit != defaultGas {
entry.GasLimit = strconv.FormatUint(vs.gasLimit, 10)
}

if entry == (proposerSettingsJSON{}) {
if vs.feeRecipient == defaultFee {
continue // Covered by the default config.
}

config.ProposerConfig[vs.pubshareHex] = entry
config.ProposerConfig[vs.pubshareHex] = proposerSettingsJSON{FeeRecipient: vs.feeRecipient}
}

b, err := json.Marshal(config)
Expand Down Expand Up @@ -244,20 +218,29 @@ func builderSettings(conf Config) *builderSettingsJSON {
}
}

// proposerConfigBuilderMatches returns whether the builder settings of the existing proposer
// config file match the configured builder flags.
func proposerConfigBuilderMatches(conf Config, path string) (bool, error) {
// proposerConfigMatches returns whether the builder settings and the gas limits of the
// existing proposer config file match the configured builder and proposer gas limit flags.
func proposerConfigMatches(conf Config, path string) (builderMatches bool, gasMatches bool, err error) {
b, err := os.ReadFile(path)
if err != nil {
return false, errors.Wrap(err, "read proposer config file", z.Str("path", path))
return false, false, errors.Wrap(err, "read proposer config file", z.Str("path", path))
}

var config proposerConfigJSON
if err := json.Unmarshal(b, &config); err != nil {
return false, errors.Wrap(err, "decode proposer config file", z.Str("path", path))
return false, false, errors.Wrap(err, "decode proposer config file", z.Str("path", path))
}

gasLimit := strconv.FormatUint(conf.ProposerGasLimit, 10)

gasMatches = config.DefaultConfig.GasLimit == gasLimit
for _, entry := range config.ProposerConfig {
if entry.GasLimit != "" && entry.GasLimit != gasLimit {
gasMatches = false
}
}

return builderSettingsEqual(config.DefaultConfig.Builder, builderSettings(conf)), nil
return builderSettingsEqual(config.DefaultConfig.Builder, builderSettings(conf)), gasMatches, nil
}

// builderSettingsEqual returns whether both builder settings are absent, or equal.
Expand All @@ -272,18 +255,18 @@ func builderSettingsEqual(a, b *builderSettingsJSON) bool {
slices.Equal(a.Builders, b.Builders)
}

// wireProposerConfigFile generates the proposer config file, or warns if the builder settings of
// an existing one differ from the builder flags, logging failures instead of returning them so
// generation never prevents duties.
// wireProposerConfigFile generates the proposer config file, or warns if the builder settings or
// gas limits of an existing one differ from the flags, logging failures instead of returning them
// so generation never prevents duties.
func wireProposerConfigFile(ctx context.Context, conf Config, lock *cluster.Lock, nodeIdx cluster.NodeIdx,
feeRecipientFunc func(core.PubKey) string, gasLimitFunc func(core.PubKey) uint64,
feeRecipientFunc func(core.PubKey) string,
) {
if conf.TestConfig.Lock != nil {
// In-memory test lock, there is no cluster directory to write the file into.
return
}

created, err := writeProposerConfigFile(conf, lock, nodeIdx, feeRecipientFunc, gasLimitFunc)
created, err := writeProposerConfigFile(conf, lock, nodeIdx, feeRecipientFunc)
if err != nil {
log.Error(ctx, "Failed generating proposer config file, VC proposer settings must be configured manually", err)
return
Expand All @@ -296,12 +279,20 @@ func wireProposerConfigFile(ctx context.Context, conf Config, lock *cluster.Lock
return
}

// The builder flags only seed a new file, the cluster changes builders together.
matches, err := proposerConfigBuilderMatches(conf, path)
// The flags only seed a new file, the cluster changes builders and gas limits together.
builderMatches, gasMatches, err := proposerConfigMatches(conf, path)
if err != nil {
log.Warn(ctx, "Failed checking builder settings of the proposer config file", err)
} else if !matches {
log.Warn(ctx, "Failed checking the proposer config file against the flags", err)
return
}

if !builderMatches {
log.Warn(ctx, "Builder flags differ from the proposer config file and only apply to a new file, "+
"change its builders with `charon proposerconfig edit` in sync with the other peers", nil, z.Str("path", path))
}

if !gasMatches {
log.Warn(ctx, "Proposer gas limit flag differs from the proposer config file and only applies to a new file, "+
"change its gas limits in sync with the other peers", nil, z.Str("path", path), z.U64("proposer_gas_limit", conf.ProposerGasLimit))
}
}
Loading
Loading