From 04a6e13f1b563e16f8eb21cef1163ead66b52083 Mon Sep 17 00:00:00 2001 From: kalo <24719519+KaloyanTanev@users.noreply.github.com> Date: Wed, 21 Oct 2026 14:06:57 +0300 Subject: [PATCH 1/2] cmd: default target gas limit to 200M --- cmd/createcluster.go | 2 +- cmd/createdkg.go | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/cmd/createcluster.go b/cmd/createcluster.go index 856fefc95..1c9aa1432 100644 --- a/cmd/createcluster.go +++ b/cmd/createcluster.go @@ -150,7 +150,7 @@ func bindClusterFlags(flags *pflag.FlagSet, config *clusterConfig) { flags.BoolVar(&config.Publish, "publish", false, "Publish lock file to obol-api.") flags.IntSliceVar(&config.DepositAmounts, "deposit-amounts", nil, "List of partial deposit amounts (integers) in ETH. Values must sum up to at least 32ETH.") flags.StringVar(&config.ConsensusProtocol, "consensus-protocol", "", "Preferred consensus protocol name for the cluster. Selected automatically when not specified.") - flags.UintVar(&config.TargetGasLimit, "target-gas-limit", 60000000, "Preferred target gas limit for transactions.") + flags.UintVar(&config.TargetGasLimit, "target-gas-limit", 200000000, "Preferred target gas limit for transactions.") flags.BoolVar(&config.Compounding, "compounding", false, "Enable compounding rewards for validators by using 0x02 withdrawal credentials.") flags.StringVar(&config.ExecutionEngineAddr, "execution-client-rpc-endpoint", "", "The address of the execution engine JSON-RPC API.") flags.BoolVar(&config.Zipped, "zipped", false, "Create a tar archive compressed with gzip of the cluster directory after creation.") diff --git a/cmd/createdkg.go b/cmd/createdkg.go index 5d5d650a6..ff8cf4430 100644 --- a/cmd/createdkg.go +++ b/cmd/createdkg.go @@ -106,7 +106,7 @@ func bindCreateDKGFlags(cmd *cobra.Command, config *createDKGConfig) { cmd.Flags().IntSliceVar(&config.DepositAmounts, "deposit-amounts", nil, "List of partial deposit amounts (integers) in ETH. Values must sum up to at least 32ETH.") cmd.Flags().StringSliceVar(&config.OperatorENRs, "operator-enrs", nil, "Comma-separated list of each operator's Charon ENR address.") cmd.Flags().StringVar(&config.ConsensusProtocol, "consensus-protocol", "", "Preferred consensus protocol name for the cluster. Selected automatically when not specified.") - cmd.Flags().UintVar(&config.TargetGasLimit, "target-gas-limit", 60000000, "Preferred target gas limit for transactions.") + cmd.Flags().UintVar(&config.TargetGasLimit, "target-gas-limit", 200000000, "Preferred target gas limit for transactions.") cmd.Flags().BoolVar(&config.Compounding, "compounding", false, "Enable compounding rewards for validators by using 0x02 withdrawal credentials.") cmd.Flags().StringVar(&config.ExecutionEngineAddr, "execution-client-rpc-endpoint", "", "The address of the execution engine JSON-RPC API.") cmd.Flags().BoolVar(&config.Publish, "publish", false, "Creates an invitation to the DKG ceremony on the DV Launchpad. Terms and conditions apply.") From b1665dc2a175a0011ebb24c3ac063066c84c16a6 Mon Sep 17 00:00:00 2001 From: kalo <24719519+KaloyanTanev@users.noreply.github.com> Date: Wed, 21 Oct 2026 14:28:30 +0300 Subject: [PATCH 2/2] app: add proposer-gas-limit flag defaulting to 200M --- app/app.go | 6 +- app/builderregistration.go | 38 +------- app/builderregistration_internal_test.go | 8 +- app/proposerconfig.go | 95 ++++++++++---------- app/proposerconfig_internal_test.go | 105 +++++++++-------------- cmd/cmd_internal_test.go | 2 + cmd/run.go | 5 ++ core/validatorapi/validatorapi.go | 10 +-- docs/configuration.md | 1 + 9 files changed, 106 insertions(+), 164 deletions(-) diff --git a/app/app.go b/app/app.go index 0fa616713..7e3ef940c 100644 --- a/app/app.go +++ b/app/app.go @@ -104,7 +104,7 @@ type Config struct { ConsensusProtocol string Nickname string BeaconNodeHeaders []string - TargetGasLimit uint + ProposerGasLimit uint64 FallbackBeaconNodeAddrs []string ExecutionEngineAddr string Graffiti []string @@ -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 { @@ -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 diff --git a/app/builderregistration.go b/app/builderregistration.go index 98292700f..d896c61c2 100644 --- a/app/builderregistration.go +++ b/app/builderregistration.go @@ -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) @@ -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 @@ -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) { @@ -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 } @@ -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 @@ -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)) @@ -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])) } diff --git a/app/builderregistration_internal_test.go b/app/builderregistration_internal_test.go index ad0f10162..ebaa9dbb7 100644 --- a/app/builderregistration_internal_test.go +++ b/app/builderregistration_internal_test.go @@ -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) }) @@ -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]) @@ -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]) }) @@ -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]) diff --git a/app/proposerconfig.go b/app/proposerconfig.go index ddf479b9d..8ed9365c6 100644 --- a/app/proposerconfig.go +++ b/app/proposerconfig.go @@ -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 @@ -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 { @@ -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 { @@ -149,37 +147,23 @@ 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{ @@ -187,28 +171,18 @@ func writeProposerConfigFile(conf Config, lock *cluster.Lock, nodeIdx cluster.No 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) @@ -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. @@ -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 @@ -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)) + } } diff --git a/app/proposerconfig_internal_test.go b/app/proposerconfig_internal_test.go index 2ae8c7aad..daea26791 100644 --- a/app/proposerconfig_internal_test.go +++ b/app/proposerconfig_internal_test.go @@ -8,17 +8,12 @@ import ( "math/rand" "os" "path/filepath" - "strconv" "testing" - eth2api "github.com/attestantio/go-eth2-client/api" - eth2v1 "github.com/attestantio/go-eth2-client/api/v1" - eth2p0 "github.com/attestantio/go-eth2-client/spec/phase0" "github.com/stretchr/testify/require" "github.com/obolnetwork/charon/cluster" "github.com/obolnetwork/charon/core" - "github.com/obolnetwork/charon/eth2util/registration" ) func TestWriteProposerConfigFile(t *testing.T) { @@ -32,16 +27,17 @@ func TestWriteProposerConfigFile(t *testing.T) { dir := t.TempDir() conf := Config{ - LockFile: filepath.Join(dir, "cluster-lock.json"), - BuilderAPI: true, + LockFile: filepath.Join(dir, "cluster-lock.json"), + BuilderAPI: true, + ProposerGasLimit: 200_000_000, } nodeIdx := cluster.NodeIdx{PeerIdx: peerIdx, ShareIdx: peerIdx + 1} path := filepath.Join(dir, proposerConfigDir, proposerConfigFilename) - // Override the first validator's fee recipient and gas limit (mimicking builder - // registration overrides), let the second fall back to its lock address, and - // give the remaining two identical settings so they form the majority. + // Override the first validator's fee recipient (mimicking builder registration + // overrides), let the second fall back to its lock address, and give the + // remaining two identical fee recipients so they form the majority. overridePubkey, err := core.PubKeyFromBytes(lock.Validators[0].PubKey) require.NoError(t, err) @@ -64,17 +60,7 @@ func TestWriteProposerConfigFile(t *testing.T) { } } - const overrideGasLimit = 42_000_000 - - gasLimitFunc := func(pubkey core.PubKey) uint64 { - if pubkey == overridePubkey { - return overrideGasLimit - } - - return 0 // Exercise the default fallback for the rest. - } - - created, err := writeProposerConfigFile(conf, &lock, nodeIdx, feeRecipientFunc, gasLimitFunc) + created, err := writeProposerConfigFile(conf, &lock, nodeIdx, feeRecipientFunc) require.NoError(t, err) require.True(t, created) @@ -86,7 +72,6 @@ func TestWriteProposerConfigFile(t *testing.T) { require.NoError(t, json.Unmarshal(b, &config)) require.EqualValues(t, 1, config.Version) - defaultGasLimit := strconv.FormatUint(registration.DefaultGasLimit, 10) feeRecipients := lock.FeeRecipientAddresses() pubshare := func(vi int) string { @@ -96,15 +81,15 @@ func TestWriteProposerConfigFile(t *testing.T) { return fmt.Sprintf("%#x", share) } - // The default config holds the majority settings (validators 2 and 3). + // The default config holds the majority fee recipient (validators 2 and 3) and the + // configured proposer gas limit. require.Equal(t, majorityAddr, config.DefaultConfig.FeeRecipient) - require.Equal(t, defaultGasLimit, config.DefaultConfig.GasLimit) + require.Equal(t, "200000000", config.DefaultConfig.GasLimit) require.Nil(t, config.DefaultConfig.Builder) // No builder URLs configured. - // The two diverging validators get entries carrying only the diverging fields: - // validator 0 diverges in both, validator 1 only in its fee recipient. + // The two diverging validators get entries carrying only their fee recipient. require.Len(t, config.ProposerConfig, 2) - require.Equal(t, proposerSettingsJSON{FeeRecipient: overrideAddr, GasLimit: strconv.FormatUint(overrideGasLimit, 10)}, config.ProposerConfig[pubshare(0)]) + require.Equal(t, proposerSettingsJSON{FeeRecipient: overrideAddr}, config.ProposerConfig[pubshare(0)]) require.Equal(t, proposerSettingsJSON{FeeRecipient: feeRecipients[1]}, config.ProposerConfig[pubshare(1)]) // No pre-gloas legacy fields in the charon schema. @@ -114,7 +99,7 @@ func TestWriteProposerConfigFile(t *testing.T) { // An existing file is never modified. require.NoError(t, os.WriteFile(path, []byte("operator managed"), 0o644)) - created, err = writeProposerConfigFile(conf, &lock, nodeIdx, feeRecipientFunc, gasLimitFunc) + created, err = writeProposerConfigFile(conf, &lock, nodeIdx, feeRecipientFunc) require.NoError(t, err) require.False(t, created) @@ -126,7 +111,7 @@ func TestWriteProposerConfigFile(t *testing.T) { require.NoError(t, os.Remove(path)) require.NoError(t, os.Mkdir(path, 0o755)) - _, err = writeProposerConfigFile(conf, &lock, nodeIdx, feeRecipientFunc, gasLimitFunc) + _, err = writeProposerConfigFile(conf, &lock, nodeIdx, feeRecipientFunc) require.ErrorContains(t, err, "is a directory") } @@ -135,7 +120,6 @@ func TestWriteProposerConfigFileBuilder(t *testing.T) { nodeIdx := cluster.NodeIdx{PeerIdx: 0, ShareIdx: 1} noOverrides := func(core.PubKey) string { return "" } - noGasOverrides := func(core.PubKey) uint64 { return 0 } t.Run("builder urls", func(t *testing.T) { dir := t.TempDir() @@ -146,9 +130,10 @@ func TestWriteProposerConfigFileBuilder(t *testing.T) { BuilderMinBid: 1_000_000_000, BuilderBoostFactor: 90, BuilderMaxExecutionPayment: 2_000_000_000, + ProposerGasLimit: 200_000_000, } - created, err := writeProposerConfigFile(conf, &lock, nodeIdx, noOverrides, noGasOverrides) + created, err := writeProposerConfigFile(conf, &lock, nodeIdx, noOverrides) require.NoError(t, err) require.True(t, created) @@ -162,7 +147,7 @@ func TestWriteProposerConfigFileBuilder(t *testing.T) { // A uniform cluster emits no per-validator entries, the default covers all. require.Empty(t, config.ProposerConfig) - require.Equal(t, strconv.FormatUint(registration.DefaultGasLimit, 10), config.DefaultConfig.GasLimit) + require.Equal(t, "200000000", config.DefaultConfig.GasLimit) // The builder configuration is only emitted on the default config, with the // shared values at the builder level and entries carrying no overrides. @@ -183,7 +168,7 @@ func TestWriteProposerConfigFileBuilder(t *testing.T) { BuilderMinBid: 1, // Inert without builder URLs (the flags reject this combination). } - created, err := writeProposerConfigFile(conf, &lock, nodeIdx, noOverrides, noGasOverrides) + created, err := writeProposerConfigFile(conf, &lock, nodeIdx, noOverrides) require.NoError(t, err) require.True(t, created) @@ -199,12 +184,11 @@ func TestWriteProposerConfigFileBuilder(t *testing.T) { }) } -func TestProposerConfigBuilderMatches(t *testing.T) { +func TestProposerConfigMatches(t *testing.T) { lock, _, _ := cluster.NewForT(t, 1, 4, 4, 0, rand.New(rand.NewSource(0))) nodeIdx := cluster.NodeIdx{PeerIdx: 0, ShareIdx: 1} noOverrides := func(core.PubKey) string { return "" } - noGasOverrides := func(core.PubKey) uint64 { return 0 } dir := t.TempDir() path := filepath.Join(dir, proposerConfigDir, proposerConfigFilename) @@ -215,36 +199,52 @@ func TestProposerConfigBuilderMatches(t *testing.T) { BuilderAPI: true, BuilderURLs: builderURLs, BuilderBoostFactor: 90, + ProposerGasLimit: 200_000_000, } } conf := newConf("https://builder1.example.com") - created, err := writeProposerConfigFile(conf, &lock, nodeIdx, noOverrides, noGasOverrides) + created, err := writeProposerConfigFile(conf, &lock, nodeIdx, noOverrides) require.NoError(t, err) require.True(t, created) b, err := os.ReadFile(path) require.NoError(t, err) - matches, err := proposerConfigBuilderMatches(conf, path) + builderMatches, gasMatches, err := proposerConfigMatches(conf, path) require.NoError(t, err) - require.True(t, matches) + require.True(t, builderMatches) + require.True(t, gasMatches) // Changed builder URLs, builder flags or no builder URLs differ, and leave the file as it is. changedBoost := conf changedBoost.BuilderBoostFactor = 100 for _, changed := range []Config{newConf("https://builder2.example.com"), changedBoost, newConf()} { - created, err = writeProposerConfigFile(changed, &lock, nodeIdx, noOverrides, noGasOverrides) + created, err = writeProposerConfigFile(changed, &lock, nodeIdx, noOverrides) require.NoError(t, err) require.False(t, created) - matches, err = proposerConfigBuilderMatches(changed, path) + builderMatches, gasMatches, err = proposerConfigMatches(changed, path) require.NoError(t, err) - require.False(t, matches) + require.False(t, builderMatches) + require.True(t, gasMatches) } + // A changed proposer gas limit differs, and leaves the file as it is. + changedGas := conf + changedGas.ProposerGasLimit = 60_000_000 + + created, err = writeProposerConfigFile(changedGas, &lock, nodeIdx, noOverrides) + require.NoError(t, err) + require.False(t, created) + + builderMatches, gasMatches, err = proposerConfigMatches(changedGas, path) + require.NoError(t, err) + require.True(t, builderMatches) + require.False(t, gasMatches) + unchanged, err := os.ReadFile(path) require.NoError(t, err) require.Equal(t, b, unchanged) @@ -252,27 +252,6 @@ func TestProposerConfigBuilderMatches(t *testing.T) { // A file that isn't a proposer config is an error. require.NoError(t, os.WriteFile(path, []byte("operator managed"), 0o644)) - _, err = proposerConfigBuilderMatches(conf, path) + _, _, err = proposerConfigMatches(conf, path) require.ErrorContains(t, err, "decode proposer config file") } - -func TestGasLimitsByPubkey(t *testing.T) { - lock, _, _ := cluster.NewForT(t, 1, 3, 3, 0, rand.New(rand.NewSource(0))) - - pubkey := eth2p0.BLSPubKey(lock.Validators[0].PubKey) - - corePubkey, err := core.PubKeyFromBytes(pubkey[:]) - require.NoError(t, err) - - regs := []*eth2api.VersionedSignedValidatorRegistration{ - {V1: ð2v1.SignedValidatorRegistration{Message: ð2v1.ValidatorRegistration{ - Pubkey: pubkey, - GasLimit: 50_000_000, - }}}, - nil, // Nil and zero gas limit entries are skipped. - {V1: ð2v1.SignedValidatorRegistration{Message: ð2v1.ValidatorRegistration{}}}, - } - - require.Equal(t, map[core.PubKey]uint64{corePubkey: 50_000_000}, gasLimitsByPubkey(regs)) - require.Empty(t, gasLimitsByPubkey(nil)) -} diff --git a/cmd/cmd_internal_test.go b/cmd/cmd_internal_test.go index 2a2f60a67..551825eb0 100644 --- a/cmd/cmd_internal_test.go +++ b/cmd/cmd_internal_test.go @@ -87,6 +87,7 @@ func TestCmdFlags(t *testing.T) { PublishAddress: "https://api.obol.tech/v1", PublishTimeout: 5 * time.Minute, BuilderBoostFactor: 100, + ProposerGasLimit: 200_000_000, }, }, { @@ -142,6 +143,7 @@ func TestCmdFlags(t *testing.T) { PublishAddress: "https://api.obol.tech/v1", PublishTimeout: 5 * time.Minute, BuilderBoostFactor: 100, + ProposerGasLimit: 200_000_000, TestConfig: app.TestConfig{ P2PFuzz: true, }, diff --git a/cmd/run.go b/cmd/run.go index f7f9e22d2..b632ad029 100644 --- a/cmd/run.go +++ b/cmd/run.go @@ -116,6 +116,7 @@ func bindRunFlags(cmd *cobra.Command, config *app.Config) { cmd.Flags().Uint64Var(&config.BuilderMinBid, "builder-min-bid", 0, "Minimum builder bid value in gwei, bids below it lose to the locally built payload. Applies from the gloas fork onwards. Requires builder-urls. Must be identical on all nodes in the cluster.") cmd.Flags().Uint64Var(&config.BuilderBoostFactor, "builder-boost-factor", 100, "Percentage multiplier applied to builder bid values when comparing them to the locally built payload. Applies from the gloas fork onwards. Requires builder-urls. Must be identical on all nodes in the cluster.") cmd.Flags().Uint64Var(&config.BuilderMaxExecutionPayment, "builder-max-execution-payment", 0, "Maximum execution layer payment in gwei counted when valuing a builder bid, 0 counts no execution payment (trustless bids only). Applies from the gloas fork onwards. Requires builder-urls. Must be identical on all nodes in the cluster.") + cmd.Flags().Uint64Var(&config.ProposerGasLimit, "proposer-gas-limit", 200_000_000, "Preferred target gas limit for proposed execution payloads, seeding the generated validator client proposer config. Applies from the gloas fork onwards. Must be identical on all nodes in the cluster.") wrapPreRunE(cmd, func(cc *cobra.Command, _ []string) error { if len(config.BeaconNodeAddrs) == 0 && !config.SimnetBMock { @@ -169,6 +170,10 @@ func bindRunFlags(cmd *cobra.Command, config *app.Config) { } } + if config.ProposerGasLimit == 0 { + return errors.New("proposer-gas-limit must be greater than 0") + } + if config.BuilderBoostFactor == 0 { return errors.New("builder-boost-factor must be greater than 0") } diff --git a/core/validatorapi/validatorapi.go b/core/validatorapi/validatorapi.go index 592cdad59..8b5c9a412 100644 --- a/core/validatorapi/validatorapi.go +++ b/core/validatorapi/validatorapi.go @@ -93,7 +93,7 @@ func NewComponentInsecure(_ *testing.T, eth2Cl eth2wrap.Client, shareIdx int) (* // NewComponent returns a new instance of the validator API core workflow component. func NewComponent(eth2Cl eth2wrap.Client, allPubSharesByKey map[core.PubKey]map[int]tbls.PublicKey, - shareIdx int, feeRecipientFunc func(core.PubKey) string, builderEnabled bool, targetGasLimit uint, opts ...Option, + shareIdx int, feeRecipientFunc func(core.PubKey) string, builderEnabled bool, targetGasLimit uint64, opts ...Option, ) (*Component, error) { var ( sharesByKey = make(map[eth2p0.BLSPubKey]eth2p0.BLSPubKey) @@ -199,7 +199,7 @@ type Component struct { insecureTest bool feeRecipientFunc func(core.PubKey) string builderEnabled bool - targetGasLimit uint + targetGasLimit uint64 builderHosts []string swallowRegFilter z.Field @@ -1708,11 +1708,11 @@ func (c Component) warnProposerPreferencesMismatch(ctx context.Context, pubkey c incProposerPrefMismatch("fee_recipient") } - // TargetGasLimit is only enforced for cluster-lock versions that support it (non-zero). - if c.targetGasLimit != 0 && msg.TargetGasLimit != uint64(c.targetGasLimit) { + // TargetGasLimit is only checked when configured (non-zero). + if c.targetGasLimit != 0 && msg.TargetGasLimit != c.targetGasLimit { log.Warn(ctx, "Proposer preferences with unexpected target gas limit", nil, z.Any("pubkey", pubkey), - z.U64("expected", uint64(c.targetGasLimit)), + z.U64("expected", c.targetGasLimit), z.U64("actual", msg.TargetGasLimit)) incProposerPrefMismatch("gas_limit") } diff --git a/docs/configuration.md b/docs/configuration.md index 043453b3b..359ee5bfb 100644 --- a/docs/configuration.md +++ b/docs/configuration.md @@ -204,6 +204,7 @@ Flags: --private-key-file string The path to the charon enr private key file. (default ".charon/charon-enr-private-key") --private-key-file-lock Enables private key locking to prevent multiple instances using the same key. --proc-directory string Directory to look into in order to detect other stack components running on the host. Enabling this exports the command lines of detected validator clients to the monitoring endpoint and debug logs, with the values of secret-shaped flags redacted. + --proposer-gas-limit uint Preferred target gas limit for proposed execution payloads, seeding the generated validator client proposer config. Applies from the gloas fork onwards. Must be identical on all nodes in the cluster. (default 200000000) --publish-address string The URL of the remote API for background fee recipient fetching. (default "https://api.obol.tech/v1") --publish-timeout duration Timeout for accessing the remote API. (default 5m0s) --simnet-beacon-mock Enables an internal mock beacon node for running a simnet.